[
  {
    "path": ".drone.yml",
    "content": "# This file is used by the Drone CI Server and Agents to determine what\n# should happen (if anything) in response to git pushes and pull requests.\nclone:\n  depth: 50\n  recursive: false\n  tags: false\n  # This is explicitly set so that forks retain a constant path.\n  path: /drone/src/github.com/reddit/reddit-public\n\ncompose:\n  # Some of these aren't actively used, but are required in that import\n  # side-effects cause connections to services (regardless of whether\n  # the services are interacted with in the tests). Each service is addressable\n  # via 'localhost' through whatever ports the image exposes.\n  postgres:\n    # Temporarily needed for this image + Docker-in-Docker. Expected to\n    # be removed when we upgrade to Drone 0.5.\n    # https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities\n    privileged: true\n    image: postgres:9.3.12\n    environment:\n      POSTGRES_USER: reddit\n      POSTGRES_PASSWORD: password\n      POSTGRES_DB: reddit\n\n  cassandra:\n    privileged: true\n    # Cassandra 1.x is old enough not to have any official Docker images.\n    # In the meantime, we've got a custom image configured specifically for r2:\n    # https://github.com/reddit/docker-cassandra\n    # We'll move over to the official images when we upgrade to C* 2.x.\n    image: reddit/cassandra:single-1.2.19-v1\n\n  rabbitmq:\n    # NOTE: Using 3.4.x instead of 3.2.4 due to tag availability.\n    image: rabbitmq:3.4\n    environment:\n      RABBITMQ_DEFAULT_VHOST: /\n      RABBITMQ_DEFAULT_USER: reddit\n      RABBITMQ_DEFAULT_PASS: reddit\n\n  memcached:\n    # NOTE: Using 1.4.21 instead of 1.4.17 due to tag availability.\n    image: memcached:1.4.21\n\n  zookeeper:\n    image: jplock/zookeeper:3.4.6\n\n# Build steps are where the setup, compilation, and tests happen.\nbuild:\n  # This is a fat Docker image with the apt dependencies pre-installed.\n  # https://github.com/reddit/docker-reddit-py\n  # Dependency changes in the install scripts can be optionally mirrored to the\n  # image for speedups, but abstaining from doing so won't break the builds.\n  image: reddit/reddit-py:latest\n  # Always re-pull the image, since we're re-using the same tag.\n  pull: true\n  environment:\n    DEBIAN_FRONTEND: noninteractive\n  commands:\n    # Prepares the environment for the test run.\n    - install/drone.sh\n    - cd r2\n    - nosetests -v .\n    - cd ..\n    - ./scripts/stylecheck_git_diff.sh\n\n# These plugins are triggered after a build failure/success.\nnotify:\n  slack:\n    webhook_url: $$CI_SLACK_WEBHOOK_URL\n    channel: ci-notifications\n"
  },
  {
    "path": ".gitignore",
    "content": "*~\n.*.sw?\n*.pyc\n*.pyo\n.DS_Store\n\n# mako\nr2/data/\n*.html.py\n\n# build outputs\n*.so\nbuild/\ndist/\nr2/r2.egg-info/\nr2/r2/lib/generated_strings.py\n\n# ini files\n*.ini\n*.update\n\n# static files\nr2/r2/public/static/names.json\nr2/r2/public/static/*.js\nr2/r2/public/static/*.css\nr2/r2/public/static/sprite*.png\nr2/r2/public/static/*.gzip\nr2/r2/public/static/js/lib/*.gzip\n\n# cython\nr2/r2/lib/mr_tools/_mr_tools.c\nr2/r2/lib/db/_sorts.c\nr2/r2/lib/sgm.c\nr2/r2/lib/utils/_utils.c\nr2/r2/lib/wrapped.c\nr2/r2/lib/utils/comment_tree_utils.c\n\n# vagrant\n.vagrant/\n"
  },
  {
    "path": ".travis.yml",
    "content": "sudo: required\ndist: trusty\n\nlanguage: python\n\npython:\n    - \"2.7\"\n\nvirtualenv:\n    system_site_packages: true\n\nservices:\n    - postgres\n    - memcached\n    - rabbitmq\n\ninstall:\n    - sudo install/travis.sh travis .\n\nbefore_script:\n    - install/setup_rabbitmq.sh\n    - install/setup_postgres.sh\n    - install/setup_cassandra.sh\n\nscript:\n    - cd r2 && nosetests\n"
  },
  {
    "path": "LICENSE",
    "content": "reddit Inc.\n\nCommon Public Attribution License Version 1.0 (CPAL)\n\n1. \"Definitions\"\n\n1.0.1 \"Commercial Use\" means distribution or otherwise making the Covered Code\n    available to a third party.\n\n1.1 \"Contributor\" means each entity that creates or contributes to the creation\n  of Modifications.\n\n1.2 \"Contributor Version\" means the combination of the Original Code, prior\n  Modifications used by a Contributor, and the Modifications made by that\n  particular Contributor.\n\n1.3 \"Covered Code\" means the Original Code or Modifications or the combination\n  of the Original Code and Modifications, in each case including portions\n  thereof.\n\n1.4 \"Electronic Distribution Mechanism\" means a mechanism generally accepted in\n  the software development community for the electronic transfer of data.\n\n1.5 \"Executable\" means Covered Code in any form other than Source Code.\n\n1.6 \"Initial Developer\" means the individual or entity identified as the Initial\n  Developer in the Source Code notice required by Exhibit A.\n\n1.7 \"Larger Work\" means a work which combines Covered Code or portions thereof\n  with code not governed by the terms of this License.\n\n1.8 \"License\" means this document.\n\n1.8.1 \"Licensable\" means having the right to grant, to the maximum extent\n    possible, whether at the time of the initial grant or subsequently acquired,\n    any and all of the rights conveyed herein.\n\n1.9 \"Modifications\" means any addition to or deletion from the substance or\n  structure of either the Original Code or any previous Modifications. When\n  Covered Code is released as a series of files, a Modification is:\n\nA. Any addition to or deletion from the contents of a file containing Original\nCode or previous Modifications.\n\nB. Any new file that contains any part of the Original Code or previous\nModifications.\n\n1.10 \"Original Code\" means Source Code of computer software code which is\n  described in the Source Code notice required by Exhibit A as Original Code,\n  and which, at the time of its release under this License is not already\n  Covered Code governed by this License.\n\n1.10.1 \"Patent Claims\" means any patent claim(s), now owned or hereafter\n     acquired, including without limitation, method, process, and apparatus\n     claims, in any patent Licensable by grantor.\n\n1.11 \"Source Code\" means the preferred form of the Covered Code for making\n  modifications to it, including all modules it contains, plus any associated\n  interface definition files, scripts used to control compilation and\n  installation of an Executable, or source code differential comparisons against\n  either the Original Code or another well known, available Covered Code of the\n  Contributor's choice. The Source Code can be in a compressed or archival form,\n  provided the appropriate decompression or de-archiving software is widely\n  available for no charge.\n\n1.12 \"You\" (or \"Your\") means an individual or a legal entity exercising rights\n  under, and complying with all of the terms of, this License or a future\n  version of this License issued under Section 6.1. For legal entities, \"You\"\n  includes any entity which controls, is controlled by, or is under common\n  control with You. For purposes of this definition, \"control\" means (a) the\n  power, direct or indirect, to cause the direction or management of such\n  entity, whether by contract or otherwise, or (b) ownership of more than fifty\n  percent (50%) of the outstanding shares or beneficial ownership of such\n  entity.\n\n2. Source Code License.\n\n2.1 The Initial Developer Grant.\n\nThe Initial Developer hereby grants You a world-wide, royalty-free,\nnon-exclusive license, subject to third party intellectual property claims:\n\n(a) under intellectual property rights (other than patent or trademark)\nLicensable by Initial Developer to use, reproduce, modify, display, perform,\nsublicense and distribute the Original Code (or portions thereof) with or\nwithout Modifications, and/or as part of a Larger Work; and\n\n(b) under Patents Claims infringed by the making, using or selling of Original\nCode, to make, have made, use, practice, sell, and offer for sale, and/or\notherwise dispose of the Original Code (or portions thereof).\n\n(c) the licenses granted in this Section 2.1(a) and (b) are effective on the\ndate Initial Developer first distributes Original Code under the terms of this\nLicense.\n\n(d) Notwithstanding Section 2.1(b) above, no patent license is granted: 1) for\ncode that You delete from the Original Code; 2) separate from the Original Code;\nor 3) for infringements caused by: i) the modification of the Original Code or\nii) the combination of the Original Code with other software or devices.\n\n2.2 Contributor Grant.\n\nSubject to third party intellectual property claims, each Contributor hereby\ngrants You a world-wide, royalty-free, non-exclusive license\n\n(a) under intellectual property rights (other than patent or trademark)\nLicensable by Contributor, to use, reproduce, modify, display, perform,\nsublicense and distribute the Modifications created by such Contributor (or\nportions thereof) either on an unmodified basis, with other Modifications, as\nCovered Code and/or as part of a Larger Work; and\n\n(b) under Patent Claims infringed by the making, using, or selling of\nModifications made by that Contributor either alone and/or in combination with\nits Contributor Version (or portions of such combination), to make, use, sell,\noffer for sale, have made, and/or otherwise dispose of: 1) Modifications made by\nthat Contributor (or portions thereof); and 2) the combination of Modifications\nmade by that Contributor with its Contributor Version (or portions of such\ncombination).\n\n(c) the licenses granted in Sections 2.2(a) and 2.2(b) are effective on the date\nContributor first makes Commercial Use of the Covered Code.\n\n(d) Notwithstanding Section 2.2(b) above, no patent license is granted: 1) for\nany code that Contributor has deleted from the Contributor Version; 2) separate\nfrom the Contributor Version; 3) for infringements caused by: i) third party\nmodifications of Contributor Version or ii) the combination of Modifications\nmade by that Contributor with other software (except as part of the Contributor\nVersion) or other devices; or 4) under Patent Claims infringed by Covered Code\nin the absence of Modifications made by that Contributor.\n\n3. Distribution Obligations.\n\n3.1 Application of License.\n\nThe Modifications which You create or to which You contribute are governed by\nthe terms of this License, including without limitation Section 2.2. The Source\nCode version of Covered Code may be distributed only under the terms of this\nLicense or a future version of this License released under Section 6.1, and You\nmust include a copy of this License with every copy of the Source Code You\ndistribute. You may not offer or impose any terms on any Source Code version\nthat alters or restricts the applicable version of this License or the\nrecipients' rights hereunder. However, You may include an additional document\noffering the additional rights described in Section 3.5.\n\n3.2 Availability of Source Code.\n\nAny Modification which You create or to which You contribute must be made\navailable in Source Code form under the terms of this License either on the same\nmedia as an Executable version or via an accepted Electronic Distribution\nMechanism to anyone to whom you made an Executable version available; and if\nmade available via Electronic Distribution Mechanism, must remain available for\nat least twelve (12) months after the date it initially became available, or at\nleast six (6) months after a subsequent version of that particular Modification\nhas been made available to such recipients. You are responsible for ensuring\nthat the Source Code version remains available even if the Electronic\nDistribution Mechanism is maintained by a third party.\n\n3.3 Description of Modifications.\n\nYou must cause all Covered Code to which You contribute to contain a file\ndocumenting the changes You made to create that Covered Code and the date of any\nchange. You must include a prominent statement that the Modification is derived,\ndirectly or indirectly, from Original Code provided by the Initial Developer and\nincluding the name of the Initial Developer in (a) the Source Code, and (b) in\nany notice in an Executable version or related documentation in which You\ndescribe the origin or ownership of the Covered Code.\n\n3.4 Intellectual Property Matters\n\n(a) Third Party Claims.\n\nIf Contributor has knowledge that a license under a third party's intellectual\nproperty rights is required to exercise the rights granted by such Contributor\nunder Sections 2.1 or 2.2, Contributor must include a text file with the Source\nCode distribution titled \"LEGAL\" which describes the claim and the party making\nthe claim in sufficient detail that a recipient will know whom to contact. If\nContributor obtains such knowledge after the Modification is made available as\ndescribed in Section 3.2, Contributor shall promptly modify the LEGAL file in\nall copies Contributor makes available thereafter and shall take other steps\n(such as notifying appropriate mailing lists or newsgroups) reasonably\ncalculated to inform those who received the Covered Code that new knowledge has\nbeen obtained.\n\n(b) Contributor APIs.\n\nIf Contributor's Modifications include an application programming interface and\nContributor has knowledge of patent licenses which are reasonably necessary to\nimplement that API, Contributor must also include this information in the LEGAL\nfile.\n\n(c) Representations.\n\nContributor represents that, except as disclosed pursuant to Section 3.4(a)\nabove, Contributor believes that Contributor's Modifications are Contributor's\noriginal creation(s) and/or Contributor has sufficient rights to grant the\nrights conveyed by this License.\n\n3.5 Required Notices.\n\nYou must duplicate the notice in Exhibit A in each file of the Source Code. If\nit is not possible to put such notice in a particular Source Code file due to\nits structure, then You must include such notice in a location (such as a\nrelevant directory) where a user would be likely to look for such a notice. If\nYou created one or more Modification(s) You may add your name as a Contributor\nto the notice described in Exhibit A. You must also duplicate this License in\nany documentation for the Source Code where You describe recipients' rights or\nownership rights relating to Covered Code. You may choose to offer, and to\ncharge a fee for, warranty, support, indemnity or liability obligations to one\nor more recipients of Covered Code. However, You may do so only on Your own\nbehalf, and not on behalf of the Initial Developer or any Contributor. You must\nmake it absolutely clear than any such warranty, support, indemnity or liability\nobligation is offered by You alone, and You hereby agree to indemnify the\nInitial Developer and every Contributor for any liability incurred by the\nInitial Developer or such Contributor as a result of warranty, support,\nindemnity or liability terms You offer.\n\n3.6 Distribution of Executable Versions.\n\nYou may distribute Covered Code in Executable form only if the requirements of\nSection 3.1-3.5 have been met for that Covered Code, and if You include a notice\nstating that the Source Code version of the Covered Code is available under the\nterms of this License, including a description of how and where You have\nfulfilled the obligations of Section 3.2. The notice must be conspicuously\nincluded in any notice in an Executable version, related documentation or\ncollateral in which You describe recipients' rights relating to the Covered\nCode. You may distribute the Executable version of Covered Code or ownership\nrights under a license of Your choice, which may contain terms different from\nthis License, provided that You are in compliance with the terms of this License\nand that the license for the Executable version does not attempt to limit or\nalter the recipient's rights in the Source Code version from the rights set\nforth in this License. If You distribute the Executable version under a\ndifferent license You must make it absolutely clear that any terms which differ\nfrom this License are offered by You alone, not by the Initial Developer,\nOriginal Developer or any Contributor. You hereby agree to indemnify the Initial\nDeveloper, Original Developer and every Contributor for any liability incurred\nby the Initial Developer, Original Developer or such Contributor as a result of\nany such terms You offer.\n\n3.7 Larger Works.\n\nYou may create a Larger Work by combining Covered Code with other code not\ngoverned by the terms of this License and distribute the Larger Work as a single\nproduct. In such a case, You must make sure the requirements of this License are\nfulfilled for the Covered Code.\n\n4. Inability to Comply Due to Statute or Regulation.\n\nIf it is impossible for You to comply with any of the terms of this License with\nrespect to some or all of the Covered Code due to statute, judicial order, or\nregulation then You must: (a) comply with the terms of this License to the\nmaximum extent possible; and (b) describe the limitations and the code they\naffect. Such description must be included in the LEGAL file described in Section\n3.4 and must be included with all distributions of the Source Code. Except to\nthe extent prohibited by statute or regulation, such description must be\nsufficiently detailed for a recipient of ordinary skill to be able to understand\nit.\n\n5. Application of this License.\n\nThis License applies to code to which the Initial Developer has attached the\nnotice in Exhibit A and to related Covered Code.\n\n6. Versions of the License.\n\n6.1 New Versions.\n\nreddit Inc. (\"reddit\") may publish revised and/or new versions of the\nLicense from time to time. Each version will be given a distinguishing version\nnumber.\n\n6.2 Effect of New Versions.\n\nOnce Covered Code has been published under a particular version of the License,\nYou may always continue to use it under the terms of that version. You may also\nchoose to use such Covered Code under the terms of any subsequent version of the\nLicense published by reddit. No one other than reddit has the right to\nmodify the terms applicable to Covered Code created under this License.\n\n6.3 Derivative Works.\n\nIf You create or use a modified version of this License (which you may only do\nin order to apply it to code which is not already Covered Code governed by this\nLicense), You must (a) rename Your license so that the phrases \"reddit\",\n\"CPAL\" or any confusingly similar phrase do not appear in your license (except\nto note that your license differs from this License) and (b) otherwise make it\nclear that Your version of the license contains terms which differ from the\nCPAL. (Filling in the name of the Initial Developer, Original Developer,\nOriginal Code or Contributor in the notice described in Exhibit A shall not of\nthemselves be deemed to be modifications of this License.)\n\n7. DISCLAIMER OF WARRANTY.\n\nCOVERED CODE IS PROVIDED UNDER THIS LICENSE ON AN \"AS IS\" BASIS, WITHOUT\nWARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, WITHOUT\nLIMITATION, WARRANTIES THAT THE COVERED CODE IS FREE OF DEFECTS, MERCHANTABLE,\nFIT FOR A PARTICULAR PURPOSE OR NON-INFRINGING. THE ENTIRE RISK AS TO THE\nQUALITY AND PERFORMANCE OF THE COVERED CODE IS WITH YOU. SHOULD ANY COVERED CODE\nPROVE DEFECTIVE IN ANY RESPECT, YOU (NOT THE INITIAL DEVELOPER, ORIGINAL\nDEVELOPER OR ANY OTHER CONTRIBUTOR) ASSUME THE COST OF ANY NECESSARY SERVICING,\nREPAIR OR CORRECTION. THIS DISCLAIMER OF WARRANTY CONSTITUTES AN ESSENTIAL PART\nOF THIS LICENSE. NO USE OF ANY COVERED CODE IS AUTHORIZED HEREUNDER EXCEPT UNDER\nTHIS DISCLAIMER.\n\n8. TERMINATION.\n\n8.1 This License and the rights granted hereunder will terminate automatically\n  if You fail to comply with terms herein and fail to cure such breach within 30\n  days of becoming aware of the breach. All sublicenses to the Covered Code\n  which are properly granted shall survive any termination of this\n  License. Provisions which, by their nature, must remain in effect beyond the\n  termination of this License shall survive.\n\n8.2 If You initiate litigation by asserting a patent infringement claim\n  (excluding declatory judgment actions) against Initial Developer, Original\n  Developer or a Contributor (the Initial Developer, Original Developer or\n  Contributor against whom You file such action is referred to as \"Participant\")\n  alleging that:\n\n(a) such Participant's Contributor Version directly or indirectly infringes any\npatent, then any and all rights granted by such Participant to You under\nSections 2.1 and/or 2.2 of this License shall, upon 60 days notice from\nParticipant terminate prospectively, unless if within 60 days after receipt of\nnotice You either: (i) agree in writing to pay Participant a mutually agreeable\nreasonable royalty for Your past and future use of Modifications made by such\nParticipant, or (ii) withdraw Your litigation claim with respect to the\nContributor Version against such Participant. If within 60 days of notice, a\nreasonable royalty and payment arrangement are not mutually agreed upon in\nwriting by the parties or the litigation claim is not withdrawn, the rights\ngranted by Participant to You under Sections 2.1 and/or 2.2 automatically\nterminate at the expiration of the 60 day notice period specified above.\n\n(b) any software, hardware, or device, other than such Participant's Contributor\nVersion, directly or indirectly infringes any patent, then any rights granted to\nYou by such Participant under Sections 2.1(b) and 2.2(b) are revoked effective\nas of the date You first made, used, sold, distributed, or had made,\nModifications made by that Participant.\n\n8.3 If You assert a patent infringement claim against Participant alleging that\n  such Participant's Contributor Version directly or indirectly infringes any\n  patent where such claim is resolved (such as by license or settlement) prior\n  to the initiation of patent infringement litigation, then the reasonable value\n  of the licenses granted by such Participant under Sections 2.1 or 2.2 shall be\n  taken into account in determining the amount or value of any payment or\n  license.\n\n8.4 In the event of termination under Sections 8.1 or 8.2 above, all end user\n  license agreements (excluding distributors and resellers) which have been\n  validly granted by You or any distributor hereunder prior to termination shall\n  survive termination.\n\n9. LIMITATION OF LIABILITY.\n\nUNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT (INCLUDING\nNEGLIGENCE), CONTRACT, OR OTHERWISE, SHALL YOU, THE INITIAL DEVELOPER, ORIGINAL\nDEVELOPER, ANY OTHER CONTRIBUTOR, OR ANY DISTRIBUTOR OF COVERED CODE, OR ANY\nSUPPLIER OF ANY OF SUCH PARTIES, BE LIABLE TO ANY PERSON FOR ANY INDIRECT,\nSPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY CHARACTER INCLUDING,\nWITHOUT LIMITATION, DAMAGES FOR LOSS OF GOODWILL, WORK STOPPAGE, COMPUTER\nFAILURE OR MALFUNCTION, OR ANY AND ALL OTHER COMMERCIAL DAMAGES OR LOSSES, EVEN\nIF SUCH PARTY SHALL HAVE BEEN INFORMED OF THE POSSIBILITY OF SUCH DAMAGES. THIS\nLIMITATION OF LIABILITY SHALL NOT APPLY TO LIABILITY FOR DEATH OR PERSONAL\nINJURY RESULTING FROM SUCH PARTY'S NEGLIGENCE TO THE EXTENT APPLICABLE LAW\nPROHIBITS SUCH LIMITATION. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OR\nLIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO THIS EXCLUSION AND\nLIMITATION MAY NOT APPLY TO YOU.\n\n10. U.S. GOVERNMENT END USERS.\n\nThe Covered Code is a \"commercial item,\" as that term is defined in 48\nC.F.R. 2.101 (Oct. 1995), consisting of \"commercial computer software\" and\n\"commercial computer software documentation,\" as such terms are used in 48\nC.F.R. 12.212 (Sept. 1995). Consistent with 48 C.F.R. 12.212 and 48\nC.F.R. 227.7202-1 through 227.7202-4 (June 1995), all U.S. Government End Users\nacquire Covered Code with only those rights set forth herein.\n\n11. MISCELLANEOUS.\n\nThis License represents the complete agreement concerning subject matter\nhereof. If any provision of this License is held to be unenforceable, such\nprovision shall be reformed only to the extent necessary to make it\nenforceable. This License shall be governed by California law provisions (except\nto the extent applicable law, if any, provides otherwise), excluding its\nconflict-of-law provisions. With respect to disputes in which at least one party\nis a citizen of, or an entity chartered or registered to do business in the\nUnited States of America, any litigation relating to this License shall be\nsubject to the jurisdiction of the Federal Courts of the Northern District of\nCalifornia, with venue lying in Santa Clara County, California, with the losing\nparty responsible for costs, including without limitation, court costs and\nreasonable attorneys' fees and expenses. The application of the United Nations\nConvention on Contracts for the International Sale of Goods is expressly\nexcluded. Any law or regulation which provides that the language of a contract\nshall be construed against the drafter shall not apply to this License.\n\n12. RESPONSIBILITY FOR CLAIMS.\n\nAs between Initial Developer, Original Developer and the Contributors, each\nparty is responsible for claims and damages arising, directly or indirectly, out\nof its utilization of rights under this License and You agree to work with\nInitial Developer, Original Developer and Contributors to distribute such\nresponsibility on an equitable basis. Nothing herein is intended or shall be\ndeemed to constitute any admission of liability.\n\n13. MULTIPLE-LICENSED CODE.\n\nInitial Developer may designate portions of the Covered Code as\nMultiple-Licensed. Multiple-Licensed means that the Initial Developer permits\nyou to utilize portions of the Covered Code under Your choice of the CPAL or the\nalternative licenses, if any, specified by the Initial Developer in the file\ndescribed in Exhibit A.\n\n14. ADDITIONAL TERM: ATTRIBUTION\n\n(a) As a modest attribution to the organizer of the development of the Original\nCode (\"Original Developer\"), in the hope that its promotional value may help\njustify the time, money and effort invested in writing the Original Code, the\nOriginal Developer may include in Exhibit B (\"Attribution Information\") a\nrequirement that each time an Executable and Source Code or a Larger Work is\nlaunched or initially run (which includes initiating a session), a prominent\ndisplay of the Original Developer's Attribution Information (as defined below)\nmust occur on the graphic user interface employed by the end user to access such\nCovered Code (which may include display on a splash screen), if any. The size of\nthe graphic image should be consistent with the size of the other elements of\nthe Attribution Information. If the access by the end user to the Executable and\nSource Code does not create a graphic user interface for access to the Covered\nCode, this obligation shall not apply. If the Original Code displays such\nAttribution Information in a particular form (such as in the form of a splash\nscreen, notice at login, an \"about\" display, or dedicated attribution area on\nuser interface screens), continued use of such form for that Attribution\nInformation is one way of meeting this requirement for notice.\n\n(b) Attribution information may only include a copyright notice, a brief phrase,\ngraphic image and a URL (\"Attribution Information\") and is subject to the\nAttribution Limits as defined below. For these purposes, prominent shall mean\ndisplay for sufficient duration to give reasonable notice to the user of the\nidentity of the Original Developer and that if You include Attribution\nInformation or similar information for other parties, You must ensure that the\nAttribution Information for the Original Developer shall be no less prominent\nthan such Attribution Information or similar information for the other\nparty. For greater certainty, the Original Developer may choose to specify in\nExhibit B below that the above attribution requirement only applies to an\nExecutable and Source Code resulting from the Original Code or any Modification,\nbut not a Larger Work. The intent is to provide for reasonably modest\nattribution, therefore the Original Developer cannot require that You display,\nat any time, more than the following information as Attribution Information: (a)\na copyright notice including the name of the Original Developer; (b) a word or\none phrase (not exceeding 10 words); (c) one graphic image provided by the\nOriginal Developer; and (d) a URL (collectively, the \"Attribution Limits\").\n\n(c) If Exhibit B does not include any Attribution Information, then there are no\nrequirements for You to display any Attribution Information of the Original\nDeveloper.\n\n(d) You acknowledge that all trademarks, service marks and/or trade names\ncontained within the Attribution Information distributed with the Covered Code\nare the exclusive property of their owners and may only be used with the\npermission of their owners, or under circumstances otherwise permitted by law or\nas expressly set out in this License.\n\n15. ADDITIONAL TERM: NETWORK USE.\n\nThe term \"External Deployment\" means the use, distribution, or communication of\nthe Original Code or Modifications in any way such that the Original Code or\nModifications may be used by anyone other than You, whether those works are\ndistributed or communicated to those persons or made available as an application\nintended for use over a network. As an express condition for the grants of\nlicense hereunder, You must treat any External Deployment by You of the Original\nCode or Modifications as a distribution under section 3.1 and make Source Code\navailable under Section 3.2.\n\nEXHIBIT A. Common Public Attribution License Version 1.0.\n\n\"The contents of this file are subject to the Common Public Attribution License\nVersion 1.0. (the \"License\"); you may not use this file except in compliance\nwith the License. You may obtain a copy of the License at\nhttp://code.reddit.com/LICENSE. The License is based on the Mozilla Public\nLicense Version 1.1, but Sections 14 and 15 have been added to cover use of\nsoftware over a computer network and provide for limited attribution for the\nOriginal Developer. In addition, Exhibit A has been modified to be consistent\nwith Exhibit B.\n\nSoftware distributed under the License is distributed on an \"AS IS\" basis,\nWITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the\nspecific language governing rights and limitations under the License.\n\nThe Original Code is reddit.\n\nThe Original Developer is the Initial Developer.  The Initial Developer of the\nOriginal Code is reddit Inc.\n\nAll portions of the code written by reddit are Copyright (c) 2006-2015\nreddit Inc. All Rights Reserved.\n\nEXHIBIT B. Attribution Information\n\nAttribution Copyright Notice: Copyright (c) 2006-2015 reddit Inc. All Rights\nReserved.\n\nAttribution Phrase (not exceeding 10 words): Powered by reddit\n\nAttribution URL: http://code.reddit.com\n\nGraphic Image as provided in the Covered Code:\nhttp://code.reddit.com/reddit_logo.png\n\nDisplay of Attribution Information is required in Larger Works which are defined\nin the CPAL as a work which combines Covered Code or portions thereof with code\nnot governed by the terms of the CPAL.\n"
  },
  {
    "path": "README.md",
    "content": "## This repository is archived.\n\nThis repository is archived and will not receive any updates or accept issues or pull requests.\n\nTo report bugs in reddit.com please make a post in [/r/bugs](http://www.reddit.com/r/bugs).\n\nIf you have found a bug that can in some way compromise the security of the\nsite or its users, please exercise [responsible\ndisclosure](http://www.reddit.com/wiki/whitehat) and e-mail\nsecurity@reddit.com.\n\n---\n\n### API\n\nFor notices about reddit API changes and discussion of reddit API client development, subscribe to the [/r/redditdev](http://www.reddit.com/r/redditdev) and [/r/changelog](http://www.reddit.com/r/changelog) subreddits.\n\nTo learn more about reddit's API, check out our [automated API documentation](http://www.reddit.com/dev/api) and the [API wiki page](https://github.com/reddit/reddit/wiki/API). Please use a unique User-Agent string and take care to abide by our [API rules](https://github.com/reddit/reddit/wiki/API#wiki-rules).\n\n### Quickstart\n\nTo set up your own instance of reddit see the [install guide](https://github.com/reddit/reddit/wiki/Install-guide).\n"
  },
  {
    "path": "SECURITY.md",
    "content": "![white hat trophy](https://b.thumbs.redditmedia.com/n0_7BYpCg_RYB1j7.png)\n\nLike all pieces of software, reddit has bugs &ndash; and it always will. Some\nof them will take the form of security vulnerabilities.\n\nIf you find a security vulnerability in reddit, please privately report it to\n[security@reddit.com](mailto:security@reddit.com). We'll get back to you ASAP,\nusually within 24 hours.\n\nOnce the issue is fixed, if you provide your reddit username, we'll credit your\naccount with a [whitehat](https://www.reddit.com/wiki/whitehat) trophy.\n\nThank you and good hunting.\n"
  },
  {
    "path": "Vagrantfile",
    "content": "# -*- mode: ruby -*-\n# vi: set ft=ruby :\n\n# This assumes that the host machine has r2 and all the reddit plugins checked\n# out and in the correct directories--pay attention to both name and position\n# relative to the r2 code:\n#\n# r2:         {ROOTDIR}/reddit\n#\n# plugins:\n# about:      {ROOTDIR}/about\n# gold:       {ROOTDIR}/gold\n#\n# All plugins are optional. A plugin will only be installed if it is listed\n# in `plugins` AND it is located in a directory that both follows the plugin\n# naming convention and is correctly located on the host machine. The general\n# rule for naming each plugin directory is that \"reddit-plugin-NAME\" should be\n# in the directory {ROOTDIR}/NAME.\n#\n# This VagrantFile allows for the creation of two VMs:\n#   * default: the primary VM, with all services necessary to run reddit\n#              locally against the local codebase.\n#   * travis:  Testing-only VM suitable for running `nosetests` and debugging\n#              issues encountered without having to wait for travis-ci to pick\n#              up the build.  This will *not* be the same environment as\n#              travis, but it should be useful for repairing broken tests.\n#\n# To start your vagrant box simply enter `vagrant up` from {ROOTDIR}/reddit.\n# You can then ssh into it with `vagrant ssh`.\n#\n# avahi-daemon is installed on the guest VM so you can access your local install\n# at https://reddit.local. If that fails you'll need to update your host\n# machine's hosts file (/etc/hosts) to include the line:\n# 192.168.56.111 reddit.local\n#\n# If you want to create additional vagrant boxes you can copy this file\n# elsewhere, but be sure to update `code_share_host_path` to be the absolute\n# path to {ROOTDIR}.\n\nvagrant_user = \"vagrant\"\n\n# code directories\nthis_path = File.absolute_path(__FILE__)\nreddit_dir = File.expand_path(\"..\", this_path)\ncode_share_host_path = File.expand_path(\"..\", reddit_dir)\ncode_share_guest_path = \"/media/reddit_code\"\nplugins = [\n  \"about\",\n  \"gold\",\n]\n\n# overlayfs directories\noverlay_mount = \"/home/#{vagrant_user}/src\"\noverlay_lower = code_share_guest_path\noverlay_upper = \"/home/#{vagrant_user}/.overlay\"\n\n# \"default\" vm config\nguest_ip = \"192.168.56.111\"\nguest_mem = \"4096\"\nguest_swap = \"4096\"\nhostname = \"reddit.local\"\n\n\nVagrant.configure(2) do |config|\n  config.vm.box = \"trusty-cloud-image\"\n  config.vm.box_url = \"https://cloud-images.ubuntu.com/vagrant/trusty/current/trusty-server-cloudimg-amd64-vagrant-disk1.box\"\n\n  # mount the host shared folder\n  config.vm.synced_folder code_share_host_path, code_share_guest_path, mount_options: [\"ro\"]\n\n  config.vm.provider \"virtualbox\" do |vb|\n    vb.memory = guest_mem\n  end\n\n  # ubuntu cloud image has no swapfile by default, set one up\n  config.vm.provision \"shell\", inline: <<-SCRIPT\n    if ! grep -q swapfile /etc/fstab; then\n      echo 'swapfile not found. Adding swapfile.'\n      fallocate -l #{guest_swap}M /swapfile\n      chmod 600 /swapfile\n      mkswap /swapfile\n      swapon /swapfile\n      echo '/swapfile none swap defaults 0 0' >> /etc/fstab\n    else\n      echo 'swapfile found. No changes made.'\n    fi\n  SCRIPT\n\n  # set up the overlay filesystem\n  config.vm.provision \"shell\", inline: <<-SCRIPT\n    if [ ! -d #{overlay_mount} ]; then\n      echo \"creating overlay mount directory #{overlay_mount}\"\n      sudo -u #{vagrant_user} mkdir #{overlay_mount}\n    fi\n\n    if [ ! -d #{overlay_upper} ]; then\n      echo \"creating overlay upper directory #{overlay_upper}\"\n      sudo -u #{vagrant_user} mkdir #{overlay_upper}\n    fi\n\n    echo \"mounting overlayfs (lower: #{overlay_lower}, upper: #{overlay_upper}, mount: #{overlay_mount})\"\n    mount -t overlayfs overlayfs -o lowerdir=#{overlay_lower},upperdir=#{overlay_upper} #{overlay_mount}\n  SCRIPT\n\n  # NOTE: This VM exists solely to assist in writing tests.  It does not actually\n  # install travis but rather builds a minimal vm with only the services\n  # available under a travis build to aid in test debugging (via `nosetests`)\n  # To use:\n  #     $ vagrant up travis\n  #     $ vagrant ssh travis\n  #     vagrant@travis$ cd src/reddit/r2 && nosetests\n  config.vm.define \"travis\", autostart: false do |travis|\n      travis.vm.hostname = \"travis\"\n      # run install script\n      travis.vm.provision \"shell\", inline: <<-SCRIPT\n        if [ ! -f /var/local/reddit_installed ]; then\n          echo \"running install script\"\n          cd /home/#{vagrant_user}/src/reddit\n          ./install/travis.sh vagrant\n          touch /var/local/reddit_installed\n        else\n          echo \"install script already run\"\n        fi\n      SCRIPT\n  end\n\n  # NB: this is the primary VM. To build run\n  #    $ vagrant up\n  # [though 'vagrant up default' will also work, the 'default' is redudnant]\n  # Once built, avahi-daemon should guarantee the instance will be accessible\n  # from https://reddit.local/\n  config.vm.define \"default\", primary: true do |redditlocal|\n      redditlocal.vm.hostname = hostname\n      # host-only network interface\n      redditlocal.vm.network \"private_network\", ip: guest_ip\n\n      # rabbitmq web interface\n      config.vm.network \"forwarded_port\", guest: 15672, host: 15672\n\n      # run install script\n      plugin_string = plugins.join(\" \")\n      redditlocal.vm.provision \"shell\", inline: <<-SCRIPT\n        if [ ! -f /var/local/reddit_installed ]; then\n          echo \"running install script\"\n          cd /home/#{vagrant_user}/src/reddit\n          REDDIT_PLUGINS=\"#{plugin_string}\" REDDIT_DOMAIN=\"#{hostname}\" ./install/reddit.sh\n          touch /var/local/reddit_installed\n        else\n          echo \"install script already run\"\n        fi\n      SCRIPT\n\n      # inject test data\n      redditlocal.vm.provision \"shell\", inline: <<-SCRIPT\n        if [ ! -f /var/local/test_data_injected ]; then\n          cd /home/#{vagrant_user}/src/reddit\n          sudo -u #{vagrant_user} reddit-run scripts/inject_test_data.py -c 'inject_test_data()'\n          touch /var/local/test_data_injected\n        else\n          echo \"inject test data already run\"\n        fi\n\n        # HACK: stop and start everything (otherwise sometimes there's an issue with\n        # ports being in use?)\n        reddit-stop\n        reddit-start\n      SCRIPT\n\n      # additional setup\n      redditlocal.vm.provision \"shell\", inline: <<-SCRIPT\n        if [ ! -f /var/local/additional_setup ]; then\n          apt-get install -y ipython avahi-daemon\n          touch /var/local/additional_setup\n        else\n          echo \"additional setup already run\"\n        fi\n      SCRIPT\n\n      # DONE: let this run whenever provision is run so that the user can see\n      # how to proceed.\n      redditlocal.vm.provision \"shell\", inline: <<-SCRIPT\n        cd /home/#{vagrant_user}/src/reddit\n        REDDIT_DOMAIN=\"#{hostname}\" ./install/done.sh\n      SCRIPT\n  end\nend\n"
  },
  {
    "path": "install/README.md",
    "content": "# Reddit installer development instructions\n\nThis folder contains all of the installation scripts required to build reddit on a stock Ubuntu 14.04 (trusty) box.  Originally all of this was included in `../install-reddit.sh` but the need to fork the image into an base installer for testing as well as a full installer for local use meant some reorganization and fracturing of the original script.\n\nWhen making updates to any of these files, Travis will test out the minimal install in `travis.sh` but not the full install.  *Please test on a fresh VM, and preferably using a modified web-install instructions:*\n\n```bash\nwget https://raw.github.com/$GITHUBUSER/reddit/$TESTBRANCH/install-reddit.sh\nchmod +x install-reddit.sh\n./install-reddit.sh\n```\n\nThis will ensure that the installation is tested in the most basic installation scenario.\n\n**NOTE** if you find yourself adding files to this folder, please update `$NEEDED` in `../install-reddit.sh` to reflect the addition.  \n"
  },
  {
    "path": "install/done.sh",
    "content": "#!/bin/bash\n# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\n# load configuration\nRUNDIR=$(dirname $0)\nsource $RUNDIR/install.cfg\n\n###############################################################################\n# All done!\n###############################################################################\ncat <<CONCLUSION\n\nCongratulations! reddit is now installed.\n\nThe reddit application code is managed with upstart, to see what's currently\nrunning, run\n\n    sudo initctl list | grep reddit\n\nCron jobs start with \"reddit-job-\" and queue processors start with\n\"reddit-consumer-\". The crons are managed by /etc/cron.d/reddit. You can\ninitiate a restart of all the consumers by running:\n\n    sudo reddit-restart\n\nor target specific ones:\n\n    sudo reddit-restart scraper_q\n\nSee the GitHub wiki for more information on these jobs:\n\n* https://github.com/reddit/reddit/wiki/Cron-jobs\n* https://github.com/reddit/reddit/wiki/Services\n\nThe reddit code can be shut down or started up with\n\n    sudo reddit-stop\n    sudo reddit-start\n\nAnd if you think caching might be hurting you, you can flush memcache with\n\n    reddit-flush\n\nNow that the core of reddit is installed, you may want to do some additional\nsteps:\n\n* Ensure that $REDDIT_DOMAIN resolves to this machine.\n\n* To populate the database with test data, run:\n\n    cd $REDDIT_SRC/reddit\n    reddit-run scripts/inject_test_data.py -c 'inject_test_data()'\n\n* Manually run reddit-job-update_reddits immediately after populating the db\n  or adding your own subreddits.\nCONCLUSION\n"
  },
  {
    "path": "install/drone.sh",
    "content": "#!/usr/bin/env bash\n###############################################################################\n# reddit Drone environment installer\n# ----------------------------------\n# This script re-purposes some of our existing vagrant/Travis install and\n# setup scripts for our Drone CI builds.\n#\n# NOTE: You don't want to run this script directly in your development\n# environment, since we assume that it's running within this Docker image\n# that Drone runs our builds within: https://github.com/reddit/docker-reddit-py\n#\n# docker-reddit-py has most of the apt dependencies pre-installed in order to\n# significantly reduce our build times.\n#\n# Refer to .drone.yml in the repo root to see where this script gets called\n# during a build.\n###############################################################################\n\n###############################################################################\n# Install prerequisites\n###############################################################################\n\n# Under normal operation, this won't install anything new. We're re-using the\n# logic that checks to make sure all services have finished starting before\n# continuing.\ninstall/install_services.sh\n\n###############################################################################\n# Install and configure the reddit code\n###############################################################################\n\npushd r2\npython setup.py develop\nmake pyx\nln -sf example.ini test.ini\npopd\n\n###############################################################################\n# Configure local services\n###############################################################################\n\n# Creates the column families required for the tests\ninstall/setup_cassandra.sh\n"
  },
  {
    "path": "install/install.cfg",
    "content": "#!/bin/bash\n# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\nset -e -x\n\n###############################################################################\n# Configuration\n###############################################################################\n# which user to install the code for; defaults to the user invoking this script\nREDDIT_USER=${REDDIT_USER:-$SUDO_USER}\n\n# the group to run reddit code as; must exist already\nREDDIT_GROUP=${REDDIT_GROUP:-nogroup}\n\n# the root directory to base the install in. must exist already\nREDDIT_HOME=${REDDIT_HOME:-/home/$REDDIT_USER}\nREDDIT_SRC=${REDDIT_SRC:-$REDDIT_HOME/src}\n\n# the domain that you will connect to your reddit install with.\n# MUST contain a . in it somewhere as browsers won't do cookies for dotless\n# domains. an IP address will suffice if nothing else is available.\nREDDIT_DOMAIN=${REDDIT_DOMAIN:-reddit.local}\n\n# the plugins to install if they adhere to plugin naming and location\n# conventions on the host\nREDDIT_PLUGINS=${REDDIT_PLUGINS:-about gold}\n\n# aptitude configuration\nAPTITUDE_OPTIONS=${APTITUDE_OPTIONS:-\"-y\"}\n\n# Custom datastax repo\nCASSANDRA_SOURCES_LIST=/etc/apt/sources.list.d/cassandra.sources.list\n\nexport DEBIAN_FRONTEND=noninteractive\n"
  },
  {
    "path": "install/install_apt.sh",
    "content": "#!/bin/bash\n# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\n# load configuration\nRUNDIR=$(dirname $0)\nsource $RUNDIR/install.cfg\n\n# run an aptitude update to make sure python-software-properties\n# dependencies are found\napt-get update\n\n# add the datastax cassandra repos (NB: this is required for\n# install_cassandra.sh to work correctly, and the non-existence of this\n# file will trigger install_cassandra.sh to rerun this script)\necho deb http://debian.datastax.com/community stable main | \\\n    sudo tee $CASSANDRA_SOURCES_LIST\n    \nwget -qO- -L https://debian.datastax.com/debian/repo_key | \\\n    sudo apt-key add -\n\n# add the reddit ppa for some custom packages\napt-get install $APTITUDE_OPTIONS python-software-properties\napt-add-repository -y ppa:reddit/ppa\n\n# pin the ppa -- packages present in the ppa will take precedence over\n# ones in other repositories (unless further pinning is done)\ncat <<HERE > /etc/apt/preferences.d/reddit\nPackage: *\nPin: release o=LP-PPA-reddit\nPin-Priority: 600\nHERE\n\n# grab the new ppas' package listings\napt-get update\n\n# travis gives us a stock libmemcached.  We have to remove that\napt-get remove $APTITUDE_OPTIONS $(dpkg-query  -W -f='${binary:Package}\\n' | grep libmemcached | tr '\\n' ' ')\napt-get autoremove $APTITUDE_OPTIONS\n\n# install prerequisites\ncat <<PACKAGES | xargs apt-get install $APTITUDE_OPTIONS\nnetcat-openbsd\ngit-core\n\npython-dev\npython-setuptools\npython-routes\npython-pylons\npython-boto\npython-tz\npython-crypto\npython-babel\npython-numpy\npython-dateutil\ncython\npython-sqlalchemy\npython-beautifulsoup\npython-chardet\npython-psycopg2\npython-pycassa\npython-imaging\npython-pycaptcha\npython-pylibmc=1.2.2-1~trusty5\npython-amqplib\npython-bcrypt\npython-snappy\npython-snudown\npython-l2cs\npython-lxml\npython-kazoo\npython-stripe\npython-tinycss2\npython-unidecode\npython-mock\npython-yaml\npython-httpagentparser\n\npython-baseplate\n\npython-flask\ngeoip-bin\ngeoip-database\npython-geoip\n\nnodejs\nnode-less\nnode-uglify\ngettext\nmake\noptipng\njpegoptim\n\nlibpcre3-dev\n\npython-gevent\npython-gevent-websocket\npython-haigha\n\npython-redis\npython-pyramid\npython-raven\nPACKAGES\n"
  },
  {
    "path": "install/install_cassandra.sh",
    "content": "#!/bin/bash\n# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\n# load configuration\nRUNDIR=$(dirname $0)\nsource $RUNDIR/install.cfg\n\nif [ ! -e $CASSANDRA_SOURCES_LIST ]; then\n    echo \"Cassandra repo not added.  Running `install_apt.sh`\"\n    $RUNDIR/install_apt.sh\nfi\n\n# install cassandra\nsudo apt-get install $APTITUDE_OPTIONS cassandra=1.2.19\n\n# we don't want to upgrade to C* 2.0 yet, so we'll put it on hold\napt-mark hold cassandra || true\n\n# cassandra doesn't auto-start after install\nsudo service cassandra start\n\n# check each port for connectivity\necho \"Waiting for cassandra to be available...\"\nwhile ! nc -vz localhost 9160; do\n    sleep 1\ndone\n"
  },
  {
    "path": "install/install_services.sh",
    "content": "#!/bin/bash\n# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\n###############################################################################\n# Install services\n###############################################################################\n\n# load configuration\nRUNDIR=$(dirname $0)\nsource $RUNDIR/install.cfg\n\n# install prerequisites\ncat <<PACKAGES | xargs apt-get install $APTITUDE_OPTIONS\nmcrouter\nmemcached\npostgresql\npostgresql-client\nrabbitmq-server\nhaproxy\nnginx\ngunicorn\nredis-server\nPACKAGES\n\n###############################################################################\n# Wait for all the services to be up\n###############################################################################\n# check each port for connectivity\necho \"Waiting for services to be available, see source for port meanings...\"\n# 11211 - memcache\n# 5432 - postgres\n# 5672 - rabbitmq\nfor port in 11211 5432 5672; do\n    while ! nc -vz localhost $port; do\n        sleep 1\n    done\ndone\n"
  },
  {
    "path": "install/install_zookeeper.sh",
    "content": "#!/bin/bash\n# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2016 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nRUNDIR=$(dirname $0)\nsource $RUNDIR/install.cfg\n\nsudo apt-get install $APTITUDE_OPTIONS zookeeperd\n\necho \"Waiting for ZooKeeper to be available...\"\nwhile ! nc -vz localhost 2181; do\n    sleep 1\ndone\n"
  },
  {
    "path": "install/reddit.sh",
    "content": "#!/bin/bash\n###############################################################################\n# reddit dev environment installer\n# --------------------------------\n# This script installs a reddit stack suitable for development. DO NOT run this\n# on a system that you use for other purposes as it might delete important\n# files, truncate your databases, and otherwise do mean things to you.\n#\n# By default, this script will install the reddit code in the current user's\n# home directory and all of its dependencies (including libraries and database\n# servers) at the system level. The installed reddit will expect to be visited\n# on the domain \"reddit.local\" unless specified otherwise.  Configuring name\n# resolution for the domain is expected to be done outside the installed\n# environment (e.g. in your host machine's /etc/hosts file) and is not\n# something this script handles.\n#\n# Several configuration options (listed in the \"Configuration\" section below)\n# are overridable with environment variables. e.g.\n#\n#    sudo REDDIT_DOMAIN=example.com ./install/reddit.sh\n#\n###############################################################################\n\n# load configuration\nRUNDIR=$(dirname $0)\nsource $RUNDIR/install.cfg\n\n\n###############################################################################\n# Sanity Checks\n###############################################################################\nif [[ $EUID -ne 0 ]]; then\n    echo \"ERROR: Must be run with root privileges.\"\n    exit 1\nfi\n\nif [[ -z \"$REDDIT_USER\" ]]; then\n    # in a production install, you'd want the code to be owned by root and run\n    # by a less privileged user. this script is intended to build a development\n    # install, so we expect the owner to run the app and not be root.\n    cat <<END\nERROR: You have not specified a user. This usually means you're running this\nscript directly as root. It is not recommended to run reddit as the root user.\n\nPlease create a user to run reddit and set the REDDIT_USER variable\nappropriately.\nEND\n    exit 1\nfi\n\nif [[ \"amd64\" != $(dpkg --print-architecture) ]]; then\n    cat <<END\nERROR: This host is running the $(dpkg --print-architecture) architecture!\n\nBecause of the pre-built dependencies in our PPA, and some extra picky things\nlike ID generation in liveupdate, installing reddit is only supported on amd64\narchitectures.\nEND\n    exit 1\nfi\n\n# seriously! these checks are here for a reason. the packages from the\n# reddit ppa aren't built for anything but trusty (14.04) right now, so\n# if you try and use this install script on another release you're gonna\n# have a bad time.\nsource /etc/lsb-release\nif [ \"$DISTRIB_ID\" != \"Ubuntu\" -o \"$DISTRIB_RELEASE\" != \"14.04\" ]; then\n    echo \"ERROR: Only Ubuntu 14.04 is supported.\"\n    exit 1\nfi\n\nif [[ \"2000000\" -gt $(awk '/MemTotal/{print $2}' /proc/meminfo) ]]; then\n    LOW_MEM_PROMPT=\"reddit requires at least 2GB of memory to work properly, continue anyway? [y/n] \"\n    read -er -n1 -p \"$LOW_MEM_PROMPT\" response\n    if [[ \"$response\" != \"y\" ]]; then\n      echo \"Quitting.\"\n      exit 1\n    fi\nfi\n\nREDDIT_AVAILABLE_PLUGINS=\"\"\nfor plugin in $REDDIT_PLUGINS; do\n    if [ -d $REDDIT_SRC/$plugin ]; then\n        if [[ -z \"$REDDIT_PLUGINS\" ]]; then\n            REDDIT_AVAILABLE_PLUGINS+=\"$plugin\"\n        else\n            REDDIT_AVAILABLE_PLUGINS+=\" $plugin\"\n        fi\n        echo \"plugin $plugin found\"\n    else\n        echo \"plugin $plugin not found\"\n    fi\ndone\n\n###############################################################################\n# Install prerequisites\n###############################################################################\n\n# install primary packages\n$RUNDIR/install_apt.sh\n\n# install cassandra from datastax\n$RUNDIR/install_cassandra.sh\n\n# install zookeeper\n$RUNDIR/install_zookeeper.sh\n\n# install services (rabbitmq, postgres, memcached, etc.)\n$RUNDIR/install_services.sh\n\n###############################################################################\n# Install the reddit source repositories\n###############################################################################\nif [ ! -d $REDDIT_SRC ]; then\n    mkdir -p $REDDIT_SRC\n    chown $REDDIT_USER $REDDIT_SRC\nfi\n\nfunction copy_upstart {\n    if [ -d ${1}/upstart ]; then\n        cp ${1}/upstart/* /etc/init/\n    fi\n}\n\nfunction clone_reddit_repo {\n    local destination=$REDDIT_SRC/${1}\n    local repository_url=https://github.com/${2}.git\n\n    if [ ! -d $destination ]; then\n        sudo -u $REDDIT_USER -H git clone $repository_url $destination\n    fi\n\n    copy_upstart $destination\n}\n\nfunction clone_reddit_service_repo {\n    clone_reddit_repo $1 reddit/reddit-service-$1\n}\n\nclone_reddit_repo reddit reddit/reddit\nclone_reddit_repo i18n reddit/reddit-i18n\nclone_reddit_service_repo websockets\nclone_reddit_service_repo activity\n\n###############################################################################\n# Configure Services\n###############################################################################\n\n# Configure Cassandra\n$RUNDIR/setup_cassandra.sh\n\n# Configure PostgreSQL\n$RUNDIR/setup_postgres.sh\n\n# Configure mcrouter\n$RUNDIR/setup_mcrouter.sh\n\n# Configure RabbitMQ\n$RUNDIR/setup_rabbitmq.sh\n\n###############################################################################\n# Install and configure the reddit code\n###############################################################################\nfunction install_reddit_repo {\n    pushd $REDDIT_SRC/$1\n    sudo -u $REDDIT_USER python setup.py build\n    python setup.py develop --no-deps\n    popd\n}\n\ninstall_reddit_repo reddit/r2\ninstall_reddit_repo i18n\nfor plugin in $REDDIT_AVAILABLE_PLUGINS; do\n    copy_upstart $REDDIT_SRC/$plugin\n    install_reddit_repo $plugin\ndone\ninstall_reddit_repo websockets\ninstall_reddit_repo activity\n\n# generate binary translation files from source\nsudo -u $REDDIT_USER make -C $REDDIT_SRC/i18n clean all\n\n# this builds static files and should be run *after* languages are installed\n# so that the proper language-specific static files can be generated and after\n# plugins are installed so all the static files are available.\npushd $REDDIT_SRC/reddit/r2\nsudo -u $REDDIT_USER make clean pyx\n\nplugin_str=$(echo -n \"$REDDIT_AVAILABLE_PLUGINS\" | tr \" \" ,)\nif [ ! -f development.update ]; then\n    cat > development.update <<DEVELOPMENT\n# after editing this file, run \"make ini\" to\n# generate a new development.ini\n\n[DEFAULT]\n# global debug flag -- displays pylons stacktrace rather than 500 page on error when true\n# WARNING: a pylons stacktrace allows remote code execution. Make sure this is false\n# if your server is publicly accessible.\ndebug = true\n\ndisable_ads = true\ndisable_captcha = true\ndisable_ratelimit = true\ndisable_require_admin_otp = true\n\ndomain = $REDDIT_DOMAIN\noauth_domain = $REDDIT_DOMAIN\n\nplugins = $plugin_str\n\nmedia_provider = filesystem\nmedia_fs_root = /srv/www/media\nmedia_fs_base_url_http = http://%(domain)s/media/\n\n[server:main]\nport = 8001\nDEVELOPMENT\n    chown $REDDIT_USER development.update\nelse\n    sed -i \"s/^plugins = .*$/plugins = $plugin_str/\" $REDDIT_SRC/reddit/r2/development.update\n    sed -i \"s/^domain = .*$/domain = $REDDIT_DOMAIN/\" $REDDIT_SRC/reddit/r2/development.update\n    sed -i \"s/^oauth_domain = .*$/oauth_domain = $REDDIT_DOMAIN/\" $REDDIT_SRC/reddit/r2/development.update\nfi\n\nsudo -u $REDDIT_USER make ini\n\nif [ ! -L run.ini ]; then\n    sudo -u $REDDIT_USER ln -nsf development.ini run.ini\nfi\n\npopd\n\n###############################################################################\n# some useful helper scripts\n###############################################################################\nfunction helper-script() {\n    cat > $1\n    chmod 755 $1\n}\n\nhelper-script /usr/local/bin/reddit-run <<REDDITRUN\n#!/bin/bash\nexec paster --plugin=r2 run $REDDIT_SRC/reddit/r2/run.ini \"\\$@\"\nREDDITRUN\n\nhelper-script /usr/local/bin/reddit-shell <<REDDITSHELL\n#!/bin/bash\nexec paster --plugin=r2 shell $REDDIT_SRC/reddit/r2/run.ini\nREDDITSHELL\n\nhelper-script /usr/local/bin/reddit-start <<REDDITSTART\n#!/bin/bash\ninitctl emit reddit-start\nREDDITSTART\n\nhelper-script /usr/local/bin/reddit-stop <<REDDITSTOP\n#!/bin/bash\ninitctl emit reddit-stop\nREDDITSTOP\n\nhelper-script /usr/local/bin/reddit-restart <<REDDITRESTART\n#!/bin/bash\ninitctl emit reddit-restart TARGET=${1:-all}\nREDDITRESTART\n\nhelper-script /usr/local/bin/reddit-flush <<REDDITFLUSH\n#!/bin/bash\necho flush_all | nc localhost 11211\nREDDITFLUSH\n\nhelper-script /usr/local/bin/reddit-serve <<REDDITSERVE\n#!/bin/bash\nexec paster serve --reload $REDDIT_SRC/reddit/r2/run.ini\nREDDITSERVE\n\n###############################################################################\n# pixel and click server\n###############################################################################\nmkdir -p /var/opt/reddit/\nchown $REDDIT_USER:$REDDIT_GROUP /var/opt/reddit/\n\nmkdir -p /srv/www/pixel\nchown $REDDIT_USER:$REDDIT_GROUP /srv/www/pixel\ncp $REDDIT_SRC/reddit/r2/r2/public/static/pixel.png /srv/www/pixel\n\nif [ ! -f /etc/gunicorn.d/click.conf ]; then\n    cat > /etc/gunicorn.d/click.conf <<CLICK\nCONFIG = {\n    \"mode\": \"wsgi\",\n    \"working_dir\": \"$REDDIT_SRC/reddit/scripts\",\n    \"user\": \"$REDDIT_USER\",\n    \"group\": \"$REDDIT_USER\",\n    \"args\": (\n        \"--bind=unix:/var/opt/reddit/click.sock\",\n        \"--workers=1\",\n        \"tracker:application\",\n    ),\n}\nCLICK\nfi\n\nservice gunicorn start\n\n###############################################################################\n# nginx\n###############################################################################\n\nmkdir -p /srv/www/media\nchown $REDDIT_USER:$REDDIT_GROUP /srv/www/media\n\ncat > /etc/nginx/sites-available/reddit-media <<MEDIA\nserver {\n    listen 9000;\n\n    expires max;\n\n    location /media/ {\n        alias /srv/www/media/;\n    }\n}\nMEDIA\n\ncat > /etc/nginx/sites-available/reddit-pixel <<PIXEL\nupstream click_server {\n  server unix:/var/opt/reddit/click.sock fail_timeout=0;\n}\n\nserver {\n  listen 8082;\n\n  log_format directlog '\\$remote_addr - \\$remote_user [\\$time_local] '\n                      '\"\\$request_method \\$request_uri \\$server_protocol\" \\$status \\$body_bytes_sent '\n                      '\"\\$http_referer\" \"\\$http_user_agent\"';\n  access_log      /var/log/nginx/traffic/traffic.log directlog;\n\n  location / {\n\n    rewrite ^/pixel/of_ /pixel.png;\n\n    add_header Last-Modified \"\";\n    add_header Pragma \"no-cache\";\n\n    expires -1;\n    root /srv/www/pixel/;\n  }\n\n  location /click {\n    proxy_pass http://click_server;\n  }\n}\nPIXEL\n\ncat > /etc/nginx/sites-available/reddit-ssl <<SSL\nmap \\$http_upgrade \\$connection_upgrade {\n  default upgrade;\n  ''      close;\n}\n\nserver {\n    listen 443;\n\n    ssl on;\n    ssl_certificate /etc/ssl/certs/ssl-cert-snakeoil.pem;\n    ssl_certificate_key /etc/ssl/private/ssl-cert-snakeoil.key;\n\n    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;\n    ssl_ciphers EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;\n    ssl_prefer_server_ciphers on;\n\n    ssl_session_cache shared:SSL:1m;\n\n    location / {\n        proxy_pass http://127.0.0.1:8080;\n        proxy_set_header Host \\$http_host;\n        proxy_http_version 1.1;\n        proxy_set_header X-Forwarded-For \\$remote_addr;\n        proxy_pass_header Server;\n\n        # allow websockets through if desired\n        proxy_set_header Upgrade \\$http_upgrade;\n        proxy_set_header Connection \\$connection_upgrade;\n    }\n}\nSSL\n\n# remove the default nginx site that may conflict with haproxy\nrm -rf /etc/nginx/sites-enabled/default\n# put our config in place\nln -nsf /etc/nginx/sites-available/reddit-media /etc/nginx/sites-enabled/\nln -nsf /etc/nginx/sites-available/reddit-pixel /etc/nginx/sites-enabled/\nln -nsf /etc/nginx/sites-available/reddit-ssl /etc/nginx/sites-enabled/\n\n# make the pixel log directory\nmkdir -p /var/log/nginx/traffic\n\n# link the ini file for the Flask click tracker\nln -nsf $REDDIT_SRC/reddit/r2/development.ini $REDDIT_SRC/reddit/scripts/production.ini\n\nservice nginx restart\n\n###############################################################################\n# haproxy\n###############################################################################\nif [ -e /etc/haproxy/haproxy.cfg ]; then\n    BACKUP_HAPROXY=$(mktemp /etc/haproxy/haproxy.cfg.XXX)\n    echo \"Backing up /etc/haproxy/haproxy.cfg to $BACKUP_HAPROXY\"\n    cat /etc/haproxy/haproxy.cfg > $BACKUP_HAPROXY\nfi\n\n# make sure haproxy is enabled\ncat > /etc/default/haproxy <<DEFAULT\nENABLED=1\nDEFAULT\n\n# configure haproxy\ncat > /etc/haproxy/haproxy.cfg <<HAPROXY\nglobal\n    maxconn 350\n\nfrontend frontend\n    mode http\n\n    bind 0.0.0.0:80\n    bind 127.0.0.1:8080\n\n    timeout client 24h\n    option forwardfor except 127.0.0.1\n    option httpclose\n\n    # make sure that requests have x-forwarded-proto: https iff tls\n    reqidel ^X-Forwarded-Proto:.*\n    acl is-ssl dst_port 8080\n    reqadd X-Forwarded-Proto:\\ https if is-ssl\n\n    # send websockets to the websocket service\n    acl is-websocket hdr(Upgrade) -i WebSocket\n    use_backend websockets if is-websocket\n\n    # send media stuff to the local nginx\n    acl is-media path_beg /media/\n    use_backend media if is-media\n\n    # send pixel stuff to local nginx\n    acl is-pixel path_beg /pixel/\n    acl is-click path_beg /click\n    use_backend pixel if is-pixel || is-click\n\n    default_backend reddit\n\nbackend reddit\n    mode http\n    timeout connect 4000\n    timeout server 30000\n    timeout queue 60000\n    balance roundrobin\n\n    server app01-8001 localhost:8001 maxconn 30\n\nbackend websockets\n    mode http\n    timeout connect 4s\n    timeout server 24h\n    balance roundrobin\n\n    server websockets localhost:9001 maxconn 250\n\nbackend media\n    mode http\n    timeout connect 4000\n    timeout server 30000\n    timeout queue 60000\n    balance roundrobin\n\n    server nginx localhost:9000 maxconn 20\n\nbackend pixel\n    mode http\n    timeout connect 4000\n    timeout server 30000\n    timeout queue 60000\n    balance roundrobin\n\n    server nginx localhost:8082 maxconn 20\nHAPROXY\n\n# this will start it even if currently stopped\nservice haproxy restart\n\n###############################################################################\n# websocket service\n###############################################################################\n\nif [ ! -f /etc/init/reddit-websockets.conf ]; then\n    cat > /etc/init/reddit-websockets.conf << UPSTART_WEBSOCKETS\ndescription \"websockets service\"\n\nstop on runlevel [!2345] or reddit-restart all or reddit-restart websockets\nstart on runlevel [2345] or reddit-restart all or reddit-restart websockets\n\nrespawn\nrespawn limit 10 5\nkill timeout 15\n\nlimit nofile 65535 65535\n\nexec baseplate-serve2 --bind localhost:9001 $REDDIT_SRC/websockets/example.ini\nUPSTART_WEBSOCKETS\nfi\n\nservice reddit-websockets restart\n\n###############################################################################\n# activity service\n###############################################################################\n\nif [ ! -f /etc/init/reddit-activity.conf ]; then\n    cat > /etc/init/reddit-activity.conf << UPSTART_ACTIVITY\ndescription \"activity service\"\n\nstop on runlevel [!2345] or reddit-restart all or reddit-restart activity\nstart on runlevel [2345] or reddit-restart all or reddit-restart activity\n\nrespawn\nrespawn limit 10 5\nkill timeout 15\n\nexec baseplate-serve2 --bind localhost:9002 $REDDIT_SRC/activity/example.ini\nUPSTART_ACTIVITY\nfi\n\nservice reddit-activity restart\n\n###############################################################################\n# geoip service\n###############################################################################\nif [ ! -f /etc/gunicorn.d/geoip.conf ]; then\n    cat > /etc/gunicorn.d/geoip.conf <<GEOIP\nCONFIG = {\n    \"mode\": \"wsgi\",\n    \"working_dir\": \"$REDDIT_SRC/reddit/scripts\",\n    \"user\": \"$REDDIT_USER\",\n    \"group\": \"$REDDIT_USER\",\n    \"args\": (\n        \"--bind=127.0.0.1:5000\",\n        \"--workers=1\",\n         \"--limit-request-line=8190\",\n         \"geoip_service:application\",\n    ),\n}\nGEOIP\nfi\n\nservice gunicorn start\n\n###############################################################################\n# Job Environment\n###############################################################################\nCONSUMER_CONFIG_ROOT=$REDDIT_HOME/consumer-count.d\n\nif [ ! -f /etc/default/reddit ]; then\n    cat > /etc/default/reddit <<DEFAULT\nexport REDDIT_ROOT=$REDDIT_SRC/reddit/r2\nexport REDDIT_INI=$REDDIT_SRC/reddit/r2/run.ini\nexport REDDIT_USER=$REDDIT_USER\nexport REDDIT_GROUP=$REDDIT_GROUP\nexport REDDIT_CONSUMER_CONFIG=$CONSUMER_CONFIG_ROOT\nalias wrap-job=$REDDIT_SRC/reddit/scripts/wrap-job\nalias manage-consumers=$REDDIT_SRC/reddit/scripts/manage-consumers\nDEFAULT\nfi\n\n###############################################################################\n# Queue Processors\n###############################################################################\nmkdir -p $CONSUMER_CONFIG_ROOT\n\nfunction set_consumer_count {\n    if [ ! -f $CONSUMER_CONFIG_ROOT/$1 ]; then\n        echo $2 > $CONSUMER_CONFIG_ROOT/$1\n    fi\n}\n\nset_consumer_count search_q 0\nset_consumer_count del_account_q 1\nset_consumer_count scraper_q 1\nset_consumer_count markread_q 1\nset_consumer_count commentstree_q 1\nset_consumer_count newcomments_q 1\nset_consumer_count vote_link_q 1\nset_consumer_count vote_comment_q 1\nset_consumer_count automoderator_q 0\nset_consumer_count butler_q 1\nset_consumer_count author_query_q 1\nset_consumer_count subreddit_query_q 1\nset_consumer_count domain_query_q 1\n\nchown -R $REDDIT_USER:$REDDIT_GROUP $CONSUMER_CONFIG_ROOT/\n\n###############################################################################\n# Complete plugin setup, if setup.sh exists\n###############################################################################\nfor plugin in $REDDIT_AVAILABLE_PLUGINS; do\n    if [ -x $REDDIT_SRC/$plugin/setup.sh ]; then\n        echo \"Found setup.sh for $plugin; running setup script\"\n        $REDDIT_SRC/$plugin/setup.sh $REDDIT_SRC $REDDIT_USER\n    fi\ndone\n\n###############################################################################\n# Start everything up\n###############################################################################\n\n# the initial database setup should be done by one process rather than a bunch\n# vying with eachother to get there first\nreddit-run -c 'print \"ok done\"'\n\n# ok, now start everything else up\ninitctl emit reddit-stop\ninitctl emit reddit-start\n\n###############################################################################\n# Cron Jobs\n###############################################################################\nif [ ! -f /etc/cron.d/reddit ]; then\n    cat > /etc/cron.d/reddit <<CRON\n0    3 * * * root /sbin/start --quiet reddit-job-update_sr_names\n30  16 * * * root /sbin/start --quiet reddit-job-update_reddits\n0    * * * * root /sbin/start --quiet reddit-job-update_promos\n*/5  * * * * root /sbin/start --quiet reddit-job-clean_up_hardcache\n*/2  * * * * root /sbin/start --quiet reddit-job-broken_things\n*/2  * * * * root /sbin/start --quiet reddit-job-rising\n0    * * * * root /sbin/start --quiet reddit-job-trylater\n\n# liveupdate\n*    * * * * root /sbin/start --quiet reddit-job-liveupdate_activity\n\n# jobs that recalculate time-limited listings (e.g. top this year)\nPGPASSWORD=password\n*/15 * * * * $REDDIT_USER $REDDIT_SRC/reddit/scripts/compute_time_listings link year \"['hour', 'day', 'week', 'month', 'year']\"\n*/15 * * * * $REDDIT_USER $REDDIT_SRC/reddit/scripts/compute_time_listings comment year \"['hour', 'day', 'week', 'month', 'year']\"\n\n# disabled by default, uncomment if you need these jobs\n#*    * * * * root /sbin/start --quiet reddit-job-email\n#0    0 * * * root /sbin/start --quiet reddit-job-update_gold_users\nCRON\nfi\n\n###############################################################################\n# Finished with install script\n###############################################################################\n# print this out here. if vagrant's involved, it's gonna do more steps\n# afterwards and then re-run this script but that's ok.\n$RUNDIR/done.sh\n"
  },
  {
    "path": "install/setup_cassandra.sh",
    "content": "#!/bin/bash\n# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\n###############################################################################\n# Configure Cassandra\n###############################################################################\n\n# update the per-thread stack size. this used to be set to 256k in cassandra\n# version 1.2.19, but we recently downgraded to 1.2.11 where it's set too low\nsed -i -e 's/-Xss180k/-Xss256k/g' /etc/cassandra/cassandra-env.sh\n\npython <<END\nimport pycassa\nsys = pycassa.SystemManager(\"localhost:9160\")\n\nif \"reddit\" not in sys.list_keyspaces():\n    print \"creating keyspace 'reddit'\"\n    sys.create_keyspace(\"reddit\", \"SimpleStrategy\", {\"replication_factor\": \"1\"})\n    print \"done\"\n\nif \"permacache\" not in sys.get_keyspace_column_families(\"reddit\"):\n    print \"creating column family 'permacache'\"\n    sys.create_column_family(\"reddit\", \"permacache\")\n    print \"done\"\nEND\n"
  },
  {
    "path": "install/setup_mcrouter.sh",
    "content": "#!/bin/bash\n# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\n###############################################################################\n# Configure mcrouter\n###############################################################################\nif [ ! -d /etc/mcrouter ]; then\n    mkdir -p /etc/mcrouter\nfi\n\nif [ ! -f /etc/mcrouter/global.conf ]; then\n    cat > /etc/mcrouter/global.conf <<MCROUTER\n{\n  // route all valid prefixes to the local memcached\n  \"pools\": {\n    \"local\": {\n      \"servers\": [\n        \"127.0.0.1:11211\",\n      ],\n      \"protocol\": \"ascii\",\n      \"keep_routing_prefix\": false,\n    },\n  },\n  \"named_handles\": [\n    {\n      \"name\": \"local-pool\",\n      \"type\": \"PoolRoute\",\n      \"pool\": \"local\",\n    },\n  ],\n  \"route\": {\n    \"type\": \"PrefixSelectorRoute\",\n    \"policies\": {\n      \"rend:\": \"local-pool\",\n      \"page:\": \"local-pool\",\n      \"pane:\": \"local-pool\",\n      \"sr:\": \"local-pool\",\n      \"account:\": \"local-pool\",\n      \"link:\": \"local-pool\",\n      \"comment:\": \"local-pool\",\n      \"message:\": \"local-pool\",\n      \"campaign:\": \"local-pool\",\n      \"award:\": \"local-pool\",\n      \"trophy:\": \"local-pool\",\n      \"flair:\": \"local-pool\",\n      \"friend:\": \"local-pool\",\n      \"inboxcomment:\": \"local-pool\",\n      \"inboxmessage:\": \"local-pool\",\n      \"reportlink:\": \"local-pool\",\n      \"reportcomment:\": \"local-pool\",\n      \"reportsr:\": \"local-pool\",\n      \"reportmessage:\": \"local-pool\",\n      \"modinbox:\": \"local-pool\",\n      \"otp:\": \"local-pool\",\n      \"captcha:\": \"local-pool\",\n      \"queuedvote:\": \"local-pool\",\n      \"geoip:\": \"local-pool\",\n      \"geopromo:\": \"local-pool\",\n      \"srpromos:\": \"local-pool\",\n      \"rising:\": \"local-pool\",\n      \"srid:\": \"local-pool\",\n      \"defaultsrs:\": \"local-pool\",\n      \"featuredsrs:\": \"local-pool\",\n      \"query:\": \"local-pool\",\n      \"rel:\": \"local-pool\",\n      \"srmember:\": \"local-pool\",\n      \"srmemberrel:\": \"local-pool\",\n    },\n    \"wildcard\": {\n      \"type\": \"PoolRoute\",\n      \"pool\": \"local\",\n    },\n  },\n}\nMCROUTER\nfi\n\n# this file is sourced by the default mcrouter upstart config, see\n# /etc/init/mcrouter.conf\ncat > /etc/default/mcrouter <<MCROUTER_DEFAULT\nMCROUTER_FLAGS=\"-f /etc/mcrouter/global.conf -L /var/log/mcrouter/mcrouter.log -p 5050 -R /././ --stats-root=/var/mcrouter/stats\"\nMCROUTER_DEFAULT\n\n# set an upstart override so mcrouter starts when reddit starts\necho \"start on networking or reddit-start\" > /etc/init/mcrouter.override\n\n# restart mcrouter to read the updated config\nservice mcrouter restart\n"
  },
  {
    "path": "install/setup_postgres.sh",
    "content": "#!/bin/bash\n# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\n# load configuration\nRUNDIR=$(dirname $0)\nsource $RUNDIR/install.cfg\n\n###############################################################################\n# Configure PostgreSQL\n###############################################################################\nSQL=\"SELECT COUNT(1) FROM pg_catalog.pg_database WHERE datname = 'reddit';\"\nIS_DATABASE_CREATED=$(sudo -u postgres psql -t -c \"$SQL\")\n\nif [ $IS_DATABASE_CREATED -ne 1 ]; then\n    cat <<PGSCRIPT | sudo -u postgres psql\nCREATE DATABASE reddit WITH ENCODING = 'utf8' TEMPLATE template0 LC_COLLATE='en_US.utf8' LC_CTYPE='en_US.utf8';\nCREATE USER reddit WITH PASSWORD 'password';\nPGSCRIPT\nfi\n\nsudo -u postgres psql reddit <<FUNCTIONSQL\ncreate or replace function hot(ups integer, downs integer, date timestamp with time zone) returns numeric as \\$\\$\n    select round(cast(log(greatest(abs(\\$1 - \\$2), 1)) * sign(\\$1 - \\$2) + (date_part('epoch', \\$3) - 1134028003) / 45000.0 as numeric), 7)\n\\$\\$ language sql immutable;\n\ncreate or replace function score(ups integer, downs integer) returns integer as \\$\\$\n    select \\$1 - \\$2\n\\$\\$ language sql immutable;\n\ncreate or replace function controversy(ups integer, downs integer) returns float as \\$\\$\n    select CASE WHEN \\$1 <= 0 or \\$2 <= 0 THEN 0\n                WHEN \\$1 > \\$2 THEN power(\\$1 + \\$2, cast(\\$2 as float) / \\$1)\n                ELSE power(\\$1 + \\$2, cast(\\$1 as float) / \\$2)\n           END;\n\\$\\$ language sql immutable;\n\ncreate or replace function ip_network(ip text) returns text as \\$\\$\n    select substring(\\$1 from E'[\\\\d]+\\.[\\\\d]+\\.[\\\\d]+')\n\\$\\$ language sql immutable;\n\ncreate or replace function base_url(url text) returns text as \\$\\$\n    select substring(\\$1 from E'(?i)(?:.+?://)?(?:www[\\\\d]*\\\\.)?([^#]*[^#/])/?')\n\\$\\$ language sql immutable;\n\ncreate or replace function domain(url text) returns text as \\$\\$\n    select substring(\\$1 from E'(?i)(?:.+?://)?(?:www[\\\\d]*\\\\.)?([^#/]*)/?')\n\\$\\$ language sql immutable;\nFUNCTIONSQL\n"
  },
  {
    "path": "install/setup_rabbitmq.sh",
    "content": "#!/bin/bash\n# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\n# load configuration\nRUNDIR=$(dirname $0)\nsource $RUNDIR/install.cfg\n\n###############################################################################\n# Configure RabbitMQ\n###############################################################################\nif ! sudo rabbitmqctl list_vhosts | egrep \"^/$\"\nthen\n    sudo rabbitmqctl add_vhost /\nfi\n\nif ! sudo rabbitmqctl list_users | egrep \"^reddit\"\nthen\n    sudo rabbitmqctl add_user reddit reddit\nfi\n\nsudo rabbitmqctl set_permissions -p / reddit \".*\" \".*\" \".*\"\nsudo rabbitmq-plugins enable rabbitmq_management\nsudo service rabbitmq-server restart\n"
  },
  {
    "path": "install/travis.sh",
    "content": "#!/bin/bash\n# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\n###############################################################################\n# reddit travis environment installer\n# -----------------------------------\n# This script installs a reddit stack suitable for running on travis-ci.\n# As such, this is a minimal build to allow for running \"nosetests\"\n# and not much more.\n###############################################################################\n\n# load configuration\nRUNDIR=$(dirname $0)\nsource $RUNDIR/install.cfg\n\n# who is running me (expects \"travis\" or \"vagrant\")\nENVIRONMENT=${1:-travis}\n\n# the root directory to base the install in. must exist already\nREDDIT_CODE=${2:-$REDDIT_SRC/reddit}\n\nif [ ! -e $REDDIT_CODE ]; then\n    echo \"Couldn't find source $REDDIT_CODE. Aborting\"\n    exit 1\nfi\n\n###############################################################################\n# Sanity Checks\n###############################################################################\nif [[ $EUID -ne 0 ]]; then\n    echo \"ERROR: Must be run with root privileges.\"\n    exit 1\nfi\n\nif [[ \"amd64\" != $(dpkg --print-architecture) ]]; then\n    cat <<END\nERROR: This host is running the $(dpkg --print-architecture) architecture!\n\nBecause of the pre-built dependencies in our PPA, and some extra picky things\nlike ID generation in liveupdate, installing reddit is only supported on amd64\narchitectures.\nEND\n    exit 1\nfi\n\n# seriously! these checks are here for a reason. the packages from the\n# reddit ppa aren't built for anything but trusty (14.04) right now, so\n# if you try and use this install script on another release you're gonna\n# have a bad time.\nsource /etc/lsb-release\nif [ \"$DISTRIB_ID\" != \"Ubuntu\" -o \"$DISTRIB_RELEASE\" != \"14.04\" ]; then\n    echo \"ERROR: Only Ubuntu 14.04 is supported.\"\n    exit 1\nfi\n\n###############################################################################\n# Install prerequisites\n###############################################################################\n$RUNDIR/install_apt.sh\n\n$RUNDIR/install_cassandra.sh\n$RUNDIR/install_zookeeper.sh\n\n###############################################################################\n# Install and configure the reddit code\n###############################################################################\n\n[ -x \"$(which pip)\" ] || easy_install pip\npip install -U pip wheel setuptools coverage\npushd $REDDIT_CODE/r2\nsudo python setup.py build\npython setup.py develop\nmake\nln -sf example.ini test.ini\npopd\n\n###############################################################################\n# Install services (for local testing only!)\n# NB: this is otherwise handled in the .travis.yml in before_script\n###############################################################################\nif [ \"$ENVIRONMENT\" == \"vagrant\" ]; then\n    # install services (cassandra, postgres, etc.)\n    $RUNDIR/install_services.sh\n    # travis doesn't have mcrouter as a possible service, so we need to\n    # be able to test without that running\n    service mcrouter stop\n    # Configure PostgreSQL\n    $RUNDIR/setup_postgres.sh\n    # Configure Cassandra\n    $RUNDIR/setup_cassandra.sh\n    # Configure RabbitMQ\n    $RUNDIR/setup_rabbitmq.sh\nfi\n\n###############################################################################\n# All done!\n###############################################################################\ncat <<CONCLUSION\n\nCongratulations! A base version of reddit is now installed.  To run the\nunit tests:\n\n    cd src/reddit/r2\n    nosetests\n\nCONCLUSION\n"
  },
  {
    "path": "install-reddit.sh",
    "content": "#!/bin/bash\n###############################################################################\n# reddit dev environment installer\n# --------------------------------\n# This script installs a reddit stack suitable for development. DO NOT run this\n# on a system that you use for other purposes as it might delete important\n# files, truncate your databases, and otherwise do mean things to you.\n#\n# By default, this script will install the reddit code in the current user's\n# home directory and all of its dependencies (including libraries and database\n# servers) at the system level. The installed reddit will expect to be visited\n# on the domain \"reddit.local\" unless specified otherwise.  Configuring name\n# resolution for the domain is expected to be done outside the installed\n# environment (e.g. in your host machine's /etc/hosts file) and is not\n# something this script handles.\n#\n# Several configuration options (listed in the \"Configuration\" section below)\n# are overridable with environment variables. e.g.\n#\n#    sudo REDDIT_DOMAIN=example.com ./install/reddit.sh\n#\n###############################################################################\nset -e\n\nif [[ $EUID -ne 0 ]]; then\n    echo \"ERROR: Must be run with root privileges.\"\n    exit 1\nfi\n\n# load configuration\nRUNDIR=$(dirname $0)\nSCRIPTDIR=\"$RUNDIR/install\"\n\n# the canonical source of all installers\nGITREPO=\"https://raw.github.com/reddit/reddit/master/install\"\nNEEDED=(\n    \"done.sh\"\n    \"install_apt.sh\"\n    \"install_cassandra.sh\"\n    \"install_services.sh\"\n    \"install_zookeeper.sh\"\n    \"reddit.sh\"\n    \"setup_cassandra.sh\"\n    \"setup_mcrouter.sh\"\n    \"setup_postgres.sh\"\n    \"setup_rabbitmq.sh\"\n    \"travis.sh\"\n)\n\nMISSING=\"\"\nfor item in ${NEEDED[*]}; do\n    if [ ! -x $SCRIPTDIR/$item ]; then\n        MISSING=\"1\"\n        break\n    fi\ndone\n\nif [ ! -e $SCRIPTDIR/install.cfg ]; then\n    NEEDED+=(\"install.cfg\")\n    MISSING=\"1\"\nfi\n\n\nfunction important() {\n    echo -e \"\\033[31m${1}\\033[0m\"\n}\n\nif [ \"$MISSING\" != \"\" ]; then\n    important \"It looks like you're installing without a local repo.  No problem!\"\n    important \"We're going to grab the scripts we need and show you where you can\"\n    important \"edit the config to suit your environment.\"\n\n    mkdir -p $SCRIPTDIR\n    pushd $SCRIPTDIR > /dev/null\n    for item in ${NEEDED[*]}; do\n        echo \"Grabbing '${item}'...\"\n        wget -q $GITREPO/$item\n        chmod +x $item\n    done\n    popd > /dev/null\n\n    echo \"Done!\"\nfi\n\necho \"#######################################################################\"\necho \"# Base configuration:\"\necho \"#######################################################################\"\nsource $SCRIPTDIR/install.cfg\nset +x\n\necho\nimportant \"Before proceeding, make sure that these look reasonable.  If not,\"\nimportant \"you can either edit install/install.cfg or set overrides when running\"\nimportant \"(they will be respected).\"\necho\nimportant \"Seriously, if this is your first time installing, stop here and read\"\nimportant \"the script (install/reddit.sh) and that config. It's got some helpful\"\nimportant \"information that can prevent common issues.\"\necho\nimportant \"Resolving to the appropriate domain name is beyond the scope of this document,\"\nimportant \"but the easiest thing is probably editing /etc/hosts on the host machine.\"\necho\nread -er -n1 -p \"proceed? [Y/n]\" response\nif [[ $response =~ ^[Yy]$ || $response == \"\" ]]; then\n    echo \"Excellent. Here we go!\"\n    $SCRIPTDIR/reddit.sh\nfi\n"
  },
  {
    "path": "r2/Makefile",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nSHELL=/bin/sh\nPYTHON=python\nSED=sed\nCAT=cat\nBUILD_DIR=build\n\n.PHONY: clean\nall: pyx static ini\nclean: clean_pyx clean_i18n clean_static\n\nDEFS_PY := Makefile.py\nDEFS_FILE := Makefile.defs\n.PHONY: $(DEFS_FILE)\n$(shell $(PYTHON) $(DEFS_PY) > $(DEFS_FILE))\ninclude $(DEFS_FILE)\nifdef DEFS_SUCCESS\n$(info [+] including definitions from $(DEFS_PY))\nelse\n$(error $(DEFS_PY) failed. aborting)\nendif\n\n#################### Generated code: Cython + Thrift\nPYX_FILES := $(shell find . -name \\*.pyx)\nPYX_C_FILES := $(PYX_FILES:.pyx=.c)\nPYX_SO_FILES := $(PYX_FILES:.pyx=.so)\n\n.PHONY: pyx clean_pyx\n\npyx:\n\t$(PYTHON) setup.py build\n\t$(PYTHON) setup.py build_ext --inplace  # copy the .so files from cython into the source tree\n\nclean_pyx:\n\trm -f $(PYX_C_FILES) $(PYX_SO_FILES)\n\n#################### i18n\nSTRINGS_FILE := r2/lib/generate_strings.py\nGENERATED_STRINGS_FILE := r2/lib/generated_strings.py\n\n.PHONY: i18n clean_i18n\n\n# POTFILE is set by Makefile.py\ni18n: $(GENERATED_STRINGS_FILE)\n\t$(PYTHON) setup.py extract_messages --input-dirs=r2,$(PLUGIN_I18N_PATHS) -o $(POTFILE)\n\n$(GENERATED_STRINGS_FILE): $(STRINGS_FILE)\n\t$(PYTHON) $(STRINGS_FILE) > $(GENERATED_STRINGS_FILE)\n\nclean_i18n:\n\trm -f $(GENERATED_STRINGS_FILE)\n\n#################### ini files\nUPDATE_FILES := $(wildcard *.update)\nINIFILES := $(UPDATE_FILES:.update=.ini)\n\n.PHONY: clean_ini\n\nini: $(INIFILES)\n\n$(INIFILES): %.ini: %.update example.ini\n\t  ./updateini.py example.ini $< > $@ || rm $@ \n\nclean_ini:\n\trm $(INIFILES)\n\n#################### CSS file lists\nSPRITED_STYLESHEETS += reddit.less compact.css modtools.less expando.less\nLESS_STYLESHEETS := wiki.less adminbar.less policies.less reddit-embed.less\nOTHER_STYLESHEETS := mobile.css highlight.css\n\n#################### Static Files\nSTATIC_ROOT := r2/public\nSTATIC_FILES := $(shell find $(STATIC_ROOT) -readable)\nSTATIC_BUILD_ROOT := $(BUILD_DIR)/public\nSTATIC_BUILD_DIR := $(STATIC_BUILD_ROOT)/static\nSTATIC_BUILDSTAMP := $(BUILD_DIR)/static-buildstamp\n\n.PHONY: clean_static\n\nstatic: plugin_static pyx css js names\n\nclean_static:\n\trm -rf $(STATIC_BUILDSTAMP) $(STATIC_BUILD_DIR) $(PLUGIN_BUILDSTAMPS) $(MANGLE_BUILDSTAMP)\n\n$(STATIC_BUILDSTAMP): $(STATIC_FILES)\n\tcp -ruTL $(STATIC_ROOT) $(STATIC_BUILD_ROOT)\n\ttouch $@\n\n#################### Plugin static files\nPLUGIN_BUILDSTAMPS := $(foreach plugin,$(PLUGINS),$(BUILD_DIR)/plugin-$(plugin)-buildstamp)\n\nplugin_static: $(PLUGIN_BUILDSTAMPS)\n\ndefine PLUGIN_STATIC_TEMPLATE\n$(BUILD_DIR)/plugin-$(1)-buildstamp: $(STATIC_BUILDSTAMP) $(shell find $(PLUGIN_PATH_$(1))/public)\n\tif [ -f $(PLUGIN_PATH_$(1))/../Makefile ]; then \\\n\t\t$(MAKE) -C $(PLUGIN_PATH_$(1))/../ static; \\\n\tfi\n\tcp -r --preserve=timestamps $(PLUGIN_PATH_$(1))/public/* $(STATIC_BUILD_ROOT)/\n\ttouch $$@\n\n$(info [+] adding make rules from plugin \"$(1)\")\n-include $(PLUGIN_PATH_$(1))/../Makefile.plugin\nendef\n$(foreach plugin,$(PLUGINS),$(eval $(call PLUGIN_STATIC_TEMPLATE,$(plugin))))\n\n#### Stylesheets\nLESSC := lessc\nCSS_COMPRESS := $(PYTHON) r2/lib/contrib/rcssmin.py\nCSS_SOURCE_DIR := $(STATIC_BUILD_DIR)/css\n\nPROCESSED_SPRITED_STYLESHEETS := $(addprefix $(STATIC_BUILD_DIR)/, $(SPRITED_STYLESHEETS:.less=.css))\nSPRITES := $(addprefix $(STATIC_BUILD_DIR)/, $(patsubst %.css,sprite-%.png, $(SPRITED_STYLESHEETS:.less=.css)))\n\nLESS_OUTPUTS := $(addprefix $(STATIC_BUILD_DIR)/, $(patsubst %.less,%.css, $(LESS_STYLESHEETS)))\n\nMINIFIED_OTHER_STYLESHEETS := $(addprefix $(STATIC_BUILD_DIR)/, $(OTHER_STYLESHEETS))\n\nPROCESSED_STYLESHEETS := $(PROCESSED_SPRITED_STYLESHEETS) $(MINIFIED_OTHER_STYLESHEETS) $(LESS_OUTPUTS)\n\nCSS_OUTPUTS = $(PROCESSED_STYLESHEETS) $(SPRITES)\n\n.PHONY: clean_css\n\ncss: $(STATIC_BUILDSTAMP) $(CSS_OUTPUTS)\n\n\n# the LESSC invocation is separated so the recipe fails in case of LESS errors.\n$(LESS_OUTPUTS): $(STATIC_BUILD_DIR)/%.css : $(CSS_SOURCE_DIR)/%.less\n\trm -f $@\n\t$(LESSC) $< > $@.tmp\n\t$(CSS_COMPRESS) < $@.tmp > $@\n\trm $@.tmp\n\n$(MINIFIED_OTHER_STYLESHEETS): $(STATIC_BUILD_DIR)/%.css: $(CSS_SOURCE_DIR)/%.css\n\t# when static file names are mangled, the original becomes a symlink to the mangled name\n\t# remove the original file here in case it's a symlink so we don't just rewrite the old file\n\trm -f $@\n\t$(CAT) $< | $(CSS_COMPRESS) > $@\n\n$(STATIC_BUILD_DIR)/sprite-%.png $(STATIC_BUILD_DIR)/%.css: $(CSS_SOURCE_DIR)/%.less $(STATIC_BUILDSTAMP)\n\t# see above\n\trm -f $(STATIC_BUILD_DIR)/sprite-$*.png $(STATIC_BUILD_DIR)/$*.css\n\t$(PYTHON) r2/lib/nymph.py $(CSS_SOURCE_DIR)/$*.less $(STATIC_BUILD_DIR)/sprite-$*.png > $(CSS_SOURCE_DIR)/$*.less.tmp\n\t$(LESSC) $(CSS_SOURCE_DIR)/$*.less.tmp > $(STATIC_BUILD_DIR)/$*.css.tmp\n\t$(CSS_COMPRESS) < $(STATIC_BUILD_DIR)/$*.css.tmp > $(STATIC_BUILD_DIR)/$*.css\n\trm $(CSS_SOURCE_DIR)/$*.less.tmp $(STATIC_BUILD_DIR)/$*.css.tmp\n\n# deprecated; remove once compact.scss has been converted to LESS\n$(STATIC_BUILD_DIR)/sprite-%.png $(STATIC_BUILD_DIR)/%.css: $(CSS_SOURCE_DIR)/%.css $(STATIC_BUILDSTAMP)\n\t# see above\n\trm -f $(STATIC_BUILD_DIR)/sprite-$*.png $(STATIC_BUILD_DIR)/$*.css\n\t$(PYTHON) r2/lib/nymph.py $< $(STATIC_BUILD_DIR)/sprite-$*.png | $(CSS_COMPRESS) > $(STATIC_BUILD_DIR)/$*.css\n\nclean_css:\n\trm -f $(CSS_OUTPUTS)\n\n#### JS\n\n.PHONY: clean_js\n\njs: $(STATIC_BUILDSTAMP) $(JS_OUTPUTS)\n\ndefine JS_MODULE_TEMPLATE\n$(JS_MODULE_OUTPUTS_$(1)): $(JS_MODULE_DEPS_$(1)) r2/lib/js.py\n\t# remove mangled output symlinks, similar to above.\n\trm -f $(JS_MODULE_OUTPUTS_$(1))\n\t$(PYTHON) r2/lib/js.py build_module \"$(1)\"\n\ttouch $$@\nendef\n\n# apply the module template to each of the modules\n# so they source their deps from js.py and build accordingly\n$(foreach module,$(JS_MODULES),$(eval $(call JS_MODULE_TEMPLATE,$(module))))\n\nclean_js:\n\trm -f $(STATIC_BUILD_DIR)/*.js\n\n#### name mangling\nMANGLEABLE_FILES := $(CSS_OUTPUTS) $(JS_OUTPUTS)\nMANGLE_BUILDSTAMP := $(BUILD_DIR)/mangle-buildstamp\nNAMES_FILE := $(STATIC_BUILD_DIR)/names.json\nMANGLED_FILES := $(wildcard $(foreach file,$(MANGLEABLE_FILES),$(basename $(file)).*$(suffix $(file))))\n\n.PHONY: clean_names\n\nnames: $(MANGLE_BUILDSTAMP)\n\n$(MANGLE_BUILDSTAMP): $(MANGLEABLE_FILES)\n\t$(PYTHON) r2/lib/static.py $(NAMES_FILE) $(MANGLEABLE_FILES)\n\ttouch $@\n\nclean_names:\n\trm -f $(MANGLE_BUILDSTAMP) $(NAMES_FILE) $(MANGLEABLE_FILES) $(MANGLED_FILES)\n\n$(shell rm -f $(DEFS_FILE))\n"
  },
  {
    "path": "r2/Makefile.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\nimport os\n\nfrom r2.lib.translation import I18N_PATH\nfrom r2.lib.plugin import PluginLoader\nfrom r2.lib import js\n\nprint 'POTFILE := ' + os.path.join(I18N_PATH, 'r2.pot')\n\nplugins = PluginLoader()\nprint 'PLUGINS := ' + ' '.join(plugin.name for plugin in plugins\n                               if plugin.needs_static_build)\n\nprint 'PLUGIN_I18N_PATHS := ' + ','.join(os.path.relpath(plugin.path)\n                                         for plugin in plugins\n                                         if plugin.needs_translation)\n\nimport sys\nfor plugin in plugins:\n    print 'PLUGIN_PATH_%s := %s' % (plugin.name, plugin.path)\n\njs.load_plugin_modules(plugins)\nmodules = dict((k, m) for k, m in js.module.iteritems())\nprint 'JS_MODULES := ' + ' '.join(modules.iterkeys())\noutputs = []\nfor name, module in modules.iteritems():\n    outputs.extend(module.outputs)\n    print 'JS_MODULE_OUTPUTS_%s := %s' % (name, ' '.join(module.outputs))\n    print 'JS_MODULE_DEPS_%s := %s' % (name, ' '.join(module.dependencies))\n\nprint 'JS_OUTPUTS := ' + ' '.join(outputs)\nprint 'DEFS_SUCCESS := 1'\n"
  },
  {
    "path": "r2/babel.cfg",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\n# .html files in JS are underscore templates, not mako\n[javascript:**/public/static/js/**.html]\n\n# Extraction from Mako templates\n\n[mako:**.html]\ninput_encoding = utf-8\n[mako:**.email]\ninput_encoding = utf-8\n[mako:**.xml]\ninput_encoding = utf-8\n[mako:**.htmllite]\ninput_encoding = utf-8\n\n# Extraction from Python source files\n\n[python:**.py]\n\n[javascript:**.js]\nencoding = utf-8\n"
  },
  {
    "path": "r2/check-code",
    "content": "#!/usr/bin/python\n# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\"\"\"Check for new style guide violations in the current branch.\n\nThis script is meant to be used in a CI process to ensure that new changes\ndo not violate PEP-8, PEP-257, or any of the validity checks of pyflakes.\n\n\"\"\"\nimport argparse\nimport collections\nimport difflib\nimport logging\nimport lxml.etree as etree\nimport os\nimport re\nimport subprocess\nimport sys\n\n\nDEVNULL = open(\"/dev/null\", \"w\")\nTOOLS = collections.OrderedDict((\n    ('pep8', [\"pep8\", \"--repeat\"]),\n    ('pep257', [\"pep257\"]),\n    ('pyflakes', [\"pyflakes\"]),\n))\n\n\n# Match *.py and *.pyx\nPYFILE = re.compile(r\".*\\.pyx?$\")\n\n\ndef assert_tools_available():\n    \"\"\"Check if the external binaries needed are available or exit.\"\"\"\n    for tool in TOOLS.values():\n        binary = tool[0]\n        try:\n            subprocess.check_call([\"which\", binary], stdout=DEVNULL)\n        except subprocess.CalledProcessError:\n            logging.error(\"command %r not found. please install it!\", binary)\n            sys.exit(1)\n\n\ndef assert_not_dirty():\n    \"\"\"Check if there are uncommitted changes in the repo and exit if so.\"\"\"\n    try:\n        subprocess.check_call([\"git\", \"diff\",\n                               \"--no-ext-diff\", \"--quiet\", \"--exit-code\"])\n    except subprocess.CalledProcessError:\n        logging.error(\"you have uncommitted changes. please commit them!\")\n        sys.exit(1)\n\n\ndef _parse_ref(ref):\n    \"\"\"Return the result of git rev-parse on the given ref.\"\"\"\n    ref = subprocess.check_output([\"git\", \"rev-parse\", ref])\n    return ref.strip()\n\n\ndef get_current_ref():\n    \"\"\"Return the most descriptive name possible of the current HEAD.\"\"\"\n    try:\n        ref = subprocess.check_output([\"git\", \"symbolic-ref\", \"HEAD\"]).strip()\n        return ref[len(\"refs/heads/\"):]\n    except subprocess.CalledProcessError:\n        return _parse_ref(\"HEAD\")\n\n\ndef get_upstream_ref():\n    \"\"\"Return the ref that this topic branch is based on.\"\"\"\n    return _parse_ref(\"master@{upstream}\")\n\n\ndef get_merge_base():\n    upstream = get_upstream_ref()\n    current = get_current_ref()\n    output = subprocess.check_output(['git', 'merge-base', upstream, current])\n    return output.strip()\n\n\ndef get_root():\n    \"\"\"Return the root directory of this git project.\"\"\"\n    return os.path.dirname(_parse_ref(\"--git-dir\"))\n\n\ndef check_ref_out(ref):\n    \"\"\"Ask git to check out the specified ref.\"\"\"\n    try:\n        subprocess.check_call(\n            [\"git\", \"checkout\", ref],\n            stdout=DEVNULL,\n            stderr=DEVNULL,\n        )\n    except subprocess.CalledProcessError:\n        logging.error(\"failed to check out %s\", ref)\n        sys.exit(1)\n\n\ndef walk_workspace():\n    root = get_root()\n    files = subprocess.check_output(['git', 'ls-files', '--full-name',\n                                     '--', root])\n    for filename in files.splitlines():\n        yield os.path.join(root, filename)\n\n\ndef select_files(files):\n    for f in files:\n        if re.match(PYFILE, f):\n            yield f\n        else:\n            try:\n                with open(f) as f_:\n                    first = f_.readline()\n                    if first.startswith('#!') and 'python' in first:\n                        yield f\n            except (IOError, OSError):\n                logging.exception(\"Unable to check-code against %s\", f)\n\n\ndef extract_errtype(violation, tool):\n    \"\"\"Based on a line of `tool`'s output, return the kind of infraction.\n\n    Mostly relevant for pep8, which has various kinds of infractions,\n    such as E501 for line too long.\n\n    \"\"\"\n    if tool == 'pep8':\n        # E501 line too long (91 characters)\n        errtype, sep, message = violation.partition(\" \")\n        if not sep:\n            errtype = 'PEP8'\n    elif tool == 'pep257':\n        errtype = 'PEP257'\n    elif tool == 'pyflakes':\n        errtype = 'pyflakes'\n    return errtype\n\n\ndef make_test_class(tool, filepath):\n    no_ext, ext = os.path.splitext(filepath)\n    test_class_suffix = no_ext.replace(os.path.sep, '_')\n    return tool + '.' + test_class_suffix\n\n\ndef extract_line_info(reportline, filepath, tool):\n    if tool == 'pep257' and reportline.startswith('Note: checks'):\n        return None\n    file_info, sep, violation = reportline.partition(\": \")\n    if not sep:\n        return None\n    file_info = file_info.split(\":\")\n    if len(file_info) < 2:\n        logging.warn(\"I don't understand this report line: %r\", reportline)\n        line_num = ''\n    else:\n        line_num = file_info[1]\n    report_entry = {\n        'file': filepath,\n        'test_class': make_test_class(tool, filepath),\n        'line': line_num,\n        'violation': violation,\n        'errtype': extract_errtype(violation, tool),\n        'tool': tool,\n    }\n    return report_entry\n\n\ndef generate_report(toolname, files=None):\n    if not files:\n        files = walk_workspace()\n\n    report = []\n    for filepath in select_files(files):\n        command = TOOLS[toolname] + [filepath]\n        logging.info(\" \".join(command))\n        process = subprocess.Popen(\n            command,\n            stdout=subprocess.PIPE,\n            stderr=subprocess.STDOUT,\n        )\n\n        lines = process.communicate()[0].splitlines()\n        ws_root = get_root()\n        ws_filepath = os.path.relpath(filepath, ws_root)\n        for line in lines:\n            line = extract_line_info(line, ws_filepath, toolname)\n            if line:\n                report.append(line)\n    return report\n\n\ndef generate_all_reports(ref=None, files=None):\n    \"\"\"Run the tools on the specified files and return errors / warnings.\"\"\"\n    if ref:\n        check_ref_out(ref)\n\n    report = collections.OrderedDict.fromkeys(TOOLS.keys())\n    for tool in TOOLS:\n        report[tool] = generate_report(tool, files)\n\n    return report\n\n\ndef get_changed_files(old_ref, new_ref):\n    \"\"\"Return a list of files that have changed from one ref to another.\"\"\"\n    root = get_root()\n    changed_files_text = subprocess.check_output([\"git\", \"diff\", \"--name-only\",\n                                                  old_ref, new_ref])\n    changed_files = changed_files_text.splitlines()\n    return [os.path.join(root, x) for x in changed_files]\n\n\ndef diffable(report):\n    \"\"\"Convert the report to a list of lines that are reasonably 'diffable'.\n\n    That is, standard diff tools should be able to identify new or fixed\n    violations by comparing results of this function\n\n    \"\"\"\n    updated = []\n    for toolname, violations in report.iteritems():\n        updated.append(toolname)\n        updated.extend('%(file)s %(violation)s' % v for v in violations)\n        updated.append('')\n    return updated\n\n\ndef human(report):\n    \"\"\"Convert the report to a list of human useful lines.\"\"\"\n    updated = []\n    for toolname, violations in report.iteritems():\n        updated.append(toolname)\n        updated.extend('%(file)s:%(line)s %(violation)s' % v\n                       for v in violations)\n        updated.append('')\n    return updated\n\n\ndef junitize(report):\n    \"\"\"Convert the report into JUnit style XML.\n\n    This allows the report to be consumed by tools that consume JUnit reports\n\n    The style used here is: each file is a <testsuite>; each violation\n    will be a <testcase> (always failed) whose \"classname\" shall be the tool\n    used (e.g., pep8) and \"name\" shall be the type of violation and line\n    number. Any additional information shall be included as the <failure>\n    message.\n\n    \"\"\"\n    by_file = {}\n    for violations in report.itervalues():\n        for violation in violations:\n            file_errors = by_file.setdefault(violation['file'], [])\n            file_errors.append(violation)\n    violations = etree.Element(\"testsuites\")\n    for filename in by_file:\n        file_errs = etree.SubElement(violations, \"testsuite\")\n        for violation in by_file[filename]:\n            entry = etree.SubElement(file_errs, \"testcase\")\n            entry.attrib['classname'] = violation['test_class']\n            entry.attrib['name'] = violation['line']\n            error_info = etree.SubElement(entry, \"failure\")\n            error_info.attrib['message'] = violation['violation']\n            error_info.attrib['type'] = violation['errtype']\n    return violations\n\n\ndef make_errname(violation):\n    \"\"\"Create a unique \"test name\" for this violation.\"\"\"\n    name = '.'.join((violation['errtype'], violation['line']))\n    return name\n\n\ndef diff_report(options):\n    if options.check_dirty:\n        assert_not_dirty()\n\n    current_ref = get_current_ref()\n    base_ref = get_merge_base()\n    if options.files:\n        files = options.files\n    else:\n        files = get_changed_files(base_ref, current_ref)\n        logging.debug(\"files changed: %r\", files)\n\n    try:\n        new_report = diffable(generate_all_reports(current_ref, files))\n        logging.debug(\"new report:\\n%r\", new_report)\n        old_report = diffable(generate_all_reports(base_ref, files))\n        logging.debug(\"old report:\\n%r\", old_report)\n    finally:\n        check_ref_out(current_ref)\n\n    return difflib.unified_diff(old_report, new_report)\n\n\ndef regression_report(options):\n    added, removed = 0, 0\n    for line in diff_report(options):\n        line = line.strip()\n        if line == \"+++\" or line == \"---\":\n            continue\n        if line.startswith(\"+\"):\n            added += 1\n        elif line.startswith(\"-\"):\n            removed += 1\n\n    if added:\n        print >> options.out, \"added %d issues\" % added\n    if removed:\n        print >> options.out, \"removed %d issues!\" % removed\n\n    return 1 if added else 0\n\n\ndef junit_report(options):\n    report = generate_all_reports(files=options.files)\n    junit = junitize(report)\n    print >> options.out, etree.tostring(junit, pretty_print=True)\n\n\ndef human_report(options):\n    if options.full:\n        report = human(generate_all_reports(files=options.files))\n    else:\n        files = (options.files or\n                 get_changed_files(get_merge_base(), get_current_ref()))\n        logging.debug(\"changed files: %r\", files)\n        report = human(generate_all_reports(files=files))\n    for line in report:\n        print >> options.out, line\n\n\ndef parse_args(args):\n    parser = argparse.ArgumentParser(description=\"Report on python problems\")\n    parser.add_argument('--dirty', dest='check_dirty', action='store_false',\n                        help=\"Skip the dirty workspace check.\")\n    parser.add_argument('-O', dest='out', type=argparse.FileType('w'),\n                        default=sys.stdout, help=\"Write the report to OUT\"\n                        \" instead of stdout.\")\n    parser.add_argument('--verbose', '-v', action='count', dest='verbosity',\n                        help=\"Show verbose reporting messages.\",\n                        default=0)\n    parser.add_argument('--quiet', '-q', action='count', default=0,\n                        help=\"Reduce verbosity\")\n    parser.add_argument('--full', action='store_true', help=\"When generating\"\n                        \" a {report}, show all files, not just changed ones.\")\n    parser.add_argument('report', choices=('junit', 'regression', 'report'))\n    parser.add_argument('files', nargs='*', metavar='FILE')\n    options = parser.parse_args(args)\n    set_up_logging(options.verbosity - options.quiet)\n    logging.debug(\"Options: %r\", options)\n    return options\n\n\ndef set_up_logging(verbosity):\n    levels = {-2: logging.ERROR, -1: logging.WARN, 0: logging.INFO,\n              1: logging.DEBUG}\n    max_level = max(levels.keys())\n    min_level = min(levels.keys())\n    verbosity = min(verbosity, max_level)\n    verbosity = max(verbosity, min_level)\n    level = levels[verbosity]\n    format_ = '%(levelname)s %(message)s'\n    logging.basicConfig(level=level, format=format_)\n\n\ndef main():\n    options = parse_args(sys.argv[1:])\n    if options.report == 'regression':\n        command = regression_report\n    elif options.report == 'junit':\n        command = junit_report\n    elif options.report == 'report':\n        command = human_report\n    assert_tools_available()\n    sys.exit(command(options))\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "r2/coverage.sh",
    "content": "#!/bin/bash\nset -e\n\nBASEDIR=$(readlink -f $(dirname $0))\ncd $BASEDIR\n\nVERSION=$(git rev-parse HEAD)\nCOVERDIR=\"$BASEDIR/build/cover-$VERSION\"\n\nfunction usage() {\n    echo \"Run unit tests and coverage reports on reddit codebase with optional\"\n    echo \"http server to the report\"\n    echo\n    echo \"Usage: `basename $0` [options]\";\n    echo\n    echo \"  -h           show this message\"\n    echo \"  -p \\$PORT     run an simple http server on \\$PORT to view results\"\n    echo \"  -v           verbose mode (set -x)\"\n    echo\n}\n\nwhile getopts \":vhp:\" opt; do\n  case $opt in\n    p) PORT=\"$OPTARG\" ;;\n    v) set -x ;;\n    h)\n      usage\n      exit 0\n      ;;\n    \\?)\n      echo \"Invalid option: -$OPTARG\" >&2\n      usage\n      exit 1\n      ;;\n    :)\n      echo \"Option -$OPTARG requires an argument.\" >&2\n      exit 1\n      ;;\n  esac\ndone\n\nnosetests \\\n    --with-coverage \\\n    --cover-html \\\n    --cover-html-dir=$COVERDIR \\\n    --cover-erase \\\n    --cover-package=r2\n\nif [ \"$PORT\" != \"\" ]; then\n    echo \"Starting http server on :$PORT (^C to exit)\"\n    pushd $COVERDIR\n    python -m SimpleHTTPServer $PORT || echo \"Done.\"\n    popd\nfi\nrm -r $COVERDIR\n"
  },
  {
    "path": "r2/pylintrc",
    "content": "[MASTER]\n\n# Specify a configuration file.\n#rcfile=\n\n# Python code to execute, usually for sys.path manipulation such as\n# pygtk.require().\n#init-hook=\n\n# Profiled execution.\nprofile=no\n\n# Add <file or directory> to the black list. It should be a base name, not a\n# path. You may set this option multiple times.\nignore=CVS\n\n# Pickle collected data for later comparisons.\npersistent=yes\n\n# List of plugins (as comma separated values of python modules names) to load,\n# usually to register additional checkers.\nload-plugins=\n\n\n[MESSAGES CONTROL]\n\n# Enable the message, report, category or checker with the given id(s). You can\n# either give multiple identifier separated by comma (,) or put this option\n# multiple time.\n#enable=\n\n# Disable the message, report, category or checker with the given id(s). You\n# can either give multiple identifier separated by comma (,) or put this option\n# multiple time (only on the command line, not in the configuration file where\n# it should appear only once).\n# E1103: X has no Y member (but some types could not be inferred)\n# W0212: Access to a protected member of X class\n# W0223: Method Y is abstract in class X but not overridden\n# C0103: Invalid name \"%s\" (should match %s)\n# C0111: Missing docstring\n# W0142: Used * or ** magic\n# R0201: Method could be a function\n# R0915: Too many statements\n# I0011: Locally disabling ... (don't need to see things we've explicitly disabled)\ndisable=E1103,W0212,W0223,C0111,W0142,R0201,R0915,I0011,C0103\n\n\n[REPORTS]\n\n# Set the output format. Available formats are text, parseable, colorized, msvs\n# (visual studio) and html\noutput-format=text\n\n# Include message's id in output\ninclude-ids=yes\n\n# Put messages in a separate file for each module / package specified on the\n# command line instead of printing them on stdout. Reports (if any) will be\n# written in a file name \"pylint_global.[txt|html]\".\nfiles-output=no\n\n# Tells whether to display a full report or only the messages\nreports=no\n\n# Python expression which should return a note less than 10 (10 is the highest\n# note). You have access to the variables errors warning, statement which\n# respectively contain the number of errors / warnings messages and the total\n# number of statements analyzed. This is used by the global evaluation report\n# (RP0004).\nevaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)\n\n# Add a comment according to your evaluation note. This is used by the global\n# evaluation report (RP0004).\ncomment=no\n\n\n[BASIC]\n\n# Required attributes for module, separated by a comma\nrequired-attributes=\n\n# List of builtins function names that should not be used, separated by a comma\nbad-functions=apply,input\n\n# Regular expression which should only match correct module names\nmodule-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$\n\n# Regular expression which should only match correct module level names\nconst-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$\n\n# Regular expression which should only match correct class names\nclass-rgx=[A-Z_][a-zA-Z0-9]+$\n\n# Regular expression which should only match correct function names\nfunction-rgx=[a-z_][a-z0-9_]{2,30}$\n\n# Regular expression which should only match correct method names\nmethod-rgx=[a-z_][a-z0-9_]{2,30}$\n\n# Regular expression which should only match correct instance attribute names\nattr-rgx=[a-z_][a-z0-9_]{2,30}$\n\n# Regular expression which should only match correct argument names\nargument-rgx=[a-z_][a-z0-9_]{2,30}$\n\n# Regular expression which should only match correct variable names\nvariable-rgx=[a-z_][a-z0-9_]{2,30}$\n\n# Regular expression which should only match correct list comprehension /\n# generator expression variable names\ninlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$\n\n# Good variable names which should always be accepted, separated by a comma\ngood-names=i,j,k,c,g,ex,Run,_\n\n# Bad variable names which should always be refused, separated by a comma\nbad-names=foo,bar,baz,toto,tutu,tata\n\n# Regular expression which should only match functions or classes name which do\n# not require a docstring\nno-docstring-rgx=__.*__\n\n\n[FORMAT]\n\n# Maximum number of characters on a single line.\nmax-line-length=80\n\n# Maximum number of lines in a module\nmax-module-lines=1000\n\n# String used as indentation unit. This is usually \" \" (4 spaces) or \"\\t\" (1\n# tab).\nindent-string='    '\n\n\n[MISCELLANEOUS]\n\n# List of note tags to take in consideration, separated by a comma.\nnotes=FIXME,XXX,TODO\n\n\n[SIMILARITIES]\n\n# Minimum lines number of a similarity.\nmin-similarity-lines=4\n\n# Ignore comments when computing similarities.\nignore-comments=yes\n\n# Ignore docstrings when computing similarities.\nignore-docstrings=yes\n\n\n[TYPECHECK]\n\n# Tells whether missing members accessed in mixin class should be ignored. A\n# mixin class is detected if its name ends with \"mixin\" (case insensitive).\nignore-mixin-members=yes\n\n# List of classes names for which member attributes should not be checked\n# (useful for classes with attributes dynamically set).\nignored-classes=SQLObject\n\n# When zope mode is activated, add a predefined set of Zope acquired attributes\n# to generated-members.\nzope=no\n\n# List of members which are set dynamically and missed by pylint inference\n# system, and so shouldn't trigger E0201 when accessed.\ngenerated-members=REQUEST,acl_users,aq_parent\n\n\n[VARIABLES]\n\n# Tells whether we should check for unused import in __init__ files.\ninit-import=no\n\n# A regular expression matching the beginning of the name of dummy variables\n# (i.e. not used).\ndummy-variables-rgx=unused|dummy\n\n# List of additional names supposed to be defined in builtins. Remember that\n# you should avoid to define new builtins when possible.\nadditional-builtins=\n\n\n[CLASSES]\n\n# List of interface methods to ignore, separated by a comma. This is used for\n# instance to not check methods defines in Zope's Interface base class.\nignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by\n\n# List of method names used to declare (i.e. assign) instance attributes.\ndefining-attr-methods=__init__,__new__,setUp\n\n\n[DESIGN]\n\n# Maximum number of arguments for function / method\nmax-args=5\n\n# Argument names that match this expression will be ignored. Default to name\n# with leading underscore\nignored-argument-names=_.*\n\n# Maximum number of locals for function / method body\nmax-locals=15\n\n# Maximum number of return / yield for function / method body\nmax-returns=6\n\n# Maximum number of branch for function / method body\nmax-branchs=25\n\n# Maximum number of statements in function / method body\nmax-statements=50\n\n# Maximum number of parents for a class (see R0901).\nmax-parents=7\n\n# Maximum number of attributes for a class (see R0902).\nmax-attributes=7\n\n# Minimum number of public methods for a class (see R0903).\nmin-public-methods=0\n\n# Maximum number of public methods for a class (see R0904).\nmax-public-methods=20\n\n\n[IMPORTS]\n\n# Deprecated modules which should not be used, separated by a comma\ndeprecated-modules=regsub,string,TERMIOS,Bastion,rexec\n\n# Create a graph of every (i.e. internal and external) dependencies in the\n# given file (report RP0402 must not be disabled)\nimport-graph=\n\n# Create a graph of external dependencies in the given file (report RP0402 must\n# not be disabled)\next-import-graph=\n\n# Create a graph of internal dependencies in the given file (report RP0402 must\n# not be disabled)\nint-import-graph=\n"
  },
  {
    "path": "r2/r2/__init__.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\n\"\"\"r2\n\nThis file loads the finished app from r2.config.middleware.\n\"\"\"\n\n# _strptime is imported with PyImport_ImportModuleNoBlock which can fail\n# miserably when multiple threads try to import it simultaneously.\n# import this here to get it over with\n# see \"Non Blocking Module Imports\" in:\n# http://code.google.com/p/modwsgi/wiki/ApplicationIssues\nimport _strptime\n\n# defer the (hefty) import until it's actually needed. this allows\n# modules below r2 to be imported before cython files are built, also\n# provides a hefty speed boost to said imports when they don't need\n# the app initialization.\ndef make_app(*args, **kwargs):\n    from r2.config.middleware import make_app as real_make_app\n    return real_make_app(*args, **kwargs)\n"
  },
  {
    "path": "r2/r2/commands.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport os, sys\n\nimport paste.fixture\nfrom paste.script import command\nfrom paste.deploy import loadapp\n\nfrom r2.lib.log import RavenErrorReporter\n\n\nclass RunCommand(command.Command):\n    max_args = 2\n    min_args = 1\n\n    usage = \"CONFIGFILE CMDFILE.py\"\n    summary = \"Executed CMDFILE with pylons support\"\n    group_name = \"Reddit\"\n\n\n    parser = command.Command.standard_parser(verbose=True)\n    parser.add_option('-c', '--command',\n                      dest='command',\n                      help=\"execute command in module\")\n    parser.add_option(\"\", \"--proctitle\",\n                      dest=\"proctitle\",\n                      help=\"set the title seen by ps and top\")\n\n    def command(self):\n        try:\n            if self.options.proctitle:\n                import setproctitle\n                setproctitle.setproctitle(\"paster \" + self.options.proctitle)\n        except ImportError:\n            pass\n\n        config_file = self.args[0]\n        config_name = 'config:%s' % config_file\n\n        report_to_sentry = \"REDDIT_ERRORS_TO_SENTRY\" in os.environ\n\n        here_dir = os.getcwd()\n\n        # Load locals and populate with objects for use in shell\n        sys.path.insert(0, here_dir)\n\n        # Load the wsgi app first so that everything is initialized right\n        global_conf = {\n            'running_as_script': \"true\",\n        }\n        wsgiapp = loadapp(\n            config_name, relative_to=here_dir, global_conf=global_conf)\n        test_app = paste.fixture.TestApp(wsgiapp)\n\n        # Query the test app to setup the environment\n        tresponse = test_app.get('/_test_vars')\n        request_id = int(tresponse.body)\n\n        # Disable restoration during test_app requests\n        test_app.pre_request_hook = lambda self: \\\n            paste.registry.restorer.restoration_end()\n        test_app.post_request_hook = lambda self: \\\n            paste.registry.restorer.restoration_begin(request_id)\n\n        # Restore the state of the Pylons special objects\n        # (StackedObjectProxies)\n        paste.registry.restorer.restoration_begin(request_id)\n\n        loaded_namespace = {}\n\n        try:\n            if self.args[1:]:\n                execfile(self.args[1], loaded_namespace)\n\n            if self.options.command:\n                exec self.options.command in loaded_namespace\n        except Exception:\n            if report_to_sentry:\n                exc_info = sys.exc_info()\n                RavenErrorReporter.capture_exception(exc_info=exc_info)\n            raise\n"
  },
  {
    "path": "r2/r2/config/__init__.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\n"
  },
  {
    "path": "r2/r2/config/environment.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport os\nimport mimetypes\n\nfrom mako.lookup import TemplateLookup\nfrom pylons.error import handle_mako_error\nfrom pylons.configuration import PylonsConfig\n\nimport r2.lib.helpers\nfrom r2.config.paths import (\n    get_r2_path,\n    get_built_statics_path,\n    get_raw_statics_path,\n)\nfrom r2.config.routing import make_map\nfrom r2.lib.app_globals import Globals\nfrom r2.lib.configparse import ConfigValue\n\n\nmimetypes.init()\n\n\ndef load_environment(global_conf={}, app_conf={}, setup_globals=True):\n    r2_path = get_r2_path()\n    root_path = os.path.join(r2_path, 'r2')\n\n    paths = {\n        'root': root_path,\n        'controllers': os.path.join(root_path, 'controllers'),\n        'templates': [os.path.join(root_path, 'templates')],\n    }\n\n    if ConfigValue.bool(global_conf.get('uncompressedJS')):\n        paths['static_files'] = get_raw_statics_path()\n    else:\n        paths['static_files'] = get_built_statics_path()\n\n    config = PylonsConfig()\n\n    config.init_app(global_conf, app_conf, package='r2', paths=paths)\n\n    # don't put action arguments onto c automatically\n    config['pylons.c_attach_args'] = False\n\n    # when accessing non-existent attributes on c, return \"\" instead of dying\n    config['pylons.strict_tmpl_context'] = False\n\n    g = Globals(config, global_conf, app_conf, paths)\n    config['pylons.app_globals'] = g\n\n    if setup_globals:\n        config['r2.import_private'] = \\\n            ConfigValue.bool(global_conf['import_private'])\n        g.setup()\n        g.plugins.declare_queues(g.queues)\n\n    g.plugins.load_plugins(config)\n    config['r2.plugins'] = g.plugins\n    g.startup_timer.intermediate(\"plugins\")\n\n    config['pylons.h'] = r2.lib.helpers\n    config['routes.map'] = make_map(config)\n\n    #override the default response options\n    config['pylons.response_options']['headers'] = {}\n\n    # when mako loads a previously compiled template file from its cache, it\n    # doesn't check that the original template path matches the current path.\n    # in the event that a new plugin defines a template overriding a reddit\n    # template, unless the mtime newer, mako doesn't update the compiled\n    # template. as a workaround, this makes mako store compiled templates with\n    # the original path in the filename, forcing it to update with the path.\n    if \"cache_dir\" in app_conf:\n        module_directory = os.path.join(app_conf['cache_dir'], 'templates')\n\n        def mako_module_path(filename, uri):\n            filename = filename.lstrip('/').replace('/', '-')\n            path = os.path.join(module_directory, filename + \".py\")\n            return os.path.abspath(path)\n    else:\n        # disable caching templates since we don't know where they should go.\n        module_directory = mako_module_path = None\n\n    # set up the templating system\n    config[\"pylons.app_globals\"].mako_lookup = TemplateLookup(\n        directories=paths[\"templates\"],\n        error_handler=handle_mako_error,\n        module_directory=module_directory,\n        input_encoding=\"utf-8\",\n        default_filters=[\"conditional_websafe\"],\n        filesystem_checks=getattr(g, \"reload_templates\", False),\n        imports=[\n            \"from r2.lib.filters import websafe, unsafe, conditional_websafe\",\n            \"from pylons import request\",\n            \"from pylons import tmpl_context as c\",\n            \"from pylons import app_globals as g\",\n            \"from pylons.i18n import _, ungettext\",\n        ],\n        modulename_callable=mako_module_path,\n    )\n\n    if setup_globals:\n        g.setup_complete()\n\n    return config\n"
  },
  {
    "path": "r2/r2/config/extensions.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom pylons import tmpl_context as c\n\ndef api_type(subtype = ''):\n    return 'api-' + subtype if subtype else 'api'\n\ndef is_api(subtype = ''):\n    return c.render_style and c.render_style.startswith(api_type(subtype))\n\ndef get_api_subtype():\n    if is_api() and c.render_style.startswith('api-'):\n        return c.render_style[4:]\n\nextension_mapping = {\n    \"rss\": (\"xml\", \"application/atom+xml; charset=UTF-8\"),\n    \"xml\": (\"xml\", \"application/atom+xml; charset=UTF-8\"),\n    \"js\": (\"js\", \"text/javascript; charset=UTF-8\"),\n    \"embed\": (\"htmllite\", \"text/javascript; charset=UTF-8\"),\n    \"mobile\": (\"mobile\", \"text/html; charset=UTF-8\"),\n    \"png\": (\"png\", \"image/png\"),\n    \"css\": (\"css\", \"text/css\"),\n    \"csv\": (\"csv\", \"text/csv; charset=UTF-8\"),\n    \"api\": (api_type(), \"application/json; charset=UTF-8\"),\n    \"json-html\": (api_type(\"html\"), \"application/json; charset=UTF-8\"),\n    \"json-compact\": (api_type(\"compact\"), \"application/json; charset=UTF-8\"),\n    \"compact\": (\"compact\", \"text/html; charset=UTF-8\"),\n    \"json\": (api_type(), \"application/json; charset=UTF-8\"),\n    \"i\": (\"compact\", \"text/html; charset=UTF-8\"),\n}\n\nAPI_TYPES = ('api', 'json')\nRSS_TYPES = ('rss', 'xml')\n\ndef set_extension(environ, ext):\n    environ[\"extension\"] = ext\n    environ[\"render_style\"], environ[\"content_type\"] = extension_mapping[ext]\n"
  },
  {
    "path": "r2/r2/config/feature/README.md",
    "content": "# Feature\n\n`r2.config.feature` is reddit's feature flagging API. It lets us quickly\nswitch on and off features for specific segments of users and requests.\n\nIt's inspired by Etsy's feature framework, at\nhttps://github.com/etsy/feature - if you're looking to add to this, you may\nwant to check there first to see if there's learning to be had. There almost\ncertainly is.\n\n## Use\n\nUsing the feature API is simple. At its core:\n\n```python\n\nfrom r2.config import feature\n\nif feature.is_enabled('some_flag'):\n    result = do_new_thing()\nelse:\n    result = do_old_thing()\n```\n\nOr in a mako template:\n\n```html\n\n% if feature.is_enabled('some_flag'):\n  <strong>New thing!</strong>\n% else:\n  <span>Old thing.</span>\n% endif\n```\n\n\nAlong with a component in live_config, currently as an \"on\" or \"off\" symbol or JSON:\n\n```ini\n\n# Completely On\nfeature_some_flag = on\n\n# Completely Off\nfeature_some_flag = off\n\n# On for admin\nfeature_some_flag = {\"admin\": true}\n\n# On for employees\nfeature_some_flag = {\"employee\": true}\n\n# On for gold users\nfeature_some_flag = {\"gold\": true}\n\n# On for users with the beta preference enabled\nfeature_some_flag = {\"beta\": true}\n\n# On for logged in users\nfeature_some_flag = {\"loggedin\": true}\n\n# On for logged out users\nfeature_some_flag = {\"loggedout\": true}\n\n# On by URL, like ?feature=public_flag_name\nfeature_some_flag = {\"url\": \"public_flag_name\"}\n\n# On by group of users\nfeature_some_flag = {\"users\": [\"umbrae\", \"ajacksified\"]}\n\n# On when viewing certain subreddits\nfeature_some_flag = {\"subreddits\": [\"wtf\", \"aww\"]}\n\n# On by subdomain\nfeature_some_flag = {\"subdomains\": [\"beta\"]}\n\n# On by OAuth client IDs\nfeature_some_flag = {\"oauth_clients\": [\"xyzABC123\"]}\n\n# On for a percentage of loggedin users (0 being no users, 100 being all of them)\nfeature_some_flag = {\"percent_loggedin\": 25}\n\n# On for a percentage of loggedout users (0 being no users, 100 being all of them)\n# N.B: This is based on the value of the `loid` cookie, if there is no `loid`\n# cookie the feature will be off.\n# The `loid` cookie is currently set in JavaScript, so you can't expect it to\n# exist on the first visit or in requests made by API clients.\nfeature_some_flag = {\"percent_loggedout\": 25}\n\n# For both admin and a group of users\nfeature_some_flag = {\"admin\": true, \"users\": [\"user1\", \"user2\"]}\n```\n\nSince we're currently overloading live_config, each feature flag should be\nprepended with `feature_` in the config. We may choose to make a live-updating\nfeatures block in the future.\n\nYou can also use feature flags to define A/B-type experiments.  Logically,\nexperiments are separated into two parts.  First, there is an *eligibility\ncheck* to determine if the user is allowed to be a part of the experiment;\neligibility is determined by the same selectors as above with the exception of\n`percent_loggedin` and `percent_loggedout` which would be redundant.  \nSecondly, eligible users are either *bucketed* into a variant or *excluded*\n(because the summed percentage of all variants is less than 100).  `is_enabled`\nwill return False for users who are non-eligible, fall into a control group, or\nare excluded; for anyone for whom this is true, you should call `variant` to\nfind the specific variant they fall into.\n\nIn code, this looks something like this:\n\n```python\nfrom r2.config import feature\n\nif feature.is_enabled('some_flag'):\n    variant = feature.variant('some_flag')\n    if variant == 'test_something':\n        do_new_thing()\n    elif variant == 'test_something_else':\n        do_other_new_thing()\n    else:\n        raise NotImplementedError('unknown variant %s for some_flag' % variant)\nelse:\n    do_old_thing()\n```\n\nwith a live_config option defining the experiment parameters:\n\n```ini\n# loggedin only experiment with two test variants\nfeature_some_flag = {\"experiment\": {\"loggedin\": true, \"experiment_id\": 12345, \"variants\": {\"test_something\": 5.5, \"test_something_else\": 10}}}\n\n# Or with custom control group sizes:\nfeature_some_flag = {\"experiment\": {\"loggedin\": true, \"experiment_id\": 12345, \"variants\": {\"test_something\": 5.5, \"test_something_else\": 10, \"control_1\": 20, \"control_2\": 20}}}\n\n# these can be mixed and matched with other selectors (and will OR)\n# this will enable the flag for gold users, and then run an experiment for other logged in users\nfeature_some_flag = {\"gold\": true, \"experiment\": {\"loggedin\": true, \"experiment_id\": 12345, \"variants\": {\"test_something\": 5.5, \"test_something_else\": 10, \"control_1\": 20, \"control_2\": 20}}}\n```\n\nIf only one non-control variant is defined (an A/A/B test), the code can be\nsimplified a little bit:\n\n```python\nfrom r2.config import feature\n\nif feature.is_enabled('some_flag'):\n    do_new_thing()\nelse:\n    do_old_thing()\n```\n\nThe experiment dict has a few fields:\n\n* **experiment_id** -- an integer.  While the feature name needs to be unique\n  across all currently-defined feature flags, the experiment id should be\n  unique across all time.  This allows the data team to uniquely identify\n  experiments while looking at historical data.\n* **variants** -- a dictionary mapping variant names to percentages.  The\n  percent indicates roughly how many eligible users will be chosen to be a part\n  of that variant.  Percentages should not exceed 100/n, where n is the number\n  of variants.  The number of variants should not change over the course of the\n  experiment, but the percentages allocated each can.  Percentages can be\n  specified to the tenths of percentages.  If not defined, two control\n  groups (\"control_1\" and \"control_2\") at 10% each will be automatically added\n  to the variants.\n* **enabled** -- a boolean, defaulting to true.  Set to false to temporarily\n  disable an experiment while still keeping its definition around.\n\nSince it's useful to be able to force bucketing for testing purposes, you can\nspecify a variant with a secondary syntax for a few flag conditions:\n\n```ini\n# ?feature=some_flag_something will force the \"test_something\" variant and\n# ?feature=some_flag_something_else will force \"test_something_else\"\nfeature_some_flag = {\"url\": {\"some_flag_something\": \"test_something\", \"some_flag_something_else\": \"test_something_else\"}}\n```\n\n## When should I use this?\n\nThis is useful for a whole lot of reasons.\n\n* To admin-launch something to the company for review before it goes live to\n  everyone, and staging isn't a good fit.\n\n* To release something to third party devs and mods before it goes live\n\n* To gradually add traffic to something that may have serious\n  impact on load\n\n* To guard something that you might need to quickly turn off for some reason\n  or another. Load shedding, security, etc.\n\n\n## Style guidelines\n\nCopied essentially wholesale from Etsy's guidelines:\n\nTo make it easier to push features through the life cycle there are a\nfew coding guidelines to observe.\n\nFirst, the feature name argument to the Feature method (`is_enabled`) should\nalways be a string literal. This will make it easier to find all the places\nthat a particular feature is checked. If you find yourself creating feature\nnames at run time and then checking them, you’re probably abusing the Feature\nsystem. Chances are in such a case you don’t really want to be using the\nFeature API but rather simply driving your code with some plain old config\ndata.\n\nSecond, the results of the Feature methods should not be cached, such\nas by calling `feature.is_enabled` once and storing the result in an\ninstance variable of some controller. The Feature machinery already\ncaches the results of the computation it does so it should already be\nplenty fast to simply call `feature.is_enabled` whenever needed. This\nwill again aid in finding the places that depend on a particular feature.\n\nThird, as a check that you’re using the Feature API properly, whenever\nyou have an if block whose test is a call to `feature.is_enabled`,\nmake sure that it would make sense to either remove the check and keep\nthe code or to delete the check and the code together. There shouldn’t\nbe bits of code within a block guarded by an is_enabled check that\nneeds to be salvaged if the feature is removed.\n"
  },
  {
    "path": "r2/r2/config/feature/__init__.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom r2.config.feature.feature import is_enabled, variant, all_enabled\n"
  },
  {
    "path": "r2/r2/config/feature/feature.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom r2.config.feature.state import FeatureState\nfrom r2.config.feature.world import World\nfrom r2.lib.hooks import HookRegistrar\n\nfeature_hooks = HookRegistrar()\n\n_world = World()\n_featurestate_cache = {}\n\n\ndef is_enabled(name, user=None, subreddit=None):\n    \"\"\"Test and return whether a given feature is enabled for this request.\n\n    If `feature` is not found, returns False.\n\n    The optional arguments allow overriding that you generally don't want, but\n    is useful outside of request contexts - cron jobs and the like.\n\n    :param name string - a given feature name\n    :param user - (optional) an Account\n    :param subreddit - (optional) a Subreddit\n    :return bool\n    \"\"\"\n    if not user:\n        user = _world.current_user()\n    if not subreddit:\n        subreddit = _world.current_subreddit()\n    subdomain = _world.current_subdomain()\n    oauth_client = _world.current_oauth_client()\n\n    return _get_featurestate(name).is_enabled(\n        user=user,\n        subreddit=subreddit,\n        subdomain=subdomain,\n        oauth_client=oauth_client,\n    )\n\ndef variant(name, user=None):\n    \"\"\"Return which variant of an experiment a user is part of.\n\n    If the experiment is not found, has no variants, or the user is not part of\n    any of them (control), return None.\n\n    :param name string - an experiment (feature) name\n    :param user - (optional) an Account.  Defaults to the currently signed in\n                  user.\n    :return string, or None if not part of an experiment\n    \"\"\"\n    if not user:\n        user = _world.current_user()\n\n    return _get_featurestate(name).variant(user)\n\ndef all_enabled(user=None):\n    \"\"\"Return a list of enabled features and experiments for the user.\n    \n    Provides the user's assigned variant and the experiment ID for experiments.\n\n    This does not trigger bucketing events, so it should not be used for\n    feature flagging purposes on the server. It is meant to let clients\n    condition features on experiment variants. Those clients should manually\n    send the appropriate bucketing events.\n\n    This does not include page-based experiments, which operate independently\n    of the particular user.\n\n    :param user - (optional) an Account. Defaults to None, for which we\n                  determine logged-out features.\n    :return dict - a dictionary mapping enabled feature keys to True or to the\n                   experiment/variant information\n    \"\"\"\n    features = FeatureState.get_all(_world)\n\n    # Get enabled features and experiments\n    active = {}\n    for feature in features:\n        experiment = feature.config.get('experiment')\n        # Exclude page experiments\n        if experiment and FeatureState.is_user_experiment(experiment):\n            # Get experiment names, ids, and assigned variants, leaving out\n            # experiments for which this user is excluded\n            variant = feature.variant(user)\n            if variant:\n                active[feature.name] = {\n                    'experiment_id': experiment.get('experiment_id'),\n                    'variant': variant\n                }\n        elif feature.is_enabled(user):\n                active[feature.name] = True\n\n    return active\n\n@feature_hooks.on('worker.live_config.update')\ndef clear_featurestate_cache():\n    global _featurestate_cache\n    _featurestate_cache = {}\n\n\ndef _get_featurestate(name):\n    \"\"\"Get a FeatureState object for this feature, creating it if necessary.\n\n    :param name string - a given feature name\n    :return FeatureState\n    \"\"\"\n\n    featurestate = _featurestate_cache.get(name, None)\n    if featurestate is None:\n        featurestate = FeatureState(name, _world)\n        _featurestate_cache[name] = featurestate\n\n    return featurestate\n"
  },
  {
    "path": "r2/r2/config/feature/state.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport logging\nimport json\nimport hashlib\n\nfrom pylons import tmpl_context as c\nfrom pylons import app_globals as g\n\n\nclass FeatureState(object):\n    \"\"\"A FeatureState is the state of a feature and its condition in the world.\n\n    It determines if this feature is enabled given the world provided.\n    \"\"\"\n\n    # Special values for globally enabled properties - no need to interrogate\n    # the world for these values.\n    GLOBALLY_ON = \"on\"\n    GLOBALLY_OFF = \"off\"\n\n    # constant config blocks\n    DISABLED_CFG = {\"enabled\": GLOBALLY_OFF}\n    ENABLED_CFG = {\"enabled\": GLOBALLY_ON}\n\n    # The number of buckets to use for any bucketing operations.  Should always\n    # be evenly divisible by 100.  Each factor of 10 over 100 gives us an\n    # additional digit of precision.\n    NUM_BUCKETS = 1000\n\n    # The variant definition for control groups that are added by default.\n    DEFAULT_CONTROL_GROUPS = {'control_1': 10, 'control_2': 10}\n\n    def __init__(self, name, world, config_name=None, config_str=None):\n        self.name = name\n        self.world = world\n        self.config = self._parse_config(name, config_name, config_str)\n\n    def _parse_config(self, name, config_name=None, config_str=None):\n        \"\"\"Find and parse a config from our live config with this given name.\n\n        :param name string - a given feature name\n        :return dict - a dictionary with at least \"enabled\". May include more\n                       depending on the enabled type.\n        \"\"\"\n        if not config_name:\n            config_name = \"feature_%s\" % name\n\n        if not config_str:\n            config_str = self.world.live_config(config_name)\n\n        if not config_str or config_str == FeatureState.GLOBALLY_OFF:\n            return self.DISABLED_CFG\n\n        if config_str == FeatureState.GLOBALLY_ON:\n            return self.ENABLED_CFG\n\n        try:\n            config = json.loads(config_str)\n        except (ValueError, TypeError) as e:\n            g.log.warning(\"Could not load config for name %r - %r\",\n                          config_name, e)\n            return self.DISABLED_CFG\n\n        if not isinstance(config, dict):\n            g.log.warning(\"Config not dict, on or off: %r\", config_name)\n            return self.DISABLED_CFG\n\n        return config\n\n    @staticmethod\n    def get_all(world):\n        \"\"\"Return FeatureState objects for all features in live_config.\n\n        Creates a FeatureState object for every config entry prefixed with\n        \"feature_\".\n\n        :param world - World proxy object to the app/request state.\n        \"\"\"\n        features = []\n        for (key, config_str) in world.live_config_iteritems():\n            if key.startswith('feature_'):\n                feature_state = FeatureState(key[8:], world, key, config_str)\n                features.append(feature_state)\n        return features\n\n    @staticmethod\n    def is_user_experiment(experiment):\n        return not FeatureState.is_page_experiment(experiment)\n\n    @staticmethod\n    def is_page_experiment(experiment):\n        return experiment.get('page')\n\n    def _calculate_bucket(self, seed, experiment_seed=None):\n        \"\"\"Sort something into one of self.NUM_BUCKETS buckets.\n\n        :param seed -- a string used for shifting the deterministic bucketing\n                       algorithm.  In most cases, this will be an Account's\n                       _fullname.\n        :return int -- a bucket, 0 <= bucket < self.NUM_BUCKETS\n        \"\"\"\n        # Mix the feature name in with the seed so the same users don't get\n        # selected for ramp-ups for every feature.\n        hashed = hashlib.sha1(self.name + seed)\n        bucket = long(hashed.hexdigest(), 16) % self.NUM_BUCKETS\n        return bucket\n\n    @classmethod\n    def _choose_variant(cls, bucket, variants):\n        \"\"\"Deterministically choose a percentage-based variant.\n\n        The algorithm satisfies two conditions:\n\n        1. It's deterministic (that is, every call with the same bucket and\n           variants will result in the same answer).\n        2. An increase in any of the variant percentages will keep the same\n           buckets in the same variants as at the smaller percentage (that is,\n           all buckets previously put in variant A will still be in variant A,\n           all buckets previously put in variant B will still be in variant B,\n           etc. and the increased percentages will be made of up buckets\n           previously not assigned to a bucket).\n\n        These attributes make it suitable for use in A/B experiments that may\n        see an increase in their variant percentages post-enabling.\n\n        :param bucket -- an integer bucket representation\n        :param variants -- a dictionary of\n                           <string:variant name>:<float:percentage> pairs.  If\n                           any percentage exceeds 1/n percent, where n is the\n                           number of variants, the percentage will be capped to\n                           1/n.  These variants will be added to\n                           DEFAULT_CONTROL_GROUPS to create the effective\n                           variant set.\n        :return string -- the variant name, or None if bucket doesn't fall into\n                          any of the variants\n        \"\"\"\n        # We want to always include two control groups, but allow overriding of\n        # their percentages.\n        all_variants = dict(cls.DEFAULT_CONTROL_GROUPS)\n        all_variants.update(variants)\n\n        # Say we have an experiment with two new things we're trying out for 2%\n        # of users (A and B), a control group with 5% (C), and a pool of\n        # excluded users (x).  The buckets will be assigned like so:\n        #\n        #     A B C A B C x x C x x C x x C x x x x x x x x x...\n        #\n        # This scheme allows us to later increase the size of A and B to 7%\n        # while keeping the experience consistent for users in any group other\n        # than excluded users:\n        #\n        #     A B C A B C A B C A B C A B C A B x A B x x x x...\n        #\n        # Rather than building this entire structure out in memory, we can use\n        # a little bit of math to figure out just the one bucket's value.\n        num_variants = len(all_variants)\n        variant_names = sorted(all_variants.keys())\n        # If the variants took up the entire set of buckets, which bucket would\n        # we be in?\n        candidate_variant = variant_names[bucket % num_variants]\n        # Log a warning if this variant is capped, to help us prevent user (us)\n        # error.  It's not the most correct to only check the one, but it's\n        # easy and quick, and anything with that high a percentage should be\n        # selected quite often.\n        variant_fraction = all_variants[candidate_variant] / 100.0\n        variant_cap = 1.0 / num_variants\n        if variant_fraction > variant_cap:\n            g.log.warning(\n                'Variant %s exceeds allowable percentage (%.2f > %.2f)',\n                candidate_variant,\n                variant_fraction,\n                variant_cap,\n            )\n        # Variant percentages are expressed as numeric percentages rather than\n        # a fraction of 1 (that is, 1.5 means 1.5%, not 150%); thus, at 100\n        # buckets, buckets and percents map 1:1 with each other.  Since we may\n        # have more than 100 buckets (causing each bucket to represent less\n        # than 1% each), we need to scale up how far \"right\" we move for each\n        # variant percent.\n        bucket_multiplier = cls.NUM_BUCKETS / 100\n        # Now check to see if we're far enough left to be included in the\n        # variant percentage.\n        if bucket < (all_variants[candidate_variant] * num_variants *\n                     bucket_multiplier):\n            return candidate_variant\n        else:\n            return None\n\n    @classmethod\n    def _is_variant_enabled(cls, variant):\n        \"\"\"Determine if a variant is \"enabled\", as returned by is_enabled.\"\"\"\n        # The excluded experimental group will have a `None` variant and\n        # this feature should be disabled.\n        # For users in control groups, the feature is considered \"not\n        # enabled\" because they should get the same behavior as ineligible\n        # users.\n        return (\n            variant is not None and\n            variant not in cls.DEFAULT_CONTROL_GROUPS\n        )\n\n    def is_enabled(self, user=None, subreddit=None, subdomain=None,\n                   oauth_client=None):\n        \"\"\"Determine if a feature is enabled.\n\n        For experiments, this induces a bucketing event by calling\n        self._is_experiment_enabled.\n        \"\"\"\n        cfg = self.config\n        kw = dict(\n            user=user,\n            subreddit=subreddit,\n            subdomain=subdomain,\n            oauth_client=oauth_client\n        )\n        # first, test if the config would be enabled without an experiment\n        if self._is_config_enabled(cfg, **kw):\n            return True\n\n        # next, test if the config is enabled fractionally\n        if self._is_percent_enabled(cfg, user=user):\n            return True\n\n        # lastly, check experiment\n        experiment = self.config.get('experiment')\n        if self._is_config_enabled(experiment, **kw):\n            return self._is_experiment_enabled(experiment, user=user)\n\n        # Unknown value, default to off.\n        return False\n\n    def _is_config_enabled(\n        self, cfg, user=None, subreddit=None, subdomain=None,\n        oauth_client=None\n    ):\n        world = self.world\n\n        if not cfg:\n            return False\n\n        if cfg.get('enabled') == self.GLOBALLY_ON:\n            return True\n\n        if cfg.get('enabled') == self.GLOBALLY_OFF:\n            return False\n\n        url_flag = cfg.get('url')\n        if url_flag:\n            if isinstance(url_flag, dict):\n                for feature in world.url_features():\n                    if feature in url_flag:\n                        return self._is_variant_enabled(url_flag[feature])\n            elif url_flag in world.url_features():\n                return True\n\n        if cfg.get('admin') and world.is_admin(user):\n            return True\n\n        if cfg.get('employee') and world.is_employee(user):\n            return True\n\n        if cfg.get('beta') and world.user_has_beta_enabled(user):\n            return True\n\n        if cfg.get('gold') and world.has_gold(user):\n            return True\n\n        loggedin = world.is_user_loggedin(user)\n        if cfg.get('loggedin') and loggedin:\n            return True\n\n        if cfg.get('loggedout') and not loggedin:\n            return True\n\n        users = [u.lower() for u in cfg.get('users', [])]\n        if users and user and user.name.lower() in users:\n            return True\n\n        subreddits = [s.lower() for s in cfg.get('subreddits', [])]\n        if subreddits and subreddit and subreddit.lower() in subreddits:\n            return True\n\n        subdomains = [s.lower() for s in cfg.get('subdomains', [])]\n        if subdomains and subdomain and subdomain.lower() in subdomains:\n            return True\n\n        clients = set(cfg.get('oauth_clients', []))\n        if clients and oauth_client and oauth_client in clients:\n            return True\n\n    def _is_percent_enabled(self, cfg, user=None):\n        loggedin = self.world.is_user_loggedin(user)\n        percent_loggedin = cfg.get('percent_loggedin', 0)\n        if percent_loggedin and loggedin:\n            bucket = self._calculate_bucket(user._fullname)\n            scaled_percent = bucket / (self.NUM_BUCKETS / 100)\n            if scaled_percent < percent_loggedin:\n                return True\n\n        percent_loggedout = cfg.get('percent_loggedout', 0)\n        if percent_loggedout and not loggedin:\n            # We want this to match the JS function for bucketing loggedout\n            # users, and JS doesn't make it easy to mix the feature name in\n            # with the LOID. Just look at the last 4 chars of the LOID.\n            loid = self.world.current_loid()\n            if loid:\n                try:\n                    bucket = int(loid[-4:], 36) % 100\n                    if bucket < percent_loggedout:\n                        return True\n                except ValueError:\n                    pass\n\n    def _is_experiment_enabled(self, experiment, user=None):\n        \"\"\" Determine if there's an active variant of the specified experiment\n        for the current user.\n\n        Sends a bucketing event.\n        \"\"\"\n        if not experiment.get('enabled', True):\n            return False\n\n        variant = None\n        if FeatureState.is_user_experiment(experiment):\n            variant = self._get_user_experiment_variant(experiment, user)\n        elif FeatureState.is_page_experiment(experiment):\n            content_id, _ = FeatureState.get_content_id()\n            variant = self._get_page_experiment_variant(experiment)\n\n        # We only want to send this event once per request, because that's\n        # an easy way to get rid of extraneous events.\n        if not c.have_sent_bucketing_event:\n            c.have_sent_bucketing_event = set()\n\n        if variant is not None and self.world.valid_experiment_request():\n            if FeatureState.is_user_experiment(experiment):\n                loid = self.world.current_loid()\n                if self.world.is_user_loggedin(user):\n                    bucketing_id = user._id\n                else:\n                    bucketing_id = loid\n\n                key = ('user', self.name, bucketing_id)\n\n                if (\n                    g.running_as_script or\n                    key not in c.have_sent_bucketing_event\n                ):\n                    g.events.bucketing_event(\n                        experiment_id=experiment.get('experiment_id'),\n                        experiment_name=self.name,\n                        variant=variant,\n                        user=user,\n                        loid=self.world.current_loid_obj(),\n                    )\n                    c.have_sent_bucketing_event.add(key)\n            else:\n                # This is a page experiment, so we know we have a content_id\n                key = ('page', self.name, content_id)\n                if (\n                    g.running_as_script or\n                    key not in c.have_sent_bucketing_event\n                ):\n                    g.events.page_bucketing_event(\n                        experiment_id=experiment.get('experiment_id'),\n                        experiment_name=self.name,\n                        variant=variant,\n                        content_id=content_id,\n                        request=request,\n                        context=c,\n                    )\n                    c.have_sent_bucketing_event.add(key)\n\n        return self._is_variant_enabled(variant)\n\n    def variant(self, user):\n        \"\"\" Determine which variant of this experiment, if any, is active.\n\n        Does not send a bucketing event.\n        \"\"\"\n        url_flag = self.config.get('url')\n        # We only care about the dict-type 'url_flag's, since those are the\n        # only ones that can specify a variant.\n        if url_flag and isinstance(url_flag, dict):\n            for feature in self.world.url_features():\n                try:\n                    return url_flag[feature]\n                except KeyError:\n                    pass\n\n        experiment = self.config.get('experiment')\n        if not experiment:\n            return None\n\n        if FeatureState.is_user_experiment(experiment):\n            return self._get_user_experiment_variant(experiment, user)\n        return self._get_page_experiment_variant(experiment)\n\n    def _get_user_experiment_variant(self, experiment, user):\n        # for logged in users, bucket based on the User's fullname\n        if self.world.is_user_loggedin(user):\n            bucket = self._calculate_bucket(user._fullname)\n        # for logged out users, bucket based on the loid if we have one\n        elif g.enable_loggedout_experiments:\n            loid = self.world.current_loid()\n            # we can't run an experiment if we have no id to vary on.\n            if not loid:\n                return None\n            bucket = self._calculate_bucket(loid)\n        # if logged out experiments are disabled, bail.\n        else:\n            return None\n\n        variant = self._choose_variant(bucket, experiment.get('variants', {}))\n        return variant\n\n    @staticmethod\n    def get_content_id():\n        from r2.lib import utils\n        thing = utils.url_to_thing(request.fullurl)\n\n        if not thing:\n            return None, None\n\n        content_id = None\n        type_name = getattr(thing, '_type_name', None)\n        if type_name == 'comment':\n            # We use the parent link for comment permalink pages, since\n            # they share a canonical URL\n            link = getattr(thing, 'link', thing.link_slow)\n            content_id = link._fullname\n        elif type_name == 'link':\n            content_id = thing._fullname\n        elif type_name == 'subreddit':\n            content_id = thing._fullname\n        return content_id, type_name\n\n    def _get_page_experiment_variant(self, experiment):\n        content_id, type_name = FeatureState.get_content_id()\n\n        if content_id is None:\n            return None\n\n        # If we've restricted the experiment to certain page types, make sure\n        # the request is for one of those\n        if (experiment.get('subreddit_only', False) and\n                type_name != 'subreddit'):\n            return None\n\n        if (experiment.get('link_only', False) and\n                (type_name != 'link' and type_name != 'comment')):\n            # We treat comment permalink pages like general comments pages\n            return None\n\n        experiment_seed = experiment.get('experiment_seed', None)\n        bucket = self._calculate_bucket(content_id, experiment_seed)\n        variant = self._choose_variant(bucket, experiment.get('variants', {}))\n        return variant\n"
  },
  {
    "path": "r2/r2/config/feature/world.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom pylons import request\nfrom pylons import tmpl_context as c\nfrom pylons import app_globals as g\n\n\nclass World(object):\n    \"\"\"A World is the proxy to the app/request state for Features.\n\n    Proxying through World allows for easy testing and caching if needed.\n    \"\"\"\n\n    @staticmethod\n    def stacked_proxy_safe_get(stacked_proxy, key, default=None):\n        \"\"\"Get a field from a StackedObjectProxy\n\n        Always succeeds, even if the proxy has not yet been initialized.\n        Normally, if the proxy hasn't been initialized, a `TypeError` is\n        raised to indicate a programming error. To avoid crashing on feature\n        checks that are done too early (e.g., during initial DB set-up of\n        the pylons environment), this function will instead return `default`\n        for an uninitialized proxy.\n\n        (Initialized proxies ALWAYS return a value, either a set value\n        or an empty string)\n\n        \"\"\"\n        try:\n            return getattr(stacked_proxy, key)\n        except TypeError:\n            return default\n\n    def current_user(self):\n        if c.user_is_loggedin:\n            return self.stacked_proxy_safe_get(c, 'user')\n\n    def current_subreddit(self):\n        site = self.stacked_proxy_safe_get(c, 'site')\n        if not site:\n            # In non-request code (eg queued jobs), there isn't necessarily a\n            # site name (or other request-type data).  In those cases, we don't\n            # want to trigger any subreddit-specific code.\n            return ''\n        return site.name\n\n    def current_subdomain(self):\n        return self.stacked_proxy_safe_get(c, 'subdomain')\n\n    def current_oauth_client(self):\n        client = self.stacked_proxy_safe_get(c, 'oauth2_client', None)\n        return getattr(client, '_id', None)\n\n    def current_loid_obj(self):\n        return self.stacked_proxy_safe_get(c, 'loid')\n\n    def current_loid(self):\n        loid = self.current_loid_obj()\n        if not loid:\n            return None\n        return loid.loid\n\n    def is_admin(self, user):\n        if not user or not hasattr(user, 'name'):\n            return False\n\n        return user.name in self.stacked_proxy_safe_get(g, 'admins', [])\n\n    def is_employee(self, user):\n        if not user:\n            return False\n        return user.employee\n\n    def user_has_beta_enabled(self, user):\n        if not user:\n            return False\n        return user.pref_beta\n\n    def has_gold(self, user):\n        if not user:\n            return False\n\n        return user.gold\n\n    def is_user_loggedin(self, user):\n        if not (user or self.current_user()):\n            return False\n        return True\n\n    def url_features(self):\n        return set(request.GET.getall('feature'))\n\n    def live_config(self, name):\n        live = self.stacked_proxy_safe_get(g, 'live_config', {})\n        return live.get(name)\n\n    def live_config_iteritems(self):\n        live = self.stacked_proxy_safe_get(g, 'live_config', {})\n        return live.iteritems()\n\n    def simple_event(self, name):\n        stats = self.stacked_proxy_safe_get(g, 'stats', None)\n        if stats:\n            return stats.simple_event(name)\n"
  },
  {
    "path": "r2/r2/config/hooks.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\n\ndef register_hooks():\n    \"\"\"Register all known non-plugin hooks. Called on app setup.\"\"\"\n    from r2.config.feature.feature import feature_hooks\n    feature_hooks.register_all()\n\n    from r2.models.admintools import admintools_hooks\n    admintools_hooks.register_all()\n\n    from r2.models.account import trylater_hooks\n    trylater_hooks.register_all()\n\n    from r2.models import subreddit\n    subreddit.trylater_hooks.register_all()\n"
  },
  {
    "path": "r2/r2/config/middleware.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\n\"\"\"Pylons middleware initialization\"\"\"\nimport importlib\nimport re\nimport urllib\nimport tempfile\nimport urlparse\nfrom threading import Lock\nimport itertools\nimport simplejson\n\nfrom paste.cascade import Cascade\nfrom paste.errordocument import StatusBasedForward\nfrom paste.recursive import RecursiveMiddleware\nfrom paste.registry import RegistryManager\nfrom paste.urlparser import StaticURLParser\nfrom paste.deploy.converters import asbool\nfrom paste.request import path_info_split\nfrom pylons import response\nfrom pylons.middleware import ErrorHandler\nfrom pylons.wsgiapp import PylonsApp\nfrom routes.middleware import RoutesMiddleware\n\nfrom r2.config import hooks\nfrom r2.config.environment import load_environment\nfrom r2.config.extensions import extension_mapping, set_extension\nfrom r2.lib.utils import is_subdomain, is_language_subdomain\nfrom r2.lib import csrf, filters\n\n\n# patch in WebOb support for HTTP 429 \"Too Many Requests\"\nimport webob.exc\nimport webob.util\n\nclass HTTPTooManyRequests(webob.exc.HTTPClientError):\n    code = 429\n    title = 'Too Many Requests'\n    explanation = ('The server has received too many requests from the client.')\n\nwebob.exc.status_map[429] = HTTPTooManyRequests\nwebob.util.status_reasons[429] = HTTPTooManyRequests.title\n\n# patch out SSRFable/XSSable endpoints in older versions of weberror\nimport weberror.evalexception\n\n\n# We could probably just set `.exposed = False`, but this makes me feel better\ndef _stub(*args, **kwargs):\n    pass\n\nweberror.evalexception.EvalException.post_traceback = _stub\nweberror.evalexception.EvalException.relay = _stub\n\n\ndef error_mapper(code, message, environ, global_conf=None, **kw):\n    if environ.get('pylons.error_call'):\n        return None\n    else:\n        environ['pylons.error_call'] = True\n\n    from pylons import tmpl_context as c\n\n    if global_conf is None:\n        global_conf = {}\n    codes = [304, 400, 401, 403, 404, 409, 415, 429, 503]\n    if not asbool(global_conf.get('debug')):\n        codes.append(500)\n    if code in codes:\n        # StatusBasedForward expects a relative URL (no SCRIPT_NAME)\n        d = dict(code = code, message = message)\n\n        exception = environ.get('r2.controller.exception')\n        if exception:\n            d['explanation'] = exception.explanation\n            error_data = getattr(exception, 'error_data', None)\n            if error_data:\n                environ['extra_error_data'] = error_data\n\n        if environ.get('REDDIT_NAME'):\n            d['srname'] = environ.get('REDDIT_NAME')\n        if environ.get('REDDIT_TAKEDOWN'):\n            d['takedown'] = environ.get('REDDIT_TAKEDOWN')\n        if environ.get('REDDIT_ERROR_NAME'):\n            d['error_name'] = environ.get('REDDIT_ERROR_NAME')\n\n        # preserve x-frame-options when 304ing\n        if code == 304:\n            d['allow_framing'] = 1 if c.allow_framing else 0\n\n        extension = environ.get(\"extension\")\n        if extension:\n            url = '/error/document/.%s?%s' % (extension, urllib.urlencode(d))\n        else:\n            url = '/error/document/?%s' % (urllib.urlencode(d))\n        return url\n\n\n# from pylons < 1.0\ndef ErrorDocuments(app, global_conf, mapper, **kw):\n    \"\"\"Wraps the app in error docs using Paste RecursiveMiddleware and\n    ErrorDocumentsMiddleware\n    \"\"\"\n    if global_conf is None:\n        global_conf = {}\n\n    return RecursiveMiddleware(StatusBasedForward(\n        app, global_conf=global_conf, mapper=mapper, **kw))\n\n\nclass ProfilingMiddleware(object):\n    def __init__(self, app, directory):\n        self.app = app\n        self.directory = directory\n\n    def __call__(self, environ, start_response):\n        import cProfile\n\n        try:\n            tmpfile = tempfile.NamedTemporaryFile(prefix='profile',\n                                                  dir=self.directory,\n                                                  delete=False)\n\n            profile = cProfile.Profile()\n            result = profile.runcall(self.app, environ, start_response)\n            profile.dump_stats(tmpfile.name)\n\n            return result\n        finally:\n            tmpfile.close()\n\n\nclass DomainMiddleware(object):\n\n    def __init__(self, app, config):\n        self.app = app\n        self.config = config\n\n    def __call__(self, environ, start_response):\n        g = self.config['pylons.app_globals']\n        http_host = environ.get('HTTP_HOST', 'localhost').lower()\n        domain, s, port = http_host.partition(':')\n\n        # remember the port\n        try:\n            environ['request_port'] = int(port)\n        except ValueError:\n            pass\n\n        # localhost is exempt so paster run/shell will work\n        # media_domain doesn't need special processing since it's just ads\n        is_media_only_domain = (is_subdomain(domain, g.media_domain) and\n                                g.domain != g.media_domain)\n        if domain == \"localhost\" or is_media_only_domain:\n            return self.app(environ, start_response)\n\n        # tell reddit_base to redirect to the appropriate subreddit for\n        # a legacy CNAME\n        if not is_subdomain(domain, g.domain):\n            environ['legacy-cname'] = domain\n            return self.app(environ, start_response)\n\n        # How many characters to chop off the end of the hostname before\n        # we start looking at subdomains\n        ignored_suffix_len = len(g.domain)\n\n        # figure out what subdomain we're on, if any\n        subdomains = domain[:-ignored_suffix_len - 1].split('.')\n\n        sr_redirect = None\n        prefix_parts = []\n        for subdomain in subdomains[:]:\n            extension = g.extension_subdomains.get(subdomain)\n            # These subdomains are reserved, don't treat them as SR\n            # or language subdomains.\n            if subdomain in g.reserved_subdomains:\n                # Some subdomains are reserved, but also can't be mixed into\n                # the domain prefix for various reasons (permalinks will be\n                # broken, etc.)\n                if subdomain in g.ignored_subdomains:\n                    continue\n                prefix_parts.append(subdomain)\n            elif extension:\n                environ['reddit-domain-extension'] = extension\n            elif is_language_subdomain(subdomain):\n                environ['reddit-prefer-lang'] = subdomain\n            else:\n                sr_redirect = subdomain\n                subdomains.remove(subdomain)\n\n        if 'reddit-prefer-lang' in environ:\n            prefix_parts.insert(0, environ['reddit-prefer-lang'])\n        if prefix_parts:\n            environ['reddit-domain-prefix'] = '.'.join(prefix_parts)\n\n        # if there was a subreddit subdomain, redirect\n        if sr_redirect and environ.get(\"FULLPATH\"):\n            if not subdomains and g.domain_prefix:\n                subdomains.append(g.domain_prefix)\n            subdomains.append(g.domain)\n            redir = \"%s/r/%s/%s\" % ('.'.join(subdomains),\n                                    sr_redirect, environ['FULLPATH'])\n            redir = g.default_scheme + \"://\" + redir.replace('//', '/')\n\n            start_response(\"301 Moved Permanently\", [(\"Location\", redir)])\n            return [\"\"]\n\n        return self.app(environ, start_response)\n\n\nclass SubredditMiddleware(object):\n    sr_pattern = re.compile(r'^/r/([^/]{2,})')\n\n    def __init__(self, app):\n        self.app = app\n\n    def __call__(self, environ, start_response):\n        path = environ['PATH_INFO']\n        sr = self.sr_pattern.match(path)\n        if sr:\n            environ['subreddit'] = sr.groups()[0]\n            environ['PATH_INFO'] = self.sr_pattern.sub('', path) or '/'\n        return self.app(environ, start_response)\n\n\nclass DomainListingMiddleware(object):\n    def __init__(self, app):\n        self.app = app\n\n    def __call__(self, environ, start_response):\n        if not environ.has_key('subreddit'):\n            path = environ['PATH_INFO']\n            domain, rest = path_info_split(path)\n            if domain == \"domain\" and rest:\n                domain, rest = path_info_split(rest)\n                environ['domain'] = domain\n                environ['PATH_INFO'] = rest or '/'\n        return self.app(environ, start_response)\n\n\nclass ExtensionMiddleware(object):\n    ext_pattern = re.compile(r'\\.([^/]+)\\Z')\n\n    def __init__(self, app):\n        self.app = app\n\n    def __call__(self, environ, start_response):\n        path = environ['PATH_INFO']\n        fname, sep, path_ext = path.rpartition('.')\n        domain_ext = environ.get('reddit-domain-extension')\n\n        ext = None\n        if path_ext in extension_mapping:\n            ext = path_ext\n            # Strip off the extension.\n            environ['PATH_INFO'] = path[:-(len(ext) + 1)]\n        elif domain_ext in extension_mapping:\n            ext = domain_ext\n\n        if ext:\n            set_extension(environ, ext)\n        else:\n            environ['render_style'] = 'html'\n            environ['content_type'] = 'text/html; charset=UTF-8'\n\n        return self.app(environ, start_response)\n\nclass FullPathMiddleware(object):\n    # Debt: we have a lot of middleware which (unfortunately) modify the\n    # global URL PATH_INFO string. To work with the original request URL, we\n    # save it to a different location here.\n    def __init__(self, app):\n        self.app = app\n\n    def __call__(self, environ, start_response):\n        environ['FULLPATH'] = environ.get('PATH_INFO')\n        qs = environ.get('QUERY_STRING')\n        if qs:\n            environ['FULLPATH'] += '?' + qs\n        return self.app(environ, start_response)\n\nclass StaticTestMiddleware(object):\n    def __init__(self, app, static_path, domain):\n        self.app = app\n        self.static_path = static_path\n        self.domain = domain\n\n    def __call__(self, environ, start_response):\n        if environ['HTTP_HOST'] == self.domain:\n            environ['PATH_INFO'] = self.static_path.rstrip('/') + environ['PATH_INFO']\n            return self.app(environ, start_response)\n        raise webob.exc.HTTPNotFound()\n\n\ndef _wsgi_json(start_response, status_int, message=\"\"):\n    status_message = webob.util.status_reasons[status_int]\n    message = message or status_message\n\n    start_response(\n        \"%s %s\" % (status_int, status_message),\n        [(\"Content-Type\", \"application/json\")])\n\n    data = simplejson.dumps({\n        \"error\": status_int,\n        \"message\": message\n    })\n    return [filters.websafe_json(data).encode(\"utf-8\")]\n\n\nclass LimitUploadSize(object):\n    \"\"\"\n    Middleware for restricting the size of uploaded files (such as\n    image files for the CSS editing capability).\n    \"\"\"\n    def __init__(self, app, max_size=1024*500):\n        self.app = app\n        self.max_size = max_size\n\n    def __call__(self, environ, start_response):\n        cl_key = 'CONTENT_LENGTH'\n        is_error = environ.get(\"pylons.error_call\", False)\n        is_api = environ.get(\"render_style\").startswith(\"api\")\n        if not is_error and environ['REQUEST_METHOD'] == 'POST':\n            if cl_key not in environ:\n\n                if is_api:\n                    return _wsgi_json(start_response, 411)\n                else:\n                    start_response(\"411 Length Required\", [])\n                    return ['<html><body>length required</body></html>']\n\n            try:\n                cl_int = int(environ[cl_key])\n            except ValueError:\n                if is_api:\n                    return _wsgi_json(start_response, 400)\n                else:\n                    start_response(\"400 Bad Request\", [])\n                    return ['<html><body>bad request</body></html>']\n\n            if cl_int > self.max_size:\n                error_msg = \"too big. keep it under %d KiB\" % (\n                    self.max_size / 1024)\n\n                if is_api:\n                    return _wsgi_json(start_response, 413, error_msg)\n                else:\n                    start_response(\"413 Too Big\", [])\n                    return [\"<html>\"\n                            \"<head>\"\n                            \"<script type='text/javascript'>\"\n                            \"parent.completedUploadImage('failed',\"\n                            \"'',\"\n                            \"'',\"\n                            \"[['BAD_CSS_NAME', ''], ['IMAGE_ERROR', '\", error_msg,\"']],\"\n                            \"'');\"\n                            \"</script></head><body>you shouldn\\'t be here</body></html>\"]\n\n        return self.app(environ, start_response)\n\n# TODO CleanupMiddleware seems to exist because cookie headers are being duplicated\n# somewhere in the response processing chain. It should be removed as soon as we\n# find the underlying issue.\nclass CleanupMiddleware(object):\n    \"\"\"\n    Put anything here that should be called after every other bit of\n    middleware. This currently includes the code for removing\n    duplicate headers (such as multiple cookie setting).  The behavior\n    here is to disregard all but the last record.\n    \"\"\"\n    def __init__(self, app):\n        self.app = app\n\n    def __call__(self, environ, start_response):\n        def custom_start_response(status, headers, exc_info = None):\n            fixed = []\n            seen = set()\n            for head, val in reversed(headers):\n                head = head.lower()\n                key = (head, val.split(\"=\", 1)[0])\n                if key not in seen:\n                    fixed.insert(0, (head, val))\n                    seen.add(key)\n            return start_response(status, fixed, exc_info)\n        return self.app(environ, custom_start_response)\n\n\nclass SafetyMiddleware(object):\n    \"\"\"Clean up any attempts at response splitting in headers.\"\"\"\n\n    has_bad_characters = re.compile(\"[\\r\\n]\")\n    sanitizer = re.compile(\"[\\r\\n]+[ \\t]*\")\n\n    def __init__(self, app):\n        self.app = app\n\n    def __call__(self, environ, start_response):\n        def safe_start_response(status, headers, exc_info=None):\n            sanitized = []\n            for name, value in headers:\n                if self.has_bad_characters.search(value):\n                    value = self.sanitizer.sub(\"\", value)\n                sanitized.append((name, value))\n            return start_response(status, sanitized, exc_info)\n        return self.app(environ, safe_start_response)\n\n\nclass RedditApp(PylonsApp):\n\n    test_mode = False\n\n    def __init__(self, *args, **kwargs):\n        super(RedditApp, self).__init__(*args, **kwargs)\n        self._loading_lock = Lock()\n        self._controllers = None\n        self._hooks_registered = False\n\n    def setup_app_env(self, environ, start_response):\n        PylonsApp.setup_app_env(self, environ, start_response)\n\n        if not self.test_mode:\n            if self._controllers and self._hooks_registered:\n                return\n\n            with self._loading_lock:\n                self.load_controllers()\n                self.register_hooks()\n\n    def _check_csrf_prevention(self):\n        from r2 import controllers\n        from pylons import app_globals as g\n\n        if not g.running_as_script:\n            controllers_iter = itertools.chain(\n                controllers._reddit_controllers.itervalues(),\n                controllers._plugin_controllers.itervalues(),\n            )\n            for controller in controllers_iter:\n                csrf.check_controller_csrf_prevention(controller)\n\n    def load_controllers(self):\n        if self._controllers:\n            return\n\n        controllers = importlib.import_module(self.package_name +\n                                              '.controllers')\n        controllers.load_controllers()\n        self.config['r2.plugins'].load_controllers()\n        self._controllers = controllers\n        self._check_csrf_prevention()\n\n    def register_hooks(self):\n        if self._hooks_registered:\n            return\n\n        hooks.register_hooks()\n        self._hooks_registered = True\n\n    def find_controller(self, controller_name):\n        if controller_name in self.controller_classes:\n            return self.controller_classes[controller_name]\n\n        controller_cls = self._controllers.get_controller(controller_name)\n        self.controller_classes[controller_name] = controller_cls\n        return controller_cls\n\ndef make_app(global_conf, full_stack=True, **app_conf):\n    \"\"\"Create a Pylons WSGI application and return it\n\n    `global_conf`\n        The inherited configuration for this application. Normally from the\n        [DEFAULT] section of the Paste ini file.\n\n    `full_stack`\n        Whether or not this application provides a full WSGI stack (by default,\n        meaning it handles its own exceptions and errors). Disable full_stack\n        when this application is \"managed\" by another WSGI middleware.\n\n    `app_conf`\n        The application's local configuration. Normally specified in the\n        [app:<name>] section of the Paste ini file (where <name> defaults to\n        main).\n    \"\"\"\n\n    # Configure the Pylons environment\n    config = load_environment(global_conf, app_conf)\n    g = config['pylons.app_globals']\n\n    # The Pylons WSGI app\n    app = RedditApp(config=config)\n    app = RoutesMiddleware(app, config[\"routes.map\"])\n\n    # CUSTOM MIDDLEWARE HERE (filtered by the error handling middlewares)\n\n    # last thing first from here down\n    app = CleanupMiddleware(app)\n\n    app = LimitUploadSize(app)\n\n    profile_directory = g.config.get('profile_directory')\n    if profile_directory:\n        app = ProfilingMiddleware(app, profile_directory)\n\n    app = DomainListingMiddleware(app)\n    app = SubredditMiddleware(app)\n    app = ExtensionMiddleware(app)\n    app = DomainMiddleware(app, config=config)\n\n    if asbool(full_stack):\n        # Handle Python exceptions\n        app = ErrorHandler(app, global_conf, **config['pylons.errorware'])\n\n        # Display error documents for 401, 403, 404 status codes (and 500 when\n        # debug is disabled)\n        app = ErrorDocuments(app, global_conf, error_mapper, **app_conf)\n\n    # Establish the Registry for this application\n    app = RegistryManager(app)\n\n    # Static files\n    static_app = StaticURLParser(config['pylons.paths']['static_files'])\n    static_cascade = [static_app, app]\n\n    if config['r2.plugins'] and g.config['uncompressedJS']:\n        plugin_static_apps = Cascade([StaticURLParser(plugin.static_dir)\n                                      for plugin in config['r2.plugins']])\n        static_cascade.insert(0, plugin_static_apps)\n    app = Cascade(static_cascade)\n\n    app = FullPathMiddleware(app)\n\n    if not g.config['uncompressedJS'] and g.config['debug']:\n        static_fallback = StaticTestMiddleware(static_app, g.config['static_path'], g.config['static_domain'])\n        app = Cascade([static_fallback, app])\n\n    app = SafetyMiddleware(app)\n\n    app.config = config\n\n    return app\n"
  },
  {
    "path": "r2/r2/config/paths.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport os.path\n\n\ndef get_r2_path():\n    # we know this file is at r2/r2/config/paths.py\n    this_path = os.path.abspath(__file__)\n    # walk up 3 directories to r2\n    r2_path = os.path.dirname(os.path.dirname(os.path.dirname(this_path)))\n    return r2_path\n\n\ndef get_built_statics_path():\n    \"\"\"Return the path for built (compiled/compressed) statics.\"\"\"\n    r2_path = get_r2_path()\n    return os.path.join(r2_path, 'build', 'public')\n\n\ndef get_raw_statics_path():\n    \"\"\"Return the path for the raw (under version control) statics\"\"\"\n    r2_path = get_r2_path()\n    return os.path.join(r2_path, 'r2', 'public')\n"
  },
  {
    "path": "r2/r2/config/queues.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom r2.lib.utils import tup\n\n\n__all__ = [\"MessageQueue\", \"declare_queues\"]\n\n\nclass Queues(dict):\n    \"\"\"A container for queue declarations.\"\"\"\n    def __init__(self, queues):\n        dict.__init__(self)\n        self.__dict__ = self\n        self.bindings = set()\n        self.declare(queues)\n\n    def __iter__(self):\n        for name, queue in self.iteritems():\n            if name != \"bindings\":\n                yield queue\n\n    def declare(self, queues):\n        for name, queue in queues.iteritems():\n            queue.name = name\n            queue.bindings = self.bindings\n            if queue.bind_to_self:\n                queue._bind(name)\n        self.update(queues)\n\n\nclass MessageQueue(object):\n    \"\"\"A representation of an AMQP message queue.\n\n    This class is solely intended for use with the Queues class above.\n\n    \"\"\"\n    def __init__(self, durable=True, exclusive=False,\n                 auto_delete=False, bind_to_self=False):\n        self.durable = durable\n        self.exclusive = exclusive\n        self.auto_delete = auto_delete\n        self.bind_to_self = bind_to_self\n\n    def _bind(self, routing_key):\n        self.bindings.add((self.name, routing_key))\n\n    def __lshift__(self, routing_keys):\n        \"\"\"Register bindings from routing keys to this queue.\"\"\"\n        routing_keys = tup(routing_keys)\n        for routing_key in routing_keys:\n            self._bind(routing_key)\n\n\ndef declare_queues(g):\n    queues = Queues({\n        \"scraper_q\": MessageQueue(bind_to_self=True),\n        \"newcomments_q\": MessageQueue(),\n        \"commentstree_q\": MessageQueue(bind_to_self=True),\n        \"commentstree_fastlane_q\": MessageQueue(bind_to_self=True),\n        \"vote_link_q\": MessageQueue(bind_to_self=True),\n        \"vote_comment_q\": MessageQueue(bind_to_self=True),\n        \"cloudsearch_changes\": MessageQueue(bind_to_self=True),\n        \"butler_q\": MessageQueue(),\n        \"markread_q\": MessageQueue(),\n        \"del_account_q\": MessageQueue(),\n        \"automoderator_q\": MessageQueue(),\n        \"event_collector\": MessageQueue(bind_to_self=True),\n        \"event_collector_failed\": MessageQueue(bind_to_self=True),\n        \"modmail_email_q\": MessageQueue(bind_to_self=True),\n        \"author_query_q\": MessageQueue(bind_to_self=True),\n        \"subreddit_query_q\": MessageQueue(bind_to_self=True),\n        \"domain_query_q\": MessageQueue(bind_to_self=True),\n    })\n\n    if g.shard_commentstree_queues:\n        sharded_commentstree_queues = {\"commentstree_%d_q\" % i :\n                                       MessageQueue(bind_to_self=True)\n                                       for i in xrange(10)}\n        queues.declare(sharded_commentstree_queues)\n\n    if g.shard_author_query_queues:\n        sharded_author_query_queues = {\n            \"author_query_%d_q\" % i: MessageQueue(bind_to_self=True)\n            for i in xrange(10)\n        }\n        queues.declare(sharded_author_query_queues)\n\n    if g.shard_subreddit_query_queues:\n        sharded_subreddit_query_queues = {\n            \"subreddit_query_%d_q\" % i: MessageQueue(bind_to_self=True)\n            for i in xrange(10)\n        }\n        queues.declare(sharded_subreddit_query_queues)\n\n    if g.shard_domain_query_queues:\n        sharded_domain_query_queues = {\n            \"domain_query_%d_q\" % i: MessageQueue(bind_to_self=True)\n            for i in xrange(10)\n        }\n        queues.declare(sharded_domain_query_queues)\n\n    queues.cloudsearch_changes << \"search_changes\"\n    queues.scraper_q << (\"new_link\", \"link_text_edited\")\n    queues.newcomments_q << \"new_comment\"\n    queues.butler_q << (\"new_comment\",\n                        \"comment_text_edited\")\n    queues.markread_q << \"mark_all_read\"\n    queues.del_account_q << \"account_deleted\"\n    queues.automoderator_q << (\n        \"auto_removed\",\n        \"new_link\",\n        \"new_comment\",\n        \"new_media_embed\",\n        \"new_report\",\n        \"link_text_edited\",\n        \"comment_text_edited\",\n    )\n    queues.event_collector << \"event_collector_test\"\n\n    return queues\n"
  },
  {
    "path": "r2/r2/config/routing.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\n\"\"\"\nSetup your Routes options here\n\"\"\"\nfrom routes import Mapper\n\n\ndef not_in_sr(environ, results):\n    return ('subreddit' not in environ and\n            'sub_domain' not in environ and\n            'domain' not in environ)\n\n\n# FIXME: submappers with path prefixes are broken in Routes 1.11. Once we\n# upgrade, we should be able to replace this ugliness with submappers.\ndef partial_connect(mc, **override_args):\n    def connect(path, **kwargs):\n        if 'path_prefix' in override_args:\n            path = override_args['path_prefix'] + path\n        kwargs.update(override_args)\n        mc(path, **kwargs)\n    return connect\n\n\ndef make_map(config):\n    map = Mapper(explicit=False)\n    map.minimization = True\n    mc = map.connect\n\n    # Username-relative userpage redirects, need to be defined here in case\n    # a plugin defines a `/user/:name` handler.\n    mc('/user/me', controller='user', action='rel_user_redirect')\n    mc('/user/me/*rest', controller='user', action='rel_user_redirect')\n\n    for plugin in reversed(config['r2.plugins']):\n        plugin.add_routes(mc)\n\n    mc('/admin/', controller='awards')\n\n    mc('/robots.txt', controller='robots', action='robots')\n    mc('/crossdomain', controller='robots', action='crossdomain')\n\n    mc('/login', controller='forms', action='login')\n    mc('/register', controller='forms', action='register')\n    mc('/logout', controller='forms', action='logout')\n    mc('/verify', controller='forms', action='verify')\n    mc('/adminon', controller='forms', action='adminon')\n    mc('/adminoff', controller='forms', action='adminoff')\n    mc('/submit', controller='front', action='submit')\n\n    # redirect old urls to the new\n    ABOUT_BASE = \"https://about.reddit.com/\"\n    mc('/about', controller='redirect', action='redirect', dest=ABOUT_BASE, \n       conditions={'function':not_in_sr})\n    mc('/about/values', controller='redirect', action='redirect', dest=ABOUT_BASE)\n    mc('/about/team', controller='redirect', action='redirect',\n       dest=ABOUT_BASE)\n    mc('/about/alien', controller='redirect', action='redirect',\n       dest=ABOUT_BASE + \"press\")\n    mc('/jobs', controller='redirect', action='redirect',\n       dest=ABOUT_BASE + \"careers\")\n\n    mc('/over18', controller='post', action='over18')\n    mc('/quarantine', controller='post', action='quarantine')\n    mc('/quarantine_optout', controller='api', action='quarantine_optout')\n\n    mc('/traffic', controller='front', action='site_traffic')\n    mc('/traffic/languages/:langcode', controller='front',\n       action='lang_traffic', langcode='')\n    mc('/traffic/adverts/:code', controller='front',\n       action='advert_traffic', code='')\n    mc('/traffic/subreddits/report', controller='front',\n       action='subreddit_traffic_report')\n    mc('/account-activity', controller='front', action='account_activity')\n\n    mc('/subreddits/create', controller='front', action='newreddit')\n    mc('/subreddits/search', controller='front', action='search_reddits')\n    mc('/subreddits/login', controller='forms', action='login')\n    mc('/subreddits/:where', controller='reddits', action='listing',\n       where='popular', conditions={'function':not_in_sr},\n       requirements=dict(where=\"popular|new|banned|employee|gold|default|\"\n                               \"quarantine|featured\"))\n    # If no subreddit is specified, might as well show a list of 'em.\n    mc('/r', controller='redirect', action='redirect', dest='/subreddits')\n\n    mc('/subreddits/mine/:where', controller='myreddits', action='listing',\n       where='subscriber', conditions={'function':not_in_sr},\n       requirements=dict(where='subscriber|contributor|moderator'))\n\n    # These routes are kept for backwards-compatibility reasons\n    # Using the above /subreddits/ ones instead is preferable\n    mc('/reddits/create', controller='front', action='newreddit')\n    mc('/reddits/search', controller='front', action='search_reddits')\n    mc('/reddits/login', controller='forms', action='login')\n    mc('/reddits/:where', controller='reddits', action='listing',\n       where='popular', conditions={'function':not_in_sr},\n       requirements=dict(where=\"popular|new|banned\"))\n\n    mc('/reddits/mine/:where', controller='myreddits', action='listing',\n       where='subscriber', conditions={'function':not_in_sr},\n       requirements=dict(where='subscriber|contributor|moderator'))\n\n    mc('/buttons', controller='buttons', action='button_demo_page')\n\n    #/button.js and buttonlite.js - the embeds\n    mc('/button', controller='buttons', action='button_embed')\n    mc('/buttonlite', controller='buttons', action='button_lite')\n\n    mc('/widget', controller='buttons', action='widget_demo_page')\n\n    mc('/awards', controller='front', action='awards')\n    mc('/awards/confirm/:code', controller='front',\n       action='confirm_award_claim')\n    mc('/awards/claim/:code', controller='front', action='claim_award')\n    mc('/awards/received', controller='front', action='received_award')\n\n    mc('/i18n', controller='redirect', action='redirect',\n       dest='https://www.reddit.com/r/i18n')\n    mc('/feedback', controller='redirect', action='redirect',\n       dest='/contact')\n    mc('/contact', controller='frontunstyled', action='contact_us')\n\n    mc('/admin/awards', controller='awards')\n    mc('/admin/awards/:awardcn/:action', controller='awards',\n       requirements=dict(action=\"give|winners\"))\n\n    mc('/admin/creddits', controller='admintool', action='creddits')\n    mc('/admin/gold', controller='admintool', action='gold')\n\n    mc('/user/:username/about', controller='user', action='about',\n       where='overview')\n    mc('/user/:username/trophies', controller='user', action='trophies')\n    mc('/user/:username/:where', controller='user', action='listing',\n       where='overview')\n    mc('/user/:username/saved/:category', controller='user', action='listing',\n       where='saved')\n\n    multi_prefixes = (\n       partial_connect(mc, path_prefix='/user/:username/m/:multipath'),\n       partial_connect(mc, path_prefix='/me/m/:multipath', my_multi=True),\n       partial_connect(mc, path_prefix='/me/f/:filtername'),\n    )\n\n    for connect in multi_prefixes:\n       connect('/', controller='hot', action='listing')\n       connect('/submit', controller='front', action='submit')\n       connect('/:sort', controller='browse', sort='top',\n          action='listing', requirements=dict(sort='top|controversial'))\n       connect('/:controller', action='listing',\n          requirements=dict(controller=\"hot|new|rising|randomrising|ads\"))\n\n    mc('/user/:username/:where/:show', controller='user', action='listing')\n    \n    mc('/explore', controller='front', action='explore')\n    mc('/api/recommend/feedback', controller='api', action='rec_feedback')\n\n    mc(\"/newsletter\", controller=\"newsletter\", action=\"newsletter\")\n\n    mc(\"/gtm/jail\", controller=\"googletagmanager\", action=\"jail\")\n    mc(\"/gtm\", controller=\"googletagmanager\", action=\"gtm\")\n\n    mc('/oembed', controller='oembed', action='oembed')\n\n    mc('/about/rules', controller='front', action='rules')\n    mc('/about/sidebar', controller='front', action='sidebar')\n    mc('/about/sticky', controller='front', action='sticky')\n    mc('/about/flair', controller='front', action='flairlisting')\n    mc('/about', controller='front', action='about')\n    for connect in (mc,) + multi_prefixes:\n       connect('/about/message/:where', controller='message',\n          action='listing')\n       connect('/about/log', controller='front', action='moderationlog')\n       connect('/about/:location', controller='front',\n          action='spamlisting',\n          requirements=dict(location='reports|spam|modqueue|unmoderated|edited'))\n       connect('/about/:where', controller='userlistlisting',\n          requirements=dict(where='contributors|banned|muted|wikibanned|'\n              'wikicontributors|moderators'), action='listing')\n       connect('/about/:location', controller='front', action='editreddit',\n          requirements=dict(location='edit|stylesheet|traffic|about'))\n       connect('/comments', controller='comments', action='listing')\n       connect('/comments/gilded', action='listing', controller='gilded')\n       connect('/gilded', action='listing', controller='gilded')\n       connect('/search', controller='front', action='search')\n\n    mc('/u/:username', controller='redirect', action='user_redirect')\n    mc('/u/:username/*rest', controller='redirect', action='user_redirect')\n\n    # preserve timereddit URLs from 4/1/2012\n    mc('/t/:timereddit', controller='redirect', action='timereddit_redirect')\n    mc('/t/:timereddit/*rest', controller='redirect',\n       action='timereddit_redirect')\n\n    # /prefs/friends is also aliased to /api/v1/me/friends\n    mc('/prefs/:where', controller='userlistlisting',\n        action='user_prefs', requirements=dict(where='blocked|friends'))\n    mc('/prefs/:location', controller='forms', action='prefs',\n       location='options')\n\n    mc('/info/0:article/*rest', controller='front',\n       action='oldinfo', dest='comments', type='ancient')\n    mc('/info/:article/:dest/:comment', controller='front',\n       action='oldinfo', type='old', dest='comments', comment=None)\n\n\n    mc('/related/:article/:title', controller='front',\n       action='related', title=None)\n    mc('/details/:article/:title', controller='front',\n       action='details', title=None)\n    mc('/traffic/:link/:campaign', controller='front', action='traffic',\n       campaign=None)\n    mc('/comments/:article/:title/:comment', controller='front',\n       action='comments', title=None, comment=None)\n    mc('/duplicates/:article/:title', controller='front',\n       action='duplicates', title=None)\n\n    mc('/mail/optout', controller='forms', action='optout')\n    mc('/mail/optin', controller='forms', action='optin')\n    mc('/mail/unsubscribe/:user/:key', controller='forms',\n       action='unsubscribe_emails')\n    mc('/stylesheet', controller='front', action='stylesheet')\n\n    mc('/share/close', controller='front', action='share_close')\n\n    # sponsor endpoints\n    mc('/sponsor/report', controller='sponsor', action='report')\n    mc('/sponsor/inventory', controller='sponsor', action='promote_inventory')\n    mc('/sponsor/lookup_user', controller='sponsor', action=\"lookup_user\")\n\n    # sponsor listings\n    mc('/sponsor/promoted/:sort', controller='sponsorlisting', action='listing',\n       requirements=dict(sort=\"future_promos|pending_promos|unpaid_promos|\"\n                              \"rejected_promos|live_promos|edited_live_promos|\"\n                              \"underdelivered|reported|house|fraud|all|\"\n                              \"unapproved_campaigns|by_platform\"))\n    mc('/sponsor', controller='sponsorlisting', action=\"listing\",\n       sort=\"all\")\n    mc('/sponsor/promoted/', controller='sponsorlisting', action=\"listing\",\n       sort=\"all\")\n    mc('/sponsor/promoted/live_promos/:sr', controller='sponsorlisting',\n       sort='live_promos', action='listing')\n\n\n    # listings of user's promos\n    mc('/promoted/:sort', controller='promotelisting', action=\"listing\",\n       requirements=dict(sort=\"future_promos|pending_promos|unpaid_promos|\"\n                              \"rejected_promos|live_promos|edited_live_promos|\"\n                              \"all\"))\n    mc('/promoted/', controller='promotelisting', action=\"listing\", sort=\"all\")\n\n    # editing endpoints\n    mc('/promoted/new_promo', controller='promote', action='new_promo')\n    mc('/promoted/edit_promo/:link', controller='promote', action='edit_promo')\n    mc('/promoted/pay/:link/:campaign', controller='promote', action='pay')\n    mc('/promoted/refund/:link/:campaign', controller='promote',\n       action='refund')\n\n    mc('/health', controller='health', action='health')\n    mc('/health/ads', controller='health', action='promohealth')\n    mc('/health/caches', controller='health', action='cachehealth')\n\n    mc('/', controller='hot', action='listing')\n\n    mc('/:controller', action='listing',\n       requirements=dict(controller=\"hot|new|rising|randomrising|ads\"))\n    mc('/saved', controller='user', action='saved_redirect')\n\n    mc('/by_id/:names', controller='byId', action='listing')\n\n    mc('/:sort', controller='browse', sort='top', action='listing',\n       requirements=dict(sort='top|controversial'))\n\n    mc('/message/compose', controller='message', action='compose')\n    mc('/message/messages/:mid', controller='message', action='listing',\n       where=\"messages\")\n    mc('/message/:where', controller='message', action='listing')\n    mc('/message/moderator/:subwhere', controller='message', action='listing',\n       where='moderator')\n\n    mc('/thanks', controller='forms', action=\"claim\", secret='')\n    mc('/thanks/:secret', controller='forms', action=\"claim\")\n\n    mc('/gold', controller='forms', action=\"gold\", is_payment=False)\n    mc('/gold/payment', controller='forms', action=\"gold\", is_payment=True)\n    mc('/gold/creditgild/:passthrough', controller='forms', action='creditgild')\n    mc('/gold/thanks', controller='front', action='goldthanks')\n    mc('/gold/subscription', controller='forms', action='subscription')\n    mc('/gilding', controller='front', action='gilding')\n    mc('/creddits', controller='redirect', action='redirect', \n       dest='/gold?goldtype=creddits')\n\n    mc('/password', controller='forms', action=\"password\")\n    mc('/random', controller='front', action=\"random\")\n    mc('/:action', controller='embed',\n       requirements=dict(action=\"blog\"))\n    mc('/help/gold', controller='redirect', action='redirect',\n       dest='/gold/about')\n\n    mc('/help/:page', controller='policies', action='policy_page',\n       conditions={'function':not_in_sr},\n       requirements={'page':'contentpolicy|privacypolicy|useragreement'})\n    mc('/rules', controller='redirect', action='redirect',\n        dest='/help/contentpolicy')\n    mc('/faq', controller='redirect', action='redirect',\n       dest='https://reddit.zendesk.com/')\n\n    mc('/wiki/create/*page', controller='wiki', action='wiki_create')\n    mc('/wiki/edit/*page', controller='wiki', action='wiki_revise')\n    mc('/wiki/revisions', controller='wiki', action='wiki_recent')\n    mc('/wiki/revisions/*page', controller='wiki', action='wiki_revisions')\n    mc('/wiki/settings/*page', controller='wiki', action='wiki_settings')\n    mc('/wiki/discussions/*page', controller='wiki', action='wiki_discussions')\n    mc('/wiki/pages', controller='wiki', action='wiki_listing')\n\n    mc('/api/wiki/edit', controller='wikiapi', action='wiki_edit')\n    mc('/api/wiki/hide', controller='wikiapi', action='wiki_revision_hide')\n    mc('/api/wiki/delete', controller='wikiapi', action='wiki_revision_delete')\n    mc('/api/wiki/revert', controller='wikiapi', action='wiki_revision_revert')\n    mc('/api/wiki/alloweditor/:act', controller='wikiapi',\n       requirements=dict(act=\"del|add\"), action='wiki_allow_editor')\n\n    mc('/wiki/*page', controller='wiki', action='wiki_page')\n    mc('/wiki/', controller='wiki', action='wiki_page')\n\n    mc('/:action', controller='wiki', requirements=dict(action=\"help\"))\n    mc('/help/*page', controller='wiki', action='wiki_redirect')\n    mc('/w/*page', controller='wiki', action='wiki_redirect')\n\n    mc('/goto', controller='toolbar', action='goto')\n    mc('/tb/:link_id', controller='front', action='link_id_redirect')\n    mc('/toolbar/*frame', controller='toolbar', action='redirect')\n\n    mc('/c/:comment_id', controller='front', action='comment_by_id')\n\n    mc('/s/*urloid', controller='toolbar', action='s')\n    # additional toolbar-related rules just above the catchall\n\n    mc('/resetpassword/:key', controller='forms',\n       action='resetpassword')\n    mc('/verification/:key', controller='forms',\n       action='verify_email')\n    mc('/resetpassword', controller='forms',\n       action='resetpassword')\n\n    mc('/modify_hsts_grant', controller='front', action='modify_hsts_grant')\n\n    mc('/post/:action/:url_user', controller='post',\n       requirements=dict(action=\"login|reg\"))\n    mc('/post/:action', controller='post',\n       requirements=dict(action=\"options|over18|unlogged_options|optout\"\n                         \"|optin|login|reg|explore_settings\"))\n\n    mc('/api', controller='redirect', action='redirect', dest='/dev/api')\n    mc('/api/distinguish/:how', controller='api', action=\"distinguish\")\n    mc('/api/spendcreddits', controller='ipn', action=\"spendcreddits\")\n    mc('/api/stripecharge/gold', controller='stripe', action='goldcharge')\n    mc('/api/modify_subscription', controller='stripe',\n       action='modify_subscription')\n    mc('/api/cancel_subscription', controller='stripe',\n       action='cancel_subscription')\n    mc('/api/stripewebhook/gold/:secret', controller='stripe',\n       action='goldwebhook')\n    mc('/api/coinbasewebhook/gold/:secret', controller='coinbase',\n       action='goldwebhook')\n    mc('/api/rgwebhook/gold/:secret', controller='redditgifts',\n       action='goldwebhook')\n    mc('/api/ipn/:secret', controller='ipn', action='ipn')\n    mc('/ipn/:secret', controller='ipn', action='ipn')\n    mc('/api/:action/:url_user', controller='api',\n       requirements=dict(action=\"login|register\"))\n    mc('/api/gadget/click/:ids', controller='api', action='gadget',\n       type='click')\n    mc('/api/gadget/:type', controller='api', action='gadget')\n    mc('/api/zendeskreply', controller='mailgunwebhook', action='zendeskreply')\n    mc('/api/:action', controller='promoteapi',\n       requirements=dict(action=(\"promote|unpromote|edit_promo|ad_s3_callback|\"\n                                 \"ad_s3_params|freebie|promote_note|update_pay|\"\n                                 \"edit_campaign|delete_campaign|\"\n                                 \"check_inventory|\"\n                                 \"refund_campaign|terminate_campaign|\"\n                                 \"review_fraud|create_promo|\"\n                                 \"toggle_pause_campaign\")))\n    mc('/api/:action', controller='apiminimal',\n       requirements=dict(action=\"new_captcha\"))\n    mc('/api/:type', controller='api',\n       requirements=dict(type='wikibannednote|bannednote|mutednote'),\n       action='relnote')\n\n    # Route /api/multi here to prioritize it over the /api/:action rule\n    mc(\"/api/multi\", controller=\"multiapi\", action=\"multi\",\n       conditions={\"method\": [\"POST\"]})\n\n    mc('/api/:action', controller='api')\n    \n    mc('/api/recommend/sr/:srnames', controller='api',\n       action='subreddit_recommendations')\n\n    mc('/api/server_seconds_visibility', controller='api',\n       action='server_seconds_visibility')\n\n    mc(\"/api/multi/mine\", controller=\"multiapi\", action=\"my_multis\")\n    mc(\"/api/multi/user/:username\", controller=\"multiapi\", action=\"list_multis\")\n    mc(\"/api/multi/copy\", controller=\"multiapi\", action=\"multi_copy\")\n    mc(\"/api/multi/rename\", controller=\"multiapi\", action=\"multi_rename\")\n    mc(\"/api/multi/*multipath/r/:srname\", controller=\"multiapi\", action=\"multi_subreddit\")\n    mc(\"/api/multi/*multipath/description\", controller=\"multiapi\", action=\"multi_description\")\n    mc(\"/api/multi/*multipath\", controller=\"multiapi\", action=\"multi\")\n    mc(\"/api/filter/*multipath/r/:srname\", controller=\"multiapi\", action=\"multi_subreddit\")\n    mc(\"/api/filter/*multipath\", controller=\"multiapi\", action=\"multi\")\n\n    mc(\"/api/v1/:action\", controller=\"oauth2frontend\",\n       requirements=dict(action=\"authorize\"))\n    mc(\"/api/v1/:action\", controller=\"oauth2access\",\n       requirements=dict(action=\"access_token|revoke_token\"))\n    mc(\"/api/v1/:action\", controller=\"apiv1scopes\",\n       requirements=dict(action=\"scopes\"))\n    mc(\"/api/v1/user/:username/trophies\",\n       controller=\"apiv1user\", action=\"usertrophies\")\n    mc(\"/api/v1/:action\", controller=\"apiv1login\",\n       requirements=dict(action=\"register|login\"))\n    mc(\"/api/v1/:action\", controller=\"apiv1user\")\n    # Same controller/action as /prefs/friends\n    mc(\"/api/v1/me/:where\", controller=\"userlistlisting\",\n        action=\"user_prefs\", requirements=dict(where=\"friends\"))\n    mc(\"/api/v1/me/:action\", controller=\"apiv1user\")\n    mc(\"/api/v1/me/:action/:username\", controller=\"apiv1user\")\n\n    mc(\"/api/v1/gold/gild/:fullname\", controller=\"apiv1gold\", action=\"gild\")\n    mc(\"/api/v1/gold/give/:username\", controller=\"apiv1gold\", action=\"give\")\n\n    mc('/dev', controller='redirect', action='redirect', dest='/dev/api')\n    mc('/dev/api', controller='apidocs', action='docs')\n    mc('/dev/api/:mode', controller='apidocs', action='docs',\n       requirements=dict(mode=\"oauth\"))\n\n    mc(\"/button_info\", controller=\"api\", action=\"url_info\", limit=1)\n\n    mc('/captcha/:iden', controller='captcha', action='captchaimg')\n\n    mc('/mediaembed/:link/:credentials',\n       controller=\"mediaembed\", action=\"mediaembed\", credentials=None)\n\n    mc('/code', controller='redirect', action='redirect',\n       dest='http://github.com/reddit/')\n\n    mc('/socialite', controller='redirect', action='redirect',\n       dest='https://addons.mozilla.org/firefox/addon/socialite/')\n\n    # Used for showing ads\n    mc(\"/ads/\", controller=\"ad\", action=\"ad\")\n\n    mc(\"/try\", controller=\"forms\", action=\"try_compact\")\n\n    mc(\"/web/timings\", controller=\"weblog\", action=\"timings\")\n\n    mc(\"/web/log/:level\", controller=\"weblog\", action=\"message\",\n       requirements=dict(level=\"error\"))\n\n    mc(\"/web/poisoning\", controller=\"weblog\", action=\"report_cache_poisoning\")\n\n    # This route handles displaying the error page and\n    # graphics used in the 404/500\n    # error pages. It should likely stay at the top\n    # to ensure that the error page is\n    # displayed properly.\n    mc('/error/document/:id', controller='error', action=\"document\")\n\n    # these should be near the buttom, because they should only kick\n    # in if everything else fails. It's the attempted catch-all\n    # reddit.com/http://... and reddit.com/34fr\n    mc('/:link_id', controller='front', action='link_id_redirect',\n       requirements=dict(link_id='[0-9a-z]{1,6}'))\n    mc('/:urloid', controller='toolbar', action='s',\n       requirements=dict(urloid=r'(\\w+\\.\\w{2,}|https?).*'))\n\n    mc(\"/*url\", controller='front', action='catchall')\n\n    return map\n"
  },
  {
    "path": "r2/r2/config/templates.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom r2.lib.manager import tp_manager\nfrom r2.lib.jsontemplates import *\n\ntpm = tp_manager.tp_manager()\n\ndef api(type, cls):\n    tpm.add_handler(type, 'api', cls())\n    tpm.add_handler(type, 'api-html', cls())\n    tpm.add_handler(type, 'api-compact', cls())\n\n\ndef register_api_templates(template_name, template_class):\n    for style in ('api', 'api-html', 'api-compact'):\n        tpm.add_handler(\n            name=template_name,\n            style=style,\n            handler=template_class,\n        )\n\n\n# blanket fallback rule\napi('templated', NullJsonTemplate)\n\n# class specific overrides\napi('link',          LinkJsonTemplate)\napi('promotedlink',  PromotedLinkJsonTemplate)\napi('message',       MessageJsonTemplate)\napi('subreddit',     SubredditJsonTemplate)\napi('labeledmulti',  LabeledMultiJsonTemplate)\napi('reddit',        RedditJsonTemplate)\napi('panestack',     PanestackJsonTemplate)\napi('htmlpanestack', NullJsonTemplate)\napi('listing',       ListingJsonTemplate)\napi('searchlisting', SearchListingJsonTemplate)\napi('userlisting',   UserListingJsonTemplate)\napi('usertableitem', UserTableItemJsonTemplate)\napi('account',       AccountJsonTemplate)\n\napi('reltableitem', RelTableItemJsonTemplate)\napi('bannedtableitem', BannedTableItemJsonTemplate)\napi('mutedtableitem', MutedTableItemJsonTemplate)\napi('invitedmodtableitem', InvitedModTableItemJsonTemplate)\napi('friendtableitem', FriendTableItemJsonTemplate)\n\napi('organiclisting',       OrganicListingJsonTemplate)\napi('subreddittraffic', TrafficJsonTemplate)\napi('takedownpane', TakedownJsonTemplate)\napi('policyview', PolicyViewJsonTemplate)\n\napi('wikibasepage', WikiJsonTemplate)\napi('wikipagerevisions', WikiJsonTemplate)\napi('wikiview', WikiViewJsonTemplate)\napi('wikirevision', WikiRevisionJsonTemplate)\n\napi('wikipagelisting', WikiPageListingJsonTemplate)\napi('wikipagediscussions', WikiJsonTemplate)\napi('wikipagesettings', WikiSettingsJsonTemplate)\n\napi('flairlist', FlairListJsonTemplate)\napi('flaircsv', FlairCsvJsonTemplate)\napi('flairselector', FlairSelectorJsonTemplate)\n\napi('subredditstylesheet', StylesheetTemplate)\napi('subredditstylesheetsource', StylesheetTemplate)\napi('createsubreddit', SubredditSettingsTemplate)\napi('uploadedimage', UploadedImageJsonTemplate)\n\napi('modaction', ModActionTemplate)\n\napi('trophy', TrophyJsonTemplate)\napi('rules', RulesJsonTemplate)\n\n\nregister_api_templates('comment', CommentJsonTemplate)\nregister_api_templates('morerecursion', MoreCommentJsonTemplate)\nregister_api_templates('morechildren', MoreCommentJsonTemplate)\n"
  },
  {
    "path": "r2/r2/controllers/__init__.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\n_reddit_controllers = {}\n_plugin_controllers = {}\n\ndef get_controller(name):\n    name = name.lower() + 'controller'\n    if name in _reddit_controllers:\n        return _reddit_controllers[name]\n    elif name in _plugin_controllers:\n        return _plugin_controllers[name]\n    else:\n        raise KeyError(name)\n\ndef add_controller(controller):\n    name = controller.__name__.lower()\n    assert name not in _plugin_controllers\n    _plugin_controllers[name] = controller\n    return controller\n\ndef load_controllers():\n    from listingcontroller import ListingController\n    from listingcontroller import HotController\n    from listingcontroller import NewController\n    from listingcontroller import RisingController\n    from listingcontroller import BrowseController\n    from listingcontroller import AdsController\n    from listingcontroller import UserListListingController\n    from listingcontroller import MessageController\n    from listingcontroller import RedditsController\n    from listingcontroller import ByIDController\n    from listingcontroller import RandomrisingController\n    from listingcontroller import UserController\n    from listingcontroller import CommentsController\n    from listingcontroller import GildedController\n\n    from listingcontroller import MyredditsController\n\n    from admin import AdminToolController\n    from front import FormsController\n    from front import FrontController\n    from health import HealthController\n    from buttons import ButtonsController\n    from captcha import CaptchaController\n    from embed import EmbedController\n    from error import ErrorController\n    from post import PostController\n    from toolbar import ToolbarController\n    from awards import AwardsController\n    from newsletter import NewsletterController\n    from googletagmanager import GoogleTagManagerController\n    from promotecontroller import PromoteController\n    from promotecontroller import SponsorController\n    from promotecontroller import PromoteApiController\n    from promotecontroller import PromoteListingController\n    from promotecontroller import SponsorListingController\n    from mediaembed import MediaembedController\n    from mediaembed import AdController\n    from oembed import OEmbedController\n    from policies import PoliciesController\n    from web import WebLogController\n\n    from wiki import WikiController\n    from wiki import WikiApiController\n\n    from api import ApiController\n    from api import ApiminimalController\n    from api_docs import ApidocsController\n    from apiv1.user import APIv1UserController\n    from apiv1.login import APIv1LoginController\n    from apiv1.gold import APIv1GoldController\n    from apiv1.scopes import APIv1ScopesController\n    from multi import MultiApiController\n    from oauth2 import OAuth2FrontendController\n    from oauth2 import OAuth2AccessController\n    from redirect import RedirectController\n    from robots import RobotsController\n    from ipn import IpnController\n    from ipn import StripeController\n    from ipn import CoinbaseController\n    from ipn import RedditGiftsController\n    from mailgun import MailgunWebhookController\n\n    _reddit_controllers.update((name.lower(), obj) for name, obj in locals().iteritems())\n"
  },
  {
    "path": "r2/r2/controllers/admin.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom reddit_base import RedditController\nfrom r2.lib.pages import AdminPage, AdminCreddits, AdminGold\nfrom r2.lib.validator import nop, validate, VAdmin\n\nclass AdminToolController(RedditController):\n    @validate(\n        VAdmin(),\n        recipient=nop('recipient'),\n    )\n    def GET_creddits(self, recipient):\n        return AdminPage(content=AdminCreddits(recipient)).render()\n\n    @validate(\n        VAdmin(),\n        recipient=nop('recipient'),\n    )\n    def GET_gold(self, recipient):\n        return AdminPage(content=AdminGold(recipient)).render()\n"
  },
  {
    "path": "r2/r2/controllers/api.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\nimport csv\nfrom collections import defaultdict\nimport hashlib\nimport re\nimport urllib\nimport urllib2\n\nfrom r2.controllers.reddit_base import (\n    abort_with_error,\n    cross_domain,\n    generate_modhash,\n    is_trusted_origin,\n    MinimalController,\n    paginated_listing,\n    RedditController,\n    set_user_cookie,\n)\n\nfrom pylons.i18n import _\nfrom pylons import request, response\nfrom pylons import tmpl_context as c\nfrom pylons import app_globals as g\n\nfrom r2.lib.validator import *\n\nfrom r2.models import *\n\nfrom r2.lib import amqp\nfrom r2.lib import recommender\nfrom r2.lib import hooks\nfrom r2.lib.ratelimit import SimpleRateLimit\n\nfrom r2.lib.utils import (\n    blockquote_text,\n    extract_user_mentions,\n    get_title,\n    query_string,\n    randstr,\n    sanitize_url,\n    timefromnow,\n    timeuntil,\n    tup,\n)\n\nfrom r2.lib.pages import (\n    BoringPage,\n    ClickGadget,\n    CssError,\n    FormPage,\n    Reddit,\n    responsive,\n    UploadedImage,\n    UrlParser,\n    WrappedUser,\n)\nfrom r2.lib.pages import FlairList, FlairCsv, FlairTemplateEditor, \\\n    FlairSelector\nfrom r2.lib.pages import PrefApps\nfrom r2.lib.pages import (\n    BannedTableItem,\n    ContributorTableItem,\n    FriendTableItem,\n    InvitedModTableItem,\n    ModTableItem,\n    MutedTableItem,\n    ReportForm,\n    SubredditReportForm,\n    SubredditStylesheet,\n    WikiBannedTableItem,\n    WikiMayContributeTableItem,\n)\n\nfrom r2.lib.pages.things import (\n    default_thing_wrapper,\n    hot_links_by_url_listing,\n    wrap_links,\n)\n\nfrom r2.lib.menus import CommentSortMenu\nfrom r2.lib.captcha import get_iden\nfrom r2.lib.strings import strings\nfrom r2.lib.template_helpers import format_html, header_url\nfrom r2.lib.filters import _force_unicode, _force_utf8, websafe_json, websafe, spaceCompress\nfrom r2.lib.db import queries\nfrom r2.lib import media\nfrom r2.lib.db import tdb_cassandra\nfrom r2.lib import promote\nfrom r2.lib import tracking, emailer, newsletter\nfrom r2.lib.subreddit_search import search_reddits\nfrom r2.lib.filters import safemarkdown\nfrom r2.lib.media import str_to_image\nfrom r2.controllers.api_docs import api_doc, api_section\nfrom r2.controllers.oauth2 import require_oauth2_scope, allow_oauth2_access\nfrom r2.lib.template_helpers import (\n    add_sr,\n    get_domain,\n    make_url_protocol_relative,\n)\nfrom r2.lib.system_messages import (\n    notify_user_added,\n    send_ban_message,\n    send_mod_removal_message,\n)\nfrom r2.controllers.ipn import generate_blob, update_blob\nfrom r2.controllers.login import handle_login, handle_register\nfrom r2.lib.lock import TimeoutExpired\nfrom r2.lib.csrf import csrf_exempt\nfrom r2.lib.voting import cast_vote\n\nfrom r2.models import wiki\nfrom r2.models.ip import set_account_ip\nfrom r2.models.recommend import AccountSRFeedback, FEEDBACK_ACTIONS\nfrom r2.models.rules import SubredditRules\nfrom r2.models.vote import Vote\nfrom r2.lib.merge import ConflictException\n\nfrom datetime import datetime, timedelta\nfrom urlparse import urlparse\n\n\nclass ApiminimalController(MinimalController):\n    \"\"\"\n    Put API calls in here which don't rely on the user being logged in\n    \"\"\"\n\n    # Since this is only a MinimalController, the\n    # @allow_oauth2_access decorator has little effect other than\n    # (1) to add the endpoint to /dev/api/oauth, and\n    # (2) to future-proof in case the function moves elsewhere\n    @allow_oauth2_access\n    @csrf_exempt\n    @validatedForm()\n    @api_doc(api_section.captcha)\n    def POST_new_captcha(self, form, jquery, *a, **kw):\n        \"\"\"\n        Responds with an `iden` of a new CAPTCHA.\n\n        Use this endpoint if a user cannot read a given CAPTCHA,\n        and wishes to receive a new CAPTCHA.\n\n        To request the CAPTCHA image for an iden, use\n        [/captcha/`iden`](#GET_captcha_{iden}).\n        \"\"\"\n\n        iden = get_iden()\n        jquery(\"body\").captcha(iden)\n        form._send_data(iden = iden) \n\n\nclass ApiController(RedditController):\n    \"\"\"\n    Controller which deals with almost all AJAX site interaction.  \n    \"\"\"\n    @validatedForm()\n    def ajax_login_redirect(self, form, jquery, dest):\n        form.redirect(\"/login\" + query_string(dict(dest=dest)))\n\n    @require_oauth2_scope(\"read\")\n    @validate(\n        things=VByName('id', multiple=True, ignore_missing=True, limit=100),\n        url=VUrl('url'),\n    )\n    @api_doc(api_section.links_and_comments, uses_site=True)\n    def GET_info(self, things, url):\n        \"\"\"\n        Return a listing of things specified by their fullnames.\n\n        Only Links, Comments, and Subreddits are allowed.\n\n        \"\"\"\n\n        if url:\n            return self.GET_url_info()\n\n        thing_classes = (Link, Comment, Subreddit)\n        things = things or []\n        things = filter(lambda thing: isinstance(thing, thing_classes), things)\n\n        c.update_last_visit = False\n        listing = wrap_links(things)\n        return BoringPage(_(\"API\"), content=listing).render()\n\n    @require_oauth2_scope(\"read\")\n    @validate(\n        url=VUrl('url'),\n        count=VLimit('limit'),\n        things=VByName('id', multiple=True, limit=100),\n    )\n    def GET_url_info(self, url, count, things):\n        \"\"\"\n        Return a list of links with the given URL.\n\n        If a subreddit is provided, only links in that subreddit will be\n        returned.\n\n        \"\"\"\n\n        if things and not url:\n            return self.GET_info()\n\n        c.update_last_visit = False\n\n        if url:\n            listing = hot_links_by_url_listing(url, sr=c.site, num=count)\n        else:\n            listing = None\n        return BoringPage(_(\"API\"), content=listing).render()\n\n    @json_validate()\n    def GET_me(self, responder):\n        \"\"\"Get info about the currently authenticated user.\n\n        Response includes a modhash, karma, and new mail status.\n\n        \"\"\"\n        if c.user_is_loggedin:\n            user_data = Wrapped(c.user).render()\n            user_data['data'].update({'features': feature.all_enabled(c.user)})\n            return user_data\n        else:\n            return {'data': {'features': feature.all_enabled(None)}}\n\n    @json_validate(user=VUname((\"user\",)))\n    @api_doc(api_section.users)\n    def GET_username_available(self, responder, user):\n        \"\"\"\n        Check whether a username is available for registration.\n        \"\"\"\n        if not (responder.has_errors(\"user\", errors.BAD_USERNAME)):\n            return bool(user)\n\n    @csrf_exempt\n    @json_validate(user=VUname((\"user\",)))\n    def POST_check_username(self, responder, user):\n        \"\"\"\n        Check whether a username is valid.\n        \"\"\"\n\n        if not (responder.has_errors(\"user\",\n                    errors.USERNAME_TOO_SHORT,\n                    errors.USERNAME_INVALID_CHARACTERS,\n                    errors.USERNAME_TAKEN_DEL,\n                    errors.USERNAME_TAKEN)):\n            # Pylons does not handle 204s correctly.\n            return {}\n\n    @csrf_exempt\n    @json_validate(password=VPassword((\"passwd\")))\n    def POST_check_password(self, responder, password):\n        \"\"\"\n        Check whether a password is valid.\n        \"\"\"\n    \n        if not (responder.has_errors(\"passwd\", errors.SHORT_PASSWORD) or\n                responder.has_errors(\"passwd\", errors.BAD_PASSWORD)):\n            # Pylons does not handle 204s correctly.\n            return {}\n\n    @csrf_exempt\n    @json_validate(email=ValidEmail(\"email\"),\n                   newsletter_subscribe=VBoolean(\"newsletter_subscribe\", default=False),\n                   sponsor=VBoolean(\"sponsor\", default=False))\n    def POST_check_email(self, responder, email, newsletter_subscribe, sponsor):\n        \"\"\"\n        Check whether an email is valid. Allows blank emails.\n\n        Additionally checks if a newsletter is requested, and will be strict\n        on blank emails if so.\n        \"\"\"\n        if newsletter_subscribe and not email:\n            c.errors.add(errors.NEWSLETTER_NO_EMAIL, field=\"email\")\n            responder.has_errors(\"email\", errors.NEWSLETTER_NO_EMAIL)\n            return\n\n        if sponsor and not email:\n            c.errors.add(errors.SPONSOR_NO_EMAIL, field=\"email\")\n            responder.has_errors(\"email\", errors.SPONSOR_NO_EMAIL)\n            return\n\n        if not (responder.has_errors(\"email\", errors.BAD_EMAIL)):\n            # Pylons does not handle 204s correctly.\n            return {}\n\n    @cross_domain(allow_credentials=True)\n    @json_validate(\n        VModhashIfLoggedIn(),\n        VRatelimit(rate_ip=True, prefix=\"rate_newsletter_\"),\n        email=ValidEmail(\"email\"),\n        source=VOneOf('source', ['newsletterbar', 'standalone'])\n    )\n    def POST_newsletter(self, responder, email, source):\n        \"\"\"Add an email to our newsletter.\"\"\"\n\n        VRatelimit.ratelimit(rate_ip=True,\n                             prefix=\"rate_newsletter_\")\n\n        try:\n            newsletter.add_subscriber(email, source=source)\n        except newsletter.EmailUnacceptableError as e:\n            c.errors.add(errors.NEWSLETTER_EMAIL_UNACCEPTABLE, field=\"email\")\n            responder.has_errors(\"email\", errors.NEWSLETTER_EMAIL_UNACCEPTABLE)\n            return\n        except newsletter.NewsletterError as e:\n            g.log.warning(\"Failed to subscribe: %r\" % e)\n            abort(500)\n\n    @allow_oauth2_access\n    @json_validate()\n    @api_doc(api_section.captcha)\n    def GET_needs_captcha(self, responder):\n        \"\"\"\n        Check whether CAPTCHAs are needed for API methods that define the\n        \"captcha\" and \"iden\" parameters.\n        \"\"\"\n        return bool(c.user.needs_captcha())\n\n    @require_oauth2_scope(\"privatemessages\")\n    @validatedForm(\n        VCaptcha(),\n        VUser(),\n        VModhash(),\n        from_sr=VSRByName('from_sr', required=False),\n        to=VMessageRecipient('to'),\n        subject=VLength('subject', 100, empty_error=errors.NO_SUBJECT),\n        body=VMarkdownLength(['text', 'message'], max_length=10000),\n    )\n    @api_doc(api_section.messages)\n    def POST_compose(self, form, jquery, from_sr, to, subject, body):\n        \"\"\"\n        Handles message composition under /message/compose.\n        \"\"\"\n        if (form.has_errors(\"to\",\n                    errors.USER_DOESNT_EXIST, errors.NO_USER,\n                    errors.SUBREDDIT_NOEXIST, errors.USER_BLOCKED,\n                ) or\n                form.has_errors(\"subject\", errors.NO_SUBJECT) or\n                form.has_errors(\"subject\", errors.TOO_LONG) or\n                form.has_errors(\"text\", errors.NO_TEXT, errors.TOO_LONG) or\n                form.has_errors(\"message\", errors.TOO_LONG) or\n                form.has_errors(\"captcha\", errors.BAD_CAPTCHA) or\n                form.has_errors(\"from_sr\", errors.SUBREDDIT_NOEXIST)):\n            return\n\n        if form.has_errors(\"to\", errors.USER_MUTED):\n            g.events.muted_forbidden_event(\"muted\", target=to,\n                request=request, context=c)\n            form.set_inputs(to=\"\", subject=\"\", text=\"\", captcha=\"\")\n            return\n\n        if from_sr and isinstance(to, Subreddit):\n            c.errors.add(errors.NO_SR_TO_SR_MESSAGE, field=\"from\")\n            form.has_errors(\"from\", errors.NO_SR_TO_SR_MESSAGE)\n            return\n\n        if from_sr and BlockedSubredditsByAccount.is_blocked(to, from_sr):\n            c.errors.add(errors.USER_BLOCKED_MESSAGE, field=\"to\")\n            form.has_errors(\"to\", errors.USER_BLOCKED_MESSAGE)\n            return\n\n        if from_sr and from_sr._spam:\n            return\n\n        if from_sr:\n            if not from_sr.is_moderator_with_perms(c.user, \"mail\"):\n                abort(403)\n            elif from_sr.is_muted(to) and not c.user_is_admin:\n                c.errors.add(errors.MUTED_FROM_SUBREDDIT, field=\"to\")\n                form.has_errors(\"to\", errors.MUTED_FROM_SUBREDDIT)\n                g.events.muted_forbidden_event(\"muted mod\", subreddit=from_sr,\n                    target=to, request=request, context=c)\n                form.set_inputs(to=\"\", subject=\"\", text=\"\", captcha=\"\")\n                return\n\n            # Don't allow mods in timeout to send a message\n            VNotInTimeout().run(target=to, subreddit=from_sr)\n            m, inbox_rel = Message._new(c.user, to, subject, body, request.ip,\n                                        sr=from_sr, from_sr=True)\n        else:\n            # Only let users in timeout message the admins\n            if (to and not (isinstance(to, Subreddit) and\n                    '/r/%s' % to.name == g.admin_message_acct)):\n                VNotInTimeout().run(target=to)\n            m, inbox_rel = Message._new(c.user, to, subject, body, request.ip)\n\n        form.set_text(\".status\", _(\"your message has been delivered\"))\n        form.set_inputs(to = \"\", subject = \"\", text = \"\", captcha=\"\")\n        queries.new_message(m, inbox_rel)\n\n    @require_oauth2_scope(\"submit\")\n    @json_validate()\n    @api_doc(api_section.subreddits, uses_site=True)\n    def GET_submit_text(self, responder):\n        \"\"\"Get the submission text for the subreddit.\n\n        This text is set by the subreddit moderators and intended to be\n        displayed on the submission form.\n\n        See also: [/api/site_admin](#POST_api_site_admin).\n\n        \"\"\"\n        if c.site.over_18 and not c.over18:\n            submit_text = None\n            submit_text_html = None\n        else:\n            submit_text = c.site.submit_text\n            submit_text_html = safemarkdown(c.site.submit_text)\n        return {'submit_text': submit_text,\n                'submit_text_html': submit_text_html}\n\n    @require_oauth2_scope(\"submit\")\n    @validatedForm(\n        VUser(),\n        VModhash(),\n        VCaptcha(),\n        VRatelimit(rate_user=True, rate_ip=True, prefix=\"rate_submit_\"),\n        VShamedDomain('url'),\n        sr=VSubmitSR('sr', 'kind'),\n        url=VUrl('url'),\n        title=VTitle('title'),\n        sendreplies=VBoolean('sendreplies'),\n        selftext=VMarkdown('text'),\n        kind=VOneOf('kind', ['link', 'self']),\n        extension=VLength(\"extension\", 20,\n                          docs={\"extension\": \"extension used for redirects\"}),\n        resubmit=VBoolean('resubmit'),\n    )\n    @api_doc(api_section.links_and_comments)\n    def POST_submit(self, form, jquery, url, selftext, kind, title,\n                    sr, extension, sendreplies, resubmit):\n        \"\"\"Submit a link to a subreddit.\n\n        Submit will create a link or self-post in the subreddit `sr` with the\n        title `title`. If `kind` is `\"link\"`, then `url` is expected to be a\n        valid URL to link to. Otherwise, `text`, if present, will be the\n        body of the self-post.\n\n        If a link with the same URL has already been submitted to the specified\n        subreddit an error will be returned unless `resubmit` is true.\n        `extension` is used for determining which view-type (e.g. `json`,\n        `compact` etc.) to use for the redirect that is generated if the\n        `resubmit` error occurs.\n\n        \"\"\"\n\n        from r2.models.admintools import is_banned_domain\n\n        if url:\n            if url.lower() == 'self':\n                url = kind = 'self'\n\n            # VUrl may have replaced 'url' by adding 'http://'\n            form.set_inputs(url=url)\n\n        is_self = (kind == \"self\")\n\n        if not kind or form.has_errors('sr', errors.INVALID_OPTION):\n            return\n\n        if form.has_errors('captcha', errors.BAD_CAPTCHA):\n            return\n\n        if form.has_errors('sr',\n                errors.SUBREDDIT_NOEXIST,\n                errors.SUBREDDIT_NOTALLOWED,\n                errors.SUBREDDIT_REQUIRED,\n                errors.INVALID_OPTION,\n                errors.NO_SELFS,\n                errors.NO_LINKS,\n                errors.IN_TIMEOUT,\n        ):\n            return\n\n        if not sr.can_submit_text(c.user) and is_self:\n            # this could happen if they actually typed \"self\" into the\n            # URL box and we helpfully translated it for them\n            c.errors.add(errors.NO_SELFS, field='sr')\n            form.has_errors('sr', errors.NO_SELFS)\n            return\n\n        if form.has_errors(\"title\", errors.NO_TEXT, errors.TOO_LONG):\n            return\n\n        if not sr.should_ratelimit(c.user, 'link'):\n            c.errors.remove((errors.RATELIMIT, 'ratelimit'))\n        else:\n            if form.has_errors('ratelimit', errors.RATELIMIT):\n                return\n\n        if not is_self:\n            if form.has_errors(\"url\", errors.NO_URL, errors.BAD_URL):\n                return\n\n            if form.has_errors(\"url\", errors.DOMAIN_BANNED):\n                g.stats.simple_event('spam.shame.link')\n                return\n\n            if not resubmit:\n                listing = hot_links_by_url_listing(url, sr=sr, num=1)\n                links = listing.things\n                if links:\n                    c.errors.add(errors.ALREADY_SUB, field='url')\n                    form.has_errors('url', errors.ALREADY_SUB)\n                    u = links[0].already_submitted_link(url, title)\n                    if extension:\n                        u = UrlParser(u)\n                        u.set_extension(extension)\n                        u = u.unparse()\n                    form.redirect(u)\n                    return\n\n        if not c.user_is_admin and is_self:\n            if len(selftext) > Link.SELFTEXT_MAX_LENGTH:\n                c.errors.add(errors.TOO_LONG, field='text',\n                    msg_params={'max_length': Link.SELFTEXT_MAX_LENGTH})\n                form.set_error(errors.TOO_LONG, 'text')\n                return\n\n        VNotInTimeout().run(action_name=\"submit\", details_text=kind, target=sr)\n\n        if not request.POST.get('sendreplies'):\n            sendreplies = is_self\n\n        # get rid of extraneous whitespace in the title\n        cleaned_title = re.sub(r'\\s+', ' ', title, flags=re.UNICODE)\n        cleaned_title = cleaned_title.strip()\n\n        l = Link._submit(\n            is_self=is_self,\n            title=cleaned_title,\n            content=selftext if is_self else url,\n            author=c.user,\n            sr=sr,\n            ip=request.ip,\n            sendreplies=sendreplies,\n        )\n\n        if not is_self:\n            ban = is_banned_domain(url)\n            if ban:\n                g.stats.simple_event('spam.domainban.link_url')\n                admintools.spam(l, banner = \"domain (%s)\" % ban.banmsg)\n                hooks.get_hook('banned_domain.submit').call(item=l, url=url,\n                                                            ban=ban)\n\n        if sr.should_ratelimit(c.user, 'link'):\n            VRatelimit.ratelimit(rate_user=True, rate_ip = True,\n                                 prefix = \"rate_submit_\")\n\n        queries.new_link(l)\n        l.update_search_index()\n        g.events.submit_event(l, request=request, context=c)\n\n        path = add_sr(l.make_permalink_slow())\n        if extension:\n            path += \".%s\" % extension\n\n        form.redirect(path)\n        form._send_data(url=path)\n        form._send_data(id=l._id36)\n        form._send_data(name=l._fullname)\n\n    @csrf_exempt\n    @validatedForm(VRatelimit(rate_ip = True,\n                              rate_user = True,\n                              prefix = 'fetchtitle_'),\n                   VUser(),\n                   url = VSanitizedUrl('url'))\n    def POST_fetch_title(self, form, jquery, url):\n        if form.has_errors('ratelimit', errors.RATELIMIT):\n            form.set_text(\".title-status\", \"\")\n            return\n\n        VRatelimit.ratelimit(rate_ip = True, rate_user = True,\n                             prefix = 'fetchtitle_', seconds=1)\n        if url:\n            title = get_title(url)\n            if title:\n                form.set_inputs(title = title)\n                form.set_text(\".title-status\", \"\")\n            else:\n                form.set_text(\".title-status\", _(\"no title found\"))\n            form._send_data(title=title)\n        \n    def _login(self, responder, user, rem = None):\n        \"\"\"\n        AJAX login handler, used by both login and register to set the\n        user cookie and send back a redirect.\n        \"\"\"\n        c.user = user\n        c.user_is_loggedin = True\n        self.login(user, rem = rem)\n\n        if request.params.get(\"hoist\") != \"cookie\":\n            responder._send_data(modhash=generate_modhash())\n            responder._send_data(cookie  = user.make_cookie())\n        responder._send_data(need_https=feature.is_enabled(\"force_https\"))\n\n    @csrf_exempt\n    @cross_domain(allow_credentials=True)\n    @validatedForm(\n        VLoggedOut(),\n        user=VThrottledLogin(['user', 'passwd']),\n        rem=VBoolean('rem'),\n    )\n    def POST_login(self, form, responder, user, rem=None, **kwargs):\n        \"\"\"Log into an account.\n\n        `rem` specifies whether or not the session cookie returned should last\n        beyond the current browser session (that is, if `rem` is `True` the\n        cookie will have an explicit expiration far in the future indicating\n        that it is not a session cookie).\n\n        \"\"\"\n        kwargs.update(dict(\n            controller=self,\n            form=form,\n            responder=responder,\n            user=user,\n            rem=rem,\n        ))\n        return handle_login(**kwargs)\n\n    @csrf_exempt\n    @cross_domain(allow_credentials=True)\n    @validatedForm(\n        VRatelimit(rate_ip=True, prefix=\"rate_register_\"),\n        name=VUname(['user']),\n        email=ValidEmail(\"email\"),\n        password=VPasswordChange(['passwd', 'passwd2']),\n        rem=VBoolean('rem'),\n        newsletter_subscribe=VBoolean('newsletter_subscribe', default=False),\n        sponsor=VBoolean('sponsor', default=False),\n    )\n    def POST_register(self, form, responder, name, email, password, **kwargs):\n        \"\"\"Create a new account.\n\n        `rem` specifies whether or not the session cookie returned should last\n        beyond the current browser session (that is, if `rem` is `True` the\n        cookie will have an explicit expiration far in the future indicating\n        that it is not a session cookie).\n\n        \"\"\"\n        kwargs.update(dict(\n            controller=self,\n            form=form,\n            responder=responder,\n            name=name,\n            email=email,\n            password=password,\n        ))\n        return handle_register(**kwargs)\n\n    @require_oauth2_scope(\"modself\")\n    @noresponse(VUser(),\n                VModhash(),\n                container = VByName('id'))\n    @api_doc(api_section.moderation)\n    def POST_leavemoderator(self, container):\n        \"\"\"Abdicate moderator status in a subreddit.\n\n        See also: [/api/friend](#POST_api_friend).\n\n        \"\"\"\n        if container and container.is_moderator(c.user):\n            container.remove_moderator(c.user)\n            ModAction.create(container, c.user, 'removemoderator', target=c.user, \n                             details='remove_self')\n\n    @require_oauth2_scope(\"modself\")\n    @noresponse(VUser(),\n                VModhash(),\n                container = VByName('id'))\n    @api_doc(api_section.moderation)\n    def POST_leavecontributor(self, container):\n        \"\"\"Abdicate approved submitter status in a subreddit.\n\n        See also: [/api/friend](#POST_api_friend).\n\n        \"\"\"\n        if container and container.is_contributor(c.user):\n            container.remove_contributor(c.user)\n\n\n    _sr_friend_types = (\n        'moderator',\n        'moderator_invite',\n        'contributor',\n        'banned',\n        'muted',\n        'wikibanned',\n        'wikicontributor',\n    )\n\n    _sr_friend_types_with_permissions = (\n        'moderator',\n        'moderator_invite',\n    )\n\n    # Changes to this dict should also update docstrings for\n    # POST_friend and POST_unfriend\n    api_friend_scope_map = {\n        'moderator': {\"modothers\"},\n        'moderator_invite': {\"modothers\"},\n        'contributor': {\"modcontributors\"},\n        'banned': {\"modcontributors\"},\n        'muted': {\"modcontributors\"},\n        'wikibanned': {\"modcontributors\", \"modwiki\"},\n        'wikicontributor': {\"modcontributors\", \"modwiki\"},\n        'friend': None,  # Handled with API v1 endpoint\n        'enemy': {\"privatemessages\"},  # Only valid for POST_unfriend\n    }\n\n    def check_api_friend_oauth_scope(self, type_):\n        if c.oauth_user:\n            needed_scopes = self.api_friend_scope_map[type_]\n            if needed_scopes is None:\n                # OAuth2 access not allowed for this friend rel type\n                # via /api/friend\n                self._auth_error(400, \"invalid_request\")\n            if not c.oauth_scope.has_access(c.site.name, needed_scopes):\n                # Token does not have the necessary scope to complete\n                # this request.\n                self._auth_error(403, \"insufficient_scope\")\n\n    @allow_oauth2_access\n    @noresponse(VUser(),\n                VModhash(),\n                nuser = VExistingUname('name'),\n                iuser = VByName('id'),\n                container = nop('container'),\n                type = VOneOf('type', ('friend', 'enemy') +\n                                      _sr_friend_types))\n    @api_doc(api_section.users, uses_site=True)\n    def POST_unfriend(self, nuser, iuser, container, type):\n        \"\"\"Remove a relationship between a user and another user or subreddit\n\n        The user can either be passed in by name (nuser)\n        or by [fullname](#fullnames) (iuser).  If type is friend or enemy,\n        'container' MUST be the current user's fullname;\n        for other types, the subreddit must be set\n        via URL (e.g., /r/funny/api/unfriend)\n\n        OAuth2 use requires appropriate scope based\n        on the 'type' of the relationship:\n\n        * moderator: `modothers`\n        * moderator_invite: `modothers`\n        * contributor: `modcontributors`\n        * banned: `modcontributors`\n        * muted: `modcontributors`\n        * wikibanned: `modcontributors` and `modwiki`\n        * wikicontributor: `modcontributors` and `modwiki`\n        * friend: Use [/api/v1/me/friends/{username}](#DELETE_api_v1_me_friends_{username})\n        * enemy: `privatemessages`\n\n        Complement to [POST_friend](#POST_api_friend)\n\n        \"\"\"\n        self.check_api_friend_oauth_scope(type)\n\n        victim = iuser or nuser\n\n        if not victim:\n            abort(400, 'No user specified')\n        \n        if type in self._sr_friend_types:\n            mod_action_by_type = dict(\n                banned='unbanuser',\n                moderator='removemoderator',\n                moderator_invite='uninvitemoderator',\n                wikicontributor='removewikicontributor',\n                wikibanned='wikiunbanned',\n                contributor='removecontributor',\n                muted='unmuteuser',\n            )\n            action = mod_action_by_type.get(type, type)\n\n            if isinstance(c.site, FakeSubreddit):\n                abort(403, 'forbidden')\n            container = c.site\n            if not (c.user == victim and type == 'moderator'):\n                # The requesting user is marked as spam or banned, and is\n                # trying to do a mod action. The only action they should be\n                # allowed to do and have it stick is demodding themself.\n                if c.user._spam:\n                    return\n                VNotInTimeout().run(action_name=action, target=victim)\n        else:\n            container = VByName('container').run(container)\n            if not container:\n                return\n\n        # The user who made the request must be an admin or a moderator\n        # for the privilege change to succeed.\n        # (Exception: a user can remove privilege from oneself)\n        required_perms = []\n        if c.user != victim:\n            if type.startswith('wiki'):\n                required_perms.append('wiki')\n            else:\n                required_perms.append('access')\n                # ability to unmute requires access and mail permissions\n                if type == 'muted':\n                    required_perms.append('mail')\n\n        if (\n            not c.user_is_admin and\n            type in self._sr_friend_types and\n            not container.is_moderator_with_perms(c.user, *required_perms)\n        ):\n            abort(403, 'forbidden')\n        if (\n            type == \"moderator\" and\n            not c.user_is_admin and\n            not container.can_demod(c.user, victim)\n        ):\n            abort(403, 'forbidden')\n\n        # if we are (strictly) unfriending, the container had better\n        # be the current user.\n        if type in (\"friend\", \"enemy\") and container != c.user:\n            abort(403, 'forbidden')\n\n        fn = getattr(container, 'remove_' + type)\n        new = fn(victim)\n\n        # for mod removals, let the now ex-mod know (NOTE: doing this earlier\n        # will make the message show up in their mod inbox, which they will\n        # immediately lose access to.)\n        if new and type == 'moderator' and victim != c.user:\n            send_mod_removal_message(container, c.user, victim)\n\n        # Log this action\n        if new and type in self._sr_friend_types:\n            ModAction.create(container, c.user, action, target=victim)\n\n        if type == \"friend\" and c.user.gold:\n            c.user.friend_rels_cache(_update=True)\n\n        if type in ('banned', 'wikibanned'):\n            container.unschedule_unban(victim, type)\n\n        if type == 'muted':\n            MutedAccountsBySubreddit.unmute(container, victim)\n\n    @require_oauth2_scope(\"modothers\")\n    @validatedForm(VSrModerator(), VModhash(),\n                   target=VExistingUname('name'),\n                   type_and_permissions=VPermissions('type', 'permissions'))\n    @api_doc(api_section.users, uses_site=True)\n    def POST_setpermissions(self, form, jquery, target, type_and_permissions):\n        if form.has_errors('name', errors.USER_DOESNT_EXIST, errors.NO_USER):\n            return\n        if form.has_errors('type', errors.INVALID_PERMISSION_TYPE):\n            return\n        if form.has_errors('permissions', errors.INVALID_PERMISSIONS):\n            return\n\n        if c.user._spam:\n            return\n\n        type, permissions = type_and_permissions\n        update = None\n\n        if type in (\"moderator\", \"moderator_invite\"):\n            if not c.user_is_admin:\n                if type == \"moderator\" and (\n                    c.user == target or not c.site.can_demod(c.user, target)):\n                    abort(403, 'forbidden')\n                if (type == \"moderator_invite\"\n                    and not c.site.is_unlimited_moderator(c.user)):\n                    abort(403, 'forbidden')\n                # Don't allow mods in timeout to set permissions\n                VNotInTimeout().run(action_name=\"editsettings\",\n                    details_text=\"set_permissions\", target=target)\n            if type == \"moderator\":\n                rel = c.site.get_moderator(target)\n            if type == \"moderator_invite\":\n                rel = c.site.get_moderator_invite(target)\n            rel.set_permissions(permissions)\n            rel._commit()\n            update = rel.encoded_permissions\n            ModAction.create(c.site, c.user, action='setpermissions',\n                             target=target, details='permission_' + type,\n                             description=update)\n\n        if update:\n            row = form.closest('tr')\n            editor = row.find('.permissions').data('PermissionEditor')\n            editor.onCommit(update)\n\n    @allow_oauth2_access\n    @validatedForm(\n        VUser(),\n        VModhash(),\n        friend=VExistingUname('name'),\n        container=nop('container'),\n        type=VOneOf('type', ('friend',) + _sr_friend_types),\n        type_and_permissions=VPermissions('type', 'permissions'),\n        note=VLength('note', 300),\n        ban_reason=VLength('ban_reason', 100),\n        duration=VInt('duration', min=1, max=999),\n        ban_message=VMarkdownLength('ban_message', max_length=1000,\n            empty_error=None),\n    )\n    @api_doc(api_section.users, uses_site=True)\n    def POST_friend(self, form, jquery, friend,\n            container, type, type_and_permissions, note, ban_reason,\n            duration, ban_message):\n        \"\"\"Create a relationship between a user and another user or subreddit\n\n        OAuth2 use requires appropriate scope based\n        on the 'type' of the relationship:\n\n        * moderator: Use \"moderator_invite\"\n        * moderator_invite: `modothers`\n        * contributor: `modcontributors`\n        * banned: `modcontributors`\n        * muted: `modcontributors`\n        * wikibanned: `modcontributors` and `modwiki`\n        * wikicontributor: `modcontributors` and `modwiki`\n        * friend: Use [/api/v1/me/friends/{username}](#PUT_api_v1_me_friends_{username})\n        * enemy: Use [/api/block](#POST_api_block)\n\n        Complement to [POST_unfriend](#POST_api_unfriend)\n\n        \"\"\"\n        self.check_api_friend_oauth_scope(type)\n\n        if type in self._sr_friend_types:\n            if isinstance(c.site, FakeSubreddit):\n                abort(403, 'forbidden')\n            container = c.site\n        else:\n            container = VByName('container').run(container)\n            if not container:\n                return\n\n        if type == \"moderator\" and not c.user_is_admin:\n            # attempts to add moderators now create moderator invites.\n            type = \"moderator_invite\"\n\n        fn = getattr(container, 'add_' + type)\n\n        # Make sure the user making the request has the correct permissions\n        # to be able to make this status change\n        if type in self._sr_friend_types:\n            mod_action_by_type = {\n                \"banned\": \"banuser\",\n                \"muted\": \"muteuser\",\n                \"contributor\": \"addcontributor\",\n                \"moderator\": \"addmoderator\",\n                \"moderator_invite\": \"invitemoderator\",\n            }\n            action = mod_action_by_type.get(type, type)\n\n            if c.user_is_admin:\n                has_perms = True\n            elif type.startswith('wiki'):\n                has_perms = container.is_moderator_with_perms(c.user, 'wiki')\n            elif type == 'moderator_invite':\n                has_perms = container.is_unlimited_moderator(c.user)\n            else:\n                has_perms = container.is_moderator_with_perms(c.user, 'access')\n\n            if not has_perms:\n                abort(403, 'forbidden')\n\n            # Don't let banned users make subreddit access changes\n            if c.user._spam:\n                return\n            VNotInTimeout().run(action_name=action, target=friend)\n\n        if type == 'moderator_invite':\n            invites = sum(1 for i in container.each_moderator_invite())\n            if invites >= g.sr_invite_limit:\n                c.errors.add(errors.SUBREDDIT_RATELIMIT, field=\"name\")\n                form.set_error(errors.SUBREDDIT_RATELIMIT, \"name\")\n                return\n\n        if (type in self._sr_friend_types and\n                not c.user_is_admin and\n                container.use_quotas):\n            sr_ratelimit = SimpleRateLimit(\n                name=\"sr_%s_%s\" % (str(type), container._id36),\n                seconds=g.sr_quota_time,\n                limit=getattr(g, \"sr_%s_quota\" % type),\n            )\n            if not sr_ratelimit.record_and_check():\n                form.set_text(\".status\", errors.SUBREDDIT_RATELIMIT)\n                c.errors.add(errors.SUBREDDIT_RATELIMIT)\n                form.set_error(errors.SUBREDDIT_RATELIMIT, None)\n                return\n\n        # if we are (strictly) friending, the container\n        # had better be the current user.\n        if type == \"friend\" and container != c.user:\n            abort(403,'forbidden')\n\n        elif form.has_errors(\"name\", errors.USER_DOESNT_EXIST, errors.NO_USER):\n            return\n        elif form.has_errors((\"note\", \"ban_reason\"), errors.TOO_LONG):\n            return\n\n        if type == \"banned\":\n            if form.has_errors(\"ban_message\", errors.TOO_LONG):\n                return\n            if ban_reason and note:\n                note = \"%s: %s\" % (ban_reason, note)\n            elif ban_reason:\n                note = ban_reason\n\n        if type in self._sr_friend_types_with_permissions:\n            if form.has_errors('type', errors.INVALID_PERMISSION_TYPE):\n                return\n            if form.has_errors('permissions', errors.INVALID_PERMISSIONS):\n                return\n        else:\n            permissions = None\n\n        if type == \"moderator_invite\" and container.is_moderator(friend):\n            c.errors.add(errors.ALREADY_MODERATOR, field=\"name\")\n            form.set_error(errors.ALREADY_MODERATOR, \"name\")\n            return\n        elif type in (\"banned\", \"muted\") and container.is_moderator(friend):\n            c.errors.add(errors.CANT_RESTRICT_MODERATOR, field=\"name\")\n            form.set_error(errors.CANT_RESTRICT_MODERATOR, \"name\")\n            return\n\n        if type == \"muted\" and not container.can_mute(c.user, friend):\n            abort(403)\n\n        # don't allow increasing privileges of banned or muted users\n        unbanned_types = (\"moderator\", \"moderator_invite\",\n                          \"contributor\", \"wikicontributor\")\n        if type in unbanned_types:\n            if container.is_banned(friend):\n                c.errors.add(errors.BANNED_FROM_SUBREDDIT, field=\"name\")\n                form.set_error(errors.BANNED_FROM_SUBREDDIT, \"name\")\n                return\n            elif container.is_muted(friend):\n                c.errors.add(errors.MUTED_FROM_SUBREDDIT, field=\"name\")\n                form.set_error(errors.MUTED_FROM_SUBREDDIT, \"name\")\n                return\n\n        if type == \"moderator\":\n            container.remove_moderator_invite(friend)\n\n        new = fn(friend, permissions=type_and_permissions[1])\n\n        if type == \"friend\" and c.user.gold:\n            # Yes, the order of the next two lines is correct.\n            # First you recalculate the rel_ids, then you find\n            # the right one and update its data.\n            c.user.friend_rels_cache(_update=True)\n            c.user.add_friend_note(friend, note or '')\n\n        # additional logging/info needed for bans\n        tempinfo = None\n        log_details = None\n        log_description = None\n\n        if type in ('banned', 'wikibanned', 'muted'):\n            container.add_rel_note(type, friend, note)\n            log_description = note\n\n        if type in ('banned', 'wikibanned'):\n            existing_ban = None\n            if not new:\n                existing_ban = container.get_tempbans(type, friend.name)\n            if duration:\n                ban_buffer = timedelta(hours=6)\n                # Temp ban that doesn't have a preceding temp ban that ends\n                # ends within ban_buffer of this new duration. this is just a\n                # small buffer to prevent repetitive ban messages sent to users\n                # when the mod wants to update a note but not the duration\n                if existing_ban:\n                    now = datetime.now(g.tz)\n                    ban_remaining = existing_ban[friend.name] - now\n                    timediff = abs(timedelta(days=duration) - ban_remaining)\n                if not existing_ban or timediff >= ban_buffer:\n                    container.unschedule_unban(friend, type)\n                    tempinfo = container.schedule_unban(\n                        type,\n                        friend,\n                        c.user,\n                        duration,\n                    )\n                    log_details = \"changed to \" if not new else \"\"\n                    log_details += \"%d days\" % duration\n            elif not new and existing_ban:\n                # Preexisting temp ban and no duration specified means turn\n                # the temporary ban into a permanent one.\n                container.unschedule_unban(friend, type)\n                log_details = \"changed to permanent\"\n            elif new:\n                # New ban without a duration is permanent\n                log_details = \"permanent\"\n        elif new and type == 'muted':\n            MutedAccountsBySubreddit.mute(container, friend, c.user)\n\n        # Log this action\n        if (new or log_details) and type in self._sr_friend_types:\n            mod_action_by_type = {\n                \"banned\": \"banuser\",\n                \"muted\": \"muteuser\",\n                \"contributor\": \"addcontributor\",\n                \"moderator\": \"addmoderator\",\n                \"moderator_invite\": \"invitemoderator\",\n            }\n            action = mod_action_by_type.get(type, type)\n\n            ModAction.create(\n                container,\n                c.user,\n                action,\n                target=friend,\n                details=log_details,\n                description=log_description,\n            )\n\n        row_cls = dict(friend=FriendTableItem,\n                       moderator=ModTableItem,\n                       moderator_invite=InvitedModTableItem,\n                       contributor=ContributorTableItem,\n                       wikicontributor=WikiMayContributeTableItem,\n                       banned=BannedTableItem,\n                       muted=MutedTableItem,\n                       wikibanned=WikiBannedTableItem).get(type)\n\n        form.set_inputs(name = \"\")\n        if note:\n            form.set_inputs(note = \"\")\n        form.removeClass(\"edited\")\n\n        if new and row_cls:\n            new._thing2 = friend\n            user_row = row_cls(new)\n            if tempinfo:\n                BannedListing.populate_from_tempbans(user_row, tempinfo)\n            form.set_text(\".status:first\", user_row.executed_message)\n            rev_types = [\"moderator\", \"moderator_invite\", \"friend\"]\n            index = 0 if user_row.type not in rev_types else -1\n            table = jquery(\".\" + type + \"-table\").show().find(\"table\")\n            table.insert_table_rows(user_row, index=index)\n            table.find(\".notfound\").hide()\n\n        if type == \"banned\":\n            # If the ban is new or has had the duration changed,\n            # send a ban message\n            if (friend.has_interacted_with(container) and \n                    (new or log_details)):\n                send_ban_message(container, c.user, friend,\n                    ban_message, duration, new)\n        elif new:\n            notify_user_added(type, c.user, friend, container)\n\n    @validatedForm(VGold(),\n                   VModhash(),\n                   friend = VExistingUname('name'),\n                   note = VLength('note', 300))\n    def POST_friendnote(self, form, jquery, friend, note):\n        if form.has_errors(\"note\", errors.TOO_LONG):\n            return\n        c.user.add_friend_note(friend, note)\n        form.set_text('.status', _(\"saved\"))\n\n    @validatedForm(\n        VModhash(),\n        type=VOneOf('type', ('bannednote', 'wikibannednote', 'mutednote')),\n        user=VExistingUname('name'),\n        note=VLength('note', 300),\n    )\n    def POST_relnote(self, form, jquery, type, user, note):\n        perm = 'wiki' if type.startswith('wiki') else 'access'\n        if (not c.user_is_admin\n            and (not c.site.is_moderator_with_perms(c.user, perm))):\n            if c.user._spam:\n                return\n            else:\n                abort(403, 'forbidden')\n\n        # Don't allow users in timeout to add relnote\n        VNotInTimeout().run(action_name=\"editrelnote\", details_text=type,\n            target=user)\n\n        if form.has_errors(\"note\", errors.TOO_LONG):\n            # NOTE: there's no error displayed in the form\n            return\n        c.site.add_rel_note(type[:-4], user, note)\n\n    @require_oauth2_scope(\"modself\")\n    @validatedForm(VUser(),\n                   VModhash())\n    @api_doc(api_section.moderation, uses_site=True)\n    def POST_accept_moderator_invite(self, form, jquery):\n        \"\"\"Accept an invite to moderate the specified subreddit.\n\n        The authenticated user must have been invited to moderate the subreddit\n        by one of its current moderators.\n\n        See also: [/api/friend](#POST_api_friend) and\n        [/subreddits/mine](#GET_subreddits_mine_{where}).\n\n        \"\"\"\n\n        rel = c.site.get_moderator_invite(c.user)\n        if not c.site.remove_moderator_invite(c.user):\n            c.errors.add(errors.NO_INVITE_FOUND)\n            form.set_error(errors.NO_INVITE_FOUND, None)\n            return\n\n        permissions = rel.get_permissions()\n        ModAction.create(c.site, c.user, \"acceptmoderatorinvite\")\n        c.site.add_moderator(c.user, permissions=rel.get_permissions())\n        notify_user_added(\"accept_moderator_invite\", c.user, c.user, c.site)\n        jquery.refresh()\n\n    @validatedForm(\n        VUser(),\n        VModhash(),\n        password=VVerifyPassword(\"curpass\", fatal=False),\n        dest=VDestination(),\n    )\n    def POST_clear_sessions(self, form, jquery, password, dest):\n        \"\"\"Clear all session cookies and replace the current one.\n\n        A valid password (`curpass`) must be supplied.\n\n        \"\"\"\n        # password is required to proceed\n        if form.has_errors(\"curpass\", errors.WRONG_PASSWORD):\n            return\n\n        form.set_text('.status',\n                      _('all other sessions have been logged out'))\n        form.set_inputs(curpass = \"\")\n\n        # deauthorize all access tokens\n        OAuth2AccessToken.revoke_all_by_user(c.user)\n        OAuth2RefreshToken.revoke_all_by_user(c.user)\n\n    def revoke_sessions_and_login(self, user, password):\n        self.revoke_sessions(user)\n\n        # run the change password command to get a new salt\n        change_password(c.user, password)\n        # the password salt has changed, so the user's cookie has been\n        # invalidated.  drop a new cookie.\n        self.login(c.user)\n\n    def revoke_sessions(self, user):\n        # deauthorize all access tokens\n        OAuth2AccessToken.revoke_all_by_user(user)\n        OAuth2RefreshToken.revoke_all_by_user(user)\n\n    @validatedForm(\n        VUser(),\n        VModhash(),\n        VVerifyPassword(\"curpass\", fatal=False),\n        email=ValidEmails(\"email\", num=1),\n        verify=VBoolean(\"verify\"),\n        dest=VDestination(),\n    )\n    def POST_update_email(self, form, jquery, email, verify, dest):\n        \"\"\"Update account email address.\n\n        Called by /prefs/update on the site.\n\n        \"\"\"\n\n        if form.has_errors(\"curpass\", errors.WRONG_PASSWORD):\n            return\n\n        if not form.has_errors(\"email\", errors.BAD_EMAILS) and email:\n            if (not hasattr(c.user, 'email') or c.user.email != email):\n                if c.user.email:\n                    emailer.email_change_email(c.user)\n\n                c.user.set_email(email)\n                c.user.email_verified = None\n                c.user._commit()\n                Award.take_away(\"verified_email\", c.user)\n\n            if verify:\n                if dest == '/':\n                    dest = None\n\n                emailer.verify_email(c.user, dest=dest)\n                form.set_inputs(curpass=\"\")\n                form.set_text('.status',\n                     _(\"you should be getting a verification email shortly.\"))\n            else:\n                form.set_text('.status', _('your email has been updated'))\n\n        # user is removing their email\n        if (not email and c.user.email and \n            (errors.NO_EMAILS, 'email') in c.errors):\n            c.errors.remove((errors.NO_EMAILS, 'email'))\n            if c.user.email:\n                emailer.email_change_email(c.user)\n            c.user.set_email('')\n            c.user.email_verified = None\n            c.user.pref_email_messages = False\n            c.user._commit()\n            Award.take_away(\"verified_email\", c.user)\n            form.set_text('.status', _('your email has been updated'))\n\n    @validatedForm(\n        VUser(),\n        VModhash(),\n        VVerifyPassword(\"curpass\", fatal=False),\n        password=VPasswordChange(['newpass', 'verpass']),\n        invalidate_oauth=VBoolean(\"invalidate_oauth\"),\n    )\n    def POST_update_password(self, form, jquery, password, invalidate_oauth):\n        \"\"\"Update account password.\n\n        Called by /prefs/update on the site. For frontend form verification\n        purposes, `newpass` and `verpass` must be equal for a password change\n        to succeed.\n\n        \"\"\"\n\n        if form.has_errors(\"curpass\", errors.WRONG_PASSWORD):\n            return\n\n        if (password and\n            not (form.has_errors(\"newpass\", errors.BAD_PASSWORD) or\n                 form.has_errors(\"verpass\", errors.BAD_PASSWORD_MATCH))):\n            if invalidate_oauth:\n                self.revoke_sessions(c.user)\n\n            change_password(c.user, password)\n\n            if c.user.email:\n                emailer.password_change_email(c.user)\n\n            form.set_text('.status', _('your password has been updated'))\n            form.set_inputs(curpass=\"\", newpass=\"\", verpass=\"\")\n\n            # the password has changed, so the user's cookie has been\n            # invalidated.  drop a new cookie.\n            self.login(c.user)\n\n    @validatedForm(VUser(),\n                   VModhash(),\n                   deactivate_message = VLength(\"deactivate_message\", max_length=500),\n                   username = VRequired(\"user\", errors.NOT_USER),\n                   user = VThrottledLogin([\"user\", \"passwd\"]),\n                   confirm = VBoolean(\"confirm\"))\n    def POST_deactivate_user(self, form, jquery, deactivate_message, username, user, confirm):\n        \"\"\"Deactivate the currently logged in account.\n\n        A valid username/password and confirmation must be supplied. An\n        optional `deactivate_message` may be supplied to explain the reason the\n        account is to be deleted.\n\n        Called by /prefs/deactivate on the site.\n\n        \"\"\"\n        if username and username.lower() != c.user.name.lower():\n            c.errors.add(errors.NOT_USER, field=\"user\")\n\n        if not confirm:\n            c.errors.add(errors.CONFIRM, field=\"confirm\")\n\n        if not (form.has_errors('ratelimit', errors.RATELIMIT) or\n                form.has_errors(\"user\", errors.NOT_USER) or\n                form.has_errors(\"passwd\", errors.WRONG_PASSWORD) or\n                form.has_errors(\"deactivate_message\", errors.TOO_LONG) or\n                form.has_errors(\"confirm\", errors.CONFIRM)):\n            redirect_url = \"/?deactivated=true\"\n            c.user.delete(deactivate_message)\n            form.redirect(redirect_url)\n\n    @require_oauth2_scope(\"edit\")\n    @noresponse(VUser(),\n                VModhash(),\n                thing = VByNameIfAuthor('id'))\n    @api_doc(api_section.links_and_comments)\n    def POST_del(self, thing):\n        \"\"\"Delete a Link or Comment.\"\"\"\n        if not thing: return\n        was_deleted = thing._deleted\n        thing._deleted = True\n        if (getattr(thing, \"promoted\", None) is not None and\n            not promote.is_promoted(thing)):\n            promote.reject_promotion(thing)\n        thing._commit()\n\n        thing.update_search_index()\n\n        if isinstance(thing, Link):\n            amqp.add_item(\"deleted_link\", thing._fullname)\n            queries.delete(thing)\n            thing.subreddit_slow.remove_sticky(thing)\n            if thing.preview_object:\n                thing.set_preview_object(None)\n                thing._commit()\n        elif isinstance(thing, Comment):\n            link = thing.link_slow\n\n            if not was_deleted:\n                # get lock before writing to avoid multiple decrements when\n                # there are simultaneous duplicate requests\n                lock_key = \"lock:del_{link}_{comment}\".format(\n                    link=link._id36,\n                    comment=thing._id36,\n                )\n                if g.lock_cache.add(lock_key, \"\", time=60):\n                    link._incr('num_comments', -1)\n\n            link.remove_sticky_comment(comment=thing, set_by=c.user)\n\n            queries.new_comment(thing, None)  # possible inbox_rels are\n                                              # handled by unnotify\n            queries.unnotify(thing)\n            amqp.add_item(\"deleted_comment\", thing._fullname)\n            queries.delete(thing)\n\n    @require_oauth2_scope(\"modposts\")\n    @noresponse(VUser(),\n                VModhash(),\n                VSrCanBan('id'),\n                thing=VByName('id', thing_cls=Link))\n    @api_doc(api_section.links_and_comments)\n    def POST_lock(self, thing):\n        \"\"\"Lock a link.\n\n        Prevents a post from receiving new comments.\n\n        See also: [/api/unlock](#POST_api_unlock).\n\n        \"\"\"\n        if thing.archived_slow:\n            return abort(400, \"Bad Request\")\n        VNotInTimeout().run(action_name=\"lock\", target=thing)\n        thing.locked = True\n        thing._commit()\n\n        ModAction.create(thing.subreddit_slow, c.user, target=thing,\n                         action='lock')\n\n    @require_oauth2_scope(\"modposts\")\n    @noresponse(VUser(),\n                VModhash(),\n                VSrCanBan('id'),\n                thing=VByName('id', thing_cls=Link))\n    @api_doc(api_section.links_and_comments)\n    def POST_unlock(self, thing):\n        \"\"\"Unlock a link.\n\n        Allow a post to receive new comments.\n\n        See also: [/api/lock](#POST_api_lock).\n\n        \"\"\"\n        if thing.archived_slow:\n            return abort(400, \"Bad Request\")\n        VNotInTimeout().run(action_name=\"unlock\", target=thing)\n        thing.locked = False\n        thing._commit()\n\n        ModAction.create(thing.subreddit_slow, c.user, target=thing,\n                         action='unlock')\n\n    @require_oauth2_scope(\"modposts\")\n    @noresponse(VUser(),\n                VModhash(),\n                VSrCanAlter('id'),\n                thing = VByName('id'))\n    @api_doc(api_section.links_and_comments)\n    def POST_marknsfw(self, thing):\n        \"\"\"Mark a link NSFW.\n\n        See also: [/api/unmarknsfw](#POST_api_unmarknsfw).\n\n        \"\"\"\n        thing.over_18 = True\n        thing._commit()\n\n        if c.user._id != thing.author_id:\n            ModAction.create(thing.subreddit_slow, c.user, target=thing,\n                             action='marknsfw')\n\n        thing.update_search_index()\n\n    @require_oauth2_scope(\"modposts\")\n    @noresponse(VUser(),\n                VModhash(),\n                VSrCanAlter('id'),\n                thing = VByName('id'))\n    @api_doc(api_section.links_and_comments)\n    def POST_unmarknsfw(self, thing):\n        \"\"\"Remove the NSFW marking from a link.\n\n        See also: [/api/marknsfw](#POST_api_marknsfw).\n\n        \"\"\"\n\n        if promote.is_promo(thing):\n            if c.user_is_sponsor:\n                # set the override attribute so this link won't be automatically\n                # reset as nsfw by promote.make_daily_promotions\n                thing.over_18_override = True\n            else:\n                abort(403,'forbidden')\n\n        thing.over_18 = False\n        thing._commit()\n\n        if c.user._id != thing.author_id:\n            ModAction.create(thing.subreddit_slow, c.user, target=thing,\n                             action='marknsfw', details='remove')\n\n        thing.update_search_index()\n\n    @require_oauth2_scope(\"edit\")\n    @noresponse(VUser(),\n                VModhash(),\n                thing=VByNameIfAuthor('id'),\n                state=VBoolean('state'))\n    @api_doc(api_section.links_and_comments)\n    def POST_sendreplies(self, thing, state):\n        \"\"\"Enable or disable inbox replies for a link or comment.\n\n        `state` is a boolean that indicates whether you are enabling or\n        disabling inbox replies - true to enable, false to disable.\n\n        \"\"\"\n        if not isinstance(thing, (Link, Comment)):\n            return\n\n        thing.sendreplies = state\n        thing._commit()\n\n    @noresponse(VUser(),\n                VModhash(),\n                VSrCanAlter('id'),\n                thing=VByName('id'))\n    def POST_rescrape(self, thing):\n        \"\"\"Re-queues the link in the media scraper.\"\"\"\n        if not isinstance(thing, Link):\n            return\n\n        # KLUDGE: changing the cache entry to a placeholder for this URL will\n        # cause the media scraper to force a rescrape.  This will be fixed\n        # when parameters can be passed to the scraper queue.\n        media_cache.MediaByURL.add_placeholder(thing.url, autoplay=False)\n\n        amqp.add_item(\"scraper_q\", thing._fullname)\n\n    @require_oauth2_scope(\"modposts\")\n    @validatedForm(\n        VUser(),\n        VModhash(),\n        VSrCanBan(\"id\"),\n        thing=VByName(\"id\", thing_cls=Link),\n        sort=VOneOf(\"sort\", CommentSortMenu.suggested_sort_options),\n        timeout=VNotInTimeout(\"id\"),\n    )\n    @api_doc(api_section.links_and_comments)\n    def POST_set_suggested_sort(self, form, jquery, thing, sort, timeout):\n        \"\"\"Set a suggested sort for a link.\n\n        Suggested sorts are useful to display comments in a certain preferred way\n        for posts. For example, casual conversation may be better sorted by new\n        by default, or AMAs may be sorted by Q&A. A sort of an empty string\n        clears the default sort.\n        \"\"\"\n        if c.user._id != thing.author_id:\n            ModAction.create(thing.subreddit_slow, c.user, target=thing,\n                             action='setsuggestedsort')\n\n        thing.suggested_sort = sort\n        thing._commit()\n        jquery.refresh()\n\n    @require_oauth2_scope(\"modposts\")\n    @validatedForm(\n        VUser(),\n        VModhash(),\n        VSrCanBan(\"id\"),\n        thing=VByName(\"id\"),\n        state=VBoolean(\"state\"),\n        timeout=VNotInTimeout(\"id\"),\n    )\n    @api_doc(api_section.links_and_comments)\n    def POST_set_contest_mode(self, form, jquery, thing, state, timeout):\n        \"\"\"Set or unset \"contest mode\" for a link's comments.\n        \n        `state` is a boolean that indicates whether you are enabling or\n        disabling contest mode - true to enable, false to disable.\n\n        \"\"\"\n        thing.contest_mode = state\n        thing._commit()\n        if state:\n            action = 'setcontestmode'\n        else:\n            action = 'unsetcontestmode'\n        ModAction.create(thing.subreddit_slow, c.user, action, target=thing)\n        jquery.refresh()\n\n    @require_oauth2_scope(\"modposts\")\n    @validatedForm(\n        VUser(),\n        VModhash(),\n        VSrCanBan('id'),\n        thing=VByName('id'),\n        state=VBoolean('state'),\n        num=VInt(\"num\", min=1, max=Subreddit.MAX_STICKIES, coerce=True),\n        timeout=VNotInTimeout(\"id\"),\n    )\n    @api_doc(api_section.links_and_comments)\n    def POST_set_subreddit_sticky(self, form, jquery, thing, state, num,\n            timeout):\n        \"\"\"Set or unset a Link as the sticky in its subreddit.\n        \n        `state` is a boolean that indicates whether to sticky or unsticky\n        this post - true to sticky, false to unsticky.\n\n        The `num` argument is optional, and only used when stickying a post.\n        It allows specifying a particular \"slot\" to sticky the post into, and\n        if there is already a post stickied in that slot it will be replaced.\n        If there is no post in the specified slot to replace, or `num` is None,\n        the bottom-most slot will be used.\n        \n        \"\"\"\n        if not isinstance(thing, Link):\n            return\n\n        sr = thing.subreddit_slow\n        stickied = thing.is_stickied(sr)\n\n        if not stickied and (thing._deleted or thing._spam):\n            abort(400, \"Can't sticky a removed or deleted post\")\n\n        if state:\n            if not thing.is_stickyable():\n                abort(400, \"Post not stickyable\")\n\n            if stickied:\n                abort(409, \"Already stickied\")\n            sr.set_sticky(thing, c.user, num=num)\n        else:\n            sr.remove_sticky(thing, c.user)\n\n        jquery.refresh()\n\n    @require_oauth2_scope(\"report\")\n    @validatedForm(\n        VUser(),\n        VModhash(),\n        thing=VByName('thing_id'),\n        reason=VLength('reason', max_length=100, empty_error=None),\n        site_reason=VLength('site_reason', max_length=100, empty_error=None),\n        other_reason=VLength('other_reason', max_length=100, empty_error=None),\n    )\n    @api_doc(api_section.links_and_comments)\n    def POST_report(self, form, jquery, thing, reason, site_reason, other_reason):\n        \"\"\"Report a link, comment or message.\n\n        Reporting a thing brings it to the attention of the subreddit's\n        moderators. Reporting a message sends it to a system for admin review.\n\n        For links and comments, the thing is implicitly hidden as well (see\n        [/api/hide](#POST_api_hide) for details).\n\n        \"\"\"\n        if not thing:\n            # preserve old behavior: we used to send the thing's fullname as the\n            # \"id\" parameter, but we can't use that because that name is used to\n            # send the form's id\n            thing_id = request.POST.get('id')\n            if thing_id:\n                thing = VByName('id').run(thing_id)\n\n        if not thing or thing._deleted:\n            return\n\n        if (form.has_errors(\"reason\", errors.TOO_LONG) or\n            form.has_errors(\"site_reason\", errors.TOO_LONG) or\n            form.has_errors(\"other_reason\", errors.TOO_LONG)):\n            return\n\n        sr = getattr(thing, 'subreddit_slow', None)\n\n        if reason == \"site_reason_selected\":\n            reason = site_reason\n        elif reason == \"other\":\n            reason = other_reason\n\n        # if it is a message that is being reported, ban it.\n        # every user is admin over their own personal inbox\n        if isinstance(thing, Message):\n            # Ensure the message is either to them directly or indirectly\n            # (through modmail), to prevent unauthorized banning through\n            # spoofing.\n            if (c.user._id != thing.to_id and\n                    not (sr and c.user._id in sr.moderator_ids())):\n                abort(403)\n            admintools.spam(thing, False, True, c.user.name)\n        # auto-hide links that are reported\n        elif isinstance(thing, Link):\n            # don't hide items from admins/moderators when reporting\n            if not (c.user_is_admin or sr.is_moderator(c.user)):\n                thing._hide(c.user)\n        # TODO: be nice to be able to remove comments that are reported\n        # from a user's inbox so they don't have to look at them.\n        elif isinstance(thing, Comment):\n            pass\n\n        # Don't allow a user in timeout to report things, but continue\n        # to hide the links of the reported items\n        VNotInTimeout().run(action_name=\"report\", target=thing)\n\n        hooks.get_hook(\"thing.report\").call(thing=thing)\n\n        if not (c.user._spam or\n                c.user.ignorereports or\n                (sr and sr.is_banned(c.user))):\n            Report.new(c.user, thing, reason)\n            admintools.report(thing)\n\n        g.events.report_event(\n            reason=reason,\n            details_text=reason,\n            subreddit=sr,\n            target=thing,\n            request=request,\n            context=c,\n        )\n\n        if isinstance(thing, (Link, Message)):\n            button = jquery(\".id-%s .report-button\" % thing._fullname)\n        elif isinstance(thing, Comment):\n            button = jquery(\".id-%s .entry:first .report-button\" % thing._fullname)\n        else:\n            return\n\n        button.text(_(\"reported\"))\n        parent_div = jquery(\".report-%s.reportform\" % thing._fullname)\n        parent_div.removeClass(\"active\")\n        parent_div.html(\"\")\n\n    @require_oauth2_scope(\"privatemessages\")\n    @noresponse(\n        VUser(),\n        VModhash(),\n        thing=VByName('id'),\n    )\n    @api_doc(api_section.messages)\n    def POST_del_msg(self, thing):\n        \"\"\"Delete messages from the recipient's view of their inbox.\"\"\"\n        if not thing:\n            return\n\n        if not isinstance(thing, Message):\n            return\n\n        if thing.to_id != c.user._id:\n            return\n\n        thing.del_on_recipient = True\n        thing._commit()\n\n        # report the message deletion to data pipeline\n        g.events.message_event(thing, event_type=\"ss.delete_message\",\n                               request=request, context=c)\n\n    @require_oauth2_scope(\"privatemessages\")\n    @noresponse(\n        VUser(),\n        VModhash(),\n        thing=VByName('id'),\n    )\n    @api_doc(api_section.messages)\n    def POST_block(self, thing):\n        '''For blocking via inbox.'''\n        if not thing:\n            return\n\n        # don't allow blocking yourself\n        if thing.author_id == c.user._id:\n            return\n\n        try:\n            sr = Subreddit._byID(thing.sr_id) if thing.sr_id else None\n        except NotFound:\n            sr = None\n\n        if getattr(thing, \"from_sr\", False) and sr:\n            # Users may only block a subreddit they don't mod\n            if not (sr.is_moderator(c.user) or c.user_is_admin):\n                BlockedSubredditsByAccount.block(c.user, sr)\n            return\n\n        # Users may only block someone who has actively harassed them\n        # directly (i.e. comment/link reply or PM). Make sure that 'thing'\n        # is in the user's inbox somewhere, unless it's modmail to a\n        # subreddit that the user moderates (since then it's not\n        # necessarily in their personal inbox)\n        is_modmail = (isinstance(thing, Message)\n            and sr\n            and sr.is_moderator_with_perms(c.user, 'mail'))\n\n        if not is_modmail:\n            inbox_cls = Inbox.rel(Account, thing.__class__)\n            rels = inbox_cls._fast_query(c.user, thing,\n                                        (\"inbox\", \"selfreply\", \"mention\"))\n            if not any(rels.values()):\n                return\n\n        block_acct = Account._byID(thing.author_id)\n        display_author = getattr(thing, \"display_author\", None)\n        if block_acct.name in g.admins or display_author:\n            return\n        c.user.add_enemy(block_acct)\n\n        # report the user blocking to data pipeline\n        g.events.report_event(\n            subreddit=sr,\n            target=thing,\n            request=request,\n            context=c,\n            event_type=\"ss.block_user\"\n        )\n\n\n    @require_oauth2_scope(\"privatemessages\")\n    @noresponse(\n        VUser(),\n        VModhash(),\n        thing=VByName('id'),\n    )\n    @api_doc(api_section.messages)\n    def POST_unblock_subreddit(self, thing):\n        if not thing:\n            return\n\n        try:\n            sr = Subreddit._byID(thing.sr_id) if thing.sr_id else None\n        except NotFound:\n            sr = None\n\n        if getattr(thing, \"from_sr\", False) and sr:\n            BlockedSubredditsByAccount.unblock(c.user, sr)\n            return\n\n    @require_oauth2_scope(\"modcontributors\")\n    @noresponse(\n        VUser(),\n        VModhash(),\n        message=VByName('id'),\n    )\n    @api_doc(api_section.moderation)\n    def POST_mute_message_author(self, message):\n        '''For muting user via modmail.'''\n        if not message:\n            return\n        subreddit = message.subreddit_slow\n\n        if not subreddit:\n            abort(403, 'Not modmail')\n\n        user = message.author_slow\n        if not subreddit.can_mute(c.user, user):\n            abort(403)\n\n        if not c.user_is_admin:\n            if not subreddit.is_moderator_with_perms(c.user, 'access', 'mail'):\n                abort(403, 'Invalid mod permissions')\n\n            if subreddit.use_quotas:\n                sr_ratelimit = SimpleRateLimit(\n                    name=\"sr_muted_%s\" % subreddit._id36,\n                    seconds=g.sr_quota_time,\n                    limit=g.sr_muted_quota,\n                )\n                if not sr_ratelimit.record_and_check():\n                    abort(403, errors.SUBREDDIT_RATELIMIT)\n\n        # Don't allow a user in timeout to mute users\n        VNotInTimeout().run(action_name=\"muteuser\", details_text=\"modmail\",\n            target=user, subreddit=subreddit)\n\n        added = subreddit.add_muted(user)\n        # Don't mute the user and create another modaction if already muted\n        if added:\n            MutedAccountsBySubreddit.mute(subreddit, user, c.user, message)\n            permalink = message.make_permalink(force_domain=True)\n            ModAction.create(subreddit, c.user, 'muteuser',\n                target=user, description=permalink)\n            subreddit.add_rel_note('muted', user, permalink)\n\n    @require_oauth2_scope(\"modcontributors\")\n    @noresponse(\n        VUser(),\n        VModhash(),\n        message=VByName('id'),\n    )\n    @api_doc(api_section.moderation)\n    def POST_unmute_message_author(self, message):\n        '''For unmuting user via modmail.'''\n        if not message:\n            return\n        subreddit = message.subreddit_slow\n\n        if not subreddit:\n            abort(403, 'Not modmail')\n\n        user = message.author_slow\n        if not c.user_is_admin:\n            if not subreddit.is_moderator_with_perms(c.user, 'access', 'mail'):\n                abort(403, 'Invalid mod permissions')\n\n        # Don't allow a user in timeout to unmute users\n        VNotInTimeout().run(action_name=\"unmuteuser\", details_text=\"modmail\",\n            target=user, subreddit=subreddit)\n\n        removed = subreddit.remove_muted(user)\n        if removed:\n            MutedAccountsBySubreddit.unmute(subreddit, user)\n            ModAction.create(subreddit, c.user, 'unmuteuser', target=user)\n\n    @require_oauth2_scope(\"edit\")\n    @validatedForm(\n        VUser(),\n        VModhash(),\n        item=VByNameIfAuthor('thing_id'),\n        text=VMarkdown('text'),\n    )\n    @api_doc(api_section.links_and_comments)\n    def POST_editusertext(self, form, jquery, item, text):\n        \"\"\"Edit the body text of a comment or self-post.\"\"\"\n        if (form.has_errors('text', errors.NO_TEXT) or\n                form.has_errors(\"thing_id\", errors.NOT_AUTHOR)):\n            return\n\n        if isinstance(item, Link) and not item.is_self:\n            return abort(403, \"forbidden\")\n            \n        if getattr(item, 'admin_takedown', False):\n            # this item has been takendown by the admins,\n            # and not not be edited\n            # would love to use a 451 (legal) here, but pylons throws an error\n            return abort(403, \"this content is locked and can not be edited\")\n\n        if isinstance(item, Comment):\n            max_length = 10000\n            admin_override = False\n        else:\n            max_length = Link.SELFTEXT_MAX_LENGTH\n            admin_override = c.user_is_admin\n\n        if not admin_override and len(text) > max_length:\n            c.errors.add(errors.TOO_LONG, field='text',\n                         msg_params={'max_length': max_length})\n            form.set_error(errors.TOO_LONG, 'text')\n            return\n\n        removed_mentions = None\n        original_text = item.body\n        if isinstance(item, Comment):\n            kind = 'comment'\n            prev_mentions = extract_user_mentions(original_text)\n            new_mentions = extract_user_mentions(text)\n            removed_mentions = prev_mentions - new_mentions\n            item.body = text\n        elif isinstance(item, Link):\n            kind = 'link'\n            if not getattr(item, \"is_self\", False):\n                return abort(403, \"forbidden\")\n            item.selftext = text\n        else:\n            g.log.warning(\"%s tried to edit usertext on %r\", c.user, item)\n            return\n\n        if item._deleted:\n            return abort(403, \"forbidden\")\n\n        if item._age > timedelta(minutes=3) or item.num_votes > 2:\n            item.editted = c.start_time\n\n        item.ignore_reports = False\n\n        item._commit()\n\n        # only add to the edited page if this is marked as edited\n        if hasattr(item, \"editted\"):\n            queries.edit(item)\n\n        item.update_search_index()\n\n        amqp.add_item('%s_text_edited' % kind, item._fullname)\n\n        hooks.get_hook(\"thing.edit\").call(\n            thing=item, original_text=original_text)\n\n        # new mentions are subject to more constraints, handled in butler_q\n        if removed_mentions:\n            queries.unnotify(item, list(Account._names_to_ids(\n                removed_mentions,\n                ignore_missing=True,\n            )))\n\n        wrapper = default_thing_wrapper(expand_children = True)\n        jquery(\"body>div.content\").replace_things(item, True, True, wrap = wrapper)\n        jquery(\"body>div.content .link .rank\").hide()\n\n    @allow_oauth2_access\n    @validatedForm(\n        VUser(),\n        VModhash(),\n        VRatelimit(rate_user=True, rate_ip=True, prefix=\"rate_comment_\"),\n        parent=VSubmitParent(['thing_id', 'parent']),\n        comment=VMarkdownLength(['text', 'comment'], max_length=10000),\n    )\n    @api_doc(api_section.links_and_comments)\n    def POST_comment(self, commentform, jquery, parent, comment):\n        \"\"\"Submit a new comment or reply to a message.\n\n        `parent` is the fullname of the thing being replied to. Its value\n        changes the kind of object created by this request:\n\n        * the fullname of a Link: a top-level comment in that Link's thread. (requires `submit` scope)\n        * the fullname of a Comment: a comment reply to that comment. (requires `submit` scope)\n        * the fullname of a Message: a message reply to that message. (requires `privatemessages` scope)\n\n        `text` should be the raw markdown body of the comment or message.\n\n        To start a new message thread, use [/api/compose](#POST_api_compose).\n\n        \"\"\"\n        should_ratelimit = True\n        #check the parent type here cause we need that for the\n        #ratelimit checks\n        if isinstance(parent, Message):\n            if (c.oauth_user and not\n                    c.oauth_scope.has_any_scope({'privatemessages', 'submit'})):\n                abort(403, 'forbidden')\n            if not getattr(parent, \"repliable\", True):\n                abort(403, 'forbidden')\n            if not parent.can_view_slow():\n                abort(403, 'forbidden')\n\n            if parent.sr_id and not c.user_is_admin:\n                sr = parent.subreddit_slow\n\n                if sr.is_moderator(c.user) and not c.user_is_admin:\n                    # don't let a moderator message a muted user\n                    muted_user = parent.get_muted_user_in_conversation()\n                    if muted_user:\n                        c.errors.add(\n                            errors.MUTED_FROM_SUBREDDIT, field=\"parent\")\n                        g.events.muted_forbidden_event(\"muted mod\",\n                            sr, parent_message=parent, target=muted_user,\n                            request=request, context=c,\n                        )\n                elif sr.is_muted(c.user):\n                    # don't let a muted user message the subreddit\n                    c.errors.add(errors.USER_MUTED, field=\"parent\")\n                    g.events.muted_forbidden_event(\"muted\",\n                        parent_message=parent, target=sr,\n                        request=request, context=c,\n                    )\n\n            is_message = True\n            should_ratelimit = False\n        else:\n            if (c.oauth_user and not\n                    c.oauth_scope.has_access(c.site.name, {'submit'})):\n                abort(403, 'forbidden')\n\n            is_message = False\n            if isinstance(parent, Link):\n                link = parent\n                parent_comment = None\n            else:\n                link = Link._byID(parent.link_id)\n                parent_comment = parent\n\n            sr = Subreddit._byID(parent.sr_id, stale=True)\n            is_author = link.author_id == c.user._id\n            if (is_author and (link.is_self or promote.is_promo(link)) or\n                    not sr.should_ratelimit(c.user, 'comment')):\n                should_ratelimit = False\n\n            hooks.get_hook(\"comment.validate\").call(sr=sr, link=link,\n                           parent_comment=parent_comment)\n\n        #remove the ratelimit error if the user's karma is high\n        if not should_ratelimit:\n            c.errors.remove((errors.RATELIMIT, 'ratelimit'))\n\n        if (commentform.has_errors(\"text\", errors.NO_TEXT, errors.TOO_LONG) or\n                commentform.has_errors(\"comment\", errors.TOO_LONG) or\n                commentform.has_errors(\"ratelimit\", errors.RATELIMIT) or\n                commentform.has_errors(\"parent\", errors.DELETED_COMMENT,\n                    errors.TOO_OLD, errors.USER_BLOCKED,\n                    errors.USER_MUTED, errors.MUTED_FROM_SUBREDDIT,\n                    errors.THREAD_LOCKED)\n        ):\n            return\n\n        if is_message:\n            if parent.from_sr:\n                to = Subreddit._byID(parent.sr_id)\n            else:\n                to = Account._byID(parent.author_id)\n\n            # Restrict messaging for users in timeout\n            if to:\n                sr_name = None\n                if isinstance(to, Subreddit):\n                    sr_name = to.name\n                # Replies in modmail have an Account as their target, but act\n                # like they're sent to everyone involved in the conversation.\n                elif isinstance(to, Account) and parent and parent.sr_id:\n                    sr = Subreddit._byID(parent.sr_id, data=True)\n                    if sr:\n                        sr_name = sr.name\n                is_messaging_admins = ('/r/%s' % sr_name) == g.admin_message_acct\n\n                # Users in timeout can only message the admins.\n                if not (sr_name and is_messaging_admins):\n                    VNotInTimeout().run(action_name='messagereply', target=parent)\n\n            subject = parent.subject\n            re = \"re: \"\n            if not subject.startswith(re):\n                subject = re + subject\n\n            item, inbox_rel = Message._new(c.user, to, subject, comment,\n                                           request.ip, parent=parent)\n        else:\n            # Don't let users in timeout comment\n            VNotInTimeout().run(action_name='comment', target=parent)\n\n            item, inbox_rel = Comment._new(c.user, link, parent_comment,\n                                           comment, request.ip)\n\n        if is_message:\n            queries.new_message(item, inbox_rel)\n        else:\n            queries.new_comment(item, inbox_rel)\n\n        if should_ratelimit:\n            VRatelimit.ratelimit(rate_user=True, rate_ip = True,\n                                 prefix = \"rate_comment_\")\n\n        # clean up the submission form and remove it from the DOM (if reply)\n        t = commentform.find(\"textarea\")\n        t.attr('rows', 3).html(\"\").val(\"\")\n        if isinstance(parent, (Comment, Message)):\n            commentform.remove()\n            jquery.things(parent._fullname).set_text(\".reply-button:first\",\n                                                     _(\"replied\"))\n\n        # insert the new comment\n        jquery.insert_things(item)\n\n        # remove any null listings that may be present\n        jquery(\"#noresults\").hide()\n\n    @validatedForm(\n        VUser(),\n        VModhash(),\n        VShareRatelimit(),\n        share_to=ValidEmailsOrExistingUnames(\"share_to\"),\n        message=VLength(\"message\", max_length=1000),\n        link=VByName('parent', thing_cls=Link),\n    )\n    def POST_share(self, shareform, jquery, share_to, message, link):\n        if not link:\n            abort(404, 'not found')\n\n        # remove the ratelimit error if the user's karma is high\n        sr = link.subreddit_slow\n        should_ratelimit = sr.should_ratelimit(c.user, 'link')\n        if not should_ratelimit:\n            c.errors.remove((errors.RATELIMIT, 'ratelimit'))\n\n        if shareform.has_errors(\"message\", errors.TOO_LONG):\n            return\n        elif shareform.has_errors(\"share_to\", errors.BAD_EMAILS,\n                                  errors.NO_EMAILS,\n                                  errors.TOO_MANY_EMAILS):\n            return\n        elif shareform.has_errors(\"ratelimit\", errors.RATELIMIT):\n            return\n\n        subreddit = link.subreddit_slow\n\n        if subreddit.quarantine or not subreddit.can_view(c.user):\n            return abort(403, 'forbidden')\n\n        VNotInTimeout().run(target=link, subreddit=subreddit)\n\n        emails, users = share_to\n\n        # disallow email share for accounts without a verified email address\n        if emails and (not c.user.email or not c.user.email_verified):\n            return abort(403, 'forbidden')\n\n        link_title = _force_unicode(link.title)\n\n        if getattr(link, \"promoted\", None) and link.disable_comments:\n            message = blockquote_text(message) + \"\\n\\n\" if message else \"\"\n            message += '\\n%s\\n\\n%s\\n\\n' % (link_title, link.url)\n            email_message = pm_message = message\n        else:\n            message = blockquote_text(message) + \"\\n\\n\" if message else \"\"\n            message += '\\n%s\\n' % link_title\n\n            message_body = '\\n'\n\n            # Deliberately not translating this, as it'd be in the\n            # sender's language\n            if link.num_comments:\n                count = (\"There are currently %(num_comments)s comments \" +\n                         \"on this link.  You can view them here:\")\n                if link.num_comments == 1:\n                    count = (\"There is currently %(num_comments)s \" +\n                             \"comment on this link.  You can view it here:\")\n                numcom = count % {'num_comments': link.num_comments}\n                message_body = message_body + \"%s\\n\\n\" % numcom\n            else:\n                message_body = message_body + \"You can leave a comment here:\\n\\n\"\n\n            url = add_sr(link.make_permalink_slow(), force_hostname=True)\n            url_parser = UrlParser(url)\n            url_parser.update_query(ref=\"share\", ref_source=\"email\")\n            email_comments_url = url_parser.unparse()\n            url_parser.update_query(ref_source=\"pm\")\n            pm_comments_url = url_parser.unparse()\n\n            message_body += '%(comments_url)s'\n            email_message = message + message_body % {\n                    \"comments_url\": email_comments_url,\n                }\n            pm_message = message + message_body % {\n                    \"comments_url\": pm_comments_url,\n                }\n        \n        # E-mail everyone\n        emailer.share(link, emails, body=email_message or \"\")\n\n        # Send the PMs\n        subject = \"%s has shared a link with you!\" % c.user.name\n        # Prepend this subject to the message - we're repeating ourselves\n        # because it looks very abrupt without it.\n        pm_message = \"%s\\n\\n%s\" % (subject, pm_message)\n        \n        for target in users:\n            m, inbox_rel = Message._new(c.user, target, subject,\n                                        pm_message, request.ip)\n            # Queue up this PM\n            amqp.add_item('new_message', m._fullname)\n\n            queries.new_message(m, inbox_rel)\n\n        g.stats.simple_event('share.email_sent', len(emails))\n        g.stats.simple_event('share.pm_sent', len(users))\n\n        # Set the ratelimiter.\n        VShareRatelimit.ratelimit()\n\n    @require_oauth2_scope(\"vote\")\n    @noresponse(VUser(),\n                VModhash(),\n                direction=VInt(\"dir\", min=-1, max=1,\n                    docs={\"dir\": \"vote direction. one of (1, 0, -1)\"}\n                ),\n                thing=VByName('id'),\n                rank=VInt(\"rank\", min=1))\n    @api_doc(api_section.links_and_comments)\n    def POST_vote(self, direction, thing, rank):\n        \"\"\"Cast a vote on a thing.\n\n        `id` should be the fullname of the Link or Comment to vote on.\n\n        `dir` indicates the direction of the vote. Voting `1` is an upvote,\n        `-1` is a downvote, and `0` is equivalent to \"un-voting\" by clicking\n        again on a highlighted arrow.\n\n        **Note: votes must be cast by humans.** That is, API clients proxying a\n        human's action one-for-one are OK, but bots deciding how to vote on\n        content or amplifying a human's vote are not. See [the reddit\n        rules](/rules) for more details on what constitutes vote cheating.\n\n        \"\"\"\n\n        # a persistent A/A to provide a consistent event stream and confidence\n        # in bucketing to the data team\n        feature.is_enabled('persistent_vote_a_a')\n\n        if not thing or thing._deleted:\n            return self.abort404()\n\n        if not thing.is_votable:\n            abort(400, \"That type of thing can't be voted on.\")\n\n        hooks.get_hook(\"vote.validate\").call(thing=thing)\n\n        if isinstance(thing, Link) and promote.is_promo(thing):\n            if not promote.is_promoted(thing):\n                return abort(400, \"Bad Request\")\n\n        if thing.archived_slow:\n            return abort(400,\n                \"This thing is archived and may no longer be voted on\")\n\n        # Don't allow users in timeout to vote\n        VNotInTimeout().run(target=thing)\n\n        # convert vote direction to enum value\n        if direction == 1:\n            direction = Vote.DIRECTIONS.up\n        elif direction == -1:\n            direction = Vote.DIRECTIONS.down\n        elif direction == 0:\n            direction = Vote.DIRECTIONS.unvote\n\n        cast_vote(c.user, thing, direction, rank=rank)\n\n    @require_oauth2_scope(\"modconfig\")\n    @validatedForm(VSrModerator(perms='config'),\n                   VModhash(),\n                   # nop is safe: handled after auth checks below\n                   stylesheet_contents=nop('stylesheet_contents',\n                       docs={\"stylesheet_contents\":\n                             \"the new stylesheet content\"}),\n                   reason=VPrintable('reason', 256, empty_error=None),\n                   op = VOneOf('op',['save','preview']))\n    @api_doc(api_section.subreddits, uses_site=True)\n    def POST_subreddit_stylesheet(self, form, jquery,\n                                  stylesheet_contents = '', prevstyle='',\n                                  op='save', reason=None):\n        \"\"\"Update a subreddit's stylesheet.\n\n        `op` should be `save` to update the contents of the stylesheet.\n\n        \"\"\"\n\n        if g.css_killswitch:\n            return abort(403, 'forbidden')\n\n        css_errors, parsed = c.site.parse_css(stylesheet_contents)\n\n        # The hook passes errors back by setting them on the form.\n        hooks.get_hook('subreddit.css.validate').call(\n            request=request, form=form, op=op,\n            stylesheet_contents=stylesheet_contents,\n            parsed_stylesheet=parsed,\n            css_errors=css_errors,\n            subreddit=c.site,\n            user=c.user\n        )\n\n        if css_errors:\n            error_items = [CssError(x).render(style='html') for x in css_errors]\n            form.set_text(\".status\", _('validation errors'))\n            form.set_html(\".errors ul\", ''.join(error_items))\n            form.find('.errors').show()\n            c.errors.add(errors.BAD_CSS, field=\"stylesheet_contents\")\n            form.has_errors(\"stylesheet_contents\", errors.BAD_CSS)\n            return\n        else:\n            form.find('.errors').hide()\n            form.set_html(\".errors ul\", '')\n\n        # Don't allow users in timeout to modify the stylesheet\n        VNotInTimeout().run(action_name=\"editsettings\",\n            details_text=\"%s_stylesheet\" % op, target=c.site)\n\n        if op == 'save' and not form.has_error():\n            wr = c.site.change_css(stylesheet_contents, parsed, reason=reason)\n            form.find('.errors').hide()\n            form.set_text(\".status\", _('saved'))\n            form.set_html(\".errors ul\", \"\")\n\n        jquery.apply_stylesheet(parsed)\n\n        if op == 'preview':\n            # try to find a link to use, otherwise give up and\n            # return\n            links = SubredditStylesheet.find_preview_links(c.site)\n            if links:\n\n                jquery('#preview-table').show()\n    \n                # do a regular link\n                jquery('#preview_link_normal').html(\n                    SubredditStylesheet.rendered_link(\n                        links, media='off', compress=False))\n                # now do one with media\n                jquery('#preview_link_media').html(\n                    SubredditStylesheet.rendered_link(\n                        links, media='on', compress=False))\n                # do a compressed link\n                jquery('#preview_link_compressed').html(\n                    SubredditStylesheet.rendered_link(\n                        links, media='off', compress=True))\n                # do a stickied link\n                jquery('#preview_link_stickied').html(\n                    SubredditStylesheet.rendered_link(\n                        links, media='off', compress=False, stickied=True))\n    \n            # and do a comment\n            comments = SubredditStylesheet.find_preview_comments(c.site)\n            if comments:\n                jquery('#preview_comment').html(\n                    SubredditStylesheet.rendered_comment(comments))\n\n                jquery('#preview_comment_gilded').html(\n                    SubredditStylesheet.rendered_comment(\n                        comments, gilded=True))\n\n    @require_oauth2_scope(\"modconfig\")\n    @validatedForm(VSrModerator(perms='config'),\n                   VModhash(),\n                   name = VCssName('img_name'))\n    @api_doc(api_section.subreddits, uses_site=True)\n    def POST_delete_sr_img(self, form, jquery, name):\n        \"\"\"Remove an image from the subreddit's custom image set.\n\n        The image will no longer count against the subreddit's image limit.\n        However, the actual image data may still be accessible for an\n        unspecified amount of time. If the image is currently referenced by the\n        subreddit's stylesheet, that stylesheet will no longer validate and\n        won't be editable until the image reference is removed.\n\n        See also: [/api/upload_sr_img](#POST_api_upload_sr_img).\n\n        \"\"\"\n        # just in case we need to kill this feature from XSS\n        if g.css_killswitch:\n            return abort(403, 'forbidden')\n\n        if form.has_errors(\"img_name\", errors.BAD_CSS_NAME):\n            return\n\n        # Don't allow users in timeout to modify the stylesheet\n        VNotInTimeout().run(action_name=\"editsettings\",\n            details_text=\"del_image\", target=c.site)\n\n        wiki.ImagesByWikiPage.delete_image(c.site, \"config/stylesheet\", name)\n        ModAction.create(c.site, c.user, action='editsettings', \n                         details='del_image', description=name)\n\n    @require_oauth2_scope(\"modconfig\")\n    @validatedForm(\n        VSrModerator(perms='config'),\n        VModhash(),\n        VNotInTimeout(),\n    )\n    @api_doc(api_section.subreddits, uses_site=True)\n    def POST_delete_sr_header(self, form, jquery):\n        \"\"\"Remove the subreddit's custom header image.\n\n        The sitewide-default header image will be shown again after this call.\n\n        See also: [/api/upload_sr_img](#POST_api_upload_sr_img).\n\n        \"\"\"\n        # just in case we need to kill this feature from XSS\n        if g.css_killswitch:\n            return abort(403, 'forbidden')\n\n        if c.site.header:\n            c.site.header = None\n            c.site.header_size = None\n            c.site._commit()\n            ModAction.create(c.site, c.user, action='editsettings', \n                             details='del_header')\n\n        # hide the button which started this\n        form.find('.delete-img').hide()\n        # hide the preview box\n        form.find('.img-preview-container').hide()\n        # reset the status boxes\n        form.set_text('.img-status', _(\"deleted\"))\n        \n    @require_oauth2_scope(\"modconfig\")\n    @validatedForm(\n        VSrModerator(perms='config'),\n        VModhash(),\n        VNotInTimeout(),\n    )\n    @api_doc(api_section.subreddits, uses_site=True)\n    def POST_delete_sr_icon(self, form, jquery):\n        \"\"\"Remove the subreddit's custom mobile icon.\n\n        See also: [/api/upload_sr_img](#POST_api_upload_sr_img).\n\n        \"\"\"\n        if c.site.icon_img:\n            c.site.icon_img = None\n            c.site.icon_size = None\n            c.site._commit()\n            ModAction.create(c.site, c.user, action='editsettings',\n                             details='del_icon')\n\n        # hide the button which started this\n        form.find('.delete-img').hide()\n        # hide the preview box\n        form.find('.img-preview-container').hide()\n        # reset the status boxes\n        form.set_text('.img-status', _(\"deleted\"))\n\n    @require_oauth2_scope(\"modconfig\")\n    @validatedForm(\n        VSrModerator(perms='config'),\n        VModhash(),\n        VNotInTimeout(),\n    )\n    @api_doc(api_section.subreddits, uses_site=True)\n    def POST_delete_sr_banner(self, form, jquery):\n        \"\"\"Remove the subreddit's custom mobile banner.\n\n        See also: [/api/upload_sr_img](#POST_api_upload_sr_img).\n\n        \"\"\"\n        if c.site.banner_img:\n            c.site.banner_img = None\n            c.site.banner_size = None\n            c.site._commit()\n            ModAction.create(c.site, c.user, action='editsettings',\n                             details='del_banner')\n\n        # hide the button which started this\n        form.find('.delete-img').hide()\n        # hide the preview box\n        form.find('.img-preview-container').hide()\n        # reset the status boxes\n        form.set_text('.img-status', _(\"deleted\"))\n\n    def GET_upload_sr_img(self, *a, **kw):\n        \"\"\"\n        Completely unnecessary method which exists because safari can\n        be dumb too.  On page reload after an image has been posted in\n        safari, the iframe to which the request posted preserves the\n        URL of the POST, and safari attempts to execute a GET against\n        it.  The iframe is hidden, so what it returns is completely\n        irrelevant.\n        \"\"\"\n        return \"nothing to see here.\"\n\n    @require_oauth2_scope(\"modconfig\")\n    @validate(VSrModerator(perms='config'),\n              VModhash(),\n              file = VUploadLength('file', max_length=1024*500),\n              name = VCssName(\"name\"),\n              img_type = VImageType('img_type'),\n              form_id = VLength('formid', max_length = 100,\n                                docs={\"formid\": \"(optional) can be ignored\"}),\n              upload_type = VOneOf('upload_type',\n                                   ('img', 'header', 'icon', 'banner')),\n              header = VInt('header', max=1, min=0))\n    @api_doc(api_section.subreddits, uses_site=True)\n    def POST_upload_sr_img(self, file, header, name, form_id, img_type,\n                           upload_type=None):\n        \"\"\"Add or replace a subreddit image, custom header logo, custom mobile\n        icon, or custom mobile banner.\n\n        * If the `upload_type` value is `img`, an image for use in the\n        subreddit stylesheet is uploaded with the name specified in `name`.\n        * If the `upload_type` value is `header` then the image uploaded will\n        be the subreddit's new logo and `name` will be ignored.\n        * If the `upload_type` value is `icon` then the image uploaded will be\n        the subreddit's new mobile icon and `name` will be ignored.\n        * If the `upload_type` value is `banner` then the image uploaded will\n        be the subreddit's new mobile banner and `name` will be ignored.\n\n        For backwards compatibility, if `upload_type` is not specified, the\n        `header` field will be used instead:\n\n        * If the `header` field has value `0`, then `upload_type` is `img`.\n        * If the `header` field has value `1`, then `upload_type` is `header`.\n\n        The `img_type` field specifies whether to store the uploaded image as a\n        PNG or JPEG.\n\n        Subreddits have a limited number of images that can be in use at any\n        given time. If no image with the specified name already exists, one of\n        the slots will be consumed.\n\n        If an image with the specified name already exists, it will be\n        replaced.  This does not affect the stylesheet immediately, but will\n        take effect the next time the stylesheet is saved.\n\n        See also: [/api/delete_sr_img](#POST_api_delete_sr_img),\n        [/api/delete_sr_header](#POST_api_delete_sr_header),\n        [/api/delete_sr_icon](#POST_api_delete_sr_icon), and\n        [/api/delete_sr_banner](#POST_api_delete_sr_banner).\n\n        \"\"\"\n\n        if c.site.quarantine:\n            abort(403)\n\n        # default error list (default values will reset the errors in\n        # the response if no error is raised)\n        errors = dict(BAD_CSS_NAME = \"\", IMAGE_ERROR = \"\")\n\n        # for backwards compatibility, map header to upload_type\n        if upload_type is None:\n            upload_type = 'header' if header else 'img'\n        \n        if upload_type == 'img' and not name:\n            # error if the name wasn't specified and the image was not for a sponsored link or header\n            # this may also fail if a sponsored image was added and the user is not an admin\n            errors['BAD_CSS_NAME'] = _(\"bad image name\")\n        \n        if upload_type == 'img' and not c.user_is_admin:\n            image_count = wiki.ImagesByWikiPage.get_image_count(\n                c.site, \"config/stylesheet\")\n            if image_count >= g.max_sr_images:\n                errors['IMAGE_ERROR'] = _(\"too many images (you only get %d)\") % g.max_sr_images\n\n        try:\n            size = str_to_image(file).size\n        except (IOError, TypeError):\n            errors['IMAGE_ERROR'] = _('Invalid image or general image error')\n        else:\n            if upload_type == 'icon':\n                if size != Subreddit.ICON_EXACT_SIZE:\n                    errors['IMAGE_ERROR'] = (\n                        _('must be %dx%d pixels') % Subreddit.ICON_EXACT_SIZE)\n            elif upload_type == 'banner':\n                aspect_ratio = float(size[0]) / size[1]\n                if abs(Subreddit.BANNER_ASPECT_RATIO - aspect_ratio) > 0.01:\n                    errors['IMAGE_ERROR'] = _('10:3 aspect ratio required')\n                elif size > Subreddit.BANNER_MAX_SIZE:\n                    errors['IMAGE_ERROR'] = (\n                        _('max %dx%d pixels') % Subreddit.BANNER_MAX_SIZE)\n                elif size < Subreddit.BANNER_MIN_SIZE:\n                    errors['IMAGE_ERROR'] = (\n                        _('min %dx%d pixels') % Subreddit.BANNER_MIN_SIZE)\n\n        if any(errors.values()):\n            return UploadedImage(\"\", \"\", \"\", errors=errors, form_id=form_id).render()\n        else:\n            try:\n                new_url = media.upload_media(file, file_type=\".\" + img_type)\n            except Exception as e:\n                g.log.warning(\"error uploading subreddit image: %s\", e)\n                errors['IMAGE_ERROR'] = _(\"Invalid image or general image error\")\n                return UploadedImage(\"\", \"\", \"\", errors=errors, form_id=form_id).render()\n\n            details_text = \"upload_image\"\n            if not upload_type == \"img\":\n                details_text = \"upload_image_%s\" % upload_type\n            VNotInTimeout().run(action_name=\"editsettings\",\n                details_text=details_text, target=c.site)\n\n            if upload_type == 'img':\n                wiki.ImagesByWikiPage.add_image(c.site, \"config/stylesheet\",\n                                                name, new_url)\n                kw = dict(details='upload_image', description=name)\n            elif upload_type == 'header':\n                c.site.header = new_url\n                c.site.header_size = size\n                c.site._commit()\n                kw = dict(details='upload_image_header')\n            elif upload_type == 'icon':\n                c.site.icon_img = new_url\n                c.site.icon_size = size\n                c.site._commit()\n                kw = dict(details='upload_image_icon')\n            elif upload_type == 'banner':\n                c.site.banner_img = new_url\n                c.site.banner_size = size\n                c.site._commit()\n                kw = dict(details='upload_image_banner')\n\n            ModAction.create(c.site, c.user, action='editsettings', **kw)\n\n            return UploadedImage(_('saved'), new_url, name, \n                                 errors=errors, form_id=form_id).render()\n\n    @require_oauth2_scope(\"modconfig\")\n    @validatedForm(VUser(),\n                   VCaptcha(),\n                   VModhash(),\n                   VRatelimit(rate_user = True,\n                              rate_ip = True,\n                              prefix = 'create_reddit_'),\n                   sr = VByName('sr'),\n                   name = VAvailableSubredditName(\"name\"),\n                   title = VLength(\"title\", max_length = 100),\n                   header_title = VLength(\"header-title\", max_length = 500),\n                   domain = VCnameDomain(\"domain\"),\n                   submit_text = VMarkdownLength(\"submit_text\", max_length=1024),\n                   public_description = VMarkdownLength(\"public_description\", max_length = 500),\n                   description = VMarkdownLength(\"description\", max_length = 5120),\n                   lang = VLang(\"lang\"),\n                   over_18 = VBoolean('over_18'),\n                   allow_top = VBoolean('allow_top'),\n                   show_media = VBoolean('show_media'),\n                   # show_media_preview = VBoolean('show_media_preview'),\n                   public_traffic = VBoolean('public_traffic'),\n                   collapse_deleted_comments = VBoolean('collapse_deleted_comments'),\n                   exclude_banned_modqueue = VBoolean('exclude_banned_modqueue'),\n                   spam_links = VOneOf('spam_links', ('low', 'high', 'all')),\n                   spam_selfposts = VOneOf('spam_selfposts', ('low', 'high', 'all')),\n                   spam_comments = VOneOf('spam_comments', ('low', 'high', 'all')),\n                   type = VOneOf('type', Subreddit.valid_types),\n                   link_type = VOneOf('link_type', ('any', 'link', 'self')),\n                   submit_link_label=VLength('submit_link_label', max_length=60),\n                   submit_text_label=VLength('submit_text_label', max_length=60),\n                   comment_score_hide_mins=VInt('comment_score_hide_mins',\n                       coerce=False, num_default=0, min=0, max=1440),\n                   wikimode = VOneOf('wikimode', ('disabled', 'modonly', 'anyone')),\n                   wiki_edit_karma = VInt(\"wiki_edit_karma\", coerce=False, num_default=0, min=0),\n                   wiki_edit_age = VInt(\"wiki_edit_age\", coerce=False, num_default=0, min=0),\n                   hide_ads = VBoolean(\"hide_ads\"),\n                   suggested_comment_sort=VOneOf('suggested_comment_sort',\n                                                 CommentSortMenu._options,\n                                                 default=None),\n                   # related_subreddits = VSubredditList('related_subreddits', limit=20),\n                   # key_color = VColor('key_color'),\n                   )\n    @api_doc(api_section.subreddits)\n    def POST_site_admin(self, form, jquery, name, sr, **kw):\n        \"\"\"Create or configure a subreddit.\n\n        If `sr` is specified, the request will attempt to modify the specified\n        subreddit. If not, a subreddit with name `name` will be created.\n\n        This endpoint expects *all* values to be supplied on every request.  If\n        modifying a subset of options, it may be useful to get the current\n        settings from [/about/edit.json](#GET_r_{subreddit}_about_edit.json)\n        first.\n\n        For backwards compatibility, `description` is the sidebar text and\n        `public_description` is the publicly visible subreddit description.\n\n        Most of the parameters for this endpoint are identical to options\n        visible in the user interface and their meanings are best explained\n        there.\n\n        See also: [/about/edit.json](#GET_r_{subreddit}_about_edit.json).\n\n        \"\"\"\n        def apply_wikid_field(sr, form, pagename, value, field):\n            try:\n                wikipage = wiki.WikiPage.get(sr, pagename)\n            except tdb_cassandra.NotFound:\n                wikipage = wiki.WikiPage.create(sr, pagename)\n            wr = wikipage.revise(value, author=c.user._id36)\n            setattr(sr, field, value)\n            if wr:\n                ModAction.create(sr, c.user, 'wikirevise',\n                                 details=wiki.modactions.get(pagename))\n\n        # This should be moved to @validatedForm above when we remove\n        # the feature flag. Down here to avoid processing when flagged off\n        # and to hide from API docs.\n        if feature.is_enabled('mobile_settings'):\n            validator = VColor('key_color')\n            value = request.params.get('key_color')\n            kw['key_color'] = validator.run(value)\n        if feature.is_enabled('related_subreddits'):\n            validator = VSubredditList('related_subreddits', limit=20)\n            value = request.params.get('related_subreddits')\n            kw['related_subreddits'] = validator.run(value)\n\n        if feature.is_enabled('autoexpand_media_previews'):\n            validator = VBoolean('show_media_preview')\n            value = request.params.get('show_media_preview')\n            kw[\"show_media_preview\"] = validator.run(value)\n\n        # the status button is outside the form -- have to reset by hand\n        form.parent().set_html('.status', \"\")\n\n        redir = False\n        keyword_fields = [\n            'allow_top',\n            'collapse_deleted_comments',\n            'comment_score_hide_mins',\n            'description',\n            'domain',\n            'exclude_banned_modqueue',\n            'header_title',\n            'hide_ads',\n            'lang',\n            'link_type',\n            'name',\n            'over_18',\n            'public_description',\n            'public_traffic',\n            'show_media',\n            'show_media_preview',\n            'spam_comments',\n            'spam_links',\n            'spam_selfposts',\n            'submit_link_label',\n            'submit_text',\n            'submit_text_label',\n            'suggested_comment_sort',\n            'title',\n            'type',\n            'wiki_edit_age',\n            'wiki_edit_karma',\n            'wikimode',\n        ]\n\n        if feature.is_enabled('mobile_settings'):\n            keyword_fields.append('key_color')\n        if sr and feature.is_enabled('related_subreddits'):\n            keyword_fields.append('related_subreddits')\n\n        kw = {k: v for k, v in kw.iteritems() if k in keyword_fields}\n\n        public_description = kw.pop('public_description')\n        description = kw.pop('description')\n        submit_text = kw.pop('submit_text')\n\n        def update_wiki_text(sr):\n            error = False\n            apply_wikid_field(\n                sr,\n                form,\n                'config/sidebar',\n                description,\n                'description',\n            )\n\n            apply_wikid_field(\n                sr,\n                form,\n                'config/submit_text',\n                submit_text,\n                'submit_text',\n            )\n\n            apply_wikid_field(\n                sr,\n                form,\n                'config/description',\n                public_description,\n                'public_description',\n            )\n        \n        if not sr and not c.user.can_create_subreddit:\n            form.set_error(errors.CANT_CREATE_SR, \"\")\n            c.errors.add(errors.CANT_CREATE_SR, field=\"\")\n\n        # only care about captcha if this is creating a subreddit\n        if not sr and form.has_errors(\"captcha\", errors.BAD_CAPTCHA):\n            return\n\n        domain = kw['domain']\n        cname_sr = domain and Subreddit._by_domain(domain)\n        if cname_sr and (not sr or sr != cname_sr):\n            c.errors.add(errors.USED_CNAME)\n\n        can_set_archived = c.user_is_admin or (sr and sr.type == 'archived')\n        if kw['type'] == 'archived' and not can_set_archived:\n            c.errors.add(errors.INVALID_OPTION, field='type')\n\n        can_set_gold_restricted = c.user_is_admin or (sr and sr.type == 'gold_restricted')\n        if kw['type'] == 'gold_restricted' and not can_set_gold_restricted:\n            c.errors.add(errors.INVALID_OPTION, field='type')\n\n        # can't create a gold only subreddit without having gold\n        can_set_gold_only = (c.user.gold or c.user.gold_charter or\n                (sr and sr.type == 'gold_only'))\n        if kw['type'] == 'gold_only' and not can_set_gold_only:\n            form.set_error(errors.GOLD_REQUIRED, 'type')\n            c.errors.add(errors.GOLD_REQUIRED, field='type')\n\n        can_set_hide_ads = can_set_gold_only and kw['type'] == 'gold_only'\n        if kw['hide_ads'] and not can_set_hide_ads:\n            form.set_error(errors.GOLD_ONLY_SR_REQUIRED, 'hide_ads')\n            c.errors.add(errors.GOLD_ONLY_SR_REQUIRED, field='hide_ads')\n        elif not can_set_hide_ads and sr:\n            kw['hide_ads'] = sr.hide_ads\n\n        can_set_employees_only = c.user.employee\n        if kw['type'] == 'employees_only' and not can_set_employees_only:\n            c.errors.add(errors.INVALID_OPTION, field='type')\n\n        if not sr and form.has_errors(\"ratelimit\", errors.RATELIMIT):\n            pass\n        elif not sr and form.has_errors(\"\", errors.CANT_CREATE_SR):\n            pass\n        # if existing subreddit is employees_only and trying to change type,\n        # require that admin mode is on\n        elif (sr and sr.type == 'employees_only' and kw['type'] != sr.type and\n                not c.user_is_admin):\n            form.set_error(errors.ADMIN_REQUIRED, 'type')\n            c.errors.add(errors.ADMIN_REQUIRED, field='type')\n        # if the user wants to convert an existing subreddit to gold_only,\n        # let them know that they'll need to contact an admin to convert it.\n        elif (sr and sr.type != 'gold_only' and kw['type'] == 'gold_only' and\n                not c.user_is_admin):\n            form.set_error(errors.CANT_CONVERT_TO_GOLD_ONLY, 'type')\n            c.errors.add(errors.CANT_CONVERT_TO_GOLD_ONLY, field='type')\n        elif form.has_errors('type', errors.GOLD_REQUIRED):\n            pass\n        elif not sr and form.has_errors(\"name\", errors.SUBREDDIT_EXISTS,\n                                        errors.BAD_SR_NAME):\n            form.find('#example_name').hide()\n        elif form.has_errors('title', errors.NO_TEXT, errors.TOO_LONG):\n            form.find('#example_title').hide()\n        elif form.has_errors('domain', errors.BAD_CNAME, errors.USED_CNAME):\n            form.find('#example_domain').hide()\n        elif (form.has_errors(('type', 'link_type', 'wikimode'),\n                              errors.INVALID_OPTION) or\n              form.has_errors(('public_description',\n                               'submit_text',\n                               'description'), errors.TOO_LONG)):\n            pass\n        elif (form.has_errors(('wiki_edit_karma', 'wiki_edit_age'), \n                              errors.BAD_NUMBER)):\n            pass\n        elif form.has_errors('comment_score_hide_mins', errors.BAD_NUMBER):\n            pass\n        elif form.has_errors('related_subreddits', errors.SUBREDDIT_NOEXIST,\n                             errors.BAD_SR_NAME, errors.TOO_MANY_SUBREDDITS):\n            pass\n        elif form.has_errors('hide_ads', errors.GOLD_ONLY_SR_REQUIRED):\n            pass\n        #creating a new reddit\n        elif not sr:\n            # Don't allow user in timeout to create a new subreddit\n            VNotInTimeout().run(action_name=\"createsubreddit\", target=None)\n\n            #sending kw is ok because it was sanitized above\n            sr = Subreddit._new(name = name, author_id = c.user._id,\n                                ip=request.ip, **kw)\n\n            update_wiki_text(sr)\n            sr._commit()\n\n            hooks.get_hook(\"subreddit.new\").call(subreddit=sr)\n\n            Subreddit.subscribe_defaults(c.user)\n            sr.add_subscriber(c.user)\n            sr.add_moderator(c.user)\n\n            if not sr.hide_contributors:\n                sr.add_contributor(c.user)\n            redir = sr.path + \"about/edit/?created=true\"\n            if not c.user_is_admin:\n                VRatelimit.ratelimit(rate_user=True,\n                                     rate_ip = True,\n                                     prefix = \"create_reddit_\")\n\n            queries.new_subreddit(sr)\n            sr.update_search_index()\n\n        #editting an existing reddit\n        elif sr.is_moderator_with_perms(c.user, 'config') or c.user_is_admin:\n            # Don't allow user in timeout to edit subreddit settings\n            VNotInTimeout().run(action_name=\"editsettings\", target=sr)\n\n            #assume sr existed, or was just built\n            old_domain = sr.domain\n\n            update_wiki_text(sr)\n\n            if sr.quarantine:\n                del kw['allow_top']\n                del kw['show_media']\n                del kw['show_media_preview']\n\n            #notify ads if sr in a collection changes over_18 to true\n            if kw.get('over_18', False) and not sr.over_18:\n                collections = []\n                for collection in Collection.get_all():\n                    if (sr.name in collection.sr_names\n                            and not collection.over_18):\n                        collections.append(collection.name)\n\n                if collections:\n                    msg = \"%s now NSFW, in collection(s) %s\"\n                    msg %= (sr.name, ', '.join(collections))\n                    emailer.sales_email(msg)\n\n            for k, v in kw.iteritems():\n                if getattr(sr, k, None) != v:\n                    ModAction.create(sr, c.user, action='editsettings',\n                                     details=k)\n\n                setattr(sr, k, v)\n            sr._commit()\n\n            #update the domain cache if the domain changed\n            if sr.domain != old_domain:\n                Subreddit._by_domain(old_domain, _update = True)\n                Subreddit._by_domain(sr.domain, _update = True)\n\n            sr.update_search_index()\n            form.parent().set_text('.status', _(\"saved\"))\n\n        if form.has_error():\n            return\n\n        if redir:\n            form.redirect(redir)\n        else:\n            jquery.refresh()\n\n    @require_oauth2_scope(\"modposts\")\n    @noresponse(VUser(), VModhash(),\n                VSrCanBan('id'),\n                thing = VByName('id'),\n                spam = VBoolean('spam', default=True))\n    @api_doc(api_section.moderation)\n    def POST_remove(self, thing, spam):\n        \"\"\"Remove a link, comment, or modmail message.\n\n        If the thing is a link, it will be removed from all subreddit listings.\n        If the thing is a comment, it will be redacted and removed from all\n        subreddit comment listings.\n\n        See also: [/api/approve](#POST_api_approve).\n\n        \"\"\"\n\n        # Don't remove a promoted link\n        if getattr(thing, \"promoted\", None):\n            return\n        action_name = \"remove\"\n        if spam:\n            action_name = \"spam\"\n        VNotInTimeout().run(action_name=action_name, target=thing)\n\n        if thing._deleted:\n            return\n\n        filtered = thing._spam\n        kw = {'target': thing}\n\n        if filtered and spam:\n            kw['details'] = 'confirm_spam'\n            train_spam = False\n        elif filtered and not spam:\n            kw['details'] = 'remove'\n            admintools.unspam(thing, unbanner=c.user.name, insert=False)\n            train_spam = False\n        elif not filtered and spam:\n            kw['details'] = 'spam'\n            train_spam = True\n        elif not filtered and not spam:\n            kw['details'] = 'remove'\n            train_spam = False\n\n        admintools.spam(thing, auto=False,\n                        moderator_banned=not c.user_is_admin,\n                        banner=c.user.name,\n                        train_spam=train_spam)\n\n        if isinstance(thing, (Link, Comment)):\n            sr = thing.subreddit_slow\n            action = 'remove' + thing.__class__.__name__.lower()\n            ModAction.create(sr, c.user, action, **kw)\n\n        if isinstance(thing, Link):\n            sr.remove_sticky(thing)\n        elif isinstance(thing, Comment):\n            thing.link_slow.remove_sticky_comment(comment=thing, set_by=c.user)\n            queries.unnotify(thing)\n\n\n    @require_oauth2_scope(\"modposts\")\n    @noresponse(VUser(), VModhash(),\n                VSrCanBan('id'),\n                thing = VByName('id'))\n    @api_doc(api_section.moderation)\n    def POST_approve(self, thing):\n        \"\"\"Approve a link or comment.\n\n        If the thing was removed, it will be re-inserted into appropriate\n        listings. Any reports on the approved thing will be discarded.\n\n        See also: [/api/remove](#POST_api_remove).\n\n        \"\"\"\n        if not thing: return\n        if thing._deleted: return\n        if c.user._spam:\n           self.abort403()\n\n        # Don't allow user in timeout to approve link or comment\n        VNotInTimeout().run(target=thing)\n\n        kw = {'target': thing}\n        if thing._spam:\n            kw['details'] = 'unspam'\n            train_spam = True\n            insert = True\n        else:\n            kw['details'] = 'confirm_ham'\n            train_spam = False\n            insert = False\n\n        admintools.unspam(thing, moderator_unbanned=not c.user_is_admin,\n                          unbanner=c.user.name, train_spam=train_spam,\n                          insert=insert)\n\n        if isinstance(thing, (Link, Comment)):\n            sr = thing.subreddit_slow\n            action = 'approve' + thing.__class__.__name__.lower()\n            ModAction.create(sr, c.user, action, **kw)\n\n        if isinstance(thing, Comment) and insert:\n            queries.renotify(thing)\n\n    @require_oauth2_scope(\"modposts\")\n    @noresponse(VUser(), VModhash(),\n                VSrCanBan('id'),\n                thing=VByName('id'))\n    @api_doc(api_section.moderation)\n    def POST_ignore_reports(self, thing):\n        \"\"\"Prevent future reports on a thing from causing notifications.\n\n        Any reports made about a thing after this flag is set on it will not\n        cause notifications or make the thing show up in the various moderation\n        listings.\n\n        See also: [/api/unignore_reports](#POST_api_unignore_reports).\n\n        \"\"\"\n        if not thing: return\n        if thing._deleted: return\n        if thing.ignore_reports: return\n\n        # Don't allow user in timeout to ignore reports\n        VNotInTimeout().run(action_name=\"ignorereports\", target=thing)\n\n        thing.ignore_reports = True\n        thing._commit()\n\n        sr = thing.subreddit_slow\n        ModAction.create(sr, c.user, 'ignorereports', target=thing)\n\n    @require_oauth2_scope(\"modposts\")\n    @noresponse(VUser(), VModhash(),\n                VSrCanBan('id'),\n                thing=VByName('id'))\n    @api_doc(api_section.moderation)\n    def POST_unignore_reports(self, thing):\n        \"\"\"Allow future reports on a thing to cause notifications.\n\n        See also: [/api/ignore_reports](#POST_api_ignore_reports).\n\n        \"\"\"\n        if not thing: return\n        if thing._deleted: return\n        if not thing.ignore_reports: return\n\n        # Don't allow user in timeout to unignore reports\n        VNotInTimeout().run(action_name=\"unignorereports\", target=thing)\n\n        thing.ignore_reports = False\n        thing._commit()\n\n        sr = thing.subreddit_slow\n        ModAction.create(sr, c.user, 'unignorereports', target=thing)\n\n    @require_oauth2_scope(\"modposts\")\n    @validatedForm(VUser(), VModhash(),\n                   VCanDistinguish(('id', 'how')),\n                   thing = VByName('id'),\n                   how = VOneOf('how', ('yes','no','admin','special')),\n                   # sticky=VBoolean('sticky', default=False),\n                   )\n    @api_doc(api_section.moderation)\n    def POST_distinguish(self, form, jquery, thing, how):\n        \"\"\"Distinguish a thing's author with a sigil.\n\n        This can be useful to draw attention to and confirm the identity of the\n        user in the context of a link or comment of theirs. The options for\n        distinguish are as follows:\n\n        * `yes` - add a moderator distinguish (`[M]`). only if the user is a\n                  moderator of the subreddit the thing is in.\n        * `no` - remove any distinguishes.\n        * `admin` - add an admin distinguish (`[A]`). admin accounts only.\n        * `special` - add a user-specific distinguish. depends on user.\n\n        The first time a top-level comment is moderator distinguished, the\n        author of the link the comment is in reply to will get a notification\n        in their inbox.\n\n        \"\"\"\n\n        # To be added to API docs when fully enabled:\n        #\n        # `sticky` is a boolean flag for comments, which will stick the\n        #  distingushed comment to the top of all comments threads. If a comment\n        #  is marked sticky, it will override any other stickied comment for that\n        #  link (as only one comment may be stickied at a time.) Only top-level\n        #  comments may be stickied.\n\n        if not thing:return\n\n        # XXX: Temporary retrieval of sticky param down here to avoid it\n        # showing up in API docs while in development. Move this to the\n        # validatedForm above when live.\n        sticky = False\n        if feature.is_enabled('sticky_comments'):\n            sticky_validator = VBoolean('sticky', default=False)\n            sticky = sticky_validator.run(request.params.get('sticky'))\n\n        if (feature.is_enabled('sticky_comments') and\n                sticky and\n                not isinstance(thing, Comment)):\n            abort(400, \"Only comments may be stickied from distinguish. To \"\n                       \"sticky a link in a subreddit use set_subreddit_sticky.\"\n                  )\n\n        c.profilepage = request.params.get('profilepage') == 'True'\n        log_modaction = True\n        log_kw = {}\n        send_message = False\n        original = getattr(thing, 'distinguished', 'no')\n        if how == original: # Distinguish unchanged\n            log_modaction = False\n        elif how in ('admin', 'special'): # Add admin/special\n            log_modaction = False\n            send_message = True\n        elif (original in ('admin', 'special') and\n                how == 'no'): # Remove admin/special\n            log_modaction = False\n        elif how == 'no': # From yes to no\n            log_kw['details'] = 'remove'\n        else: # From no to yes\n            send_message = True\n\n        if isinstance(thing, Comment):\n            link = thing.link_slow\n\n            # Send a message if this is a top-level comment on a submission or\n            # comment that has disabled receiving inbox notifications of\n            # replies, if it's the first distinguish for this comment, and if\n            # the user isn't banned or blocked by the author (replying didn't\n            # generate an inbox notification, send one now upon distinguishing\n            # it)\n            if not thing.parent_id:\n                to = Account._byID(link.author_id, data=True)\n                replies_enabled = link.sendreplies\n            else:\n                parent = Comment._byID(thing.parent_id, data=True)\n                to = Account._byID(parent.author_id, data=True)\n                replies_enabled = parent.sendreplies\n\n            previously_distinguished = hasattr(thing, 'distinguished')\n            user_can_notify = (not c.user._spam and\n                               c.user._id not in to.enemies and\n                               to.name != c.user.name)\n\n            if (send_message and\n                    not replies_enabled and\n                    not previously_distinguished and\n                    user_can_notify):\n                inbox_rel = Inbox._add(to, thing, 'selfreply')\n                queries.update_comment_notifications(thing, inbox_rel)\n\n            # Sticky handling - done before commit so that if there is an error\n            # setting sticky we don't distinguish. This ordering does leave the\n            # potential for an erroneous sticky if there's a commit error on\n            # distinguish, but a stickied comment that's not distinguished is\n            # not the end of the world, and handling rollback would probably be\n            # more error prone if we're hitting commit errors anyhow.\n            if feature.is_enabled('sticky_comments'):\n                try:\n                    if not sticky or how == 'no':\n                        # Un-distinguished a comment or sticky was False? Check\n                        # to see if it was previously stickied and unsticky if\n                        # so.\n                        if link.sticky_comment_id == thing._id:\n                            link.remove_sticky_comment(set_by=c.user)\n                    elif sticky and how != 'no':\n                        link.set_sticky_comment(thing, set_by=c.user)\n                except RedditError as error:\n                    abort_with_error(error, error.code or 400)\n\n        thing.distinguished = how\n        thing._commit()\n\n        hooks.get_hook(\"thing.distinguish\").call(thing=thing)\n\n        wrapper = default_thing_wrapper(expand_children = True)\n        w = wrap_links(thing, wrapper)\n        jquery(\"body>div.content\").replace_things(w, True, True)\n        jquery(\"body>div.content .link .rank\").hide()\n        if log_modaction:\n            sr = thing.subreddit_slow\n            ModAction.create(sr, c.user, 'distinguish', target=thing, **log_kw)\n\n    @require_oauth2_scope(\"save\")\n    @json_validate(VUser())\n    @api_doc(api_section.links_and_comments)\n    def GET_saved_categories(self, responder):\n        \"\"\"Get a list of categories in which things are currently saved.\n\n        See also: [/api/save](#POST_api_save).\n\n        \"\"\"\n        if not c.user.gold:\n            abort(403)\n        categories = LinkSavesByCategory.get_saved_categories(c.user)\n        categories += CommentSavesByCategory.get_saved_categories(c.user)\n        categories = sorted(set(categories), key=lambda name: name.lower())\n        categories = [dict(category=category) for category in categories]\n        return {'categories': categories}\n\n    @require_oauth2_scope(\"save\")\n    @noresponse(\n        VUser(),\n        VModhash(),\n        category=VSavedCategory('category'),\n        thing=VByName('id'),\n    )\n    @api_doc(api_section.links_and_comments)\n    def POST_save(self, thing, category):\n        \"\"\"Save a link or comment.\n\n        Saved things are kept in the user's saved listing for later perusal.\n\n        See also: [/api/unsave](#POST_api_unsave).\n\n        \"\"\"\n        if not thing or not isinstance(thing, (Link, Comment)):\n            abort(400)\n\n        if category and not c.user.gold:\n            category = None\n\n        if ('BAD_SAVE_CATEGORY', 'category') in c.errors:\n            abort(403)\n\n        thing._save(c.user, category=category)\n\n    @require_oauth2_scope(\"save\")\n    @noresponse(\n        VUser(),\n        VModhash(),\n        thing=VByName('id'),\n    )\n    @api_doc(api_section.links_and_comments)\n    def POST_unsave(self, thing):\n        \"\"\"Unsave a link or comment.\n\n        This removes the thing from the user's saved listings as well.\n\n        See also: [/api/save](#POST_api_save).\n\n        \"\"\"\n        if not thing or not isinstance(thing, (Link, Comment)):\n            abort(400)\n\n        thing._unsave(c.user)\n\n    def collapse_handler(self, things, collapse):\n        if not things:\n            return\n        things = tup(things)\n        srs = Subreddit._byID([t.sr_id for t in things if t.sr_id],\n                              return_dict = True)\n        for t in things:\n            if hasattr(t, \"to_id\") and c.user._id == t.to_id:\n                t.to_collapse = collapse\n            elif hasattr(t, \"author_id\") and c.user._id == t.author_id:\n                t.author_collapse = collapse\n            elif isinstance(t, Message) and t.sr_id:\n                if srs[t.sr_id].is_moderator(c.user):\n                    t.to_collapse = collapse\n            t._commit()\n\n    @noresponse(VUser(),\n                VModhash(),\n                things = VByName('id', multiple = True))\n    @api_doc(api_section.messages)\n    def POST_collapse_message(self, things):\n        \"\"\"Collapse a message\n\n        See also: [/api/uncollapse_message](#POST_uncollapse_message)\n\n        \"\"\"\n        self.collapse_handler(things, True)\n\n    @noresponse(VUser(),\n                VModhash(),\n                things = VByName('id', multiple = True))\n    @api_doc(api_section.messages)\n    def POST_uncollapse_message(self, things):\n        \"\"\"Uncollapse a message\n\n        See also: [/api/collapse_message](#POST_collapse_message)\n\n        \"\"\"\n        self.collapse_handler(things, False)\n\n    @require_oauth2_scope(\"privatemessages\")\n    @noresponse(VUser(),\n                VModhash(),\n                things = VByName('id', multiple=True, limit=25))\n    @api_doc(api_section.messages)\n    def POST_unread_message(self, things):\n        if not things:\n            if (errors.TOO_MANY_THING_IDS, 'id') in c.errors:\n                return abort(413)\n            else:\n                return abort(400)\n\n        queries.unread_handler(things, c.user, unread=True)\n\n    @require_oauth2_scope(\"privatemessages\")\n    @noresponse(VUser(),\n                VModhash(),\n                things = VByName('id', multiple=True, limit=25))\n    @api_doc(api_section.messages)\n    def POST_read_message(self, things):\n        if not things:\n            if (errors.TOO_MANY_THING_IDS, 'id') in c.errors:\n                return abort(413)\n            else:\n                return abort(400)\n\n        queries.unread_handler(things, c.user, unread=False)\n\n    @require_oauth2_scope(\"privatemessages\")\n    @noresponse(VUser(),\n                VModhash(),\n                VRatelimit(rate_user=True, prefix=\"rate_read_all_\", fatal=True))\n    @api_doc(api_section.messages)\n    def POST_read_all_messages(self):\n        \"\"\"Queue up marking all messages for a user as read.\n\n        This may take some time, and returns 202 to acknowledge acceptance of\n        the request.\n        \"\"\"\n        amqp.add_item('mark_all_read', c.user._fullname)\n        # Mark usage in the ratelimiter.\n        VRatelimit.ratelimit(rate_user=True, prefix='rate_read_all_')\n\n        return abort(202)\n\n    @require_oauth2_scope(\"report\")\n    @noresponse(VUser(),\n                VModhash(),\n                links=VByName('id', thing_cls=Link, multiple=True, limit=50))\n    @api_doc(api_section.links_and_comments)\n    def POST_hide(self, links):\n        \"\"\"Hide a link.\n\n        This removes it from the user's default view of subreddit listings.\n\n        See also: [/api/unhide](#POST_api_unhide).\n\n        \"\"\"\n        if not links:\n            return abort(400)\n\n        LinkHidesByAccount._hide(c.user, links)\n\n    @require_oauth2_scope(\"report\")\n    @noresponse(VUser(),\n                VModhash(),\n                links=VByName('id', thing_cls=Link, multiple=True, limit=50))\n    @api_doc(api_section.links_and_comments)\n    def POST_unhide(self, links):\n        \"\"\"Unhide a link.\n\n        See also: [/api/hide](#POST_api_hide).\n\n        \"\"\"\n        if not links:\n            return abort(400)\n\n        LinkHidesByAccount._unhide(c.user, links)\n\n\n    @csrf_exempt\n    @validatedForm(VUser(),\n                   parent = VByName('parent_id'))\n    def POST_moremessages(self, form, jquery, parent):\n        if not parent.can_view_slow():\n            return abort(403, 'forbidden')\n\n        if parent.sr_id:\n            builder = SrMessageBuilder(parent.subreddit_slow,\n                                       parent = parent, skip = False)\n        else:\n            builder = UserMessageBuilder(c.user, parent = parent, skip = False)\n\n        listing = Listing(builder).listing()\n\n        a = []\n        for item in listing.things:\n            a.append(item)\n            if hasattr(item, \"child\"):\n                for x in item.child.things:\n                    a.append(x)\n\n        for item in a:\n            if hasattr(item, \"child\"):\n                item.child = None\n\n        jquery.things(parent._fullname).parent().replace_things(a, False, True)\n\n    @require_oauth2_scope(\"read\")\n    @validatedForm(\n        link=VByName('link_id', thing_cls=Link),\n        sort=VMenu('morechildren', CommentSortMenu, remember=False),\n        children=VCommentIDs('children'),\n        mc_id=nop(\n            \"id\",\n            docs={\"id\": \"(optional) id of the associated MoreChildren object\"}),\n    )\n    @api_doc(api_section.links_and_comments)\n    def GET_morechildren(self, form, jquery, link, sort, children, mc_id):\n        \"\"\"Retrieve additional comments omitted from a base comment tree.\n\n        When a comment tree is rendered, the most relevant comments are\n        selected for display first. Remaining comments are stubbed out with\n        \"MoreComments\" links. This API call is used to retrieve the additional\n        comments represented by those stubs, up to 20 at a time.\n\n        The two core parameters required are `link` and `children`.  `link` is\n        the fullname of the link whose comments are being fetched. `children`\n        is a comma-delimited list of comment ID36s that need to be fetched.\n\n        If `id` is passed, it should be the ID of the MoreComments object this\n        call is replacing. This is needed only for the HTML UI's purposes and\n        is optional otherwise.\n\n        **NOTE:** you may only make one request at a time to this API endpoint.\n        Higher concurrency will result in an error being returned.\n\n        \"\"\"\n\n        CHILD_FETCH_COUNT = 20\n\n        lock = None\n        if c.user_is_loggedin:\n            lock = g.make_lock(\"morechildren\", \"morechildren-\" + c.user.name,\n                               timeout=0)\n            try:\n                lock.acquire()\n            except TimeoutExpired:\n                abort(429)\n\n        try:\n            if not link or not link.subreddit_slow.can_view(c.user):\n                return abort(403,'forbidden')\n\n            if children:\n                children = list(set(children))\n                builder = CommentBuilder(link, CommentSortMenu.operator(sort),\n                                         children=children,\n                                         num=CHILD_FETCH_COUNT)\n                listing = Listing(builder, nextprev = False)\n                items = listing.get_items()\n                def _children(cur_items):\n                    items = []\n                    for cm in cur_items:\n                        items.append(cm)\n                        if hasattr(cm, 'child'):\n                            if hasattr(cm.child, 'things'):\n                                items.extend(_children(cm.child.things))\n                                cm.child = None\n                            else:\n                                items.append(cm.child)\n\n                    return items\n                # assumes there is at least one child\n                # a = _children(items[0].child.things)\n                a = []\n                for item in items:\n                    a.append(item)\n                    if hasattr(item, 'child'):\n                        a.extend(_children(item.child.things))\n                        item.child = None\n\n                # the result is not always sufficient to replace the\n                # morechildren link\n                jquery.things(str(mc_id)).remove()\n                jquery.insert_things(a, append = True)\n        finally:\n            if lock:\n                lock.release()\n\n    @csrf_exempt\n    @require_oauth2_scope(\"read\")\n    def POST_morechildren(self):\n        \"\"\"Wrapper around `GET_morechildren` for backwards-compatibility\"\"\"\n        return self.GET_morechildren()\n\n    @validatedForm(VUser(),\n                   VModhash(),\n                   code=VPrintable(\"code\", 30))\n    def POST_claimgold(self, form, jquery, code):\n        status = ''\n        if not code:\n            c.errors.add(errors.NO_TEXT, field = \"code\")\n            form.has_errors(\"code\", errors.NO_TEXT)\n            return\n\n        rv = claim_gold(code, c.user._id)\n\n        if rv is None:\n            c.errors.add(errors.INVALID_CODE, field = \"code\")\n        elif rv == \"already claimed\":\n            c.errors.add(errors.CLAIMED_CODE, field = \"code\")\n        else:\n            days, subscr_id = rv\n            if days <= 0:\n                raise ValueError(\"days = %r?\" % days)\n\n            if subscr_id:\n                c.user.gold_subscr_id = subscr_id\n\n            if code.startswith(\"cr_\"):\n                c.user.gold_creddits += int(days / 31)\n                c.user._commit()\n                status = 'claimed-creddits'\n            else:\n                # send the user a message if they don't already have gold\n                if not c.user.gold:\n                    subject = \"You claimed a reddit gold code!\"\n                    message = strings.gold_claimed_code\n                    message += \"\\n\\n\" + strings.gold_benefits_msg\n\n                    if g.lounge_reddit:\n                        message += \"\\n\\n\" + strings.lounge_msg\n                    message = append_random_bottlecap_phrase(message)\n\n                    try:\n                        send_system_message(c.user, subject, message,\n                                            distinguished='gold-auto')\n                    except MessageError:\n                        g.log.error('claimgold: could not send system message')\n\n                admintools.adjust_gold_expiration(c.user, days=days)\n\n                status = 'claimed-gold'\n                jquery(\".lounge\").show()\n\n        # Activate any errors we just manually set\n        if not form.has_errors(\"code\", errors.INVALID_CODE, errors.CLAIMED_CODE,\n                               errors.NO_TEXT):\n            form.redirect(\"/gold/thanks?v=%s\" % status)\n\n    @csrf_exempt\n    @validatedForm(\n        VRatelimit(rate_ip=True, prefix=\"rate_password_\"),\n        user=VUserWithEmail('name'),\n    )\n    def POST_password(self, form, jquery, user):\n        \"\"\"Send password reset email.\"\"\"\n        def _event(error):\n            email = user.email if user else None\n            email_verified = bool(user.email_verified) if email else None\n            g.events.login_event(\n                'password_reset',\n                error_msg=error,\n                user_name=request.POST.get('name'),\n                email=email,\n                email_verified=email_verified,\n                request=request,\n                context=c)\n\n        if form.has_errors('name', errors.USER_DOESNT_EXIST):\n            _event(error='USER_DOESNT_EXIST')\n\n        elif form.has_errors('name', errors.NO_EMAIL_FOR_USER):\n            _event(error='NO_EMAIL_FOR_USER')\n\n        elif form.has_errors('ratelimit', errors.RATELIMIT):\n            _event(error='RATELIMIT')\n\n        else:\n            VRatelimit.ratelimit(rate_ip=True, prefix=\"rate_password_\")\n            if emailer.password_email(user):\n                _event(error=None)\n                form.set_text(\".status\",\n                      _(\"an email will be sent to that account's address shortly\"))\n            else:\n                _event(error='RESET_LIMIT')\n                form.set_text(\".status\", _(\"try again tomorrow\"))\n\n    @csrf_exempt\n    @validatedForm(token=VOneTimeToken(PasswordResetToken, \"key\"),\n                   password=VPasswordChange([\"passwd\", \"passwd2\"]))\n    def POST_resetpassword(self, form, jquery, token, password):\n        # was the token invalid or has it expired?\n        if not token:\n            form.redirect(\"/password?expired=true\")\n            return\n\n        # did they fill out the password form correctly?\n        form.has_errors(\"passwd\",  errors.BAD_PASSWORD)\n        form.has_errors(\"passwd2\", errors.BAD_PASSWORD_MATCH)\n        if form.has_error():\n            return\n\n        # at this point, we should mark the token used since it's either\n        # valid now or will never be valid again.\n        token.consume()\n\n        # load up the user and check that things haven't changed\n        user = Account._by_fullname(token.user_id)\n        if not token.valid_for_user(user):\n            form.redirect('/password?expired=true')\n            return\n\n        # Prevent banned users from resetting, and thereby logging in\n        if user._banned:\n            return\n\n        # successfully entered user name and valid new password\n        change_password(user, password)\n        if user.email:\n            emailer.password_change_email(user)\n        g.log.warning(\"%s did a password reset for %s via %s\",\n                      request.ip, user.name, token._id)\n\n        # add this ip to the user's account so they can sign in even if\n        # their account is being brute forced by a third party.\n        set_account_ip(user._id, request.ip, c.start_time)\n\n        # if the token is for the current user, their cookies will be\n        # invalidated and they'll have to log in again.\n        if not c.user_is_loggedin or c.user._fullname == token.user_id:\n            jquery.redirect('/login')\n\n        form.set_text(\".status\", _(\"password updated\"))\n\n    @require_oauth2_scope(\"subscribe\")\n    @noresponse(VUser(),\n                VModhash(),\n                action = VOneOf('action', ('sub', 'unsub')),\n                sr = VSubscribeSR('sr', 'sr_name'))\n    @api_doc(api_section.subreddits)\n    def POST_subscribe(self, action, sr):\n        \"\"\"Subscribe to or unsubscribe from a subreddit.\n\n        To subscribe, `action` should be `sub`. To unsubscribe, `action` should\n        be `unsub`. The user must have access to the subreddit to be able to\n        subscribe to it.\n\n        See also: [/subreddits/mine/](#GET_subreddits_mine_{where}).\n\n        \"\"\"\n\n        if not sr:\n            return abort(404, 'not found')\n        elif action == \"sub\" and not sr.can_view(c.user):\n            return abort(403, 'permission denied')\n        elif isinstance(sr, FakeSubreddit):\n            return abort(403, 'permission denied')\n\n        Subreddit.subscribe_defaults(c.user)\n\n        if action == \"sub\":\n            SubredditParticipationByAccount.mark_participated(c.user, sr)\n\n            if not sr.is_subscriber(c.user):\n                sr.add_subscriber(c.user)\n        else:\n            if sr.is_subscriber(c.user):\n                sr.remove_subscriber(c.user)\n            else:\n                # tried to unsubscribe but user was not subscribed\n                return abort(404, 'not found')\n        sr.update_search_index(boost_only=True)\n\n    @validatedForm(\n        VAdmin(),\n        VModhash(),\n        subreddit=VByName('subreddit'),\n        quarantine=VBoolean('quarantine'),\n        subject=VLength('subject', 1000),\n        body=VMarkdownLength('body', max_length=10000),\n    )\n    def POST_quarantine(self, form, jquery, subreddit, quarantine, subject, body):\n        if subreddit.quarantine == quarantine:\n            return\n\n        subreddit.quarantine = quarantine\n        subreddit._commit()\n        system_user = Account.system_user()\n        kw = dict(\n            sr_id36=subreddit._id36,\n            mod_id36=system_user._id36,\n            action=\"editsettings\",\n            details=\"quarantine\",\n        )\n        ma = ModAction(**kw)\n        ma._commit()\n\n        if config['r2.import_private']:\n            from r2admin.lib.admin_utils import record_admin_event\n            if quarantine:\n                record_admin_event('quarantine', page=\"subreddit_page\",\n                    target_thing=subreddit)\n            else:\n                record_admin_event('unquarantine', page=\"subreddit_page\",\n                    target_thing=subreddit)\n\n        if body.strip():\n            send_system_message(subreddit, subject, body,\n                distinguished='admin', repliable=False)\n\n        # Refresh the CSS since images aren't allowed\n        stylesheet_contents = subreddit.fetch_stylesheet_source()\n        css_errors, parsed = subreddit.parse_css(stylesheet_contents)\n        subreddit.change_css(stylesheet_contents, parsed, author=system_user)\n        jquery.refresh()\n\n    @require_oauth2_scope(\"subscribe\")\n    @noresponse(\n        VUser(),\n        VModhash(),\n        sr=VSRByName('sr_name'),\n    )\n    def POST_quarantine_optout(self, sr):\n        \"\"\"Opt out from a quarantined subreddit\"\"\"\n        if not sr:\n            return abort(404, 'not found')\n        else:\n            g.events.quarantine_event('quarantine_opt_out', sr,\n                request=request, context=c)\n            QuarantinedSubredditOptInsByAccount.opt_out(c.user, sr)\n        return self.redirect('/')\n\n    @require_oauth2_scope(\"subscribe\")\n    @noresponse(\n        VUser(),\n        VModhash(),\n        sr=VSRByName('sr_name'),\n    )\n    def POST_quarantine_optin(self, sr):\n        \"\"\"Opt in to a quarantined subreddit\"\"\"\n        if not sr:\n            return abort(404, 'not found')\n        elif not c.user.email_verified:\n            return abort(403, 'email not verified')\n        else:\n            g.events.quarantine_event('quarantine_opt_in', sr,\n                request=request, context=c)\n            QuarantinedSubredditOptInsByAccount.opt_in(c.user, sr)\n        return self.redirect('/r/%s' % sr.name)\n\n    @validatedForm(VAdmin(),\n                   VModhash(),\n                   hexkey=VLength(\"hexkey\", max_length=32),\n                   nickname=VLength(\"nickname\", max_length = 1000),\n                   status = VOneOf(\"status\",\n                      (\"new\", \"severe\", \"interesting\", \"normal\", \"fixed\")))\n    def POST_edit_error(self, form, jquery, hexkey, nickname, status):\n        if form.has_errors((\"hexkey\", \"nickname\", \"status\"),\n                           errors.NO_TEXT, errors.INVALID_OPTION):\n            pass\n\n        if form.has_error():\n            return\n\n        key = \"error_nickname-%s\" % str(hexkey)\n        g.hardcache.set(key, nickname, 86400 * 365)\n\n        key = \"error_status-%s\" % str(hexkey)\n        g.hardcache.set(key, status, 86400 * 365)\n\n        form.set_text(\".status\", _('saved'))\n\n    @validatedForm(VAdmin(),\n                   VModhash(),\n                   award=VByName(\"fullname\"),\n                   colliding_award=VAwardByCodename((\"codename\", \"fullname\")),\n                   codename=VLength(\"codename\", max_length = 100),\n                   title=VLength(\"title\", max_length = 100),\n                   awardtype=VOneOf(\"awardtype\",\n                                    (\"regular\", \"manual\", \"invisible\")),\n                   api_ok=VBoolean(\"api_ok\"),\n                   imgurl=VLength(\"imgurl\", max_length = 1000))\n    def POST_editaward(self, form, jquery, award, colliding_award, codename,\n                       title, awardtype, api_ok, imgurl):\n        if form.has_errors((\"codename\", \"title\", \"awardtype\", \"imgurl\"),\n                           errors.NO_TEXT):\n            pass\n\n        if awardtype is None:\n            form.set_text(\".status\", \"bad awardtype\")\n            return\n\n        if form.has_errors((\"codename\"), errors.INVALID_OPTION):\n            form.set_text(\".status\", \"some other award has that codename\")\n            pass\n\n        url_ok = True\n\n        if not imgurl.startswith(\"//\"):\n            url_ok = False\n            form.set_text(\".status\", \"the url must be protocol-relative\")\n\n        try:\n            imgurl % 1\n        except TypeError:\n            url_ok = False\n            form.set_text(\".status\", \"the url must have a %d for size\")\n\n        if not url_ok:\n            c.errors.add(errors.BAD_URL, field=\"imgurl\")\n            form.has_errors(\"imgurl\", errors.BAD_URL)\n\n        if form.has_error():\n            return\n\n        if award is None:\n            Award._new(codename, title, awardtype, imgurl, api_ok)\n            form.set_text(\".status\", \"saved. reload to see it.\")\n            return\n\n        award.codename = codename\n        award.title = title\n        award.awardtype = awardtype\n        award.imgurl = imgurl\n        award.api_ok = api_ok\n        award._commit()\n        form.set_text(\".status\", _('saved'))\n\n    @require_oauth2_scope(\"modflair\")\n    @validatedForm(VSrModerator(perms='flair'),\n                   VModhash(),\n                   user = VFlairAccount(\"name\"),\n                   link = VFlairLink('link'),\n                   text = VFlairText(\"text\"),\n                   css_class = VFlairCss(\"css_class\"))\n    @api_doc(api_section.flair, uses_site=True)\n    def POST_flair(self, form, jquery, user, link, text, css_class):\n        if form.has_errors('css_class', errors.BAD_CSS_NAME):\n            form.set_text(\".status:first\", _('invalid css class'))\n            return\n        if form.has_errors('css_class', errors.TOO_MUCH_FLAIR_CSS):\n            form.set_text(\".status:first\", _('too many css classes'))\n            return\n\n        if link:\n            if form.has_errors(\"link\", errors.BAD_FLAIR_TARGET):\n                return\n\n            if not c.user_is_admin and not link.can_flair_slow(c.user):\n                abort(403)\n\n            # If the user is in timeout, don't let them set flair\n            VNotInTimeout().run(action_name=\"editflair\", details_text=\"set\",\n                target=link)\n\n            link.set_flair(text, css_class, set_by=c.user)\n        else:\n            if form.has_errors(\"name\", errors.BAD_FLAIR_TARGET):\n                return\n\n            # If the user is in timeout, don't let them set flair\n            VNotInTimeout().run(action_name=\"editflair\", details_text=\"set\",\n                target=user)\n\n            user.set_flair(c.site, text, css_class, set_by=c.user)\n\n            # XXX: this is still gross with all the UI code in here\n            if not text and not css_class:\n                jquery('#flairrow_%s' % user._id36).hide()\n            elif not c.site.is_flair(user):\n                jquery.redirect('?name=%s' % user.name)\n                return\n\n            wrapped_user = WrappedUser(\n                user, force_show_flair=True, include_flair_selector=True)\n            rendered = wrapped_user.render(style='html')\n            jquery('.tagline .flairselectable.id-%s'\n                % user._fullname).parent().html(rendered)\n            jquery('input[name=\"text\"]').data('saved', text)\n            jquery('input[name=\"css_class\"]').data('saved', css_class)\n            form.set_text('.status', _('saved'))\n\n    @require_oauth2_scope(\"modflair\")\n    @validatedForm(\n        VSrModerator(perms='flair'),\n        VModhash(),\n        user=VFlairAccount(\"name\"),\n    )\n    @api_doc(api_section.flair, uses_site=True)\n    def POST_deleteflair(self, form, jquery, user):\n        if form.has_errors('name', errors.USER_DOESNT_EXIST, errors.NO_USER):\n            return\n\n        VNotInTimeout().run(action_name=\"editflair\", details_text=\"delete\",\n            target=user)\n        user.set_flair(c.site, None, None, set_by=c.user)\n\n        jquery('#flairrow_%s' % user._id36).remove()\n        unflair = WrappedUser(\n            user, include_flair_selector=True).render(style='html')\n        jquery('.tagline .id-%s' % user._fullname).parent().html(unflair)\n\n    @require_oauth2_scope(\"modflair\")\n    @validate(\n        VSrModerator(perms='flair'),\n        VModhash(),\n        VNotInTimeout(),\n        flair_csv=nop(\"flair_csv\",\n            docs={\"flair_csv\": \"comma-seperated flair information\"}),\n    )\n    @api_doc(api_section.flair, uses_site=True)\n    def POST_flaircsv(self, flair_csv):\n        \"\"\"Change the flair of multiple users in the same subreddit with a\n        single API call.\n\n        Requires a string 'flair_csv' which has up to 100 lines of the form\n        '`user`,`flairtext`,`cssclass`' (Lines beyond the 100th are ignored).\n\n        If both `cssclass` and `flairtext` are the empty string for a given\n        `user`, instead clears that user's flair.\n\n        Returns an array of objects indicating if each flair setting was \n        applied, or a reason for the failure.\n\n        \"\"\"\n\n        if not flair_csv:\n            return\n        \n        limit = 100  # max of 100 flair settings per call\n        results = FlairCsv()\n        # encode to UTF-8, since csv module doesn't fully support unicode\n        infile = csv.reader(flair_csv.strip().encode('utf-8').split('\\n'))\n        for i, row in enumerate(infile):\n            line_result = results.add_line()\n            line_no = i + 1\n            if line_no > limit:\n                line_result.error('row',\n                                  'limit of %d rows per call reached' % limit)\n                break\n\n            try:\n                name, text, css_class = row\n            except ValueError:\n                line_result.error('row', 'improperly formatted row, ignoring')\n                continue\n\n            user = VFlairAccount('name').run(name)\n            if not user:\n                line_result.error('user',\n                                  \"unable to resolve user `%s', ignoring\"\n                                  % name)\n                continue\n\n            orig_text = text\n            text = VFlairText('text').run(orig_text)\n            if text and orig_text and len(text) < len(orig_text):\n                line_result.warn('text',\n                                 'truncating flair text to %d chars'\n                                 % len(text))\n\n            if css_class and not VFlairCss('css_class').run(css_class):\n                line_result.error('css',\n                                  \"invalid css class `%s', ignoring\"\n                                  % css_class)\n                continue\n\n            # all validation passed, enflair the user\n            user.set_flair(\n                c.site, text, css_class, set_by=c.user, log_details=\"csv\")\n\n            if text or css_class:\n                mode = 'added'\n            else:\n                mode = 'removed'\n            line_result.status = '%s flair for user %s' % (mode, user.name)\n            line_result.ok = True\n\n        return BoringPage(_(\"API\"), content = results).render()\n\n    @require_oauth2_scope(\"flair\")\n    @validatedForm(VUser(),\n                   VModhash(),\n                   flair_enabled = VBoolean(\"flair_enabled\"))\n    @api_doc(api_section.flair, uses_site=True)\n    def POST_setflairenabled(self, form, jquery, flair_enabled):\n        setattr(c.user, 'flair_%s_enabled' % c.site._id, flair_enabled)\n        c.user._commit()\n        jquery.refresh()\n\n    @require_oauth2_scope(\"modflair\")\n    @validatedForm(\n        VSrModerator(perms='flair'),\n        VModhash(),\n        flair_enabled=VBoolean(\"flair_enabled\"),\n        flair_position=VOneOf(\"flair_position\", (\"left\", \"right\")),\n        link_flair_position=VOneOf(\"link_flair_position\",\n            (\"\", \"left\", \"right\")),\n        flair_self_assign_enabled=VBoolean(\"flair_self_assign_enabled\"),\n        link_flair_self_assign_enabled =\n            VBoolean(\"link_flair_self_assign_enabled\"),\n        timeout=VNotInTimeout(),\n    )\n    @api_doc(api_section.flair, uses_site=True)\n    def POST_flairconfig(self, form, jquery, flair_enabled, flair_position,\n            link_flair_position, flair_self_assign_enabled,\n            link_flair_self_assign_enabled, timeout):\n        if c.site.flair_enabled != flair_enabled:\n            c.site.flair_enabled = flair_enabled\n            ModAction.create(c.site, c.user, action='editflair',\n                             details='flair_enabled')\n        if c.site.flair_position != flair_position:\n            c.site.flair_position = flair_position\n            ModAction.create(c.site, c.user, action='editflair',\n                             details='flair_position')\n        if c.site.link_flair_position != link_flair_position:\n            c.site.link_flair_position = link_flair_position\n            ModAction.create(c.site, c.user, action='editflair',\n                             details='link_flair_position')\n        if c.site.flair_self_assign_enabled != flair_self_assign_enabled:\n            c.site.flair_self_assign_enabled = flair_self_assign_enabled\n            ModAction.create(c.site, c.user, action='editflair',\n                             details='flair_self_enabled')\n        if (c.site.link_flair_self_assign_enabled\n            != link_flair_self_assign_enabled):\n            c.site.link_flair_self_assign_enabled = (\n                link_flair_self_assign_enabled)\n            ModAction.create(c.site, c.user, action='editflair',\n                             details='link_flair_self_enabled')\n        c.site._commit()\n        jquery.refresh()\n\n    @require_oauth2_scope(\"modflair\")\n    @paginated_listing(max_page_size=1000)\n    @validate(\n        VSrModerator(perms='flair'),\n        user=VFlairAccount('name'),\n    )\n    @api_doc(api_section.flair, uses_site=True)\n    def GET_flairlist(self, num, after, reverse, count, user):\n        if user and user._deleted:\n            return self.abort403()\n\n        # Don't allow users in timeout to modify flairs\n        VNotInTimeout().run(action_name=\"editflair\", details_text=\"flair_list\",\n            target=user)\n\n        flair = FlairList(num, after, reverse, '', user)\n        return BoringPage(_(\"API\"), content = flair).render()\n\n    @require_oauth2_scope(\"modflair\")\n    @validatedForm(VSrModerator(perms='flair'),\n                   VModhash(),\n                   flair_template = VFlairTemplateByID('flair_template_id'),\n                   text = VFlairText('text'),\n                   css_class = VFlairCss('css_class'),\n                   text_editable = VBoolean('text_editable'),\n                   flair_type = VOneOf('flair_type', (USER_FLAIR, LINK_FLAIR),\n                                       default=USER_FLAIR))\n    @api_doc(api_section.flair, uses_site=True)\n    def POST_flairtemplate(self, form, jquery, flair_template, text,\n                           css_class, text_editable, flair_type):\n        if text is None:\n            text = ''\n        if css_class is None:\n            css_class = ''\n\n        # Check validation.\n        if form.has_errors('css_class', errors.BAD_CSS_NAME):\n            form.set_text(\".status:first\", _('invalid css class'))\n            return\n        if form.has_errors('css_class', errors.TOO_MUCH_FLAIR_CSS):\n            form.set_text(\".status:first\", _('too many css classes'))\n            return\n\n        # Don't allow users in timeout to modify the flair templates\n        VNotInTimeout().run(action_name=\"editflair\",\n            details_text=\"flair_template\", target=c.site)\n\n        # Load flair template thing.\n        if flair_template:\n            flair_template.text = text\n            flair_template.css_class = css_class\n            flair_template.text_editable = text_editable\n            flair_template._commit()\n            new = False\n        else:\n            try:\n                flair_template = FlairTemplateBySubredditIndex.create_template(\n                    c.site._id, text=text, css_class=css_class,\n                    text_editable=text_editable,\n                    flair_type=flair_type)\n            except OverflowError:\n                form.set_text(\".status:first\", _('max flair templates reached'))\n                return\n\n            new = True\n\n        # Push changes back to client.\n        if new:\n            empty_ids = {\n                USER_FLAIR: '#empty-user-flair-template',\n                LINK_FLAIR: '#empty-link-flair-template',\n            }\n            empty_id = empty_ids[flair_type]\n            jquery(empty_id).before(\n                FlairTemplateEditor(flair_template, flair_type)\n                .render(style='html'))\n            empty_template = FlairTemplate()\n            empty_template._committed = True  # to disable unnecessary warning\n            jquery(empty_id).html(\n                FlairTemplateEditor(empty_template, flair_type)\n                .render(style='html'))\n            form.set_text('.status', _('saved'))\n        else:\n            jquery('#%s' % flair_template._id).html(\n                FlairTemplateEditor(flair_template, flair_type)\n                .render(style='html'))\n            form.set_text('.status', _('saved'))\n            jquery('input[name=\"text\"]').data('saved', text)\n            jquery('input[name=\"css_class\"]').data('saved', css_class)\n        ModAction.create(c.site, c.user, action='editflair',\n                             details='flair_template')\n\n    @require_oauth2_scope(\"modflair\")\n    @validatedForm(\n        VSrModerator(perms='flair'),\n        VModhash(),\n        flair_template=VFlairTemplateByID('flair_template_id'),\n    )\n    @api_doc(api_section.flair, uses_site=True)\n    def POST_deleteflairtemplate(self, form, jquery, flair_template):\n        if not flair_template:\n            return self.abort404()\n\n        VNotInTimeout().run(action_name=\"editflair\",\n            details_text=\"flair_delete_template\", target=c.site)\n        idx = FlairTemplateBySubredditIndex.by_sr(c.site._id)\n        if idx.delete_by_id(flair_template._id):\n            jquery('#%s' % flair_template._id).parent().remove()\n            ModAction.create(c.site, c.user, action='editflair',\n                             details='flair_delete_template')\n\n    @require_oauth2_scope(\"modflair\")\n    @validatedForm(\n        VSrModerator(perms='flair'),\n        VModhash(),\n        VNotInTimeout(),\n        flair_type=VOneOf('flair_type', (USER_FLAIR, LINK_FLAIR),\n            default=USER_FLAIR),\n    )\n    @api_doc(api_section.flair, uses_site=True)\n    def POST_clearflairtemplates(self, form, jquery, flair_type):\n        FlairTemplateBySubredditIndex.clear(c.site._id, flair_type=flair_type)\n        jquery.refresh()\n        ModAction.create(c.site, c.user, action='editflair',\n                         details='flair_clear_template')\n\n    @csrf_exempt\n    @require_oauth2_scope(\"flair\")\n    @validate(\n        VUser(),\n        user=VFlairAccount('name'),\n        link=VFlairLink('link'),\n    )\n    @api_doc(api_section.flair, uses_site=True)\n    def POST_flairselector(self, user, link):\n        \"\"\"Return information about a users's flair options.\n\n        If `link` is given, return link flair options.\n        Otherwise, return user flair options for this subreddit.\n\n        The logged in user's flair is also returned.\n        Subreddit moderators may give a user by `name` to instead\n        retrieve that user's flair.\n\n        \"\"\"\n\n        if link:\n            if not (c.user_is_admin or link.can_flair_slow(c.user)):\n                abort(403)\n\n            site = link.subreddit_slow\n            user = c.user\n            return FlairSelector(user, site, link).render()\n        else:\n            if isinstance(c.site, FakeSubreddit):\n                abort(403)\n            else:\n                site = c.site\n\n            if user:\n                if (user != c.user and\n                        not c.user_is_admin and\n                        not site.is_moderator_with_perms(c.user, 'flair')):\n                    abort(403)\n            else:\n                user = c.user\n\n            if user._deleted:\n                abort(403)\n\n            return FlairSelector(user, site).render()\n\n    @require_oauth2_scope(\"flair\")\n    @validatedForm(VUser(),\n                   VModhash(),\n                   user = VFlairAccount('name'),\n                   link = VFlairLink('link'),\n                   flair_template_id = nop('flair_template_id'),\n                   text = VFlairText('text'))\n    @api_doc(api_section.flair, uses_site=True)\n    def POST_selectflair(self, form, jquery, user, link, flair_template_id,\n                         text):\n        if link:\n            flair_type = LINK_FLAIR\n            subreddit = link.subreddit_slow\n            if not (c.user_is_admin or link.can_flair_slow(c.user)):\n                abort(403)\n        elif user:\n            flair_type = USER_FLAIR\n            subreddit = c.site\n            if not (c.user_is_admin or user.can_flair_in_sr(c.user, subreddit)):\n                abort(403)\n        else:\n            return self.abort404()\n\n        if flair_template_id:\n            try:\n                flair_template = FlairTemplateBySubredditIndex.get_template(\n                    subreddit._id, flair_template_id, flair_type=flair_type)\n            except NotFound:\n                # TODO: serve error to client\n                g.log.debug('invalid flair template for subreddit %s', subreddit._id)\n                return\n\n            text_editable = flair_template.text_editable\n\n            # Ignore given text if user doesn't have permission to customize it.\n            if not (text_editable or\n                        c.user_is_admin or\n                        subreddit.is_moderator_with_perms(c.user, \"flair\")):\n                text = None\n\n            if not text:\n                text = flair_template.text\n\n            css_class = flair_template.css_class\n        else:\n            flair_template = None\n            text_editable = False\n            text = None\n            css_class = None\n\n        if flair_type == LINK_FLAIR:\n            VNotInTimeout().run(action_name=\"editflair\", details_text=\"select\",\n                target=link)\n            link.set_flair(text, css_class, set_by=c.user)\n\n            # XXX: gross UI code\n            # Push some client-side updates back to the browser.\n\n            jquery('.id-%s' % link._fullname).removeLinkFlairClass()\n            jquery('.id-%s .entry .linkflairlabel' % link._fullname).remove()\n            title_path = '.id-%s .entry > .title > .title' % link._fullname\n\n            # TODO: move this to a template\n            if flair_template:\n                classes = ' '.join('linkflair-' + c for c in css_class.split())\n                jquery('.id-%s' % link._fullname).addClass('linkflair').addClass(classes)\n                flair = format_html('<span class=\"linkflairlabel\">%s</span>', text)\n\n                if subreddit.link_flair_position == 'left':\n                    jquery(title_path).before(flair)\n                elif subreddit.link_flair_position == 'right':\n                    jquery(title_path).after(flair)\n\n            # TODO: close the selector popup more gracefully\n            jquery('body').click()\n        else:\n            VNotInTimeout().run(action_name=\"editflair\", details_text=\"select\",\n                target=user)\n            user.set_flair(subreddit, text, css_class, set_by=c.user)\n\n            # XXX: gross UI code\n            # Push some client-side updates back to the browser.\n            u = WrappedUser(user, force_show_flair=True,\n                            flair_text_editable=text_editable,\n                            include_flair_selector=True)\n            flair = u.render(style='html')\n            jquery('.tagline .flairselectable.id-%s'\n                % user._fullname).parent().html(flair)\n            jquery('#flairrow_%s input[name=\"text\"]' % user._id36).data(\n                'saved', text).val(text)\n            jquery('#flairrow_%s input[name=\"css_class\"]' % user._id36).data(\n                'saved', css_class).val(css_class)\n\n    @validatedForm(\n        VUser(),\n        VModhash(),\n        sr_style_enabled=VBoolean(\"sr_style_enabled\")\n    )\n    def POST_set_sr_style_enabled(self, form, jquery, sr_style_enabled):\n        \"\"\"Update enabling of individual sr themes; refresh the page style\"\"\"\n        if feature.is_enabled('stylesheets_everywhere'):\n            c.user.set_subreddit_style(c.site, sr_style_enabled)\n            c.can_apply_styles = True\n            sr = DefaultSR()\n\n            if sr_style_enabled:\n                sr = c.site\n            elif (c.user.pref_default_theme_sr and\n                    feature.is_enabled('stylesheets_everywhere')):\n                sr = Subreddit._by_name(c.user.pref_default_theme_sr)\n                if (not sr.can_view(c.user) or\n                        not c.user.pref_enable_default_themes):\n                    sr = DefaultSR()\n            sr_stylesheet_url = Reddit.get_subreddit_stylesheet_url(sr)\n            if not sr_stylesheet_url:\n                sr_stylesheet_url = \"\"\n                c.can_apply_styles = False\n\n            jquery.apply_stylesheet_url(sr_stylesheet_url, sr_style_enabled)\n\n            if not sr.header or header_url(sr.header) == g.default_header_url:\n                jquery.remove_header_image();\n            else:\n                jquery.apply_header_image(header_url(sr.header),\n                    sr.header_size, sr.header_title)\n\n    @validatedForm(secret_used=VAdminOrAdminSecret(\"secret\"),\n                   award=VByName(\"fullname\"),\n                   description=VLength(\"description\", max_length=1000),\n                   url=VLength(\"url\", max_length=1000),\n                   recipient=VExistingUname(\"recipient\"))\n    def POST_givetrophy(self, form, jquery, secret_used, award, description,\n                        url, recipient):\n        if form.has_errors(\"recipient\", errors.USER_DOESNT_EXIST,\n                                        errors.NO_USER):\n            pass\n\n        if form.has_errors(\"fullname\", errors.NO_TEXT, errors.NO_THING_ID):\n            pass\n\n        if secret_used and not award.api_ok:\n            c.errors.add(errors.NO_API, field='secret')\n            form.has_errors('secret', errors.NO_API)\n\n        if form.has_error():\n            return\n\n        t = Trophy._new(recipient, award, description=description, url=url)\n\n        form.set_text(\".status\", _('saved'))\n        form._send_data(trophy_fn=t._id36)\n\n    @validatedForm(secret_used=VAdminOrAdminSecret(\"secret\"),\n                   trophy = VTrophy(\"trophy_fn\"))\n    def POST_removetrophy(self, form, jquery, secret_used, trophy):\n        if not trophy:\n            return self.abort404()\n        recipient = trophy._thing1\n        award = trophy._thing2\n        if secret_used and not award.api_ok:\n            c.errors.add(errors.NO_API, field='secret')\n            form.has_errors('secret', errors.NO_API)\n        \n        if form.has_error():\n            return\n\n        trophy._delete()\n        Trophy.by_account(recipient, _update=True)\n        Trophy.by_award(award, _update=True)\n\n    @validatedForm(\n        VAdmin(),\n        VModhash(),\n        recipient=VExistingUname(\"recipient\"),\n        num_creddits=VInt('num_creddits', num_default=0),\n    )\n    def POST_givecreddits(self, form, jquery, recipient, num_creddits):\n        if form.has_errors(\"recipient\",\n                           errors.USER_DOESNT_EXIST, errors.NO_USER):\n            return\n\n        with creddits_lock(recipient):\n            recipient.gold_creddits += num_creddits\n            # make sure it doesn't go into the negative\n            recipient.gold_creddits = max(0, recipient.gold_creddits)\n            recipient._commit()\n\n        form.set_text(\".status\", _('saved'))\n\n    @validatedForm(\n        VAdmin(),\n        VModhash(),\n        recipient=VExistingUname(\"recipient\"),\n        num_months=VInt('num_months', num_default=0),\n    )\n    def POST_givegold(self, form, jquery, recipient, num_months):\n        if form.has_errors(\"recipient\",\n                           errors.USER_DOESNT_EXIST, errors.NO_USER):\n            return\n        \n        if not recipient.gold and num_months < 0:\n            form.set_text(\".status\", _('no gold to take'))\n            return\n\n        admintools.adjust_gold_expiration(recipient, months=num_months)\n        form.set_text(\".status\", _('saved'))\n\n    @noresponse(VUser(),\n                VModhash(),\n                ui_elem=VOneOf('id', ('organic',)))\n    def POST_disable_ui(self, ui_elem):\n        if ui_elem:\n            pref = \"pref_%s\" % ui_elem\n            if getattr(c.user, pref):\n                setattr(c.user, \"pref_\" + ui_elem, False)\n                c.user._commit()\n\n    @noresponse(VUser(),\n                VModhash(),\n                show_nsfw_media=VBoolean(\"show_nsfw_media\"))\n    def POST_set_nsfw_media_pref(self, show_nsfw_media):\n        changed = False\n\n        if show_nsfw_media is not None:\n            no_profanity = not show_nsfw_media\n            if c.user.pref_no_profanity != no_profanity:\n                c.user.pref_no_profanity = no_profanity\n                changed = True\n\n        if not c.user.nsfw_media_acknowledged:\n            c.user.nsfw_media_acknowledged = True\n            changed = True\n\n        if changed:\n            c.user._commit()\n\n    @validatedForm(type = VOneOf('type', ('click'), default = 'click'),\n                   links = VByName('ids', thing_cls = Link, multiple = True))\n    def GET_gadget(self, form, jquery, type, links):\n        if not links and type == 'click':\n            # malformed cookie, clear it out\n            set_user_cookie('recentclicks2', '')\n\n        if not links:\n            return\n\n        content = ClickGadget(links).make_content()\n\n        jquery('.gadget').show().find('.click-gadget').html(\n            spaceCompress(content))\n\n    @csrf_exempt\n    @require_oauth2_scope(\"read\")\n    @json_validate(query=VPrintable('query', max_length=50),\n                   include_over_18=VBoolean('include_over_18', default=True),\n                   exact=VBoolean('exact', default=False))\n    @api_doc(api_section.subreddits)\n    def POST_search_reddit_names(self, responder, query, include_over_18, exact):\n        \"\"\"List subreddit names that begin with a query string.\n\n        Subreddits whose names begin with `query` will be returned. If\n        `include_over_18` is false, subreddits with over-18 content\n        restrictions will be filtered from the results.\n\n        If `exact` is true, only an exact match will be returned.\n        \"\"\"\n        if query:\n            query = sr_path_rx.sub('\\g<name>', query.strip())\n\n        names = []\n        if query and exact:\n            try:\n                sr = Subreddit._by_name(query.strip())\n            except NotFound:\n                self.abort404()\n            else:\n                # not respecting include_over_18 for exact match\n                names = [sr.name]\n        elif query:\n            names = search_reddits(query, include_over_18)\n\n        return {'names': names}\n\n    @validate(link=VByName('link_id', thing_cls=Link))\n    def GET_expando(self, link):\n        if not link:\n            abort(404, 'not found')\n\n        # pass through wrap_links/IDBuilder to ensure the user can view the link\n        listing = wrap_links(link)\n        try:\n            wrapped_link = listing.things[0]\n        except IndexError:\n            wrapped_link = None\n\n        if wrapped_link and wrapped_link.link_child:\n            content = wrapped_link.link_child.content()\n            return websafe(spaceCompress(content))\n        else:\n            abort(404, 'not found')\n\n    @csrf_exempt\n    def POST_expando(self):\n        return self.GET_expando()\n\n    @validatedForm(\n        VUser(),\n        VModhash(),\n        VVerifyPassword(\"password\", fatal=False),\n        VOneTimePassword(\"otp\",\n                         required=not g.disable_require_admin_otp),\n        remember=VBoolean(\"remember\"),\n        dest=VDestination(),\n    )\n    def POST_adminon(self, form, jquery, remember, dest):\n        if c.user.name not in g.admins:\n            self.abort403()\n\n        if form.has_errors('password', errors.WRONG_PASSWORD):\n            return\n\n        if form.has_errors(\"otp\", errors.WRONG_PASSWORD,\n                                  errors.NO_OTP_SECRET,\n                                  errors.RATELIMIT):\n            return\n\n        if remember:\n            self.remember_otp(c.user)\n\n        self.enable_admin_mode(c.user)\n        form.redirect(dest)\n\n    @validatedForm(\n        VUser(),\n        VModhash(),\n        VVerifyPassword(\"password\", fatal=False),\n    )\n    def POST_generate_otp_secret(self, form, jquery):\n        if form.has_errors(\"password\", errors.WRONG_PASSWORD):\n            return\n\n        if c.user.otp_secret:\n            c.errors.add(errors.OTP_ALREADY_ENABLED, field=\"password\")\n            form.has_errors(\"password\", errors.OTP_ALREADY_ENABLED)\n            return\n\n        secret = totp.generate_secret()\n        g.gencache.set(\"otp:secret_\" + c.user._id36, secret, time=300)\n        jquery(\"body\").make_totp_qrcode(secret)\n\n    @validatedForm(VUser(),\n                   VModhash(),\n                   otp=nop(\"otp\"))\n    def POST_enable_otp(self, form, jquery, otp):\n        if form.has_errors(\"password\", errors.WRONG_PASSWORD):\n            return\n\n        if c.user.otp_secret:\n            c.errors.add(errors.OTP_ALREADY_ENABLED, field=\"otp\")\n            form.has_errors(\"otp\", errors.OTP_ALREADY_ENABLED)\n            return\n\n        secret = g.gencache.get(\"otp:secret_\" + c.user._id36)\n        if not secret:\n            c.errors.add(errors.EXPIRED, field=\"otp\")\n            form.has_errors(\"otp\", errors.EXPIRED)\n            return\n\n        if not VOneTimePassword.validate_otp(secret, otp):\n            c.errors.add(errors.WRONG_PASSWORD, field=\"otp\")\n            form.has_errors(\"otp\", errors.WRONG_PASSWORD)\n            return\n\n        c.user.otp_secret = secret\n        c.user._commit()\n\n        form.redirect(\"/prefs/security\")\n\n    @validatedForm(\n        VUser(),\n        VModhash(),\n        VVerifyPassword(\"password\", fatal=False),\n        VOneTimePassword(\"otp\", required=True),\n    )\n    def POST_disable_otp(self, form, jquery):\n        if form.has_errors(\"password\", errors.WRONG_PASSWORD):\n            return\n\n        if form.has_errors(\"otp\", errors.WRONG_PASSWORD,\n                                  errors.NO_OTP_SECRET,\n                                  errors.RATELIMIT):\n            return\n\n        c.user.otp_secret = \"\"\n        c.user._commit()\n        form.redirect(\"/prefs/security\")\n\n    @require_oauth2_scope(\"read\")\n    @json_validate(query=VLength(\"query\", max_length=50))\n    @api_doc(api_section.subreddits)\n    def GET_subreddits_by_topic(self, responder, query):\n        \"\"\"Return a list of subreddits that are relevant to a search query.\"\"\"\n        if not g.CLOUDSEARCH_SEARCH_API:\n            return []\n\n        query = query and query.strip()\n        if not query or len(query) < 2:\n            return []\n\n        # http://en.wikipedia.org/wiki/Most_common_words_in_English\n        common_english_words = {\n            'the', 'be', 'to', 'of', 'and', 'in', 'that', 'have', 'it', 'for',\n            'not', 'on', 'with', 'he', 'as', 'you', 'do', 'at', 'this', 'but',\n            'his', 'by', 'from', 'they', 'we', 'say', 'her', 'she', 'or', 'an',\n            'will', 'my', 'one', 'all', 'would', 'there', 'their', 'what', 'so',\n            'up', 'out', 'if', 'about', 'who', 'get', 'which', 'go', 'me',\n            'when', 'make', 'can', 'like', 'time', 'no', 'just', 'him', 'know',\n            'take', 'people', 'into', 'year', 'your', 'good', 'some', 'could',\n            'them', 'see', 'other', 'than', 'then', 'now', 'look', 'only',\n            'come', 'its', 'over', 'think', 'also', 'back', 'after', 'use',\n            'two', 'how', 'our', 'work', 'first', 'well', 'way', 'even', 'new',\n            'want', 'because', 'any', 'these', 'give', 'day', 'most', 'us',\n        }\n\n        if query.lower() in common_english_words:\n            return []\n\n        exclude = Subreddit.default_subreddits()\n\n        faceting = {\"reddit\":{\"sort\":\"-sum(text_relevance)\", \"count\":20}}\n        try:\n            results = g.search.SearchQuery(query, sort=\"relevance\",\n                                           faceting=faceting, num=0,\n                                           syntax=\"plain\").run()\n        except g.search.SearchException:\n            abort(500)\n\n        sr_results = []\n        for sr, count in results.subreddit_facets:\n            if (sr._id in exclude or (sr.over_18 and not c.over18)\n                  or sr.type == \"archived\"):\n                continue\n\n            sr_results.append({\n                \"name\": sr.name,\n            })\n\n        return sr_results\n\n    @noresponse(VUser(),\n                VModhash(),\n                client=VOAuth2ClientID())\n    def POST_revokeapp(self, client):\n        if client:\n            client.revoke(c.user)\n\n    @validatedForm(VUser(),\n                   VModhash(),\n                   name=VRequired('name', errors.NO_TEXT,\n                                  docs=dict(name=\"a name for the app\")),\n                   about_url=VSanitizedUrl('about_url'),\n                   icon_url=VSanitizedUrl('icon_url'),\n                   redirect_uri=VRedirectUri('redirect_uri'),\n                   app_type=VOneOf('app_type', OAuth2Client.APP_TYPES))\n    def POST_updateapp(self, form, jquery, name, about_url, icon_url,\n                       redirect_uri, app_type):\n        if (form.has_errors('name', errors.NO_TEXT) |\n            form.has_errors('redirect_uri', errors.BAD_URL) |\n            form.has_errors('redirect_uri', errors.NO_URL) |\n            form.has_errors('app_type', errors.INVALID_OPTION)):\n            return\n\n        # Web apps should be redirecting to web\n        if app_type == 'web':\n            parsed = urlparse(redirect_uri)\n            if parsed.scheme not in ('http', 'https'):\n                c.errors.add(errors.INVALID_SCHEME, field='redirect_uri',\n                        msg_params={\"schemes\": \"http, https\"})\n                form.has_errors('redirect_uri', errors.INVALID_SCHEME)\n                return\n\n        description = request.POST.get('description', '')\n\n        client_id = request.POST.get('client_id')\n        if client_id:\n            # client_id was specified, updating existing OAuth2Client\n            client = OAuth2Client.get_token(client_id)\n            if client.is_first_party() and not c.user_is_admin:\n                form.set_text('.status', _('this app can not be modified from this interface'))\n                return\n            if app_type != client.app_type:\n                # App type cannot be changed after creation\n                abort(400, \"invalid request\")\n                return\n            if not client:\n                form.set_text('.status', _('invalid client id'))\n                return\n            if client.deleted:\n                form.set_text('.status', _('cannot update deleted app'))\n                return\n            if not client.has_developer(c.user):\n                form.set_text('.status', _('app does not belong to you'))\n                return\n\n            client.name = name\n            client.description = description\n            client.about_url = about_url or ''\n            client.redirect_uri = redirect_uri\n            client._commit()\n            form.set_text('.status', _('application updated'))\n            apps = PrefApps([], [client])\n            jquery('#developed-app-%s' % client._id).replaceWith(\n                apps.render_developed_app(client, collapsed=False))\n        else:\n            # client_id was omitted or empty, creating new OAuth2Client\n            client = OAuth2Client._new(name=name,\n                                       description=description,\n                                       about_url=about_url or '',\n                                       redirect_uri=redirect_uri,\n                                       app_type=app_type)\n            client._commit()\n            client.add_developer(c.user)\n            form.set_text('.status', _('application created'))\n            apps = PrefApps([], [client])\n            jquery('#developed-apps > h1').show()\n            jquery('#developed-apps > ul').append(\n                apps.render_developed_app(client, collapsed=False))\n\n    @validatedForm(VUser(),\n                   VModhash(),\n                   client=VOAuth2ClientDeveloper(),\n                   account=VExistingUname('name'))\n    def POST_adddeveloper(self, form, jquery, client, account):\n        if not client:\n            return\n        if form.has_errors('name', errors.USER_DOESNT_EXIST, errors.NO_USER):\n            return\n        if client.is_first_party() and not c.user_is_admin:\n            c.errors.add(errors.DEVELOPER_FIRST_PARTY_APP, field='name')\n            form.set_error(errors.DEVELOPER_FIRST_PARTY_APP, 'name')\n            return\n        if ((account.employee or account == Account.system_user()) and\n           not c.user_is_admin):\n            c.errors.add(errors.DEVELOPER_PRIVILEGED_ACCOUNT, field='name')\n            form.set_error(errors.DEVELOPER_PRIVILEGED_ACCOUNT, 'name')\n            return\n        if client.has_developer(account):\n            c.errors.add(errors.DEVELOPER_ALREADY_ADDED, field='name')\n            form.set_error(errors.DEVELOPER_ALREADY_ADDED, 'name')\n            return\n        try:\n            client.add_developer(account)\n        except OverflowError:\n            c.errors.add(errors.TOO_MANY_DEVELOPERS, field='')\n            form.set_error(errors.TOO_MANY_DEVELOPERS, '')\n            return\n\n        form.set_text('.status', _('developer added'))\n        apps = PrefApps([], [client])\n        (jquery('#app-developer-%s input[name=\"name\"]' % client._id).val('')\n            .closest('.prefright').find('ul').append(\n                apps.render_editable_developer(client, account)))\n\n    @validatedForm(VUser(),\n                   VModhash(),\n                   client=VOAuth2ClientDeveloper(),\n                   account=VExistingUname('name'))\n    def POST_removedeveloper(self, form, jquery, client, account):\n        if client.is_first_party() and not c.user_is_admin:\n            c.errors.add(errors.DEVELOPER_FIRST_PARTY_APP, field='name')\n            form.set_error(errors.DEVELOPER_FIRST_PARTY_APP, 'name')\n            return\n        if client and account and not form.has_errors('name'):\n            client.remove_developer(account)\n            if account._id == c.user._id:\n                jquery('#developed-app-%s' % client._id).fadeOut()\n            else:\n                jquery('li#app-dev-%s-%s' % (client._id, account._id)).fadeOut()\n\n    @noresponse(VUser(),\n                VModhash(),\n                client=VOAuth2ClientDeveloper())\n    def POST_deleteapp(self, client):\n        if client:\n            client.deleted = True\n            client._commit()\n\n    @validatedMultipartForm(VUser(),\n                            VModhash(),\n                            client=VOAuth2ClientDeveloper(),\n                            icon_file=VUploadLength(\n                                'file', max_length=1024*128,\n                                docs=dict(file=\"an icon (72x72)\")))\n    def POST_setappicon(self, form, jquery, client, icon_file):\n        if not icon_file:\n            form.set_error(errors.TOO_LONG, 'file')\n        if not form.has_error():\n            try:\n                client.icon_url = media.upload_icon(icon_file, (72, 72))\n            except IOError, ex:\n                c.errors.add(errors.BAD_IMAGE,\n                             msg_params=dict(message=ex.message),\n                             field='file')\n                form.set_error(errors.BAD_IMAGE, 'file')\n            else:\n                client._commit()\n                form.set_text('.status', 'uploaded')\n                jquery('#developed-app-%s .app-icon img'\n                       % client._id).attr('src', make_url_protocol_relative(client.icon_url))\n                jquery('#developed-app-%s .ajax-upload-form'\n                       % client._id).hide()\n                jquery('#developed-app-%s .edit-app-icon-button'\n                       % client._id).toggleClass('collapsed')\n\n    @json_validate(\n        VUser(),\n        VModhash(),\n        thing=VByName(\"thing\"),\n        signed=VBoolean(\"signed\")\n    )\n    def POST_generate_payment_blob(self, responder, thing, signed):\n        if not thing:\n            abort(400, \"Bad Request\")\n\n        if thing._deleted:\n            abort(403, \"Forbidden\")\n\n        thing_sr = Subreddit._byID(thing.sr_id, data=True)\n        if (not thing_sr.can_view(c.user) or\n            not thing_sr.allow_gilding):\n            abort(403, \"Forbidden\")\n\n        try:\n            recipient = Account._byID(thing.author_id, data=True)\n        except NotFound:\n            self.abort404()\n\n        if recipient._deleted:\n            self.abort404()\n\n        VNotInTimeout().run(action_name=\"gild\", target=thing)\n\n        return generate_blob(dict(\n            goldtype=\"gift\",\n            account_id=c.user._id,\n            account_name=c.user.name,\n            status=\"initialized\",\n            signed=signed,\n            recipient=recipient.name,\n            giftmessage=None,\n            thing=thing._fullname,\n        ))\n\n    @json_validate(\n        VUser(),\n        VModhash(),\n        code=nop(\"code\"),\n        signed=VBoolean(\"signed\", default=False),\n        message=nop(\"message\")\n    )\n    def POST_modify_payment_blob(self, responder, code, signed, message):\n        if c.user.gild_reveal_username != signed:\n            c.user.gild_reveal_username = signed\n            c.user._commit()\n\n        updates = {}\n        updates[\"signed\"] = signed\n        if message and message.strip() != \"\":\n            updates[\"giftmessage\"] = _force_utf8(message)\n\n        update_blob(str(code), updates)\n\n    def OPTIONS_request_promo(self):\n        \"\"\"Send CORS headers for request_promo requests.\"\"\"\n        if \"Origin\" in request.headers:\n            origin = request.headers[\"Origin\"]\n            response.headers[\"Access-Control-Allow-Origin\"] = \"*\"\n            response.headers[\"Access-Control-Allow-Methods\"] = \"POST\"\n            response.headers[\"Access-Control-Allow-Headers\"] = \"Authorization, \"\n            response.headers[\"Access-Control-Allow-Credentials\"] = \"false\"\n            response.headers['Access-Control-Expose-Headers'] = \\\n                self.COMMON_REDDIT_HEADERS\n\n    @csrf_exempt\n    @validate(srnames=VPrintable(\"srnames\", max_length=2100))\n    def POST_request_promo(self, srnames):\n        self.OPTIONS_request_promo()\n\n        if not srnames:\n            return\n\n        srnames = srnames.split('+')\n        try:\n            srnames.remove(Frontpage.name)\n            srnames.append('')\n        except ValueError:\n            pass\n\n        promo_tuples = promote.lottery_promoted_links(srnames, n=10)\n        builder = CampaignBuilder(promo_tuples,\n                                  wrap=default_thing_wrapper(),\n                                  keep_fn=promote.promo_keep_fn,\n                                  num=1,\n                                  skip=True)\n        listing = LinkListing(builder, nextprev=False).listing()\n        promote.add_trackers(listing.things, c.site)\n        promote.update_served(listing.things)\n        if listing.things:\n            w = listing.things[0]\n            w.num = \"\"\n            return responsive(w.render(), space_compress=True)\n\n    @json_validate(\n        VUser(),\n        VModhash(),\n        collapsed=VBoolean('collapsed'),\n    )\n    def POST_set_left_bar_collapsed(self, responder, collapsed):\n        c.user.pref_collapse_left_bar = collapsed\n        c.user._commit()\n\n    @require_oauth2_scope(\"read\")\n    @validate(srs=VSRByNames(\"srnames\"),\n              to_omit=VSRByNames(\"omit\", required=False))\n    @api_doc(api_section.subreddits, uri='/api/recommend/sr/{srnames}')\n    def GET_subreddit_recommendations(self, srs, to_omit):\n        \"\"\"Return subreddits recommended for the given subreddit(s).\n\n        Gets a list of subreddits recommended for `srnames`, filtering out any\n        that appear in the optional `omit` param.\n\n        \"\"\"\n\n        srs = [sr for sr in srs.values() if not isinstance(sr, FakeSubreddit)]\n        to_omit = [sr for sr in to_omit.values() if not isinstance(sr, FakeSubreddit)]\n\n        omit_id36s = [sr._id36 for sr in to_omit]\n        rec_srs = recommender.get_recommendations(srs, to_omit=omit_id36s)\n        sr_data = [{'sr_name': sr.name} for sr in rec_srs]\n        return json.dumps(sr_data)\n\n\n    @validatedForm(VUser(),\n                   VModhash(),\n                   action=VOneOf(\"type\", FEEDBACK_ACTIONS),\n                   srs=VSRByNames(\"srnames\"))\n    def POST_rec_feedback(self, form, jquery, action, srs):\n        if form.has_errors(\"type\", errors.INVALID_OPTION):\n            return self.abort404()\n        AccountSRFeedback.record_feedback(c.user, srs.values(), action)\n\n\n    @validatedForm(\n        VUser(),\n        VModhash(),\n        seconds_visibility=VOneOf(\n            \"seconds_visibility\",\n            (\"public\", \"private\"),\n            default=\"private\",\n        ),\n    )\n    def POST_server_seconds_visibility(self, form, jquery, seconds_visibility):\n        c.user.pref_public_server_seconds = seconds_visibility == \"public\"\n        c.user._commit()\n\n        hook = hooks.get_hook(\"server_seconds_visibility.change\")\n        hook.call(user=c.user, value=c.user.pref_public_server_seconds)\n\n    @require_oauth2_scope(\"save\")\n    @noresponse(VGold(),\n                VModhash(),\n                links = VByName('links', thing_cls=Link, multiple=True,\n                                limit=100))\n    @api_doc(api_section.links_and_comments)\n    def POST_store_visits(self, links):\n        if not c.user.pref_store_visits or not links:\n            return\n\n        LinkVisitsByAccount._visit(c.user, links)\n\n    @validatedForm(\n        VAdmin(),\n        VModhash(),\n        system=VLength('system', 1024),\n        subject=VLength('subject', 1024),\n        note=VLength('note', 10000),\n        author=VLength('author', 1024),\n    )\n    def POST_add_admin_note(self, form, jquery, system, subject, note, author):\n        if form.has_errors(('system', 'subject', 'note', 'author'),\n                           errors.TOO_LONG):\n            return\n\n        if note:\n            from r2.models.admin_notes import AdminNotesBySystem\n            AdminNotesBySystem.add(system, subject, note, author)\n        form.refresh()\n\n    @require_oauth2_scope(\"modconfig\")\n    @validatedForm(\n        VSrModerator(perms=\"config\"),\n        VModhash(),\n        short_name=VAvailableSubredditRuleName(\"short_name\"),\n        description=VMarkdownLength(\"description\", max_length=500),\n        kind=VOneOf('kind', ['link', 'comment', 'all']),\n    )\n    def POST_add_subreddit_rule(self, form, jquery, short_name, description,\n            kind):\n        if not feature.is_enabled(\"subreddit_rules\", subreddit=c.site.name):\n            abort(404)\n        if form.has_errors(\"short_name\", errors.TOO_SHORT, errors.NO_TEXT,\n                errors.TOO_LONG, errors.SR_RULE_EXISTS, errors.SR_RULE_TOO_MANY):\n            return\n        if form.has_errors(\"description\", errors.TOO_LONG):\n            return\n        if form.has_errors(\"kind\", errors.INVALID_OPTION):\n            return\n\n        SubredditRules.create(c.site, short_name, description, kind)\n        ModAction.create(c.site, c.user, 'createrule', details=short_name)\n\n        if description:\n            description_html = safemarkdown(description, wrap=False)\n            form._send_data(description_html=description_html)\n\n        form.refresh()\n\n    @require_oauth2_scope(\"modconfig\")\n    @validatedForm(\n        VSrModerator(perms=\"config\"),\n        VModhash(),\n        rule=VSubredditRule(\"old_short_name\"),\n        short_name=VAvailableSubredditRuleName(\"short_name\", updating=True),\n        description=VMarkdownLength('description', max_length=500),\n        kind=VOneOf('kind', ['link', 'comment', 'all']),\n    )\n    def POST_update_subreddit_rule(self, form, jquery, rule,\n            short_name, description, kind):\n        if not feature.is_enabled(\"subreddit_rules\", subreddit=c.site.name):\n            abort(404)\n        if form.has_errors(\"old_short_name\", errors.SR_RULE_DOESNT_EXIST):\n            return\n        if form.has_errors(\"short_name\", errors.TOO_SHORT, errors.NO_TEXT,\n                errors.TOO_LONG, errors.SR_RULE_TOO_MANY):\n            return\n        # if the short_name is changing and the new short_name already exists\n        if rule[\"short_name\"] != short_name:\n            if form.has_errors(\"short_name\", errors.SR_RULE_EXISTS):\n                return\n        if form.has_errors(\"description\", errors.TOO_LONG):\n            return\n        if form.has_errors(\"kind\", errors.INVALID_OPTION):\n            return\n\n        SubredditRules.update(c.site, rule[\"short_name\"], short_name,\n            description, kind)\n        ModAction.create(c.site, c.user, 'editrule', details=short_name)\n\n        if description:\n            description_html = safemarkdown(description, wrap=False)\n            form._send_data(description_html=description_html)\n\n        form.refresh()\n\n    @require_oauth2_scope(\"modconfig\")\n    @validatedForm(\n        VSrModerator(perms=\"config\"),\n        VModhash(),\n        rule=VSubredditRule(\"short_name\"),\n    )\n    def POST_remove_subreddit_rule(self, form, jquery, rule):\n        if not feature.is_enabled(\"subreddit_rules\", subreddit=c.site.name):\n            abort(404)\n        if form.has_errors(\"rule\", errors.SR_RULE_DOESNT_EXIST):\n            return\n        short_name = rule[\"short_name\"]\n        SubredditRules.remove_rule(c.site, short_name)\n        ModAction.create(c.site, c.user, 'deleterule', details=short_name)\n        form.refresh()\n\n    @validatedForm(\n        VUser(),\n        VModhash(),\n        thing=VByName('thing'),\n    )\n    def GET_report_form(self, form, jquery, thing):\n        \"\"\"Return information about report reasons for the thing.\"\"\"\n        if feature.is_enabled(\"new_report_dialog\"):\n            if request.params.get(\"api_type\") == \"json\":\n                style = \"json\"\n                filter_by_kind = False\n            else:\n                style = \"html\"\n                filter_by_kind = True\n\n            report_form = SubredditReportForm(thing, filter_by_kind=filter_by_kind)\n\n            if style == \"json\":\n                form._send_data(\n                    rules=report_form.rules,\n                    sr_name=report_form.sr_name,\n                )\n            return report_form.render(style=style)\n        else:\n            return ReportForm(thing).render(style=\"html\")\n        abort(404, 'not found')\n\n\n    @validatedForm(VModhashIfLoggedIn())\n    def POST_hide_locationbar(self, form, jquery):\n        c.user.pref_hide_locationbar = True\n        c.user._commit()\n        jquery(\".locationbar\").hide()\n\n    @validatedForm(VModhashIfLoggedIn())\n    def POST_use_global_defaults(self, form, jquery):\n        c.user.pref_use_global_defaults = True\n        c.user._commit()\n        jquery.refresh()\n"
  },
  {
    "path": "r2/r2/controllers/api_docs.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport re\nfrom collections import defaultdict\nfrom itertools import chain\nimport inspect\nfrom os.path import abspath, relpath\n\nfrom pylons import app_globals as g\nfrom pylons.i18n import _\nfrom reddit_base import RedditController\nfrom r2.lib.utils import Storage\nfrom r2.lib.pages import BoringPage, ApiHelp\nfrom r2.lib.validator import validate, VOneOf\n\n# API sections displayed in the documentation page.\n# Each section can have a title and a markdown-formatted description.\nsection_info = {\n    'account': {\n        'title': 'account',\n    },\n    'flair': {\n        'title': 'flair',\n    },\n    'gold': {\n        'title': 'reddit gold',\n    },\n    'links_and_comments': {\n        'title': 'links & comments',\n    },\n    'messages': {\n        'title': 'private messages',\n    },\n    'moderation': {\n        'title': 'moderation',\n    },\n    'misc': {\n        'title': 'misc',\n    },\n    'listings': {\n        'title': 'listings',\n    },\n    'search': {\n        'title': 'search',\n    },\n    'subreddits': {\n        'title': 'subreddits',\n    },\n    'multis': {\n        'title': 'multis',\n    },\n    'users': {\n        'title': 'users',\n    },\n    'wiki': {\n        'title': 'wiki',\n    },\n    'captcha': {\n        'title': 'captcha',\n    }\n}\n\napi_section = Storage((k, k) for k in section_info)\n\ndef api_doc(section, uses_site=False, **kwargs):\n    \"\"\"\n    Add documentation annotations to the decorated function.\n\n    See ApidocsController.docs_from_controller for a list of annotation fields.\n    \"\"\"\n    def add_metadata(api_function):\n        doc = api_function._api_doc = getattr(api_function, '_api_doc', {})\n        if 'extends' in kwargs:\n            kwargs['extends'] = kwargs['extends']._api_doc\n        doc.update(kwargs)\n        doc['uses_site'] = uses_site\n        doc['section'] = section\n\n        return api_function\n    return add_metadata\n\nclass ApidocsController(RedditController):\n    @staticmethod\n    def docs_from_controller(controller, url_prefix='/api', oauth_only=False):\n        \"\"\"\n        Examines a controller for documentation.  A dictionary index of\n        sections containing dictionaries of URLs is returned.  For each URL, a\n        dictionary of HTTP methods (GET, POST, etc.) is contained.  For each\n        URL/method pair, a dictionary containing the following items is\n        available:\n\n        - `doc`: Markdown-formatted docstring.\n        - `uri`: Manually-specified URI to list the API method as\n        - `uri_variants`: Alternate URIs to access the API method from\n        - `supports_rss`: Indicates the URI also supports rss consumption\n        - `parameters`: Dictionary of possible parameter names and descriptions.\n        - `extends`: API method from which to inherit documentation\n        - `json_model`: The JSON model used instead of normal POST parameters\n        \"\"\"\n\n        api_docs = defaultdict(lambda: defaultdict(dict))\n        for name, func in controller.__dict__.iteritems():\n            method, sep, action = name.partition('_')\n            if not action:\n                continue\n\n            valid_methods = ('GET', 'POST', 'PUT', 'DELETE', 'PATCH')\n            api_doc = getattr(func, '_api_doc', None)\n            if api_doc and 'section' in api_doc and method in valid_methods:\n                docs = {}\n                docs['doc'] = inspect.getdoc(func)\n\n                if 'extends' in api_doc:\n                    docs.update(api_doc['extends'])\n                    # parameters are handled separately.\n                    docs['parameters'] = {}\n                docs.update(api_doc)\n\n                # hide parameters that don't need to be public\n                if 'parameters' in api_doc:\n                    docs['parameters'].pop('timeout', None)\n\n                # append a message to the docstring if supplied\n                notes = docs.get(\"notes\")\n                if notes:\n                    notes = \"\\n\".join(notes)\n                    if docs[\"doc\"]:\n                        docs[\"doc\"] += \"\\n\\n\" + notes\n                    else:\n                        docs[\"doc\"] = notes\n\n                uri = docs.get('uri') or '/'.join((url_prefix, action))\n                docs['uri'] = uri\n\n                if 'supports_rss' not in docs:\n                    docs['supports_rss'] = False\n\n                if api_doc['uses_site']:\n                    docs[\"in-subreddit\"] = True\n\n                oauth_perms = getattr(func, 'oauth2_perms', {})\n                oauth_allowed = oauth_perms.get('oauth2_allowed', False)\n                if not oauth_allowed:\n                    # Endpoint is not available over OAuth\n                    docs['oauth_scopes'] = []\n                else:\n                    # [None] signifies to the template to state\n                    # that the endpoint is accessible to any oauth client\n                    docs['oauth_scopes'] = (oauth_perms['required_scopes'] or\n                                            [None])\n\n                # add every variant to the index -- the templates will filter\n                # out variants in the long-form documentation\n                if oauth_only:\n                    if not oauth_allowed:\n                        continue\n                    for scope in docs['oauth_scopes']:\n                        for variant in chain([uri],\n                                             docs.get('uri_variants', [])):\n                            api_docs[scope][variant][method] = docs\n                else:\n                    for variant in chain([uri], docs.get('uri_variants', [])):\n                        api_docs[docs['section']][variant][method] = docs\n\n        return api_docs\n\n    @validate(\n        mode=VOneOf('mode', options=('methods', 'oauth'), default='methods'))\n    def GET_docs(self, mode):\n        # controllers to gather docs from.\n        from r2.controllers.api import ApiController, ApiminimalController\n        from r2.controllers.apiv1.user import APIv1UserController\n        from r2.controllers.apiv1.gold import APIv1GoldController\n        from r2.controllers.apiv1.scopes import APIv1ScopesController\n        from r2.controllers.captcha import CaptchaController\n        from r2.controllers.front import FrontController\n        from r2.controllers.wiki import WikiApiController, WikiController\n        from r2.controllers.multi import MultiApiController\n        from r2.controllers import listingcontroller\n\n        api_controllers = [\n            (APIv1UserController, '/api/v1'),\n            (APIv1GoldController, '/api/v1'),\n            (APIv1ScopesController, '/api/v1'),\n            (ApiController, '/api'),\n            (ApiminimalController, '/api'),\n            (WikiApiController, '/api/wiki'),\n            (WikiController, '/wiki'),\n            (MultiApiController, '/api/multi'),\n            (CaptchaController, ''),\n            (FrontController, ''),\n        ]\n        for name, value in vars(listingcontroller).iteritems():\n            if name.endswith('Controller'):\n                api_controllers.append((value, ''))\n\n        # bring in documented plugin controllers\n        api_controllers.extend(g.plugins.get_documented_controllers())\n\n        # merge documentation info together.\n        api_docs = defaultdict(dict)\n        oauth_index = defaultdict(set)\n        for controller, url_prefix in api_controllers:\n            controller_docs = self.docs_from_controller(controller, url_prefix,\n                                                        mode == 'oauth')\n            for section, contents in controller_docs.iteritems():\n                api_docs[section].update(contents)\n                for variant, method_dict in contents.iteritems():\n                    for method, docs in method_dict.iteritems():\n                        for scope in docs['oauth_scopes']:\n                            oauth_index[scope].add((section, variant, method))\n\n        return BoringPage(\n            _('api documentation'),\n            content=ApiHelp(\n                api_docs=api_docs,\n                oauth_index=oauth_index,\n                mode=mode,\n            ),\n            css_class=\"api-help\",\n            show_sidebar=False,\n            show_infobar=False\n        ).render()\n"
  },
  {
    "path": "r2/r2/controllers/apiv1/__init__.py",
    "content": ""
  },
  {
    "path": "r2/r2/controllers/apiv1/gold.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom pylons import request\nfrom pylons import tmpl_context as c\nfrom pylons import app_globals as g\n\nfrom r2.controllers.api_docs import api_doc, api_section\nfrom r2.controllers.oauth2 import require_oauth2_scope\nfrom r2.controllers.reddit_base import OAuth2OnlyController\nfrom r2.controllers.ipn import send_gift\nfrom r2.lib.errors import RedditError\nfrom r2.lib.validator import (\n    validate,\n    VAccountByName,\n    VByName,\n    VInt,\n    VNotInTimeout,\n)\nfrom r2.models import Account, Comment, Link, NotFound\nfrom r2.models.gold import creddits_lock\nfrom r2.lib.validator import VUser\n\n\nclass APIv1GoldController(OAuth2OnlyController):\n    def _gift_using_creddits(self, recipient, months=1, thing_fullname=None,\n            proxying_for=None):\n        with creddits_lock(c.user):\n            if not c.user.employee and c.user.gold_creddits < months:\n                err = RedditError(\"INSUFFICIENT_CREDDITS\")\n                self.on_validation_error(err)\n\n            note = None\n            buyer = c.user\n            if c.user.name.lower() in g.live_config[\"proxy_gilding_accounts\"]:\n                note = \"proxy-%s\" % c.user.name\n                if proxying_for:\n                    try:\n                        buyer = Account._by_name(proxying_for)\n                    except NotFound:\n                        pass\n\n            send_gift(\n                buyer=buyer,\n                recipient=recipient,\n                months=months,\n                days=months * 31,\n                signed=False,\n                giftmessage=None,\n                thing_fullname=thing_fullname,\n                note=note,\n            )\n\n            if not c.user.employee:\n                c.user.gold_creddits -= months\n                c.user._commit()\n\n    @require_oauth2_scope(\"creddits\")\n    @validate(\n        VUser(),\n        target=VByName(\"fullname\"),\n    )\n    @api_doc(\n        api_section.gold,\n        uri=\"/api/v1/gold/gild/{fullname}\",\n    )\n    def POST_gild(self, target):\n        if not isinstance(target, (Comment, Link)):\n            err = RedditError(\"NO_THING_ID\")\n            self.on_validation_error(err)\n\n        if target.subreddit_slow.quarantine:\n            err = RedditError(\"GILDING_NOT_ALLOWED\")\n            self.on_validation_error(err)\n        VNotInTimeout().run(target=target, subreddit=target.subreddit_slow)\n\n        self._gift_using_creddits(\n            recipient=target.author_slow,\n            thing_fullname=target._fullname,\n            proxying_for=request.POST.get(\"proxying_for\"),\n        )\n\n    @require_oauth2_scope(\"creddits\")\n    @validate(\n        VUser(),\n        user=VAccountByName(\"username\"),\n        months=VInt(\"months\", min=1, max=36),\n        timeout=VNotInTimeout(),\n    )\n    @api_doc(\n        api_section.gold,\n        uri=\"/api/v1/gold/give/{username}\",\n    )\n    def POST_give(self, user, months, timeout):\n        self._gift_using_creddits(\n            recipient=user,\n            months=months,\n            proxying_for=request.POST.get(\"proxying_for\"),\n        )\n"
  },
  {
    "path": "r2/r2/controllers/apiv1/login.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\nfrom pylons import request\nfrom pylons import tmpl_context as c\n\nfrom r2.config.extensions import set_extension\nfrom r2.controllers.reddit_base import RedditController, generate_modhash\nfrom r2.controllers.login import handle_login, handle_register\nfrom r2.lib.csrf import csrf_exempt\nfrom r2.lib.validator import (\n    json_validate,\n    ValidEmail,\n    VPasswordChange,\n    VRatelimit,\n    VSigned,\n    VThrottledLogin,\n    VUname,\n)\n\n\nclass APIv1LoginController(RedditController):\n\n    def pre(self):\n        super(APIv1LoginController, self).pre()\n        c.extension = \"json\"\n        set_extension(request.environ, \"json\")\n\n    @csrf_exempt\n    @json_validate(\n        VRatelimit(rate_ip=True, prefix=\"rate_register_\"),\n        signature=VSigned(),\n        name=VUname(['user']),\n        email=ValidEmail(\"email\"),\n        password=VPasswordChange(['passwd', 'passwd2']),\n    )\n    def POST_register(self, responder, name, email, password, **kwargs):\n        kwargs.update(dict(\n            controller=self,\n            form=responder(\"noop\"),\n            responder=responder,\n            name=name,\n            email=email,\n            password=password,\n        ))\n        return handle_register(**kwargs)\n\n    @csrf_exempt\n    @json_validate(\n        signature=VSigned(),\n        user=VThrottledLogin(['user', 'passwd']),\n    )\n    def POST_login(self, responder, user, **kwargs):\n        kwargs.update(dict(\n            controller=self,\n            form=responder(\"noop\"),\n            responder=responder,\n            user=user,\n        ))\n        return handle_login(**kwargs)\n\n    def _login(self, responder, user, rem=None):\n        \"\"\"Login the user.\n\n        AJAX login handler, used by both login and register to set the\n        user cookie and send back a redirect.\n        \"\"\"\n        c.user = user\n        c.user_is_loggedin = True\n        self.login(user, rem=rem)\n\n        responder._send_data(modhash=generate_modhash())\n        responder._send_data(cookie=user.make_cookie())\n"
  },
  {
    "path": "r2/r2/controllers/apiv1/scopes.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\nfrom webob.exc import HTTPBadRequest\nfrom r2.controllers import api_docs\nfrom r2.controllers.oauth2 import allow_oauth2_access\nfrom r2.controllers.reddit_base import RedditController\nfrom r2.lib.base import abort\nfrom r2.lib.validator import validate, nop\nfrom r2.models import OAuth2Scope\n\n\nclass APIv1ScopesController(RedditController):\n    THREE_SIXTY = OAuth2Scope.FULL_ACCESS\n\n    @allow_oauth2_access\n    @validate(\n        scope_str=nop(\"scopes\",\n                   docs={\"scopes\": \"(optional) An OAuth2 scope string\"}),\n    )\n    @api_docs.api_doc(api_docs.api_section.misc)\n    def GET_scopes(self, scope_str):\n        \"\"\"Retrieve descriptions of reddit's OAuth2 scopes.\n\n        If no scopes are given, information on all scopes are returned.\n\n        Invalid scope(s) will result in a 400 error with body that indicates\n        the invalid scope(s).\n\n        \"\"\"\n        scopes = OAuth2Scope(scope_str or self.THREE_SIXTY)\n        if scope_str and not scopes.is_valid():\n            invalid = [s for s in scopes.scopes if s not in scopes.scope_info]\n            error = {\"error\": \"invalid_scopes\", \"invalid_scopes\": invalid}\n            http_err = HTTPBadRequest()\n            http_err.error_data = error\n            abort(http_err)\n        return self.api_wrapper({k: v for k, v in scopes.details() if k})\n"
  },
  {
    "path": "r2/r2/controllers/apiv1/user.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\nfrom pylons import response\nfrom pylons import tmpl_context as c\n\nfrom r2.controllers.api_docs import api_doc, api_section\nfrom r2.controllers.oauth2 import require_oauth2_scope\nfrom r2.controllers.reddit_base import OAuth2OnlyController\nfrom r2.lib.jsontemplates import (\n    FriendTableItemJsonTemplate,\n    get_usertrophies,\n    IdentityJsonTemplate,\n    KarmaListJsonTemplate,\n    PrefsJsonTemplate,\n)\nfrom r2.lib.pages import FriendTableItem\nfrom r2.lib.validator import (\n    validate,\n    VAccountByName,\n    VFriendOfMine,\n    VLength,\n    VList,\n    VUser,\n    VValidatedJSON,\n)\nfrom r2.models import Account, Trophy\nimport r2.lib.errors as errors\nimport r2.lib.validator.preferences as vprefs\n\n\nPREFS_JSON_SPEC = VValidatedJSON.PartialObject({\n    k[len(\"pref_\"):]: v for k, v in\n    vprefs.PREFS_VALIDATORS.iteritems()\n})\n\n\nclass APIv1UserController(OAuth2OnlyController):\n    @require_oauth2_scope(\"identity\")\n    @validate(\n        VUser(),\n    )\n    @api_doc(api_section.account)\n    def GET_me(self):\n        \"Returns the identity of the user currently authenticated via OAuth.\"\n        resp = IdentityJsonTemplate().data(c.oauth_user)\n        return self.api_wrapper(resp)\n\n    @require_oauth2_scope(\"identity\")\n    @validate(\n        VUser(),\n        fields=VList(\n            \"fields\",\n            choices=PREFS_JSON_SPEC.spec.keys(),\n            error=errors.errors.NON_PREFERENCE,\n        ),\n    )\n    @api_doc(api_section.account, uri='/api/v1/me/prefs')\n    def GET_prefs(self, fields):\n        \"\"\"Return the preference settings of the logged in user\"\"\"\n        resp = PrefsJsonTemplate(fields).data(c.oauth_user)\n        return self.api_wrapper(resp)\n\n    @require_oauth2_scope(\"read\")\n    @validate(\n        user=VAccountByName('username'),\n    )\n    @api_doc(\n        section=api_section.users,\n        uri='/api/v1/user/{username}/trophies',\n    )\n    def GET_usertrophies(self, user):\n        \"\"\"Return a list of trophies for the a given user.\"\"\"\n        return self.api_wrapper(get_usertrophies(user))\n\n    @require_oauth2_scope(\"identity\")\n    @validate(\n        VUser(),\n    )\n    @api_doc(\n        section=api_section.account,\n        uri='/api/v1/me/trophies',\n    )\n    def GET_trophies(self):\n        \"\"\"Return a list of trophies for the current user.\"\"\"\n        return self.api_wrapper(get_usertrophies(c.oauth_user))\n\n    @require_oauth2_scope(\"mysubreddits\")\n    @validate(\n        VUser(),\n    )\n    @api_doc(\n        section=api_section.account,\n        uri='/api/v1/me/karma',\n    )\n    def GET_karma(self):\n        \"\"\"Return a breakdown of subreddit karma.\"\"\"\n        karmas = c.oauth_user.all_karmas(include_old=False)\n        resp = KarmaListJsonTemplate().render(karmas)\n        return self.api_wrapper(resp.finalize())\n\n    PREFS_JSON_VALIDATOR = VValidatedJSON(\"json\", PREFS_JSON_SPEC,\n                                          body=True)\n\n    @require_oauth2_scope(\"account\")\n    @validate(\n        VUser(),\n        validated_prefs=PREFS_JSON_VALIDATOR,\n    )\n    @api_doc(api_section.account, json_model=PREFS_JSON_VALIDATOR,\n             uri='/api/v1/me/prefs')\n    def PATCH_prefs(self, validated_prefs):\n        user_prefs = c.user.preferences()\n        for short_name, new_value in validated_prefs.iteritems():\n            pref_name = \"pref_\" + short_name\n            user_prefs[pref_name] = new_value\n        vprefs.filter_prefs(user_prefs, c.user)\n        vprefs.set_prefs(c.user, user_prefs)\n        c.user._commit()\n        return self.api_wrapper(PrefsJsonTemplate().data(c.user))\n\n    FRIEND_JSON_SPEC = VValidatedJSON.PartialObject({\n        \"name\": VAccountByName(\"name\"),\n        \"note\": VLength(\"note\", 300),\n    })\n    FRIEND_JSON_VALIDATOR = VValidatedJSON(\"json\", spec=FRIEND_JSON_SPEC,\n                                           body=True)\n    @require_oauth2_scope('subscribe')\n    @validate(\n        VUser(),\n        friend=VAccountByName('username'),\n        notes_json=FRIEND_JSON_VALIDATOR,\n    )\n    @api_doc(api_section.users, json_model=FRIEND_JSON_VALIDATOR,\n             uri='/api/v1/me/friends/{username}')\n    def PUT_friends(self, friend, notes_json):\n        \"\"\"Create or update a \"friend\" relationship.\n\n        This operation is idempotent. It can be used to add a new\n        friend, or update an existing friend (e.g., add/change the\n        note on that friend)\n\n        \"\"\"\n        err = None\n        if 'name' in notes_json and notes_json['name'] != friend:\n            # The 'name' in the JSON is optional, but if present, must\n            # match the username from the URL\n            err = errors.RedditError('BAD_USERNAME', fields='name')\n        if 'note' in notes_json and not c.user.gold:\n            err = errors.RedditError('GOLD_REQUIRED', fields='note')\n        if err:\n            self.on_validation_error(err)\n\n        # See if the target is already an existing friend.\n        # If not, create the friend relationship.\n        friend_rel = Account.get_friend(c.user, friend)\n        rel_exists = bool(friend_rel)\n        if not friend_rel:\n            friend_rel = c.user.add_friend(friend)\n            response.status = 201\n\n        if 'note' in notes_json:\n            note = notes_json['note'] or ''\n            if not rel_exists:\n                # If this is a newly created friend relationship,\n                # the cache needs to be updated before a note can\n                # be applied\n                c.user.friend_rels_cache(_update=True)\n            c.user.add_friend_note(friend, note)\n        rel_view = FriendTableItem(friend_rel)\n        return self.api_wrapper(FriendTableItemJsonTemplate().data(rel_view))\n\n    @require_oauth2_scope('mysubreddits')\n    @validate(\n        VUser(),\n        friend_rel=VFriendOfMine('username'),\n    )\n    @api_doc(api_section.users, uri='/api/v1/me/friends/{username}')\n    def GET_friends(self, friend_rel):\n        \"\"\"Get information about a specific 'friend', such as notes.\"\"\"\n        rel_view = FriendTableItem(friend_rel)\n        return self.api_wrapper(FriendTableItemJsonTemplate().data(rel_view))\n\n    @require_oauth2_scope('subscribe')\n    @validate(\n        VUser(),\n        friend_rel=VFriendOfMine('username'),\n    )\n    @api_doc(api_section.users, uri='/api/v1/me/friends/{username}')\n    def DELETE_friends(self, friend_rel):\n        \"\"\"Stop being friends with a user.\"\"\"\n        c.user.remove_friend(friend_rel._thing2)\n        if c.user.gold:\n            c.user.friend_rels_cache(_update=True)\n        response.status = 204\n"
  },
  {
    "path": "r2/r2/controllers/awards.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom pylons import request\nfrom pylons import app_globals as g\nfrom reddit_base import RedditController\nfrom r2.lib.pages import AdminPage, AdminAwards\nfrom r2.lib.pages import AdminAwardGive, AdminAwardWinners\nfrom r2.lib.validator import *\n\nclass AwardsController(RedditController):\n\n    @validate(VAdmin())\n    def GET_index(self):\n        res = AdminPage(content = AdminAwards(),\n                        title = 'awards').render()\n        return res\n\n    @validate(VAdmin(),\n              award = VAwardByCodename('awardcn'),\n              recipient = nop('recipient'),\n              desc = nop('desc'),\n              url = nop('url'),\n              hours = nop('hours'))\n    def GET_give(self, award, recipient, desc, url, hours):\n        if award is None:\n            abort(404, 'page not found')\n\n        res = AdminPage(content = AdminAwardGive(award, recipient, desc,\n                                                 url, hours),\n                        title='give an award').render()\n        return res\n\n    @validate(VAdmin(),\n              award = VAwardByCodename('awardcn'))\n    def GET_winners(self, award):\n        if award is None:\n            abort(404, 'page not found')\n\n        res = AdminPage(content = AdminAwardWinners(award),\n                        title='award winners').render()\n        return res\n"
  },
  {
    "path": "r2/r2/controllers/buttons.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom reddit_base import RedditController, UnloggedUser\nfrom r2.lib.pages import (ButtonLite, ButtonDemoPanel, WidgetDemoPanel,\n                          BoringPage)\nfrom r2.lib.pages.things import wrap_links\nfrom r2.models import *\nfrom r2.lib.validator import *\nfrom pylons import request, response\nfrom pylons import tmpl_context as c\nfrom pylons.i18n import _\n\nclass ButtonsController(RedditController):\n    def get_wrapped_link(self, url, link = None, wrapper = None):\n        try:\n            links = []\n            if link:\n                links = [link]\n            else:\n                sr = None if isinstance(c.site, FakeSubreddit) else c.site\n                try:\n                    links = Link._by_url(url, sr)\n                except NotFound:\n                    pass\n\n            if links:\n                kw = {}\n                if wrapper:\n                    links = wrap_links(links, wrapper = wrapper)\n                else:\n                    links = wrap_links(links)\n                links = list(links)\n                links = max(links, key = lambda x: x._score) if links else None\n            if not links and wrapper:\n                return wrapper(None)\n            return links\n            # note: even if _by_url successed or a link was passed in,\n            # it is possible link_listing.things is empty if the\n            # link(s) is/are members of a private reddit\n            # return the link with the highest score (if more than 1)\n        except:\n            #we don't want to return 500s in other people's pages.\n            import traceback\n            g.log.debug(\"FULLPATH: get_link error in buttons code\")\n            g.log.debug(traceback.format_exc())\n            if wrapper:\n                return wrapper(None)\n\n    @validate(buttontype = VInt('t', 1, 5))\n    def GET_button_embed(self, buttontype):\n        if not buttontype:\n            abort(404)\n\n        return self.redirect('/static/button/button%s.js' % buttontype,\n                             code=301)\n\n    @validate(buttonimage = VInt('i', 0, 14),\n              title = nop('title'),\n              url = VSanitizedUrl('url'),\n              newwindow = VBoolean('newwindow', default = False),\n              styled = VBoolean('styled', default=True))\n    def GET_button_lite(self, buttonimage, title, url, styled, newwindow):\n        c.user = UnloggedUser([c.lang])\n        c.user_is_loggedin = False\n        c.render_style = 'js'\n\n        if not url:\n            url = request.referer\n\n        def builder_wrapper(thing = None):\n            kw = {}\n            if not thing:\n                kw['url'] = url\n                kw['title'] = title\n            return ButtonLite(thing,\n                              image = 1 if buttonimage is None else buttonimage,\n                              target = \"_new\" if newwindow else \"_parent\",\n                              styled = styled, **kw)\n\n        bjs = self.get_wrapped_link(url, wrapper = builder_wrapper)\n        response.content_type = \"text/javascript\"\n        return bjs.render()\n\n    def GET_button_demo_page(self):\n        # no buttons for domain listings -> redirect to top level\n        if isinstance(c.site, DomainSR):\n            return self.redirect('/buttons')\n        return BoringPage(_(\"reddit buttons\"),\n                          show_sidebar = False, \n                          content=ButtonDemoPanel()).render()\n\n    def GET_widget_demo_page(self):\n        return BoringPage(_(\"reddit widget\"),\n                          show_sidebar = False, \n                          content=WidgetDemoPanel()).render()\n"
  },
  {
    "path": "r2/r2/controllers/captcha.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom reddit_base import RedditController\nimport StringIO\nimport r2.lib.captcha as captcha\nfrom pylons import response\n\nfrom r2.controllers.api_docs import api_doc, api_section\nfrom r2.controllers.oauth2 import allow_oauth2_access\n\nclass CaptchaController(RedditController):\n    @allow_oauth2_access\n    @api_doc(api_section.captcha, uri='/captcha/{iden}')\n    def GET_captchaimg(self, iden):\n        \"\"\"\n        Request a CAPTCHA image given an `iden`.\n\n        An iden is given as the `captcha` field with a `BAD_CAPTCHA`\n        error, you should use this endpoint if you get a\n        `BAD_CAPTCHA` error response.\n\n        Responds with a 120x50 `image/png` which should be displayed\n        to the user.\n\n        The user's response to the CAPTCHA should be sent as `captcha`\n        along with your request.\n\n        To request a new CAPTCHA,\n        use [/api/new_captcha](#POST_api_new_captcha).\n        \"\"\"\n        image = captcha.get_image(iden)\n        f = StringIO.StringIO()\n        image.save(f, \"PNG\")\n        response.content_type = \"image/png;\"\n        return f.getvalue()\n    \n"
  },
  {
    "path": "r2/r2/controllers/embed.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom r2.controllers.reddit_base import RedditController\nfrom r2.lib.base import proxyurl\nfrom r2.lib.csrf import csrf_exempt\nfrom r2.lib.template_helpers import get_domain\nfrom r2.lib.pages import Embed, BoringPage, HelpPage\nfrom r2.lib.filters import websafe, SC_OFF, SC_ON\nfrom r2.lib.memoize import memoize\n\nfrom pylons.i18n import _\nfrom pylons import request\nfrom pylons import tmpl_context as c\nfrom pylons import app_globals as g\n\nfrom BeautifulSoup import BeautifulSoup, Tag\n\nfrom urllib2 import HTTPError\n\n@memoize(\"renderurl_cached\", time=60)\ndef renderurl_cached(path):\n    # Needed so http://reddit.com/help/ works\n    fp = path.rstrip(\"/\")\n    u = \"https://code.reddit.com/wiki\" + fp + '?stripped=1'\n\n    g.log.debug(\"Pulling %s for help\" % u)\n\n    try:\n        return fp, proxyurl(u)\n    except HTTPError, e:\n        if e.code != 404:\n            print \"error %s\" % e.code\n            print e.fp.read()\n        return (None, None)\n\nclass EmbedController(RedditController):\n    allow_stylesheets = True\n\n    def rendercontent(self, input, fp):\n        soup = BeautifulSoup(input)\n\n        output = soup.find(\"div\", { 'class':'wiki', 'id':'content'} )\n\n        # Replace all links to \"/wiki/help/...\" with \"/help/...\"\n        for link in output.findAll('a'):\n            if link.has_key('href') and link['href'].startswith(\"/wiki/help\"):\n                link['href'] = link['href'][5:]\n\n        output = SC_OFF + unicode(output) + SC_ON\n\n        return HelpPage(_(\"help\"),\n                        content = Embed(content=output),\n                        show_sidebar = None).render()\n\n    @csrf_exempt\n    def renderurl(self, override=None):\n        if override:\n            path = override\n        else:\n            path = request.path\n\n        fp, content = renderurl_cached(path)\n        if content is None:\n            self.abort404()\n        return self.rendercontent(content, fp)\n\n    GET_help = POST_help = renderurl\n\n    def GET_blog(self):\n        return self.redirect(\"https://blog.%s/\" %\n                             get_domain(subreddit=False, no_www=True))\n\n    def GET_faq(self):\n        if c.default_sr:\n            return self.redirect('/help/faq')\n        else:\n            return self.renderurl('/help/faqs/' + c.site.name)\n"
  },
  {
    "path": "r2/r2/controllers/error.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport json\nimport os\nimport random\n\nimport pylons\n\nfrom webob.exc import HTTPFound, HTTPMovedPermanently\nfrom pylons.i18n import _\nfrom pylons import request, response\nfrom pylons import tmpl_context as c\nfrom pylons import app_globals as g\n\n\ntry:\n    # place all r2 specific imports in here.  If there is a code error, it'll\n    # get caught and the stack trace won't be presented to the user in\n    # production\n    from r2.config import extensions\n    from r2.controllers.reddit_base import RedditController, UnloggedUser\n    from r2.lib.cookies import Cookies\n    from r2.lib.errors import ErrorSet\n    from r2.lib.filters import (\n        safemarkdown,\n        scriptsafe_dumps,\n        websafe,\n        websafe_json,\n    )\n    from r2.lib import log, pages\n    from r2.lib.strings import get_funny_translated_string\n    from r2.lib.template_helpers import static\n    from r2.lib.base import abort\n    from r2.models.link import Link\n    from r2.models.subreddit import DefaultSR, Subreddit\nexcept Exception, e:\n    if g.debug:\n        # if debug mode, let the error filter up to pylons to be handled\n        raise e\n    else:\n        # production environment: protect the code integrity!\n        print \"HuffmanEncodingError: make sure your python compiles before deploying, stupid!\"\n        # kill this app\n        os._exit(1)\n\n\nredditbroke =  \\\n'''<html>\n  <head>\n    <title>reddit broke!</title>\n  </head>\n  <body>\n    <div style=\"margin: auto; text-align: center\">\n      <p>\n        <a href=\"/\">\n          <img border=\"0\" src=\"%s\" alt=\"you broke reddit\" />\n        </a>\n      </p>\n      <p>\n        %s\n      </p>\n  </body>\n</html>\n'''\n\n\nFAILIEN_COUNT = 3\ndef make_failien_url():\n    failien_number = random.randint(1, FAILIEN_COUNT)\n    failien_name = \"youbrokeit%d.png\" % failien_number\n    return static(failien_name)\n\n\nclass ErrorController(RedditController):\n    \"\"\"Generates error documents as and when they are required.\n\n    The ErrorDocuments middleware forwards to ErrorController when error\n    related status codes are returned from the application.\n\n    This behaviour can be altered by changing the parameters to the\n    ErrorDocuments middleware in your config/middleware.py file.\n    \"\"\"\n    # Handle POST endpoints redirecting to the error controller\n    handles_csrf = True\n\n    def check_for_bearer_token(self):\n        pass\n\n    allowed_render_styles = ('html', 'xml', 'js', 'embed', '', \"compact\", 'api')\n    # List of admins to blame (skip the first admin, \"reddit\")\n    # If list is empty, just blame \"an admin\"\n    admins = g.admins[1:] or [\"an admin\"]\n    def __before__(self):\n        try:\n            c.error_page = True\n            RedditController.__before__(self)\n        except (HTTPMovedPermanently, HTTPFound):\n            # ignore an attempt to redirect from an error page\n            pass\n        except Exception as e:\n            handle_awful_failure(\"ErrorController.__before__: %r\" % e)\n\n        # c.error_page is special-cased in a couple places to bypass\n        # c.site checks. We shouldn't allow the user to get here other\n        # than through `middleware.py:error_mapper`.\n        if not request.environ.get('pylons.error_call'):\n            abort(403, \"direct access to error controller disallowed\")\n\n    def __after__(self): \n        try:\n            RedditController.__after__(self)\n        except Exception as e:\n            handle_awful_failure(\"ErrorController.__after__: %r\" % e)\n\n    def __call__(self, environ, start_response):\n        try:\n            return RedditController.__call__(self, environ, start_response)\n        except Exception as e:\n            return handle_awful_failure(\"ErrorController.__call__: %r\" % e)\n\n\n    def send400(self):\n        if 'usable_error_content' in request.environ:\n            return request.environ['usable_error_content']\n        else:\n            res = pages.RedditError(\n                title=_(\"bad request (%(domain)s)\") % dict(domain=g.domain),\n                message=_(\"you sent an invalid request\"),\n                explanation=request.GET.get('explanation'))\n            return res.render()\n\n    def send403(self):\n        c.site = DefaultSR()\n        if 'usable_error_content' in request.environ:\n            return request.environ['usable_error_content']\n        else:\n            res = pages.RedditError(\n                title=_(\"forbidden (%(domain)s)\") % dict(domain=g.domain),\n                message=_(\"you are not allowed to do that\"),\n                explanation=request.GET.get('explanation'))\n            return res.render()\n\n    def send404(self):\n        if 'usable_error_content' in request.environ:\n            return request.environ['usable_error_content']\n        return pages.RedditError(_(\"page not found\"),\n                                 _(\"the page you requested does not exist\")).render()\n\n    def send429(self):\n        retry_after = request.environ.get(\"retry_after\")\n        if retry_after:\n            response.headers[\"Retry-After\"] = str(retry_after)\n            template_name = '/ratelimit_toofast.html'\n        else:\n            template_name = '/ratelimit_throttled.html'\n\n        template = g.mako_lookup.get_template(template_name)\n        return template.render(\n            logo_url=static(g.default_header_url),\n            retry_after=retry_after,\n        )\n\n    def send503(self):\n        retry_after = request.environ.get(\"retry_after\")\n        if retry_after:\n            response.headers[\"Retry-After\"] = str(retry_after)\n        return request.environ['usable_error_content']\n\n    def GET_document(self):\n        try:\n            c.errors = c.errors or ErrorSet()\n            # clear cookies the old fashioned way \n            c.cookies = Cookies()\n\n            code =  request.GET.get('code', '')\n            try:\n                code = int(code)\n            except ValueError:\n                code = 404\n            srname = request.GET.get('srname', '')\n            takedown = request.GET.get('takedown', '')\n            error_name = request.GET.get('error_name', '')\n\n            if isinstance(c.user, basestring):\n                # somehow requests are getting here with c.user unset\n                c.user_is_loggedin = False\n                c.user = UnloggedUser(browser_langs=None)\n\n            if srname:\n                c.site = Subreddit._by_name(srname)\n\n            if request.GET.has_key('allow_framing'):\n                c.allow_framing = bool(request.GET['allow_framing'] == '1')\n\n            if (error_name == 'IN_TIMEOUT' and\n                    not 'usable_error_content' in request.environ):\n                timeout_days_remaining = c.user.days_remaining_in_timeout\n\n                errpage = pages.InterstitialPage(\n                    _(\"suspended\"),\n                    content=pages.InTimeoutInterstitial(\n                        timeout_days_remaining=timeout_days_remaining,\n                    ),\n                )\n                request.environ['usable_error_content'] = errpage.render()\n\n            if code in (204, 304):\n                # NEVER return a content body on 204/304 or downstream\n                # caches may become very confused.\n                return \"\"\n            elif c.render_style not in self.allowed_render_styles:\n                return str(code)\n            elif c.render_style in extensions.API_TYPES:\n                data = request.environ.get('extra_error_data', {'error': code})\n                message = request.GET.get('message', '')\n                if message:\n                    data['message'] = message\n                if request.environ.get(\"WANT_RAW_JSON\"):\n                    return scriptsafe_dumps(data)\n                return websafe_json(json.dumps(data))\n            elif takedown and code == 404:\n                link = Link._by_fullname(takedown)\n                return pages.TakedownPage(link).render()\n            elif code == 400:\n                return self.send400()\n            elif code == 403:\n                return self.send403()\n            elif code == 429:\n                return self.send429()\n            elif code == 500:\n                failien_url = make_failien_url()\n                sad_message = get_funny_translated_string(\"500_page\")\n                sad_message %= {'admin': random.choice(self.admins)}\n                sad_message = safemarkdown(sad_message)\n                return redditbroke % (failien_url, sad_message)\n            elif code == 503:\n                return self.send503()\n            elif c.site:\n                return self.send404()\n            else:\n                return \"page not found\"\n        except Exception as e:\n            return handle_awful_failure(\"ErrorController.GET_document: %r\" % e)\n\n    POST_document = GET_document\n    PUT_document = GET_document\n    PATCH_document = GET_document\n    DELETE_document = GET_document\n\n\ndef handle_awful_failure(fail_text):\n    \"\"\"\n    Makes sure that no errors generated in the error handler percolate\n    up to the user unless debug is enabled.\n    \"\"\"\n    if g.debug:\n        import sys\n        s = sys.exc_info()\n        # reraise the original error with the original stack trace\n        raise s[1], None, s[2]\n    try:\n        # log the traceback, and flag the \"path\" as the error location\n        import traceback\n        log.write_error_summary(fail_text)\n        for line in traceback.format_exc().splitlines():\n            g.log.error(line)\n        return redditbroke % (make_failien_url(), websafe(fail_text))\n    except:\n        # we are doomed.  Admit defeat\n        return \"This is an error that should never occur.  You win.\"\n"
  },
  {
    "path": "r2/r2/controllers/front.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom pylons.i18n import _, ungettext\nfrom r2.controllers.reddit_base import (\n    base_listing,\n    disable_subreddit_css,\n    paginated_listing,\n    RedditController,\n    require_https,\n)\nfrom r2 import config\nfrom r2.models import *\nfrom r2.models.recommend import ExploreSettings\nfrom r2.config import feature\nfrom r2.config.extensions import is_api, API_TYPES, RSS_TYPES\nfrom r2.lib import hooks, recommender, embeds, pages\nfrom r2.lib.pages import *\nfrom r2.lib.pages.things import hot_links_by_url_listing\nfrom r2.lib.pages import trafficpages\nfrom r2.lib.menus import *\nfrom r2.lib.csrf import csrf_exempt\nfrom r2.lib.utils import to36, sanitize_url, title_to_url\nfrom r2.lib.utils import query_string, UrlParser, url_links_builder\nfrom r2.lib.template_helpers import get_domain\nfrom r2.lib.filters import unsafe, _force_unicode, _force_utf8\nfrom r2.lib.emailer import Email, generate_notification_email_unsubscribe_token\nfrom r2.lib.db.operators import desc\nfrom r2.lib.db import queries\nfrom r2.lib.db.tdb_cassandra import MultiColumnQuery\nfrom r2.lib.strings import strings\nfrom r2.lib.validator import *\nfrom r2.lib import jsontemplates\nimport r2.lib.db.thing as thing\nfrom r2.lib.errors import errors, ForbiddenError\nfrom listingcontroller import ListingController\nfrom oauth2 import require_oauth2_scope\nfrom api_docs import api_doc, api_section\n\nfrom pylons import request\nfrom pylons import tmpl_context as c\nfrom pylons import app_globals as g\n\nfrom r2.models.token import EmailVerificationToken\nfrom r2.controllers.ipn import generate_blob, validate_blob, GoldException\n\nfrom operator import attrgetter\nimport string\nimport random as rand\nimport re\nfrom urllib import quote_plus\n\nclass FrontController(RedditController):\n\n    allow_stylesheets = True\n\n    @validate(link=VLink('link_id'))\n    def GET_link_id_redirect(self, link):\n        if not link:\n            abort(404)\n        elif not link.subreddit_slow.can_view(c.user):\n            # don't disclose the subreddit/title of a post via the redirect url\n            abort(403)\n        else:\n            redirect_url = link.make_permalink_slow(force_domain=True)\n\n        query_params = dict(request.GET)\n        if query_params:\n            url = UrlParser(redirect_url)\n            url.update_query(**query_params)\n            redirect_url = url.unparse()\n\n        return self.redirect(redirect_url, code=301)\n\n    @validate(article=VLink('article'),\n              comment=VCommentID('comment'))\n    def GET_oldinfo(self, article, type, dest, rest=None, comment=''):\n        \"\"\"Legacy: supporting permalink pages from '06,\n           and non-search-engine-friendly links\"\"\"\n        if not (dest in ('comments','details')):\n                dest = 'comments'\n        if type == 'ancient':\n            #this could go in config, but it should never change\n            max_link_id = 10000000\n            new_id = max_link_id - int(article._id)\n            return self.redirect('/info/' + to36(new_id) + '/' + rest)\n        if type == 'old':\n            if not article.subreddit_slow.can_view(c.user):\n                self.abort403()\n\n            new_url = \"/%s/%s/%s\" % \\\n                      (dest, article._id36,\n                       quote_plus(title_to_url(article.title).encode('utf-8')))\n            if not c.default_sr:\n                new_url = \"/r/%s%s\" % (c.site.name, new_url)\n            if comment:\n                new_url = new_url + \"/%s\" % comment._id36\n            if c.extension:\n                new_url = new_url + \"/.%s\" % c.extension\n\n            new_url = new_url + query_string(request.GET)\n\n            # redirect should be smarter and handle extensions, etc.\n            return self.redirect(new_url, code=301)\n\n    @require_oauth2_scope(\"read\")\n    @api_doc(api_section.listings, uses_site=True)\n    def GET_random(self):\n        \"\"\"The Serendipity button\"\"\"\n        sort = rand.choice(('new','hot'))\n        q = c.site.get_links(sort, 'all')\n        if isinstance(q, thing.Query):\n            q._limit = g.num_serendipity\n            names = [link._fullname for link in q]\n        else:\n            names = list(q)[:g.num_serendipity]\n\n        rand.shuffle(names)\n\n        def keep_fn(item):\n            return (\n                item.fresh and\n                item.keep_item(item) and\n                item.subreddit.discoverable\n            )\n\n        builder = IDBuilder(names, skip=True, keep_fn=keep_fn, num=1)\n        links, first, last, before, after = builder.get_items()\n\n        if links:\n            redirect_url = links[0].make_permalink_slow(force_domain=True)\n            return self.redirect(redirect_url)\n        else:\n            return self.redirect(add_sr('/'))\n\n    @disable_subreddit_css()\n    @validate(\n        VAdmin(),\n        thing=VByName('article'),\n        oldid36=nop('article'),\n        after=nop('after'),\n        before=nop('before'),\n        count=VCount('count'),\n        listing_only=VBoolean('listing_only'),\n    )\n    def GET_details(self, thing, oldid36, after, before, count, listing_only):\n        \"\"\"The (now deprecated) details page.  Content on this page\n        has been subsubmed by the presence of the LinkInfoBar on the\n        rightbox, so it is only useful for Admin-only wizardry.\"\"\"\n        if not thing:\n            try:\n                link = Link._byID36(oldid36)\n                return self.redirect('/details/' + link._fullname)\n            except (NotFound, ValueError):\n                abort(404)\n\n        kw = {\n            'count': count,\n            'listing_only': listing_only,\n        }\n        if before:\n            kw['after'] = before\n            kw['reverse'] = True\n        else:\n            kw['after'] = after\n            kw['reverse'] = False\n        c.referrer_policy = \"always\"\n        page = DetailsPage(thing=thing, expand_children=False, **kw)\n        if listing_only:\n            return page.details.listing.listing().render()\n        return page.render()\n\n    @validate(VUser())\n    def GET_explore(self):\n        settings = ExploreSettings.for_user(c.user)\n        recs = recommender.get_recommended_content_for_user(c.user,\n                                                            settings,\n                                                            record_views=True)\n        content = ExploreItemListing(recs, settings)\n        return BoringPage(_(\"explore\"),\n                          show_sidebar=True,\n                          show_chooser=True,\n                          page_classes=['explore-page'],\n                          content=content).render()\n\n    @validate(article=VLink('article'))\n    def GET_shirt(self, article):\n        if not can_view_link_comments(article):\n            abort(403, 'forbidden')\n        return self.abort404()\n\n    @require_oauth2_scope(\"read\")\n    @validate(article=VLink('article',\n                  docs={\"article\": \"ID36 of a link\"}),\n              comment=VCommentID('comment',\n                  docs={\"comment\": \"(optional) ID36 of a comment\"}),\n              context=VInt('context', min=0, max=8),\n              sort=VOneOf('sort', CommentSortMenu._options),\n              limit=VInt('limit',\n                  docs={\"limit\": \"(optional) an integer\"}),\n              depth=VInt('depth',\n                  docs={\"depth\": \"(optional) an integer\"}),\n              showedits=VBoolean(\"showedits\", default=True),\n              showmore=VBoolean(\"showmore\", default=True),\n              sr_detail=VBoolean(\n                  \"sr_detail\", docs={\"sr_detail\": \"(optional) expand subreddits\"}),\n              )\n    @api_doc(api_section.listings,\n             uri='/comments/{article}',\n             uses_site=True,\n             supports_rss=True)\n    def GET_comments(\n        self, article, comment, context, sort, limit, depth,\n            showedits=True, showmore=True, sr_detail=False):\n        \"\"\"Get the comment tree for a given Link `article`.\n\n        If supplied, `comment` is the ID36 of a comment in the comment tree for\n        `article`. This comment will be the (highlighted) focal point of the\n        returned view and `context` will be the number of parents shown.\n\n        `depth` is the maximum depth of subtrees in the thread.\n\n        `limit` is the maximum number of comments to return.\n\n        See also: [/api/morechildren](#GET_api_morechildren) and\n        [/api/comment](#POST_api_comment).\n\n        \"\"\"\n        if not sort:\n            sort = c.user.pref_default_comment_sort\n\n            # hot sort no longer exists but might still be set as a preference\n            if sort == \"hot\":\n                sort = \"confidence\"\n\n        if comment and comment.link_id != article._id:\n            return self.abort404()\n\n        sr = Subreddit._byID(article.sr_id, True)\n\n        if sr.name == g.takedown_sr:\n            request.environ['REDDIT_TAKEDOWN'] = article._fullname\n            return self.abort404()\n\n        if not c.default_sr and c.site._id != sr._id:\n            return self.abort404()\n\n        if not can_view_link_comments(article):\n            abort(403, 'forbidden')\n\n        # check over 18\n        if (\n            article.is_nsfw and\n            not c.over18 and\n            c.render_style == 'html' and\n            not request.parsed_agent.bot\n        ):\n            return self.intermediate_redirect(\"/over18\", sr_path=False)\n\n        canonical_link = article.make_canonical_link(sr)\n\n        # Determine if we should show the embed link for comments\n        c.can_embed = bool(comment) and article.is_embeddable\n\n        is_embed = embeds.prepare_embed_request()\n        if is_embed and comment:\n            embeds.set_up_comment_embed(sr, comment, showedits=showedits)\n\n        # Temporary hook until IAMA app \"OP filter\" is moved from partners\n        # Not to be open-sourced\n        page = hooks.get_hook(\"comments_page.override\").call_until_return(\n            controller=self,\n            article=article,\n            limit=limit,\n        )\n        if page:\n            return page\n\n        # If there is a focal comment, communicate down to\n        # comment_skeleton.html who that will be. Also, skip\n        # comment_visits check\n        previous_visits = None\n        if comment:\n            c.focal_comment = comment._id36\n        elif (c.user_is_loggedin and\n                (c.user.gold or sr.is_moderator(c.user)) and\n                c.user.pref_highlight_new_comments):\n            timer = g.stats.get_timer(\"gold.comment_visits\")\n            timer.start()\n            previous_visits = CommentVisitsByUser.get_and_update(\n                c.user, article, c.start_time)\n            timer.stop()\n\n        # check if we just came from the submit page\n        infotext = None\n        infotext_class = None\n        infotext_show_icon = False\n        if request.GET.get('already_submitted'):\n            submit_url = request.GET.get('submit_url') or article.url\n            submit_title = request.GET.get('submit_title') or \"\"\n            resubmit_url = Link.resubmit_link(submit_url, submit_title)\n            if c.user_is_loggedin and c.site.can_submit(c.user):\n                resubmit_url = add_sr(resubmit_url)\n            infotext = strings.already_submitted % resubmit_url\n        elif article.archived_slow:\n            infotext = strings.archived_post_message\n            infotext_class = 'archived-infobar'\n            infotext_show_icon = True\n        elif article.locked:\n            infotext = strings.locked_post_message\n            infotext_class = 'locked-infobar'\n            infotext_show_icon = True\n\n        if not c.user.pref_num_comments:\n            num = g.num_comments\n        elif c.user_is_loggedin and (c.user.gold or sr.is_moderator(c.user)):\n            num = min(c.user.pref_num_comments, g.max_comments_gold)\n        else:\n            num = min(c.user.pref_num_comments, g.max_comments)\n\n        kw = {}\n        # allow depth to be reset (I suspect I'll turn the VInt into a\n        # validator on my next pass of .compact)\n        if depth is not None and 0 < depth < MAX_RECURSION:\n            kw['max_depth'] = depth\n        elif c.render_style == \"compact\":\n            kw['max_depth'] = 5\n\n        kw[\"edits_visible\"] = showedits\n        kw[\"load_more\"] = kw[\"continue_this_thread\"] = showmore\n        kw[\"show_deleted\"] = embeds.is_embed()\n\n        displayPane = PaneStack()\n\n        # allow the user's total count preferences to be overwritten\n        # (think of .embed as the use case together with depth=1)\n\n        if limit and limit > 0:\n            num = limit\n\n        if c.user_is_loggedin and (c.user.gold or sr.is_moderator(c.user)):\n            if num > g.max_comments_gold:\n                displayPane.append(InfoBar(message =\n                                           strings.over_comment_limit_gold\n                                           % max(0, g.max_comments_gold)))\n                num = g.max_comments_gold\n        elif num > g.max_comments:\n            if limit:\n                displayPane.append(InfoBar(message =\n                                       strings.over_comment_limit\n                                       % dict(max=max(0, g.max_comments),\n                                              goldmax=max(0,\n                                                   g.max_comments_gold))))\n            num = g.max_comments\n\n        page_classes = ['comments-page']\n\n        # if permalink page, add that message first to the content\n        if comment:\n            displayPane.append(PermalinkMessage(article.make_permalink_slow()))\n            page_classes.append('comment-permalink-page')\n\n        displayPane.append(LinkCommentSep())\n\n        # insert reply box only for logged in user\n        if (not is_api() and\n                c.user_is_loggedin and\n                article.can_comment_slow(c.user)):\n            # no comment box for permalinks\n            display = not comment\n\n            # show geotargeting notice only if user is able to comment\n            if article.promoted:\n                geotargeted, city_target = promote.is_geotargeted_promo(article)\n                if geotargeted:\n                    displayPane.append(GeotargetNotice(city_target=city_target))\n\n            data_attrs = {'type': 'link', 'event-action': 'comment'}\n\n            displayPane.append(UserText(item=article, creating=True,\n                                        post_form='comment',\n                                        display=display,\n                                        cloneable=True,\n                                        data_attrs=data_attrs))\n\n        if previous_visits:\n            displayPane.append(CommentVisitsBox(previous_visits))\n\n        if c.site.allows_referrers:\n            c.referrer_policy = \"always\"\n\n        suggested_sort_active = False\n        if not c.user.pref_ignore_suggested_sort:\n            suggested_sort = article.sort_if_suggested()\n        else:\n            suggested_sort = None\n\n        # Special override: if the suggested sort is Q&A, and a responder of\n        # the thread is viewing it, we don't want to suggest to them to view\n        # the thread in Q&A mode (as it hides many unanswered questions)\n        if (suggested_sort == \"qa\" and\n                c.user_is_loggedin and\n                c.user._id in article.responder_ids):\n            suggested_sort = None\n\n        if article.contest_mode:\n            if c.user_is_loggedin and sr.is_moderator(c.user):\n                # Default to top for contest mode to make determining winners\n                # easier, but allow them to override it for moderation\n                # purposes.\n                if 'sort' not in request.params:\n                    sort = \"top\"\n            else:\n                sort = \"random\"\n        elif suggested_sort and 'sort' not in request.params:\n            sort = suggested_sort\n            suggested_sort_active = True\n\n        # finally add the comment listing\n        displayPane.append(CommentPane(article, CommentSortMenu.operator(sort),\n                                       comment, context, num, **kw))\n\n        subtitle_buttons = []\n        disable_comments = article.promoted and article.disable_comments\n\n        if (c.focal_comment or\n            context is not None or\n            disable_comments):\n            subtitle = None\n        elif article.num_comments == 0:\n            subtitle = _(\"no comments (yet)\")\n        elif article.num_comments <= num:\n            subtitle = _(\"all %d comments\") % article.num_comments\n        else:\n            subtitle = _(\"top %d comments\") % num\n\n            if g.max_comments > num:\n                self._add_show_comments_link(subtitle_buttons, article, num,\n                                             g.max_comments, gold=False)\n\n            if (c.user_is_loggedin and\n                    (c.user.gold or sr.is_moderator(c.user)) and\n                    article.num_comments > g.max_comments):\n                self._add_show_comments_link(subtitle_buttons, article, num,\n                                             g.max_comments_gold, gold=True)\n\n        sort_menu = CommentSortMenu(\n            default=sort,\n            css_class='suggested' if suggested_sort_active else '',\n            suggested_sort=suggested_sort,\n        )\n\n        link_settings = LinkCommentsSettings(\n            article,\n            sort=sort,\n            suggested_sort=suggested_sort,\n        )\n\n        # Check for click urls on promoted links\n        click_url = None\n        campaign_fullname = None\n        if article.promoted and not article.is_self:\n            campaign_fullname = request.GET.get(\"campaign\", None)\n            click_url = request.GET.get(\"click_url\", None)\n            click_hash = request.GET.get(\"click_hash\", \"\")\n\n            if (click_url and not promote.is_valid_click_url(\n                    link=article,\n                    click_url=click_url,\n                    click_hash=click_hash)):\n                click_url = None\n\n        # event target for screenviews\n        if comment:\n            event_target = {\n                'target_type': 'comment',\n                'target_fullname': comment._fullname,\n                'target_id': comment._id,\n            }\n        elif article.is_self:\n            event_target = {\n                'target_type': 'self',\n                'target_fullname': article._fullname,\n                'target_id': article._id,\n                'target_sort': sort,\n            }\n        else:\n            event_target = {\n                'target_type': 'link',\n                'target_fullname': article._fullname,\n                'target_id': article._id,\n                'target_url': article.url,\n                'target_url_domain': article.link_domain(),\n                'target_sort': sort,\n            }\n        extra_js_config = {'event_target': event_target}\n\n        res = LinkInfoPage(\n            link=article,\n            comment=comment,\n            disable_comments=disable_comments,\n            content=displayPane,\n            page_classes=page_classes,\n            subtitle=subtitle,\n            subtitle_buttons=subtitle_buttons,\n            nav_menus=[sort_menu, link_settings],\n            infotext=infotext,\n            infotext_class=infotext_class,\n            infotext_show_icon=infotext_show_icon,\n            sr_detail=sr_detail,\n            campaign_fullname=campaign_fullname,\n            click_url=click_url,\n            canonical_link=canonical_link,\n            extra_js_config=extra_js_config,\n        )\n\n        return res.render()\n\n    def _add_show_comments_link(self, array, article, num, max_comm, gold=False):\n        if num == max_comm:\n            return\n        elif article.num_comments <= max_comm:\n            link_text = _(\"show all %d\") % article.num_comments\n        else:\n            link_text = _(\"show %d\") % max_comm\n\n        limit_param = \"?limit=%d\" % max_comm\n\n        if gold:\n            link_class = \"gold\"\n        else:\n            link_class = \"\"\n\n        more_link = article.make_permalink_slow() + limit_param\n        array.append( (link_text, more_link, link_class) )\n\n    @validate(VUser(),\n              name=nop('name'))\n    def GET_newreddit(self, name):\n        \"\"\"Create a subreddit form\"\"\"\n        VNotInTimeout().run(action_name=\"pageview\", details_text=\"newreddit\")\n        title = _('create a subreddit')\n        captcha = Captcha() if c.user.needs_captcha() else None\n        content = CreateSubreddit(name=name or '', captcha=captcha)\n        res = FormPage(_(\"create a subreddit\"),\n                       content=content,\n                       captcha=captcha,\n                       ).render()\n        return res\n\n    @require_oauth2_scope(\"modconfig\")\n    @api_doc(api_section.moderation, uses_site=True)\n    def GET_stylesheet(self):\n        \"\"\"Redirect to the subreddit's stylesheet if one exists.\n\n        See also: [/api/subreddit_stylesheet](#POST_api_subreddit_stylesheet).\n\n        \"\"\"\n        # de-stale the subreddit object so we don't poison downstream caches\n        if not isinstance(c.site, FakeSubreddit):\n            c.site = Subreddit._byID(c.site._id, data=True, stale=False)\n\n        url = Reddit.get_subreddit_stylesheet_url(c.site)\n        if url:\n            return self.redirect(url)\n        else:\n            self.abort404()\n\n    def GET_share_close(self):\n        \"\"\"Render a page that closes itself.\n\n        Intended for use as a redirect target for facebook sharing.\n        \"\"\"\n        return ShareClose().render()\n\n    def _make_moderationlog(self, srs, num, after, reverse, count, mod=None, action=None):\n        query = Subreddit.get_modactions(srs, mod=mod, action=action)\n        builder = ModActionBuilder(\n            query, num=num, after=after, count=count, reverse=reverse,\n            wrap=default_thing_wrapper())\n        listing = ModActionListing(builder)\n        pane = listing.listing()\n        return pane\n\n    modname_splitter = re.compile('[ ,]+')\n\n    @require_oauth2_scope(\"modlog\")\n    @disable_subreddit_css()\n    @paginated_listing(max_page_size=500, backend='cassandra')\n    @validate(\n        mod=nop('mod', docs={\"mod\": \"(optional) a moderator filter\"}),\n        action=VOneOf('type', ModAction.actions),\n    )\n    @api_doc(api_section.moderation, uses_site=True,\n             uri=\"/about/log\", supports_rss=True)\n    def GET_moderationlog(self, num, after, reverse, count, mod, action):\n        \"\"\"Get a list of recent moderation actions.\n\n        Moderator actions taken within a subreddit are logged. This listing is\n        a view of that log with various filters to aid in analyzing the\n        information.\n\n        The optional `mod` parameter can be a comma-delimited list of moderator\n        names to restrict the results to, or the string `a` to restrict the\n        results to admin actions taken within the subreddit.\n\n        The `type` parameter is optional and if sent limits the log entries\n        returned to only those of the type specified.\n\n        \"\"\"\n        if not c.user_is_loggedin or not (c.user_is_admin or\n                                          c.site.is_moderator(c.user)):\n            return self.abort404()\n\n        VNotInTimeout().run(action_name=\"pageview\", details_text=\"modlog\")\n        if mod:\n            if mod == 'a':\n                modnames = g.admins\n            else:\n                modnames = self.modname_splitter.split(mod)\n            mod = []\n            for name in modnames:\n                try:\n                    mod.append(Account._by_name(name, allow_deleted=True))\n                except NotFound:\n                    continue\n            mod = mod or None\n\n        if isinstance(c.site, (MultiReddit, ModSR)):\n            srs = Subreddit._byID(c.site.sr_ids, return_dict=False)\n\n            # grab all moderators\n            mod_ids = set(Subreddit.get_all_mod_ids(srs))\n            mods = Account._byID(mod_ids, data=True)\n\n            pane = self._make_moderationlog(srs, num, after, reverse, count,\n                                            mod=mod, action=action)\n        elif isinstance(c.site, FakeSubreddit):\n            return self.abort404()\n        else:\n            mod_ids = c.site.moderators\n            mods = Account._byID(mod_ids, data=True)\n\n            pane = self._make_moderationlog(c.site, num, after, reverse, count,\n                                            mod=mod, action=action)\n\n        panes = PaneStack()\n        panes.append(pane)\n\n        action_buttons = [QueryButton(_('all'), None, query_param='type',\n                                      css_class='primary')]\n        for a in ModAction.actions:\n            button = QueryButton(ModAction._menu[a], a, query_param='type')\n            action_buttons.append(button)\n\n        mod_buttons = [QueryButton(_('all'), None, query_param='mod',\n                                   css_class='primary')]\n        for mod_id in mod_ids:\n            mod = mods[mod_id]\n            mod_buttons.append(QueryButton(mod.name, mod.name,\n                                           query_param='mod'))\n        # add a choice for the automoderator account if it's not a mod\n        if (g.automoderator_account and\n                all(mod.name != g.automoderator_account\n                    for mod in mods.values())):\n            automod_button = QueryButton(\n                g.automoderator_account,\n                g.automoderator_account,\n                query_param=\"mod\",\n            )\n            mod_buttons.append(automod_button)\n        mod_buttons.append(QueryButton(_('admins*'), 'a', query_param='mod'))\n        base_path = request.path\n        menus = [NavMenu(action_buttons, base_path=base_path,\n                         title=_('filter by action'), type='lightdrop', css_class='modaction-drop'),\n                NavMenu(mod_buttons, base_path=base_path,\n                        title=_('filter by moderator'), type='lightdrop')]\n        extension_handling = \"private\" if c.user.pref_private_feeds else False\n        return EditReddit(content=panes,\n                          nav_menus=menus,\n                          location=\"log\",\n                          extension_handling=extension_handling).render()\n\n    def _make_spamlisting(self, location, only, num, after, reverse, count):\n        include_links, include_comments = True, True\n        if only == 'links':\n            include_comments = False\n        elif only == 'comments':\n            include_links = False\n\n        if location == 'reports':\n            query = c.site.get_reported(include_links=include_links,\n                                        include_comments=include_comments)\n        elif location == 'spam':\n            query = c.site.get_spam(include_links=include_links,\n                                    include_comments=include_comments)\n        elif location == 'modqueue':\n            query = c.site.get_modqueue(include_links=include_links,\n                                        include_comments=include_comments)\n        elif location == 'unmoderated':\n            query = c.site.get_unmoderated()\n        elif location == 'edited':\n            query = c.site.get_edited(include_links=include_links,\n                                      include_comments=include_comments)\n        else:\n            raise ValueError\n\n        if isinstance(query, thing.Query):\n            builder_cls = QueryBuilder\n        elif isinstance (query, list):\n            builder_cls = QueryBuilder\n        else:\n            builder_cls = IDBuilder\n\n        def keep_fn(x):\n            # no need to bother mods with banned users, or deleted content\n            if x._deleted:\n                return False\n            if getattr(x,'author',None) == c.user and c.user._spam:\n                return False\n\n            if location == \"reports\":\n                return x.reported > 0 and not x._spam\n            elif location == \"spam\":\n                return x._spam\n            elif location == \"modqueue\":\n                if x.reported > 0 and not x._spam:\n                    return True # reported but not banned\n                if x.author._spam and x.subreddit.exclude_banned_modqueue:\n                    # banned user, don't show if subreddit pref excludes\n                    return False\n\n                verdict = getattr(x, \"verdict\", None)\n                if verdict is None:\n                    return True # anything without a verdict\n                if x._spam:\n                    ban_info = getattr(x, \"ban_info\", {})\n                    if ban_info.get(\"auto\", True):\n                        return True # spam, unless banned by a moderator\n                return False\n            elif location == \"unmoderated\":\n                # banned user, don't show if subreddit pref excludes\n                if x.author._spam and x.subreddit.exclude_banned_modqueue:\n                    return False\n                if x._spam:\n                    ban_info = getattr(x, \"ban_info\", {})\n                    if ban_info.get(\"auto\", True):\n                        return True\n                return not getattr(x, 'verdict', None)\n            elif location == \"edited\":\n                return bool(getattr(x, \"editted\", False))\n            else:\n                raise ValueError\n\n        builder = builder_cls(query,\n                              skip=True,\n                              num=num, after=after,\n                              keep_fn=keep_fn,\n                              count=count, reverse=reverse,\n                              wrap=ListingController.builder_wrapper,\n                              spam_listing=True)\n        listing = LinkListing(builder)\n        pane = listing.listing()\n\n        # Indicate that the comment tree wasn't built for comments\n        for i in pane.things:\n            if hasattr(i, 'body'):\n                i.child = None\n\n        return pane\n\n    def _edit_normal_reddit(self, location, created):\n        if (location == 'edit' and\n                c.user_is_loggedin and\n                (c.user_is_admin or\n                    c.site.is_moderator_with_perms(c.user, 'config'))):\n            pane = PaneStack()\n\n            if created == 'true':\n                infobar_message = strings.sr_created\n                pane.append(InfoBar(message=infobar_message))\n\n            c.allow_styles = True\n            c.site = Subreddit._byID(c.site._id, data=True, stale=False)\n            pane.append(CreateSubreddit(site=c.site))\n        elif (location == 'stylesheet'\n              and c.site.can_change_stylesheet(c.user)\n              and not g.css_killswitch):\n            stylesheet_contents = c.site.fetch_stylesheet_source()\n            c.allow_styles = True\n            pane = SubredditStylesheet(site=c.site,\n                                       stylesheet_contents=stylesheet_contents)\n        elif (location == 'stylesheet'\n              and c.site.can_view(c.user)\n              and not g.css_killswitch):\n            stylesheet = c.site.fetch_stylesheet_source()\n            pane = SubredditStylesheetSource(stylesheet_contents=stylesheet)\n        elif (location == 'traffic' and\n              (c.site.public_traffic or\n               (c.user_is_loggedin and\n                (c.site.is_moderator(c.user) or c.user.employee)))):\n            pane = trafficpages.SubredditTraffic()\n        elif (location == \"about\") and is_api():\n            return self.redirect(add_sr('about.json'), code=301)\n        else:\n            return self.abort404()\n\n        return EditReddit(content=pane,\n                          location=location,\n                          extension_handling=False).render()\n\n    @require_oauth2_scope(\"read\")\n    @base_listing\n    @disable_subreddit_css()\n    @validate(\n        VSrModerator(perms='posts'),\n        location=nop('location'),\n        only=VOneOf('only', ('links', 'comments')),\n        timeout=VNotInTimeout(),\n    )\n    @api_doc(\n        api_section.moderation,\n        uses_site=True,\n        uri='/about/{location}',\n        uri_variants=['/about/' + loc for loc in\n                      ('reports', 'spam', 'modqueue', 'unmoderated', 'edited')],\n    )\n    def GET_spamlisting(self, location, only, num, after, reverse, count,\n            timeout):\n        \"\"\"Return a listing of posts relevant to moderators.\n\n        * reports: Things that have been reported.\n        * spam: Things that have been marked as spam or otherwise removed.\n        * modqueue: Things requiring moderator review, such as reported things\n            and items caught by the spam filter.\n        * unmoderated: Things that have yet to be approved/removed by a mod.\n        * edited: Things that have been edited recently.\n\n        Requires the \"posts\" moderator permission for the subreddit.\n\n        \"\"\"\n        c.allow_styles = True\n        c.profilepage = True\n        panes = PaneStack()\n\n        # We clone and modify this when a user clicks 'reply' on a comment.\n        replyBox = UserText(item=None, display=False, cloneable=True,\n                            creating=True, post_form='comment')\n        panes.append(replyBox)\n\n        spamlisting = self._make_spamlisting(location, only, num, after,\n                                             reverse, count)\n        panes.append(spamlisting)\n\n        extension_handling = \"private\" if c.user.pref_private_feeds else False\n\n        if location in ('reports', 'spam', 'modqueue', 'edited'):\n            buttons = [\n                QueryButton(_('posts and comments'), None, query_param='only'),\n                QueryButton(_('posts'), 'links', query_param='only'),\n                QueryButton(_('comments'), 'comments', query_param='only'),\n            ]\n            menus = [NavMenu(buttons, base_path=request.path, title=_('show'),\n                             type='lightdrop')]\n        else:\n            menus = None\n        return EditReddit(content=panes,\n                          location=location,\n                          nav_menus=menus,\n                          extension_handling=extension_handling).render()\n\n    @base_listing\n    @disable_subreddit_css()\n    @validate(\n        VSrModerator(perms='flair'),\n        name=nop('name'),\n        timeout=VNotInTimeout(),\n    )\n    def GET_flairlisting(self, num, after, reverse, count, name, timeout):\n        user = None\n        if name:\n            try:\n                user = Account._by_name(name)\n            except NotFound:\n                c.errors.add(errors.USER_DOESNT_EXIST, field='name')\n\n        c.allow_styles = True\n        pane = FlairPane(num, after, reverse, name, user)\n        return EditReddit(content=pane, location='flair').render()\n\n    @require_oauth2_scope(\"modconfig\")\n    @disable_subreddit_css()\n    @validate(location=nop('location'),\n              created=VOneOf('created', ('true','false'),\n                             default='false'))\n    @api_doc(api_section.subreddits, uri=\"/r/{subreddit}/about/edit\")\n    def GET_editreddit(self, location, created):\n        \"\"\"Get the current settings of a subreddit.\n\n        In the API, this returns the current settings of the subreddit as used\n        by [/api/site_admin](#POST_api_site_admin).  On the HTML site, it will\n        display a form for editing the subreddit.\n\n        \"\"\"\n        c.profilepage = True\n        if isinstance(c.site, FakeSubreddit):\n            return self.abort404()\n        else:\n            VNotInTimeout().run(action_name=\"pageview\",\n                details_text=\"editreddit_%s\" % location, target=c.site)\n            return self._edit_normal_reddit(location, created)\n\n    @require_oauth2_scope(\"read\")\n    @api_doc(api_section.subreddits, uri='/r/{subreddit}/about')\n    def GET_about(self):\n        \"\"\"Return information about the subreddit.\n\n        Data includes the subscriber count, description, and header image.\"\"\"\n        if not is_api() or isinstance(c.site, FakeSubreddit):\n            return self.abort404()\n\n        # we do this here so that item.accounts_active_count is only present on\n        # this one endpoint, and not all the /subreddit listings etc. since\n        # looking up activity across multiple subreddits is more work.\n        accounts_active_count = None\n        activity = c.site.count_activity()\n        if activity:\n            accounts_active_count = activity.logged_in.count\n\n        item = Wrapped(c.site, accounts_active_count=accounts_active_count)\n        Subreddit.add_props(c.user, [item])\n        return Reddit(content=item).render()\n\n    @require_oauth2_scope(\"read\")\n    @api_doc(api_section.subreddits, uses_site=True)\n    def GET_sidebar(self):\n        \"\"\"Get the sidebar for the current subreddit\"\"\"\n        usertext = UserText(c.site, c.site.description)\n        return Reddit(content=usertext).render()\n\n    @require_oauth2_scope(\"read\")\n    @api_doc(api_section.subreddits, uri='/r/{subreddit}/about/rules')\n    def GET_rules(self):\n        \"\"\"Get the rules for the current subreddit\"\"\"\n        if not feature.is_enabled(\"subreddit_rules\", subreddit=c.site.name):\n            abort(404)\n        if isinstance(c.site, FakeSubreddit):\n            abort(404)\n\n        kind_labels = {\n            \"all\": _(\"Posts & Comments\"),\n            \"link\": _(\"Posts only\"),\n            \"comment\": _(\"Comments only\"),\n        }\n        title_string = _(\"Rules for r/%(subreddit)s\") % { \"subreddit\" : c.site.name }\n        content = Rules(\n            title=title_string,\n            kind_labels=kind_labels,\n        )\n        extra_js_config = {\"kind_labels\": kind_labels}\n        return ModToolsPage(\n            title=title_string,\n            content=content,\n            extra_js_config=extra_js_config,\n        ).render()\n\n    @require_oauth2_scope(\"read\")\n    @api_doc(api_section.subreddits, uses_site=True)\n    @validate(\n        num=VInt(\"num\",\n            min=1, max=Subreddit.MAX_STICKIES, num_default=1, coerce=True),\n    )\n    def GET_sticky(self, num):\n        \"\"\"Redirect to one of the posts stickied in the current subreddit\n\n        The \"num\" argument can be used to select a specific sticky, and will\n        default to 1 (the top sticky) if not specified.\n        Will 404 if there is not currently a sticky post in this subreddit.\n\n        \"\"\"\n        if not num or not c.site.sticky_fullnames:\n            abort(404)\n\n        try:\n            fullname = c.site.sticky_fullnames[num-1]\n        except IndexError:\n            abort(404)\n        sticky = Link._by_fullname(fullname, data=True)\n        self.redirect(sticky.make_permalink_slow())\n\n    def GET_awards(self):\n        \"\"\"The awards page.\"\"\"\n        return BoringPage(_(\"awards\"), content=UserAwards()).render()\n\n    @base_listing\n    @require_oauth2_scope(\"read\")\n    @validate(article=VLink('article'))\n    def GET_related(self, num, article, after, reverse, count):\n        \"\"\"Related page: removed, redirects to comments page.\"\"\"\n        if not can_view_link_comments(article):\n            abort(403, 'forbidden')\n\n        self.redirect(article.make_permalink_slow(), code=301)\n\n    @base_listing\n    @require_oauth2_scope(\"read\")\n    @validate(article=VLink('article'))\n    @api_doc(\n        api_section.listings,\n        uri=\"/duplicates/{article}\",\n        supports_rss=True,\n    )\n    def GET_duplicates(self, article, num, after, reverse, count):\n        \"\"\"Return a list of other submissions of the same URL\"\"\"\n        if not can_view_link_comments(article):\n            abort(403, 'forbidden')\n\n        builder = url_links_builder(article.url, exclude=article._fullname,\n                                    num=num, after=after, reverse=reverse,\n                                    count=count)\n        if after and not builder.valid_after(after):\n            g.stats.event_count(\"listing.invalid_after\", \"duplicates\")\n            self.abort403()\n        num_duplicates = len(builder.get_items()[0])\n        listing = LinkListing(builder).listing()\n\n        res = LinkInfoPage(link=article,\n                           comment=None,\n                           num_duplicates=num_duplicates,\n                           content=listing,\n                           page_classes=['other-discussions-page'],\n                           subtitle=_('other discussions')).render()\n        return res\n\n    @base_listing\n    @require_oauth2_scope(\"read\")\n    @validate(query=nop('q', docs={\"q\": \"a search query\"}),\n              sort=VMenu('sort', SubredditSearchSortMenu, remember=False))\n    @api_doc(api_section.subreddits, uri='/subreddits/search', supports_rss=True)\n    def GET_search_reddits(self, query, reverse, after, count, num, sort):\n        \"\"\"Search subreddits by title and description.\"\"\"\n\n        # trigger redirect to /over18\n        if request.GET.get('over18') == 'yes':\n            u = UrlParser(request.fullurl)\n            del u.query_dict['over18']\n            search_url = u.unparse()\n            return self.intermediate_redirect('/over18', sr_path=False,\n                                              fullpath=search_url)\n\n        # show NSFW to API and RSS users unless obey_over18=true\n        is_api_or_rss = (c.render_style in API_TYPES\n                         or c.render_style in RSS_TYPES)\n        if is_api_or_rss:\n            include_over18 = not c.obey_over18 or c.over18\n        elif feature.is_enabled('safe_search'):\n            include_over18 = c.over18\n        else:\n            include_over18 = True\n\n        if query:\n            q = g.search.SubredditSearchQuery(query, sort=sort, faceting={},\n                                              include_over18=include_over18)\n            content = self._search(q, num=num, reverse=reverse,\n                                   after=after, count=count,\n                                   skip_deleted_authors=False)\n        else:\n            content = None\n\n        # event target for screenviews (/subreddits/search)\n        event_target = {}\n        if after:\n            event_target['target_count'] = count\n            if reverse:\n                event_target['target_before'] = after._fullname\n            else:\n                event_target['target_after'] = after._fullname\n        extra_js_config = {'event_target': event_target}\n\n        res = SubredditsPage(content=content,\n                             prev_search=query,\n                             page_classes=['subreddits-page'],\n                             extra_js_config=extra_js_config,\n                             # update if we ever add sorts\n                             search_params={},\n                             title=_(\"search results\"),\n                             simple=True).render()\n        return res\n\n    search_help_page = \"/wiki/search\"\n    verify_langs_regex = re.compile(r\"\\A[a-z][a-z](,[a-z][a-z])*\\Z\")\n\n    @base_listing\n    @require_oauth2_scope(\"read\")\n    @validate(query=VLength('q', max_length=512),\n              sort=VMenu('sort', SearchSortMenu, remember=False),\n              recent=VMenu('t', TimeMenu, remember=False),\n              restrict_sr=VBoolean('restrict_sr', default=False),\n              include_facets=VBoolean('include_facets', default=False),\n              result_types=VResultTypes('type'),\n              syntax=VOneOf('syntax', options=g.search_syntaxes))\n    @api_doc(api_section.search, supports_rss=True, uses_site=True)\n    def GET_search(self, query, num, reverse, after, count, sort, recent,\n                   restrict_sr, include_facets, result_types, syntax, sr_detail):\n        \"\"\"Search links page.\"\"\"\n        if c.site.login_required and not c.user_is_loggedin:\n            raise UserRequiredException\n\n        # trigger redirect to /over18\n        if request.GET.get('over18') == 'yes':\n            u = UrlParser(request.fullurl)\n            del u.query_dict['over18']\n            search_url = u.unparse()\n            return self.intermediate_redirect('/over18', sr_path=False,\n                                              fullpath=search_url)\n\n        if query and '.' in query:\n            url = sanitize_url(query, require_scheme=True)\n            if url:\n                return self.redirect(\"/submit\" + query_string({'url':url}))\n\n        if not restrict_sr:\n            site = DefaultSR()\n        else:\n            site = c.site\n\n        has_query = query or not isinstance(site, (DefaultSR, AllSR))\n\n        if not syntax:\n            syntax = g.search.SearchQuery.default_syntax\n\n        # show NSFW to API and RSS users unless obey_over18=true\n        is_api_or_rss = (c.render_style in API_TYPES\n                         or c.render_style in RSS_TYPES)\n        if is_api_or_rss:\n            include_over18 = not c.obey_over18 or c.over18\n        elif feature.is_enabled('safe_search'):\n            include_over18 = c.over18\n        else:\n            include_over18 = True\n\n        # do not request facets--they are not popular with users and result in\n        # looking up unpopular subreddits (which is bad for site performance)\n        faceting = {}\n\n        # no subreddit results if fielded search or structured syntax\n        if syntax == 'cloudsearch' or (query and ':' in query):\n            result_types = result_types - {'sr'}\n\n        # combined results on first page only\n        if not after and not restrict_sr and result_types == {'link', 'sr'}:\n            # hardcoded to 3 subreddits (or fewer)\n            sr_num = min(3, int(num / 3))\n            num = num - sr_num\n        elif result_types == {'sr'}:\n            sr_num = num\n            num = 0\n        else:\n            sr_num = 0\n\n        content = None\n        subreddits = None\n        nav_menus = None\n        cleanup_message = None\n        converted_data = None\n        subreddit_facets = None\n        legacy_render_class = feature.is_enabled('legacy_search') or c.user.pref_legacy_search\n\n        if num > 0 and has_query:\n            nav_menus = [SearchSortMenu(default=sort), TimeMenu(default=recent)]\n            try:\n                q = g.search.SearchQuery(query, site, sort=sort,\n                                         faceting=faceting,\n                                         include_over18=include_over18,\n                                         recent=recent, syntax=syntax)\n                content = self._search(q, num=num, after=after, reverse=reverse,\n                                       count=count, sr_detail=sr_detail,\n                                       heading=_('posts'), nav_menus=nav_menus,\n                                       legacy_render_class=legacy_render_class)\n                converted_data = q.converted_data\n                subreddit_facets = content.subreddit_facets\n\n            except g.search.InvalidQuery:\n                g.stats.simple_event('cloudsearch.error.invalidquery')\n\n                # Clean the search of characters that might be causing the\n                # InvalidQuery exception. If the cleaned search boils down\n                # to an empty string, the search code is expected to bail\n                # out early with an empty result set.\n                cleaned = re.sub(\"[^\\w\\s]+\", \" \", query)\n                cleaned = cleaned.lower().strip()\n\n                q = g.search.SearchQuery(cleaned, site, sort=sort,\n                                         faceting=faceting,\n                                         include_over18=include_over18,\n                                         recent=recent)\n                content = self._search(q, num=num, after=after, reverse=reverse,\n                                       count=count, heading=_('posts'), nav_menus=nav_menus,\n                                       legacy_render_class=legacy_render_class)\n                converted_data = q.converted_data\n                subreddit_facets = content.subreddit_facets\n\n                if cleaned:\n                    cleanup_message = strings.invalid_search_query % {\n                                                        \"clean_query\": cleaned\n                                                                      }\n                    cleanup_message += \" \"\n                    cleanup_message += strings.search_help % {\n                                          \"search_help\": self.search_help_page\n                                                              }\n                else:\n                    cleanup_message = strings.completely_invalid_search_query\n\n        # extra search request for subreddit results\n        if sr_num > 0 and has_query:\n            sr_q = g.search.SubredditSearchQuery(query, sort='relevance',\n                                                 faceting={},\n                                                 include_over18=include_over18)\n            subreddits = self._search(sr_q, num=sr_num, reverse=reverse,\n                                      after=after, count=count, type='sr',\n                                      skip_deleted_authors=False, heading=_('subreddits'),\n                                      legacy_render_class=legacy_render_class)\n\n            # backfill with facets if no subreddit search results\n            if subreddit_facets and not subreddits.things:\n                names = [sr._fullname for sr, count in subreddit_facets]\n                builder = IDBuilder(names, num=sr_num)\n                listing = SearchListing(builder, nextprev=False)\n                subreddits = listing.listing(\n                    legacy_render_class=legacy_render_class)\n\n            # ensure response is not list for subreddit only result type\n            if is_api() and not content:\n                content = subreddits\n                subreddits = None\n\n        # event target for screenviews (/search)\n        event_target = {\n            'target_sort': sort,\n            'target_filter_time': recent,\n        }\n        if after:\n            event_target['target_count'] = count\n            if reverse:\n                event_target['target_before'] = after._fullname\n            else:\n                event_target['target_after'] = after._fullname\n        extra_js_config = {'event_target': event_target}\n\n        res = SearchPage(_('search results'), query,\n                         content=content,\n                         subreddits=subreddits,\n                         nav_menus=nav_menus,\n                         search_params=dict(sort=sort, t=recent),\n                         infotext=cleanup_message,\n                         simple=False, site=c.site,\n                         restrict_sr=restrict_sr,\n                         syntax=syntax,\n                         converted_data=converted_data,\n                         facets=subreddit_facets,\n                         sort=sort,\n                         recent=recent,\n                         extra_js_config=extra_js_config,\n                         ).render()\n\n        return res\n\n    def _search_builder_wrapper(self, q):\n        query = q.query\n        recent = str(q.recent) if q.recent else None\n        sort = q.sort\n        def wrapper_fn(thing):\n            w = Wrapped(thing)\n            w.prev_search = query\n            w.recent = recent\n            w.sort = sort\n\n            if isinstance(thing, Link):\n                w.render_class = SearchResultLink\n            elif isinstance(thing, Subreddit):\n                w.render_class = SearchResultSubreddit\n            return w\n        return wrapper_fn\n\n    def _legacy_search_builder_wrapper(self):\n        default_wrapper = default_thing_wrapper()\n        def wrapper_fn(thing):\n            w = default_wrapper(thing)\n            if isinstance(thing, Link):\n                w.render_class = LegacySearchResultLink\n            return w\n        return wrapper_fn\n\n    def _search(self, query_obj, num, after, reverse, count=0, type=None,\n                skip_deleted_authors=True, sr_detail=False,\n                heading=None, nav_menus=None, legacy_render_class=True):\n        \"\"\"Helper function for interfacing with search.  Basically a\n           thin wrapper for SearchBuilder.\"\"\"\n\n        if legacy_render_class:\n            builder_wrapper = self._legacy_search_builder_wrapper()\n        else:\n            builder_wrapper = self._search_builder_wrapper(query_obj)\n\n        builder = SearchBuilder(query_obj,\n                                after=after, num=num, reverse=reverse,\n                                count=count,\n                                wrap=builder_wrapper,\n                                skip_deleted_authors=skip_deleted_authors,\n                                sr_detail=sr_detail)\n        if after and not builder.valid_after(after):\n            g.stats.event_count(\"listing.invalid_after\", \"search\")\n            self.abort403()\n\n        params = request.GET.copy()\n        if type:\n            params['type'] = type\n\n        listing = SearchListing(builder, show_nums=True, params=params,\n                                heading=heading, nav_menus=nav_menus)\n\n        try:\n            res = listing.listing(legacy_render_class)\n        except g.search.SearchException as e:\n            return self.search_fail(e)\n\n        return res\n\n    @validate(VAdmin(),\n              comment=VCommentByID('comment_id'))\n    def GET_comment_by_id(self, comment):\n        href = comment.make_permalink_slow(context=5, anchor=True)\n        return self.redirect(href)\n\n    @validate(url=VRequired('url', None),\n              title=VRequired('title', None),\n              text=VRequired('text', None),\n              selftext=VRequired('selftext', None))\n    def GET_submit(self, url, title, text, selftext):\n        \"\"\"Submit form.\"\"\"\n        resubmit = request.GET.get('resubmit')\n        url = sanitize_url(url)\n\n        if url and not resubmit:\n            # check to see if the url has already been submitted\n\n            def keep_fn(item):\n                # skip promoted links\n                would_keep = item.keep_item(item)\n                return would_keep and getattr(item, \"promoted\", None) is None\n\n            listing = hot_links_by_url_listing(\n                url, sr=c.site, num=100, skip=True, keep_fn=keep_fn)\n            links = listing.things\n\n            if links and len(links) == 1:\n                # redirect the user to the existing link's comments\n                existing_submission_url = links[0].already_submitted_link(\n                    url, title)\n                return self.redirect(existing_submission_url)\n            elif links:\n                # show the user a listing of all the other links with this url\n                # an infotext to resubmit it\n                resubmit_url = Link.resubmit_link(url, title)\n                sr_resubmit_url = add_sr(resubmit_url)\n                infotext = strings.multiple_submitted % sr_resubmit_url\n                res = BoringPage(\n                    _(\"seen it\"), content=listing, infotext=infotext).render()\n                return res\n\n        if not c.user_is_loggedin:\n            raise UserRequiredException\n\n        if not (c.default_sr or c.site.can_submit(c.user)):\n            abort(403, \"forbidden\")\n\n        target = c.site if not isinstance(c.site, FakeSubreddit) else None\n        VNotInTimeout().run(action_name=\"pageview\", details_text=\"submit\",\n            target=target)\n\n        captcha = Captcha() if c.user.needs_captcha() else None\n\n        extra_subreddits = []\n        if isinstance(c.site, MultiReddit):\n            extra_subreddits.append((\n                _('%s subreddits') % c.site.name,\n                c.site.srs\n            ))\n\n        newlink = NewLink(\n            url=url or '',\n            title=title or '',\n            text=text or '',\n            selftext=selftext or '',\n            captcha=captcha,\n            resubmit=resubmit,\n            default_sr=c.site if not c.default_sr else None,\n            extra_subreddits=extra_subreddits,\n            show_link=c.default_sr or c.site.can_submit_link(c.user),\n            show_self=((c.default_sr or c.site.can_submit_text(c.user))\n                      and not request.GET.get('no_self')),\n        )\n\n        return FormPage(_(\"submit\"),\n                        show_sidebar=True,\n                        page_classes=['submit-page'],\n                        content=newlink).render()\n\n    def GET_catchall(self):\n        return self.abort404()\n\n    @require_oauth2_scope(\"modtraffic\")\n    @validate(VSponsor('link'),\n              link=VLink('link'),\n              campaign=VPromoCampaign('campaign'),\n              before=VDate('before', format='%Y%m%d%H'),\n              after=VDate('after', format='%Y%m%d%H'))\n    def GET_traffic(self, link, campaign, before, after):\n        if link and campaign and link._id != campaign.link_id:\n            return self.abort404()\n\n        if c.render_style == 'csv':\n            return trafficpages.PromotedLinkTraffic.as_csv(campaign or link)\n\n        content = trafficpages.PromotedLinkTraffic(link, campaign, before,\n                                                   after)\n        return LinkInfoPage(link=link,\n                            page_classes=[\"promoted-traffic\"],\n                            show_sidebar=False, comment=None,\n                            show_promote_button=True, content=content).render()\n\n    @validate(VEmployee())\n    def GET_site_traffic(self):\n        return trafficpages.SitewideTrafficPage().render()\n\n    @validate(VEmployee())\n    def GET_lang_traffic(self, langcode):\n        return trafficpages.LanguageTrafficPage(langcode).render()\n\n    @validate(VEmployee())\n    def GET_advert_traffic(self, code):\n        return trafficpages.AdvertTrafficPage(code).render()\n\n    @validate(VEmployee())\n    def GET_subreddit_traffic_report(self):\n        content = trafficpages.SubredditTrafficReport()\n\n        if c.render_style == 'csv':\n            return content.as_csv()\n        return trafficpages.TrafficPage(content=content).render()\n\n    @validate(VUser())\n    def GET_account_activity(self):\n        return AccountActivityPage().render()\n\n    def GET_contact_us(self):\n        return BoringPage(_(\"contact us\"), show_sidebar=False,\n                          content=ContactUs(), page_classes=[\"contact-us-page\"]\n                          ).render()\n\n    @validate(vendor=VOneOf(\"v\", (\"claimed-gold\", \"claimed-creddits\",\n                                  \"spent-creddits\", \"paypal\", \"coinbase\",\n                                  \"stripe\"),\n                            default=\"claimed-gold\"))\n    def GET_goldthanks(self, vendor):\n        vendor_url = None\n        lounge_md = None\n\n        if vendor == \"claimed-gold\":\n            claim_msg = _(\"Claimed! Enjoy your reddit gold membership.\")\n            if g.lounge_reddit:\n                lounge_md = strings.lounge_msg\n        elif vendor == \"claimed-creddits\":\n            claim_msg = _(\"Your gold creddits have been claimed! Now go to \"\n                          \"someone's userpage and give them a present!\")\n        elif vendor == \"spent-creddits\":\n            claim_msg = _(\"Thanks for buying reddit gold! Your transaction \"\n                          \"has been completed.\")\n        elif vendor == \"paypal\":\n            claim_msg = _(\"Thanks for buying reddit gold! Your transaction \"\n                          \"has been completed and emailed to you. You can \"\n                          \"check the details by signing into your account \"\n                          \"at:\")\n            vendor_url = \"https://www.paypal.com/us\"\n        elif vendor in {\"coinbase\", \"stripe\"}:  # Pending vendors\n            claim_msg = _(\"Thanks for buying reddit gold! Your transaction is \"\n                          \"being processed. If you have any questions please \"\n                          \"email us at %(gold_email)s\")\n            claim_msg = claim_msg % {'gold_email': g.goldsupport_email}\n        else:\n            abort(404)\n\n        return BoringPage(_(\"thanks\"), show_sidebar=False,\n                          content=GoldThanks(claim_msg=claim_msg,\n                                             vendor_url=vendor_url,\n                                             lounge_md=lounge_md),\n                          page_classes=[\"gold-page-ga-tracking\"]\n                         ).render()\n\n    @validate(VUser(),\n              token=VOneTimeToken(AwardClaimToken, \"code\"))\n    def GET_confirm_award_claim(self, token):\n        if not token:\n            abort(403)\n\n        award = Award._by_fullname(token.awardfullname)\n        trophy = FakeTrophy(c.user, award, token.description, token.url)\n        content = ConfirmAwardClaim(trophy=trophy, user=c.user.name,\n                                    token=token)\n        return BoringPage(_(\"claim this award?\"), content=content).render()\n\n    @validate(VUser(),\n              VModhash(),\n              token=VOneTimeToken(AwardClaimToken, \"code\"))\n    def POST_claim_award(self, token):\n        if not token:\n            abort(403)\n\n        token.consume()\n\n        award = Award._by_fullname(token.awardfullname)\n        trophy, preexisting = Trophy.claim(c.user, token.uid, award,\n                                           token.description, token.url)\n        redirect = '/awards/received?trophy=' + trophy._id36\n        if preexisting:\n            redirect += '&duplicate=true'\n        self.redirect(redirect)\n\n    @validate(trophy=VTrophy('trophy'),\n              preexisting=VBoolean('duplicate'))\n    def GET_received_award(self, trophy, preexisting):\n        content = AwardReceived(trophy=trophy, preexisting=preexisting)\n        return BoringPage(_(\"award claim\"), content=content).render()\n\n    def GET_gilding(self):\n        return BoringPage(\n            _(\"gilding\"),\n            show_sidebar=False,\n            content=Gilding(),\n            page_classes=[\"gold-page\", \"gilding\"],\n        ).render()\n\n    @csrf_exempt\n    @validate(dest=VDestination(default='/'))\n    def _modify_hsts_grant(self, dest):\n        \"\"\"Endpoint subdomains can redirect through to update HSTS grants.\"\"\"\n        # TODO: remove this once it stops getting hit\n        from r2.lib.base import abort\n        require_https()\n        if request.host != g.domain:\n            abort(ForbiddenError(errors.WRONG_DOMAIN))\n\n        # We can't send the user back to http: if they're forcing HTTPS\n        dest_parsed = UrlParser(dest)\n        dest_parsed.scheme = \"https\"\n        dest = dest_parsed.unparse()\n\n        return self.redirect(dest, code=307)\n\n    POST_modify_hsts_grant = _modify_hsts_grant\n    GET_modify_hsts_grant = _modify_hsts_grant\n    DELETE_modify_hsts_grant = _modify_hsts_grant\n    PUT_modify_hsts_grant = _modify_hsts_grant\n\n\nclass FormsController(RedditController):\n\n    def GET_password(self):\n        \"\"\"The 'what is my password' page\"\"\"\n        return BoringPage(_(\"password\"), content=Password()).render()\n\n    @validate(VUser(),\n              dest=VDestination(),\n              reason=nop('reason'))\n    def GET_verify(self, dest, reason):\n        if c.user.email_verified:\n            content = InfoBar(message=strings.email_verified)\n            if dest:\n                return self.redirect(dest)\n        else:\n            if reason == \"submit\":\n                infomsg = strings.verify_email_submit\n            else:\n                infomsg = strings.verify_email\n\n            content = PaneStack(\n                [InfoBar(message=infomsg),\n                 PrefUpdate(email=True, verify=True,\n                            password=False, dest=dest)])\n        return BoringPage(_(\"verify email\"), content=content).render()\n\n    @validate(VUser(),\n              token=VOneTimeToken(EmailVerificationToken, \"key\"),\n              dest=VDestination(default=\"/prefs/update?verified=true\"))\n    def GET_verify_email(self, token, dest):\n        fail_msg = None\n        if token and token.user_id != c.user._fullname:\n            fail_msg = strings.email_verify_wrong_user\n        elif c.user.email_verified:\n            # they've already verified.\n            if token:\n                # consume and ignore this token (if not already consumed).\n                token.consume()\n            return self.redirect(dest)\n        elif token and token.valid_for_user(c.user):\n            # successful verification!\n            token.consume()\n            c.user.email_verified = True\n            c.user._commit()\n            Award.give_if_needed(\"verified_email\", c.user)\n            return self.redirect(dest)\n\n        # failure. let 'em know.\n        content = PaneStack(\n            [InfoBar(message=fail_msg or strings.email_verify_failed),\n             PrefUpdate(email=True,\n                        verify=True,\n                        password=False)])\n        return BoringPage(_(\"verify email\"), content=content).render()\n\n    @validate(token=VOneTimeToken(PasswordResetToken, \"key\"),\n              key=nop(\"key\"))\n    def GET_resetpassword(self, token, key):\n        \"\"\"page hit once a user has been sent a password reset email\n        to verify their identity before allowing them to update their\n        password.\"\"\"\n\n        done = False\n        if not key and request.referer:\n            referer_path = request.referer.split(g.domain)[-1]\n            done = referer_path.startswith(request.fullpath)\n        elif not token:\n            return self.redirect(\"/password?expired=true\")\n\n        token_user = Account._by_fullname(token.user_id, data=True)\n\n        return BoringPage(\n            _(\"reset password\"),\n            content=ResetPassword(\n                key=key,\n                done=done,\n                username=token_user.name,\n            )\n        ).render()\n\n    @validate(\n        user_id36=nop('user'),\n        provided_mac=nop('key')\n    )\n    def GET_unsubscribe_emails(self, user_id36, provided_mac):\n        from r2.lib.utils import constant_time_compare\n\n        expected_mac = generate_notification_email_unsubscribe_token(user_id36)\n        if not constant_time_compare(provided_mac or '', expected_mac):\n            error_page = pages.RedditError(\n                title=_('incorrect message token'),\n                message='',\n            )\n            request.environ[\"usable_error_content\"] = error_page.render()\n            self.abort404()\n        user = Account._byID36(user_id36, data=True)\n        user.pref_email_messages = False\n        user._commit()\n\n        return BoringPage(_('emails unsubscribed'),\n                          content=MessageNotificationEmailsUnsubscribe()).render()\n\n    @disable_subreddit_css()\n    @validate(VUser(),\n              location=nop(\"location\"),\n              verified=VBoolean(\"verified\"))\n    def GET_prefs(self, location='', verified=False):\n        \"\"\"Preference page\"\"\"\n        content = None\n        infotext = None\n        if not location or location == 'options':\n            content = PrefOptions(\n                done=request.GET.get('done'),\n                error_style_override=request.GET.get('error_style_override'),\n                generic_error=request.GET.get('generic_error'),\n            )\n        elif location == 'update':\n            if verified:\n                infotext = strings.email_verified\n            content = PrefUpdate()\n        elif location == 'apps':\n            content = PrefApps(my_apps=OAuth2Client._by_user_grouped(c.user),\n                               developed_apps=OAuth2Client._by_developer(c.user))\n        elif location == 'feeds' and c.user.pref_private_feeds:\n            content = PrefFeeds()\n        elif location == 'deactivate':\n            content = PrefDeactivate()\n        elif location == 'delete':\n            return self.redirect('/prefs/deactivate', code=301)\n        elif location == 'security':\n            if c.user.name not in g.admins:\n                return self.redirect('/prefs/')\n            content = PrefSecurity()\n        else:\n            return self.abort404()\n\n        return PrefsPage(content=content, infotext=infotext).render()\n\n    @validate(dest=VDestination())\n    def GET_login(self, dest):\n        \"\"\"The /login form.  No link to this page exists any more on\n        the site (all actions invoking it now go through the login\n        cover).  However, this page is still used for logging the user\n        in during submission or voting from the bookmarklets.\"\"\"\n\n        if (c.user_is_loggedin and\n            not request.environ.get('extension') == 'embed'):\n            return self.redirect(dest)\n        return LoginPage(dest=dest).render()\n\n\n    @validate(dest=VDestination())\n    def GET_register(self, dest):\n        if (c.user_is_loggedin and\n            not request.environ.get('extension') == 'embed'):\n            return self.redirect(dest)\n        return RegisterPage(dest=dest).render()\n\n    @validate(VUser(),\n              VModhash(),\n              dest=VDestination())\n    def GET_logout(self, dest):\n        return self.redirect(dest)\n\n    @validate(VUser(),\n              VModhash(),\n              dest=VDestination())\n    def POST_logout(self, dest):\n        \"\"\"wipe login cookie and redirect to referer.\"\"\"\n        self.logout()\n        self.redirect(dest)\n\n    @validate(VUser(),\n              dest=VDestination())\n    def GET_adminon(self, dest):\n        \"\"\"Enable admin interaction with site\"\"\"\n        #check like this because c.user_is_admin is still false\n        if not c.user.name in g.admins:\n            return self.abort404()\n\n        return InterstitialPage(\n            _(\"turn admin on\"),\n            content=AdminInterstitial(dest=dest)).render()\n\n    @validate(VAdmin(),\n              dest=VDestination())\n    def GET_adminoff(self, dest):\n        \"\"\"disable admin interaction with site.\"\"\"\n        if not c.user.name in g.admins:\n            return self.abort404()\n        self.disable_admin_mode(c.user)\n        return self.redirect(dest)\n\n    def _render_opt_in_out(self, msg_hash, leave):\n        \"\"\"Generates the form for an optin/optout page\"\"\"\n        email = Email.handler.get_recipient(msg_hash)\n        if not email:\n            return self.abort404()\n        sent = (has_opted_out(email) == leave)\n        return BoringPage(_(\"opt out\") if leave else _(\"welcome back\"),\n                          content=OptOut(email=email, leave=leave,\n                                           sent=sent,\n                                           msg_hash=msg_hash)).render()\n\n    @validate(msg_hash=nop('x'))\n    def GET_optout(self, msg_hash):\n        \"\"\"handles /mail/optout to add an email to the optout mailing\n        list.  The actual email addition comes from the user posting\n        the subsequently rendered form and is handled in\n        ApiController.POST_optout.\"\"\"\n        return self._render_opt_in_out(msg_hash, True)\n\n    @validate(msg_hash=nop('x'))\n    def GET_optin(self, msg_hash):\n        \"\"\"handles /mail/optin to remove an email address from the\n        optout list. The actual email removal comes from the user\n        posting the subsequently rendered form and is handled in\n        ApiController.POST_optin.\"\"\"\n        return self._render_opt_in_out(msg_hash, False)\n\n    @validate(dest=VDestination(\"dest\"))\n    def GET_try_compact(self, dest):\n        c.render_style = \"compact\"\n        return TryCompact(dest=dest).render()\n\n    @validate(VUser(),\n              secret=VPrintable(\"secret\", 50))\n    def GET_claim(self, secret):\n        \"\"\"The page to claim reddit gold trophies\"\"\"\n        return BoringPage(_(\"thanks\"), content=Thanks(secret)).render()\n\n    @validate(VUser(),\n              passthrough=nop('passthrough'))\n    def GET_creditgild(self, passthrough):\n        \"\"\"Used only for setting up credit card payments for gilding.\"\"\"\n        try:\n            payment_blob = validate_blob(passthrough)\n        except GoldException:\n            self.abort404()\n\n        if c.user != payment_blob['buyer']:\n            self.abort404()\n\n        if not payment_blob['goldtype'] == 'gift':\n            self.abort404()\n\n        recipient = payment_blob['recipient']\n        thing = payment_blob.get('thing')\n        if not thing:\n            thing = payment_blob['comment']\n        if (not thing or\n            thing._deleted or\n            not thing.subreddit_slow.can_view(c.user)):\n            self.abort404()\n\n        if isinstance(thing, Comment):\n            summary = strings.gold_summary_gilding_page_comment\n        else:\n            summary = strings.gold_summary_gilding_page_link\n        summary = summary % {'recipient': recipient.name}\n        months = 1\n        price = g.gold_month_price * months\n\n        if isinstance(thing, Comment):\n            desc = thing.body\n        else:\n            desc = thing.markdown_link_slow()\n\n        content = CreditGild(\n            summary=summary,\n            price=price,\n            months=months,\n            stripe_key=g.secrets['stripe_public_key'],\n            passthrough=passthrough,\n            description=desc,\n            period=None,\n        )\n\n        return BoringPage(_(\"reddit gold\"),\n                          show_sidebar=False,\n                          content=content,\n                          page_classes=[\"gold-page-ga-tracking\"]\n                         ).render()\n\n    @validate(is_payment=VBoolean(\"is_payment\"),\n              goldtype=VOneOf(\"goldtype\",\n                              (\"autorenew\", \"onetime\", \"creddits\", \"gift\",\n                               \"code\")),\n              period=VOneOf(\"period\", (\"monthly\", \"yearly\")),\n              months=VInt(\"months\"),\n              num_creddits=VInt(\"num_creddits\"),\n              # variables below are just for gifts\n              signed=VBoolean(\"signed\", default=True),\n              recipient=VExistingUname(\"recipient\", default=None),\n              thing=VByName(\"thing\"),\n              giftmessage=VLength(\"giftmessage\", 10000),\n              email=ValidEmail(\"email\"),\n              edit=VBoolean(\"edit\", default=False),\n    )\n    def GET_gold(self, is_payment, goldtype, period, months, num_creddits,\n                 signed, recipient, giftmessage, thing, email, edit):\n        VNotInTimeout().run(action_name=\"pageview\", details_text=\"gold\",\n            target=thing)\n        if thing:\n            thing_sr = Subreddit._byID(thing.sr_id, data=True)\n            if (thing._deleted or\n                    thing._spam or\n                    not thing_sr.can_view(c.user) or\n                    not thing_sr.allow_gilding):\n                thing = None\n\n        start_over = False\n\n        if edit:\n            start_over = True\n\n        if not c.user_is_loggedin:\n            if goldtype != \"code\":\n                start_over = True\n            elif months is None or months < 1:\n                start_over = True\n            elif not email:\n                start_over = True\n        elif goldtype == \"autorenew\":\n            if period is None:\n                start_over = True\n            elif c.user.has_gold_subscription:\n                return self.redirect(\"/gold/subscription\")\n        elif goldtype in (\"onetime\", \"code\"):\n            if months is None or months < 1:\n                start_over = True\n        elif goldtype == \"creddits\":\n            if num_creddits is None or num_creddits < 1:\n                start_over = True\n            else:\n                months = num_creddits\n        elif goldtype == \"gift\":\n            if months is None or months < 1:\n                start_over = True\n\n            if thing:\n                recipient = Account._byID(thing.author_id, data=True)\n                if recipient._deleted:\n                    thing = None\n                    recipient = None\n                    start_over = True\n            elif not recipient:\n                start_over = True\n        else:\n            goldtype = \"\"\n            start_over = True\n\n        if start_over:\n            # If we have a form that didn't validate, and we're on the payment\n            # page, redirect to the form, passing all of our form fields\n            # (which are currently GET parameters).\n            if is_payment:\n                g.stats.simple_event(\"gold.checkout_redirects.to_form\")\n                qs = query_string(request.GET)\n                return self.redirect('/gold' + qs)\n\n            can_subscribe = (c.user_is_loggedin and\n                             not c.user.has_gold_subscription)\n            if not can_subscribe and goldtype == \"autorenew\":\n                self.redirect(\"/creddits\", code=302)\n\n            return BoringPage(_(\"reddit gold\"),\n                              show_sidebar=False,\n                              content=Gold(goldtype, period, months, signed,\n                                           email, recipient,\n                                           giftmessage,\n                                           can_subscribe=can_subscribe,\n                                           edit=edit),\n                              page_classes=[\"gold-page\", \"gold-signup\", \"gold-page-ga-tracking\"],\n                              ).render()\n        else:\n            # If we have a validating form, and we're not yet on the payment\n            # page, redirect to it, passing all of our form fields\n            # (which are currently GET parameters).\n            if not is_payment:\n                g.stats.simple_event(\"gold.checkout_redirects.to_payment\")\n                qs = query_string(request.GET)\n                return self.redirect('/gold/payment' + qs)\n\n            payment_blob = dict(goldtype=goldtype,\n                                status=\"initialized\")\n            if c.user_is_loggedin:\n                payment_blob[\"account_id\"] = c.user._id\n                payment_blob[\"account_name\"] = c.user.name\n            else:\n                payment_blob[\"email\"] = email\n\n            if goldtype == \"gift\":\n                payment_blob[\"signed\"] = signed\n                payment_blob[\"recipient\"] = recipient.name\n                payment_blob[\"giftmessage\"] = _force_utf8(giftmessage)\n                if thing:\n                    payment_blob[\"thing\"] = thing._fullname\n\n            passthrough = generate_blob(payment_blob)\n\n            page_classes = [\"gold-page\", \"gold-payment\", \"gold-page-ga-tracking\"]\n            if goldtype == \"creddits\":\n                page_classes.append(\"creddits-payment\")\n\n            return BoringPage(_(\"reddit gold\"),\n                              show_sidebar=False,\n                              content=GoldPayment(goldtype, period, months,\n                                                  signed, recipient,\n                                                  giftmessage, passthrough,\n                                                  thing),\n                              page_classes=page_classes,\n                              ).render()\n\n    def GET_creddits(self):\n        return BoringPage(_(\"purchase creddits\"),\n                          show_sidebar=False,\n                          content=Creddits(),\n                          page_classes=[\"gold-page\", \"creddits-purchase\", \"gold-page-ga-tracking\"],\n                          ).render()\n\n    @validate(VUser())\n    def GET_subscription(self):\n        user = c.user\n        content = GoldSubscription(user)\n        return BoringPage(_(\"reddit gold subscription\"),\n                          show_sidebar=False,\n                          content=content,\n                          page_classes=[\"gold-page-ga-tracking\"]\n                         ).render()\n\n\nclass FrontUnstyledController(FrontController):\n    allow_stylesheets = False\n"
  },
  {
    "path": "r2/r2/controllers/googletagmanager.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom pylons import app_globals as g\nfrom pylons import tmpl_context as c\nfrom pylons import request\n\nfrom r2.controllers.reddit_base import MinimalController\nfrom r2.lib.pages import (\n    GoogleTagManagerJail,\n    GoogleTagManager,\n)\nfrom r2.lib.validator import (\n    validate,\n    VGTMContainerId,\n)\n\n\nclass GoogleTagManagerController(MinimalController):\n    def pre(self):\n        if request.host != g.media_domain:\n            # don't serve up untrusted content except on our\n            # specifically untrusted domain\n            self.abort404()\n\n        MinimalController.pre(self)\n\n        c.allow_framing = True\n\n    @validate(\n        container_id=VGTMContainerId(\"id\")\n    )\n    def GET_jail(self, container_id):\n        return GoogleTagManagerJail(container_id=container_id).render()\n\n    @validate(\n        container_id=VGTMContainerId(\"id\")\n    )\n    def GET_gtm(self, container_id):\n        return GoogleTagManager(container_id=container_id).render()\n"
  },
  {
    "path": "r2/r2/controllers/health.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport json\nimport os\n\nimport pylibmc\nfrom pylons import request, response\nfrom pylons import app_globals as g\nfrom pylons.controllers.util import abort\n\nfrom r2.controllers.reddit_base import MinimalController\nfrom r2.lib import promote, cache\n\n\nclass HealthController(MinimalController):\n    def pre(self):\n        pass\n\n    def post(self):\n        pass\n\n    def GET_health(self):\n        if os.path.exists(\"/var/opt/reddit/quiesce\"):\n            request.environ[\"usable_error_content\"] = \"No thanks, I'm full.\"\n            abort(503)\n\n        response.content_type = \"application/json\"\n        return json.dumps(g.versions, sort_keys=True, indent=4)\n\n    def GET_promohealth(self):\n        response.content_type = \"application/json\"\n        return json.dumps(promote.health_check())\n\n    def GET_cachehealth(self):\n        results = {}\n        behaviors = {\n            # Passed on to poll(2) in milliseconds\n            \"connect_timeout\": 1000,\n            # Passed on to setsockopt(2) in microseconds\n            \"receive_timeout\": int(1e6),\n            \"send_timeout\": int(1e6),\n        }\n        for server in cache._CACHE_SERVERS:\n            try:\n                if server.startswith(\"udp:\"):\n                    # libmemcached doesn't support UDP get/fetch operations\n                    continue\n                mc = pylibmc.Client([server], behaviors=behaviors)\n                # it's ok that not all caches are mcrouter, we'll just ignore\n                # the miss either way\n                mc.get(\"__mcrouter__.version\")\n                results[server] = \"OK\"\n            except pylibmc.Error as e:\n                g.log.warning(\"Health check for %s FAILED: %s\", server, e)\n                results[server] = \"FAILED %s\" % e\n        return json.dumps(results)\n"
  },
  {
    "path": "r2/r2/controllers/ipn.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom datetime import datetime, timedelta\n\nimport json\n\nfrom pylons import request\nfrom pylons import tmpl_context as c\nfrom pylons import app_globals as g\nfrom pylons.i18n import _\nfrom sqlalchemy.exc import IntegrityError\nimport stripe\n\nfrom r2.controllers.reddit_base import RedditController\nfrom r2.lib.base import abort\nfrom r2.lib.csrf import csrf_exempt\nfrom r2.lib.emailer import _system_email\nfrom r2.lib.errors import MessageError\nfrom r2.lib.filters import _force_unicode, _force_utf8\nfrom r2.lib.hooks import get_hook\nfrom r2.lib.pages import GoldGiftCodeEmail\nfrom r2.lib.strings import strings\nfrom r2.lib.utils import constant_time_compare, randstr, timeago\nfrom r2.lib.validator import (\n    nop,\n    textresponse,\n    validatedForm,\n    VByName,\n    VDecimal,\n    VFloat,\n    VInt,\n    VLength,\n    VModhash,\n    VOneOf,\n    VPrintable,\n    VUser,\n)\nfrom r2.models import (\n    Account,\n    account_by_payingid,\n    account_from_stripe_customer_id,\n    accountid_from_subscription,\n    admintools,\n    append_random_bottlecap_phrase,\n    cancel_subscription,\n    Comment,\n    creddits_lock,\n    create_claimed_gold,\n    create_gift_gold,\n    create_gold_code,\n    Email,\n    get_discounted_price,\n    has_prev_subscr_payments,\n    Link,\n    make_gold_message,\n    NotFound,\n    retrieve_gold_transaction,\n    send_system_message,\n    Thing,\n    update_gold_transaction,\n    generate_token,\n)\n\nBLOB_TTL = 86400 * 30\nstripe.api_key = g.secrets['stripe_secret_key']\n\n\ndef generate_blob(data):\n    passthrough = generate_token(15)\n\n    g.hardcache.set(\"payment_blob-\" + passthrough,\n                    data, BLOB_TTL)\n    g.log.info(\"just set payment_blob-%s\", passthrough)\n    return passthrough\n\n\ndef get_blob(code):\n    key = \"payment_blob-\" + code\n    with g.make_lock(\"payment_blob\", \"payment_blob_lock-\" + code):\n        blob = g.hardcache.get(key)\n        if not blob:\n            raise NotFound(\"No payment_blob-\" + code)\n        if blob.get('status', None) != 'initialized':\n            raise ValueError(\"payment_blob %s has status = %s\" %\n                             (code, blob.get('status', None)))\n        blob['status'] = \"locked\"\n        g.hardcache.set(key, blob, BLOB_TTL)\n    return key, blob\n\n\ndef update_blob(code, updates=None):\n    blob = g.hardcache.get(\"payment_blob-%s\" % code)\n    if not blob:\n        raise NotFound(\"No payment_blob-\" + code)\n    if blob.get('account_id', None) != c.user._id:\n        raise ValueError(\"%s doesn't have access to payment_blob %s\" %\n                         (c.user._id, code))\n\n    for item, value in updates.iteritems():\n        blob[item] = value\n    g.hardcache.set(\"payment_blob-%s\" % code, blob, BLOB_TTL)\n\n\ndef has_blob(custom):\n    if not custom:\n        return False\n\n    blob = g.hardcache.get('payment_blob-%s' % custom)\n    return bool(blob)\n\ndef dump_parameters(parameters):\n    for k, v in parameters.iteritems():\n        g.log.info(\"IPN: %r = %r\" % (k, v))\n\ndef check_payment_status(payment_status):\n    if payment_status is None:\n        payment_status = ''\n\n    psl = payment_status.lower()\n\n    if psl == 'completed':\n        return (None, psl)\n    elif psl == 'refunded':\n        return (\"Ok\", psl)\n    elif psl == 'pending':\n        return (\"Ok\", psl)\n    elif psl == 'reversed':\n        return (\"Ok\", psl)\n    elif psl == 'canceled_reversal':\n        return (\"Ok\", psl)\n    elif psl == 'failed':\n        return (\"Ok\", psl)\n    elif psl == 'denied':\n        return (\"Ok\", psl)\n    elif psl == '':\n        return (None, psl)\n    else:\n        raise ValueError(\"Unknown IPN status: %r\" % payment_status)\n\ndef check_txn_type(txn_type, psl):\n    if txn_type == 'subscr_signup':\n        return (\"Ok\", None)\n    elif txn_type == 'subscr_cancel':\n        return (\"Ok\", \"cancel\")\n    elif txn_type == 'subscr_eot':\n        return (\"Ok\", None)\n    elif txn_type == 'subscr_failed':\n        return (\"Ok\", None)\n    elif txn_type == 'subscr_modify':\n        return (\"Ok\", None)\n    elif txn_type == 'send_money':\n        return (\"Ok\", None)\n    elif txn_type in ('new_case',\n        'recurring_payment_suspended_due_to_max_failed_payment'):\n        return (\"Ok\", None)\n    elif txn_type == 'subscr_payment' and psl == 'completed':\n        return (None, \"new\")\n    elif txn_type == 'web_accept' and psl == 'completed':\n        return (None, None)\n    elif txn_type == \"paypal_here\":\n        return (\"Ok\", None)\n    else:\n        raise ValueError(\"Unknown IPN txn_type / psl %r\" %\n                         ((txn_type, psl),))\n\n\ndef existing_subscription(subscr_id, paying_id, custom):\n    if subscr_id is None:\n        return None\n\n    account_id = accountid_from_subscription(subscr_id)\n\n    if not account_id and has_blob(custom):\n        # New subscription contains the user info in hardcache\n        return None\n\n    should_set_subscriber = False\n    if account_id is None:\n        # Payment from legacy subscription (subscr_id not set), fall back\n        # to guessing the user from the paying_id\n        account_id = account_by_payingid(paying_id)\n        should_set_subscriber = True\n        if account_id is None:\n            return None\n\n    try:\n        account = Account._byID(account_id, data=True)\n\n        if account._deleted:\n            g.log.info(\"IPN renewal for deleted account %d (%s)\", account_id,\n                       subscr_id)\n            return \"deleted account\"\n\n        if should_set_subscriber:\n            if hasattr(account, \"gold_subscr_id\") and account.gold_subscr_id:\n                g.log.warning(\"Attempted to set subscr_id (%s) for account (%d) \"\n                              \"that already has one.\" % (subscr_id, account_id))\n                return None\n\n            account.gold_subscr_id = subscr_id\n            account._commit()\n    except NotFound:\n        g.log.info(\"Just got IPN renewal for non-existent account #%d\" % account_id)\n\n    return account\n\ndef months_and_days_from_pennies(pennies, discount=False):\n    if discount:\n        year_pennies = get_discounted_price(g.gold_year_price).pennies\n        month_pennies = get_discounted_price(g.gold_month_price).pennies\n    else:\n        year_pennies = g.gold_year_price.pennies\n        month_pennies = g.gold_month_price.pennies\n\n    if pennies >= year_pennies:\n        years = pennies / year_pennies\n        months = 12 * years\n        days  = 366 * years\n    else:\n        months = pennies / month_pennies\n        days   = 31 * months\n    return (months, days)\n\ndef send_gift(buyer, recipient, months, days, signed, giftmessage,\n              thing_fullname, note=None):\n    admintools.adjust_gold_expiration(recipient, days=days)\n\n    # increment num_gildings for all types of gildings not to themselves\n    if buyer != recipient:\n        buyer._incr(\"num_gildings\")\n\n    if thing_fullname:\n        thing = Thing._by_fullname(thing_fullname, data=True)\n        thing._gild(buyer)\n        if isinstance(thing, Comment):\n            gilding_type = 'comment gild'\n        else:\n            gilding_type = 'post gild'\n    else:\n        thing = None\n        get_hook('user.gild').call(recipient=recipient, gilder=buyer)\n        gilding_type = 'user gild'\n\n    if signed:\n        sender = buyer.name\n        md_sender = \"/u/%s\" % sender\n        repliable = True\n    else:\n        sender = \"An anonymous redditor\"\n        md_sender = \"An anonymous redditor\"\n\n        if buyer.name in g.live_config[\"proxy_gilding_accounts\"]:\n            repliable = False\n        else:    \n            repliable = True\n\n    create_gift_gold(buyer._id, recipient._id, days, c.start_time, signed, note, gilding_type)\n\n    if months == 1:\n        amount = \"a month\"\n    else:\n        amount = \"%d months\" % months\n\n    if not thing:\n        subject = 'Let there be gold! %s just sent you reddit gold!' % sender\n        message = strings.youve_got_gold % dict(sender=md_sender, amount=amount)\n    else:\n        url = thing.make_permalink_slow()\n        if isinstance(thing, Comment):\n            subject = 'Your comment has been gilded!'\n            message = strings.youve_been_gilded_comment \n            message %= {'sender': md_sender, 'url': url}\n        else:\n            subject = 'Your submission has been gilded!'\n            message = strings.youve_been_gilded_link \n            message %= {'sender': md_sender, 'url': url}\n\n    if giftmessage and giftmessage.strip():\n        message += (\"\\n\\n\" + strings.giftgold_note + \n                    _force_unicode(giftmessage) + '\\n\\n----')\n\n    message += '\\n\\n' + strings.gold_benefits_msg\n    if g.lounge_reddit:\n        message += '\\n\\n' + strings.lounge_msg\n    message = append_random_bottlecap_phrase(message)\n\n    if not signed:\n        if not repliable:\n            message += '\\n\\n' + strings.unsupported_respond_to_gilder\n        else:\n            message += '\\n\\n' + strings.respond_to_anonymous_gilder\n\n    try:\n        send_system_message(recipient, subject, message, author=buyer,\n                            distinguished='gold-auto', repliable=repliable,\n                            signed=signed)\n    except MessageError:\n        g.log.error('send_gift: could not send system message')\n\n    g.log.info(\"%s gifted %s to %s\" % (buyer.name, amount, recipient.name))\n    return thing\n\n\ndef send_gold_code(buyer, months, days,\n                   trans_id=None, payer_email='', pennies=0, buyer_email=None):\n    if buyer:\n        paying_id = buyer._id\n        buyer_name = buyer.name\n    else:\n        paying_id = buyer_email\n        buyer_name = buyer_email\n    code = create_gold_code(trans_id, payer_email,\n                            paying_id, pennies, days, c.start_time)\n    # format the code so it's easier to read (XXXXX-XXXXX)\n    split_at = len(code) / 2\n    code = code[:split_at] + '-' + code[split_at:]\n\n    if months == 1:\n        amount = \"a month\"\n    else:\n        amount = \"%d months\" % months\n\n    subject = _('Your gold gift code has been generated!')\n    message = _('Here is your gift code for %(amount)s of reddit gold:\\n\\n'\n                '%(code)s\\n\\nThe recipient (or you!) can enter it at '\n                'https://www.reddit.com/gold or go directly to '\n                'https://www.reddit.com/thanks/%(code)s to claim it.'\n              ) % {'amount': amount, 'code': code}\n\n    if buyer:\n        # bought by a logged-in user, send a reddit PM\n        message = append_random_bottlecap_phrase(message)\n        send_system_message(buyer, subject, message, distinguished='gold-auto')\n    else:\n        # bought by a logged-out user, send an email\n        contents = GoldGiftCodeEmail(message=message).render(style='email')\n        _system_email(buyer_email, contents, Email.Kind.GOLD_GIFT_CODE)\n                      \n    g.log.info(\"%s bought a gold code for %s\", buyer_name, amount)\n    return code\n\n\nclass IpnController(RedditController):\n    # Used when buying gold with creddits\n    @validatedForm(VUser(),\n                   VModhash(),\n                   months = VInt(\"months\"),\n                   passthrough = VPrintable(\"passthrough\", max_length=50))\n    def POST_spendcreddits(self, form, jquery, months, passthrough):\n        if months is None or months < 1:\n            form.set_text(\".status\", _(\"nice try.\"))\n            return\n\n        days = months * 31\n\n        if not passthrough:\n            raise ValueError(\"/spendcreddits got no passthrough?\")\n\n        blob_key, payment_blob = get_blob(passthrough)\n        if payment_blob[\"goldtype\"] not in (\"gift\", \"code\", \"onetime\"):\n            raise ValueError(\"/spendcreddits payment_blob %s has goldtype %s\" %\n                             (passthrough, payment_blob[\"goldtype\"]))\n\n        if payment_blob[\"account_id\"] != c.user._id:\n            fmt = (\"/spendcreddits payment_blob %s has userid %d \" +\n                   \"but c.user._id is %d\")\n            raise ValueError(fmt % passthrough,\n                             payment_blob[\"account_id\"],\n                             c.user._id)\n\n        if payment_blob[\"goldtype\"] == \"gift\":\n            signed = payment_blob[\"signed\"]\n            giftmessage = _force_unicode(payment_blob[\"giftmessage\"])\n            recipient_name = payment_blob[\"recipient\"]\n\n            try:\n                recipient = Account._by_name(recipient_name)\n            except NotFound:\n                raise ValueError(\"Invalid username %s in spendcreddits, buyer = %s\"\n                                 % (recipient_name, c.user.name))\n\n            if recipient._deleted:\n                form.set_text(\".status\", _(\"that user has deleted their account\"))\n                return\n\n        redirect_to_spent = False\n        thing = None\n\n        with creddits_lock(c.user):\n            if not c.user.employee and c.user.gold_creddits < months:\n                msg = \"%s is trying to sneak around the creddit check\"\n                msg %= c.user.name\n                raise ValueError(msg)\n\n            if payment_blob[\"goldtype\"] == \"gift\":\n                thing_fullname = payment_blob.get(\"thing\")\n                thing = send_gift(c.user, recipient, months, days, signed,\n                                  giftmessage, thing_fullname)\n                form.set_text(\".status\", _(\"the gold has been delivered!\"))\n            elif payment_blob[\"goldtype\"] == \"code\":\n                try:\n                    send_gold_code(c.user, months, days)\n                except MessageError:\n                    msg = _(\"there was an error creating a gift code. \"\n                            \"please try again later, or contact %(email)s \"\n                            \"for assistance.\") % {'email': g.goldsupport_email}\n                    form.set_text(\".status\", msg)\n                    return\n                form.set_text(\".status\",\n                              _(\"the gift code has been messaged to you!\"))\n            elif payment_blob[\"goldtype\"] == \"onetime\":\n                admintools.adjust_gold_expiration(c.user, days=days)\n                form.set_text(\".status\", _(\"the gold has been delivered!\"))\n\n            redirect_to_spent = True\n\n            if not c.user.employee:\n                c.user.gold_creddits -= months\n                c.user._commit()\n\n        form.find(\"button\").hide()\n\n        payment_blob[\"status\"] = \"processed\"\n        g.hardcache.set(blob_key, payment_blob, BLOB_TTL)\n\n        if thing:\n            gilding_message = make_gold_message(thing, user_gilded=True)\n            jquery.gild_thing(thing_fullname, gilding_message, thing.gildings)\n        elif redirect_to_spent:\n            form.redirect(\"/gold/thanks?v=spent-creddits\")\n\n    @csrf_exempt\n    @textresponse(paypal_secret = VPrintable('secret', 50),\n                  payment_status = VPrintable('payment_status', 20),\n                  txn_id = VPrintable('txn_id', 20),\n                  paying_id = VPrintable('payer_id', 50),\n                  payer_email = VPrintable('payer_email', 250),\n                  mc_currency = VPrintable('mc_currency', 20),\n                  mc_gross = VDecimal('mc_gross'),\n                  custom = VPrintable('custom', 50))\n    def POST_ipn(self, paypal_secret, payment_status, txn_id, paying_id,\n                 payer_email, mc_currency, mc_gross, custom):\n\n        parameters = request.POST.copy()\n\n        # Make sure it's really PayPal\n        if not constant_time_compare(paypal_secret,\n                                     g.secrets['paypal_webhook']):\n            raise ValueError\n\n        # Return early if it's an IPN class we don't care about\n        response, psl = check_payment_status(payment_status)\n        if response:\n            return response\n\n        # Return early if it's a txn_type we don't care about\n        response, subscription = check_txn_type(parameters['txn_type'], psl)\n        if subscription is None:\n            subscr_id = None\n        elif subscription == \"new\":\n            subscr_id = parameters['subscr_id']\n        elif subscription == \"cancel\":\n            cancel_subscription(parameters['subscr_id'])\n        else:\n            raise ValueError(\"Weird subscription: %r\" % subscription)\n\n        if response:\n            return response\n\n        if mc_currency != 'USD':\n            raise ValueError(\"Somehow got non-USD IPN %r\" % mc_currency)\n\n        if not (txn_id and paying_id and payer_email and mc_gross):\n            dump_parameters(parameters)\n            raise ValueError(\"Got incomplete IPN\")\n\n        pennies = int(mc_gross * 100)\n        months, days = months_and_days_from_pennies(pennies)\n\n        # Special case: autorenewal payment\n        existing = existing_subscription(subscr_id, paying_id, custom)\n        if existing:\n            if existing != \"deleted account\":\n                try:\n                    create_claimed_gold (\"P\" + txn_id, payer_email, paying_id,\n                                         pennies, days, None, existing._id,\n                                         c.start_time, subscr_id)\n                except IntegrityError:\n                    return \"Ok\"\n                admintools.adjust_gold_expiration(existing, days=days)\n\n                subject, message = subscr_pm(pennies, months, new_subscr=False)\n                message = append_random_bottlecap_phrase(message)\n                send_system_message(existing.name, subject, message,\n                                    distinguished='gold-auto')\n\n                g.log.info(\"Just applied IPN renewal for %s, %d days\" %\n                           (existing.name, days))\n            return \"Ok\"\n\n        # More sanity checks that all non-autorenewals should pass:\n\n        if not custom:\n            dump_parameters(parameters)\n            raise ValueError(\"Got IPN with txn_id=%s and no custom\"\n                             % txn_id)\n\n        self.finish(parameters, \"P\" + txn_id,\n                    payer_email, paying_id, subscr_id,\n                    custom, pennies, months, days)\n\n    def finish(self, parameters, txn_id,\n               payer_email, paying_id, subscr_id,\n               custom, pennies, months, days):\n\n        try:\n            blob_key, payment_blob = get_blob(custom)\n        except ValueError:\n            g.log.error(\"whoops, %s was locked\", custom)\n            return\n\n        buyer = None\n        buyer_email = None\n        buyer_id = payment_blob.get('account_id', None)\n        if buyer_id:\n            try:\n                buyer = Account._byID(buyer_id, data=True)\n            except NotFound:\n                dump_parameters(parameters)\n                raise ValueError(\"Invalid buyer_id %d in IPN with custom='%s'\"\n                                 % (buyer_id, custom))\n        else:\n            buyer_email = payment_blob.get('email')\n            if not buyer_email:\n                dump_parameters(parameters)\n                error = \"No buyer_id or email in IPN with custom='%s'\" % custom\n                raise ValueError(error)\n\n        if subscr_id:\n            buyer.gold_subscr_id = subscr_id\n\n        instagift = False\n        if payment_blob['goldtype'] == 'onetime':\n            admintools.adjust_gold_expiration(buyer, days=days)\n\n            subject = _(\"Eureka! Thank you for investing in reddit gold!\")\n            message = _(\"Thank you for buying reddit gold. Your patronage \"\n                        \"supports the site and makes future development \"\n                        \"possible. For example, one month of reddit gold \"\n                        \"pays for 5 instance hours of reddit's servers.\")\n            message += \"\\n\\n\" + strings.gold_benefits_msg\n            if g.lounge_reddit:\n                message += \"\\n\\n\" + strings.lounge_msg\n        elif payment_blob['goldtype'] == 'autorenew':\n            admintools.adjust_gold_expiration(buyer, days=days)\n            subject, message = subscr_pm(pennies, months, new_subscr=True)\n        elif payment_blob['goldtype'] == 'creddits':\n            buyer._incr(\"gold_creddits\", months)\n            buyer._commit()\n            subject = _(\"Eureka! Thank you for investing in reddit gold \"\n                        \"creddits!\")\n\n            message = _(\"Thank you for buying creddits. Your patronage \"\n                        \"supports the site and makes future development \"\n                        \"possible. To spend your creddits and spread reddit \"\n                        \"gold, visit [/gold](/gold) or your favorite \"\n                        \"person's user page.\")\n            message += \"\\n\\n\" + strings.gold_benefits_msg + \"\\n\\n\"\n            message += _(\"Thank you again for your support, and have fun \"\n                         \"spreading gold!\")\n        elif payment_blob['goldtype'] == 'gift':\n            recipient_name = payment_blob.get('recipient', None)\n            try:\n                recipient = Account._by_name(recipient_name)\n            except NotFound:\n                dump_parameters(parameters)\n                raise ValueError(\"Invalid recipient_name %s in IPN/GC with custom='%s'\"\n                                 % (recipient_name, custom))\n            signed = payment_blob.get(\"signed\", False)\n            giftmessage = _force_unicode(payment_blob.get(\"giftmessage\", \"\"))\n            thing_fullname = payment_blob.get(\"thing\")\n            send_gift(buyer, recipient, months, days, signed, giftmessage,\n                      thing_fullname)\n            instagift = True\n            subject = _(\"Thanks for giving the gift of reddit gold!\")\n            message = _(\"Your classy gift to %s has been delivered.\\n\\n\"\n                        \"Thank you for gifting reddit gold. Your patronage \"\n                        \"supports the site and makes future development \"\n                        \"possible.\") % recipient.name\n            message += \"\\n\\n\" + strings.gold_benefits_msg + \"\\n\\n\"\n            message += _(\"Thank you again for your support, and have fun \"\n                         \"spreading gold!\")\n        elif payment_blob['goldtype'] == 'code':\n            pass\n        else:\n            dump_parameters(parameters)\n            raise ValueError(\"Got status '%s' in IPN/GC\" % payment_blob['status'])\n\n        if payment_blob['goldtype'] == 'code':\n            send_gold_code(buyer, months, days, txn_id, payer_email,\n                           pennies, buyer_email)\n        else:\n            # Reuse the old \"secret\" column as a place to record the goldtype\n            # and \"custom\", just in case we need to debug it later or something\n            secret = payment_blob['goldtype'] + \"-\" + custom\n\n            if instagift:\n                status=\"instagift\"\n            else:\n                status=\"processed\"\n\n            create_claimed_gold(txn_id, payer_email, paying_id, pennies, days,\n                                secret, buyer_id, c.start_time,\n                                subscr_id, status=status)\n\n            message = append_random_bottlecap_phrase(message)\n\n            try:\n                send_system_message(buyer, subject, message,\n                                    distinguished='gold-auto')\n            except MessageError:\n                g.log.error('finish: could not send system message')\n\n        payment_blob[\"status\"] = \"processed\"\n        g.hardcache.set(blob_key, payment_blob, BLOB_TTL)\n\n\nclass Webhook(object):\n    def __init__(self, passthrough=None, transaction_id=None, subscr_id=None,\n                 pennies=None, months=None, payer_email='', payer_id='',\n                 goldtype=None, buyer=None, recipient=None, signed=False,\n                 giftmessage=None, thing=None, buyer_email=None):\n        self.passthrough = passthrough\n        self.transaction_id = transaction_id\n        self.subscr_id = subscr_id\n        self.pennies = pennies\n        self.months = months\n        self.payer_email = payer_email\n        self.payer_id = payer_id\n        self.goldtype = goldtype\n        self.buyer = buyer\n        self.buyer_email = buyer_email\n        self.recipient = recipient\n        self.signed = signed\n        self.giftmessage = giftmessage\n        self.thing = thing\n\n    def load_blob(self):\n        payment_blob = validate_blob(self.passthrough)\n        self.goldtype = payment_blob['goldtype']\n        self.buyer = payment_blob.get('buyer')\n        self.buyer_email = payment_blob.get('email')\n        self.recipient = payment_blob.get('recipient')\n        self.signed = payment_blob.get('signed', False)\n        self.giftmessage = payment_blob.get('giftmessage')\n        thing = payment_blob.get('thing')\n        self.thing = thing._fullname if thing else None\n\n    def __repr__(self):\n        return '<%s: transaction %s>' % (self.__class__.__name__, self.transaction_id)\n\n\nclass GoldPaymentController(RedditController):\n    name = ''\n    webhook_secret = ''\n    event_type_mappings = {}\n    abort_on_error = True\n\n    @csrf_exempt\n    @textresponse(secret=VPrintable('secret', 50))\n    def POST_goldwebhook(self, secret):\n        self.validate_secret(secret)\n        status, webhook = self.process_response()\n\n        try:\n            event_type = self.event_type_mappings[status]\n        except KeyError:\n            g.log.error('%s %s: unknown status %s' % (self.name,\n                                                      webhook,\n                                                      status))\n            if self.abort_on_error:\n                self.abort403()\n            else:\n                return\n        self.process_webhook(event_type, webhook)\n\n    def validate_secret(self, secret):\n        if not constant_time_compare(secret, self.webhook_secret):\n            g.log.error('%s: invalid webhook secret from %s' % (self.name,\n                                                                request.ip))\n            self.abort403() \n\n    @classmethod\n    def process_response(cls):\n        \"\"\"Extract status and webhook.\"\"\"\n        raise NotImplementedError\n\n    def process_webhook(self, event_type, webhook):\n        if event_type == 'noop':\n            return\n\n        existing = retrieve_gold_transaction(webhook.transaction_id)\n        if not existing and webhook.passthrough:\n            try:\n                webhook.load_blob()\n            except GoldException as e:\n                g.log.error('%s: payment_blob %s', webhook.transaction_id, e)\n                if self.abort_on_error:\n                    self.abort403()\n                else:\n                    return\n        msg = None\n\n        if event_type == 'cancelled':\n            subject = _('reddit gold payment cancelled')\n            msg = _('Your reddit gold payment has been cancelled, contact '\n                    '%(gold_email)s for details') % {'gold_email':\n                                                     g.goldsupport_email}\n            if existing:\n                # note that we don't check status on existing, probably\n                # should update gold_table when a cancellation happens\n                reverse_gold_purchase(webhook.transaction_id)\n        elif event_type == 'succeeded':\n            if (existing and\n                    existing.status in ('processed', 'unclaimed', 'claimed')):\n                g.log.info('POST_goldwebhook skipping %s' % webhook.transaction_id)\n                return\n\n            self.complete_gold_purchase(webhook)\n        elif event_type == 'failed':\n            subject = _('reddit gold payment failed')\n            msg = _('Your reddit gold payment has failed, contact '\n                    '%(gold_email)s for details') % {'gold_email':\n                                                     g.goldsupport_email}\n        elif event_type == 'deleted_subscription':\n            # the subscription may have been deleted directly by the user using\n            # POST_delete_subscription, in which case gold_subscr_id is already\n            # unset and we don't need to message them\n            if webhook.buyer and webhook.buyer.gold_subscr_id:\n                subject = _('reddit gold subscription cancelled')\n                msg = _('Your reddit gold subscription has been cancelled '\n                        'because your credit card could not be charged. '\n                        'Contact %(gold_email)s for details')\n                msg %= {'gold_email': g.goldsupport_email}\n                webhook.buyer.gold_subscr_id = None\n                webhook.buyer._commit()\n        elif event_type == 'refunded':\n            if not (existing and existing.status == 'processed'):\n                return\n\n            subject = _('reddit gold refund')\n            msg = _('Your reddit gold payment has been refunded, contact '\n                   '%(gold_email)s for details') % {'gold_email':\n                                                    g.goldsupport_email}\n            reverse_gold_purchase(webhook.transaction_id)\n\n        if msg:\n            if existing:\n                buyer = Account._byID(int(existing.account_id), data=True)\n            elif webhook.buyer:\n                buyer = webhook.buyer\n            else:\n                return\n\n            try:\n                send_system_message(buyer, subject, msg)\n            except MessageError:\n                g.log.error('process_webhook: send_system_message error')\n\n    @classmethod\n    def complete_gold_purchase(cls, webhook):\n        \"\"\"After receiving a message from a payment processor, apply gold.\n\n        Shared endpoint for all payment processing systems. Validation of gold\n        purchase (sender, recipient, etc.) should happen before hitting this.\n\n        \"\"\"\n\n        secret = webhook.passthrough\n        transaction_id = webhook.transaction_id\n        payer_email = webhook.payer_email\n        payer_id = webhook.payer_id\n        subscr_id = webhook.subscr_id\n        pennies = webhook.pennies\n        months = webhook.months\n        goldtype = webhook.goldtype\n        buyer = webhook.buyer\n        buyer_email = webhook.buyer_email\n        recipient = webhook.recipient\n        signed = webhook.signed\n        giftmessage = webhook.giftmessage\n        thing = webhook.thing\n\n        days = days_from_months(months)\n\n        # locking isn't necessary for code purchases\n        if goldtype == 'code':\n            send_gold_code(buyer, months, days, transaction_id,\n                           payer_email, pennies, buyer_email)\n            # the rest of the function isn't needed for a code purchase\n            return\n\n        gold_recipient = recipient or buyer\n        with gold_recipient.get_read_modify_write_lock() as lock:\n            gold_recipient.update_from_cache(lock)\n\n            secret_pieces = [goldtype]\n            if goldtype == 'gift':\n                secret_pieces.append(recipient.name)\n            secret_pieces.append(secret or transaction_id)\n            secret = '-'.join(secret_pieces)\n\n            if goldtype in ('onetime', 'autorenew'):\n                admintools.adjust_gold_expiration(buyer, days=days)\n                if goldtype == 'onetime':\n                    subject = \"thanks for buying reddit gold!\"\n                    if g.lounge_reddit:\n                        message = strings.lounge_msg\n                    else:\n                        message = \":)\"\n                else:\n                    if has_prev_subscr_payments(subscr_id):\n                        secret = None\n                        subject, message = subscr_pm(pennies, months, new_subscr=False)\n                    else:\n                        subject, message = subscr_pm(pennies, months, new_subscr=True)\n\n            elif goldtype == 'creddits':\n                buyer._incr('gold_creddits', months)\n                subject = \"thanks for buying creddits!\"\n                message = (\"To spend them, visit %s://%s/gold or your \"\n                           \"favorite person's userpage.\" % (g.default_scheme,\n                                                            g.domain))\n\n            elif goldtype == 'gift':\n                send_gift(buyer, recipient, months, days, signed, giftmessage,\n                          thing)\n                subject = \"thanks for giving reddit gold!\"\n                message = \"Your gift to %s has been delivered.\" % recipient.name\n\n            try:\n                create_claimed_gold(transaction_id, payer_email, payer_id,\n                                    pennies, days, secret, buyer._id,\n                                    c.start_time, subscr_id=subscr_id,\n                                    status='processed')\n            except IntegrityError:\n                g.log.error('gold: got duplicate gold transaction')\n\n            try:\n                message = append_random_bottlecap_phrase(message)\n                send_system_message(buyer, subject, message,\n                                    distinguished='gold-auto')\n            except MessageError:\n                g.log.error('complete_gold_purchase: send_system_message error')\n\n\ndef handle_stripe_error(fn):\n    def wrapper(cls, form, *a, **kw):\n        try:\n            return fn(cls, form, *a, **kw)\n        except stripe.CardError as e:\n            form.set_text('.status',\n                          _('error: %(error)s') % {'error': e.message})\n        except stripe.InvalidRequestError as e:\n            form.set_text('.status', _('invalid request'))\n        except stripe.APIConnectionError as e:\n            form.set_text('.status', _('api error'))\n        except stripe.AuthenticationError as e:\n            form.set_text('.status', _('connection error'))\n        except stripe.StripeError as e:\n            form.set_text('.status', _('error'))\n            g.log.error('stripe error: %s' % e)\n        except:\n            raise\n        form.find('.stripe-submit').removeAttr('disabled').end()\n    return wrapper\n\n\nclass StripeController(GoldPaymentController):\n    name = 'stripe'\n    webhook_secret = g.secrets['stripe_webhook']\n    event_type_mappings = {\n        'charge.succeeded': 'succeeded',\n        'charge.failed': 'failed',\n        'charge.refunded': 'refunded',\n        'charge.dispute.created': 'noop',\n        'charge.dispute.updated': 'noop',\n        'charge.dispute.closed': 'noop',\n        'charge.dispute.funds_withdrawn': 'noop',\n        'charge.updated': 'noop',\n        'customer.created': 'noop',\n        'customer.card.created': 'noop',\n        'customer.card.updated': 'noop',\n        'customer.card.deleted': 'noop',\n        'customer.source.updated': 'noop',\n        'transfer.created': 'noop',\n        'transfer.paid': 'noop',\n        'balance.available': 'noop',\n        'invoice.created': 'noop',\n        'invoice.updated': 'noop',\n        'invoice.payment_succeeded': 'noop',\n        'invoice.payment_failed': 'noop',\n        'invoiceitem.deleted': 'noop',\n        'customer.subscription.created': 'noop',\n        'customer.deleted': 'noop',\n        'customer.updated': 'noop',\n        'customer.subscription.deleted': 'deleted_subscription',\n        'customer.subscription.trial_will_end': 'noop',\n        'customer.subscription.updated': 'noop',\n        'review.opened': 'noop',\n        'dummy': 'noop',\n    }\n\n    @classmethod\n    def process_response(cls):\n        event_dict = json.loads(request.body)\n        stripe_secret = g.secrets['stripe_secret_key']\n        event = stripe.Event.construct_from(event_dict, stripe_secret)\n        status = event.type\n\n        if status == 'invoice.created':\n            # sent 1 hr before a subscription is charged or immediately for\n            # a new subscription\n            invoice = event.data.object\n            customer_id = invoice.customer\n            account = account_from_stripe_customer_id(customer_id)\n            # if the charge hasn't been attempted (meaning this is 1 hr before\n            # the charge) check that the account can receive the gold\n            if (not invoice.attempted and\n                (not account or (account and account._banned))):\n                # there's no associated account - delete the subscription\n                # to cancel the charge\n                g.log.error('no account for stripe invoice: %s', invoice)\n                try:\n                    cancel_stripe_subscription(customer_id)\n                except stripe.InvalidRequestError:\n                    pass\n        elif status == 'customer.subscription.deleted':\n            subscription = event.data.object\n            customer_id = subscription.customer\n            buyer = account_from_stripe_customer_id(customer_id)\n            webhook = Webhook(subscr_id=customer_id, buyer=buyer)\n            return status, webhook\n\n        event_type = cls.event_type_mappings.get(status)\n        if not event_type:\n            raise ValueError('Stripe: unrecognized status %s' % status)\n        elif event_type == 'noop':\n            return status, None\n\n        charge = event.data.object\n        description = charge.description\n        invoice_id = charge.invoice\n        transaction_id = 'S%s' % charge.id\n        pennies = charge.amount\n        months, days = months_and_days_from_pennies(pennies)\n\n        if status == 'charge.failed' and invoice_id:\n            # we'll get an additional failure notification event of\n            # \"invoice.payment_failed\", don't double notify\n            return 'dummy', None\n        elif status == 'charge.failed' and not description:\n            # create_customer can POST successfully but fail to create a\n            # customer because the card is declined. This will trigger a\n            # 'charge.failed' notification but without description so we can't\n            # do anything with it\n            return 'dummy', None\n        elif invoice_id:\n            # subscription charge - special handling\n            customer_id = charge.customer\n            buyer = account_from_stripe_customer_id(customer_id)\n            if not buyer and status == 'charge.refunded':\n                # refund may happen after the subscription has been cancelled\n                # and removed from the user's account. the refund process will\n                # be able to find the user from the transaction record\n                webhook = Webhook(transaction_id=transaction_id)\n                return status, webhook\n            elif not buyer:\n                charge_date = datetime.fromtimestamp(charge.created, tz=g.tz)\n\n                # don't raise exception if charge date is within the past hour\n                # db replication lag may cause the account lookup to fail\n                if charge_date < timeago('1 hour'):\n                    raise ValueError('no buyer for charge: %s' % charge.id)\n                else:\n                    abort(404, \"not found\")\n            webhook = Webhook(transaction_id=transaction_id,\n                              subscr_id=customer_id, pennies=pennies,\n                              months=months, goldtype='autorenew',\n                              buyer=buyer)\n            return status, webhook\n        else:\n            try:\n                passthrough = description[:20]\n            except (AttributeError, ValueError):\n                g.log.error('stripe_error on charge: %s', charge)\n                raise\n\n            webhook = Webhook(passthrough=passthrough,\n                transaction_id=transaction_id, pennies=pennies, months=months)\n            return status, webhook\n\n    @classmethod\n    @handle_stripe_error\n    def create_customer(cls, form, token, description):\n        customer = stripe.Customer.create(card=token, description=description)\n\n        if (customer['active_card']['address_line1_check'] == 'fail' or\n            customer['active_card']['address_zip_check'] == 'fail'):\n            form.set_text('.status',\n                          _('error: address verification failed'))\n            form.find('.stripe-submit').removeAttr('disabled').end()\n            return None\n        elif customer['active_card']['cvc_check'] == 'fail':\n            form.set_text('.status', _('error: cvc check failed'))\n            form.find('.stripe-submit').removeAttr('disabled').end()\n            return None\n        else:\n            return customer\n\n    @classmethod\n    @handle_stripe_error\n    def charge_customer(cls, form, customer, pennies, passthrough,\n                        description):\n        charge = stripe.Charge.create(\n            amount=pennies,\n            currency=\"usd\",\n            customer=customer['id'],\n            description='%s-%s' % (passthrough, description),\n        )\n        return charge\n\n    @classmethod\n    @handle_stripe_error\n    def set_creditcard(cls, form, user, token):\n        if not user.has_stripe_subscription:\n            return\n\n        customer = stripe.Customer.retrieve(user.gold_subscr_id)\n        customer.card = token\n        customer.save()\n        return customer\n\n    @classmethod\n    @handle_stripe_error\n    def set_subscription(cls, form, customer, plan_id):\n        subscription = customer.update_subscription(plan=plan_id)\n        return subscription\n\n    @classmethod\n    @handle_stripe_error\n    def cancel_subscription(cls, form, user):\n        if not user.has_stripe_subscription:\n            return\n\n        customer = cancel_stripe_subscription(user.gold_subscr_id)\n\n        user.gold_subscr_id = None\n        user._commit()\n        subject = _('your gold subscription has been cancelled')\n        message = _('if you have any questions please email %(email)s')\n        message %= {'email': g.goldsupport_email}\n        send_system_message(user, subject, message)\n        return customer\n\n    @csrf_exempt\n    @validatedForm(token=nop('stripeToken'),\n                   passthrough=VPrintable(\"passthrough\", max_length=50),\n                   pennies=VInt('pennies'),\n                   months=VInt(\"months\"),\n                   period=VOneOf(\"period\", (\"monthly\", \"yearly\")))\n    def POST_goldcharge(self, form, jquery, token, passthrough, pennies, months,\n                        period):\n        \"\"\"\n        Submit charge to stripe.\n\n        Called by GoldPayment form. This submits the charge to stripe, and gold\n        will be applied once we receive a webhook from stripe.\n\n        \"\"\"\n\n        try:\n            payment_blob = validate_blob(passthrough)\n        except GoldException as e:\n            # This should never happen. All fields in the payment_blob\n            # are validated on creation\n            form.set_text('.status',\n                          _('something bad happened, try again later'))\n            g.log.debug('POST_goldcharge: %s' % e.message)\n            return\n\n        if period:\n            plan_id = (g.STRIPE_MONTHLY_GOLD_PLAN if period == 'monthly'\n                       else g.STRIPE_YEARLY_GOLD_PLAN)\n            if c.user.has_gold_subscription:\n                form.set_text('.status',\n                              _('your account already has a gold subscription'))\n                return\n        else:\n            plan_id = None\n            penny_months, days = months_and_days_from_pennies(pennies)\n            if not months or months != penny_months:\n                form.set_text('.status', _('stop trying to trick the form'))\n                return\n\n        if c.user_is_loggedin:\n            description = c.user.name\n        else:\n            description = payment_blob[\"email\"]\n        customer = self.create_customer(form, token, description)\n        if not customer:\n            return\n\n        if period:\n            subscription = self.set_subscription(form, customer, plan_id)\n            if not subscription:\n                return\n\n            c.user.gold_subscr_id = customer.id\n            c.user._commit()\n\n            status = _('subscription created')\n            subject = _('reddit gold subscription')\n            body = _('Your subscription is being processed and reddit gold '\n                     'will be delivered shortly.')\n        else:\n            charge = self.charge_customer(form, customer, pennies,\n                                          passthrough, description)\n            if not charge:\n                return\n\n            status = _('payment submitted')\n            subject = _('reddit gold payment')\n            body = _('Your payment is being processed and reddit gold '\n                     'will be delivered shortly.')\n\n        form.set_text('.status', status)\n        if c.user_is_loggedin:\n            body = append_random_bottlecap_phrase(body)\n            send_system_message(c.user, subject, body, distinguished='gold-auto')\n            form.redirect(\"/gold/thanks?v=stripe\")\n\n    @validatedForm(VUser(),\n                   VModhash(),\n                   token=nop('stripeToken'))\n    def POST_modify_subscription(self, form, jquery, token):\n        customer = self.set_creditcard(form, c.user, token)\n        if not customer:\n            return\n\n        form.set_text('.status', _('your payment details have been updated'))\n\n    @validatedForm(VUser(),\n                   VModhash(),\n                   user=VByName('user'))\n    def POST_cancel_subscription(self, form, jquery, user):\n        if user != c.user and not c.user_is_admin:\n            self.abort403()\n        customer = self.cancel_subscription(form, user)\n        if not customer:\n            return\n\n        form.set_text(\".status\", _(\"your subscription has been cancelled\"))\n\nclass CoinbaseController(GoldPaymentController):\n    name = 'coinbase'\n    webhook_secret = g.secrets['coinbase_webhook']\n    event_type_mappings = {\n        'completed': 'succeeded',\n        'cancelled': 'cancelled',\n        'mispaid': 'noop',\n        'expired': 'noop',\n        'payout': 'noop',\n    }\n    abort_on_error = False\n\n    @classmethod\n    def process_response(cls):\n        event_dict = json.loads(request.body)\n\n        # handle non-payment events we can ignore\n        if 'payout' in event_dict:\n            return 'payout', None\n\n        order = event_dict['order']\n        transaction_id = 'C%s' % order['id']\n        status = order['status']    # new/completed/cancelled\n        pennies = int(order['total_native']['cents'])\n        months, days = months_and_days_from_pennies(pennies, discount=True)\n        passthrough = order['custom']\n        webhook = Webhook(passthrough=passthrough,\n            transaction_id=transaction_id, pennies=pennies, months=months)\n        return status, webhook\n\n\nclass RedditGiftsController(GoldPaymentController):\n    \"\"\"Handle notifications of gold purchases from reddit gifts.\n\n    Payment is handled by reddit gifts. Once an order is complete they can hit\n    this route to apply gold to a user's account.\n\n    The post should include data in the form:\n    {\n        'transaction_id', transaction_id,\n        'goldtype': goldtype,\n        'buyer': buyer name,\n        'pennies': pennies,\n        'months': months,\n        ['recipient': recipient name,]\n        ['giftmessage': message,]\n        ['signed': bool,]\n    }\n\n    \"\"\"\n\n    name = 'redditgifts'\n    webhook_secret = g.secrets['redditgifts_webhook']\n    event_type_mappings = {'succeeded': 'succeeded'}\n\n    def process_response(self):\n        data = request.POST\n\n        transaction_id = 'RG%s' % data['transaction_id']\n        pennies = int(data['pennies'])\n        months = int(data['months'])\n        status = 'succeeded'\n\n        goldtype = data['goldtype']\n        buyer = Account._by_name(data['buyer'])\n\n        if goldtype == 'gift':\n            gift_kw = {\n                'recipient': Account._by_name(data['recipient']),\n                'giftmessage': _force_unicode(data.get('giftmessage', None)),\n                'signed': data.get('signed') == 'True',\n            }\n        else:\n            gift_kw = {}\n\n        webhook = Webhook(transaction_id=transaction_id, pennies=pennies,\n                          months=months, goldtype=goldtype, buyer=buyer,\n                          **gift_kw)\n        return status, webhook\n\n\nclass GoldException(Exception): pass\n\n\ndef validate_blob(custom):\n    \"\"\"Validate payment_blob and return a dict with everything looked up.\"\"\"\n    ret = {}\n\n    if not custom:\n        raise GoldException('no custom')\n\n    payment_blob = g.hardcache.get('payment_blob-%s' % str(custom))\n    if not payment_blob:\n        raise GoldException('no payment_blob')\n\n    if 'account_id' in payment_blob and 'account_name' in payment_blob:\n        try:\n            buyer = Account._byID(payment_blob['account_id'], data=True)\n            ret['buyer'] = buyer\n        except NotFound:\n            raise GoldException('bad account_id')\n\n        if not buyer.name.lower() == payment_blob['account_name'].lower():\n            raise GoldException('buyer mismatch')\n    elif 'email' in payment_blob:\n        ret['email'] = payment_blob['email']\n    else:\n        raise GoldException('no account_id or email')\n\n    goldtype = payment_blob['goldtype']\n    ret['goldtype'] = goldtype\n\n    if goldtype == 'gift':\n        recipient_name = payment_blob.get('recipient', None)\n        if not recipient_name:\n            raise GoldException('gift missing recpient')\n        try:\n            recipient = Account._by_name(recipient_name)\n            ret['recipient'] = recipient\n        except NotFound:\n            raise GoldException('bad recipient')\n        thing_fullname = payment_blob.get('thing', None)\n        if thing_fullname:\n            try:\n                ret['thing'] = Thing._by_fullname(thing_fullname)\n            except NotFound:\n                raise GoldException('bad thing')\n        ret['signed'] = payment_blob.get('signed', False)\n        giftmessage = payment_blob.get('giftmessage')\n        giftmessage = _force_unicode(giftmessage) if giftmessage else None\n        ret['giftmessage'] = giftmessage\n    elif goldtype not in ('onetime', 'autorenew', 'creddits', 'code'):\n        raise GoldException('bad goldtype')\n\n    return ret\n\n\ndef days_from_months(months):\n    if months >= 12:\n        assert months % 12 == 0\n        years = months / 12\n        days = years * 366\n    else:\n        days = months * 31\n    return days\n\n\ndef subtract_gold_days(user, days):\n    user.gold_expiration -= timedelta(days=days)\n    if user.gold_expiration < datetime.now(g.display_tz):\n        admintools.degolden(user)\n    user._commit()\n\n\ndef subtract_gold_creddits(user, num):\n    user._incr('gold_creddits', -num)\n\n\ndef reverse_gold_purchase(transaction_id):\n    transaction = retrieve_gold_transaction(transaction_id)\n\n    if not transaction:\n        raise GoldException('gold_table %s not found' % transaction_id)\n\n    buyer = Account._byID(int(transaction.account_id), data=True)\n    recipient = None\n    days = transaction.days\n    months = days / 31\n\n    if transaction.subscr_id:\n        goldtype = 'autorenew'\n    else:\n        secret = transaction.secret\n        pieces = secret.split('-')\n        goldtype = pieces[0]\n\n    if goldtype == 'gift':\n        recipient_name, secret = pieces[1:]\n        recipient = Account._by_name(recipient_name)\n\n    gold_recipient = recipient or buyer\n    with gold_recipient.get_read_modify_write_lock() as lock:\n        gold_recipient.update_from_cache(lock)\n\n        if goldtype in ('onetime', 'autorenew'):\n            subtract_gold_days(buyer, days)\n\n        elif goldtype == 'creddits':\n            subtract_gold_creddits(buyer, months)\n\n        elif goldtype == 'gift':\n            subtract_gold_days(recipient, days)\n            subject = 'your gifted gold has been reversed'\n            message = 'sorry, but the payment was reversed'\n            send_system_message(recipient, subject, message)\n    update_gold_transaction(transaction_id, 'reversed')\n\n\ndef cancel_stripe_subscription(customer_id):\n    customer = stripe.Customer.retrieve(customer_id)\n    if hasattr(customer, 'deleted'):\n        return customer\n    customer.delete()\n    return customer\n\n\ndef subscr_pm(pennies, months, new_subscr=True):\n    price = \"$%0.2f\" % (pennies/100.0)\n    if new_subscr:\n        if months % 12 == 0:\n            message = _(\"You have created a yearly Reddit Gold subscription \"\n                \"for %(price)s per year.\\n\\nThis subscription will renew \"\n                \"automatically yearly until you cancel. You may cancel your \"\n                \"subscription at any time by visiting %(subscr_url)s.\\n\\n\")\n        else:\n            message = _(\"You have created a monthly Reddit Gold subscription \"\n                \"for %(price)s per month.\\n\\nThis subscription will renew \"\n                \"automatically monthly until you cancel. You may cancel your \"\n                \"subscription at any time by visiting %(subscr_url)s.\\n\\n\")\n    else:\n        if months == 1:\n            message = _(\"Your Reddit Gold subscription has been renewed \"\n                \"for 1 month for %(price)s.\\n\\nThis subscription will renew \"\n                \"automatically monthly until you cancel. You may cancel your \"\n                \"subscription at any time by visiting %(subscr_url)s.\\n\\n\")\n        else:\n            message = _(\"Your Reddit Gold subscription has been renewed \"\n                \"for 1 year for %(price)s.\\n\\nThis subscription will renew \"\n                \"automatically yearly until you cancel. You may cancel your \"\n                \"subscription at any time by visiting %(subscr_url)s.\\n\\n\")\n\n    subject = _(\"Reddit Gold Subscription\")\n    message += _(\"If you cancel, you will not be billed for any additional \"\n        \"months of service, and service will continue until the end of the \"\n        \"billing period. If you cancel, you will not receive a refund for any \"\n        \"service already paid for.\\n\\nIf you have any questions, please \"\n        \"contact %(gold_email)s.\")\n\n    message %= {\n        \"price\": price,\n        \"subscr_url\": \"https://www.reddit.com/gold/subscription\",\n        \"gold_email\": g.goldsupport_email,\n    }\n    return subject, message\n"
  },
  {
    "path": "r2/r2/controllers/listingcontroller.py",
    "content": "# -*- coding: utf-8 -*-\n# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport urllib\n\nfrom oauth2 import require_oauth2_scope\nfrom reddit_base import RedditController, base_listing, paginated_listing\n\nfrom r2.models import *\nfrom r2.models.query_cache import CachedQuery, MergedCachedQuery\nfrom r2.config.extensions import is_api\nfrom r2.lib.filters import _force_unicode\nfrom r2.lib.jsontemplates import get_usertrophies\nfrom r2.lib.pages import *\nfrom r2.lib.pages.things import wrap_links\nfrom r2.lib.menus import TimeMenu, CommentsTimeMenu, SortMenu, RecSortMenu, ProfileSortMenu\nfrom r2.lib.menus import ControversyTimeMenu, ProfileOverviewTimeMenu, menu, QueryButton\nfrom r2.lib.rising import get_rising, normalized_rising\nfrom r2.lib.wrapped import Wrapped\nfrom r2.lib.normalized_hot import normalized_hot\nfrom r2.lib.db.thing import Query, Merge, Relations\nfrom r2.lib.db import queries\nfrom r2.lib.strings import Score\nfrom r2.lib.template_helpers import add_sr\nfrom r2.lib.csrf import csrf_exempt\nfrom r2.lib.utils import (\n    extract_user_mentions,\n    iters,\n    query_string,\n    timeago,\n    to36,\n    trunc_string,\n    precise_format_timedelta,\n)\nfrom r2.lib import hooks, organic, trending\nfrom r2.lib.memoize import memoize\nfrom r2.lib.validator import *\nimport socket\n\nfrom api_docs import api_doc, api_section\n\nfrom pylons import app_globals as g\nfrom pylons.i18n import _\n\nfrom datetime import timedelta\nimport random\nfrom functools import partial\n\nclass ListingController(RedditController):\n    \"\"\"Generalized controller for pages with lists of links.\"\"\"\n\n\n    # toggle skipping of links based on the users' save/hide/vote preferences\n    skip = True\n\n    # allow stylesheets on listings\n    allow_stylesheets = True\n\n    # toggle sending origin-only referrer headers on cross-domain navigation\n    private_referrer = True\n\n    # toggles showing numbers\n    show_nums = True\n\n    # any text that should be shown on the top of the page\n    infotext = None\n    infotext_class = None\n\n    # builder class to use to generate the listing. if none, we'll try\n    # to figure it out based on the query type\n    builder_cls = None\n\n    # page title\n    title_text = ''\n\n    # login box, subreddit box, submit box, etc, visible\n    show_sidebar = True\n    show_chooser = False\n    suppress_reply_buttons = False\n\n    # class (probably a subclass of Reddit) to use to render the page.\n    render_cls = Reddit\n\n    # class for suggestions next to \"next/prev\" buttons\n    next_suggestions_cls = None\n\n    #extra parameters to send to the render_cls constructor\n    render_params = {}\n    extra_page_classes = ['listing-page']\n\n    @property\n    def menus(self):\n        \"\"\"list of menus underneat the header (e.g., sort, time, kind,\n        etc) to be displayed on this listing page\"\"\"\n        return []\n\n    def can_send_referrer(self):\n        \"\"\"Return whether links within this listing may have full referrers\"\"\"\n        if not self.private_referrer:\n            return c.site.allows_referrers\n        return False\n\n    def build_listing(self, num, after, reverse, count, sr_detail=None, **kwargs):\n        \"\"\"uses the query() method to define the contents of the\n        listing and renders the page self.render_cls(..).render() with\n        the listing as contents\"\"\"\n        self.num = num\n        self.count = count\n        self.after = after\n        self.reverse = reverse\n        self.sr_detail = sr_detail\n\n        if c.site.login_required and not c.user_is_loggedin:\n            raise UserRequiredException\n\n        self.query_obj = self.query()\n        self.builder_obj = self.builder()\n\n        # Don't leak info about things the user can't view\n        if after and not self.builder_obj.valid_after(after):\n            listing_name = self.__class__.__name__\n            g.stats.event_count(\"listing.invalid_after\", listing_name)\n            self.abort403()\n\n        self.listing_obj = self.listing()\n\n        content = self.content()\n        return self.render_cls(content=content,\n                               page_classes=self.extra_page_classes,\n                               show_sidebar=self.show_sidebar,\n                               show_chooser=self.show_chooser,\n                               show_newsletterbar=True,\n                               nav_menus=self.menus,\n                               title=self.title(),\n                               infotext=self.infotext,\n                               infotext_class=self.infotext_class,\n                               robots=getattr(self, \"robots\", None),\n                               **self.render_params).render()\n\n    def content(self):\n        \"\"\"Renderable object which will end up as content of the render_cls\"\"\"\n        return self.listing_obj\n\n    def query(self):\n        \"\"\"Query to execute to generate the listing\"\"\"\n        raise NotImplementedError\n\n    def builder(self):\n        #store the query itself so it can be used elsewhere\n        if self.builder_cls:\n            builder_cls = self.builder_cls\n        elif isinstance(self.query_obj, Query):\n            builder_cls = QueryBuilder\n        elif isinstance(self.query_obj, g.search.SearchQuery):\n            builder_cls = SearchBuilder\n        elif isinstance(self.query_obj, iters):\n            builder_cls = IDBuilder\n        elif isinstance(self.query_obj, (queries.CachedResults, queries.MergedCachedResults)):\n            builder_cls = IDBuilder\n        elif isinstance(self.query_obj, (CachedQuery, MergedCachedQuery)):\n            builder_cls = IDBuilder\n\n        builder = builder_cls(\n            self.query_obj,\n            num=self.num,\n            skip=self.skip,\n            after=self.after,\n            count=self.count,\n            reverse=self.reverse,\n            keep_fn=self.keep_fn(),\n            sr_detail=self.sr_detail,\n            wrap=self.builder_wrapper,\n            prewrap_fn=self.prewrap_fn(),\n        )\n        return builder\n\n    def keep_fn(self):\n        def keep(item):\n            wouldkeep = item.keep_item(item)\n\n            if isinstance(c.site, AllSR):\n                if not item.subreddit.discoverable:\n                    return False\n            elif isinstance(c.site, FriendsSR):\n                if item.author._deleted or item.author._spam:\n                    return False\n\n            if getattr(item, \"promoted\", None) is not None:\n                return False\n\n            if item._deleted and not c.user_is_admin:\n                return False\n\n            return wouldkeep\n\n        return keep\n\n    def prewrap_fn(self):\n        return\n\n    def listing(self):\n        \"\"\"Listing to generate from the builder\"\"\"\n        if (getattr(c.site, \"_id\", -1) == Subreddit.get_promote_srid() and\n            not c.user_is_sponsor):\n            abort(403, 'forbidden')\n\n        model = LinkListing(self.builder_obj, show_nums=self.show_nums)\n\n        suggestions = None\n        if c.render_style == \"html\" and self.next_suggestions_cls:\n            suggestions = self.next_suggestions_cls()\n\n        pane = model.listing(next_suggestions=suggestions)\n\n        # Indicate that the comment tree wasn't built for comments\n        for i in pane:\n            if hasattr(i, 'full_comment_path'):\n                i.child = None\n            i.suppress_reply_buttons = self.suppress_reply_buttons\n\n        return pane\n\n    def title(self):\n        \"\"\"Page <title>\"\"\"\n        return _(self.title_text) + \" : \" + c.site.name\n\n    def rightbox(self):\n        \"\"\"Contents of the right box when rendering\"\"\"\n        pass\n\n    builder_wrapper = staticmethod(default_thing_wrapper())\n\n    @require_oauth2_scope(\"read\")\n    @base_listing\n    def GET_listing(self, **env):\n        if isinstance(c.site, ModSR):\n            VNotInTimeout().run(action_name=\"pageview\",\n                details_text=\"mod_subreddit\")\n        if self.can_send_referrer():\n            c.referrer_policy = \"always\"\n        return self.build_listing(**env)\n\nlisting_api_doc = partial(\n    api_doc,\n    section=api_section.listings,\n    extends=ListingController.GET_listing,\n    notes=[paginated_listing.doc_note],\n    supports_rss=True,\n)\n\n\nclass SubredditListingController(ListingController):\n    private_referrer = False\n\n    def _build_og_title(self, max_length=256):\n        sr_fragment = \"/r/\" + c.site.name\n        title = c.site.title.strip()\n        if not title:\n            return trunc_string(sr_fragment, max_length)\n\n        if sr_fragment in title:\n            return _force_unicode(trunc_string(title, max_length))\n\n        # We'd like to always show the whole subreddit name, so let's\n        # truncate the title while still ensuring the entire thing is under\n        # the limit.\n        # This doesn't handle `max_length`s shorter than `sr_fragment`.\n        # Unknown what the behavior should be, but realistically it shouldn't\n        # happen, since this is scoped pretty small.\n        max_title_length = max_length - len(u\" • %s\" % sr_fragment)\n        title = trunc_string(title, max_title_length)\n\n        return u\"%s • %s\" % (_force_unicode(title), sr_fragment)\n\n    def canonical_link(self):\n        \"\"\"Return the canonical link of the subreddit.\n\n        Ordinarily canonical links are created using request.url.\n        In the case of subreddits, we perform a bit of magic to strip the\n        subreddit path from the url. This means that a path like:\n\n        https:///www.reddit.com/r/hiphopheads/\n\n        will instead show:\n\n        https://www.reddit.com/\n\n        See SubredditMiddleware for more information.\n\n        This method constructs our url from scratch given other information.\n        \"\"\"\n        return add_sr('/', force_https=True)\n\n    def _build_og_description(self):\n        description = c.site.public_description.strip()\n        if not description:\n            description = _(g.short_description)\n        return _force_unicode(trunc_string(description, MAX_DESCRIPTION_LENGTH))\n\n    @property\n    def render_params(self):\n        render_params = {}\n\n        if isinstance(c.site, DefaultSR):\n            render_params.update({'show_locationbar': True})\n        else:\n            if not c.user_is_loggedin:\n                # This data is only for scrapers, which shouldn't be logged in.\n                twitter_card = {\n                    \"site\": \"reddit\",\n                    \"card\": \"summary\",\n                    \"title\": self._build_og_title(max_length=70),\n                    # Twitter will fall back to any defined OpenGraph\n                    # attributes, so we don't need to define\n                    # 'twitter:image' or 'twitter:description'.\n                }\n                hook = hooks.get_hook('subreddit_listing.twitter_card')\n                hook.call(tags=twitter_card, sr_name=c.site.name)\n\n                render_params.update({\n                    \"og_data\": {\n                        \"site_name\": \"reddit\",\n                        \"title\": self._build_og_title(),\n                        \"image\": static('icon.png', absolute=True),\n                        \"description\": self._build_og_description(),\n                    },\n                    \"twitter_card\": twitter_card,\n                })\n\n        # event target for screenviews\n        event_target = {\n            'target_type': 'listing',\n        }\n        if not isinstance(c.site, FakeSubreddit):\n            event_target['target_fullname'] = c.site._fullname\n            event_target['target_id'] = c.site._id\n        if hasattr(self, 'sort'):\n            event_target['target_sort'] = self.sort\n        elif hasattr(self, 'where'):\n            event_target['target_sort'] = self.where\n        if hasattr(self, 'time'):\n            event_target['target_filter_time'] = self.time\n        if self.after:\n            event_target['target_count'] = self.count\n            if self.reverse:\n                event_target['target_before'] = self.after._fullname\n            else:\n                event_target['target_after'] = self.after._fullname\n        render_params['extra_js_config'] = {'event_target': event_target}\n\n        render_params['canonical_link'] = self.canonical_link()\n\n        return render_params\n\n\nclass ListingWithPromos(SubredditListingController):\n    show_organic = False\n\n    def make_requested_ad(self, requested_ad):\n        try:\n            link = Link._by_fullname(requested_ad, data=True)\n        except NotFound:\n            self.abort404()\n\n        is_link_creator = c.user_is_loggedin and (c.user._id == link.author_id)\n        if (not (is_link_creator or c.user_is_sponsor) and\n                not promote.is_live_on_sr(link, c.site)):\n            self.abort403()\n\n        res = wrap_links([link._fullname], wrapper=self.builder_wrapper,\n                         skip=False)\n        res.parent_name = \"promoted\"\n        if res.things:\n            return res\n\n    def make_single_ad(self):\n        keywords = promote.keywords_from_context(c.user, c.site)\n        if keywords:\n            return SpotlightListing(show_promo=c.site.allow_ads, keywords=keywords,\n                                    navigable=False).listing()\n\n    def make_spotlight(self):\n        \"\"\"Build the Spotlight.\n\n        The frontpage gets a Spotlight box that contains promoted and organic\n        links from the user's subscribed subreddits and promoted links targeted\n        to the frontpage. If the user has disabled ads promoted links will not\n        be shown. Promoted links are requested from the adserver client-side.\n\n        \"\"\"\n\n        organic_fullnames = organic.organic_links(c.user)\n        promoted_links = []\n\n        show_promo = False\n        keywords = []\n        can_show_promo = not c.user.pref_hide_ads or not c.user.gold and c.site.allow_ads\n        try_show_promo = ((c.user_is_loggedin and random.random() > 0.5) or\n                          not c.user_is_loggedin)\n\n        if can_show_promo and try_show_promo:\n            keywords = promote.keywords_from_context(c.user, c.site)\n            if keywords:\n                show_promo = True\n\n        def organic_keep_fn(item):\n            base_keep_fn = super(ListingWithPromos, self).keep_fn()\n            would_keep = base_keep_fn(item)\n            return would_keep and item.fresh\n\n        random.shuffle(organic_fullnames)\n        organic_fullnames = organic_fullnames[:10]\n        b = IDBuilder(organic_fullnames,\n                      wrap=self.builder_wrapper,\n                      keep_fn=organic_keep_fn,\n                      skip=True)\n        organic_links = b.get_items()[0]\n\n        has_subscribed = c.user.has_subscribed\n        interestbar_prob = g.live_config['spotlight_interest_sub_p'\n                                         if has_subscribed else\n                                         'spotlight_interest_nosub_p']\n        interestbar = InterestBar(has_subscribed)\n\n        s = SpotlightListing(organic_links=organic_links,\n                             interestbar=interestbar,\n                             interestbar_prob=interestbar_prob,\n                             show_promo=show_promo,\n                             keywords=keywords,\n                             max_num = self.listing_obj.max_num,\n                             max_score = self.listing_obj.max_score).listing()\n        return s\n\n    def content(self):\n        # only send a spotlight listing for HTML rendering\n        if c.render_style == \"html\":\n            spotlight = None\n            show_sponsors = not c.user.pref_hide_ads or not c.user.gold\n            show_organic = self.show_organic and c.user.pref_organic\n            on_frontpage = isinstance(c.site, DefaultSR)\n            requested_ad = request.GET.get('ad')\n\n            if on_frontpage:\n                self.extra_page_classes = \\\n                    self.extra_page_classes + ['front-page']\n\n            if requested_ad:\n                spotlight = self.make_requested_ad(requested_ad)\n            elif on_frontpage and show_organic:\n                spotlight = self.make_spotlight()\n            elif show_sponsors:\n                spotlight = self.make_single_ad()\n\n            self.spotlight = spotlight\n\n            if spotlight:\n                return PaneStack([spotlight, self.listing_obj],\n                                 css_class='spacer')\n        return self.listing_obj\n\n\nclass HotController(ListingWithPromos):\n    where = 'hot'\n    extra_page_classes = ListingController.extra_page_classes + ['hot-page']\n    show_chooser = True\n    next_suggestions_cls = ListingSuggestions\n    show_organic = True\n\n    def query(self):\n\n        if isinstance(c.site, DefaultSR):\n            sr_ids = Subreddit.user_subreddits(c.user)\n            return normalized_hot(sr_ids)\n        elif isinstance(c.site, MultiReddit):\n            return normalized_hot(c.site.kept_sr_ids, obey_age_limit=False,\n                                  ageweight=c.site.ageweight)\n        else:\n            sticky_fullnames = c.site.sticky_fullnames\n            if sticky_fullnames:\n                # make a copy of the list so we're not inadvertently modifying\n                # the subreddit's list of sticky fullnames\n                link_list = sticky_fullnames[:]\n                \n                wrapped = wrap_links(link_list,\n                                     wrapper=self.builder_wrapper,\n                                     keep_fn=self.keep_fn(),\n                                     skip=True)\n                # add all other items and decrement count if sticky is visible\n                if wrapped.things:\n                    link_list += [l for l in c.site.get_links('hot', 'all')\n                                    if l not in sticky_fullnames]\n                    if not self.after:\n                        self.count -= len(sticky_fullnames)\n                        self.num += len(sticky_fullnames)\n                    return link_list\n            \n            # no sticky or sticky hidden\n            return c.site.get_links('hot', 'all')\n\n    @classmethod\n    def trending_info(cls):\n        if not c.user.pref_show_trending:\n            return None\n\n        trending_data = trending.get_trending_subreddits()\n\n        if not trending_data:\n            return None\n\n        link = Link._byID(trending_data['link_id'], data=True, stale=True)\n        return {\n            'subreddit_names': trending_data['subreddit_names'],\n            'comment_url': trending_data['permalink'],\n            'comment_count': link.num_comments,\n        }\n\n    def content(self):\n        content = super(HotController, self).content()\n\n        if c.render_style == \"html\":\n            stack = None\n\n            hot_hook = hooks.get_hook(\"hot.get_content\")\n            hot_pane = hot_hook.call_until_return(controller=self)\n\n            if hot_pane:\n                stack = [\n                    self.spotlight,\n                    hot_pane,\n                    self.listing_obj\n                ]\n            elif isinstance(c.site, DefaultSR) and not self.listing_obj.prev:\n                trending_info = self.trending_info()\n                if trending_info:\n                    stack = [\n                        self.spotlight,\n                        TrendingSubredditsBar(**trending_info),\n                        self.listing_obj,\n                    ]\n\n            if stack:\n                return PaneStack(filter(None, stack), css_class='spacer')\n\n        return content\n\n    def title(self):\n        return c.site.title\n\n    @require_oauth2_scope(\"read\")\n    @listing_api_doc(uri='/hot', uses_site=True)\n    def GET_listing(self, **env):\n        self.infotext = request.GET.get('deactivated') and strings.user_deactivated\n        return ListingController.GET_listing(self, **env)\n\nclass NewController(ListingWithPromos):\n    where = 'new'\n    title_text = _('newest submissions')\n    extra_page_classes = ListingController.extra_page_classes + ['new-page']\n    show_chooser = True\n    next_suggestions_cls = ListingSuggestions\n\n    def keep_fn(self):\n        def keep(item):\n            if item.promoted is not None:\n                return False\n            else:\n                return item.keep_item(item)\n        return keep\n\n    def query(self):\n        return c.site.get_links('new', 'all')\n\n    @csrf_exempt\n    def POST_listing(self, **env):\n        # Redirect to GET mode in case of any legacy requests\n        return self.redirect(request.fullpath)\n\n    @require_oauth2_scope(\"read\")\n    @listing_api_doc(uri='/new', uses_site=True)\n    def GET_listing(self, **env):\n        return ListingController.GET_listing(self, **env)\n\nclass RisingController(NewController):\n    where = 'rising'\n    title_text = _('rising submissions')\n    extra_page_classes = ListingController.extra_page_classes + ['rising-page']\n\n    def query(self):\n        if isinstance(c.site, DefaultSR):\n            sr_ids = Subreddit.user_subreddits(c.user)\n            return normalized_rising(sr_ids)\n        elif isinstance(c.site, MultiReddit):\n            return normalized_rising(c.site.kept_sr_ids)\n\n        return get_rising(c.site)\n\nclass BrowseController(ListingWithPromos):\n    where = 'browse'\n    show_chooser = True\n    next_suggestions_cls = ListingSuggestions\n\n    def keep_fn(self):\n        \"\"\"For merged time-listings, don't show items that are too old\n           (this can happen when mr_top hasn't run in a while)\"\"\"\n        if self.time != 'all' and c.default_sr:\n            oldest = timeago('1 %s' % (str(self.time),))\n            def keep(item):\n                if isinstance(c.site, AllSR):\n                    if not item.subreddit.discoverable:\n                        return False\n                return item._date > oldest and item.keep_item(item)\n            return keep\n        else:\n            return ListingController.keep_fn(self)\n\n    @property\n    def menus(self):\n        return [ControversyTimeMenu(default = self.time)]\n\n    def query(self):\n        return c.site.get_links(self.sort, self.time)\n\n    @csrf_exempt\n    @validate(t = VMenu('sort', ControversyTimeMenu))\n    def POST_listing(self, sort, t, **env):\n        # VMenu validator will save the value of time before we reach this\n        # point. Now just redirect to GET mode.\n        return self.redirect(\n            request.fullpath + query_string(dict(sort=sort, t=t)))\n\n    @require_oauth2_scope(\"read\")\n    @validate(t = VMenu('sort', ControversyTimeMenu))\n    @listing_api_doc(uri='/{sort}', uri_variants=['/top', '/controversial'],\n                     uses_site=True)\n    def GET_listing(self, sort, t, **env):\n        self.sort = sort\n        if sort == 'top':\n            self.title_text = _('top scoring links')\n            self.extra_page_classes = \\\n                self.extra_page_classes + ['top-page']\n        elif sort == 'controversial':\n            self.title_text = _('most controversial links')\n            self.extra_page_classes = \\\n                self.extra_page_classes + ['controversial-page']\n        else:\n            # 'sort' is forced to top/controversial by routing.py,\n            # but in case something has gone wrong...\n            abort(404)\n        self.time = t\n        return ListingController.GET_listing(self, **env)\n\n\nclass AdsController(SubredditListingController):\n    where = 'ads'\n    builder_cls = CampaignBuilder\n    title_text = _('promoted links')\n\n    @property\n    def infotext(self):\n        infotext = _(\"want to advertise? [click here!](%(link)s)\")\n        if c.user.pref_show_promote or c.user_is_sponsor:\n            return infotext % {'link': '/promoted'}\n        else:\n            return infotext % {'link': '/advertising'}\n\n    def keep_fn(self):\n        def keep(item):\n            if item._fullname in self.promos:\n                return False\n            if item.promoted and not item._deleted:\n                self.promos.add(item._fullname)\n                return True\n            return False\n        return keep\n\n    def query(self):\n        try:\n            return c.site.get_live_promos()\n        except NotImplementedError:\n            self.abort404()\n\n    def listing(self):\n        listing = ListingController.listing(self)\n        return listing\n\n    def GET_listing(self, *a, **kw):\n        self.promos = set()\n        if not c.site.allow_ads:\n            self.abort404()\n        return SubredditListingController.GET_listing(self, *a, **kw)\n\n\nclass RandomrisingController(ListingWithPromos):\n    where = 'randomrising'\n    title_text = _('you\\'re really bored now, eh?')\n    next_suggestions_cls = ListingSuggestions\n\n    def query(self):\n        links = get_rising(c.site)\n\n        if not links:\n            # just pull from the new page if the rising page isn't\n            # populated for some reason\n            links = c.site.get_links('new', 'all')\n            if isinstance(links, Query):\n                links._limit = 200\n                links = [x._fullname for x in links]\n\n        links = list(links)\n        random.shuffle(links)\n\n        return links\n\nclass ByIDController(ListingController):\n    title_text = _('API')\n    skip = False\n\n    def query(self):\n        return self.names\n\n    @require_oauth2_scope(\"read\")\n    @validate(links=VByName(\"names\", thing_cls=Link,\n                            ignore_missing=True, multiple=True))\n    @api_doc(api_section.listings, uri='/by_id/{names}')\n    def GET_listing(self, links, **env):\n        \"\"\"Get a listing of links by fullname.\n\n        `names` is a list of fullnames for links separated by commas or spaces.\n\n        \"\"\"\n        if not links:\n            return self.abort404()\n        self.names = [l._fullname for l in links]\n        return ListingController.GET_listing(self, **env)\n\n\nclass UserController(ListingController):\n    render_cls = ProfilePage\n    show_nums = False\n    skip = True\n\n    @property\n    def menus(self):\n        res = []\n        if (self.where in ('overview', 'submitted', 'comments')):\n            res.append(ProfileSortMenu(default = self.sort))\n            if self.sort not in (\"hot\", \"new\"):\n                if self.where == \"comments\":\n                    res.append(CommentsTimeMenu(default = self.time))\n                elif self.where == \"overview\":\n                    res.append(ProfileOverviewTimeMenu(default = self.time))\n                else:\n                    res.append(TimeMenu(default = self.time))\n        if self.where == 'saved' and c.user.gold:\n            srnames = LinkSavesBySubreddit.get_saved_subreddits(self.vuser)\n            srnames += CommentSavesBySubreddit.get_saved_subreddits(self.vuser)\n            srs = Subreddit._by_name(set(srnames), stale=True)\n            srnames = [name for name, sr in srs.iteritems()\n                            if sr.can_view(c.user)]\n            srnames = sorted(set(srnames), key=lambda name: name.lower())\n            if len(srnames) > 1:\n                sr_buttons = [QueryButton(_('all'), None, query_param='sr',\n                                        css_class='primary')]\n                for srname in srnames:\n                    sr_buttons.append(QueryButton(srname, srname, query_param='sr'))\n                base_path = '/user/%s/saved' % self.vuser.name\n                if self.savedcategory:\n                    base_path += '/%s' % urllib.quote(self.savedcategory)\n                sr_menu = NavMenu(sr_buttons, base_path=base_path,\n                                  title=_('filter by subreddit'),\n                                  type='lightdrop')\n                res.append(sr_menu)\n            categories = LinkSavesByCategory.get_saved_categories(self.vuser)\n            categories += CommentSavesByCategory.get_saved_categories(self.vuser)\n            categories = sorted(set(categories))\n            if len(categories) >= 1:\n                cat_buttons = [NavButton(_('all'), '/', css_class='primary')]\n                for cat in categories:\n                    cat_buttons.append(NavButton(cat,\n                                                 urllib.quote(cat),\n                                                 use_params=True))\n                base_path = '/user/%s/saved/' % self.vuser.name\n                cat_menu = NavMenu(cat_buttons, base_path=base_path,\n                                   title=_('filter by category'),\n                                   type='lightdrop')\n                res.append(cat_menu)\n        elif (self.where == 'gilded' and\n                (c.user == self.vuser or c.user_is_admin)):\n            path = '/user/%s/gilded/' % self.vuser.name\n            buttons = [NavButton(_(\"gildings received\"), dest='/'),\n                       NavButton(_(\"gildings given\"), dest='/given')]\n            res.append(NavMenu(buttons, base_path=path, type='flatlist'))\n\n        return res\n\n    def title(self):\n        titles = {'overview': _(\"overview for %(user)s\"),\n                  'comments': _(\"comments by %(user)s\"),\n                  'submitted': _(\"submitted by %(user)s\"),\n                  'gilded': _(\"gilded by %(user)s\"),\n                  'upvoted': _(\"upvoted by %(user)s\"),\n                  'downvoted': _(\"downvoted by %(user)s\"),\n                  'saved': _(\"saved by %(user)s\"),\n                  'hidden': _(\"hidden by %(user)s\"),\n                  'promoted': _(\"promoted by %(user)s\")}\n        if self.where == 'gilded' and self.show == 'given':\n            return _(\"gildings given by %(user)s\") % {'user': self.vuser.name}\n\n        title = titles.get(self.where, _('profile for %(user)s')) \\\n            % dict(user = self.vuser.name, site = c.site.name)\n        return title\n\n    def keep_fn(self):\n        def keep(item):\n            if self.where == 'promoted':\n                return bool(getattr(item, \"promoted\", None))\n\n            if item._deleted and not c.user_is_admin:\n                return False\n\n            if c.user == self.vuser:\n                if not item.likes and self.where == 'upvoted':\n                    g.stats.simple_event(\"vote.missing_votes_by_account\")\n                    return False\n                if item.likes is not False and self.where == 'downvoted':\n                    g.stats.simple_event(\"vote.missing_votes_by_account\")\n                    return False\n                if self.where == 'saved' and not item.saved:\n                    return False\n\n            if (self.time != 'all' and\n                item._date <= utils.timeago('1 %s' % str(self.time))):\n                return False\n\n            if self.where == 'gilded' and item.gildings <= 0:\n                return False\n\n            if self.where == 'deleted' and not item._deleted:\n                return False\n\n            is_promoted = getattr(item, \"promoted\", None) is not None\n            if self.where != 'saved' and is_promoted:\n                return False\n\n            return True\n        return keep\n\n    def query(self):\n        q = None\n        if self.where == 'overview':\n            q = queries.get_overview(self.vuser, self.sort, self.time)\n\n        elif self.where == 'comments':\n            q = queries.get_comments(self.vuser, self.sort, self.time)\n\n        elif self.where == 'submitted':\n            q = queries.get_submitted(self.vuser, self.sort, self.time)\n\n        elif self.where == 'gilded':\n            if self.show == 'given':\n                q = queries.get_user_gildings(self.vuser)\n            else:\n                q = queries.get_gilded_user(self.vuser)\n\n        elif self.where in ('upvoted', 'downvoted'):\n            if self.where == 'upvoted':\n                q = queries.get_liked(self.vuser)\n            else:\n                q = queries.get_disliked(self.vuser)\n\n        elif self.where == 'hidden':\n            q = queries.get_hidden(self.vuser)\n\n        elif self.where == 'saved':\n            if not self.savedcategory and c.user.gold:\n                self.builder_cls = SavedBuilder\n            sr_id = self.savedsr._id if self.savedsr else None\n            q = queries.get_saved(self.vuser, sr_id,\n                                  category=self.savedcategory)\n        elif self.where == 'actions':\n            if not votes_visible(self.vuser):\n                q = queries.get_overview(self.vuser, self.sort, self.time)\n            else:\n                q = queries.get_user_actions(self.vuser, 'new', 'all')\n                self.builder_cls = ActionBuilder\n\n        elif c.user_is_sponsor and self.where == 'promoted':\n            q = queries.get_promoted_links(self.vuser._id)\n\n        if q is None:\n            return self.abort404()\n\n        return q\n\n    @require_oauth2_scope(\"history\")\n    @validate(vuser = VExistingUname('username', allow_deleted=True),\n              sort = VMenu('sort', ProfileSortMenu, remember = False),\n              time = VMenu('t', TimeMenu, remember = False),\n              show=VOneOf('show', ('given',)))\n    @listing_api_doc(section=api_section.users, uri='/user/{username}/{where}',\n                     uri_variants=['/user/{username}/' + where for where in [\n                                       'overview', 'submitted', 'comments',\n                                       'upvoted', 'downvoted', 'hidden',\n                                       'saved', 'gilded']])\n    def GET_listing(self, where, vuser, sort, time, show, **env):\n        # the validator will ensure that vuser is a valid account\n        if not vuser:\n            return self.abort404()\n\n        if (vuser.in_timeout and\n                vuser != c.user and\n                not c.user_is_admin and\n                not vuser.timeout_expiration):\n            errpage = InterstitialPage(\n                _(\"suspended\"),\n                content=BannedUserInterstitial(),\n            )\n            request.environ['usable_error_content'] = errpage.render()\n            return self.abort403()\n\n        if (c.user_is_loggedin and\n                not c.user_is_admin and\n                vuser._id in c.user.enemies):\n            errpage = InterstitialPage(\n                _(\"blocked\"),\n                content=UserBlockedInterstitial(),\n            )\n            request.environ['usable_error_content'] = errpage.render()\n            return self.abort403()\n\n        # continue supporting /liked and /disliked paths for API clients\n        # but 301 redirect non-API users to the new location\n        changed_wheres = {\"liked\": \"upvoted\", \"disliked\": \"downvoted\"}\n        new_where = changed_wheres.get(where)\n        if new_where:\n            where = new_where\n            if not is_api():\n                path = \"/\".join((\"/user\", vuser.name, where))\n                query_string = request.environ.get('QUERY_STRING')\n                if query_string:\n                    path += \"?\" + query_string\n                return self.redirect(path, code=301)\n        \n        self.where = where\n        self.sort = sort\n        self.time = time\n        self.show = show\n\n        # only allow admins to view deleted users\n        if vuser._deleted and not c.user_is_admin:\n            errpage = InterstitialPage(\n                _(\"deleted\"),\n                content=DeletedUserInterstitial(),\n            )\n            request.environ['usable_error_content'] = errpage.render()\n            return self.abort404()\n\n        if c.user_is_admin:\n            c.referrer_policy = \"always\"\n\n        if self.sort in ('hot', 'new'):\n            self.time = 'all'\n\n        # hide spammers profile pages\n        if vuser._spam and not vuser.banned_profile_visible:\n            if (not c.user_is_loggedin or\n                    not (c.user._id == vuser._id or\n                         c.user_is_admin or\n                         c.user_is_sponsor and where == \"promoted\")):\n                return self.abort404()\n\n        if where in ('upvoted', 'downvoted') and not votes_visible(vuser):\n            return self.abort403()\n\n        if ((where in ('saved', 'hidden') or\n                (where == 'gilded' and show == 'given')) and\n                not (c.user_is_loggedin and c.user._id == vuser._id) and\n                not c.user_is_admin):\n            return self.abort403()\n\n        if where == 'saved':\n            self.show_chooser = True\n            category = VSavedCategory('category').run(env.get('category'))\n            srname = request.GET.get('sr')\n            if srname and c.user.gold:\n                try:\n                    sr = Subreddit._by_name(srname)\n                except NotFound:\n                    sr = None\n            else:\n                sr = None\n            if category and not c.user.gold:\n                category = None\n            self.savedsr = sr\n            self.savedcategory = category\n\n        self.vuser = vuser\n\n        c.profilepage = True\n        self.suppress_reply_buttons = True\n\n        if vuser.pref_hide_from_robots:\n            self.robots = 'noindex,nofollow'\n\n        return ListingController.GET_listing(self, **env)\n\n    @property\n    def render_params(self):\n        render_params = {'user': self.vuser}\n\n        # event target for screenviews\n        event_target = {\n            'target_type': 'account',\n            'target_fullname': self.vuser._fullname,\n            'target_id': self.vuser._id,\n            'target_name': self.vuser.name,\n            'target_sort': self.sort,\n            'target_filter_time': self.time,\n        }\n        if self.after:\n            event_target['target_count'] = self.count\n            if self.reverse:\n                event_target['target_before'] = self.after._fullname\n            else:\n                event_target['target_after'] = self.after._fullname\n        render_params['extra_js_config'] = {'event_target': event_target}\n\n        return render_params\n\n    @require_oauth2_scope(\"read\")\n    @validate(vuser = VExistingUname('username'))\n    @api_doc(section=api_section.users, uri='/user/{username}/about')\n    def GET_about(self, vuser):\n        \"\"\"Return information about the user, including karma and gold status.\"\"\"\n        if (not is_api() or\n                not vuser or\n                (vuser._spam and vuser != c.user)):\n            return self.abort404()\n        return Reddit(content = Wrapped(vuser)).render()\n\n    def GET_saved_redirect(self):\n        if not c.user_is_loggedin:\n            abort(404)\n\n        dest = \"/\".join((\"/user\", c.user.name, \"saved\"))\n        extension = request.environ.get('extension')\n        if extension:\n            dest = \".\".join((dest, extension))\n        query_string = request.environ.get('QUERY_STRING')\n        if query_string:\n            dest += \"?\" + query_string\n        return self.redirect(dest)\n\n    @validate(VUser())\n    def GET_rel_user_redirect(self, rest=\"\"):\n        url = \"/user/%s/%s\" % (c.user.name, rest)\n        if request.query_string:\n            url += \"?\" + request.query_string\n        return self.redirect(url, code=302)\n\n    @validate(\n        user=VAccountByName('username'),\n    )\n    def GET_trophies(self, user):\n        \"\"\"Return a list of trophies for the a given user.\"\"\"\n        if not is_api():\n            return self.abort404()\n        return self.api_wrapper(get_usertrophies(user))\n\n\nclass MessageController(ListingController):\n    show_nums = False\n    render_cls = MessagePage\n    allow_stylesheets = False\n    # note: this intentionally replaces the listing-page class which doesn't\n    # conceptually fit for styling these pages.\n    extra_page_classes = ['messages-page']\n\n    @property\n    def show_sidebar(self):\n        if c.default_sr and not isinstance(c.site, (ModSR, MultiReddit)):\n            return False\n\n        return self.where in (\"moderator\", \"multi\")\n\n    @property\n    def menus(self):\n        if c.default_sr and self.where in ('inbox', 'messages', 'comments',\n                          'selfreply', 'unread', 'mentions'):\n            buttons = [NavButton(_(\"all\"), \"inbox\"),\n                       NavButton(_(\"unread\"), \"unread\"),\n                       NavButton(plurals.messages, \"messages\"),\n                       NavButton(_(\"comment replies\"), 'comments'),\n                       NavButton(_(\"post replies\"), 'selfreply'),\n                       NavButton(_(\"username mentions\"), \"mentions\"),\n            ]\n\n            return [NavMenu(buttons, base_path = '/message/',\n                            default = 'inbox', type = \"flatlist\")]\n        elif not c.default_sr or self.where in ('moderator', 'multi'):\n            buttons = (NavButton(_(\"all\"), \"inbox\"),\n                       NavButton(_(\"unread\"), \"unread\"))\n            return [NavMenu(buttons, base_path = '/message/moderator/',\n                            default = 'inbox', type = \"flatlist\")]\n        return []\n\n\n    def title(self):\n        return _('messages') + ': ' + _(self.where)\n\n    def keep_fn(self):\n        def keep(item):\n            wouldkeep = True\n            # TODO: Consider a flag to disable this (and see above plus builder.py)\n            if item._deleted and not c.user_is_admin:\n                return False\n            if (item._spam and\n                    item.author_id != c.user._id and\n                    not c.user_is_admin):\n                return False\n\n            if self.where == 'unread' or self.subwhere == 'unread':\n                # don't show user their own unread stuff\n                if item.author_id == c.user._id:\n                    wouldkeep = False\n                else:\n                    wouldkeep = item.new\n            elif item.is_mention:\n                wouldkeep = (\n                    c.user.name.lower() in extract_user_mentions(item.body)\n                )\n\n            if c.user_is_admin:\n                return wouldkeep\n\n            if (hasattr(item, \"subreddit\") and\n                    item.subreddit.is_moderator(c.user)):\n                return wouldkeep\n\n            if item.author_id in c.user.enemies:\n                return False\n\n            # do not show messages which were deleted on recipient\n            if (isinstance(item, Message) and\n                    item.to_id == c.user._id and item.del_on_recipient):\n                return False\n\n            if item.author_id == c.user._id:\n                return wouldkeep\n\n            return wouldkeep and item.keep_item(item)\n        return keep\n\n    @staticmethod\n    def builder_wrapper(thing):\n        if isinstance(thing, Comment):\n            f = thing._fullname\n            w = Wrapped(thing)\n            w.render_class = Message\n            w.to_id = c.user._id\n            w.was_comment = True\n            w._fullname = f\n        else:\n            w = ListingController.builder_wrapper(thing)\n\n        return w\n\n    def builder(self):\n        if (self.where == 'messages' or\n            (self.where in (\"moderator\", \"multi\") and self.subwhere != \"unread\")):\n            root = c.user\n            message_cls = UserMessageBuilder\n\n            if self.where == \"multi\":\n                root = c.site\n                message_cls = MultiredditMessageBuilder\n            elif not c.default_sr:\n                root = c.site\n                message_cls = SrMessageBuilder\n            elif self.where == 'moderator' and self.subwhere != 'unread':\n                message_cls = ModeratorMessageBuilder\n            elif self.message and self.message.sr_id:\n                sr = self.message.subreddit_slow\n                if sr.is_moderator_with_perms(c.user, 'mail'):\n                    # this is a moderator message and the user is a moderator.\n                    # use the ModeratorMessageBuilder because not all messages\n                    # will be in the user's mailbox\n                    message_cls = ModeratorMessageBuilder\n\n            parent = None\n            skip = False\n            if self.message:\n                if self.message.first_message:\n                    parent = Message._byID(self.message.first_message,\n                                           data=True)\n                else:\n                    parent = self.message\n            elif c.user.pref_threaded_messages:\n                skip = (c.render_style == \"html\")\n\n            if (message_cls is UserMessageBuilder and parent and parent.sr_id\n                and not parent.from_sr):\n                # Make sure we use the subreddit message builder for modmail,\n                # because the per-user cache will be wrong if more than two\n                # parties are involved in the thread.\n                root = Subreddit._byID(parent.sr_id)\n                message_cls = SrMessageBuilder\n\n            enable_threaded = (\n                (self.where == \"moderator\" or\n                    parent and parent.sr_id) and\n                c.user.pref_threaded_modmail and\n                c.render_style == \"html\"\n            )\n\n            return message_cls(\n                root,\n                wrap=self.builder_wrapper,\n                parent=parent,\n                skip=skip,\n                num=self.num,\n                after=self.after,\n                keep_fn=self.keep_fn(),\n                reverse=self.reverse,\n                threaded=enable_threaded,\n            )\n        return ListingController.builder(self)\n\n    def _verify_inbox_count(self, kept_msgs):\n        \"\"\"If a user has experienced drift in their inbox counts, correct it.\n\n        A small percentage (~0.2%) of users are seeing drift in their inbox\n        counts (presumably because _incr is experiencing rare failures). If the\n        user has no unread messages in their inbox currently, this will repair\n        that drift and log it. Yes, this is a hack.\n        \"\"\"\n        if g.disallow_db_writes:\n            return\n\n        if not len(kept_msgs) and c.user.inbox_count != 0:\n            g.log.info(\n                \"Fixing inbox drift for %r. Kept msgs: %d. Inbox_count: %d.\",\n                c.user,\n                len(kept_msgs),\n                c.user.inbox_count,\n            )\n            g.stats.simple_event(\"inbox_counts.drift_fix\")\n            c.user._incr('inbox_count', -c.user.inbox_count)\n\n    def listing(self):\n        if not c.default_sr:\n            target = c.site if not isinstance(c.site, FakeSubreddit) else None\n            VNotInTimeout().run(action_name=\"pageview\",\n                details_text=\"modmail\", target=target)\n        if (self.where == 'messages' and\n            (c.user.pref_threaded_messages or self.message)):\n            return Listing(self.builder_obj).listing()\n        pane = ListingController.listing(self)\n\n        # Indicate that the comment tree wasn't built for comments\n        for i in pane.things:\n            if i.was_comment:\n                i.child = None\n\n        if self.where == 'unread':\n            self._verify_inbox_count(pane.things)\n\n        return pane\n\n    def query(self):\n        if self.where == 'messages':\n            q = queries.get_inbox_messages(c.user)\n        elif self.where == 'comments':\n            q = queries.get_inbox_comments(c.user)\n        elif self.where == 'selfreply':\n            q = queries.get_inbox_selfreply(c.user)\n        elif self.where == 'mentions':\n            q = queries.get_inbox_comment_mentions(c.user)\n        elif self.where == 'inbox':\n            q = queries.get_inbox(c.user)\n        elif self.where == 'unread':\n            q = queries.get_unread_inbox(c.user)\n        elif self.where == 'sent':\n            q = queries.get_sent(c.user)\n        elif self.where == 'multi' and self.subwhere == 'unread':\n            q = queries.get_unread_subreddit_messages_multi(c.site.kept_sr_ids)\n        elif self.where == 'moderator' and self.subwhere == 'unread':\n            if c.default_sr:\n                srids = Subreddit.reverse_moderator_ids(c.user)\n                srs = [sr for sr in Subreddit._byID(srids, data=False,\n                                                    return_dict=False)\n                       if sr.is_moderator_with_perms(c.user, 'mail')]\n                q = queries.get_unread_subreddit_messages_multi(srs)\n            else:\n                q = queries.get_unread_subreddit_messages(c.site)\n        elif self.where in ('moderator', 'multi'):\n            if c.have_mod_messages and self.mark != 'false':\n                c.have_mod_messages = False\n                c.user.modmsgtime = False\n                c.user._commit()\n            # the query is handled by the builder on the moderator page\n            return\n        else:\n            return self.abort404()\n        if self.where != 'sent':\n            #reset the inbox\n            if c.have_messages and c.user.pref_mark_messages_read and self.mark != 'false':\n                c.have_messages = False\n\n        return q\n\n    @property\n    def render_params(self):\n        render_params = {'source': self.source}\n\n        # event target for screenviews\n        event_target = {}\n        if self.message:\n            event_target['target_type'] = 'message'\n            event_target['target_fullname'] = self.message._fullname\n            event_target['target_id'] = self.message._id\n        if self.after:\n            event_target['target_count'] = self.count\n            if self.reverse:\n                event_target['target_before'] = self.after._fullname\n            else:\n                event_target['target_after'] = self.after._fullname\n        render_params['extra_js_config'] = {'event_target': event_target}\n\n        return render_params\n\n    @require_oauth2_scope(\"privatemessages\")\n    @validate(VUser(),\n              message = VMessageID('mid'),\n              mark = VOneOf('mark',('true','false')))\n    @listing_api_doc(section=api_section.messages,\n                     uri='/message/{where}',\n                     uri_variants=['/message/inbox', '/message/unread', '/message/sent'])\n    def GET_listing(self, where, mark, message, subwhere = None, **env):\n        if not (c.default_sr\n                or c.site.is_moderator_with_perms(c.user, 'mail')\n                or c.user_is_admin):\n            abort(403, \"forbidden\")\n        if isinstance(c.site, MultiReddit):\n            if not (c.user_is_admin or c.site.is_moderator(c.user)):\n                self.abort403()\n            self.where = \"multi\"\n        elif isinstance(c.site, ModSR) or not c.default_sr:\n            self.where = \"moderator\"\n        else:\n            self.where = where\n\n        # don't allow access to modmail when user is in timeout\n        if self.where == \"moderator\":\n            VNotInTimeout().run(action_name=\"pageview\", details_text=\"modmail\",\n                target=message)\n\n        self.subwhere = subwhere\n        self.message = message\n        if mark is not None:\n            self.mark = mark\n        elif self.message:\n            self.mark = \"false\"\n        elif is_api():\n            self.mark = 'false'\n        elif c.render_style and c.render_style == \"xml\":\n            self.mark = 'false'\n        else:\n            self.mark = 'true'\n        if c.user_is_admin:\n            c.referrer_policy = \"always\"\n        if self.where == 'unread':\n            self.next_suggestions_cls = UnreadMessagesSuggestions\n\n        if self.message:\n            self.source = \"permalink\"\n        elif self.where in {\"moderator\", \"multi\"}:\n            self.source = \"modmail\"\n        else:\n            self.source = \"usermail\"\n\n        return ListingController.GET_listing(self, **env)\n\n    @validate(\n        VUser(),\n        to=nop('to'),\n        subject=nop('subject'),\n        message=nop('message'),\n    )\n    def GET_compose(self, to, subject, message):\n        mod_srs = []\n        subreddit_message = False\n        only_as_subreddit = False\n        self.where = \"compose\"\n\n        if isinstance(c.site, MultiReddit):\n            mod_srs = c.site.srs_with_perms(c.user, \"mail\")\n            if not mod_srs:\n                abort(403)\n            subreddit_message = True\n        elif not isinstance(c.site, FakeSubreddit):\n            if not c.site.is_moderator_with_perms(c.user, \"mail\"):\n                abort(403)\n            mod_srs = [c.site]\n            subreddit_message = True\n            only_as_subreddit = True\n        elif c.user.is_moderator_somewhere:\n            mod_srs = Mod.srs_with_perms(c.user, \"mail\")\n            subreddit_message = bool(mod_srs)\n\n        captcha = Captcha() if c.user.needs_captcha() else None\n\n        if subreddit_message:\n            content = ModeratorMessageCompose(\n                mod_srs,\n                only_as_subreddit=only_as_subreddit,\n                to=to,\n                subject=subject,\n                captcha=captcha,\n                message=message,\n                restrict_recipient=c.user.in_timeout)\n        else:\n            content = MessageCompose(\n                to=to,\n                subject=subject,\n                captcha=captcha,\n                message=message,\n                restrict_recipient=c.user.in_timeout)\n\n        return MessagePage(\n            content=content,\n            title=self.title(),\n            page_classes=self.extra_page_classes + ['compose-page'],\n        ).render()\n\n\nclass RedditsController(ListingController):\n    render_cls = SubredditsPage\n    extra_page_classes = ListingController.extra_page_classes + ['subreddits-page']\n\n    def title(self):\n        return _('subreddits')\n\n    def keep_fn(self):\n        base_keep_fn = ListingController.keep_fn(self)\n        def keep(item):\n            if self.where == 'featured':\n                if item.type not in ('public', 'restricted'):\n                    return False\n                if not item.discoverable:\n                    return False\n            return base_keep_fn(item) and (c.over18 or not item.over_18)\n        return keep\n\n    def query(self):\n        if self.where == 'banned' and c.user_is_admin:\n            reddits = Subreddit._query(Subreddit.c._spam == True,\n                                       sort = desc('_date'),\n                                       write_cache = True,\n                                       read_cache = True,\n                                       cache_time = 5 * 60,\n                                       stale = True)\n        else:\n            reddits = None\n            if self.where == 'new':\n                reddits = Subreddit._query( write_cache = True,\n                                            read_cache = True,\n                                            cache_time = 5 * 60,\n                                            stale = True)\n                reddits._sort = desc('_date')\n            elif self.where == 'employee':\n                if c.user_is_loggedin and c.user.employee:\n                    reddits = Subreddit._query(\n                        Subreddit.c.type=='employees_only',\n                        write_cache=True,\n                        read_cache=True,\n                        cache_time=5 * 60,\n                        stale=True,\n                    )\n                    reddits._sort = desc('_downs')\n                else:\n                    abort(404)\n            elif self.where == 'quarantine':\n                if c.user_is_admin:\n                    reddits = Subreddit._query(\n                        Subreddit.c.quarantine==True,\n                        write_cache=True,\n                        read_cache=True,\n                        cache_time=5 * 60,\n                        stale=True,\n                    )\n                    reddits._sort = desc('_downs')\n                else:\n                    abort(404)\n            elif self.where == 'gold':\n                reddits = Subreddit._query(\n                    Subreddit.c.type=='gold_only',\n                    write_cache=True,\n                    read_cache=True,\n                    cache_time=5 * 60,\n                    stale=True,\n                )\n                reddits._sort = desc('_downs')\n            elif self.where == 'default':\n                return [\n                    sr._fullname\n                    for sr in Subreddit.default_subreddits(ids=False)\n                ]\n            elif self.where == 'featured':\n                return [\n                    sr._fullname\n                    for sr in Subreddit.featured_subreddits()\n                ]\n            else:\n                reddits = Subreddit._query( write_cache = True,\n                                            read_cache = True,\n                                            cache_time = 60 * 60,\n                                            stale = True)\n                reddits._sort = desc('_downs')\n\n            if g.domain != 'reddit.com':\n                # don't try to render /r/promos on opensource installations\n                promo_sr_id = Subreddit.get_promote_srid()\n                if promo_sr_id:\n                    reddits._filter(Subreddit.c._id != promo_sr_id)\n\n        return reddits\n\n    @property\n    def render_params(self):\n        render_params = {}\n\n        if self.where == 'popular':\n            render_params['show_interestbar'] = True\n\n        # event target for screenviews (/subreddits)\n        event_target = {\n            'target_sort': self.where,\n        }\n        if self.after:\n            event_target['target_count'] = self.count\n            if self.reverse:\n                event_target['target_before'] = self.after._fullname\n            else:\n                event_target['target_after'] = self.after._fullname\n        render_params['extra_js_config'] = {'event_target': event_target}\n\n        return render_params\n\n    @require_oauth2_scope(\"read\")\n    @listing_api_doc(section=api_section.subreddits,\n                     uri='/subreddits/{where}',\n                     uri_variants=[\n                         '/subreddits/popular',\n                         '/subreddits/new',\n                         '/subreddits/gold',\n                         '/subreddits/default',\n                     ])\n    def GET_listing(self, where, **env):\n        \"\"\"Get all subreddits.\n\n        The `where` parameter chooses the order in which the subreddits are\n        displayed.  `popular` sorts on the activity of the subreddit and the\n        position of the subreddits can shift around. `new` sorts the subreddits\n        based on their creation date, newest first.\n\n        \"\"\"\n        self.where = where\n        return ListingController.GET_listing(self, **env)\n\nclass MyredditsController(ListingController):\n    render_cls = MySubredditsPage\n    extra_page_classes = ListingController.extra_page_classes + ['subreddits-page']\n\n    @property\n    def menus(self):\n        buttons = (NavButton(plurals.subscriber,  'subscriber'),\n                    NavButton(getattr(plurals, \"approved submitter\"), 'contributor'),\n                    NavButton(plurals.moderator,   'moderator'))\n\n        return [NavMenu(buttons, base_path = '/subreddits/mine/',\n                        default = 'subscriber', type = \"flatlist\")]\n\n    def title(self):\n        return _('subreddits: ') + self.where\n\n    def builder_wrapper(self, thing):\n        w = ListingController.builder_wrapper(thing)\n        if self.where == 'moderator':\n            is_moderator = thing.is_moderator(c.user)\n            if is_moderator:\n                w.mod_permissions = is_moderator.get_permissions()\n        return w\n\n    def query(self):\n        if self.where == 'moderator' and not c.user.is_moderator_somewhere:\n            return []\n\n        if self.where == \"subscriber\":\n            sr_ids = Subreddit.subscribed_ids_by_user(c.user)\n        else:\n            q = SRMember._simple_query(\n                [\"_thing1_id\"],\n                SRMember.c._name == self.where,\n                SRMember.c._thing2_id == c.user._id,\n                #hack to prevent the query from\n                #adding it's own date\n                sort=(desc('_t1_ups'), desc('_t1_date')),\n            )\n            sr_ids = [row._thing1_id for row in q]\n\n        sr_fullnames = [\n            Subreddit._fullname_from_id36(to36(sr_id)) for sr_id in sr_ids]\n        return sr_fullnames\n\n    def content(self):\n        user = c.user if c.user_is_loggedin else None\n        num_subscriptions = len(Subreddit.subscribed_ids_by_user(user))\n        if self.where == 'subscriber' and num_subscriptions == 0:\n            message = strings.sr_messages['empty']\n        else:\n            message = strings.sr_messages.get(self.where)\n\n        stack = PaneStack()\n\n        if message:\n            stack.append(InfoBar(message=message))\n\n        stack.append(self.listing_obj)\n\n        return stack\n\n    def build_listing(self, after=None, **kwargs):\n        if after and not isinstance(after, Subreddit):\n            abort(400, 'gimme a subreddit')\n\n        return ListingController.build_listing(self, after=after, **kwargs)\n\n    @property\n    def render_params(self):\n        render_params = {}\n\n        # event target for screenviews (/subreddits/mine)\n        event_target = {\n            'target_sort': self.where,\n        }\n        if self.after:\n            event_target['target_count'] = self.count\n            if self.reverse:\n                event_target['target_before'] = self.after._fullname\n            else:\n                event_target['target_after'] = self.after._fullname\n        render_params['extra_js_config'] = {'event_target': event_target}\n\n        return render_params\n\n    @require_oauth2_scope(\"mysubreddits\")\n    @validate(VUser())\n    @listing_api_doc(section=api_section.subreddits,\n                     uri='/subreddits/mine/{where}',\n                     uri_variants=['/subreddits/mine/subscriber', '/subreddits/mine/contributor', '/subreddits/mine/moderator'])\n    def GET_listing(self, where='subscriber', **env):\n        \"\"\"Get subreddits the user has a relationship with.\n\n        The `where` parameter chooses which subreddits are returned as follows:\n\n        * `subscriber` - subreddits the user is subscribed to\n        * `contributor` - subreddits the user is an approved submitter in\n        * `moderator` - subreddits the user is a moderator of\n\n        See also: [/api/subscribe](#POST_api_subscribe),\n        [/api/friend](#POST_api_friend), and\n        [/api/accept_moderator_invite](#POST_api_accept_moderator_invite).\n\n        \"\"\"\n        self.where = where\n        return ListingController.GET_listing(self, **env)\n\n\nclass CommentsController(SubredditListingController):\n    title_text = _('comments')\n\n    def keep_fn(self):\n        def keep(item):\n            if c.user_is_admin:\n                return True\n\n            if item._deleted:\n                return False\n\n            if isinstance(c.site, FriendsSR):\n                if item.author._deleted or item.author._spam:\n                    return False\n\n            if c.user_is_loggedin:\n                if item.subreddit.is_moderator(c.user):\n                    return True\n\n                if item.author_id == c.user._id:\n                    return True\n\n            if item._spam:\n                return False\n            return item.keep_item(item)\n        return keep\n\n    def query(self):\n        return c.site.get_all_comments()\n\n    @require_oauth2_scope(\"read\")\n    def GET_listing(self, **env):\n        c.profilepage = True\n        self.suppress_reply_buttons = True\n        return ListingController.GET_listing(self, **env)\n\n\nclass UserListListingController(ListingController):\n    builder_cls = UserListBuilder\n    allow_stylesheets = False\n    skip = False\n    friends_compat = True\n\n    @property\n    def infotext(self):\n        if self.where == 'friends':\n            return strings.friends % Friends.path\n        elif self.where == 'blocked':\n            return _(\"To block a user click 'block user'  below a message\"\n                     \" from a user you wish to block from messaging you.\")\n\n    @property\n    def render_params(self):\n        params = {}\n        is_wiki_action = self.where in [\"wikibanned\", \"wikicontributors\"]\n        params[\"show_wiki_actions\"] = is_wiki_action\n        return params\n\n    @property\n    def render_cls(self):\n        if self.where in [\"friends\", \"blocked\"]:\n            return PrefsPage\n        return Reddit\n\n    def moderator_wrap(self, rel, invited=False):\n        rel._permission_class = ModeratorPermissionSet\n        cls = ModTableItem if not invited else InvitedModTableItem\n        return cls(rel, editable=self.editable)\n\n    @property\n    def builder_wrapper(self):\n        if self.where == 'banned':\n            cls = BannedTableItem\n        elif self.where == 'muted':\n            cls = MutedTableItem\n        elif self.where == 'moderators':\n            return self.moderator_wrap\n        elif self.where == 'wikibanned':\n            cls = WikiBannedTableItem\n        elif self.where == 'contributors':\n            cls = ContributorTableItem\n        elif self.where == 'wikicontributors':\n            cls = WikiMayContributeTableItem\n        elif self.where == 'friends':\n            cls = FriendTableItem\n        elif self.where == 'blocked':\n            cls = EnemyTableItem\n        return lambda rel : cls(rel, editable=self.editable)\n\n    def title(self):\n        section_title = menu[self.where]\n\n        # We'll probably want to slowly start opting more and more things into\n        # having this suffix, to make similar tabs on different subreddits\n        # distinct.\n        if self.where == 'moderators':\n            return '%(section)s - /r/%(subreddit)s' % {\n                'section': section_title,\n                'subreddit': c.site.name,\n            }\n\n        return section_title\n\n    def rel(self):\n        if self.where in ['friends', 'blocked']:\n            return Friend\n        return SRMember\n\n    def name(self):\n        return self._names.get(self.where)\n\n    _names = {\n              'friends': 'friend',\n              'blocked': 'enemy',\n              'moderators': 'moderator',\n              'contributors': 'contributor',\n              'banned': 'banned',\n              'muted': 'muted',\n              'wikibanned': 'wikibanned',\n              'wikicontributors': 'wikicontributor',\n             }\n\n    def query(self):\n        rel = self.rel()\n        if self.where in [\"friends\", \"blocked\"]:\n            thing1_id = c.user._id\n        else:\n            thing1_id = c.site._id\n        reversed_types = [\"friends\", \"moderators\", \"blocked\"]\n        sort = desc if self.where not in reversed_types else asc\n        q = rel._query(rel.c._thing1_id == thing1_id,\n                       rel.c._name == self.name(),\n                       sort=sort('_date'),\n                       data=True)\n        if self.jump_to_val:\n            thing2_id = self.user._id if self.user else None\n            q._filter(rel.c._thing2_id == thing2_id)\n        return q\n\n    def listing(self):\n        listing = self.listing_cls(self.builder_obj,\n                                   addable=self.editable,\n                                   show_jump_to=self.show_jump_to,\n                                   jump_to_value=self.jump_to_val,\n                                   show_not_found=self.show_not_found,\n                                   nextprev=self.paginated,\n                                   has_add_form=self.editable)\n        return listing.listing()\n\n    def invited_mod_listing(self):\n        query = SRMember._query(SRMember.c._name == 'moderator_invite',\n                                SRMember.c._thing1_id == c.site._id,\n                                sort=asc('_date'), data=True)\n        wrapper = lambda rel: self.moderator_wrap(rel, invited=True)\n        b = self.builder_cls(query,\n                             keep_fn=self.keep_fn(),\n                             wrap=wrapper,\n                             skip=False,\n                             num=0)\n        return InvitedModListing(b, nextprev=False).listing()\n\n    def content(self):\n        is_api = c.render_style in extensions.API_TYPES\n        if self.where == 'moderators' and self.editable and not is_api:\n            # Do not stack the invited mod list in api mode\n            # to allow for api compatibility with older api users.\n            content = PaneStack()\n            content.append(self.listing_obj)\n            content.append(self.invited_mod_listing())\n        elif self.where == 'friends' and is_api and self.friends_compat:\n            content = PaneStack()\n            content.append(self.listing_obj)\n            empty_builder = IDBuilder([])\n            # Append an empty UserList on the api for backwards\n            # compatibility with the old blocked list.\n            content.append(UserListing(empty_builder, nextprev=False).listing())\n        else:\n            content = self.listing_obj\n        return content\n\n    @require_oauth2_scope(\"read\")\n    @validate(VUser())\n    @base_listing\n    @listing_api_doc(section=api_section.account,\n                     uri='/prefs/{where}',\n                     uri_variants=['/prefs/friends', '/prefs/blocked',\n                         '/api/v1/me/friends', '/api/v1/me/blocked'])\n    def GET_user_prefs(self, where, **kw):\n        self.where = where\n\n        self.listing_cls = None\n        self.editable = True\n        self.paginated = False\n        self.jump_to_val = None\n        self.show_not_found = False\n        self.show_jump_to = False\n        # The /prefs/friends version of this endpoint used to contain\n        # two lists of users: friends AND blocked users. For backwards\n        # compatibility with the old JSON structure, an empty list\n        # of \"blocked\" users is sent.\n        # The /api/v1/me/friends version of the friends list does not\n        # have this requirement, so it will send just the \"friends\"\n        # data structure.\n        self.friends_compat = not request.path.startswith('/api/v1/me/')\n\n        if where == 'friends':\n            self.listing_cls = FriendListing\n        elif where == 'blocked':\n            self.listing_cls = EnemyListing\n            self.show_not_found = True\n        else:\n            abort(404)\n\n        kw['num'] = 0\n        return self.build_listing(**kw)\n\n\n    @require_oauth2_scope(\"read\")\n    @validate(user=VAccountByName('user'))\n    @base_listing\n    @listing_api_doc(section=api_section.subreddits,\n                     uses_site=True,\n                     uri='/about/{where}',\n                     uri_variants=['/about/' + where for where in [\n                        'banned', 'muted', 'wikibanned', 'contributors',\n                        'wikicontributors', 'moderators']])\n    def GET_listing(self, where, user=None, **kw):\n        if isinstance(c.site, FakeSubreddit):\n            return self.abort404()\n\n        self.where = where\n\n        has_mod_access = ((c.user_is_loggedin and\n                           c.site.is_moderator_with_perms(c.user, 'access'))\n                          or c.user_is_admin)\n\n        if not c.user_is_loggedin and where not in ['contributors', 'moderators']:\n            abort(403)\n\n        self.listing_cls = None\n        self.editable = not (c.user_is_loggedin and c.user.in_timeout)\n        self.paginated = True\n        self.jump_to_val = request.GET.get('user')\n        self.show_not_found = bool(self.jump_to_val)\n\n        if where == 'contributors':\n            # On public reddits, only moderators may see the whitelist.\n            if c.site.type == 'public' and not has_mod_access:\n                abort(403)\n            # Used for subreddits like /r/lounge\n            if c.site.hide_subscribers:\n                abort(403)\n            # used for subreddits that don't allow access to approved submitters\n            if c.site.hide_contributors:\n                abort(403)\n            self.listing_cls = ContributorListing\n            self.editable = self.editable and has_mod_access \n\n        elif where == 'banned':\n            if not has_mod_access:\n                abort(403)\n            VNotInTimeout().run(action_name=\"pageview\",\n                details_text=\"banned\", target=c.site)\n            self.listing_cls = BannedListing\n\n        elif where == 'muted':\n            if not (c.user_is_admin or (has_mod_access and\n                    c.site.is_moderator_with_perms(c.user, 'mail'))):\n                abort(403)\n            VNotInTimeout().run(action_name=\"pageview\",\n                details_text=\"muted\", target=c.site)\n            self.listing_cls = MutedListing\n\n        elif where == 'wikibanned':\n            if not (c.site.is_moderator_with_perms(c.user, 'wiki') or\n                    c.user_is_admin):\n                abort(403)\n            VNotInTimeout().run(action_name=\"pageview\",\n                details_text=\"wikibanned\", target=c.site)\n            self.listing_cls = WikiBannedListing\n\n        elif where == 'wikicontributors':\n            if not (c.site.is_moderator_with_perms(c.user, 'wiki') or\n                    c.user_is_admin):\n                abort(403)\n            VNotInTimeout().run(action_name=\"pageview\",\n                details_text=\"wikicontributors\", target=c.site)\n            self.listing_cls = WikiMayContributeListing\n\n        elif where == 'moderators':\n            self.editable = ((self.editable and\n                              c.user_is_loggedin and\n                              c.site.is_unlimited_moderator(c.user)) or\n                             c.user_is_admin)\n            self.listing_cls = ModListing\n            self.paginated = False\n\n        if not self.listing_cls:\n            abort(404)\n\n        self.user = user\n        self.show_jump_to = self.paginated\n\n        if not self.paginated:\n            kw['num'] = 0\n\n        return self.build_listing(**kw)\n\n\nclass GildedController(SubredditListingController):\n    where = 'gilded'\n    title_text = _(\"gilded\")\n\n    @property\n    def infotext(self):\n        if isinstance(c.site, FakeSubreddit):\n            return ''\n\n        seconds = c.site.gilding_server_seconds\n        if not seconds:\n            return ''\n\n        delta = timedelta(seconds=seconds)\n        server_time = precise_format_timedelta(\n            delta, threshold=5, locale=c.locale)\n        message = _(\"gildings in this subreddit have paid for %(time)s of \"\n                    \"server time\")\n        return message % {'time': server_time}\n\n    @property\n    def infotext_class(self):\n        return \"rounded gold-accent\"\n\n    def keep_fn(self):\n        def keep(item):\n            return item.gildings > 0 and not item._deleted and not item._spam\n        return keep\n\n    def query(self):\n        try:\n            return c.site.get_gilded()\n        except NotImplementedError:\n            abort(404)\n\n    @require_oauth2_scope(\"read\")\n    def GET_listing(self, **env):\n        c.profilepage = True\n        self.suppress_reply_buttons = True\n        if not c.site.allow_gilding:\n            self.abort404()\n        return ListingController.GET_listing(self, **env)\n"
  },
  {
    "path": "r2/r2/controllers/login.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\nfrom pylons import request\nfrom pylons import tmpl_context as c\nfrom pylons import app_globals as g\n\nfrom r2.lib.validator import VRatelimit\nfrom r2.lib import amqp\nfrom r2.lib import emailer\nfrom r2.lib import hooks\nfrom r2.lib import newsletter\nfrom r2.lib.base import abort\nfrom r2.lib.errors import errors, reddit_http_error\n\nfrom r2.models.account import register, AccountExists\n\n\ndef handle_login(\n    controller, form, responder, user, rem=None, signature=None, **kwargs\n):\n    def _event(error):\n        g.events.login_event(\n            'login_attempt',\n            error_msg=error,\n            user_name=request.urlvars.get('url_user'),\n            remember_me=rem,\n            signature=signature,\n            request=request,\n            context=c)\n\n    if signature and not signature.is_valid():\n        _event(error=\"SIGNATURE\")\n        abort(403)\n\n    hook_error = hooks.get_hook(\"account.login\").call_until_return(\n        responder=responder,\n        request=request,\n        context=c,\n    )\n    # if any of the hooks returned an error, abort the login.  The\n    # set_error in this case also needs to exist in the hook.\n    if hook_error:\n        _event(error=hook_error)\n        return\n\n    exempt_ua = (request.user_agent and\n                 any(ua in request.user_agent for ua\n                     in g.config.get('exempt_login_user_agents', ())))\n    if (errors.LOGGED_IN, None) in c.errors:\n        if user == c.user or exempt_ua:\n            # Allow funky clients to re-login as the current user.\n            c.errors.remove((errors.LOGGED_IN, None))\n        else:\n            _event(error='LOGGED_IN')\n            abort(reddit_http_error(409, errors.LOGGED_IN))\n\n    if responder.has_errors(\"ratelimit\", errors.RATELIMIT):\n        _event(error='RATELIMIT')\n\n    elif responder.has_errors(\"passwd\", errors.WRONG_PASSWORD):\n        _event(error='WRONG_PASSWORD')\n\n    else:\n        controller._login(responder, user, rem)\n        _event(error=None)\n\n\ndef handle_register(\n    controller, form, responder, name, email,\n    password, rem=None, newsletter_subscribe=False,\n    sponsor=False, signature=None, **kwargs\n):\n\n    def _event(error):\n        g.events.login_event(\n            'register_attempt',\n            error_msg=error,\n            user_name=request.urlvars.get('url_user'),\n            email=request.POST.get('email'),\n            remember_me=rem,\n            newsletter=newsletter_subscribe,\n            signature=signature,\n            request=request,\n            context=c)\n\n    if signature and not signature.is_valid():\n        _event(error=\"SIGNATURE\")\n        abort(403)\n\n    if responder.has_errors('user', errors.USERNAME_TOO_SHORT):\n        _event(error='USERNAME_TOO_SHORT')\n\n    elif responder.has_errors('user', errors.USERNAME_INVALID_CHARACTERS):\n        _event(error='USERNAME_INVALID_CHARACTERS')\n\n    elif responder.has_errors('user', errors.USERNAME_TAKEN_DEL):\n        _event(error='USERNAME_TAKEN_DEL')\n\n    elif responder.has_errors('user', errors.USERNAME_TAKEN):\n        _event(error='USERNAME_TAKEN')\n\n    elif responder.has_errors('email', errors.BAD_EMAIL):\n        _event(error='BAD_EMAIL')\n\n    elif responder.has_errors('passwd', errors.SHORT_PASSWORD):\n        _event(error='SHORT_PASSWORD')\n\n    elif responder.has_errors('passwd', errors.BAD_PASSWORD):\n        # BAD_PASSWORD is set when SHORT_PASSWORD is set\n        _event(error='BAD_PASSWORD')\n\n    elif responder.has_errors('passwd2', errors.BAD_PASSWORD_MATCH):\n        _event(error='BAD_PASSWORD_MATCH')\n\n    elif responder.has_errors('ratelimit', errors.RATELIMIT):\n        _event(error='RATELIMIT')\n\n    elif (not g.disable_captcha and\n            responder.has_errors('captcha', errors.BAD_CAPTCHA)):\n        _event(error='BAD_CAPTCHA')\n\n    elif newsletter_subscribe and not email:\n        c.errors.add(errors.NEWSLETTER_NO_EMAIL, field=\"email\")\n        form.has_errors(\"email\", errors.NEWSLETTER_NO_EMAIL)\n        _event(error='NEWSLETTER_NO_EMAIL')\n\n    elif sponsor and not email:\n        c.errors.add(errors.SPONSOR_NO_EMAIL, field=\"email\")\n        form.has_errors(\"email\", errors.SPONSOR_NO_EMAIL)\n        _event(error='SPONSOR_NO_EMAIL')\n\n    else:\n        try:\n            user = register(name, password, request.ip)\n        except AccountExists:\n            c.errors.add(errors.USERNAME_TAKEN, field=\"user\")\n            form.has_errors(\"user\", errors.USERNAME_TAKEN)\n            _event(error='USERNAME_TAKEN')\n            return\n\n        VRatelimit.ratelimit(rate_ip=True, prefix=\"rate_register_\")\n\n        # anything else we know (email, languages)?\n        if email:\n            user.set_email(email)\n            emailer.verify_email(user)\n\n        user.pref_lang = c.lang\n        user._commit()\n\n        amqp.add_item('new_account', user._fullname)\n\n        hooks.get_hook(\"account.registered\").call(user=user)\n\n        reject = hooks.get_hook(\"account.spotcheck\").call(account=user)\n        if any(reject):\n            _event(error='ACCOUNT_SPOTCHECK')\n            return\n\n        if newsletter_subscribe and email:\n            try:\n                newsletter.add_subscriber(email, source=\"register\")\n            except newsletter.NewsletterError as e:\n                g.log.warning(\"Failed to subscribe: %r\" % e)\n\n        controller._login(responder, user, rem)\n        _event(error=None)\n"
  },
  {
    "path": "r2/r2/controllers/mailgun.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport hashlib\nimport hmac\nimport re\nimport time\n\nfrom pylons import app_globals as g\nfrom pylons import request\n\nfrom r2.config import feature\nfrom r2.controllers.reddit_base import RedditController\nfrom r2.lib.base import abort\nfrom r2.lib.csrf import csrf_exempt\nfrom r2.lib.db import queries\nfrom r2.lib.filters import markdown_souptest\nfrom r2.lib.message_to_email import (\n    parse_and_validate_reply_to_address,\n    queue_blocked_muted_email,\n)\nfrom r2.lib.souptest import SoupError\nfrom r2.lib.utils import constant_time_compare\nfrom r2.models import (\n    Account,\n    Message,\n    Subreddit,\n)\n\n\nMAX_TIMESTAMP_DEVIATION = 600\nZENDESK_PREFIX = \"##- Please type your reply above this line -##\"\n\n\ndef validate_mailgun_webhook(timestamp, token, signature):\n    \"\"\"Check whether this is a valid webhook sent by Mailgun.\n\n    See https://documentation.mailgun.com/user_manual.html#securing-webhooks\n\n    NOTE:\n    A single Mailgun account is used for both outbound email (Mailgun HTTP API)\n    and inbound email (Mailgun Routes + MailgunWebhookController). As a result\n    the `mailgun_api_key` is used by both.\n\n    \"\"\"\n\n    message = ''.join((timestamp, token))\n    expected_mac = hmac.new(\n        g.secrets['mailgun_api_key'], message, hashlib.sha256).hexdigest()\n    if not constant_time_compare(expected_mac, signature):\n        g.stats.simple_event(\"mailgun.incoming.bad_signature\")\n        return False\n\n    if abs(int(timestamp) - time.time()) > MAX_TIMESTAMP_DEVIATION:\n        g.stats.simple_event(\"mailgun.incoming.bad_timestamp\")\n        return False\n\n    return True\n\n\nclass MailgunWebhookController(RedditController):\n    \"\"\"Handle POST requests from Mailgun generated by inbound emails.\"\"\"\n\n    @csrf_exempt\n    def POST_zendeskreply(self):\n        request_body = request.POST\n        recipient = request_body[\"recipient\"]\n        sender_email = request_body[\"sender\"]\n        from_ = request_body[\"from\"]\n        subject = request_body[\"subject\"]\n        body_plain = request_body[\"body-plain\"]\n        stripped_text = request_body[\"stripped-text\"]\n        timestamp = request_body[\"timestamp\"]\n        token = request_body[\"token\"]\n        signature = request_body[\"signature\"]\n        email_id = request_body[\"Message-Id\"]\n\n        if not validate_mailgun_webhook(timestamp, token, signature):\n            # per Mailgun docs send a 406 so the message won't be retried\n            abort(406, \"invalid signature\")\n\n        message_id36 = parse_and_validate_reply_to_address(recipient)\n\n        if not message_id36:\n            # per Mailgun docs send a 406 so the message won't be retried\n            abort(406, \"invalid message\")\n\n        parent = Message._byID36(message_id36, data=True)\n        to = Account._byID(parent.author_id, data=True)\n        sr = Subreddit._byID(parent.sr_id, data=True)\n\n        if stripped_text.startswith(ZENDESK_PREFIX):\n            stripped_text = stripped_text[len(ZENDESK_PREFIX):].lstrip()\n\n        if len(stripped_text) > 10000:\n            body = stripped_text[:10000] + \"\\n\\n--snipped--\"\n        else:\n            body = stripped_text\n\n        try:\n            markdown_souptest(body)\n        except SoupError:\n            g.log.warning(\"bad markdown in modmail email: %s\", body)\n            abort(406, \"invalid body\")\n\n        if parent.get_muted_user_in_conversation():\n            queue_blocked_muted_email(sr, parent, sender_email, email_id)\n            return\n\n        # keep the subject consistent\n        message_subject = parent.subject\n        if not message_subject.startswith(\"re: \"):\n            message_subject = \"re: \" + message_subject\n\n        # from_ is like '\"NAME (GROUP)\" <something@domain.zendesk.com>'\n        match = re.search(\"\\\"(?P<name>\\w+) [\\w ()]*\\\"\", from_)\n        from_sr = True\n        author = Account.system_user()\n\n        if match and match.group(\"name\") in g.live_config['modmail_account_map']:\n            zendesk_name = match.group(\"name\")\n            moderator_name = g.live_config['modmail_account_map'][zendesk_name]\n            moderator = Account._by_name(moderator_name)\n            if sr.is_moderator_with_perms(moderator, \"mail\"):\n                author = moderator\n                from_sr = False\n\n        message, inbox_rel = Message._new(\n            author=author,\n            to=to,\n            subject=message_subject,\n            body=body,\n            ip='0.0.0.0',\n            parent=parent,\n            sr=sr,\n            from_sr=from_sr,\n            can_send_email=False,\n            sent_via_email=True,\n            email_id=email_id,\n        )\n        message._commit()\n        queries.new_message(message, inbox_rel)\n        g.stats.simple_event(\"mailgun.incoming.success\")\n        g.stats.simple_event(\"modmail_email.incoming_email\")\n"
  },
  {
    "path": "r2/r2/controllers/mediaembed.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport hashlib\nimport hmac\n\nfrom pylons import request\nfrom pylons import tmpl_context as c\nfrom pylons import app_globals as g\nfrom pylons.controllers.util import abort\n\nfrom r2.controllers.reddit_base import MinimalController\nfrom r2.lib.pages import MediaEmbedBody\nfrom r2.lib.media import get_media_embed\nfrom r2.lib.utils import constant_time_compare\nfrom r2.lib.validator import validate, VLink, nop\nfrom r2.models import Subreddit\n\n\nclass MediaembedController(MinimalController):\n    @validate(\n        link=VLink('link'),\n        credentials=nop('credentials'),\n    )\n    def GET_mediaembed(self, link, credentials):\n        if request.host != g.media_domain:\n            # don't serve up untrusted content except on our\n            # specifically untrusted domain\n            abort(404)\n\n        if link.subreddit_slow.type in Subreddit.private_types:\n            expected_mac = hmac.new(g.secrets[\"media_embed\"], link._id36,\n                                    hashlib.sha1).hexdigest()\n            if not constant_time_compare(credentials or \"\", expected_mac):\n                abort(404)\n\n        if not c.secure:\n            media_object = link.media_object\n        else:\n            media_object = link.secure_media_object\n\n        if not media_object:\n            abort(404)\n        elif isinstance(media_object, dict):\n            # otherwise it's the new style, which is a dict(type=type, **args)\n            media_embed = get_media_embed(media_object)\n            content = media_embed.content\n\n        c.allow_framing = True\n\n        return MediaEmbedBody(body = content).render()\n\n\nclass AdController(MinimalController):\n    def GET_ad(self):\n        return \"This is a placeholder ad.\"\n"
  },
  {
    "path": "r2/r2/controllers/multi.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom pylons import request, response\nfrom pylons import tmpl_context as c\nfrom pylons import app_globals as g\nfrom pylons.i18n import _\n\nfrom r2.config.extensions import set_extension\nfrom r2.controllers.api_docs import api_doc, api_section\nfrom r2.controllers.reddit_base import RedditController, abort_with_error\nfrom r2.controllers.oauth2 import require_oauth2_scope\nfrom r2.models.account import Account\nfrom r2.models.subreddit import (\n    FakeSubreddit,\n    Subreddit,\n    LabeledMulti,\n    TooManySubredditsError,\n)\nfrom r2.lib.db import tdb_cassandra\nfrom r2.lib.validator import (\n    validate,\n    VAccountByName,\n    VBoolean,\n    VColor,\n    VLength,\n    VMarkdownLength,\n    VModhash,\n    VMultiPath,\n    VMultiByPath,\n    VOneOf,\n    VSubredditName,\n    VSRByName,\n    VUser,\n    VValidatedJSON,\n)\nfrom r2.lib.pages.things import wrap_things\nfrom r2.lib.jsontemplates import (\n    LabeledMultiJsonTemplate,\n    LabeledMultiDescriptionJsonTemplate,\n)\nfrom r2.lib.errors import RedditError\n\n\nmulti_sr_data_json_spec = VValidatedJSON.Object({\n    'name': VSubredditName('name', allow_language_srs=True),\n})\n\nMAX_DESC = 10000\nMAX_DISP_NAME = 50\nWRITABLE_MULTI_FIELDS = ('visibility', 'description_md', 'display_name',\n                         'key_color', 'weighting_scheme')\n\nmulti_json_spec = VValidatedJSON.PartialObject({\n    'description_md': VMarkdownLength('description_md', max_length=MAX_DESC,\n                                      empty_error=None),\n    'display_name': VLength('display_name', max_length=MAX_DISP_NAME),\n    'icon_name': VOneOf('icon_name', g.multi_icons + (\"\", None)),\n    'key_color': VColor('key_color'),\n    'visibility': VOneOf('visibility', ('private', 'public', 'hidden')),\n    'weighting_scheme': VOneOf('weighting_scheme', ('classic', 'fresh')),\n    'subreddits': VValidatedJSON.ArrayOf(multi_sr_data_json_spec),\n})\n\n\nmulti_description_json_spec = VValidatedJSON.Object({\n    'body_md': VMarkdownLength('body_md', max_length=MAX_DESC, empty_error=None),\n})\n\n\nclass MultiApiController(RedditController):\n    def on_validation_error(self, error):\n        abort_with_error(error, error.code or 400)\n\n    def pre(self):\n        set_extension(request.environ, \"json\")\n        RedditController.pre(self)\n\n    def _format_multi_list(self, multis, viewer, expand_srs):\n        templ = LabeledMultiJsonTemplate(expand_srs)\n        resp = [templ.render(multi).finalize() for multi in multis\n                if multi.can_view(viewer)]\n        return self.api_wrapper(resp)\n\n    @require_oauth2_scope(\"read\")\n    @validate(\n        user=VAccountByName(\"username\"),\n        expand_srs=VBoolean(\"expand_srs\"),\n    )\n    @api_doc(api_section.multis, uri=\"/api/multi/user/{username}\")\n    def GET_list_multis(self, user, expand_srs):\n        \"\"\"Fetch a list of public multis belonging to `username`\"\"\"\n        multis = LabeledMulti.by_owner(user)\n        return self._format_multi_list(multis, c.user, expand_srs)\n\n    @require_oauth2_scope(\"read\")\n    @validate(VUser(), expand_srs=VBoolean(\"expand_srs\"))\n    @api_doc(api_section.multis, uri=\"/api/multi/mine\")\n    def GET_my_multis(self, expand_srs):\n        \"\"\"Fetch a list of multis belonging to the current user.\"\"\"\n        multis = LabeledMulti.by_owner(c.user)\n        return self._format_multi_list(multis, c.user, expand_srs)\n\n    def _format_multi(self, multi, expand_sr_info=False):\n        multi_info = LabeledMultiJsonTemplate(expand_sr_info).render(multi)\n        return self.api_wrapper(multi_info.finalize())\n\n    @require_oauth2_scope(\"read\")\n    @validate(\n        multi=VMultiByPath(\"multipath\", require_view=True),\n        expand_srs=VBoolean(\"expand_srs\"),\n    )\n    @api_doc(\n        api_section.multis,\n        uri=\"/api/multi/{multipath}\",\n        uri_variants=['/api/filter/{filterpath}'],\n    )\n    def GET_multi(self, multi, expand_srs):\n        \"\"\"Fetch a multi's data and subreddit list by name.\"\"\"\n        return self._format_multi(multi, expand_srs)\n\n    def _check_new_multi_path(self, path_info):\n        if path_info['owner'].lower() != c.user.name.lower():\n            raise RedditError('MULTI_CANNOT_EDIT', code=403,\n                              fields='multipath')\n        return c.user\n\n    def _add_multi_srs(self, multi, sr_datas):\n        srs = Subreddit._by_name(sr_data['name'] for sr_data in sr_datas)\n\n        for sr in srs.itervalues():\n            if isinstance(sr, FakeSubreddit):\n                raise RedditError('MULTI_SPECIAL_SUBREDDIT',\n                                  msg_params={'path': sr.path},\n                                  code=400)\n\n        sr_props = {}\n        for sr_data in sr_datas:\n            try:\n                sr = srs[sr_data['name']]\n            except KeyError:\n                raise RedditError('SUBREDDIT_NOEXIST', code=400)\n            else:\n                # name is passed in via the API data format, but should not be\n                # stored on the model.\n                del sr_data['name']\n                sr_props[sr] = sr_data\n\n        try:\n            multi.add_srs(sr_props)\n        except TooManySubredditsError as e:\n            raise RedditError('MULTI_TOO_MANY_SUBREDDITS', code=409)\n\n        return sr_props\n\n    def _write_multi_data(self, multi, data):\n        srs = data.pop('subreddits', None)\n        if srs is not None:\n            multi.clear_srs()\n            try:\n                self._add_multi_srs(multi, srs)\n            except:\n                multi._revert()\n                raise\n\n        if 'icon_name' in data:\n            try:\n                multi.set_icon_by_name(data.pop('icon_name'))\n            except:\n                multi._revert()\n                raise\n\n        for key, val in data.iteritems():\n            if key in WRITABLE_MULTI_FIELDS:\n                setattr(multi, key, val)\n\n        multi._commit()\n        return multi\n\n    @require_oauth2_scope(\"subscribe\")\n    @validate(\n        VUser(),\n        VModhash(),\n        path_info=VMultiPath(\"multipath\", required=False),\n        data=VValidatedJSON(\"model\", multi_json_spec),\n    )\n    @api_doc(api_section.multis, extends=GET_multi)\n    def POST_multi(self, path_info, data):\n        \"\"\"Create a multi. Responds with 409 Conflict if it already exists.\"\"\"\n\n        if not path_info and \"path\" in data:\n            path_info = VMultiPath(\"\").run(data[\"path\"])\n        elif 'display_name' in data:\n            # if path not provided, create multi for user\n            path = LabeledMulti.slugify(c.user, data['display_name'])\n            path_info = VMultiPath(\"\").run(path)\n\n        if not path_info:\n            raise RedditError('BAD_MULTI_PATH', code=400)\n\n        owner = self._check_new_multi_path(path_info)\n\n        try:\n            LabeledMulti._byID(path_info['path'])\n        except tdb_cassandra.NotFound:\n            multi = LabeledMulti.create(path_info['path'], owner)\n            response.status = 201\n        else:\n            raise RedditError('MULTI_EXISTS', code=409, fields='multipath')\n\n        self._write_multi_data(multi, data)\n        return self._format_multi(multi)\n\n    @require_oauth2_scope(\"subscribe\")\n    @validate(\n        VUser(),\n        VModhash(),\n        path_info=VMultiPath(\"multipath\"),\n        data=VValidatedJSON(\"model\", multi_json_spec),\n    )\n    @api_doc(api_section.multis, extends=GET_multi)\n    def PUT_multi(self, path_info, data):\n        \"\"\"Create or update a multi.\"\"\"\n\n        owner = self._check_new_multi_path(path_info)\n\n        try:\n            multi = LabeledMulti._byID(path_info['path'])\n        except tdb_cassandra.NotFound:\n            multi = LabeledMulti.create(path_info['path'], owner)\n            response.status = 201\n\n        self._write_multi_data(multi, data)\n        return self._format_multi(multi)\n\n    @require_oauth2_scope(\"subscribe\")\n    @validate(\n        VUser(),\n        VModhash(),\n        multi=VMultiByPath(\"multipath\", require_edit=True),\n    )\n    @api_doc(api_section.multis, extends=GET_multi)\n    def DELETE_multi(self, multi):\n        \"\"\"Delete a multi.\"\"\"\n        multi.delete()\n\n    def _copy_multi(self, from_multi, to_path_info, rename=False):\n        \"\"\"Copy a multi to a user account.\"\"\"\n\n        to_owner = self._check_new_multi_path(to_path_info)\n\n        # rename requires same owner\n        if rename and from_multi.owner != to_owner:\n            raise RedditError('MULTI_CANNOT_EDIT', code=400)\n\n        try:\n            LabeledMulti._byID(to_path_info['path'])\n        except tdb_cassandra.NotFound:\n            to_multi = LabeledMulti.copy(to_path_info['path'], from_multi,\n                                         owner=to_owner)\n        else:\n            raise RedditError('MULTI_EXISTS', code=409, fields='multipath')\n\n        return to_multi\n\n    @require_oauth2_scope(\"subscribe\")\n    @validate(\n        VUser(),\n        VModhash(),\n        from_multi=VMultiByPath(\"from\", require_view=True, kinds='m'),\n        to_path_info=VMultiPath(\"to\", required=False,\n            docs={\"to\": \"destination multireddit url path\"},\n        ),\n        display_name=VLength(\"display_name\", max_length=MAX_DISP_NAME,\n                             empty_error=None),\n    )\n    @api_doc(\n        api_section.multis,\n        uri=\"/api/multi/copy\",\n    )\n    def POST_multi_copy(self, from_multi, to_path_info, display_name):\n        \"\"\"Copy a multi.\n\n        Responds with 409 Conflict if the target already exists.\n\n        A \"copied from ...\" line will automatically be appended to the\n        description.\n\n        \"\"\"\n        if not to_path_info:\n            if display_name:\n                # if path not provided, copy multi to same owner\n                path = LabeledMulti.slugify(from_multi.owner, display_name)\n                to_path_info = VMultiPath(\"\").run(path)\n            else:\n                raise RedditError('BAD_MULTI_PATH', code=400)\n\n        to_multi = self._copy_multi(from_multi, to_path_info)\n\n        from_path = from_multi.path\n        to_multi.copied_from = from_path\n        if to_multi.description_md:\n            to_multi.description_md += '\\n\\n'\n        to_multi.description_md += _('copied from %(source)s') % {\n            # force markdown linking since /user/foo is not autolinked\n            'source': '[%s](%s)' % (from_path, from_path)\n        }\n        to_multi.visibility = 'private'\n        if display_name:\n            to_multi.display_name = display_name\n        to_multi._commit()\n\n        return self._format_multi(to_multi)\n\n    @require_oauth2_scope(\"subscribe\")\n    @validate(\n        VUser(),\n        VModhash(),\n        from_multi=VMultiByPath(\"from\", require_edit=True, kinds='m'),\n        to_path_info=VMultiPath(\"to\", required=False,\n            docs={\"to\": \"destination multireddit url path\"},\n        ),\n        display_name=VLength(\"display_name\", max_length=MAX_DISP_NAME,\n                             empty_error=None),\n    )\n    @api_doc(\n        api_section.multis,\n        uri=\"/api/multi/rename\",\n    )\n    def POST_multi_rename(self, from_multi, to_path_info, display_name):\n        \"\"\"Rename a multi.\"\"\"\n        if not to_path_info:\n            if display_name:\n                path = LabeledMulti.slugify(from_multi.owner, display_name)\n                to_path_info = VMultiPath(\"\").run(path)\n            else:\n                raise RedditError('BAD_MULTI_PATH', code=400)\n\n        to_multi = self._copy_multi(from_multi, to_path_info, rename=True)\n\n        if display_name:\n            to_multi.display_name = display_name\n            to_multi._commit()\n        from_multi.delete()\n\n        return self._format_multi(to_multi)\n\n    def _get_multi_subreddit(self, multi, sr):\n        resp = LabeledMultiJsonTemplate.sr_props(multi, [sr])[0]\n        return self.api_wrapper(resp)\n\n    @require_oauth2_scope(\"read\")\n    @validate(\n        VUser(),\n        multi=VMultiByPath(\"multipath\", require_view=True),\n        sr=VSRByName('srname'),\n    )\n    @api_doc(\n        api_section.multis,\n        uri=\"/api/multi/{multipath}/r/{srname}\",\n        uri_variants=['/api/filter/{filterpath}/r/{srname}'],\n    )\n    def GET_multi_subreddit(self, multi, sr):\n        \"\"\"Get data about a subreddit in a multi.\"\"\"\n        return self._get_multi_subreddit(multi, sr)\n\n    @require_oauth2_scope(\"subscribe\")\n    @validate(\n        VUser(),\n        VModhash(),\n        multi=VMultiByPath(\"multipath\", require_edit=True),\n        sr_name=VSubredditName('srname', allow_language_srs=True),\n        data=VValidatedJSON(\"model\", multi_sr_data_json_spec),\n    )\n    @api_doc(api_section.multis, extends=GET_multi_subreddit)\n    def PUT_multi_subreddit(self, multi, sr_name, data):\n        \"\"\"Add a subreddit to a multi.\"\"\"\n\n        new = not any(sr.name.lower() == sr_name.lower() for sr in multi.srs)\n\n        data['name'] = sr_name\n        sr_props = self._add_multi_srs(multi, [data])\n        sr = sr_props.items()[0][0]\n        multi._commit()\n\n        if new:\n            response.status = 201\n\n        return self._get_multi_subreddit(multi, sr)\n\n    @require_oauth2_scope(\"subscribe\")\n    @validate(\n        VUser(),\n        VModhash(),\n        multi=VMultiByPath(\"multipath\", require_edit=True),\n        sr=VSRByName('srname'),\n    )\n    @api_doc(api_section.multis, extends=GET_multi_subreddit)\n    def DELETE_multi_subreddit(self, multi, sr):\n        \"\"\"Remove a subreddit from a multi.\"\"\"\n        multi.del_srs(sr)\n        multi._commit()\n\n    def _format_multi_description(self, multi):\n        resp = LabeledMultiDescriptionJsonTemplate().render(multi).finalize()\n        return self.api_wrapper(resp)\n\n    @require_oauth2_scope(\"read\")\n    @validate(\n        VUser(),\n        multi=VMultiByPath(\"multipath\", require_view=True, kinds='m'),\n    )\n    @api_doc(\n        api_section.multis,\n        uri=\"/api/multi/{multipath}/description\",\n    )\n    def GET_multi_description(self, multi):\n        \"\"\"Get a multi's description.\"\"\"\n        return self._format_multi_description(multi)\n\n    @require_oauth2_scope(\"read\")\n    @validate(\n        VUser(),\n        VModhash(),\n        multi=VMultiByPath(\"multipath\", require_edit=True, kinds='m'),\n        data=VValidatedJSON('model', multi_description_json_spec),\n    )\n    @api_doc(api_section.multis, extends=GET_multi_description)\n    def PUT_multi_description(self, multi, data):\n        \"\"\"Change a multi's markdown description.\"\"\"\n        multi.description_md = data['body_md']\n        multi._commit()\n        return self._format_multi_description(multi)\n"
  },
  {
    "path": "r2/r2/controllers/newsletter.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom r2.controllers.reddit_base import RedditController\nfrom r2.lib.pages import Newsletter\n\n\nclass NewsletterController(RedditController):\n    def GET_newsletter(self):\n        return Newsletter().render()\n"
  },
  {
    "path": "r2/r2/controllers/oauth2.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom datetime import datetime\nfrom urllib import urlencode\nimport base64\nimport simplejson\n\nfrom pylons import request, response\nfrom pylons import tmpl_context as c\nfrom pylons import app_globals as g\nfrom pylons.i18n import _\n\nfrom r2.config.extensions import set_extension\nfrom r2.lib.base import abort\nfrom reddit_base import RedditController, MinimalController, require_https\nfrom r2.lib.db import tdb_cassandra\nfrom r2.lib.db.thing import NotFound\nfrom r2.lib.pages import RedditError\nfrom r2.lib.strings import strings\nfrom r2.models import Account, admintools, create_gift_gold, send_system_message\nfrom r2.models.token import (\n    OAuth2Client, OAuth2AuthorizationCode, OAuth2AccessToken,\n    OAuth2RefreshToken, OAuth2Scope)\nfrom r2.lib.errors import BadRequestError, ForbiddenError, errors\nfrom r2.lib.pages import OAuth2AuthorizationPage\nfrom r2.lib.require import RequirementException, require, require_split\nfrom r2.lib.utils import constant_time_compare, parse_http_basic, UrlParser\nfrom r2.lib.validator import (\n    nop,\n    validate,\n    VRequired,\n    VThrottledLogin,\n    VOneOf,\n    VUser,\n    VModhash,\n    VOAuth2ClientID,\n    VOAuth2Scope,\n    VOAuth2RefreshToken,\n    VRatelimit,\n    VLength,\n)\n\n\ndef _update_redirect_uri(base_redirect_uri, params, as_fragment=False):\n    parsed = UrlParser(base_redirect_uri)\n    if as_fragment:\n        parsed.fragment = urlencode(params)\n    else:\n        parsed.update_query(**params)\n    return parsed.unparse()\n\n\ndef get_device_id(client):\n    if client.is_first_party():\n        return request.POST.get('device_id')\n\n\nclass OAuth2FrontendController(RedditController):\n    def check_for_bearer_token(self):\n        pass\n\n    def pre(self):\n        RedditController.pre(self)\n        require_https()\n\n    def _abort_oauth_error(self, error):\n        g.stats.simple_event('oauth2.errors.%s' % error)\n        abort(BadRequestError(error))\n\n    def _check_redirect_uri(self, client, redirect_uri):\n        if (errors.OAUTH2_INVALID_CLIENT, 'client_id') in c.errors:\n            self._abort_oauth_error(errors.OAUTH2_INVALID_CLIENT)\n\n        if not redirect_uri or redirect_uri != client.redirect_uri:\n            self._abort_oauth_error(errors.OAUTH2_INVALID_REDIRECT_URI)\n\n    def _check_response_type_and_scope(self, response_type, scope):\n        if (errors.INVALID_OPTION, 'response_type') in c.errors:\n            self._abort_oauth_error(errors.OAUTH2_INVALID_RESPONSE_TYPE)\n\n        if (errors.OAUTH2_INVALID_SCOPE, 'scope') in c.errors:\n            self._abort_oauth_error(errors.OAUTH2_INVALID_SCOPE)\n\n    def _check_client_type_and_duration(self, response_type, client, duration):\n        if response_type == \"token\" and client.is_confidential():\n            # Prevent \"confidential\" clients from distributing tokens\n            # in a non-confidential manner\n            self._abort_oauth_error(errors.OAUTH2_CONFIDENTIAL_TOKEN)\n\n        if response_type == \"token\" and duration != \"temporary\":\n            # implicit grant -> No refresh tokens allowed\n            self._abort_oauth_error(errors.OAUTH2_NO_REFRESH_TOKENS_ALLOWED)\n\n    def _error_response(self, state, redirect_uri, as_fragment=False):\n        \"\"\"Return an error redirect.\"\"\"\n        resp = {\"state\": state}\n\n        if (errors.OAUTH2_ACCESS_DENIED, \"authorize\") in c.errors:\n            resp[\"error\"] = \"access_denied\"\n        elif (errors.INVALID_MODHASH, None) in c.errors:\n            resp[\"error\"] = \"access_denied\"\n        else:\n            resp[\"error\"] = \"invalid_request\"\n\n        final_redirect = _update_redirect_uri(redirect_uri, resp, as_fragment)\n        return self.redirect(final_redirect, code=302)\n\n    def _check_employee_grants(self, client, scope):\n        if not c.user.employee or not client or not scope:\n            return\n        if client._id in g.employee_approved_clients:\n            return\n        if client._id in g.mobile_auth_allowed_clients:\n            return\n        # The identity scope doesn't leak much, and we don't mind if employees\n        # prove their identity to some external service\n        if scope.scopes == {\"identity\"}:\n            return\n        error_page = RedditError(\n            title=_('this app has not been approved for use with employee accounts'),\n            message=\"\",\n        )\n        request.environ[\"usable_error_content\"] = error_page.render()\n        self.abort403()\n\n    @validate(VUser(),\n              response_type = VOneOf(\"response_type\", (\"code\", \"token\")),\n              client = VOAuth2ClientID(),\n              redirect_uri = VRequired(\"redirect_uri\", errors.OAUTH2_INVALID_REDIRECT_URI),\n              scope = VOAuth2Scope(),\n              state = VRequired(\"state\", errors.NO_TEXT),\n              duration = VOneOf(\"duration\", (\"temporary\", \"permanent\"),\n                                default=\"temporary\"))\n    def GET_authorize(self, response_type, client, redirect_uri, scope, state,\n                      duration):\n        \"\"\"\n        First step in [OAuth 2.0](http://oauth.net/2/) authentication.\n        End users will be prompted for their credentials (username/password)\n        and asked if they wish to authorize the application identified by\n        the **client_id** parameter with the permissions specified by the\n        **scope** parameter.  They are then redirected to the endpoint on\n        the client application's side specified by **redirect_uri**.\n\n        If the user granted permission to the application, the response will\n        contain a **code** parameter with a temporary authorization code\n        which can be exchanged for an access token at\n        [/api/v1/access_token](#api_method_access_token).\n\n        **redirect_uri** must match the URI configured for the client in the\n        [app preferences](/prefs/apps).  All errors will show a 400 error\n        page along with some information on what option was wrong.\n        \"\"\"\n        self._check_employee_grants(client, scope)\n\n        # Check redirect URI first; it will ensure client exists\n        self._check_redirect_uri(client, redirect_uri)\n\n        self._check_response_type_and_scope(response_type, scope)\n\n        self._check_client_type_and_duration(response_type, client, duration)\n\n        if not c.errors:\n            return OAuth2AuthorizationPage(client, redirect_uri, scope, state,\n                                           duration, response_type).render()\n        else:\n            self._abort_oauth_error(errors.INVALID_OPTION)\n\n    @validate(VUser(),\n              VModhash(fatal=False),\n              client = VOAuth2ClientID(),\n              redirect_uri = VRequired(\"redirect_uri\", errors.OAUTH2_INVALID_REDIRECT_URI),\n              scope = VOAuth2Scope(),\n              state = VRequired(\"state\", errors.NO_TEXT),\n              duration = VOneOf(\"duration\", (\"temporary\", \"permanent\"),\n                                default=\"temporary\"),\n              authorize = VRequired(\"authorize\", errors.OAUTH2_ACCESS_DENIED),\n              response_type = VOneOf(\"response_type\", (\"code\", \"token\"),\n                                     default=\"code\"))\n    def POST_authorize(self, authorize, client, redirect_uri, scope, state,\n                       duration, response_type):\n        \"\"\"Endpoint for OAuth2 authorization.\"\"\"\n\n        self._check_employee_grants(client, scope)\n\n        self._check_redirect_uri(client, redirect_uri)\n\n        self._check_response_type_and_scope(response_type, scope)\n\n        self._check_client_type_and_duration(response_type, client, duration)\n\n        if c.errors:\n            return self._error_response(state, redirect_uri,\n                                        as_fragment=(response_type == \"token\"))\n\n        if response_type == \"code\":\n            code = OAuth2AuthorizationCode._new(client._id, redirect_uri,\n                                            c.user._id36, scope,\n                                            duration == \"permanent\")\n            resp = {\"code\": code._id, \"state\": state}\n            final_redirect = _update_redirect_uri(redirect_uri, resp)\n            g.stats.simple_event('oauth2.POST_authorize.authorization_code_create')\n        elif response_type == \"token\":\n            device_id = get_device_id(client)\n            token = OAuth2AccessToken._new(\n                client_id=client._id,\n                user_id=c.user._id36,\n                scope=scope,\n                device_id=device_id,\n            )\n            resp = OAuth2AccessController._make_new_token_response(token)\n            resp[\"state\"] = state\n            final_redirect = _update_redirect_uri(redirect_uri, resp, as_fragment=True)\n            g.stats.simple_event('oauth2.POST_authorize.access_token_create')\n\n        # If this is the first time the user is logging in with an official\n        # mobile app, gild them\n        if (g.live_config.get('mobile_gild_first_login') and\n                not c.user.has_used_mobile_app and\n                client._id in g.mobile_auth_gild_clients):\n            buyer = Account.system_user()\n            admintools.adjust_gold_expiration(\n                c.user, days=g.mobile_auth_gild_time)\n            create_gift_gold(\n                buyer._id, c.user._id, g.mobile_auth_gild_time,\n                datetime.now(g.tz), signed=True, note='first_mobile_auth')\n            subject = 'Let there be gold! Reddit just sent you Reddit gold!'\n            message = (\n                \"Thank you for using the Reddit mobile app!  As a thank you \"\n                \"for logging in during launch week, you've been gifted %s of \"\n                \"Reddit Gold.\\n\\n\"\n                \"Reddit Gold is Reddit's premium membership program, which \"\n                \"grants you: \\n\"\n                \"An ads-free experience in Reddit's mobile apps, and\\n\"\n                \"Extra site features on desktop\\n\\n\"\n                \"Discuss and get help on the features and perks at \"\n                \"r/goldbenefits.\"\n            ) % g.mobile_auth_gild_message\n            message += '\\n\\n' + strings.gold_benefits_msg\n            send_system_message(c.user, subject, message, add_to_sent=False)\n            c.user.has_used_mobile_app = True\n            c.user._commit()\n\n        return self.redirect(final_redirect, code=302)\n\nclass OAuth2AccessController(MinimalController):\n    handles_csrf = True\n\n    def pre(self):\n        if g.disallow_db_writes:\n            abort(403)\n\n        set_extension(request.environ, \"json\")\n        MinimalController.pre(self)\n        require_https()\n        if request.method != \"OPTIONS\":\n            c.oauth2_client = self._get_client_auth()\n\n    def _get_client_auth(self):\n        auth = request.headers.get(\"Authorization\")\n        try:\n            client_id, client_secret = parse_http_basic(auth)\n            require(client_id)\n            client = OAuth2Client.get_token(client_id)\n            require(client)\n            if client.is_confidential():\n                require(client_secret)\n                require(constant_time_compare(client.secret, client_secret))\n            return client\n        except RequirementException:\n            abort(401, headers=[(\"WWW-Authenticate\", 'Basic realm=\"reddit\"')])\n\n    def OPTIONS_access_token(self):\n        \"\"\"Send CORS headers for access token requests\n\n        * Allow all origins\n        * Only POST requests allowed to /api/v1/access_token\n        * No ambient credentials\n        * Authorization header required to identify the client\n        * Expose common reddit headers\n\n        \"\"\"\n        if \"Origin\" in request.headers:\n            response.headers[\"Access-Control-Allow-Origin\"] = \"*\"\n            response.headers[\"Access-Control-Allow-Methods\"] = \\\n                \"POST\"\n            response.headers[\"Access-Control-Allow-Headers\"] = \\\n                    \"Authorization, \"\n            response.headers[\"Access-Control-Allow-Credentials\"] = \"false\"\n            response.headers['Access-Control-Expose-Headers'] = \\\n                self.COMMON_REDDIT_HEADERS\n\n    @validate(\n        grant_type=VOneOf(\"grant_type\",\n            (\n                 \"authorization_code\",\n                 \"refresh_token\",\n                 \"password\",\n                 \"client_credentials\",\n                 \"https://oauth.reddit.com/grants/installed_client\",\n            )\n        ),\n    )\n    def POST_access_token(self, grant_type):\n        \"\"\"\n        Exchange an [OAuth 2.0](http://oauth.net/2/) authorization code\n        or refresh token (from [/api/v1/authorize](#api_method_authorize)) for\n        an access token.\n\n        On success, returns a URL-encoded dictionary containing\n        **access_token**, **token_type**, **expires_in**, and **scope**.\n        If an authorization code for a permanent grant was given, a\n        **refresh_token** will be included. If there is a problem, an **error**\n        parameter will be returned instead.\n\n        Must be called using SSL, and must contain a HTTP `Authorization:`\n        header which contains the application's client identifier as the\n        username and client secret as the password.  (The client id and secret\n        are visible on the [app preferences page](/prefs/apps).)\n\n        Per the OAuth specification, **grant_type** must be one of:\n\n        * ``authorization_code`` for the initial access token (\"standard\" OAuth2 flow)\n        * ``refresh_token`` for renewing the access token.\n        * ``password`` for script-type apps using password auth\n        * ``client_credentials`` for application-only (signed out) access - confidential clients\n        * ``https://oauth.reddit.com/grants/installed_client`` extension grant for application-only (signed out)\n                access - non-confidential (installed) clients\n\n        **redirect_uri** must exactly match the value that was used in the call\n        to [/api/v1/authorize](#api_method_authorize) that created this grant.\n\n        See reddit's [OAuth2 wiki](https://github.com/reddit/reddit/wiki/OAuth2) for\n        more information.\n\n        \"\"\"\n        self.OPTIONS_access_token()\n        if grant_type == \"authorization_code\":\n            return self._access_token_code()\n        elif grant_type == \"refresh_token\":\n            return self._access_token_refresh()\n        elif grant_type == \"password\":\n            return self._access_token_password()\n        elif grant_type == \"client_credentials\":\n            return self._access_token_client_credentials()\n        elif grant_type == \"https://oauth.reddit.com/grants/installed_client\":\n            return self._access_token_extension_client_credentials()\n        else:\n            resp = {\"error\": \"unsupported_grant_type\"}\n            return self.api_wrapper(resp)\n\n    def _check_for_errors(self):\n        resp = {}\n        if (errors.INVALID_OPTION, \"scope\") in c.errors:\n            resp[\"error\"] = \"invalid_scope\"\n        else:\n            resp[\"error\"] = \"invalid_request\"\n        return resp\n\n    @classmethod\n    def _make_new_token_response(cls, access_token, refresh_token=None):\n        if not access_token:\n            return {\"error\": \"invalid_grant\"}\n        expires_in = int(access_token._ttl) if access_token._ttl else None\n        resp = {\n            \"access_token\": access_token._id,\n            \"token_type\": access_token.token_type,\n            \"expires_in\": expires_in,\n            \"scope\": access_token.scope,\n        }\n        if refresh_token:\n            resp[\"refresh_token\"] = refresh_token._id\n\n        if access_token.device_id:\n            resp['device_id'] = access_token.device_id\n\n        return resp\n\n    @validate(code=nop(\"code\"),\n              redirect_uri=VRequired(\"redirect_uri\",\n                                     errors.OAUTH2_INVALID_REDIRECT_URI))\n    def _access_token_code(self, code, redirect_uri):\n        if not code:\n            c.errors.add(\"NO_TEXT\", field=\"code\")\n        if c.errors:\n            return self.api_wrapper(self._check_for_errors())\n\n        access_token = None\n        refresh_token = None\n\n        client = c.oauth2_client\n        auth_token = OAuth2AuthorizationCode.use_token(code, client._id, redirect_uri)\n\n        if auth_token:\n            if auth_token.refreshable:\n                refresh_token = OAuth2RefreshToken._new(\n                    client_id=auth_token.client_id,\n                    user_id=auth_token.user_id,\n                    scope=auth_token.scope,\n                )\n                g.stats.simple_event(\n                    'oauth2.access_token_code.refresh_token_create')\n\n            device_id = get_device_id(client)\n            access_token = OAuth2AccessToken._new(\n                client_id=auth_token.client_id,\n                user_id=auth_token.user_id,\n                scope=auth_token.scope,\n                refresh_token=refresh_token._id if refresh_token else \"\",\n                device_id=device_id,\n            )\n            g.stats.simple_event(\n                'oauth2.access_token_code.access_token_create')\n\n        resp = self._make_new_token_response(access_token, refresh_token)\n\n        return self.api_wrapper(resp)\n\n    @validate(refresh_token=VOAuth2RefreshToken(\"refresh_token\"))\n    def _access_token_refresh(self, refresh_token):\n        access_token = None\n        if refresh_token:\n            if refresh_token.client_id == c.oauth2_client._id:\n                access_token = OAuth2AccessToken._new(\n                    refresh_token.client_id, refresh_token.user_id,\n                    refresh_token.scope,\n                    refresh_token=refresh_token._id)\n                g.stats.simple_event(\n                    'oauth2.access_token_refresh.access_token_create')\n            else:\n                g.stats.simple_event(\n                    'oauth2.errors.OAUTH2_INVALID_REFRESH_TOKEN')\n                c.errors.add(errors.OAUTH2_INVALID_REFRESH_TOKEN)\n        else:\n            g.stats.simple_event('oauth2.errors.NO_TEXT')\n            c.errors.add(\"NO_TEXT\", field=\"refresh_token\")\n\n        if c.errors:\n            resp = self._check_for_errors()\n            response.status = 400\n        else:\n            g.stats.simple_event('oauth2.access_token_refresh.success')\n            resp = self._make_new_token_response(access_token)\n        return self.api_wrapper(resp)\n\n    @validate(user=VThrottledLogin([\"username\", \"password\"]),\n              scope=nop(\"scope\"))\n    def _access_token_password(self, user, scope):\n        # username:password auth via OAuth is only allowed for\n        # private use scripts\n        client = c.oauth2_client\n        if client.app_type != \"script\":\n            g.stats.simple_event('oauth2.errors.PASSWORD_UNAUTHORIZED_CLIENT')\n            return self.api_wrapper({\"error\": \"unauthorized_client\",\n                \"error_description\": \"Only script apps may use password auth\"})\n        dev_ids = client._developer_ids\n        if not user or user._id not in dev_ids:\n            g.stats.simple_event('oauth2.errors.INVALID_GRANT')\n            return self.api_wrapper({\"error\": \"invalid_grant\"})\n        if c.errors:\n            return self.api_wrapper(self._check_for_errors())\n\n        if scope:\n            scope = OAuth2Scope(scope)\n            if not scope.is_valid():\n                g.stats.simple_event('oauth2.errors.PASSWORD_INVALID_SCOPE')\n                c.errors.add(errors.INVALID_OPTION, \"scope\")\n                return self.api_wrapper({\"error\": \"invalid_scope\"})\n        else:\n            scope = OAuth2Scope(OAuth2Scope.FULL_ACCESS)\n\n        device_id = get_device_id(client)\n        access_token = OAuth2AccessToken._new(\n            client_id=client._id,\n            user_id=user._id36,\n            scope=scope,\n            device_id=device_id,\n        )\n        g.stats.simple_event(\n            'oauth2.access_token_password.access_token_create')\n        resp = self._make_new_token_response(access_token)\n        return self.api_wrapper(resp)\n\n    @validate(\n        scope=nop(\"scope\"),\n    )\n    def _access_token_client_credentials(self, scope):\n        client = c.oauth2_client\n        if not client.is_confidential():\n            g.stats.simple_event(\n                'oauth2.errors.CLIENT_CREDENTIALS_UNAUTHORIZED_CLIENT')\n            return self.api_wrapper({\"error\": \"unauthorized_client\",\n                \"error_description\": \"Only confidential clients may use client_credentials auth\"})\n        if scope:\n            scope = OAuth2Scope(scope)\n            if not scope.is_valid():\n                g.stats.simple_event(\n                    'oauth2.errors.CLIENT_CREDENTIALS_INVALID_SCOPE')\n                c.errors.add(errors.INVALID_OPTION, \"scope\")\n                return self.api_wrapper({\"error\": \"invalid_scope\"})\n        else:\n            scope = OAuth2Scope(OAuth2Scope.FULL_ACCESS)\n\n        device_id = get_device_id(client)\n        access_token = OAuth2AccessToken._new(\n            client_id=client._id,\n            user_id=\"\",\n            scope=scope,\n            device_id=device_id,\n        )\n        g.stats.simple_event(\n            'oauth2.access_token_client_credentials.access_token_create')\n        resp = self._make_new_token_response(access_token)\n        return self.api_wrapper(resp)\n\n    @validate(\n        scope=nop(\"scope\"),\n        device_id=VLength(\"device_id\", 50, min_length=20),\n    )\n    def _access_token_extension_client_credentials(self, scope, device_id):\n        if ((errors.NO_TEXT, \"device_id\") in c.errors or\n                (errors.TOO_SHORT, \"device_id\") in c.errors or\n                (errors.TOO_LONG, \"device_id\") in c.errors):\n            g.stats.simple_event('oauth2.errors.BAD_DEVICE_ID')\n            return self.api_wrapper({\n                \"error\": \"invalid_request\",\n                \"error_description\": \"bad device_id\",\n            })\n\n        client = c.oauth2_client\n        if scope:\n            scope = OAuth2Scope(scope)\n            if not scope.is_valid():\n                g.stats.simple_event(\n                    'oauth2.errors.EXTENSION_CLIENT_CREDENTIALS_INVALID_SCOPE')\n                c.errors.add(errors.INVALID_OPTION, \"scope\")\n                return self.api_wrapper({\"error\": \"invalid_scope\"})\n        else:\n            scope = OAuth2Scope(OAuth2Scope.FULL_ACCESS)\n\n        access_token = OAuth2AccessToken._new(\n            client_id=client._id,\n            user_id=\"\",\n            scope=scope,\n            device_id=device_id,\n        )\n        g.stats.simple_event(\n            'oauth2.access_token_extension_client_credentials.'\n            'access_token_create')\n        resp = self._make_new_token_response(access_token)\n        return self.api_wrapper(resp)\n\n    def OPTIONS_revoke_token(self):\n        \"\"\"Send CORS headers for token revocation requests\n\n        * Allow all origins\n        * Only POST requests allowed to /api/v1/revoke_token\n        * No ambient credentials\n        * Authorization header required to identify the client\n        * Expose common reddit headers\n\n        \"\"\"\n        if \"Origin\" in request.headers:\n            response.headers[\"Access-Control-Allow-Origin\"] = \"*\"\n            response.headers[\"Access-Control-Allow-Methods\"] = \\\n                \"POST\"\n            response.headers[\"Access-Control-Allow-Headers\"] = \\\n                    \"Authorization, \"\n            response.headers[\"Access-Control-Allow-Credentials\"] = \"false\"\n            response.headers['Access-Control-Expose-Headers'] = \\\n                self.COMMON_REDDIT_HEADERS\n\n    @validate(\n        VRatelimit(rate_user=False, rate_ip=True, prefix=\"rate_revoke_token_\"),\n        token_id=nop(\"token\"),\n        token_hint=VOneOf(\"token_type_hint\", (\"access_token\", \"refresh_token\")),\n    )\n    def POST_revoke_token(self, token_id, token_hint):\n        '''Revoke an OAuth2 access or refresh token.\n\n        token_type_hint is optional, and hints to the server\n        whether the passed token is a refresh or access token.\n\n        A call to this endpoint is considered a success if\n        the passed `token_id` is no longer valid. Thus, if an invalid\n        `token_id` was passed in, a successful 204 response will be returned.\n\n        See [RFC7009](http://tools.ietf.org/html/rfc7009)\n\n        '''\n        self.OPTIONS_revoke_token()\n        # In success cases, this endpoint returns no data.\n        response.status = 204\n\n        if not token_id:\n            return\n\n        types = (OAuth2AccessToken, OAuth2RefreshToken)\n        if token_hint == \"refresh_token\":\n            types = reversed(types)\n\n        for token_type in types:\n            try:\n                token = token_type._byID(token_id)\n            except tdb_cassandra.NotFound:\n                g.stats.simple_event(\n                    'oauth2.POST_revoke_token.cass_not_found.%s'\n                    % token_type.__name__)\n                continue\n            else:\n                break\n        else:\n            # No Token found. The given token ID is already gone\n            # or never existed. Either way, from the client's perspective,\n            # the passed in token is no longer valid.\n            return\n\n        if constant_time_compare(token.client_id, c.oauth2_client._id):\n            token.revoke()\n        else:\n            # RFC 7009 is not clear on how to handle this case.\n            # Given that a malicious client could do much worse things\n            # with a valid token then revoke it, returning an error\n            # here is best as it may help certain clients debug issues\n            response.status = 400\n            g.stats.simple_event(\n                'oauth2.errors.REVOKE_TOKEN_UNAUTHORIZED_CLIENT')\n            return self.api_wrapper({\"error\": \"unauthorized_client\"})\n\n\ndef require_oauth2_scope(*scopes):\n    def oauth2_scope_wrap(fn):\n        fn.oauth2_perms = {\"required_scopes\": scopes, \"oauth2_allowed\": True}\n        return fn\n    return oauth2_scope_wrap\n\n\ndef allow_oauth2_access(fn):\n    fn.oauth2_perms = {\"required_scopes\": [], \"oauth2_allowed\": True}\n    return fn\n"
  },
  {
    "path": "r2/r2/controllers/oembed.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom datetime import datetime\n\nfrom pylons import response\nfrom pylons import app_globals as g\nfrom pylons import tmpl_context as c\nfrom pylons.i18n import _\n\nfrom r2.controllers.reddit_base import MinimalController\nfrom r2.lib import embeds\nfrom r2.lib.base import abort\nfrom r2.lib.errors import errors, ForbiddenError\nfrom r2.lib.filters import scriptsafe_dumps, websafe, _force_unicode\nfrom r2.lib.utils import url_to_thing, UrlParser\nfrom r2.lib.template_helpers import format_html, make_url_https\nfrom r2.lib.validator import can_view_link_comments, validate, VBoolean, VUrl\nfrom r2.models import Comment, Link, Subreddit\n\n_OEMBED_BASE = {\n    \"version\": \"1.0\",\n    \"provider_name\": \"reddit\",\n    \"provider_url\": make_url_https('/'),\n}\n\nEMBEDLY_SCRIPT = 'https://embed.redditmedia.com/widgets/platform.js'\nSCRIPT_TEMPLATE = '<script async src=\"%(embedly_script)s\" charset=\"UTF-8\"></script>'\nPOST_EMBED_TEMPLATE = \"\"\"\n    <blockquote class=\"reddit-card\" %(live_data_attr)s>\n      <a href=\"%(link_url)s\">%(title)s</a> from\n      <a href=\"%(subreddit_url)s\">%(subreddit_name)s</a>\n    </blockquote>\n    %(script)s\n\"\"\"\n\ndef _oembed_for(thing, **embed_options):\n    \"\"\"Given a Thing, return a dict of oEmbed data for that thing.\n\n    Raises NotImplementedError if this Thing type does not yet support oEmbeds.\n    \"\"\"\n\n    if isinstance(thing, Comment):\n        return _oembed_comment(thing, **embed_options)\n    elif isinstance(thing, Link):\n        return _oembed_post(thing, **embed_options)\n\n    raise NotImplementedError(\"Unable to render oembed for thing '%r'\", thing)\n\n\ndef _oembed_post(thing, **embed_options):\n    subreddit = thing.subreddit_slow\n    if (not can_view_link_comments(thing) or\n            subreddit.type in Subreddit.private_types):\n        raise ForbiddenError(errors.POST_NOT_ACCESSIBLE)\n\n    live = ''\n    if embed_options.get('live'):\n        time = datetime.now(g.tz).isoformat()\n        live = 'data-card-created=\"{}\"'.format(time)\n\n    script = ''\n    if not embed_options.get('omitscript', False):\n        script = format_html(SCRIPT_TEMPLATE,\n                             embedly_script=EMBEDLY_SCRIPT,\n                             )\n\n    link_url = UrlParser(thing.make_permalink_slow(force_domain=True))\n    link_url.update_query(ref='share', ref_source='embed')\n\n    author_name = \"\"\n    if not thing._deleted:\n        author = thing.author_slow\n        if author._deleted:\n            author_name = _(\"[account deleted]\")\n        else:\n            author_name = author.name\n\n    html = format_html(POST_EMBED_TEMPLATE,\n                       live_data_attr=live,\n                       link_url=link_url.unparse(),\n                       title=websafe(thing.title),\n                       subreddit_url=make_url_https(subreddit.path),\n                       subreddit_name=subreddit.name,\n                       script=script,\n                       )\n\n    oembed_response = dict(_OEMBED_BASE,\n                           type=\"rich\",\n                           title=thing.title,\n                           author_name=author_name,\n                           html=html,\n                           )\n\n    return oembed_response\n\n\ndef _oembed_comment(thing, **embed_options):\n    link = thing.link_slow\n    subreddit = link.subreddit_slow\n    if (not can_view_link_comments(link) or\n            subreddit.type in Subreddit.private_types):\n        raise ForbiddenError(errors.COMMENT_NOT_ACCESSIBLE)\n\n    if not thing._deleted:\n        author = thing.author_slow\n        if author._deleted:\n            author_name = _(\"[account deleted]\")\n        else:\n            author_name = author.name\n\n        title = _('%(author)s\\'s comment from discussion \"%(title)s\"') % {\n            \"author\": author_name,\n            \"title\": _force_unicode(link.title),\n        }\n    else:\n        author_name = \"\"\n        title = \"\"\n\n    html = format_html(embeds.get_inject_template(embed_options.get('omitscript')),\n                       media=g.media_domain,\n                       parent=\"true\" if embed_options.get('parent') else \"false\",\n                       live=\"true\" if embed_options.get('live') else \"false\",\n                       created=datetime.now(g.tz).isoformat(),\n                       comment=thing.make_permalink_slow(force_domain=True),\n                       link=link.make_permalink_slow(force_domain=True),\n                       title=websafe(title),\n                       )\n\n    oembed_response = dict(_OEMBED_BASE,\n                           type=\"rich\",\n                           title=title,\n                           author_name=author_name,\n                           html=html,\n                           )\n\n    if author_name:\n        oembed_response['author_url'] = make_url_https('/user/' + author_name)\n\n    return oembed_response\n\n\nclass OEmbedController(MinimalController):\n    def pre(self):\n        c.user = g.auth_provider.get_authenticated_account()\n        if c.user and c.user._deleted:\n            c.user = None\n        c.user_is_loggedin = bool(c.user)\n\n    @validate(\n        url=VUrl('url'),\n        parent=VBoolean(\"parent\", default=False),\n        live=VBoolean(\"live\", default=False),\n        omitscript=VBoolean(\"omitscript\", default=False)\n    )\n    def GET_oembed(self, url, parent, live, omitscript):\n        \"\"\"Get the oEmbed response for a URL, if any exists.\n\n        Spec: http://www.oembed.com/\n\n        Optional parameters (parent, live) are passed through as embed options\n        to oEmbed renderers.\n        \"\"\"\n        response.content_type = \"application/json\"\n\n        thing = url_to_thing(url)\n        if not thing:\n            abort(404)\n\n        embed_options = {\n            \"parent\": parent,\n            \"live\": live,\n            \"omitscript\": omitscript\n        }\n\n        try:\n            return scriptsafe_dumps(_oembed_for(thing, **embed_options))\n        except ForbiddenError:\n            abort(403)\n        except NotImplementedError:\n            abort(404)\n"
  },
  {
    "path": "r2/r2/controllers/policies.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom pylons import tmpl_context as c\nfrom pylons import app_globals as g\nfrom pylons.i18n import _\nfrom BeautifulSoup import BeautifulSoup, Tag\n\nfrom r2.lib.base import abort\nfrom r2.controllers.reddit_base import RedditController\nfrom r2.models.subreddit import Frontpage\nfrom r2.models.wiki import WikiPage, WikiRevision, WikiBadRevision\nfrom r2.lib.db import tdb_cassandra\nfrom r2.lib.filters import unsafe, wikimarkdown, generate_table_of_contents\nfrom r2.lib.validator import validate, nop\nfrom r2.lib.pages import PolicyPage, PolicyView\n\n\nclass PoliciesController(RedditController):\n    @validate(requested_rev=nop('v'))\n    def GET_policy_page(self, page, requested_rev):\n        if c.render_style == 'compact':\n            self.redirect('/wiki/' + page)\n        if page == 'privacypolicy':\n            wiki_name = g.wiki_page_privacy_policy\n            pagename = _('privacy policy')\n        elif page == 'useragreement':\n            wiki_name = g.wiki_page_user_agreement\n            pagename = _('user agreement')\n        elif page == 'contentpolicy':\n            wiki_name = g.wiki_page_content_policy\n            pagename = _('content policy')\n        else:\n            abort(404)\n\n        wp = WikiPage.get(Frontpage, wiki_name)\n\n        revs = list(wp.get_revisions())\n\n        # collapse minor edits into revisions with reasons\n        rev_info = []\n        last_edit = None\n        for rev in revs:\n            if rev.is_hidden:\n                continue\n\n            if not last_edit:\n                last_edit = rev\n\n            if rev._get('reason'):\n                rev_info.append({\n                    'id': str(last_edit._id),\n                    'title': rev._get('reason'),\n                })\n                last_edit = None\n\n        if requested_rev:\n            try:\n                display_rev = WikiRevision.get(requested_rev, wp._id)\n            except (tdb_cassandra.NotFound, WikiBadRevision):\n                abort(404)\n        else:\n            display_rev = revs[0]\n\n        doc_html = wikimarkdown(display_rev.content, include_toc=False)\n        soup = BeautifulSoup(doc_html.decode('utf-8'))\n        toc = generate_table_of_contents(soup, prefix='section')\n        self._number_sections(soup)\n        self._linkify_headings(soup)\n\n        content = PolicyView(\n            body_html=unsafe(soup),\n            toc_html=unsafe(toc),\n            revs=rev_info,\n            display_rev=str(display_rev._id),\n        )\n        return PolicyPage(\n            pagename=pagename,\n            content=content,\n        ).render()\n\n    def _number_sections(self, soup):\n        count = 1\n        for para in soup.find('div', 'md').findAll(['p'], recursive=False):\n            a = Tag(soup, 'a', [\n                ('class', 'p-anchor'),\n                ('id', 'p_%d' % count),\n                ('href', '#p_%d' % count),\n            ])\n            a.append(str(count))\n            para.insert(0, a)\n            para.insert(1, ' ')\n            count += 1\n\n    def _linkify_headings(self, soup):\n        md_el = soup.find('div', 'md')\n        for heading in md_el.findAll(['h1', 'h2', 'h3'], recursive=False):\n            heading_a = Tag(soup, \"a\", [('href', '#%s' % heading['id'])])\n            heading_a.contents = heading.contents\n            heading.contents = []\n            heading.append(heading_a)\n"
  },
  {
    "path": "r2/r2/controllers/post.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\nfrom pylons import request\nfrom pylons import tmpl_context as c\nfrom pylons import app_globals as g\nfrom pylons import url\nfrom pylons.controllers.util import redirect\nfrom pylons.i18n import _\n\nfrom r2.lib.pages import *\nfrom reddit_base import set_over18_cookie, delete_over18_cookie\nfrom api import ApiController\nfrom r2.lib.utils import query_string, UrlParser\nfrom r2.lib.emailer import opt_in, opt_out\nfrom r2.lib.validator import *\nfrom r2.lib.validator.preferences import (\n    filter_prefs,\n    PREFS_VALIDATORS,\n    set_prefs,\n)\nfrom r2.lib.csrf import csrf_exempt\nfrom r2.models.recommend import ExploreSettings\nfrom r2.controllers.login import handle_login, handle_register\nfrom r2.models import *\nfrom r2.config import feature\n\nclass PostController(ApiController):\n    @csrf_exempt\n    @validate(pref_lang = VLang('lang'),\n              all_langs = VOneOf('all-langs', ('all', 'some'), default='all'))\n    def POST_unlogged_options(self, all_langs, pref_lang):\n        prefs = {\"pref_lang\": pref_lang}\n        set_prefs(c.user, prefs)\n        c.user._commit()\n        return self.redirect(request.referer)\n\n    @validate(VUser(), VModhash(),\n              all_langs=VOneOf('all-langs', ('all', 'some'), default='all'),\n              **PREFS_VALIDATORS)\n    def POST_options(self, all_langs, **prefs):\n        if feature.is_enabled(\"autoexpand_media_previews\"):\n            validator = VOneOf('media_preview', ('on', 'off', 'subreddit'))\n            value = request.params.get('media_preview')\n            prefs[\"pref_media_preview\"] = validator.run(value)\n\n        u = UrlParser(c.site.path + \"prefs\")\n\n        filter_prefs(prefs, c.user)\n        if c.errors.errors:\n            for error in c.errors.errors:\n                if error[1] == 'stylesheet_override':\n                    u.update_query(error_style_override=error[0])\n                else:\n                    u.update_query(generic_error=error[0])\n            return self.redirect(u.unparse())\n\n        set_prefs(c.user, prefs)\n        c.user._commit()\n        u.update_query(done='true')\n        return self.redirect(u.unparse())\n\n    def GET_over18(self):\n        return InterstitialPage(\n            _(\"over 18?\"),\n            content=Over18Interstitial(),\n        ).render()\n\n    @validate(\n        dest=VDestination(default='/'),\n    )\n    def GET_quarantine(self, dest):\n        sr = UrlParser(dest).get_subreddit()\n\n        # if dest doesn't include a quarantined subreddit,\n        # redirect to the homepage or the original destination\n        if not sr:\n            return self.redirect('/')\n        elif isinstance(sr, FakeSubreddit) or sr.is_exposed(c.user):\n            return self.redirect(dest)\n\n        errpage = InterstitialPage(\n            _(\"quarantined\"),\n            content=QuarantineInterstitial(\n                sr_name=sr.name,\n                logged_in=c.user_is_loggedin,\n                email_verified=c.user_is_loggedin and c.user.email_verified,\n            ),\n        )\n        request.environ['usable_error_content'] = errpage.render()\n        self.abort403()\n\n    @csrf_exempt\n    @validate(\n        over18=nop('over18'),\n        dest=VDestination(default='/'),\n    )\n    def POST_over18(self, over18, dest):\n        if over18 == 'yes':\n            if c.user_is_loggedin and not c.errors:\n                c.user.pref_over_18 = True\n                c.user._commit()\n            else:\n                set_over18_cookie()\n            return self.redirect(dest)\n        else:\n            if c.user_is_loggedin and not c.errors:\n                c.user.pref_over_18 = False\n                c.user._commit()\n            else:\n                delete_over18_cookie()\n            return self.redirect('/')\n\n    @validate(\n        VModhash(fatal=False),\n        sr=VSRByName('sr_name'),\n        accept=VBoolean('accept'),\n        dest=VDestination(default='/'),\n    )\n    def POST_quarantine(self, sr, accept, dest):\n        can_opt_in = c.user_is_loggedin and c.user.email_verified\n\n        if accept and can_opt_in and not c.errors:\n            QuarantinedSubredditOptInsByAccount.opt_in(c.user, sr)\n            g.events.quarantine_event('quarantine_opt_in', sr,\n                request=request, context=c)\n            return self.redirect(dest)\n        else:\n            if c.user_is_loggedin and not c.errors:\n                QuarantinedSubredditOptInsByAccount.opt_out(c.user, sr)\n            g.events.quarantine_event('quarantine_interstitial_dismiss', sr,\n                request=request, context=c)\n            return self.redirect('/')\n\n    @csrf_exempt\n    @validate(msg_hash = nop('x'))\n    def POST_optout(self, msg_hash):\n        email, sent = opt_out(msg_hash)\n        if not email:\n            return self.abort404()\n        return BoringPage(_(\"opt out\"),\n                          content = OptOut(email = email, leave = True,\n                                           sent = True,\n                                           msg_hash = msg_hash)).render()\n\n    @csrf_exempt\n    @validate(msg_hash = nop('x'))\n    def POST_optin(self, msg_hash):\n        email, sent = opt_in(msg_hash)\n        if not email:\n            return self.abort404()\n        return BoringPage(_(\"welcome back\"),\n                          content = OptOut(email = email, leave = False,\n                                           sent = True,\n                                           msg_hash = msg_hash)).render()\n\n\n    @csrf_exempt\n    @validate(dest = VDestination(default = \"/\"))\n    def POST_login(self, dest, *a, **kw):\n        super(PostController, self).POST_login(*a, **kw)\n        c.render_style = \"html\"\n        response.content_type = \"text/html\"\n\n        if not c.user_is_loggedin:\n            return LoginPage(user_login = request.POST.get('user'),\n                             dest = dest).render()\n\n        return self.redirect(dest)\n\n    @csrf_exempt\n    @validate(dest = VDestination(default = \"/\"))\n    def POST_reg(self, dest, *a, **kw):\n        super(PostController, self).POST_register(*a, **kw)\n        c.render_style = \"html\"\n        response.content_type = \"text/html\"\n\n        if not c.user_is_loggedin:\n            return LoginPage(user_reg = request.POST.get('user'),\n                             dest = dest).render()\n\n        return self.redirect(dest)\n\n    def GET_login(self, *a, **kw):\n        return self.redirect('/login' + query_string(dict(dest=\"/\")))\n\n    @validatedForm(\n        VUser(),\n        VModhash(),\n        personalized=VBoolean('pers', default=False),\n        discovery=VBoolean('disc', default=False),\n        rising=VBoolean('ris', default=False),\n        nsfw=VBoolean('nsfw', default=False),\n    )\n    def POST_explore_settings(self,\n                              form,\n                              jquery,\n                              personalized,\n                              discovery,\n                              rising,\n                              nsfw):\n        ExploreSettings.record_settings(\n            c.user,\n            personalized=personalized,\n            discovery=discovery,\n            rising=rising,\n            nsfw=nsfw,\n        )\n        return redirect(url(controller='front', action='explore'))\n"
  },
  {
    "path": "r2/r2/controllers/promotecontroller.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\nfrom collections import defaultdict\nfrom datetime import datetime, timedelta\n\nfrom babel.dates import format_date\nfrom babel.numbers import format_number\nimport hashlib\nimport hmac\nimport json\nimport urllib\nimport mimetypes\nimport os\n\nfrom pylons import request\nfrom pylons import tmpl_context as c\nfrom pylons import app_globals as g\nfrom pylons.i18n import _, N_\n\nfrom r2.config import feature\nfrom r2.controllers.api import ApiController\nfrom r2.controllers.listingcontroller import ListingController\nfrom r2.controllers.reddit_base import RedditController\nfrom r2.lib.authorize import (\n    get_or_create_customer_profile,\n    add_or_update_payment_method,\n    PROFILE_LIMIT,\n)\nfrom r2.lib.authorize.api import AuthorizeNetException\nfrom r2.lib import (\n    hooks,\n    inventory,\n    media,\n    promote,\n    s3_helpers,\n)\nfrom r2.lib.base import abort\nfrom r2.lib.db import queries\nfrom r2.lib.errors import errors\nfrom r2.lib.filters import (\n    jssafe,\n    scriptsafe_dumps,\n    websafe,\n)\nfrom r2.lib.template_helpers import (\n    add_sr,\n    format_html,\n)\nfrom r2.lib.memoize import memoize\nfrom r2.lib.menus import NamedButton, NavButton, NavMenu, QueryButton\nfrom r2.lib.pages import (\n    LinkInfoPage,\n    PaymentForm,\n    PromoteInventory,\n    PromotePage,\n    PromoteLinkEdit,\n    PromoteLinkNew,\n    PromoteReport,\n    Reddit,\n    RefundPage,\n    RenderableCampaign,\n    SponsorLookupUser,\n)\nfrom r2.lib.pages.things import default_thing_wrapper, wrap_links\nfrom r2.lib.system_messages import user_added_messages\nfrom r2.lib.utils import (\n    constant_time_compare,\n    is_subdomain,\n    to_date,\n    to36,\n    UrlParser,\n)\nfrom r2.lib.validator import (\n    json_validate,\n    nop,\n    noresponse,\n    VAccountByName,\n    ValidAddress,\n    validate,\n    validatedMultipartForm,\n    validatedForm,\n    ValidCard,\n    ValidEmail,\n    VBoolean,\n    VByName,\n    VCollection,\n    VDate,\n    VExistingUname,\n    VFloat,\n    VFrequencyCap,\n    VImageType,\n    VInt,\n    VLength,\n    VLink,\n    VList,\n    VLocation,\n    VModhash,\n    VOneOf,\n    VOSVersion,\n    VPrintable,\n    VPriority,\n    VPromoCampaign,\n    VPromoTarget,\n    VRatelimit,\n    VMarkdownLength,\n    VShamedDomain,\n    VSponsor,\n    VSponsorAdmin,\n    VSponsorAdminOrAdminSecret,\n    VVerifiedSponsor,\n    VSubmitSR,\n    VTitle,\n    VUploadLength,\n    VUrl,\n)\nfrom r2.models import (\n    Account,\n    AccountsByCanonicalEmail,\n    calc_impressions,\n    Collection,\n    Frontpage,\n    Link,\n    Message,\n    NotFound,\n    PromoCampaign,\n    PromotionLog,\n    PromotionPrices,\n    PromotionWeights,\n    PROMOTE_STATUS,\n    Subreddit,\n    Target,\n)\nfrom r2.models.promo import PROMOTE_COST_BASIS, PROMOTE_PRIORITIES\n\nIOS_DEVICES = ('iPhone', 'iPad', 'iPod',)\nANDROID_DEVICES = ('phone', 'tablet',)\n\nADZERK_URL_MAX_LENGTH = 499\n\nEXPIRES_DATE_FORMAT = \"%Y-%m-%dT%H:%M:%S\"\nALLOWED_IMAGE_TYPES = set([\"image/jpg\", \"image/jpeg\", \"image/png\"])\n\ndef _format_expires(expires):\n    return expires.strftime(EXPIRES_DATE_FORMAT)\n\n\ndef _get_callback_hmac(username, key, expires):\n    secret = g.secrets[\"s3_direct_post_callback\"]\n    expires_str = _format_expires(expires)\n    data = \"|\".join([username, key, expires_str])\n\n    return hmac.new(secret, data, hashlib.sha256).hexdigest()\n\n\ndef _force_images(link, thumbnail, mobile):\n    changed = False\n\n    if thumbnail:\n        media.force_thumbnail(link, thumbnail[\"data\"], thumbnail[\"ext\"])\n        changed = True\n\n    if feature.is_enabled(\"mobile_targeting\") and mobile:\n        media.force_mobile_ad_image(link, mobile[\"data\"], mobile[\"ext\"])\n        changed = True\n\n    return changed\n\n\ndef campaign_has_oversold_error(form, campaign):\n    if campaign.priority.inventory_override:\n        return\n\n    return has_oversold_error(\n        form,\n        campaign,\n        start=campaign.start_date,\n        end=campaign.end_date,\n        total_budget_pennies=campaign.total_budget_pennies,\n        cpm=campaign.bid_pennies,\n        target=campaign.target,\n        location=campaign.location,\n    )\n\n\ndef has_oversold_error(form, campaign, start, end, total_budget_pennies, cpm,\n        target, location):\n    ndays = (to_date(end) - to_date(start)).days\n    total_request = calc_impressions(total_budget_pennies, cpm)\n    daily_request = int(total_request / ndays)\n    oversold = inventory.get_oversold(\n        target, start, end, daily_request, ignore=campaign, location=location)\n\n    if oversold:\n        min_daily = min(oversold.values())\n        available = min_daily * ndays\n        msg_params = {\n            'available': format_number(available, locale=c.locale),\n            'target': target.pretty_name,\n            'start': start.strftime('%m/%d/%Y'),\n            'end': end.strftime('%m/%d/%Y'),\n        }\n        c.errors.add(errors.OVERSOLD_DETAIL, field='total_budget_dollars',\n                     msg_params=msg_params)\n        form.has_errors('total_budget_dollars', errors.OVERSOLD_DETAIL)\n        return True\n\n\ndef _key_to_dict(key, data=False):\n    timer = g.stats.get_timer(\"providers.s3.get_ads_key_meta.with_%s\" %\n        (\"data\" if data else \"no_data\"))\n    timer.start()\n\n    url = key.generate_url(expires_in=0, query_auth=False)\n    # Generating an S3 url without authentication fails for IAM roles.\n    # This removes the bad query params.\n    # see: https://github.com/boto/boto/issues/2043\n    url = promote.update_query(url, {\"x-amz-security-token\": None}, unset=True)\n\n    result = {\n        \"url\": url,\n        \"data\": key.get_contents_as_string() if data else None,\n        \"ext\": key.get_metadata(\"ext\"),\n    }\n\n    timer.stop()\n\n    return result\n\n\ndef _get_ads_keyspace(thing):\n    return \"ads/%s/\" % thing._fullname\n\n\ndef _get_ads_images(thing, data=False, **kwargs):\n    images = {}\n\n    timer = g.stats.get_timer(\"providers.s3.get_ads_image_keys\")\n    timer.start()\n\n    keys = s3_helpers.get_keys(g.s3_client_uploads_bucket, prefix=_get_ads_keyspace(thing), **kwargs)\n\n    timer.stop()\n\n    for key in keys:\n        filename = os.path.basename(key.key)\n        name, ext = os.path.splitext(filename)\n\n        if name not in (\"mobile\", \"thumbnail\"):\n            continue\n\n        images[name] = _key_to_dict(key, data=data)\n\n    return images\n\n\ndef _clear_ads_images(thing):\n    timer = g.stats.get_timer(\"providers.s3.delete_ads_image_keys\")\n    timer.start()\n\n    s3_helpers.delete_keys(g.s3_client_uploads_bucket, prefix=_get_ads_keyspace(thing))\n\n    timer.stop()\n\n\nclass PromoteController(RedditController):\n    @validate(VSponsor())\n    def GET_new_promo(self):\n        ads_images = _get_ads_images(c.user)\n        images = {k: v.get(\"url\") for k, v in ads_images.iteritems()}\n\n        return PromotePage(title=_(\"create sponsored link\"),\n                           content=PromoteLinkNew(images),\n                           extra_js_config={\n                            \"ads_virtual_page\": \"new-promo\",\n                           }).render()\n\n    @validate(VSponsor('link'),\n              link=VLink('link'))\n    def GET_edit_promo(self, link):\n        if not link or link.promoted is None:\n            return self.abort404()\n        rendered = wrap_links(link, skip=False)\n        form = PromoteLinkEdit(link, rendered)\n        page = PromotePage(title=_(\"edit sponsored link\"), content=form,\n                      show_sidebar=False, extension_handling=False)\n        return page.render()\n\n    @validate(VSponsorAdmin(),\n              link=VLink(\"link\"),\n              campaign=VPromoCampaign(\"campaign\"))\n    def GET_refund(self, link, campaign):\n        if link._id != campaign.link_id:\n            return self.abort404()\n\n        content = RefundPage(link, campaign)\n        return Reddit(\"refund\", content=content, show_sidebar=False).render()\n\n    @validate(VVerifiedSponsor(\"link\"),\n              link=VLink(\"link\"),\n              campaign=VPromoCampaign(\"campaign\"))\n    def GET_pay(self, link, campaign):\n        if link._id != campaign.link_id:\n            return self.abort404()\n\n        # no need for admins to play in the credit card area\n        if c.user_is_loggedin and c.user._id != link.author_id:\n            return self.abort404()\n\n        if g.authorizenetapi:\n            data = get_or_create_customer_profile(c.user)\n            content = PaymentForm(link, campaign,\n                                  customer_id=data.customerProfileId,\n                                  profiles=data.paymentProfiles,\n                                  max_profiles=PROFILE_LIMIT)\n        else:\n            content = None\n        res = LinkInfoPage(link=link,\n                            content=content,\n                            show_sidebar=False,\n                            extra_js_config={\n                              \"ads_virtual_page\": \"checkout\",\n                            })\n        return res.render()\n\n\nclass SponsorController(PromoteController):\n    @validate(VSponsorAdminOrAdminSecret('secret'),\n              start=VDate('startdate'),\n              end=VDate('enddate'),\n              link_text=nop('link_text'),\n              owner=VAccountByName('owner'),\n              grouping=VOneOf(\"grouping\", (\"total\", \"day\"), default=\"total\"))\n    def GET_report(self, start, end, grouping, link_text=None, owner=None):\n        now = datetime.now(g.tz).replace(hour=0, minute=0, second=0,\n                                         microsecond=0)\n        if not start or not end:\n            start = promote.promo_datetime_now(offset=1).date()\n            end = promote.promo_datetime_now(offset=8).date()\n            c.errors.remove((errors.BAD_DATE, 'startdate'))\n            c.errors.remove((errors.BAD_DATE, 'enddate'))\n        end = end or now - timedelta(days=1)\n        start = start or end - timedelta(days=7)\n\n        links = []\n        bad_links = []\n        owner_name = owner.name if owner else ''\n\n        if owner:\n            campaign_ids = PromotionWeights.get_campaign_ids(\n                start, end, author_id=owner._id)\n            campaigns = PromoCampaign._byID(campaign_ids, data=True)\n            link_ids = {camp.link_id for camp in campaigns.itervalues()}\n            links.extend(Link._byID(link_ids, data=True, return_dict=False))\n\n        if link_text is not None:\n            id36s = link_text.replace(',', ' ').split()\n            try:\n                links_from_text = Link._byID36(id36s, data=True)\n            except NotFound:\n                links_from_text = {}\n\n            bad_links = [id36 for id36 in id36s if id36 not in links_from_text]\n            links.extend(links_from_text.values())\n\n        content = PromoteReport(links, link_text, owner_name, bad_links, start,\n                                end, group_by_date=grouping == \"day\")\n        if c.render_style == 'csv':\n            return content.as_csv()\n        else:\n            return PromotePage(title=_(\"sponsored link report\"),\n                               content=content).render()\n\n    @validate(\n        VSponsorAdmin(),\n        start=VDate('startdate'),\n        end=VDate('enddate'),\n        sr_name=nop('sr_name'),\n        collection_name=nop('collection_name'),\n    )\n    def GET_promote_inventory(self, start, end, sr_name, collection_name):\n        if not start or not end:\n            start = promote.promo_datetime_now(offset=1).date()\n            end = promote.promo_datetime_now(offset=8).date()\n            c.errors.remove((errors.BAD_DATE, 'startdate'))\n            c.errors.remove((errors.BAD_DATE, 'enddate'))\n\n        target = Target(Frontpage.name)\n        if sr_name:\n            try:\n                sr = Subreddit._by_name(sr_name)\n                target = Target(sr.name)\n            except NotFound:\n                c.errors.add(errors.SUBREDDIT_NOEXIST, field='sr_name')\n        elif collection_name:\n            collection = Collection.by_name(collection_name)\n            if not collection:\n                c.errors.add(errors.COLLECTION_NOEXIST, field='collection_name')\n            else:\n                target = Target(collection)\n\n        content = PromoteInventory(start, end, target)\n\n        if c.render_style == 'csv':\n            return content.as_csv()\n        else:\n            return PromotePage(title=_(\"sponsored link inventory\"),\n                               content=content).render()\n\n    @validate(\n        VSponsorAdmin(),\n        id_user=VByName('name', thing_cls=Account),\n        email=ValidEmail(\"email\"),\n    )\n    def GET_lookup_user(self, id_user, email):\n        email_users = AccountsByCanonicalEmail.get_accounts(email)\n        content = SponsorLookupUser(\n            id_user=id_user, email=email, email_users=email_users)\n        return PromotePage(title=\"look up user\", content=content).render()\n\n\nclass PromoteListingController(ListingController):\n    where = 'promoted'\n    render_cls = PromotePage\n    titles = {\n        'future_promos': N_('unapproved promoted links'),\n        'pending_promos': N_('accepted promoted links'),\n        'unpaid_promos': N_('unpaid promoted links'),\n        'rejected_promos': N_('rejected promoted links'),\n        'live_promos': N_('live promoted links'),\n        'edited_live_promos': N_('edited live promoted links'),\n        'all': N_('all promoted links'),\n    }\n    base_path = '/promoted'\n\n    default_filters = [\n        NamedButton('all_promos', dest='',\n                    use_params=False,\n                    aliases=['/sponsor']),\n        NamedButton('future_promos',\n                    use_params=False),\n        NamedButton('unpaid_promos',\n                    use_params=False),\n        NamedButton('rejected_promos',\n                    use_params=False),\n        NamedButton('pending_promos',\n                    use_params=False),\n        NamedButton('live_promos',\n                    use_params=False),\n        NamedButton('edited_live_promos',\n                    use_params=False),\n    ]\n\n    def title(self):\n        return _(self.titles[self.sort])\n\n    @property\n    def title_text(self):\n        return _('promoted by you')\n\n    @property\n    def menus(self):\n        filters = [\n            NamedButton('all_promos', dest='',\n                        use_params=False,\n                        aliases=['/sponsor']),\n            NamedButton('future_promos',\n                        use_params=False),\n            NamedButton('unpaid_promos',\n                        use_params=False),\n            NamedButton('rejected_promos',\n                        use_params=False),\n            NamedButton('pending_promos',\n                        use_params=False),\n            NamedButton('live_promos',\n                        use_params=False),\n        ]\n        menus = [NavMenu(filters, base_path=self.base_path, title='show',\n                         type='lightdrop')]\n        return menus\n\n    def builder_wrapper(self, thing):\n        builder_wrapper = default_thing_wrapper()\n        w = builder_wrapper(thing)\n        w.hide_after_seen = self.sort == \"future_promos\"\n\n        return w\n\n    def keep_fn(self):\n        def keep(item):\n            if self.sort == \"future_promos\":\n                # this sort is used to review links that need to be approved\n                # skip links that don't have any paid campaigns\n                campaigns = list(PromoCampaign._by_link(item._id))\n                if not any(promote.authed_or_not_needed(camp)\n                           for camp in campaigns):\n                    return False\n\n            if item.promoted and not item._deleted:\n                return True\n            else:\n                return False\n        return keep\n\n    def query(self):\n        if self.sort == \"future_promos\":\n            return queries.get_unapproved_links(c.user._id)\n        elif self.sort == \"pending_promos\":\n            return queries.get_accepted_links(c.user._id)\n        elif self.sort == \"unpaid_promos\":\n            return queries.get_unpaid_links(c.user._id)\n        elif self.sort == \"rejected_promos\":\n            return queries.get_rejected_links(c.user._id)\n        elif self.sort == \"live_promos\":\n            return queries.get_live_links(c.user._id)\n        elif self.sort == \"edited_live_promos\":\n            return queries.get_edited_live_links(c.user._id)\n        elif self.sort == \"all\":\n            return queries.get_promoted_links(c.user._id)\n\n    @validate(VSponsor())\n    def GET_listing(self, sort=\"all\", **env):\n        self.sort = sort\n        return ListingController.GET_listing(self, **env)\n\n\nclass SponsorListingController(PromoteListingController):\n    titles = dict(PromoteListingController.titles.items() + {\n        'underdelivered': N_('underdelivered promoted links'),\n        'reported': N_('reported promoted links'),\n        'house': N_('house promoted links'),\n        'fraud': N_('fraud suspected promoted links'),\n    }.items())\n    base_path = '/sponsor/promoted'\n\n    @property\n    def title_text(self):\n        return _('promos on reddit')\n\n    @property\n    def menus(self):\n        managed_menu = NavMenu([\n            QueryButton(\"exclude managed\", dest=None,\n                        query_param='include_managed'),\n            QueryButton(\"include managed\", dest=\"yes\",\n                        query_param='include_managed'),\n        ], base_path=request.path, type='lightdrop')\n\n        if self.sort in {'underdelivered', 'reported', 'house', 'fraud'}:\n            menus = []\n\n            if self.sort == 'fraud':\n                fraud_menu = NavMenu([\n                    QueryButton(\"exclude unpaid\", dest=None,\n                                query_param='exclude_unpaid'),\n                    QueryButton(\"include unpaid\", dest=\"no\",\n                                query_param='exclude_unpaid'),\n                ], base_path=request.path, type='lightdrop')\n                menus.append(fraud_menu)\n            if self.sort in ('house', 'fraud'):\n                menus.append(managed_menu)\n        else:\n            menus = super(SponsorListingController, self).menus\n            menus.append(managed_menu)\n\n        if self.sort == 'live_promos':\n            srnames = promote.all_live_promo_srnames()\n            buttons = [NavButton('all', '', use_params=True)]\n            try:\n                srnames.remove(Frontpage.name)\n                frontbutton = NavButton('FRONTPAGE', Frontpage.name,\n                                        use_params=True,\n                                        aliases=['/promoted/live_promos/%s' %\n                                                 urllib.quote(Frontpage.name)])\n                buttons.append(frontbutton)\n            except KeyError:\n                pass\n\n            srnames = sorted(srnames, key=lambda name: name.lower())\n            buttons.extend(\n                NavButton(name, name, use_params=True) for name in srnames)\n            base_path = self.base_path + '/live_promos'\n            menus.append(NavMenu(buttons, base_path=base_path,\n                                 title='subreddit', type='lightdrop'))\n        return menus\n\n    @classmethod\n    @memoize('live_by_subreddit', time=300)\n    def _live_by_subreddit(cls, sr_names):\n        promotuples = promote.get_live_promotions(sr_names)\n        return [pt.link for pt in promotuples]\n\n    def live_by_subreddit(cls, sr):\n        return cls._live_by_subreddit([sr.name])\n\n    @classmethod\n    @memoize('house_link_names', time=60)\n    def get_house_link_names(cls):\n        now = promote.promo_datetime_now()\n        campaign_ids = PromotionWeights.get_campaign_ids(now)\n        q = PromoCampaign._query(PromoCampaign.c._id.in_(campaign_ids),\n                                 PromoCampaign.c.priority_name == 'house',\n                                 data=True)\n        link_names = {Link._fullname_from_id36(to36(camp.link_id))\n                      for camp in q}\n        return sorted(link_names, reverse=True)\n\n    def keep_fn(self):\n        base_keep_fn = PromoteListingController.keep_fn(self)\n\n        if self.exclude_unpaid:\n            exclude = set(queries.get_all_unpaid_links())\n        else:\n            exclude = set()\n\n        def keep(item):\n            if not self.include_managed and item.managed_promo:\n                return False\n\n            if self.exclude_unpaid and item._fullname in exclude:\n                return False\n\n            return base_keep_fn(item)\n        return keep\n\n    def query(self):\n        if self.sort == \"future_promos\":\n            return queries.get_all_unapproved_links()\n        elif self.sort == \"pending_promos\":\n            return queries.get_all_accepted_links()\n        elif self.sort == \"unpaid_promos\":\n            return queries.get_all_unpaid_links()\n        elif self.sort == \"rejected_promos\":\n            return queries.get_all_rejected_links()\n        elif self.sort == \"live_promos\" and self.sr:\n            return self.live_by_subreddit(self.sr)\n        elif self.sort == 'live_promos':\n            return queries.get_all_live_links()\n        elif self.sort == 'edited_live_promos':\n            return queries.get_all_edited_live_links()\n        elif self.sort == 'underdelivered':\n            q = queries.get_underdelivered_campaigns()\n            campaigns = PromoCampaign._by_fullname(list(q), data=True,\n                                                   return_dict=False)\n            link_ids = [camp.link_id for camp in campaigns]\n            return [Link._fullname_from_id36(to36(id)) for id in link_ids]\n        elif self.sort == 'reported':\n            return queries.get_reported_links(Subreddit.get_promote_srid())\n        elif self.sort == 'fraud':\n            return queries.get_payment_flagged_links()\n        elif self.sort == 'house':\n            return self.get_house_link_names()\n        elif self.sort == 'all':\n            return queries.get_all_promoted_links()\n\n    def listing(self):\n        \"\"\"For sponsors, update wrapped links to include their campaigns.\"\"\"\n        pane = super(self.__class__, self).listing()\n\n        if c.user_is_sponsor:\n            link_ids = {item._id for item in pane.things}\n            campaigns = PromoCampaign._by_link(link_ids)\n            campaigns_by_link = defaultdict(list)\n            for camp in campaigns:\n                campaigns_by_link[camp.link_id].append(camp)\n\n            for item in pane.things:\n                campaigns = campaigns_by_link[item._id]\n                item.campaigns = RenderableCampaign.from_campaigns(\n                    item, campaigns, full_details=False)\n                item.cachable = False\n                item.show_campaign_summary = True\n        return pane\n\n    @validate(\n        VSponsorAdmin(),\n        srname=nop('sr'),\n        include_managed=VBoolean(\"include_managed\"),\n        exclude_unpaid=VBoolean(\"exclude_unpaid\"),\n    )\n    def GET_listing(self, srname=None, include_managed=False,\n                    exclude_unpaid=None, sort=\"all\", **kw):\n        self.sort = sort\n        self.sr = None\n        self.include_managed = include_managed\n\n        if \"exclude_unpaid\" not in request.GET:\n            self.exclude_unpaid = self.sort == \"fraud\"\n        else:\n            self.exclude_unpaid = exclude_unpaid\n\n        if srname:\n            try:\n                self.sr = Subreddit._by_name(srname)\n            except NotFound:\n                pass\n        return ListingController.GET_listing(self, **kw)\n\n\ndef allowed_location_and_target(location, target):\n    if c.user_is_sponsor or feature.is_enabled('ads_auction'):\n        return True\n\n    # regular users can only use locations when targeting frontpage\n    is_location = location and location.country\n    is_frontpage = (not target.is_collection and\n                    target.subreddit_name == Frontpage.name)\n    return not is_location or is_frontpage\n\n\nclass PromoteApiController(ApiController):\n    @json_validate(sr=VSubmitSR('sr', promotion=True),\n                   collection=VCollection('collection'),\n                   location=VLocation(),\n                   start=VDate('startdate'),\n                   end=VDate('enddate'),\n                   platform=VOneOf('platform', ('mobile', 'desktop', 'all'),\n                                   default='all'))\n    def GET_check_inventory(self, responder, sr, collection, location, start,\n                            end, platform):\n        if collection:\n            target = Target(collection)\n            sr = None\n        else:\n            sr = sr or Frontpage\n            target = Target(sr.name)\n\n        if not allowed_location_and_target(location, target):\n            return abort(403, 'forbidden')\n\n        available = inventory.get_available_pageviews(\n                        target, start, end, location=location, platform=platform,\n                        datestr=True)\n\n        return {'inventory': available}\n\n    @validatedForm(VSponsorAdmin(),\n                   VModhash(),\n                   link=VLink(\"link_id36\"),\n                   campaign=VPromoCampaign(\"campaign_id36\"))\n    def POST_freebie(self, form, jquery, link, campaign):\n        if not link or not campaign or link._id != campaign.link_id:\n            return abort(404, 'not found')\n\n        if campaign_has_oversold_error(form, campaign):\n            form.set_text(\".freebie\", _(\"target oversold, can't freebie\"))\n            return\n\n        if promote.is_promo(link) and campaign:\n            promote.free_campaign(link, campaign, c.user)\n            form.redirect(promote.promo_edit_url(link))\n\n    @validatedForm(VSponsorAdmin(),\n                   VModhash(),\n                   link=VByName(\"link\"),\n                   note=nop(\"note\"))\n    def POST_promote_note(self, form, jquery, link, note):\n        if promote.is_promo(link):\n            text = PromotionLog.add(link, note)\n            form.find(\".notes\").children(\":last\").after(\n                format_html(\"<p>%s</p>\", text))\n\n    @validatedForm(\n        VSponsorAdmin(),\n        VModhash(),\n        thing = VByName(\"thing_id\"),\n        is_fraud=VBoolean(\"fraud\"),\n    )\n    def POST_review_fraud(self, form, jquery, thing, is_fraud):\n        if not thing or not getattr(thing, \"promoted\", False):\n            return\n\n        promote.review_fraud(thing, is_fraud)\n\n        button = jquery(\".id-%s .fraud-button\" % thing._fullname)\n        button.text(_(\"fraud\" if is_fraud else \"not fraud\"))\n        form.parents('.link').fadeOut()\n\n    @noresponse(VSponsorAdmin(),\n                VModhash(),\n                thing=VByName('id'))\n    def POST_promote(self, thing):\n        if promote.is_promo(thing):\n            promote.accept_promotion(thing)\n\n    @noresponse(VSponsorAdmin(),\n                VModhash(),\n                thing=VByName('id'),\n                reason=nop(\"reason\"))\n    def POST_unpromote(self, thing, reason):\n        if promote.is_promo(thing):\n            promote.reject_promotion(thing, reason=reason)\n\n    @validatedForm(VSponsorAdmin(),\n                   VModhash(),\n                   link=VLink('link'),\n                   campaign=VPromoCampaign('campaign'))\n    def POST_refund_campaign(self, form, jquery, link, campaign):\n        if not link or not campaign or link._id != campaign.link_id:\n            return abort(404, 'not found')\n\n        # If created before switch to auction, use old billing method\n        if hasattr(campaign, 'cpm'):\n            billable_impressions = promote.get_billable_impressions(campaign)\n            billable_amount = promote.get_billable_amount(campaign,\n                billable_impressions)\n            refund_amount = promote.get_refund_amount(campaign, billable_amount)\n        # Otherwise, use adserver_spent_pennies\n        else:\n            billable_amount = campaign.total_budget_pennies / 100.\n            refund_amount = (billable_amount -\n                (campaign.adserver_spent_pennies / 100.))\n            billable_impressions = None\n\n        if refund_amount <= 0:\n            form.set_text('.status', _('refund not needed'))\n            return\n\n        if promote.refund_campaign(link, campaign, refund_amount,\n                billable_amount, billable_impressions):\n            form.set_text('.status', _('refund succeeded'))\n        else:\n            form.set_text('.status', _('refund failed'))\n\n    @validatedForm(\n        VSponsor('link_id36'),\n        VModhash(),\n        VRatelimit(rate_user=True,\n                   rate_ip=True,\n                   prefix='create_promo_'),\n        VShamedDomain('url'),\n        username=VLength('username', 100, empty_error=None),\n        title=VTitle('title'),\n        url=VUrl('url', allow_self=False),\n        selftext=VMarkdownLength('text', max_length=40000),\n        kind=VOneOf('kind', ['link', 'self']),\n        disable_comments=VBoolean(\"disable_comments\"),\n        sendreplies=VBoolean(\"sendreplies\"),\n        media_url=VUrl(\"media_url\", allow_self=False,\n                       valid_schemes=('http', 'https')),\n        gifts_embed_url=VUrl(\"gifts_embed_url\", allow_self=False,\n                             valid_schemes=('http', 'https')),\n        media_url_type=VOneOf(\"media_url_type\", (\"redditgifts\", \"scrape\")),\n        media_autoplay=VBoolean(\"media_autoplay\"),\n        media_override=VBoolean(\"media-override\"),\n        domain_override=VLength(\"domain\", 100),\n        third_party_tracking=VUrl(\"third_party_tracking\"),\n        third_party_tracking_2=VUrl(\"third_party_tracking_2\"),\n        is_managed=VBoolean(\"is_managed\"),\n    )\n    def POST_create_promo(self, form, jquery, username, title, url,\n                          selftext, kind, disable_comments, sendreplies,\n                          media_url, media_autoplay, media_override,\n                          iframe_embed_url, media_url_type, domain_override,\n                          third_party_tracking, third_party_tracking_2,\n                          is_managed):\n\n        images = _get_ads_images(c.user, data=True, meta=True)\n\n        return self._edit_promo(\n            form, jquery, username, title, url,\n            selftext, kind, disable_comments, sendreplies,\n            media_url, media_autoplay, media_override,\n            iframe_embed_url, media_url_type, domain_override,\n            third_party_tracking, third_party_tracking_2, is_managed,\n            thumbnail=images.get(\"thumbnail\", None),\n            mobile=images.get(\"mobile\", None),\n        )\n\n    @validatedForm(\n        VSponsor('link_id36'),\n        VModhash(),\n        VRatelimit(rate_user=True,\n                   rate_ip=True,\n                   prefix='create_promo_'),\n        VShamedDomain('url'),\n        username=VLength('username', 100, empty_error=None),\n        title=VTitle('title'),\n        url=VUrl('url', allow_self=False),\n        selftext=VMarkdownLength('text', max_length=40000),\n        kind=VOneOf('kind', ['link', 'self']),\n        disable_comments=VBoolean(\"disable_comments\"),\n        sendreplies=VBoolean(\"sendreplies\"),\n        media_url=VUrl(\"media_url\", allow_self=False,\n                       valid_schemes=('http', 'https')),\n        gifts_embed_url=VUrl(\"gifts_embed_url\", allow_self=False,\n                             valid_schemes=('http', 'https')),\n        media_url_type=VOneOf(\"media_url_type\", (\"redditgifts\", \"scrape\")),\n        media_autoplay=VBoolean(\"media_autoplay\"),\n        media_override=VBoolean(\"media-override\"),\n        domain_override=VLength(\"domain\", 100),\n        third_party_tracking=VUrl(\"third_party_tracking\"),\n        third_party_tracking_2=VUrl(\"third_party_tracking_2\"),\n        is_managed=VBoolean(\"is_managed\"),\n        l=VLink('link_id36'),\n    )\n    def POST_edit_promo(self, form, jquery, username, title, url,\n                        selftext, kind, disable_comments, sendreplies,\n                        media_url, media_autoplay, media_override,\n                        iframe_embed_url, media_url_type, domain_override,\n                        third_party_tracking, third_party_tracking_2,\n                        is_managed, l):\n\n        images = _get_ads_images(l, data=True, meta=True)\n\n        return self._edit_promo(\n            form, jquery, username, title, url,\n            selftext, kind, disable_comments, sendreplies,\n            media_url, media_autoplay, media_override,\n            iframe_embed_url, media_url_type, domain_override,\n            third_party_tracking, third_party_tracking_2, is_managed,\n            l=l,\n            thumbnail=images.get(\"thumbnail\", None),\n            mobile=images.get(\"mobile\", None),\n        )\n\n    def _edit_promo(self, form, jquery, username, title, url,\n                    selftext, kind, disable_comments, sendreplies,\n                    media_url, media_autoplay, media_override,\n                    iframe_embed_url, media_url_type, domain_override,\n                    third_party_tracking, third_party_tracking_2,\n                    is_managed, l=None, thumbnail=None, mobile=None):\n        should_ratelimit = False\n        is_self = (kind == \"self\")\n        is_link = not is_self\n        is_new_promoted = not l\n        if not c.user_is_sponsor:\n            should_ratelimit = True\n\n        if not should_ratelimit:\n            c.errors.remove((errors.RATELIMIT, 'ratelimit'))\n\n        # check for user override\n        if is_new_promoted and c.user_is_sponsor and username:\n            try:\n                user = Account._by_name(username)\n            except NotFound:\n                c.errors.add(errors.USER_DOESNT_EXIST, field=\"username\")\n                form.set_error(errors.USER_DOESNT_EXIST, \"username\")\n                return\n\n            if not user.email:\n                c.errors.add(errors.NO_EMAIL_FOR_USER, field=\"username\")\n                form.set_error(errors.NO_EMAIL_FOR_USER, \"username\")\n                return\n\n            if not user.email_verified:\n                c.errors.add(errors.NO_VERIFIED_EMAIL, field=\"username\")\n                form.set_error(errors.NO_VERIFIED_EMAIL, \"username\")\n                return\n        else:\n            user = c.user\n\n        # check for shame banned domains\n        if form.has_errors(\"url\", errors.DOMAIN_BANNED):\n            g.stats.simple_event('spam.shame.link')\n            return\n\n        # demangle URL in canonical way\n        if url:\n            if isinstance(url, (unicode, str)):\n                form.set_inputs(url=url)\n            elif isinstance(url, tuple) or isinstance(url[0], Link):\n                # there's already one or more links with this URL, but\n                # we're allowing mutliple submissions, so we really just\n                # want the URL\n                url = url[0].url\n\n            # Adzerk limits URLs length for creatives\n            if len(url) > ADZERK_URL_MAX_LENGTH:\n                c.errors.add(errors.TOO_LONG, field='url',\n                    msg_params={'max_length': PROMO_URL_MAX_LENGTH})\n\n        if is_link:\n            if form.has_errors('url', errors.NO_URL, errors.BAD_URL,\n                    errors.TOO_LONG):\n                return\n\n        # users can change the disable_comments on promoted links\n        if ((is_new_promoted or not promote.is_promoted(l)) and\n            (form.has_errors('title', errors.NO_TEXT, errors.TOO_LONG) or\n             jquery.has_errors('ratelimit', errors.RATELIMIT))):\n            return\n\n        if is_self and form.has_errors('text', errors.TOO_LONG):\n            return\n\n        if is_new_promoted:\n            # creating a new promoted link\n            l = promote.new_promotion(\n                is_self=is_self,\n                title=title,\n                content=(selftext if is_self else url),\n                author=user,\n                ip=request.ip,\n            )\n\n            if c.user_is_sponsor:\n                l.managed_promo = is_managed\n                l.domain_override = domain_override or None\n                l.third_party_tracking = third_party_tracking or None\n                l.third_party_tracking_2 = third_party_tracking_2 or None\n            l._commit()\n\n            _force_images(l, thumbnail=thumbnail, mobile=mobile)\n\n            form.redirect(promote.promo_edit_url(l))\n\n        elif not promote.is_promo(l):\n            return\n\n        changed = False\n        if title and title != l.title:\n            l.title = title\n            changed = True\n\n        if _force_images(l, thumbnail=thumbnail, mobile=mobile):\n            changed = True\n\n        # type changing\n        if is_self != l.is_self:\n            l.set_content(is_self, selftext if is_self else url)\n            changed = True\n\n        if is_link and url and url != l.url:\n            l.url = url\n            changed = True\n\n        # only trips if changed by a non-sponsor\n        if changed and not c.user_is_sponsor and promote.is_promoted(l):\n            promote.edited_live_promotion(l)\n\n        # selftext can be changed at any time\n        if is_self:\n            l.selftext = selftext\n\n        # comment disabling and sendreplies is free to be changed any time.\n        l.disable_comments = disable_comments\n        l.sendreplies = sendreplies\n\n        if c.user_is_sponsor:\n            if (form.has_errors(\"media_url\", errors.BAD_URL) or\n                    form.has_errors(\"gifts_embed_url\", errors.BAD_URL)):\n                return\n\n        scraper_embed = media_url_type == \"scrape\"\n        media_url = media_url or None\n        gifts_embed_url = gifts_embed_url or None\n\n        if c.user_is_sponsor and scraper_embed and media_url != l.media_url:\n            if media_url:\n                scraped = media._scrape_media(\n                    media_url, autoplay=media_autoplay,\n                    save_thumbnail=False, use_cache=True)\n\n                if scraped:\n                    l.set_media_object(scraped.media_object)\n                    l.set_secure_media_object(scraped.secure_media_object)\n                    l.media_url = media_url\n                    l.gifts_embed_url = None\n                    l.media_autoplay = media_autoplay\n                else:\n                    c.errors.add(errors.SCRAPER_ERROR, field=\"media_url\")\n                    form.set_error(errors.SCRAPER_ERROR, \"media_url\")\n                    return\n            else:\n                l.set_media_object(None)\n                l.set_secure_media_object(None)\n                l.media_url = None\n                l.gifts_embed_url = None\n                l.media_autoplay = False\n\n        if (c.user_is_sponsor and not scraper_embed and\n                gifts_embed_url != l.gifts_embed_url):\n            if gifts_embed_url:\n                parsed = UrlParser(gifts_embed_url)\n                if not is_subdomain(parsed.hostname, \"redditgifts.com\"):\n                    c.errors.add(errors.BAD_URL, field=\"gifts_embed_url\")\n                    form.set_error(errors.BAD_URL, \"gifts_embed_url\")\n                    return\n\n                sandbox = (\n                    'allow-popups',\n                    'allow-forms',\n                    'allow-same-origin',\n                    'allow-scripts',\n                )\n                iframe_attributes = {\n                    'embed_url': websafe(iframe_embed_url),\n                    'sandbox': ' '.join(sandbox),\n                }\n                iframe = \"\"\"\n                    <iframe class=\"redditgifts-embed\"\n                            src=\"%(embed_url)s\"\n                            width=\"710\" height=\"500\" scrolling=\"no\"\n                            frameborder=\"0\" allowfullscreen\n                            sandbox=\"%(sandbox)s\">\n                    </iframe>\n                \"\"\" % iframe_attributes\n                media_object = {\n                    'oembed': {\n                        'description': 'redditgifts embed',\n                        'height': 500,\n                        'html': iframe,\n                        'provider_name': 'redditgifts',\n                        'provider_url': 'http://www.redditgifts.com/',\n                        'title': 'redditgifts secret santa 2014',\n                        'type': 'rich',\n                        'width': 710},\n                        'type': 'redditgifts'\n                }\n                l.set_media_object(media_object)\n                l.set_secure_media_object(media_object)\n                l.media_url = None\n                l.gifts_embed_url = gifts_embed_url\n                l.media_autoplay = False\n            else:\n                l.set_media_object(None)\n                l.set_secure_media_object(None)\n                l.media_url = None\n                l.gifts_embed_url = None\n                l.media_autoplay = False\n\n        if c.user_is_sponsor:\n            l.media_override = media_override\n            l.domain_override = domain_override or None\n            l.third_party_tracking = third_party_tracking or None\n            l.third_party_tracking_2 = third_party_tracking_2 or None\n            l.managed_promo = is_managed\n\n        l._commit()\n\n        # ensure plugins are notified of the final edits to the link.\n        # other methods also call this hook earlier in the process.\n        # see: `promote.unapprove_promotion`\n        if not is_new_promoted:\n            hooks.get_hook('promote.edit_promotion').call(link=l)\n\n        # clean up so the same images don't reappear if they create\n        # another link\n        _clear_ads_images(thing=c.user if is_new_promoted else l)\n\n        form.redirect(promote.promo_edit_url(l))\n\n    def _lowest_max_cpm_bid_dollars(self, total_budget_dollars, bid_dollars,\n                                    start, end):\n        \"\"\"\n        Calculate the lower between g.max_bid_pennies\n        and maximum bid per day by budget\n        \"\"\"\n        ndays = (to_date(end) - to_date(start)).days\n        max_daily_bid = total_budget_dollars / ndays\n        max_bid_dollars = g.max_bid_pennies / 100.\n\n        return min(max_daily_bid, max_bid_dollars)\n\n    @validatedForm(\n        VSponsor('link_id36'),\n        VModhash(),\n        is_auction=VBoolean('is_auction'),\n        start=VDate('startdate', required=False),\n        end=VDate('enddate'),\n        link=VLink('link_id36'),\n        target=VPromoTarget(),\n        campaign_id36=nop(\"campaign_id36\"),\n        frequency_cap=VFrequencyCap((\"frequency_capped\",\n                                     \"frequency_cap\"),),\n        priority=VPriority(\"priority\"),\n        location=VLocation(),\n        platform=VOneOf(\"platform\", (\"mobile\", \"desktop\", \"all\"), default=\"desktop\"),\n        mobile_os=VList(\"mobile_os\", choices=[\"iOS\", \"Android\"]),\n        os_versions=VOneOf('os_versions', ('all', 'filter'), default='all'),\n        ios_devices=VList('ios_device', choices=IOS_DEVICES),\n        android_devices=VList('android_device', choices=ANDROID_DEVICES),\n        ios_versions=VOSVersion('ios_version_range', 'ios'),\n        android_versions=VOSVersion('android_version_range', 'android'),\n        total_budget_dollars=VFloat('total_budget_dollars', coerce=False),\n        cost_basis=VOneOf('cost_basis', ('cpc', 'cpm',), default=None),\n        bid_dollars=VFloat('bid_dollars', coerce=True),\n    )\n    def POST_edit_campaign(self, form, jquery, is_auction, link, campaign_id36,\n                           start, end, target, frequency_cap,\n                           priority, location, platform, mobile_os,\n                           os_versions, ios_devices, ios_versions,\n                           android_devices, android_versions,\n                           total_budget_dollars, cost_basis, bid_dollars):\n        if not link:\n            return\n\n        if (form.has_errors('frequency_cap', errors.INVALID_FREQUENCY_CAP) or\n                form.has_errors('frequency_cap', errors.FREQUENCY_CAP_TOO_LOW)):\n            return\n\n        if not target:\n            # run form.has_errors to populate the errors in the response\n            form.has_errors('sr', errors.SUBREDDIT_NOEXIST,\n                            errors.SUBREDDIT_NOTALLOWED,\n                            errors.SUBREDDIT_REQUIRED)\n            form.has_errors('collection', errors.COLLECTION_NOEXIST)\n            form.has_errors('targeting', errors.INVALID_TARGET)\n            return\n\n        if form.has_errors('location', errors.INVALID_LOCATION):\n            return\n\n        if not allowed_location_and_target(location, target):\n            return abort(403, 'forbidden')\n\n        if (form.has_errors('startdate', errors.BAD_DATE) or\n                form.has_errors('enddate', errors.BAD_DATE)):\n            return\n\n        if not campaign_id36 and not start:\n            c.errors.add(errors.BAD_DATE, field='startdate')\n            form.set_error('startdate', errors.BAD_DATE)\n\n        if (not feature.is_enabled('mobile_targeting') and\n                platform != 'desktop'):\n            return abort(403, 'forbidden')\n\n        if link.over_18 and not target.over_18:\n            c.errors.add(errors.INVALID_NSFW_TARGET, field='targeting')\n            form.has_errors('targeting', errors.INVALID_NSFW_TARGET)\n            return\n\n        if not feature.is_enabled('cpc_pricing'):\n            cost_basis = 'cpm'\n\n        # Setup campaign details for existing campaigns\n        campaign = None\n        if campaign_id36:\n            try:\n                campaign = PromoCampaign._byID36(campaign_id36, data=True)\n            except NotFound:\n                pass\n\n            if (not campaign\n                    or (campaign._deleted or link._id != campaign.link_id)):\n                return abort(404, 'not found')\n\n            requires_reapproval = False\n            is_live = promote.is_live_promo(link, campaign)\n            is_complete = promote.is_complete_promo(link, campaign)\n\n            if not c.user_is_sponsor:\n                # If campaign is live, start_date and total_budget_dollars\n                # must not be changed\n                if is_live:\n                    start = campaign.start_date\n                    total_budget_dollars = campaign.total_budget_dollars\n\n        # Configure priority, cost_basis, and bid_pennies\n        if feature.is_enabled('ads_auction'):\n            if c.user_is_sponsor:\n                if is_auction:\n                    priority = PROMOTE_PRIORITIES['auction']\n                    cost_basis = PROMOTE_COST_BASIS[cost_basis]\n                else:\n                    cost_basis = PROMOTE_COST_BASIS.fixed_cpm\n            else:\n                # if non-sponsor, is_auction is not part of the POST request,\n                # so must be set independently\n                is_auction = True\n                priority = PROMOTE_PRIORITIES['auction']\n                cost_basis = PROMOTE_COST_BASIS[cost_basis]\n\n                # Error if bid is outside acceptable range\n                min_bid_dollars = g.min_bid_pennies / 100.\n                max_bid_dollars = self._lowest_max_bid_dollars(\n                    total_budget_dollars=total_budget_dollars,\n                    bid_dollars=bid_dollars,\n                    start=start,\n                    end=end)\n\n                if bid_dollars < min_bid_dollars or bid_dollars > max_bid_dollars:\n                    c.errors.add(errors.BAD_BID, field='bid',\n                        msg_params={'min': '%.2f' % round(min_bid_dollars, 2),\n                                    'max': '%.2f' % round(max_bid_dollars, 2)}\n                    )\n                    form.has_errors('bid', errors.BAD_BID)\n                    return\n\n        else:\n            cost_basis = PROMOTE_COST_BASIS.fixed_cpm\n\n        if priority == PROMOTE_PRIORITIES['auction']:\n            bid_pennies = bid_dollars * 100\n        else:\n            link_owner = Account._byID(link.author_id)\n            bid_pennies = PromotionPrices.get_price(link_owner, target,\n                location)\n\n        if platform == 'desktop':\n            mobile_os = None\n        else:\n            # check if platform includes mobile, but no mobile OS is selected\n            if not mobile_os:\n                c.errors.add(errors.BAD_PROMO_MOBILE_OS, field='mobile_os')\n                form.set_error(errors.BAD_PROMO_MOBILE_OS, 'mobile_os')\n                return\n            elif os_versions == 'filter':\n                # check if OS is selected, but OS devices are not\n                if (('iOS' in mobile_os and not ios_devices) or\n                        ('Android' in mobile_os and not android_devices)):\n                    c.errors.add(errors.BAD_PROMO_MOBILE_DEVICE, field='os_versions')\n                    form.set_error(errors.BAD_PROMO_MOBILE_DEVICE, 'os_versions')\n                    return\n                # check if OS versions are invalid\n                if form.has_errors('os_version', errors.INVALID_OS_VERSION):\n                    c.errors.add(errors.INVALID_OS_VERSION, field='os_version')\n                    form.set_error(errors.INVALID_OS_VERSION, 'os_version')\n                    return\n\n        min_start, max_start, max_end = promote.get_date_limits(\n            link, c.user_is_sponsor)\n\n        if campaign:\n            if feature.is_enabled('ads_auction'):\n                # non-sponsors cannot update fixed CPM campaigns,\n                # even if they haven't launched (due to auction)\n                if not c.user_is_sponsor and not campaign.is_auction:\n                    c.errors.add(errors.COST_BASIS_CANNOT_CHANGE,\n                        field='cost_basis')\n                    form.set_error(errors.COST_BASIS_CANNOT_CHANGE, 'cost_basis')\n                    return\n\n            if not c.user_is_sponsor:\n                # If target is changed, require reapproval\n                if campaign.target != target:\n                    requires_reapproval = True\n\n            if campaign.start_date.date() != start.date():\n                # Can't edit the start date of campaigns that have served\n                if campaign.has_served:\n                    c.errors.add(errors.START_DATE_CANNOT_CHANGE, field='startdate')\n                    form.has_errors('startdate', errors.START_DATE_CANNOT_CHANGE)\n                    return\n\n                if is_live or is_complete:\n                    c.errors.add(errors.START_DATE_CANNOT_CHANGE, field='startdate')\n                    form.has_errors('startdate', errors.START_DATE_CANNOT_CHANGE)\n                    return\n\n        elif start.date() < min_start:\n            c.errors.add(errors.DATE_TOO_EARLY,\n                         msg_params={'day': min_start.strftime(\"%m/%d/%Y\")},\n                         field='startdate')\n            form.has_errors('startdate', errors.DATE_TOO_EARLY)\n            return\n\n        if start.date() > max_start:\n            c.errors.add(errors.DATE_TOO_LATE,\n                         msg_params={'day': max_start.strftime(\"%m/%d/%Y\")},\n                         field='startdate')\n            form.has_errors('startdate', errors.DATE_TOO_LATE)\n            return\n\n        if end.date() > max_end:\n            c.errors.add(errors.DATE_TOO_LATE,\n                         msg_params={'day': max_end.strftime(\"%m/%d/%Y\")},\n                         field='enddate')\n            form.has_errors('enddate', errors.DATE_TOO_LATE)\n            return\n\n        if end < start:\n            c.errors.add(errors.BAD_DATE_RANGE, field='enddate')\n            form.has_errors('enddate', errors.BAD_DATE_RANGE)\n            return\n\n        # Limit the number of PromoCampaigns a Link can have\n        # Note that the front end should prevent the user from getting\n        # this far\n        existing_campaigns = list(PromoCampaign._by_link(link._id))\n        if len(existing_campaigns) > g.MAX_CAMPAIGNS_PER_LINK:\n            c.errors.add(errors.TOO_MANY_CAMPAIGNS,\n                         msg_params={'count': g.MAX_CAMPAIGNS_PER_LINK},\n                         field='title')\n            form.has_errors('title', errors.TOO_MANY_CAMPAIGNS)\n            return\n\n        if not priority == PROMOTE_PRIORITIES['house']:\n            # total_budget_dollars is submitted as a float;\n            # convert it to pennies\n            total_budget_pennies = int(total_budget_dollars * 100)\n            if c.user_is_sponsor:\n                min_total_budget_pennies = 0\n                max_total_budget_pennies = 0\n            else:\n                min_total_budget_pennies = g.min_total_budget_pennies\n                max_total_budget_pennies = g.max_total_budget_pennies\n\n            if (total_budget_pennies is None or\n                    total_budget_pennies < min_total_budget_pennies or\n                    (max_total_budget_pennies and\n                    total_budget_pennies > max_total_budget_pennies)):\n                c.errors.add(errors.BAD_BUDGET, field='total_budget_dollars',\n                             msg_params={'min': min_total_budget_pennies,\n                                         'max': max_total_budget_pennies or\n                                         g.max_total_budget_pennies})\n                form.has_errors('total_budget_dollars', errors.BAD_BUDGET)\n                return\n\n            # you cannot edit the bid of a live ad unless it's a freebie\n            if (campaign and\n                    total_budget_pennies != campaign.total_budget_pennies and\n                    promote.is_live_promo(link, campaign) and\n                    not campaign.is_freebie()):\n                c.errors.add(errors.BUDGET_LIVE, field='total_budget_dollars')\n                form.has_errors('total_budget_dollars', errors.BUDGET_LIVE)\n                return\n        else:\n            total_budget_pennies = 0\n\n        # Check inventory\n        campaign = campaign if campaign_id36 else None\n        if not priority.inventory_override:\n            oversold = has_oversold_error(form, campaign, start, end,\n                                          total_budget_pennies, bid_pennies,\n                                          target, location)\n            if oversold:\n                return\n\n        # Always set frequency_cap_default for auction campaign if frequency_cap\n        # is not set\n        if not frequency_cap and is_auction:\n            frequency_cap = g.frequency_cap_default\n\n        dates = (start, end)\n\n        campaign_dict = {\n            'dates': dates,\n            'target': target,\n            'frequency_cap': frequency_cap,\n            'priority': priority,\n            'location': location,\n            'total_budget_pennies': total_budget_pennies,\n            'cost_basis': cost_basis,\n            'bid_pennies': bid_pennies,\n            'platform': platform,\n            'mobile_os': mobile_os,\n            'ios_devices': ios_devices,\n            'ios_version_range': ios_versions,\n            'android_devices': android_devices,\n            'android_version_range': android_versions,\n        }\n\n        if campaign:\n            if requires_reapproval and promote.is_accepted(link):\n                campaign_dict['is_approved'] = False\n\n            promote.edit_campaign(\n                link,\n                campaign,\n                **campaign_dict\n            )\n        else:\n            campaign = promote.new_campaign(\n                link,\n                **campaign_dict\n            )\n        rc = RenderableCampaign.from_campaigns(link, campaign)\n        jquery.update_campaign(campaign._fullname, rc.render_html())\n\n    @validatedForm(VSponsor('link_id36'),\n                   VModhash(),\n                   l=VLink('link_id36'),\n                   campaign=VPromoCampaign(\"campaign_id36\"))\n    def POST_delete_campaign(self, form, jquery, l, campaign):\n        if not campaign or not l or l._id != campaign.link_id:\n            return abort(404, 'not found')\n\n        promote.delete_campaign(l, campaign)\n\n    @validatedForm(\n        VSponsor('link_id36'),\n        VModhash(),\n        link=VLink('link_id36'),\n        campaign=VPromoCampaign('campaign_id36'),\n        should_pause=VBoolean('should_pause'),)\n    def POST_toggle_pause_campaign(self, form, jquery, link, campaign,\n            should_pause=False):\n        if (not link or not campaign or link._id != campaign.link_id\n                or not feature.is_enabled('pause_ads')):\n            return abort(404, 'not found')\n\n        if campaign.paused == should_pause:\n            return\n\n        promote.toggle_pause_campaign(link, campaign, should_pause)\n        rc = RenderableCampaign.from_campaigns(link, campaign)\n        jquery.update_campaign(campaign._fullname, rc.render_html())\n\n    @validatedForm(VSponsorAdmin(),\n                   VModhash(),\n                   link=VLink('link_id36'),\n                   campaign=VPromoCampaign(\"campaign_id36\"))\n    def POST_terminate_campaign(self, form, jquery, link, campaign):\n        if not link or not campaign or link._id != campaign.link_id:\n            return abort(404, 'not found')\n\n        promote.terminate_campaign(link, campaign)\n        rc = RenderableCampaign.from_campaigns(link, campaign)\n        jquery.update_campaign(campaign._fullname, rc.render_html())\n\n    @validatedForm(\n        VVerifiedSponsor('link'),\n        VModhash(),\n        link=VByName(\"link\"),\n        campaign=VPromoCampaign(\"campaign\"),\n        customer_id=VInt(\"customer_id\", min=0),\n        pay_id=VInt(\"account\", min=0),\n        edit=VBoolean(\"edit\"),\n        address=ValidAddress(\n            [\"firstName\", \"lastName\", \"company\", \"address\", \"city\", \"state\",\n             \"zip\", \"country\", \"phoneNumber\"]\n        ),\n        creditcard=ValidCard([\"cardNumber\", \"expirationDate\", \"cardCode\"]),\n    )\n    def POST_update_pay(self, form, jquery, link, campaign, customer_id, pay_id,\n                        edit, address, creditcard):\n\n        def _handle_failed_payment(reason=None):\n            promote.failed_payment_method(c.user, link)\n            msg = reason or _(\"failed to authenticate card. sorry.\")\n            form.set_text(\".status\", msg)\n\n        if not g.authorizenetapi:\n            return\n\n        if not link or not campaign or link._id != campaign.link_id:\n            return abort(404, 'not found')\n\n        # Check inventory\n        if not campaign.is_auction:\n            if campaign_has_oversold_error(form, campaign):\n                return\n\n        # check the campaign dates are still valid (user may have created\n        # the campaign a few days ago)\n        min_start, max_start, max_end = promote.get_date_limits(\n            link, c.user_is_sponsor)\n\n        if campaign.start_date.date() > max_start:\n            msg = _(\"please change campaign start date to %(date)s or earlier\")\n            date = format_date(max_start, format=\"short\", locale=c.locale)\n            msg %= {'date': date}\n            form.set_text(\".status\", msg)\n            return\n\n        if campaign.start_date.date() < min_start:\n            msg = _(\"please change campaign start date to %(date)s or later\")\n            date = format_date(min_start, format=\"short\", locale=c.locale)\n            msg %= {'date': date}\n            form.set_text(\".status\", msg)\n            return\n\n        new_payment = not pay_id\n\n        address_modified = new_payment or edit\n        if address_modified:\n            address_fields = [\"firstName\", \"lastName\", \"company\", \"address\",\n                              \"city\", \"state\", \"zip\", \"country\", \"phoneNumber\"]\n            card_fields = [\"cardNumber\", \"expirationDate\", \"cardCode\"]\n\n            if (form.has_errors(address_fields, errors.BAD_ADDRESS) or\n                    form.has_errors(card_fields, errors.BAD_CARD)):\n                return\n\n            try:\n                pay_id = add_or_update_payment_method(\n                    c.user, address, creditcard, pay_id)\n\n                if pay_id:\n                    promote.new_payment_method(user=c.user,\n                                               ip=request.ip,\n                                               address=address,\n                                               link=link)\n\n            except AuthorizeNetException:\n                _handle_failed_payment()\n                return\n\n        if pay_id:\n            success, reason = promote.auth_campaign(link, campaign, c.user,\n                                                    pay_id)\n\n            if success:\n                hooks.get_hook(\"promote.campaign_paid\").call(link=link, campaign=campaign)\n                if not address and g.authorizenetapi:\n                    profiles = get_or_create_customer_profile(c.user).paymentProfiles\n                    profile = {p.customerPaymentProfileId: p for p in profiles}[pay_id]\n\n                    address = profile.billTo\n\n                promote.successful_payment(link, campaign, request.ip, address)\n\n                jquery.payment_redirect(promote.promo_edit_url(link),\n                        new_payment, campaign.total_budget_pennies)\n                return\n            else:\n                _handle_failed_payment(reason)\n\n        else:\n            _handle_failed_payment()\n\n    @json_validate(\n        VSponsor(\"link\"),\n        VModhash(),\n        link=VLink(\"link\"),\n        kind=VOneOf(\"kind\", [\"thumbnail\", \"mobile\"]),\n        filepath=nop(\"filepath\"),\n        ajax=VBoolean(\"ajax\", default=True)\n    )\n    def POST_ad_s3_params(self, responder, link, kind, filepath, ajax):\n        filename, ext = os.path.splitext(filepath)\n        mime_type, encoding = mimetypes.guess_type(filepath)\n\n        if not mime_type or mime_type not in ALLOWED_IMAGE_TYPES:\n            request.environ[\"extra_error_data\"] = {\n                \"message\": _(\"image must be a jpg or png\"),\n            }\n            abort(403)\n\n        keyspace = _get_ads_keyspace(link if link else c.user)\n        key = os.path.join(keyspace, kind)\n        redirect = None\n\n        if not ajax:\n            now = datetime.now().replace(tzinfo=g.tz)\n            signature = _get_callback_hmac(\n                username=c.user.name,\n                key=key,\n                expires=now,\n            )\n            path = (\"/api/ad_s3_callback?hmac=%s&ts=%s\" %\n                (signature, _format_expires(now)))\n            redirect = add_sr(path, sr_path=False)\n\n        return s3_helpers.get_post_args(\n            bucket=g.s3_client_uploads_bucket,\n            key=key,\n            success_action_redirect=redirect,\n            success_action_status=\"201\",\n            content_type=mime_type,\n            meta={\n                \"x-amz-meta-ext\": ext,\n            },\n        )\n\n    @validate(\n        VSponsor(),\n        expires=VDate(\"ts\", format=EXPIRES_DATE_FORMAT),\n        signature=VPrintable(\"hmac\", 255),\n        callback=nop(\"callback\"),\n        key=nop(\"key\"),\n    )\n    def GET_ad_s3_callback(self, expires, signature, callback, key):\n        now = datetime.now(tz=g.tz)\n        if (expires + timedelta(minutes=10) < now):\n            self.abort404()\n\n        expected_mac = _get_callback_hmac(\n            username=c.user.name,\n            key=key,\n            expires=expires,\n        )\n\n        if not constant_time_compare(signature, expected_mac):\n            self.abort404()\n\n        template = \"<script>parent.__s3_callbacks__[%(callback)s](%(data)s);</script>\"\n        image = _key_to_dict(\n            s3_helpers.get_key(g.s3_client_uploads_bucket, key))\n        response = {\n            \"callback\": scriptsafe_dumps(callback),\n            \"data\": scriptsafe_dumps(image),\n        }\n\n        return format_html(template, response)\n"
  },
  {
    "path": "r2/r2/controllers/reddit_base.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport collections\nimport json\nimport re\nimport simplejson\nimport socket\nimport itertools\n\nfrom Cookie import CookieError\nfrom copy import copy\nfrom datetime import datetime, timedelta\nfrom functools import wraps\nfrom hashlib import sha1, md5\nfrom urllib import quote, unquote\nfrom urlparse import urlparse\n\nimport babel.core\nimport pylibmc\n\nfrom mako.filters import url_escape\nfrom pylons import request, response\nfrom pylons import tmpl_context as c\nfrom pylons import app_globals as g\nfrom pylons.i18n import _\nfrom pylons.i18n.translation import LanguageError\n\nfrom r2.config import feature\nfrom r2.config.extensions import is_api, set_extension\nfrom r2.lib import (\n    baseplate_integration,\n    filters,\n    geoip,\n    hooks,\n    pages,\n    ratelimit,\n    utils,\n)\nfrom r2.lib.base import BaseController, abort\nfrom r2.lib.cookies import (\n    change_user_cookie_security,\n    Cookies,\n    Cookie,\n    delete_secure_session_cookie,\n    have_secure_session_cookie,\n    upgrade_cookie_security,\n    NEVER,\n    DELETE,\n)\nfrom r2.lib.errors import (\n    ErrorSet,\n    BadRequestError,\n    ForbiddenError,\n    errors,\n    reddit_http_error,\n)\nfrom r2.lib.filters import _force_utf8, _force_unicode, scriptsafe_dumps\nfrom r2.lib.loid import LoId\nfrom r2.lib.require import RequirementException, require, require_split\nfrom r2.lib.strings import strings\nfrom r2.lib.template_helpers import add_sr, JSPreload\nfrom r2.lib.tracking import encrypt, decrypt, get_pageview_pixel_url\nfrom r2.lib.translation import set_lang\nfrom r2.lib.utils import (\n    SimpleSillyStub,\n    UniqueIterator,\n    extract_subdomain,\n    http_utils,\n    is_subdomain,\n    is_throttled,\n    tup,\n    UrlParser,\n)\nfrom r2.lib.validator import (\n    build_arg_list,\n    fullname_regex,\n    valid_jsonp_callback,\n    validate,\n    VBoolean,\n    VByName,\n    VCount,\n    VLang,\n    VLength,\n    VLimit,\n    VTarget,\n)\nfrom r2.models import (\n    Account,\n    All,\n    AllFiltered,\n    AllMinus,\n    DefaultSR,\n    DomainSR,\n    FakeAccount,\n    FakeSubreddit,\n    Friends,\n    Frontpage,\n    LabeledMulti,\n    Link,\n    Mod,\n    ModFiltered,\n    ModMinus,\n    MultiReddit,\n    NotFound,\n    OAuth2AccessToken,\n    OAuth2Client,\n    OAuth2Scope,\n    Random,\n    RandomNSFW,\n    RandomSubscription,\n    Subreddit,\n    valid_admin_cookie,\n    valid_feed,\n    valid_otp_cookie,\n)\nfrom r2.lib.db import tdb_cassandra\n\n\n# Cookies which may be set in a response without making it uncacheable\nCACHEABLE_COOKIES = ()\n\n\nclass UnloggedUser(FakeAccount):\n    COOKIE_NAME = \"_options\"\n    allowed_prefs = {\n        \"pref_lang\": VLang.validate_lang,\n        \"pref_hide_locationbar\": bool,\n        \"pref_use_global_defaults\": bool,\n    }\n\n    def __init__(self, browser_langs, *a, **kw):\n        FakeAccount.__init__(self, *a, **kw)\n        lang = browser_langs[0] if browser_langs else g.lang\n        self._defaults = self._defaults.copy()\n        self._defaults['pref_lang'] = lang\n        self._defaults['pref_hide_locationbar'] = False\n        self._defaults['pref_use_global_defaults'] = False\n        self._t.update(self._from_cookie())\n\n    @property\n    def name(self):\n        raise NotImplementedError\n\n    def _decode_json(self, json_blob):\n        data = json.loads(json_blob)\n        validated = {}\n        for k, v in data.iteritems():\n            validator = self.allowed_prefs.get(k)\n            if validator:\n                try:\n                    validated[k] = validator(v)\n                except ValueError:\n                    pass  # don't override defaults for bad data\n        return validated\n\n    def _from_cookie(self):\n        cookie = c.cookies.get(self.COOKIE_NAME)\n        if not cookie:\n            return {}\n\n        try:\n            return self._decode_json(cookie.value)\n        except ValueError:\n            # old-style _options cookies are encrypted\n            try:\n                plaintext = decrypt(cookie.value)\n                values = self._decode_json(plaintext)\n            except (TypeError, ValueError):\n                # this cookie is totally invalid, delete it\n                c.cookies[self.COOKIE_NAME] = Cookie(value=\"\", expires=DELETE)\n                return {}\n            else:\n                self._to_cookie(values)  # upgrade the cookie\n                return values\n\n    def _to_cookie(self, data):\n        allowed_data = {k: v for k, v in data.iteritems()\n                        if k in self.allowed_prefs}\n        jsonified = json.dumps(allowed_data, sort_keys=True)\n        c.cookies[self.COOKIE_NAME] = Cookie(value=jsonified)\n\n    def _subscribe(self, sr):\n        pass\n\n    def _unsubscribe(self, sr):\n        pass\n\n    def _commit(self):\n        if self._dirty:\n            for k, (oldv, newv) in self._dirties.iteritems():\n                self._t[k] = newv\n            self._to_cookie(self._t)\n\ndef read_user_cookie(name):\n    uname = c.user.name if c.user_is_loggedin else \"\"\n    cookie_name = uname + '_' + name\n    if cookie_name in c.cookies:\n        return c.cookies[cookie_name].value\n    else:\n        return ''\n\ndef set_user_cookie(name, val, **kwargs):\n    uname = c.user.name if c.user_is_loggedin else \"\"\n    c.cookies[uname + '_' + name] = Cookie(value=val, **kwargs)\n\n\nvalid_click_cookie = fullname_regex(Link, True).match\ndef set_recent_clicks():\n    c.recent_clicks = []\n    if not c.user_is_loggedin:\n        return\n\n    click_cookie = read_user_cookie('recentclicks2')\n    if click_cookie:\n        if valid_click_cookie(click_cookie):\n            names = [ x for x in UniqueIterator(click_cookie.split(',')) if x ]\n\n            if len(names) > 5:\n                names = names[:5]\n                set_user_cookie('recentclicks2', ','.join(names))\n            #eventually this will look at the user preference\n            names = names[:5]\n\n            try:\n                c.recent_clicks = Link._by_fullname(names, data=True,\n                                                    return_dict=False)\n            except NotFound:\n                # clear their cookie because it's got bad links in it\n                set_user_cookie('recentclicks2', '')\n        else:\n            #if the cookie wasn't valid, clear it\n            set_user_cookie('recentclicks2', '')\n\ndef delete_obsolete_cookies():\n    for cookie_name in c.cookies:\n        if cookie_name.endswith((\"_last_thing\", \"_mod\")):\n            c.cookies[cookie_name] = Cookie(\"\", expires=DELETE)\n\ndef over18():\n    if c.user_is_loggedin:\n        return c.user.pref_over_18 or c.user_is_admin\n    else:\n        if 'over18' in c.cookies:\n            cookie = c.cookies['over18'].value\n            if cookie == \"1\":\n                return True\n            else:\n                delete_over18_cookie()\n\n\ndef set_over18_cookie():\n    c.cookies.add(\"over18\", \"1\")\n\n\ndef delete_over18_cookie():\n    c.cookies[\"over18\"] = Cookie(value=\"\", expires=DELETE)\n\n\ndef set_obey_over18():\n    \"querystring parameter for API to obey over18 filtering rules\"\n    c.obey_over18 = request.GET.get(\"obey_over18\") == \"true\"\n\nvalid_ascii_domain = re.compile(r'\\A(\\w[-\\w]*\\.)+[\\w]+\\Z')\ndef set_subreddit():\n    #the r parameter gets added by javascript for API requests so we\n    #can reference c.site in api.py\n    sr_name = request.environ.get(\"subreddit\", request.params.get('r'))\n    domain = request.environ.get(\"domain\")\n\n    can_stale = request.method.upper() in ('GET', 'HEAD')\n\n    c.site = Frontpage\n    if not sr_name:\n        #check for cnames\n        cname = request.environ.get('legacy-cname')\n        if cname:\n            sr = Subreddit._by_domain(cname) or Frontpage\n            domain = g.domain\n            if g.domain_prefix:\n                domain = \".\".join((g.domain_prefix, domain))\n            path = '%s://%s%s' % (g.default_scheme, domain, sr.path)\n            abort(301, location=BaseController.format_output_url(path))\n    elif '+' in sr_name:\n        name_filter = lambda name: Subreddit.is_valid_name(name,\n            allow_language_srs=True)\n        sr_names = filter(name_filter, sr_name.split('+'))\n        srs = Subreddit._by_name(sr_names, stale=can_stale).values()\n        if All in srs:\n            c.site = All\n        elif Friends in srs:\n            c.site = Friends\n        else:\n            srs = [sr for sr in srs if not isinstance(sr, FakeSubreddit)]\n            if len(srs) == 1:\n                c.site = srs[0]\n            elif srs:\n                found = {sr.name.lower() for sr in srs}\n                sr_names = filter(lambda name: name.lower() in found, sr_names)\n                sr_name = '+'.join(sr_names)\n                multi_path = '/r/' + sr_name\n                c.site = MultiReddit(multi_path, srs)\n            elif not c.error_page:\n                abort(404)\n    elif '-' in sr_name:\n        sr_names = sr_name.split('-')\n        base_sr_name, exclude_sr_names = sr_names[0], sr_names[1:]\n        srs = Subreddit._by_name(sr_names, stale=can_stale)\n        base_sr = srs.pop(base_sr_name, None)\n        exclude_srs = [sr for sr in srs.itervalues()\n                          if not isinstance(sr, FakeSubreddit)]\n\n        if base_sr == All:\n            if exclude_srs:\n                c.site = AllMinus(exclude_srs)\n            else:\n                c.site = All\n        elif base_sr == Mod:\n            if exclude_srs:\n                c.site = ModMinus(exclude_srs)\n            else:\n                c.site = Mod\n        else:\n            path = \"/subreddits/search?q=%s\" % sr_name\n            abort(302, location=BaseController.format_output_url(path))\n    else:\n        try:\n            c.site = Subreddit._by_name(sr_name, stale=can_stale)\n        except NotFound:\n            if Subreddit.is_valid_name(sr_name):\n                path = \"/subreddits/search?q=%s\" % sr_name\n                abort(302, location=BaseController.format_output_url(path))\n            elif not c.error_page and not request.path.startswith(\"/api/login/\") :\n                abort(404)\n\n    #if we didn't find a subreddit, check for a domain listing\n    if not sr_name and isinstance(c.site, DefaultSR) and domain:\n        # Redirect IDN to their IDNA name if necessary\n        try:\n            idna = _force_unicode(domain).encode(\"idna\")\n            if idna != domain:\n                path_info = request.environ[\"PATH_INFO\"]\n                path = \"/domain/%s%s\" % (idna, path_info)\n                abort(302, location=BaseController.format_output_url(path))\n        except UnicodeError:\n            domain = ''  # Ensure valid_ascii_domain fails\n        if not c.error_page and not valid_ascii_domain.match(domain):\n            abort(404)\n        c.site = DomainSR(domain)\n\n    if isinstance(c.site, FakeSubreddit):\n        c.default_sr = True\n\n_FILTER_SRS = {\"mod\": ModFiltered, \"all\": AllFiltered}\ndef set_multireddit():\n    routes_dict = request.environ[\"pylons.routes_dict\"]\n    if \"multipath\" in routes_dict or (\"m\" in request.GET and is_api()):\n        fullpath = routes_dict.get(\"multipath\", \"\").lower()\n        multipaths = fullpath.split(\"+\")\n        multi_ids = None\n        logged_in_username = c.user.name.lower() if c.user_is_loggedin else None\n        multiurl = None\n\n        if c.user_is_loggedin and routes_dict.get(\"my_multi\"):\n            multi_ids = [\"/user/%s/m/%s\" % (logged_in_username, multipath)\n                         for multipath in multipaths]\n            multiurl = \"/me/m/\" + fullpath\n        elif \"username\" in routes_dict:\n            username = routes_dict[\"username\"].lower()\n\n            if c.user_is_loggedin:\n                # redirect /user/foo/m/... to /me/m/... for user foo.\n                if username == logged_in_username and not is_api():\n                    # trim off multi id\n                    url_parts = request.path_qs.split(\"/\")[5:]\n                    url_parts.insert(0, \"/me/m/%s\" % fullpath)\n                    path = \"/\".join(url_parts)\n                    abort(302, location=BaseController.format_output_url(path))\n\n            multiurl = \"/user/\" + username + \"/m/\" + fullpath\n            multi_ids = [\"/user/%s/m/%s\" % (username, multipath)\n                        for multipath in multipaths]\n        elif \"m\" in request.GET and is_api():\n            # Only supported via API as we don't have a valid non-query\n            # parameter equivalent for cross-user multis, which means\n            # we can't generate proper links to /new, /top, etc in HTML\n            multi_ids = [m.lower() for m in request.GET.getall(\"m\") if m]\n            multiurl = \"\"\n\n        if multi_ids is not None:\n            multis = LabeledMulti._byID(multi_ids, return_dict=False) or []\n            multis = [m for m in multis if m.can_view(c.user)]\n            if not multis:\n                abort(404)\n            elif len(multis) == 1:\n                c.site = multis[0]\n            else:\n                sr_ids = Subreddit.random_reddits(\n                    logged_in_username,\n                    list(set(itertools.chain.from_iterable(\n                        multi.sr_ids for multi in multis\n                    ))),\n                    LabeledMulti.MAX_SR_COUNT,\n                )\n                srs = Subreddit._byID(sr_ids, data=True, return_dict=False)\n                c.site = MultiReddit(multiurl, srs)\n                if any(m.weighting_scheme == \"fresh\" for m in multis):\n                    c.site.weighting_scheme = \"fresh\"\n\n    elif \"filtername\" in routes_dict:\n        if not c.user_is_loggedin:\n            abort(404)\n        filtername = routes_dict[\"filtername\"].lower()\n        filtersr = _FILTER_SRS.get(filtername)\n        if not filtersr:\n            abort(404)\n        c.site = filtersr()\n\n\ndef set_content_type():\n    e = request.environ\n    c.render_style = e['render_style']\n    response.content_type = e['content_type']\n\n    if e.has_key('extension'):\n        c.extension = ext = e['extension']\n        if ext in ('embed', 'widget'):\n            wrapper = request.params.get(\"callback\", \"document.write\")\n            wrapper = filters._force_utf8(wrapper)\n            if not valid_jsonp_callback(wrapper):\n                abort(BadRequestError(errors.BAD_JSONP_CALLBACK))\n\n            # force logged-out state since these can be accessed cross-domain\n            c.user = UnloggedUser(get_browser_langs())\n            c.user_is_loggedin = False\n            c.forced_loggedout = True\n\n            def to_js(content):\n                # Add a comment to the beginning to prevent the \"Rosetta Flash\"\n                # XSS when an attacker controls the beginning of a resource\n                return \"/**/\" + wrapper + \"(\" + utils.string2js(content) + \");\"\n\n            c.response_wrapper = to_js\n        if ext in (\"rss\", \"api\", \"json\") and request.method.upper() == \"GET\":\n            user = valid_feed(request.GET.get(\"user\"),\n                              request.GET.get(\"feed\"),\n                              request.path)\n            if user and not g.read_only_mode:\n                c.user = user\n                c.user_is_loggedin = True\n        if ext in (\"mobile\", \"m\") and not request.GET.get(\"keep_extension\"):\n            try:\n                if request.cookies['reddit_mobility'] == \"compact\":\n                    c.extension = \"compact\"\n                    c.render_style = \"compact\"\n            except (ValueError, KeyError):\n                c.suggest_compact = True\n        if ext in (\"mobile\", \"m\", \"compact\"):\n            if request.GET.get(\"keep_extension\"):\n                c.cookies['reddit_mobility'] = Cookie(ext, expires=NEVER)\n\n    # allow content and api calls to set an loid\n    if is_api() or c.render_style in (\"html\", \"mobile\", \"compact\"):\n        c.loid = LoId.load(request, c)\n\n    # allow JSONP requests to generate callbacks, but do not allow\n    # the user to be logged in for these\n    callback = request.GET.get(\"jsonp\")\n    if is_api() and request.method.upper() == \"GET\" and callback:\n        if not valid_jsonp_callback(callback):\n            abort(BadRequestError(errors.BAD_JSONP_CALLBACK))\n        c.allowed_callback = callback\n        c.user = UnloggedUser(get_browser_langs())\n        c.user_is_loggedin = False\n        c.forced_loggedout = True\n        response.content_type = \"application/javascript\"\n\ndef get_browser_langs():\n    browser_langs = []\n    langs = request.environ.get('HTTP_ACCEPT_LANGUAGE')\n    if langs:\n        langs = langs.split(',')\n        browser_langs = []\n        seen_langs = set()\n        # extract languages from browser string\n        for l in langs:\n            if ';' in l:\n                l = l.split(';')[0]\n            if l not in seen_langs and l in g.languages:\n                browser_langs.append(l)\n                seen_langs.add(l)\n            if '-' in l:\n                l = l.split('-')[0]\n            if l not in seen_langs and l in g.languages:\n                browser_langs.append(l)\n                seen_langs.add(l)\n    return browser_langs\n\ndef set_iface_lang():\n    host_lang = request.environ.get('reddit-prefer-lang')\n    lang = host_lang or c.user.pref_lang\n\n    if getattr(g, \"lang_override\") and lang == \"en\":\n        lang = g.lang_override\n\n    c.lang = lang\n\n    try:\n        set_lang(lang, fallback_lang=g.lang)\n    except LanguageError:\n        lang = g.lang\n        set_lang(lang, graceful_fail=True)\n\n    try:\n        c.locale = babel.core.Locale.parse(lang, sep='-')\n    except (babel.core.UnknownLocaleError, ValueError):\n        c.locale = babel.core.Locale.parse(g.lang, sep='-')\n\n\ndef set_colors():\n    theme_rx = re.compile(r'')\n    color_rx = re.compile(r'\\A([a-fA-F0-9]){3}(([a-fA-F0-9]){3})?\\Z')\n    c.theme = None\n    if color_rx.match(request.GET.get('bgcolor') or ''):\n        c.bgcolor = request.GET.get('bgcolor')\n    if color_rx.match(request.GET.get('bordercolor') or ''):\n        c.bordercolor = request.GET.get('bordercolor')\n\n\ndef ratelimit_agent(agent, limit=10, slice_size=10):\n\n    # Ensure the agent regex is a valid memcached key\n    h = md5()\n    h.update(agent)\n    hashed_agent = h.hexdigest()\n\n    slice_size = min(slice_size, 60)\n    time_slice = ratelimit.get_timeslice(slice_size)\n    usage = ratelimit.record_usage(\"rl-agent-\" + hashed_agent, time_slice)\n    if usage > limit:\n        request.environ['retry_after'] = time_slice.remaining\n        abort(429)\n\n\nappengine_re = re.compile(r'AppEngine-Google; \\(\\+http://code.google.com/appengine; appid: (?:dev|s)~([a-z0-9-]{6,30})\\)\\Z')\ndef ratelimit_agents():\n    user_agent = request.user_agent\n\n    if not user_agent:\n        return\n\n    # parse out the appid for appengine apps\n    appengine_match = appengine_re.search(user_agent)\n    if appengine_match:\n        appid = appengine_match.group(1)\n        ratelimit_agent(appid)\n        return\n\n    # Search anywhere in the useragent for the given regex\n    for agent_re, limit in g.user_agent_ratelimit_regexes.iteritems():\n        if agent_re.search(user_agent):\n            ratelimit_agent(agent_re.pattern, limit)\n            return\n\n\ndef ratelimit_throttled():\n    ip = request.ip.strip()\n    if is_throttled(ip):\n        abort(429)\n\n\ndef paginated_listing(default_page_size=25, max_page_size=100, backend='sql'):\n    def decorator(fn):\n        @validate(num=VLimit('limit', default=default_page_size,\n                             max_limit=max_page_size),\n                  after=VByName('after', backend=backend),\n                  before=VByName('before', backend=backend),\n                  count=VCount('count'),\n                  target=VTarget(\"target\"),\n                  sr_detail=VBoolean(\n                      \"sr_detail\", docs={\"sr_detail\": \"(optional) expand subreddits\"}),\n                  show=VLength('show', 3, empty_error=None,\n                               docs={\"show\": \"(optional) the string `all`\"}),\n        )\n        @wraps(fn)\n        def new_fn(self, before, **env):\n            if c.render_style == \"htmllite\":\n                c.link_target = env.get(\"target\")\n            elif \"target\" in env:\n                del env[\"target\"]\n\n            if \"show\" in env and env['show'] == 'all':\n                c.ignore_hide_rules = True\n            kw = build_arg_list(fn, env)\n\n            #turn before into after/reverse\n            kw['reverse'] = False\n            if before:\n                kw['after'] = before\n                kw['reverse'] = True\n\n            return fn(self, **kw)\n\n        if hasattr(fn, \"_api_doc\"):\n            notes = fn._api_doc[\"notes\"] or []\n            if paginated_listing.doc_note not in notes:\n                notes.append(paginated_listing.doc_note)\n            fn._api_doc[\"notes\"] = notes\n\n        return new_fn\n    return decorator\n\npaginated_listing.doc_note = \"*This endpoint is [a listing](#listings).*\"\n\n#TODO i want to get rid of this function. once the listings in front.py are\n#moved into listingcontroller, we shouldn't have a need for this\n#anymore\ndef base_listing(fn):\n    return paginated_listing()(fn)\n\ndef is_trusted_origin(origin):\n    try:\n        origin = urlparse(origin)\n    except ValueError:\n        return False\n\n    return any(is_subdomain(origin.hostname, domain) for domain in g.trusted_domains)\n\ndef cross_domain(origin_check=is_trusted_origin, **options):\n    \"\"\"Set up cross domain validation and hoisting for a request handler.\"\"\"\n    def cross_domain_wrap(fn):\n        cors_perms = {\n            \"origin_check\": origin_check,\n            \"allow_credentials\": bool(options.get(\"allow_credentials\"))\n        }\n\n        @wraps(fn)\n        def cross_domain_handler(self, *args, **kwargs):\n            if request.params.get(\"hoist\") == \"cookie\":\n                # Cookie polling response\n                if cors_perms[\"origin_check\"](g.origin):\n                    name = request.environ[\"pylons.routes_dict\"][\"action_name\"]\n                    resp = fn(self, *args, **kwargs)\n                    c.cookies.add('hoist_%s' % name, ''.join(tup(resp)))\n                    response.content_type = 'text/html'\n                    return \"\"\n                else:\n                    abort(403)\n            else:\n                self.check_cors()\n                return fn(self, *args, **kwargs)\n\n        cross_domain_handler.cors_perms = cors_perms\n        return cross_domain_handler\n    return cross_domain_wrap\n\n\ndef make_url_https(url):\n    \"\"\"Turn a possibly relative URL into a fully-qualified HTTPS URL.\"\"\"\n    new_url = UrlParser(url)\n    new_url.scheme = \"https\"\n    if not new_url.hostname:\n        new_url.hostname = request.host.lower()\n    return new_url.unparse()\n\n\ndef generate_modhash():\n    # OAuth clients should never receive a modhash of any kind as they could\n    # use it in a CSRF attack to bypass their permitted OAuth scopes\n    if c.oauth_user:\n        return None\n\n    modhash = hooks.get_hook(\"modhash.generate\").call_until_return()\n    if modhash is not None:\n        return modhash\n\n    # if no plugins generate a modhash, just use the user name\n    return c.user.name\n\n\ndef enforce_https():\n    \"\"\"Enforce policy for forced usage of HTTPS.\"\"\"\n\n    # OAuth HTTPS enforcement is dealt with elsewhere\n    if c.oauth_user:\n        return\n\n    # This is likely a cross-domain request, the initiator has no way of\n    # respecting the user's HTTPS preferences and redirecting them is unlikely\n    # to stop them from making future requests via HTTP.\n    if c.forced_loggedout or c.render_style == \"js\":\n        return\n\n    # It's not possible to redirect inside the error handler\n    if request.environ.get('pylons.error_call', False):\n        return\n\n    is_api_request = is_api() or request.path.startswith(\"/api/\")\n    redirect_url = None\n\n    # This is likely a request from an API client. Redirecting them or giving\n    # them an HSTS grant is unlikely to stop them from making requests to HTTP.\n    if is_api_request and not c.secure:\n        # Record the violation so we know who to talk to to get this fixed.\n        # This is preferable to redirecting insecure API reqs right away\n        # because a lot of clients just break on redirect, it would create two\n        # requests for every request, and it wouldn't increase security.\n        ua = request.user_agent\n        g.stats.count_string('https.security_violation', ua)\n        # It's especially bad to send credentials over HTTP\n        if c.user_is_loggedin:\n            g.stats.count_string('https.loggedin_security_violation', ua)\n\n        # They didn't send a login cookie, but their cookies indicate they won't\n        # be authed properly unless we redirect them to the secure version.\n        if have_secure_session_cookie() and not c.user_is_loggedin:\n            redirect_url = make_url_https(request.fullurl)\n\n    # These are all safe to redirect to HTTPS\n    if c.render_style in {\"html\", \"compact\", \"mobile\"} and not is_api_request:\n        want_redirect = (feature.is_enabled(\"force_https\") or\n                         feature.is_enabled(\"https_redirect\"))\n        if not c.secure and want_redirect:\n            redirect_url = make_url_https(request.fullurl)\n\n    if redirect_url:\n        headers = {\"Cache-Control\": \"private, no-cache\", \"Pragma\": \"no-cache\"}\n        # Browsers change the method to GET on 301, and 308 is ill-supported.\n        status_code = 301 if request.method == \"GET\" else 307\n        abort(status_code, location=redirect_url, headers=headers)\n\n\ndef require_https():\n    if not c.secure:\n        abort(ForbiddenError(errors.HTTPS_REQUIRED))\n\n\ndef require_domain(required_domain):\n    if not is_subdomain(request.host, required_domain):\n        abort(ForbiddenError(errors.WRONG_DOMAIN))\n\n\ndef disable_subreddit_css():\n    def wrap(f):\n        @wraps(f)\n        def no_funny_business(*args, **kwargs):\n            c.allow_styles = False\n            return f(*args, **kwargs)\n        return no_funny_business\n    return wrap\n\n\ndef request_timer_name(action):\n    return \"service_time.web.\" + action\n\n\ndef flatten_response(content):\n    \"\"\"Convert a content iterable to a string, properly handling unicode.\"\"\"\n    # TODO: it would be nice to replace this with response.body someday\n    # once unicode issues are ironed out.\n    return \"\".join(_force_utf8(x) for x in tup(content) if x)\n\n\ndef abort_with_error(error, code=None):\n    if not code and not error.code:\n        raise ValueError('Error %r missing status code' % error)\n\n    abort(reddit_http_error(\n        code=code or error.code,\n        error_name=error.name,\n        explanation=error.message,\n        fields=error.fields,\n    ))\n\n\nclass MinimalController(BaseController):\n\n    allow_stylesheets = False\n    defer_ratelimiting = False\n\n    def run_sitewide_ratelimits(self):\n        \"\"\"Ratelimit users and add ratelimit headers to the response.\n\n        Headers added are:\n        X-Ratelimit-Used: Number of requests used in this period\n        X-Ratelimit-Remaining: Number of requests left to use\n        X-Ratelimit-Reset: Approximate number of seconds to end of period\n\n        This function only has an effect if one of\n        g.RL_SITEWIDE_ENABLED or g.RL_OAUTH_SITEWIDE_ENABLED\n        are set to 'true' in the app configuration\n\n        If the ratelimit is exceeded, a 429 response will be sent,\n        unless the app configuration has g.ENFORCE_RATELIMIT off.\n        Headers will be sent even on aborted requests.\n\n        \"\"\"\n        if c.error_page:\n            # ErrorController is re-running pre, don't double ratelimit\n            return\n\n        if c.oauth_user and g.RL_OAUTH_SITEWIDE_ENABLED:\n            type_ = \"oauth\"\n            period = g.RL_OAUTH_RESET_SECONDS\n            max_reqs = c.oauth2_client._max_reqs\n            # Convert client_id to ascii str for use as memcache key\n            client_id = c.oauth2_access_token.client_id.encode(\"ascii\")\n            # OAuth2 ratelimits are per user-app combination\n            key = 'siterl-oauth-' + c.user._id36 + \":\" + client_id\n        elif c.cdn_cacheable:\n            type_ = \"cdn\"\n        elif not is_api():\n            type_ = \"web\"\n        elif g.RL_SITEWIDE_ENABLED:\n            type_ = \"api\"\n            max_reqs = g.RL_MAX_REQS\n            period = g.RL_RESET_SECONDS\n            # API (non-oauth) limits are per-ip\n            key = 'siterl-api-' + request.ip\n        else:\n            type_ = \"none\"\n\n        g.stats.event_count(\"ratelimit.type\", type_, sample_rate=0.01)\n        if type_ in (\"cdn\", \"web\", \"none\"):\n            # No ratelimiting or headers for:\n            # * Web requests (HTML)\n            # * CDN requests (logged out via www.reddit.com)\n            return\n\n        time_slice = ratelimit.get_timeslice(period)\n\n        try:\n            recent_reqs = ratelimit.record_usage(key, time_slice)\n        except ratelimit.RatelimitError as e:\n            # Ratelimiting is non-critical; if the system is\n            # having issues, just skip adding the headers\n            g.log.info(\"ratelimit error: %s\", e)\n            return\n        reqs_remaining = max(0, max_reqs - recent_reqs)\n\n        c.ratelimit_headers = {\n            \"X-Ratelimit-Used\": str(recent_reqs),\n            \"X-Ratelimit-Reset\": str(time_slice.remaining),\n            \"X-Ratelimit-Remaining\": str(reqs_remaining),\n        }\n\n        event_type = None\n\n        if reqs_remaining <= 0:\n            if recent_reqs > (2 * max_reqs):\n                event_type = \"hyperbolic\"\n            else:\n                event_type = \"over\"\n            if g.ENFORCE_RATELIMIT:\n                # For non-abort situations, the headers will be added in post()\n                request.environ['retry_after'] = time_slice.remaining\n                response.headers.update(c.ratelimit_headers)\n                abort(429)\n        elif reqs_remaining < (0.1 * max_reqs):\n            event_type = \"close\"\n\n        if event_type is not None:\n            g.stats.event_count(\"ratelimit.exceeded\", event_type)\n            if type_ == \"oauth\":\n                g.stats.count_string(\"oauth.{}\".format(event_type), client_id)\n\n    def pre(self):\n        action = request.environ[\"pylons.routes_dict\"].get(\"action\")\n        if action:\n            if not self._get_action_handler():\n                action = 'invalid'\n            controller = request.environ[\"pylons.routes_dict\"][\"controller\"]\n            key = \"{}.{}\".format(controller, action)\n            c.request_timer = g.stats.get_timer(request_timer_name(key))\n        else:\n            c.request_timer = SimpleSillyStub()\n\n        baseplate_integration.make_server_span(span_name=key).start()\n\n        c.response_wrapper = None\n        c.start_time = datetime.now(g.tz)\n        c.request_timer.start()\n        g.reset_caches()\n\n        c.domain_prefix = request.environ.get(\"reddit-domain-prefix\",\n                                              g.domain_prefix)\n        c.secure = request.environ[\"wsgi.url_scheme\"] == \"https\"\n        c.request_origin = request.host_url\n\n        #check if user-agent needs a dose of rate-limiting\n        if not c.error_page:\n            ratelimit_throttled()\n            ratelimit_agents()\n\n        # Allow opting out of the `websafe_json` madness\n        if \"WANT_RAW_JSON\" not in request.environ:\n            want_raw_json = request.params.get(\"raw_json\", \"\") == \"1\"\n            request.environ[\"WANT_RAW_JSON\"] = want_raw_json\n\n        c.allow_framing = False\n\n        # According to http://www.w3.org/TR/2014/WD-referrer-policy-20140807/\n        # we really want \"origin-when-crossorigin\", but that isn't widely\n        # supported yet.\n        c.referrer_policy = \"origin\"\n\n        c.cdn_cacheable = (request.via_cdn and\n                           g.login_cookie not in request.cookies)\n\n        c.extension = request.environ.get('extension')\n        # the domain has to be set before Cookies get initialized\n        set_subreddit()\n        c.subdomain = extract_subdomain()\n        c.errors = ErrorSet()\n        c.cookies = Cookies()\n        # if an rss feed, this will also log the user in if a feed=\n        # GET param is included\n        set_content_type()\n\n        c.request_timer.intermediate(\"minimal-pre\")\n        # True/False forces. None updates for most non-POST requests\n        c.update_last_visit = None\n\n        if is_subdomain(request.host, g.oauth_domain):\n            self.check_cors()\n\n        if not self.defer_ratelimiting:\n            self.run_sitewide_ratelimits()\n            c.request_timer.intermediate(\"minimal-ratelimits\")\n\n        hooks.get_hook(\"reddit.request.minimal_begin\").call()\n\n    def post(self):\n        c.request_timer.intermediate(\"action\")\n\n        # if the action raised an HTTPException (i.e. it aborted) then pylons\n        # will have replaced response with the exception itself.\n        c.is_exception_response = getattr(response, \"_exception\", False)\n\n        if c.response_wrapper and not c.is_exception_response:\n            content = flatten_response(response.content)\n            wrapped_content = c.response_wrapper(content)\n            response.content = wrapped_content\n\n        # we need to not add X-Frame-Options to requests (such as media embeds)\n        # that intend to allow framing.\n        if not c.allow_framing:\n            response.headers[\"X-Frame-Options\"] = \"SAMEORIGIN\"\n\n        # set some headers related to client security\n        response.headers['X-Content-Type-Options'] = 'nosniff'\n        response.headers['X-XSS-Protection'] = '1; mode=block'\n\n        if (feature.is_enabled(\"force_https\")\n                and feature.is_enabled(\"upgrade_cookies\")):\n            upgrade_cookie_security()\n\n        # Don't poison the cache with uncacheable cookies\n        dirty_cookies = (k for k, v in c.cookies.iteritems() if v.dirty)\n        would_poison = any((k not in CACHEABLE_COOKIES) for k in dirty_cookies)\n\n        if c.user_is_loggedin or would_poison:\n            # Based off logged in <https://en.wikipedia.org/>,\n            # must-revalidate might not be necessary, but should force\n            # similar behaviour to no-cache (in theory.)\n            # Normally you'd prefer `no-store`, but many of reddit's\n            # listings are ephemeral, and the content might not even\n            # exist anymore if you force a refresh when hitting back.\n            cache_control = (\n                'private',\n                's-maxage=0',\n                'max-age=0',\n                'must-revalidate',\n            )\n            response.headers['Expires'] = '-1'\n            response.headers['Cache-Control'] = ', '.join(cache_control)\n\n        if c.ratelimit_headers:\n            response.headers.update(c.ratelimit_headers)\n\n        # write loid cookie if necessary\n        if c.loid:\n            c.loid.save(domain=g.domain)\n\n        # send cookies\n        secure_cookies = feature.is_enabled(\"force_https\")\n        for k, v in c.cookies.iteritems():\n            if v.dirty:\n                v_secure = v.secure if v.secure is not None else secure_cookies\n                response.set_cookie(key=k,\n                                    value=quote(v.value),\n                                    domain=v.domain,\n                                    expires=v.expires,\n                                    secure=v_secure,\n                                    httponly=getattr(v, 'httponly', False))\n\n\n        if not isinstance(c.site, FakeSubreddit) and not g.disallow_db_writes:\n            if c.user_is_loggedin:\n                c.site.record_visitor_activity(\"logged_in\", c.user._fullname)\n\n        if self.should_update_last_visit():\n            c.user.update_last_visit(c.start_time)\n\n        hooks.get_hook(\"reddit.request.end\").call()\n\n        # this thread is probably going to be reused, but it could be\n        # a while before it is. So we might as well dump the cache in\n        # the mean time so that we don't have dead objects hanging\n        # around taking up memory\n        g.reset_caches()\n\n        c.request_timer.intermediate(\"post\")\n\n        # add tags to the trace\n        c.trace.set_tag(\"user\", c.user._fullname if c.user_is_loggedin else None)\n        c.trace.set_tag(\"render_style\", c.render_style)\n\n        # push data to statsd\n        baseplate_integration.finish_server_span()\n        c.request_timer.stop()\n        g.stats.flush()\n\n    def on_validation_error(self, error):\n        if error.name == errors.USER_REQUIRED:\n            self.intermediate_redirect('/login')\n        elif error.name == errors.VERIFIED_USER_REQUIRED:\n            self.intermediate_redirect('/verify')\n\n    def abort404(self):\n        abort(404, \"not found\")\n\n    def abort403(self):\n        abort(403, \"forbidden\")\n\n    COMMON_REDDIT_HEADERS = \", \".join((\n        \"X-Ratelimit-Used\",\n        \"X-Ratelimit-Remaining\",\n        \"X-Ratelimit-Reset\",\n        \"X-Moose\",\n    ))\n\n    def check_cors(self):\n        origin = request.headers.get(\"Origin\")\n        if c.cors_checked or not origin:\n            return\n\n        method = request.method\n        if method == 'OPTIONS':\n            # preflight request\n            method = request.headers.get(\"Access-Control-Request-Method\")\n            if not method:\n                self.abort403()\n\n        via_oauth = is_subdomain(request.host, g.oauth_domain)\n        if via_oauth:\n            response.headers[\"Access-Control-Allow-Origin\"] = \"*\"\n            response.headers[\"Access-Control-Allow-Methods\"] = \\\n                \"GET, POST, PUT, PATCH, DELETE\"\n            response.headers[\"Access-Control-Allow-Headers\"] = \\\n                \"Authorization, \"\n            response.headers[\"Access-Control-Allow-Credentials\"] = \"false\"\n            response.headers['Access-Control-Expose-Headers'] = \\\n                self.COMMON_REDDIT_HEADERS\n        else:\n            action = request.environ[\"pylons.routes_dict\"][\"action_name\"]\n\n            handler = self._get_action_handler(action, method)\n            cors = handler and getattr(handler, \"cors_perms\", None)\n\n            if cors and cors[\"origin_check\"](origin):\n                response.headers[\"Access-Control-Allow-Origin\"] = origin\n                if cors.get(\"allow_credentials\"):\n                    response.headers[\"Access-Control-Allow-Credentials\"] = \"true\"\n        c.cors_checked = True\n\n    def OPTIONS(self):\n        \"\"\"Return empty responses for CORS preflight requests\"\"\"\n        self.check_cors()\n\n    def update_qstring(self, dict):\n        merged = copy(request.GET)\n        merged.update(dict)\n        return request.path + utils.query_string(merged)\n\n    def api_wrapper(self, kw):\n        if request.environ.get(\"WANT_RAW_JSON\"):\n            return scriptsafe_dumps(kw)\n        return filters.websafe_json(simplejson.dumps(kw))\n\n    def should_update_last_visit(self):\n        if g.disallow_db_writes:\n            return False\n\n        if not c.user_is_loggedin:\n            return False\n\n        if c.update_last_visit is not None:\n            return c.update_last_visit\n\n        return request.method.upper() != \"POST\"\n\n\nclass OAuth2ResourceController(MinimalController):\n    defer_ratelimiting = True\n\n    def authenticate_with_token(self):\n        set_extension(request.environ, \"json\")\n        set_content_type()\n        require_https()\n        require_domain(g.oauth_domain)\n\n        try:\n            access_token = OAuth2AccessToken.get_token(self._get_bearer_token())\n            require(access_token)\n            require(access_token.check_valid())\n            c.oauth2_access_token = access_token\n            if access_token.user_id:\n                account = Account._byID36(access_token.user_id, data=True)\n                require(account)\n                require(not account._deleted)\n                c.user = c.oauth_user = account\n                c.user_is_loggedin = True\n            else:\n                c.user = UnloggedUser(get_browser_langs())\n                c.user_is_loggedin = False\n            c.oauth2_client = OAuth2Client._byID(access_token.client_id)\n        except RequirementException:\n            self._auth_error(401, \"invalid_token\")\n\n        handler = self._get_action_handler()\n        if handler:\n            oauth2_perms = getattr(handler, \"oauth2_perms\", {})\n            if oauth2_perms.get(\"oauth2_allowed\", False):\n                grant = OAuth2Scope(access_token.scope)\n                required = set(oauth2_perms['required_scopes'])\n                if not grant.has_access(c.site.name, required):\n                    self._auth_error(403, \"insufficient_scope\")\n                c.oauth_scope = grant\n            else:\n                self._auth_error(400, \"invalid_request\")\n\n    def check_for_bearer_token(self):\n        if self._get_bearer_token(strict=False):\n            self.authenticate_with_token()\n\n    def _auth_error(self, code, error):\n        abort(code, headers=[(\"WWW-Authenticate\", 'Bearer realm=\"reddit\", error=\"%s\"' % error)])\n\n    def _get_bearer_token(self, strict=True):\n        auth = request.headers.get(\"Authorization\")\n        if not auth:\n            return None\n        try:\n            auth_scheme, bearer_token = require_split(auth, 2)\n            require(auth_scheme.lower() == \"bearer\")\n            return bearer_token\n        except RequirementException:\n            if strict:\n                self._auth_error(400, \"invalid_request\")\n            else:\n                return None\n\n    def set_up_user_context(self):\n        if c.user.inbox_count > 0:\n            c.have_messages = True\n        c.have_mod_messages = bool(c.user.modmsgtime)\n\n        c.user_special_distinguish = c.user.special_distinguish()\n\n\nclass OAuth2OnlyController(OAuth2ResourceController):\n    \"\"\"Base controller for endpoints that may only be accessed via OAuth 2\"\"\"\n\n    # OAuth2 doesn't rely on ambient credentials for authentication,\n    # so CSRF prevention is unnecessary.\n    handles_csrf = True\n\n    def pre(self):\n        OAuth2ResourceController.pre(self)\n        if request.method != \"OPTIONS\":\n            self.authenticate_with_token()\n            self.set_up_user_context()\n            self.run_sitewide_ratelimits()\n\n    def on_validation_error(self, error):\n        abort_with_error(error, error.code or 400)\n\n\nclass RedditController(OAuth2ResourceController):\n\n    @staticmethod\n    def login(user, rem=False):\n        # This can't be handled in post() due to PRG and ErrorController fun.\n        user.update_last_visit(c.start_time)\n        force_https = feature.is_enabled(\"force_https\", user)\n        c.cookies[g.login_cookie] = Cookie(value=user.make_cookie(),\n                                           expires=NEVER if rem else None,\n                                           httponly=True,\n                                           secure=force_https)\n        # Make sure user-specific cookies get the secure flag set properly\n        change_user_cookie_security(secure=force_https, remember=rem)\n\n    @staticmethod\n    def logout():\n        c.cookies[g.login_cookie] = Cookie(value='', expires=DELETE)\n        delete_secure_session_cookie()\n\n    @staticmethod\n    def enable_admin_mode(user, first_login=None):\n        # no expiration time so the cookie dies with the browser session\n        admin_cookie = user.make_admin_cookie(first_login=first_login)\n        c.cookies[g.admin_cookie] = Cookie(\n            value=admin_cookie,\n            httponly=True,\n            secure=feature.is_enabled(\"force_https\"),\n        )\n\n    @staticmethod\n    def remember_otp(user):\n        cookie = user.make_otp_cookie()\n        expiration = datetime.utcnow() + timedelta(seconds=g.OTP_COOKIE_TTL)\n        set_user_cookie(g.otp_cookie,\n                        cookie,\n                        secure=True,\n                        httponly=True,\n                        expires=expiration)\n\n    @staticmethod\n    def disable_admin_mode(user):\n        c.cookies[g.admin_cookie] = Cookie(value='', expires=DELETE)\n\n    def pre(self):\n        record_timings = g.admin_cookie in request.cookies or g.debug\n        admin_bar_eligible = response.content_type == 'text/html'\n        if admin_bar_eligible and record_timings:\n            g.stats.start_logging_timings()\n\n        # set up stuff needed in base templates at error time here.\n        c.js_preload = JSPreload()\n\n        MinimalController.pre(self)\n\n        # Set IE to always use latest rendering engine\n        response.headers[\"X-UA-Compatible\"] = \"IE=edge\"\n\n        # populate c.cookies unless we're on the unsafe media_domain\n        if request.host != g.media_domain or g.media_domain == g.domain:\n            cookie_counts = collections.Counter()\n            for k, v in request.cookies.iteritems():\n                # minimalcontroller can still set cookies\n                if k not in c.cookies:\n                    # we can unquote even if it's not quoted\n                    c.cookies[k] = Cookie(value=unquote(v), dirty=False)\n                    cookie_counts[Cookie.classify(k)] += 1\n\n            for cookietype, count in cookie_counts.iteritems():\n                g.stats.simple_event(\"cookie.%s\" % cookietype, count)\n\n        delete_obsolete_cookies()\n\n        # the user could have been logged in via one of the feeds\n        maybe_admin = False\n        is_otpcookie_valid = False\n\n        self.check_for_bearer_token()\n\n        # no logins for RSS feed unless valid_feed has already been called\n        if not c.user:\n            if c.extension != \"rss\":\n                if not g.read_only_mode:\n                    c.user = g.auth_provider.get_authenticated_account()\n\n                    if c.user and c.user._deleted:\n                        c.user = None\n                else:\n                    c.user = None\n                c.user_is_loggedin = bool(c.user)\n\n                admin_cookie = c.cookies.get(g.admin_cookie)\n                if c.user_is_loggedin and admin_cookie:\n                    maybe_admin, first_login = valid_admin_cookie(admin_cookie.value)\n\n                    if maybe_admin:\n                        self.enable_admin_mode(c.user, first_login=first_login)\n                    else:\n                        self.disable_admin_mode(c.user)\n\n                otp_cookie = read_user_cookie(g.otp_cookie)\n                if c.user_is_loggedin and otp_cookie:\n                    is_otpcookie_valid = valid_otp_cookie(otp_cookie)\n\n            if not c.user:\n                c.user = UnloggedUser(get_browser_langs())\n                # patch for fixing mangled language preferences\n                if not isinstance(c.user.pref_lang, basestring):\n                    c.user.pref_lang = g.lang\n                    c.user._commit()\n\n        if c.user_is_loggedin:\n            self.set_up_user_context()\n            c.modhash = generate_modhash()\n            c.user_is_admin = maybe_admin and c.user.name in g.admins\n            c.user_is_sponsor = c.user_is_admin or c.user.name in g.sponsors\n            c.otp_cached = is_otpcookie_valid\n\n        enforce_https()\n\n        c.request_timer.intermediate(\"base-auth\")\n\n        self.run_sitewide_ratelimits()\n        c.request_timer.intermediate(\"base-ratelimits\")\n\n        c.over18 = over18()\n        set_obey_over18()\n\n        # looking up the multireddit requires c.user.\n        set_multireddit()\n\n        #set_browser_langs()\n        set_iface_lang()\n        set_recent_clicks()\n        # used for HTML-lite templates\n        set_colors()\n\n        # set some environmental variables in case we hit an abort\n        if not isinstance(c.site, FakeSubreddit):\n            request.environ['REDDIT_NAME'] = c.site.name\n\n        # random reddit trickery\n        if c.site == Random:\n            c.site = Subreddit.random_reddit(user=c.user)\n            site_path = c.site.path.strip('/')\n            path = \"/\" + site_path + request.path_qs\n            abort(302, location=self.format_output_url(path))\n        elif c.site == RandomSubscription:\n            if not c.user.gold:\n                abort(302, location=self.format_output_url('/gold/about'))\n            c.site = Subreddit.random_subscription(c.user)\n            site_path = c.site.path.strip('/')\n            path = '/' + site_path + request.path_qs\n            abort(302, location=self.format_output_url(path))\n        elif c.site == RandomNSFW:\n            c.site = Subreddit.random_reddit(over18=True, user=c.user)\n            site_path = c.site.path.strip('/')\n            path = '/' + site_path + request.path_qs\n            abort(302, location=self.format_output_url(path))\n\n        if not request.path.startswith(\"/api/login/\"):\n            # is the subreddit banned?\n            if c.site.spammy() and not c.user_is_admin and not c.error_page:\n                ban_info = getattr(c.site, \"ban_info\", {})\n                if \"message\" in ban_info and ban_info['message']:\n                    message = ban_info['message']\n                else:\n                    message = None\n\n                errpage = pages.InterstitialPage(\n                    _(\"banned\"),\n                    content=pages.BannedInterstitial(\n                        message=message,\n                        ban_time=ban_info.get(\"banned_at\"),\n                    ),\n                )\n\n                request.environ['usable_error_content'] = errpage.render()\n                self.abort404()\n\n            # check if the user has access to this subreddit\n            # Allow OPTIONS requests through, as no response body\n            # is sent in those cases - just a set of headers\n            if (not c.site.can_view(c.user) and not c.error_page and\n                    request.method != \"OPTIONS\"):\n                allowed_to_view = c.site.is_allowed_to_view(c.user)\n\n                if isinstance(c.site, LabeledMulti):\n                    # do not leak the existence of multis via 403.\n                    self.abort404()\n                elif not allowed_to_view and c.site.type == 'gold_only':\n                    errpage = pages.InterstitialPage(\n                        _(\"gold members only\"),\n                        content=pages.GoldOnlyInterstitial(\n                            sr_name=c.site.name,\n                            sr_description=c.site.public_description,\n                        ),\n                    )\n                    request.environ['usable_error_content'] = errpage.render()\n                    self.abort403()\n                elif not allowed_to_view:\n                    errpage = pages.InterstitialPage(\n                        _(\"private\"),\n                        content=pages.PrivateInterstitial(\n                            sr_name=c.site.name,\n                            sr_description=c.site.public_description,\n                        ),\n                    )\n                    request.environ['usable_error_content'] = errpage.render()\n                    self.abort403()\n                else:\n                    if c.render_style != 'html':\n                        self.abort403()\n                    g.events.quarantine_event('quarantine_interstitial_view', c.site,\n                        request=request, context=c)\n                    return self.intermediate_redirect(\"/quarantine\", sr_path=False)\n\n            # check over 18\n            if (\n                c.site.over_18 and not c.over18 and\n                request.path != \"/over18\" and\n                c.render_style == 'html' and\n                not request.parsed_agent.bot\n            ):\n                return self.intermediate_redirect(\"/over18\", sr_path=False)\n\n        #check whether to allow custom styles\n        c.allow_styles = True\n        c.can_apply_styles = self.allow_stylesheets\n\n        # use override stylesheet if one exists and:\n        #   this page has no custom stylesheet\n        #   or the user disabled the stylesheet for this sr (indiv or global)\n        has_style_override = (c.user.pref_default_theme_sr and\n                feature.is_enabled('stylesheets_everywhere') and\n                Subreddit._by_name(c.user.pref_default_theme_sr).can_view(c.user))\n        sr_stylesheet_enabled = c.user.use_subreddit_style(c.site)\n\n        if (not sr_stylesheet_enabled and\n                not has_style_override):\n            c.can_apply_styles = False\n\n        c.bare_content = request.GET.pop('bare', False)\n\n        c.show_admin_bar = admin_bar_eligible and (c.user_is_admin or g.debug)\n        if not c.show_admin_bar:\n            g.stats.end_logging_timings()\n\n        hooks.get_hook(\"reddit.request.begin\").call()\n\n        c.request_timer.intermediate(\"base-pre\")\n\n    def post(self):\n        MinimalController.post(self)\n        if response.content_type == \"text/html\":\n            self._embed_html_timing_data()\n\n        # allow logged-out JSON requests to be read cross-domain\n        if (not c.cors_checked and request.method.upper() == \"GET\" and\n                not c.user_is_loggedin and c.render_style == \"api\"):\n            response.headers[\"Access-Control-Allow-Origin\"] = \"*\"\n\n            request_origin = request.headers.get('Origin')\n            if request_origin and request_origin != g.origin:\n                g.stats.simple_event('cors.api_request')\n                g.stats.count_string('origins', request_origin)\n\n        if g.tracker_url and request.method.upper() == \"GET\" and is_api():\n            tracking_url = make_url_https(get_pageview_pixel_url())\n            response.headers[\"X-Reddit-Tracking\"] = tracking_url\n\n    def _embed_html_timing_data(self):\n        timings = g.stats.end_logging_timings()\n\n        if not timings or not c.show_admin_bar or c.is_exception_response:\n            return\n\n        timings = [{\n            \"key\": timing.key,\n            \"start\": round(timing.start, 4),\n            \"end\": round(timing.end, 4),\n        } for timing in timings]\n\n        content = flatten_response(response.content)\n        # inject stats script tag at the end of the <body>\n        body_parts = list(content.rpartition(\"</body>\"))\n        if body_parts[1]:\n            script = ('<script type=\"text/javascript\">'\n                      'window.r = window.r || {};'\n                      'r.timings = %s'\n                      '</script>') % simplejson.dumps(timings)\n            body_parts.insert(1, script)\n            response.content = \"\".join(body_parts)\n\n    def search_fail(self, exception):\n        errpage = pages.RedditError(_(\"search failed\"),\n                                    strings.search_failed)\n\n        request.environ['usable_error_content'] = errpage.render()\n        request.environ['retry_after'] = 60\n        abort(503)\n"
  },
  {
    "path": "r2/r2/controllers/redirect.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom pylons import request\nfrom pylons import tmpl_context as c\nfrom pylons.controllers.util import abort\n\nfrom r2.lib.base import BaseController\nfrom r2.lib.validator import chkuser\nfrom r2.models import Subreddit\n\n\nclass RedirectController(BaseController):\n    def pre(self, *k, **kw):\n        BaseController.pre(self, *k, **kw)\n        c.extension = request.environ.get('extension')\n\n    def GET_redirect(self, dest):\n        return self.redirect(str(dest))\n\n    def GET_user_redirect(self, username, rest=None):\n        user = chkuser(username)\n        if not user:\n            abort(400)\n        url = \"/user/\" + user\n        if rest:\n            url += \"/\" + rest\n        if request.query_string:\n            url += \"?\" + request.query_string\n        return self.redirect(str(url), code=301)\n\n    def GET_timereddit_redirect(self, timereddit, rest=None):\n        sr_name = \"t:\" + timereddit\n        if not Subreddit.is_valid_name(sr_name, allow_time_srs=True):\n            abort(400)\n        if rest:\n            rest = str(rest)\n        else:\n            rest = ''\n        return self.redirect(\"/r/%s/%s\" % (sr_name, rest), code=301)\n"
  },
  {
    "path": "r2/r2/controllers/robots.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\nfrom pylons import request, response\nfrom pylons import app_globals as g\n\nfrom r2.controllers.reddit_base import MinimalController\nfrom r2.lib.base import abort\nfrom r2.lib.pages import Robots, CrossDomain\nfrom r2.lib import utils\n\n\nclass RobotsController(MinimalController):\n    def pre(self):\n        pass\n\n    def post(self):\n        pass\n\n    def on_crawlable_domain(self):\n        # This ensures we don't have the port included.\n        requested_domain = utils.domain(request.host)\n\n        # If someone CNAMEs myspammysite.com to reddit.com or something, we\n        # don't want search engines to index that.\n        if not utils.is_subdomain(requested_domain, g.domain):\n            return False\n\n        # Only allow the canonical desktop site and mobile subdomains, since\n        # we have canonicalization set up appropriately for them.\n        # Note: in development, DomainMiddleware needs to be temporarily\n        # modified to not skip assignment of reddit-domain-extension on\n        # localhost for this to work properly.\n        return (requested_domain == g.domain or\n                request.environ.get('reddit-domain-extension') in\n                    ('mobile', 'compact'))\n\n    def GET_robots(self):\n        response.content_type = \"text/plain\"\n        if self.on_crawlable_domain():\n            return Robots().render(style='txt')\n        else:\n            return \"User-Agent: *\\nDisallow: /\\n\"\n\n    def GET_crossdomain(self):\n        # Our middleware is weird and won't let us add a route for just\n        # '/crossdomain.xml'. Just 404 for other extensions.\n        if request.environ.get('extension', None) != 'xml':\n            abort(404)\n        response.content_type = \"text/x-cross-domain-policy\"\n        return CrossDomain().render(style='xml')\n"
  },
  {
    "path": "r2/r2/controllers/toolbar.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\nimport re\nimport string\n\nfrom pylons import tmpl_context as c\nfrom pylons import app_globals as g\n\nfrom reddit_base import RedditController\nfrom r2.lib import utils\nfrom r2.lib.pages import *\nfrom r2.lib.pages.things import hot_links_by_url_listing\nfrom r2.lib.template_helpers import add_sr\nfrom r2.lib.validator import *\nfrom r2.models import *\nfrom r2.models.admintools import is_shamed_domain\n\n# strips /r/foo/, /s/, or both\nstrip_sr          = re.compile('\\A/r/[a-zA-Z0-9_-]+')\nstrip_s_path      = re.compile('\\A/s/')\nleading_slash     = re.compile('\\A/+')\nhas_protocol      = re.compile('\\A[a-zA-Z_-]+:')\nallowed_protocol  = re.compile('\\Ahttps?:')\nneed_insert_slash = re.compile('\\Ahttps?:/[^/]')\ndef demangle_url(path):\n    # there's often some URL mangling done by the stack above us, so\n    # let's clean up the URL before looking it up\n    path = strip_sr.sub('', path)\n    path = strip_s_path.sub('', path)\n    path = leading_slash.sub(\"\", path)\n\n    if has_protocol.match(path):\n        if not allowed_protocol.match(path):\n            return None\n    else:\n        path = '%s://%s' % (g.default_scheme, path)\n\n    if need_insert_slash.match(path):\n        path = string.replace(path, '/', '//', 1)\n\n    try:\n        path = utils.sanitize_url(path)\n    except TypeError:\n        return None\n\n    return path\n\ndef force_html():\n    \"\"\"Because we can take URIs like /s/http://.../foo.png, and we can\n       guarantee that the toolbar will never be used with a non-HTML\n       render style, we don't want to interpret the extension from the\n       target URL. So here we rewrite Middleware's interpretation of\n       the extension to force it to be HTML\n    \"\"\"\n\n    c.render_style = 'html'\n    c.extension = None\n    c.content_type = 'text/html; charset=UTF-8'\n\n\nclass ToolbarController(RedditController):\n\n    allow_stylesheets = True\n\n    @validate(link1 = VByName('id'),\n              link2 = VLink('id', redirect = False))\n    def GET_goto(self, link1, link2):\n        \"\"\"Support old /goto?id= urls. deprecated\"\"\"\n        link = link2 if link2 else link1\n        if link:\n            return self.redirect(add_sr(\"/tb/\" + link._id36))\n        return self.abort404()\n\n    @validate(urloid=nop('urloid'))\n    def GET_s(self, urloid):\n        \"\"\"/s/http://..., show a given URL with the toolbar. if it's\n           submitted, redirect to /tb/$id36\"\"\"\n        force_html()\n        path = demangle_url(request.fullpath)\n\n        if not path:\n            # it was malformed\n            self.abort404()\n\n        # if the domain is shame-banned, bail out.\n        if is_shamed_domain(path)[0]:\n            self.abort404()\n\n        listing = hot_links_by_url_listing(path, sr=c.site, num=1)\n        link = listing.things[0] if listing.things else None\n\n        if link:\n            # we were able to find it, let's send them to the\n            # toolbar (if enabled) or comments (if not)\n            return self.redirect(add_sr(\"/tb/\" + link._id36))\n        else:\n            # It hasn't been submitted yet. Give them a chance to\n            qs = utils.query_string({\"url\": path})\n            return self.redirect(add_sr(\"/submit\" + qs))\n\n    def GET_redirect(self):\n        return self.redirect('/', code=301)\n"
  },
  {
    "path": "r2/r2/controllers/web.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport hashlib\nimport hmac\nimport json\nimport re\n\nfrom pylons import request, response\nfrom pylons import tmpl_context as c\nfrom pylons import app_globals as g\nfrom pylons.i18n import _\n\nfrom r2.controllers.reddit_base import RedditController, abort_with_error\nfrom r2.lib.base import abort\nfrom r2.lib.cache_poisoning import make_poisoning_report_mac\nfrom r2.lib.csrf import csrf_exempt\nfrom r2.lib.utils import constant_time_compare, UrlParser, is_subdomain\nfrom r2.lib.validator import (\n    nop,\n    validate,\n    VFloat,\n    VInt,\n    VOneOf,\n    VPrintable,\n    VRatelimit,\n    VValidatedJSON,\n)\n\n\nclass WebLogController(RedditController):\n    on_validation_error = staticmethod(abort_with_error)\n\n    @csrf_exempt\n    @validate(\n        VRatelimit(rate_user=False, rate_ip=True, prefix='rate_weblog_'),\n        level=VOneOf('level', ('error',)),\n        logs=VValidatedJSON('logs',\n            VValidatedJSON.ArrayOf(VValidatedJSON.PartialObject({\n                'msg': VPrintable('msg', max_length=256),\n                'url': VPrintable('url', max_length=256),\n                'tag': VPrintable('tag', max_length=32),\n            }))\n        ),\n    )\n    def POST_message(self, level, logs):\n        # Whitelist tags to keep the frontend from creating too many keys in statsd\n        valid_frontend_log_tags = {\n            'unknown',\n            'reddit-config-migrate-error',\n        }\n\n        # prevent simple CSRF by requiring a custom header\n        if not request.headers.get('X-Loggit'):\n            abort(403)\n\n        uid = c.user._id if c.user_is_loggedin else '-'\n\n        # only accept a maximum of 3 entries per request\n        for log in logs[:3]:\n            if 'msg' not in log or 'url' not in log:\n                continue\n\n            tag = 'unknown'\n\n            if log.get('tag') in valid_frontend_log_tags:\n                tag = log['tag']\n\n            g.stats.simple_event('frontend.error.' + tag)\n\n            g.log.warning('[web frontend] %s: %s | U: %s FP: %s UA: %s',\n                          level, log['msg'], uid, log['url'],\n                          request.user_agent)\n\n        VRatelimit.ratelimit(rate_user=False, rate_ip=True,\n                             prefix=\"rate_weblog_\", seconds=10)\n\n    def OPTIONS_report_cache_poisoning(self):\n        \"\"\"Send CORS headers for cache poisoning reports.\"\"\"\n        if \"Origin\" not in request.headers:\n            return\n        origin = request.headers[\"Origin\"]\n        parsed_origin = UrlParser(origin)\n        if not is_subdomain(parsed_origin.hostname, g.domain):\n            return\n        response.headers[\"Access-Control-Allow-Origin\"] = origin\n        response.headers[\"Access-Control-Allow-Methods\"] = \"POST\"\n        response.headers[\"Access-Control-Allow-Headers\"] = \\\n            \"Authorization, X-Loggit, \"\n        response.headers[\"Access-Control-Allow-Credentials\"] = \"false\"\n        response.headers['Access-Control-Expose-Headers'] = \\\n            self.COMMON_REDDIT_HEADERS\n\n    @csrf_exempt\n    @validate(\n        VRatelimit(rate_user=False, rate_ip=True, prefix='rate_poison_'),\n        report_mac=VPrintable('report_mac', 255),\n        poisoner_name=VPrintable('poisoner_name', 255),\n        poisoner_id=VInt('poisoner_id'),\n        poisoner_canary=VPrintable('poisoner_canary', 2, min_length=2),\n        victim_canary=VPrintable('victim_canary', 2, min_length=2),\n        render_time=VInt('render_time'),\n        route_name=VPrintable('route_name', 255),\n        url=VPrintable('url', 2048),\n        # To differentiate between web and mweb in the future\n        source=VOneOf('source', ('web', 'mweb')),\n        cache_policy=VOneOf('cache_policy',\n            ('loggedin_www', 'loggedin_www_new', 'loggedin_mweb')\n        ),\n        # JSON-encoded response headers from when our script re-requested\n        # the poisoned page\n        resp_headers=nop('resp_headers'),\n    )\n    def POST_report_cache_poisoning(\n            self,\n            report_mac,\n            poisoner_name,\n            poisoner_id,\n            poisoner_canary,\n            victim_canary,\n            render_time,\n            route_name,\n            url,\n            source,\n            cache_policy,\n            resp_headers,\n    ):\n        \"\"\"Report an instance of cache poisoning and its details\"\"\"\n\n        self.OPTIONS_report_cache_poisoning()\n\n        if c.errors:\n            abort(400)\n\n        # prevent simple CSRF by requiring a custom header\n        if not request.headers.get('X-Loggit'):\n            abort(403)\n\n        # Eh? Why are you reporting this if the canaries are the same?\n        if poisoner_canary == victim_canary:\n            abort(400)\n\n        expected_mac = make_poisoning_report_mac(\n            poisoner_canary=poisoner_canary,\n            poisoner_name=poisoner_name,\n            poisoner_id=poisoner_id,\n            cache_policy=cache_policy,\n            source=source,\n            route_name=route_name,\n        )\n        if not constant_time_compare(report_mac, expected_mac):\n            abort(403)\n\n        if resp_headers:\n            try:\n                resp_headers = json.loads(resp_headers)\n                # Verify this is a JSON map of `header_name => [value, ...]`\n                if not isinstance(resp_headers, dict):\n                    abort(400)\n                for hdr_name, hdr_vals in resp_headers.iteritems():\n                    if not isinstance(hdr_name, basestring):\n                        abort(400)\n                    if not all(isinstance(h, basestring) for h in hdr_vals):\n                        abort(400)\n            except ValueError:\n                abort(400)\n\n        if not resp_headers:\n            resp_headers = {}\n\n        poison_info = dict(\n            poisoner_name=poisoner_name,\n            poisoner_id=str(poisoner_id),\n            # Convert the JS timestamp to a standard one\n            render_time=render_time * 1000,\n            route_name=route_name,\n            url=url,\n            source=source,\n            cache_policy=cache_policy,\n            resp_headers=resp_headers,\n        )\n\n        # For immediate feedback when tracking the effects of caching changes\n        g.stats.simple_event(\"cache.poisoning.%s.%s\" % (source, cache_policy))\n        # For longer-term diagnosing of caching issues\n        g.events.cache_poisoning_event(poison_info, request=request, context=c)\n\n        VRatelimit.ratelimit(rate_ip=True, prefix=\"rate_poison_\", seconds=10)\n\n        return self.api_wrapper({})\n"
  },
  {
    "path": "r2/r2/controllers/wiki.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom pylons import request\nfrom pylons import tmpl_context as c\nfrom pylons import app_globals as g\n\nfrom reddit_base import RedditController\nfrom r2.controllers.oauth2 import require_oauth2_scope\nfrom r2.lib.utils import url_links_builder\nfrom reddit_base import paginated_listing\nfrom r2.models.wiki import (\n    ContentLengthError,\n    modactions,\n    WikiPage,\n    WikiPageExists,\n    WikiRevision,\n)\nfrom r2.models.subreddit import Subreddit\nfrom r2.models.modaction import ModAction\nfrom r2.models.builder import WikiRevisionBuilder, WikiRecentRevisionBuilder\n\nfrom r2.lib.template_helpers import join_urls\n\n\nfrom r2.lib.validator import (\n    nop,\n    validate,\n    VAdmin,\n    VBoolean,\n    VExistingUname,\n    VInt,\n    VMarkdown,\n    VModhash,\n    VNotInTimeout,\n    VOneOf,\n    VPrintable,\n    VRatelimit,\n)\n\nfrom r2.lib.validator.wiki import (\n    VWikiPage,\n    VWikiPageAndVersion,\n    VWikiModerator,\n    VWikiPageRevise,\n    this_may_revise,\n    this_may_view,\n    VWikiPageName,\n)\nfrom r2.controllers.api_docs import api_doc, api_section\nfrom r2.lib.pages.wiki import (WikiPageView, WikiNotFound, WikiRevisions,\n                              WikiEdit, WikiSettings, WikiRecent,\n                              WikiListing, WikiDiscussions,\n                              WikiCreate)\n\nfrom r2.config.extensions import set_extension\nfrom r2.lib.template_helpers import add_sr\nfrom r2.lib.db import tdb_cassandra\nfrom r2.models.listing import WikiRevisionListing\nfrom r2.lib.pages.things import default_thing_wrapper\nfrom r2.lib.pages import BoringPage, CssError\nfrom reddit_base import base_listing\nfrom r2.models import IDBuilder, LinkListing, DefaultSR\nfrom r2.lib.merge import ConflictException, make_htmldiff\nfrom pylons.i18n import _\nfrom r2.lib.pages import PaneStack\nfrom r2.lib.utils import timesince\nfrom r2.config import extensions\nfrom r2.lib.base import abort\nfrom r2.lib.errors import reddit_http_error\nfrom r2.lib.automoderator import Ruleset\n\nimport json\n\npage_descriptions = {\n    \"config/stylesheet\": _(\"This page is the subreddit stylesheet, changes here apply to the subreddit css\"),\n    \"config/submit_text\": _(\"The contents of this page appear on the submit page\"),\n    \"config/sidebar\": _(\"The contents of this page appear on the subreddit sidebar\"),\n    \"config/description\": _(\"The contents of this page appear in the public subreddit description and when the user does not have access to the subreddit\"),\n    \"config/automoderator\": _(\"This page is used to configure AutoModerator for the subreddit, please see [the full documentation](/wiki/automoderator/full-documentation) for information\"),\n}\n\nATTRIBUTE_BY_PAGE = {\"config/sidebar\": \"description\",\n                     \"config/submit_text\": \"submit_text\",\n                     \"config/description\": \"public_description\"}\nRENDERERS_BY_PAGE = {\n    \"config/automoderator\": \"automoderator\",\n    \"config/description\": \"reddit\",\n    \"config/sidebar\": \"reddit\",\n    \"config/stylesheet\": \"stylesheet\",\n    \"config/submit_text\": \"reddit\",\n    \"toolbox\": \"rawcode\",\n    \"usernotes\": \"rawcode\",\n}\n\nclass WikiController(RedditController):\n    allow_stylesheets = True\n\n    @require_oauth2_scope(\"wikiread\")\n    @api_doc(api_section.wiki, uri='/wiki/{page}', uses_site=True)\n    @validate(pv=VWikiPageAndVersion(('page', 'v', 'v2'),\n                                     required=False,\n                                     restricted=False,\n                                     allow_hidden_revision=False),\n              page_name=VWikiPageName('page',\n                                      error_on_name_normalized=True))\n    def GET_wiki_page(self, pv, page_name):\n        \"\"\"Return the content of a wiki page\n\n        If `v` is given, show the wiki page as it was at that version\n        If both `v` and `v2` are given, show a diff of the two\n\n        \"\"\"\n        message = None\n\n        if c.errors.get(('PAGE_NAME_NORMALIZED', 'page')):\n            url = join_urls(c.wiki_base_url, page_name)\n            return self.redirect(url)\n\n        page, version, version2 = pv\n\n        if not page:\n            is_api = c.render_style in extensions.API_TYPES\n            if this_may_revise():\n                if is_api:\n                    self.handle_error(404, 'PAGE_NOT_CREATED')\n                errorpage = WikiNotFound(page=page_name)\n                request.environ['usable_error_content'] = errorpage.render()\n            elif is_api:\n                self.handle_error(404, 'PAGE_NOT_FOUND')\n            self.abort404()\n\n        if version:\n            edit_by = version.get_author()\n            edit_date = version.date\n        else:\n            edit_by = page.get_author()\n            edit_date = page._get('last_edit_date')\n\n        diffcontent = None\n        if not version:\n            content = page.content\n            if c.is_wiki_mod and page.name in page_descriptions:\n                message = page_descriptions[page.name]\n        else:\n            message = _(\"viewing revision from %s\") % timesince(version.date)\n            if version2:\n                t1 = timesince(version.date)\n                t2 = timesince(version2.date)\n                timestamp1 = _(\"%s ago\") % t1\n                timestamp2 = _(\"%s ago\") % t2\n                message = _(\"comparing revisions from %(date_1)s and %(date_2)s\") \\\n                          % {'date_1': t1, 'date_2': t2}\n                diffcontent = make_htmldiff(version.content, version2.content, timestamp1, timestamp2)\n                content = version2.content\n            else:\n                message = _(\"viewing revision from %s ago\") % timesince(version.date)\n                content = version.content\n\n        renderer = RENDERERS_BY_PAGE.get(page.name, 'wiki') \n\n        return WikiPageView(content, alert=message, v=version, diff=diffcontent,\n                            may_revise=this_may_revise(page), edit_by=edit_by,\n                            edit_date=edit_date, page=page.name,\n                            renderer=renderer).render()\n\n    @require_oauth2_scope(\"wikiread\")\n    @api_doc(api_section.wiki, uri='/wiki/revisions/{page}', uses_site=True)\n    @paginated_listing(max_page_size=100, backend='cassandra')\n    @validate(page=VWikiPage(('page'), restricted=False))\n    def GET_wiki_revisions(self, num, after, reverse, count, page):\n        \"\"\"Retrieve a list of revisions of this wiki `page`\"\"\"\n        revisions = page.get_revisions()\n        wikiuser = c.user if c.user_is_loggedin else None\n        builder = WikiRevisionBuilder(revisions, user=wikiuser, sr=c.site,\n                                      num=num, reverse=reverse, count=count,\n                                      after=after, skip=not c.is_wiki_mod,\n                                      wrap=default_thing_wrapper(),\n                                      page=page)\n        listing = WikiRevisionListing(builder).listing()\n        return WikiRevisions(listing, page=page.name, may_revise=this_may_revise(page)).render()\n\n    @validate(wp=VWikiPageRevise('page'),\n              page=VWikiPageName('page'))\n    def GET_wiki_create(self, wp, page):\n        api = c.render_style in extensions.API_TYPES\n        error = c.errors.get(('WIKI_CREATE_ERROR', 'page'))\n        if error:\n            error = error.msg_params\n        if wp[0]:\n            VNotInTimeout().run(action_name=\"wikirevise\",\n                details_text=\"create\", target=page)\n            return self.redirect(join_urls(c.wiki_base_url, wp[0].name))\n        elif api:\n            if error:\n                self.handle_error(403, **error)\n            else:\n                self.handle_error(404, 'PAGE_NOT_CREATED')\n        elif error:\n            error_msg = ''\n            if error['reason'] == 'PAGE_NAME_LENGTH':\n                error_msg = _(\"this wiki cannot handle page names of that magnitude!  please select a page name shorter than %d characters\") % error['max_length']\n            elif error['reason'] == 'PAGE_CREATED_ELSEWHERE':\n                error_msg = _(\"this page is a special page, please go into the subreddit settings and save the field once to create this special page\")\n            elif error['reason'] == 'PAGE_NAME_MAX_SEPARATORS':\n                error_msg = _('a max of %d separators \"/\" are allowed in a wiki page name.') % error['max_separators']\n            return BoringPage(_(\"Wiki error\"), infotext=error_msg).render()\n        else:\n            VNotInTimeout().run(action_name=\"wikirevise\",\n                details_text=\"create\")\n            return WikiCreate(page=page, may_revise=True).render()\n\n    @validate(wp=VWikiPageRevise('page', restricted=True, required=True))\n    def GET_wiki_revise(self, wp, page, message=None, **kw):\n        wp = wp[0]\n        VNotInTimeout().run(action_name=\"wikirevise\", details_text=\"revise\",\n            target=wp)\n        error = c.errors.get(('MAY_NOT_REVISE', 'page'))\n        if error:\n            self.handle_error(403, **(error.msg_params or {}))\n        \n        previous = kw.get('previous', wp._get('revision'))\n        content = kw.get('content', wp.content)\n        if not message and wp.name in page_descriptions:\n            message = page_descriptions[wp.name]\n        return WikiEdit(content, previous, alert=message, page=wp.name,\n                        may_revise=True).render()\n\n    @require_oauth2_scope(\"wikiread\")\n    @api_doc(api_section.wiki, uri='/wiki/revisions', uses_site=True)\n    @paginated_listing(max_page_size=100, backend='cassandra')\n    def GET_wiki_recent(self, num, after, reverse, count):\n        \"\"\"Retrieve a list of recently changed wiki pages in this subreddit\"\"\"\n        revisions = WikiRevision.get_recent(c.site)\n        wikiuser = c.user if c.user_is_loggedin else None\n        builder = WikiRecentRevisionBuilder(revisions,  num=num, count=count,\n                                            reverse=reverse, after=after,\n                                            wrap=default_thing_wrapper(),\n                                            skip=not c.is_wiki_mod,\n                                            user=wikiuser, sr=c.site)\n        listing = WikiRevisionListing(builder).listing()\n        return WikiRecent(listing).render()\n\n    @require_oauth2_scope(\"wikiread\")\n    @api_doc(api_section.wiki, uri='/wiki/pages', uses_site=True)\n    def GET_wiki_listing(self):\n        \"\"\"Retrieve a list of wiki pages in this subreddit\"\"\"\n        def check_hidden(page):\n            return page.listed and this_may_view(page)\n        pages, linear_pages = WikiPage.get_listing(c.site, filter_check=check_hidden)\n        return WikiListing(pages, linear_pages).render()\n\n    def GET_wiki_redirect(self, page='index'):\n        return self.redirect(str(\"%s/%s\" % (c.wiki_base_url, page)), code=301)\n\n    @require_oauth2_scope(\"wikiread\")\n    @api_doc(api_section.wiki, uri='/wiki/discussions/{page}', uses_site=True)\n    @base_listing\n    @validate(page=VWikiPage('page', restricted=True))\n    def GET_wiki_discussions(self, page, num, after, reverse, count):\n        \"\"\"Retrieve a list of discussions about this wiki `page`\"\"\"\n        page_url = add_sr(\"%s/%s\" % (c.wiki_base_url, page.name))\n        builder = url_links_builder(page_url, num=num, after=after,\n                                    reverse=reverse, count=count)\n        listing = LinkListing(builder).listing()\n        return WikiDiscussions(listing, page=page.name,\n                               may_revise=this_may_revise(page)).render()\n\n    @require_oauth2_scope(\"modwiki\")\n    @api_doc(api_section.wiki, uri='/wiki/settings/{page}', uses_site=True)\n    @validate(page=VWikiPage('page', restricted=True, modonly=True))\n    def GET_wiki_settings(self, page):\n        \"\"\"Retrieve the current permission settings for `page`\"\"\"\n        settings = {'permlevel': page._get('permlevel', 0),\n                    'listed': page.listed}\n        VNotInTimeout().run(action_name=\"pageview\",\n                details_text=\"wikisettings\", target=page)\n        mayedit = page.get_editor_accounts()\n        restricted = (not page.special) and page.restricted\n        show_editors = not restricted\n        return WikiSettings(settings, mayedit, show_settings=not page.special,\n                            page=page.name, show_editors=show_editors,\n                            restricted=restricted,\n                            may_revise=True).render()\n\n    @require_oauth2_scope(\"modwiki\")\n    @api_doc(api_section.wiki, uri='/wiki/settings/{page}', uses_site=True)\n    @validate(\n        VModhash(),\n        page=VWikiPage('page', restricted=True, modonly=True),\n        permlevel=VInt('permlevel'),\n        listed=VBoolean('listed'),\n    )\n    def POST_wiki_settings(self, page, permlevel, listed):\n        \"\"\"Update the permissions and visibility of wiki `page`\"\"\"\n        oldpermlevel = page.permlevel\n        if oldpermlevel != permlevel:\n            VNotInTimeout().run(action_name=\"wikipermlevel\",\n                details_text=\"edit\", target=page)\n        if page.listed != listed:\n            VNotInTimeout().run(action_name=\"wikipagelisted\",\n                details_text=\"edit\", target=page)\n\n        try:\n            page.change_permlevel(permlevel)\n        except ValueError:\n            self.handle_error(403, 'INVALID_PERMLEVEL')\n        if page.listed != listed:\n            page.listed = listed\n            page._commit()\n            verb = 'Relisted' if listed else 'Delisted'\n            description = '%s page %s' % (verb, page.name)\n            ModAction.create(c.site, c.user, 'wikipagelisted',\n                             description=description)\n        if oldpermlevel != permlevel:\n            description = 'Page: %s, Changed from %s to %s' % (\n                page.name, oldpermlevel, permlevel\n            )\n            ModAction.create(c.site, c.user, 'wikipermlevel',\n                             description=description)\n        return self.GET_wiki_settings(page=page.name)\n\n    def on_validation_error(self, error):\n        RedditController.on_validation_error(self, error)\n        if error.code:\n            self.handle_error(error.code, error.name)\n\n    def handle_error(self, code, reason=None, **data):\n        abort(reddit_http_error(code, reason, **data))\n\n    def pre(self):\n        RedditController.pre(self)\n        if g.disable_wiki and not c.user_is_admin:\n            self.handle_error(403, 'WIKI_DOWN')\n        if not c.site._should_wiki:\n            self.handle_error(404, 'NOT_WIKIABLE')  # /r/mod for an example\n        frontpage = isinstance(c.site, DefaultSR)\n        c.wiki_base_url = join_urls(c.site.path, 'wiki')\n        c.wiki_api_url = join_urls(c.site.path, '/api/wiki')\n        c.wiki_id = g.default_sr if frontpage else c.site.name\n        self.editconflict = False\n        c.is_wiki_mod = (\n            c.user_is_admin or c.site.is_moderator_with_perms(c.user, 'wiki')\n            ) if c.user_is_loggedin else False\n        c.wikidisabled = False\n\n        mode = c.site.wikimode\n        if not mode or mode == 'disabled':\n            if not c.is_wiki_mod:\n                self.handle_error(403, 'WIKI_DISABLED')\n            else:\n                c.wikidisabled = True\n\n    # Redirects from the old wiki\n    def GET_faq(self):\n        return self.GET_wiki_redirect(page='faq')\n\n    GET_help = GET_wiki_redirect\n\n\nclass WikiApiController(WikiController):\n    @require_oauth2_scope(\"wikiedit\")\n    @validate(VModhash(),\n              pageandprevious=VWikiPageRevise(('page', 'previous'), restricted=True),\n              content=nop(('content')),\n              page_name=VWikiPageName('page'),\n              reason=VPrintable('reason', 256, empty_error=None))\n    @api_doc(api_section.wiki, uri='/api/wiki/edit', uses_site=True)\n    def POST_wiki_edit(self, pageandprevious, content, page_name, reason):\n        \"\"\"Edit a wiki `page`\"\"\"\n        page, previous = pageandprevious\n\n        if c.user._spam:\n            error = _(\"You are doing that too much, please try again later.\")\n            self.handle_error(415, 'SPECIAL_ERRORS', special_errors=[error])\n\n        if not page:\n            error = c.errors.get(('WIKI_CREATE_ERROR', 'page'))\n            if error:\n                self.handle_error(403, **(error.msg_params or {}))\n\n            VNotInTimeout().run(action_name=\"wikirevise\", details_text=\"create\")\n            try:\n                page = WikiPage.create(c.site, page_name)\n            except WikiPageExists:\n                self.handle_error(400, 'WIKI_CREATE_ERROR')\n\n        else:\n            VNotInTimeout().run(action_name=\"wikirevise\", details_text=\"edit\",\n                target=page)\n            error = c.errors.get(('MAY_NOT_REVISE', 'page'))\n            if error:\n                self.handle_error(403, **(error.msg_params or {}))\n\n        renderer = RENDERERS_BY_PAGE.get(page.name, 'wiki')\n        if renderer in ('wiki', 'reddit'):\n            content = VMarkdown(('content'), renderer=renderer).run(content)\n\n        # Use the raw POST value as we need to tell the difference between\n        # None/Undefined and an empty string.  The validators use a default\n        # value with both of those cases and would need to be changed.\n        # In order to avoid breaking functionality, this was done instead.\n        previous = previous._id if previous else request.POST.get('previous')\n        try:\n            # special validation methods\n            if page.name == 'config/stylesheet':\n                css_errors, parsed = c.site.parse_css(content, verify=False)\n                if g.css_killswitch:\n                    self.handle_error(403, 'STYLESHEET_EDIT_DENIED')\n                if css_errors:\n                    error_items = [CssError(x).message for x in css_errors]\n                    self.handle_error(415, 'SPECIAL_ERRORS', special_errors=error_items)\n            elif page.name == \"config/automoderator\":\n                try:\n                    rules = Ruleset(content)\n                except ValueError as e:\n                    error_items = [e.message]\n                    self.handle_error(415, \"SPECIAL_ERRORS\", special_errors=error_items)\n\n            # special saving methods\n            if page.name == \"config/stylesheet\":\n                c.site.change_css(content, parsed, previous, reason=reason)\n            else:\n                try:\n                    page.revise(content, previous, c.user._id36, reason=reason)\n                except ContentLengthError as e:\n                    self.handle_error(403, 'CONTENT_LENGTH_ERROR', max_length=e.max_length)\n\n                # continue storing the special pages as data attributes on the subreddit\n                # object. TODO: change this to minimize subreddit get sizes.\n                if page.special and page.name in ATTRIBUTE_BY_PAGE:\n                    setattr(c.site, ATTRIBUTE_BY_PAGE[page.name], content)\n                    c.site._commit()\n\n                if page.special or c.is_wiki_mod:\n                    description = modactions.get(page.name, 'Page %s edited' % page.name)\n                    ModAction.create(c.site, c.user, \"wikirevise\",\n                        details=description, description=reason)\n        except ConflictException as e:\n            self.handle_error(409, 'EDIT_CONFLICT', newcontent=e.new, newrevision=page.revision, diffcontent=e.htmldiff)\n        return json.dumps({})\n\n    @require_oauth2_scope(\"modwiki\")\n    @validate(VModhash(),\n              VWikiModerator(),\n              page=VWikiPage('page'),\n              act=VOneOf('act', ('del', 'add')),\n              user=VExistingUname('username'))\n    @api_doc(api_section.wiki, uri='/api/wiki/alloweditor/{act}',\n             uses_site=True,\n             uri_variants=['/api/wiki/alloweditor/%s' % act for act in ('del', 'add')])\n    def POST_wiki_allow_editor(self, act, page, user):\n        \"\"\"Allow/deny `username` to edit this wiki `page`\"\"\"\n        if not user:\n            self.handle_error(404, 'UNKNOWN_USER')\n        elif act == 'del':\n            VNotInTimeout().run(action_name=\"wikipermlevel\",\n                details_text=\"del_editor\", target=user)\n            page.remove_editor(user._id36)\n        elif act == 'add':\n            VNotInTimeout().run(action_name=\"wikipermlevel\",\n                details_text=\"allow_editor\", target=user)\n            page.add_editor(user._id36)\n        else:\n            self.handle_error(400, 'INVALID_ACTION')\n        return json.dumps({})\n\n    @validate(\n        VModhash(),\n        VAdmin(),\n        pv=VWikiPageAndVersion(('page', 'revision')),\n        deleted=VBoolean('deleted'),\n    )\n    def POST_wiki_revision_delete(self, pv, deleted):\n        page, revision = pv\n        if not revision:\n            self.handle_error(400, 'INVALID_REVISION')\n        if deleted and page.revision == str(revision._id):\n            self.handle_error(400, 'REVISION_IS_CURRENT')\n        revision.admin_deleted = deleted\n        revision._commit()\n        return json.dumps({'status': revision.admin_deleted})\n\n    @require_oauth2_scope(\"modwiki\")\n    @validate(VModhash(),\n              VWikiModerator(),\n              pv=VWikiPageAndVersion(('page', 'revision')))\n    @api_doc(api_section.wiki, uri='/api/wiki/hide', uses_site=True)\n    def POST_wiki_revision_hide(self, pv):\n        \"\"\"Toggle the public visibility of a wiki page revision\"\"\"\n        page, revision = pv\n        if not revision:\n            self.handle_error(400, 'INVALID_REVISION')\n\n        VNotInTimeout().run(action_name=\"wikirevise\",\n                details_text=\"revision_hide\", target=page)\n        return json.dumps({'status': revision.toggle_hide()})\n\n    @require_oauth2_scope(\"modwiki\")\n    @validate(VModhash(),\n              VWikiModerator(),\n              pv=VWikiPageAndVersion(('page', 'revision')))\n    @api_doc(api_section.wiki, uri='/api/wiki/revert', uses_site=True)\n    def POST_wiki_revision_revert(self, pv):\n        \"\"\"Revert a wiki `page` to `revision`\"\"\"\n        page, revision = pv\n        if not revision:\n            self.handle_error(400, 'INVALID_REVISION')\n        VNotInTimeout().run(action_name=\"wikirevise\",\n                details_text=\"revision_revert\", target=page)\n        content = revision.content\n        reason = 'reverted back %s' % timesince(revision.date)\n        if page.name == 'config/stylesheet':\n            css_errors, parsed = c.site.parse_css(content)\n            if css_errors:\n                self.handle_error(403, 'INVALID_CSS')\n            c.site.change_css(content, parsed, prev=None, reason=reason, force=True)\n        else:\n            try:\n                page.revise(content, author=c.user._id36, reason=reason, force=True)\n\n                # continue storing the special pages as data attributes on the subreddit\n                # object. TODO: change this to minimize subreddit get sizes.\n                if page.name in ATTRIBUTE_BY_PAGE:\n                    setattr(c.site, ATTRIBUTE_BY_PAGE[page.name], content)\n                    c.site._commit()\n            except ContentLengthError as e:\n                self.handle_error(403, 'CONTENT_LENGTH_ERROR', max_length=e.max_length)\n        return json.dumps({})\n\n    def pre(self):\n        WikiController.pre(self)\n        c.render_style = 'api'\n        set_extension(request.environ, 'json')\n\n"
  },
  {
    "path": "r2/r2/data/locations.json",
    "content": "{\n  \"AD\": {\n    \"name\": \"Andorra\"\n  }, \n  \"AE\": {\n    \"name\": \"United Arab Emirates\"\n  }, \n  \"AF\": {\n    \"name\": \"Afghanistan\"\n  }, \n  \"AG\": {\n    \"name\": \"Antigua and Barbuda\"\n  }, \n  \"AI\": {\n    \"name\": \"Anguilla\"\n  }, \n  \"AL\": {\n    \"name\": \"Albania\"\n  }, \n  \"AM\": {\n    \"name\": \"Armenia\"\n  }, \n  \"AO\": {\n    \"name\": \"Angola\"\n  }, \n  \"AP\": {\n    \"name\": \"Asia/Pacific Region\"\n  }, \n  \"AQ\": {\n    \"name\": \"Antarctica\"\n  }, \n  \"AR\": {\n    \"name\": \"Argentina\"\n  }, \n  \"AS\": {\n    \"name\": \"American Samoa\"\n  }, \n  \"AT\": {\n    \"name\": \"Austria\"\n  }, \n  \"AU\": {\n    \"name\": \"Australia\"\n  }, \n  \"AW\": {\n    \"name\": \"Aruba\"\n  }, \n  \"AX\": {\n    \"name\": \"Aland Islands\"\n  }, \n  \"AZ\": {\n    \"name\": \"Azerbaijan\"\n  }, \n  \"BA\": {\n    \"name\": \"Bosnia and Herzegovina\"\n  }, \n  \"BB\": {\n    \"name\": \"Barbados\"\n  }, \n  \"BD\": {\n    \"name\": \"Bangladesh\"\n  }, \n  \"BE\": {\n    \"name\": \"Belgium\"\n  }, \n  \"BF\": {\n    \"name\": \"Burkina Faso\"\n  }, \n  \"BG\": {\n    \"name\": \"Bulgaria\"\n  }, \n  \"BH\": {\n    \"name\": \"Bahrain\"\n  }, \n  \"BI\": {\n    \"name\": \"Burundi\"\n  }, \n  \"BJ\": {\n    \"name\": \"Benin\"\n  }, \n  \"BL\": {\n    \"name\": \"Saint Bartelemey\"\n  }, \n  \"BM\": {\n    \"name\": \"Bermuda\"\n  }, \n  \"BN\": {\n    \"name\": \"Brunei Darussalam\"\n  }, \n  \"BO\": {\n    \"name\": \"Bolivia\"\n  }, \n  \"BQ\": {\n    \"name\": \"Bonaire\"\n  }, \n  \"BR\": {\n    \"name\": \"Brazil\"\n  }, \n  \"BS\": {\n    \"name\": \"Bahamas\"\n  }, \n  \"BT\": {\n    \"name\": \"Bhutan\"\n  }, \n  \"BV\": {\n    \"name\": \"Bouvet Island\"\n  }, \n  \"BW\": {\n    \"name\": \"Botswana\"\n  }, \n  \"BY\": {\n    \"name\": \"Belarus\"\n  }, \n  \"BZ\": {\n    \"name\": \"Belize\"\n  }, \n  \"CA\": {\n    \"name\": \"Canada\"\n  }, \n  \"CC\": {\n    \"name\": \"Cocos (Keeling) Islands\"\n  }, \n  \"CD\": {\n    \"name\": \"Congo\"\n  }, \n  \"CF\": {\n    \"name\": \"Central African Republic\"\n  }, \n  \"CG\": {\n    \"name\": \"Congo\"\n  }, \n  \"CH\": {\n    \"name\": \"Switzerland\"\n  }, \n  \"CI\": {\n    \"name\": \"Cote d'Ivoire\"\n  }, \n  \"CK\": {\n    \"name\": \"Cook Islands\"\n  }, \n  \"CL\": {\n    \"name\": \"Chile\"\n  }, \n  \"CM\": {\n    \"name\": \"Cameroon\"\n  }, \n  \"CN\": {\n    \"name\": \"China\"\n  }, \n  \"CO\": {\n    \"name\": \"Colombia\"\n  }, \n  \"CR\": {\n    \"name\": \"Costa Rica\"\n  }, \n  \"CU\": {\n    \"name\": \"Cuba\"\n  }, \n  \"CV\": {\n    \"name\": \"Cape Verde\"\n  }, \n  \"CW\": {\n    \"name\": \"Curacao\"\n  }, \n  \"CX\": {\n    \"name\": \"Christmas Island\"\n  }, \n  \"CY\": {\n    \"name\": \"Cyprus\"\n  }, \n  \"CZ\": {\n    \"name\": \"Czech Republic\"\n  }, \n  \"DE\": {\n    \"name\": \"Germany\"\n  }, \n  \"DJ\": {\n    \"name\": \"Djibouti\"\n  }, \n  \"DK\": {\n    \"name\": \"Denmark\"\n  }, \n  \"DM\": {\n    \"name\": \"Dominica\"\n  }, \n  \"DO\": {\n    \"name\": \"Dominican Republic\"\n  }, \n  \"DZ\": {\n    \"name\": \"Algeria\"\n  }, \n  \"EC\": {\n    \"name\": \"Ecuador\"\n  }, \n  \"EE\": {\n    \"name\": \"Estonia\"\n  }, \n  \"EG\": {\n    \"name\": \"Egypt\"\n  }, \n  \"EH\": {\n    \"name\": \"Western Sahara\"\n  }, \n  \"ER\": {\n    \"name\": \"Eritrea\"\n  }, \n  \"ES\": {\n    \"name\": \"Spain\"\n  }, \n  \"ET\": {\n    \"name\": \"Ethiopia\"\n  }, \n  \"EU\": {\n    \"name\": \"Europe\"\n  }, \n  \"FI\": {\n    \"name\": \"Finland\"\n  }, \n  \"FJ\": {\n    \"name\": \"Fiji\"\n  }, \n  \"FK\": {\n    \"name\": \"Falkland Islands (Malvinas)\"\n  }, \n  \"FM\": {\n    \"name\": \"Micronesia\"\n  }, \n  \"FO\": {\n    \"name\": \"Faroe Islands\"\n  }, \n  \"FR\": {\n    \"name\": \"France\"\n  }, \n  \"GA\": {\n    \"name\": \"Gabon\"\n  }, \n  \"GB\": {\n    \"name\": \"United Kingdom\"\n  }, \n  \"GD\": {\n    \"name\": \"Grenada\"\n  }, \n  \"GE\": {\n    \"name\": \"Georgia\"\n  }, \n  \"GF\": {\n    \"name\": \"French Guiana\"\n  }, \n  \"GG\": {\n    \"name\": \"Guernsey\"\n  }, \n  \"GH\": {\n    \"name\": \"Ghana\"\n  }, \n  \"GI\": {\n    \"name\": \"Gibraltar\"\n  }, \n  \"GL\": {\n    \"name\": \"Greenland\"\n  }, \n  \"GM\": {\n    \"name\": \"Gambia\"\n  }, \n  \"GN\": {\n    \"name\": \"Guinea\"\n  }, \n  \"GP\": {\n    \"name\": \"Guadeloupe\"\n  }, \n  \"GQ\": {\n    \"name\": \"Equatorial Guinea\"\n  }, \n  \"GR\": {\n    \"name\": \"Greece\"\n  }, \n  \"GS\": {\n    \"name\": \"South Georgia and the South Sandwich Islands\"\n  }, \n  \"GT\": {\n    \"name\": \"Guatemala\"\n  }, \n  \"GU\": {\n    \"name\": \"Guam\"\n  }, \n  \"GW\": {\n    \"name\": \"Guinea-Bissau\"\n  }, \n  \"GY\": {\n    \"name\": \"Guyana\"\n  }, \n  \"HK\": {\n    \"name\": \"Hong Kong\"\n  }, \n  \"HM\": {\n    \"name\": \"Heard Island and McDonald Islands\"\n  }, \n  \"HN\": {\n    \"name\": \"Honduras\"\n  }, \n  \"HR\": {\n    \"name\": \"Croatia\"\n  }, \n  \"HT\": {\n    \"name\": \"Haiti\"\n  }, \n  \"HU\": {\n    \"name\": \"Hungary\"\n  }, \n  \"ID\": {\n    \"name\": \"Indonesia\"\n  }, \n  \"IE\": {\n    \"name\": \"Ireland\"\n  }, \n  \"IL\": {\n    \"name\": \"Israel\"\n  }, \n  \"IM\": {\n    \"name\": \"Isle of Man\"\n  }, \n  \"IN\": {\n    \"name\": \"India\"\n  }, \n  \"IO\": {\n    \"name\": \"British Indian Ocean Territory\"\n  }, \n  \"IQ\": {\n    \"name\": \"Iraq\"\n  }, \n  \"IR\": {\n    \"name\": \"Iran\"\n  }, \n  \"IS\": {\n    \"name\": \"Iceland\"\n  }, \n  \"IT\": {\n    \"name\": \"Italy\"\n  }, \n  \"JE\": {\n    \"name\": \"Jersey\"\n  }, \n  \"JM\": {\n    \"name\": \"Jamaica\"\n  }, \n  \"JO\": {\n    \"name\": \"Jordan\"\n  }, \n  \"JP\": {\n    \"name\": \"Japan\"\n  }, \n  \"KE\": {\n    \"name\": \"Kenya\"\n  }, \n  \"KG\": {\n    \"name\": \"Kyrgyzstan\"\n  }, \n  \"KH\": {\n    \"name\": \"Cambodia\"\n  }, \n  \"KI\": {\n    \"name\": \"Kiribati\"\n  }, \n  \"KM\": {\n    \"name\": \"Comoros\"\n  }, \n  \"KN\": {\n    \"name\": \"Saint Kitts and Nevis\"\n  }, \n  \"KP\": {\n    \"name\": \"Korea\"\n  }, \n  \"KR\": {\n    \"name\": \"Korea\"\n  }, \n  \"KW\": {\n    \"name\": \"Kuwait\"\n  }, \n  \"KY\": {\n    \"name\": \"Cayman Islands\"\n  }, \n  \"KZ\": {\n    \"name\": \"Kazakhstan\"\n  }, \n  \"LA\": {\n    \"name\": \"Lao People's Democratic Republic\"\n  }, \n  \"LB\": {\n    \"name\": \"Lebanon\"\n  }, \n  \"LC\": {\n    \"name\": \"Saint Lucia\"\n  }, \n  \"LI\": {\n    \"name\": \"Liechtenstein\"\n  }, \n  \"LK\": {\n    \"name\": \"Sri Lanka\"\n  }, \n  \"LR\": {\n    \"name\": \"Liberia\"\n  }, \n  \"LS\": {\n    \"name\": \"Lesotho\"\n  }, \n  \"LT\": {\n    \"name\": \"Lithuania\"\n  }, \n  \"LU\": {\n    \"name\": \"Luxembourg\"\n  }, \n  \"LV\": {\n    \"name\": \"Latvia\"\n  }, \n  \"LY\": {\n    \"name\": \"Libyan Arab Jamahiriya\"\n  }, \n  \"MA\": {\n    \"name\": \"Morocco\"\n  }, \n  \"MC\": {\n    \"name\": \"Monaco\"\n  }, \n  \"MD\": {\n    \"name\": \"Moldova\"\n  }, \n  \"ME\": {\n    \"name\": \"Montenegro\"\n  }, \n  \"MF\": {\n    \"name\": \"Saint Martin\"\n  }, \n  \"MG\": {\n    \"name\": \"Madagascar\"\n  }, \n  \"MH\": {\n    \"name\": \"Marshall Islands\"\n  }, \n  \"MK\": {\n    \"name\": \"Macedonia\"\n  }, \n  \"ML\": {\n    \"name\": \"Mali\"\n  }, \n  \"MM\": {\n    \"name\": \"Myanmar\"\n  }, \n  \"MN\": {\n    \"name\": \"Mongolia\"\n  }, \n  \"MO\": {\n    \"name\": \"Macao\"\n  }, \n  \"MP\": {\n    \"name\": \"Northern Mariana Islands\"\n  }, \n  \"MQ\": {\n    \"name\": \"Martinique\"\n  }, \n  \"MR\": {\n    \"name\": \"Mauritania\"\n  }, \n  \"MS\": {\n    \"name\": \"Montserrat\"\n  }, \n  \"MT\": {\n    \"name\": \"Malta\"\n  }, \n  \"MU\": {\n    \"name\": \"Mauritius\"\n  }, \n  \"MV\": {\n    \"name\": \"Maldives\"\n  }, \n  \"MW\": {\n    \"name\": \"Malawi\"\n  }, \n  \"MX\": {\n    \"name\": \"Mexico\"\n  }, \n  \"MY\": {\n    \"name\": \"Malaysia\"\n  }, \n  \"MZ\": {\n    \"name\": \"Mozambique\"\n  }, \n  \"NA\": {\n    \"name\": \"Namibia\"\n  }, \n  \"NC\": {\n    \"name\": \"New Caledonia\"\n  }, \n  \"NE\": {\n    \"name\": \"Niger\"\n  }, \n  \"NF\": {\n    \"name\": \"Norfolk Island\"\n  }, \n  \"NG\": {\n    \"name\": \"Nigeria\"\n  }, \n  \"NI\": {\n    \"name\": \"Nicaragua\"\n  }, \n  \"NL\": {\n    \"name\": \"Netherlands\"\n  }, \n  \"NO\": {\n    \"name\": \"Norway\"\n  }, \n  \"NP\": {\n    \"name\": \"Nepal\"\n  }, \n  \"NR\": {\n    \"name\": \"Nauru\"\n  }, \n  \"NU\": {\n    \"name\": \"Niue\"\n  }, \n  \"NZ\": {\n    \"name\": \"New Zealand\"\n  }, \n  \"O1\": {\n    \"name\": \"Other Country\"\n  }, \n  \"OM\": {\n    \"name\": \"Oman\"\n  }, \n  \"PA\": {\n    \"name\": \"Panama\"\n  }, \n  \"PE\": {\n    \"name\": \"Peru\"\n  }, \n  \"PF\": {\n    \"name\": \"French Polynesia\"\n  }, \n  \"PG\": {\n    \"name\": \"Papua New Guinea\"\n  }, \n  \"PH\": {\n    \"name\": \"Philippines\"\n  }, \n  \"PK\": {\n    \"name\": \"Pakistan\"\n  }, \n  \"PL\": {\n    \"name\": \"Poland\"\n  }, \n  \"PM\": {\n    \"name\": \"Saint Pierre and Miquelon\"\n  }, \n  \"PN\": {\n    \"name\": \"Pitcairn\"\n  }, \n  \"PR\": {\n    \"name\": \"Puerto Rico\"\n  }, \n  \"PS\": {\n    \"name\": \"Palestinian Territory\"\n  }, \n  \"PT\": {\n    \"name\": \"Portugal\"\n  }, \n  \"PW\": {\n    \"name\": \"Palau\"\n  }, \n  \"PY\": {\n    \"name\": \"Paraguay\"\n  }, \n  \"QA\": {\n    \"name\": \"Qatar\"\n  }, \n  \"RE\": {\n    \"name\": \"Reunion\"\n  }, \n  \"RO\": {\n    \"name\": \"Romania\"\n  }, \n  \"RS\": {\n    \"name\": \"Serbia\"\n  }, \n  \"RU\": {\n    \"name\": \"Russian Federation\"\n  }, \n  \"RW\": {\n    \"name\": \"Rwanda\"\n  }, \n  \"SA\": {\n    \"name\": \"Saudi Arabia\"\n  }, \n  \"SB\": {\n    \"name\": \"Solomon Islands\"\n  }, \n  \"SC\": {\n    \"name\": \"Seychelles\"\n  }, \n  \"SD\": {\n    \"name\": \"Sudan\"\n  }, \n  \"SE\": {\n    \"name\": \"Sweden\"\n  }, \n  \"SG\": {\n    \"name\": \"Singapore\"\n  }, \n  \"SH\": {\n    \"name\": \"Saint Helena\"\n  }, \n  \"SI\": {\n    \"name\": \"Slovenia\"\n  }, \n  \"SJ\": {\n    \"name\": \"Svalbard and Jan Mayen\"\n  }, \n  \"SK\": {\n    \"name\": \"Slovakia\"\n  }, \n  \"SL\": {\n    \"name\": \"Sierra Leone\"\n  }, \n  \"SM\": {\n    \"name\": \"San Marino\"\n  }, \n  \"SN\": {\n    \"name\": \"Senegal\"\n  }, \n  \"SO\": {\n    \"name\": \"Somalia\"\n  }, \n  \"SR\": {\n    \"name\": \"Suriname\"\n  }, \n  \"SS\": {\n    \"name\": \"South Sudan\"\n  }, \n  \"ST\": {\n    \"name\": \"Sao Tome and Principe\"\n  }, \n  \"SV\": {\n    \"name\": \"El Salvador\"\n  }, \n  \"SX\": {\n    \"name\": \"Sint Maarten\"\n  }, \n  \"SY\": {\n    \"name\": \"Syrian Arab Republic\"\n  }, \n  \"SZ\": {\n    \"name\": \"Swaziland\"\n  }, \n  \"TC\": {\n    \"name\": \"Turks and Caicos Islands\"\n  }, \n  \"TD\": {\n    \"name\": \"Chad\"\n  }, \n  \"TF\": {\n    \"name\": \"French Southern Territories\"\n  }, \n  \"TG\": {\n    \"name\": \"Togo\"\n  }, \n  \"TH\": {\n    \"name\": \"Thailand\"\n  }, \n  \"TJ\": {\n    \"name\": \"Tajikistan\"\n  }, \n  \"TK\": {\n    \"name\": \"Tokelau\"\n  }, \n  \"TL\": {\n    \"name\": \"Timor-Leste\"\n  }, \n  \"TM\": {\n    \"name\": \"Turkmenistan\"\n  }, \n  \"TN\": {\n    \"name\": \"Tunisia\"\n  }, \n  \"TO\": {\n    \"name\": \"Tonga\"\n  }, \n  \"TR\": {\n    \"name\": \"Turkey\"\n  }, \n  \"TT\": {\n    \"name\": \"Trinidad and Tobago\"\n  }, \n  \"TV\": {\n    \"name\": \"Tuvalu\"\n  }, \n  \"TW\": {\n    \"name\": \"Taiwan\"\n  }, \n  \"TZ\": {\n    \"name\": \"Tanzania\"\n  }, \n  \"UA\": {\n    \"name\": \"Ukraine\"\n  }, \n  \"UG\": {\n    \"name\": \"Uganda\"\n  }, \n  \"UM\": {\n    \"name\": \"United States Minor Outlying Islands\"\n  }, \n  \"US\": {\n    \"name\": \"United States\",\n    \"regions\": {\n      \"AK\": {\n        \"metros\": {\n          \"743\": {\n            \"name\": \"Anchorage, AK\"\n          }, \n          \"745\": {\n            \"name\": \"Fairbanks, AK\"\n          }, \n          \"747\": {\n            \"name\": \"Juneau, AK\"\n          }\n        }, \n        \"name\": \"Alaska\"\n      }, \n      \"AL\": {\n        \"metros\": {\n          \"606\": {\n            \"name\": \"Dothan, AL\"\n          }, \n          \"630\": {\n            \"name\": \"Birmingham, AL\"\n          }, \n          \"686\": {\n            \"name\": \"Mobile, AL-Pensacola, FL\"\n          }, \n          \"691\": {\n            \"name\": \"Huntsville-Decatur (Florence), AL\"\n          }, \n          \"698\": {\n            \"name\": \"Montgomery (Selma), AL\"\n          }\n        }, \n        \"name\": \"Alabama\"\n      }, \n      \"AR\": {\n        \"metros\": {\n          \"628\": {\n            \"name\": \"Monroe, LA-El Dorado, AR\"\n          }, \n          \"670\": {\n            \"name\": \"Ft Smith-Springdale, AR\"\n          }, \n          \"693\": {\n            \"name\": \"Little Rock-Pine Bluff, AR\"\n          }, \n          \"734\": {\n            \"name\": \"Jonesboro, AR\"\n          }\n        }, \n        \"name\": \"Arkansas\"\n      }, \n      \"AZ\": {\n        \"metros\": {\n          \"753\": {\n            \"name\": \"Phoenix, AZ\"\n          }, \n          \"771\": {\n            \"name\": \"Yuma, AZ-El Centro, CA\"\n          }, \n          \"789\": {\n            \"name\": \"Tucson (Sierra Vista), AZ\"\n          }\n        }, \n        \"name\": \"Arizona\"\n      }, \n      \"CA\": {\n        \"metros\": {\n          \"771\": {\n            \"name\": \"Yuma, AZ-El Centro, CA\"\n          }, \n          \"800\": {\n            \"name\": \"Bakersfield, CA\"\n          }, \n          \"802\": {\n            \"name\": \"Eureka, CA\"\n          }, \n          \"803\": {\n            \"name\": \"Los Angeles, CA\"\n          }, \n          \"804\": {\n            \"name\": \"Palm Springs, CA\"\n          }, \n          \"807\": {\n            \"name\": \"San Francisco-Oakland-San Jose, CA\"\n          }, \n          \"825\": {\n            \"name\": \"San Diego, CA\"\n          }, \n          \"828\": {\n            \"name\": \"Monterey-Salinas, CA\"\n          }, \n          \"855\": {\n            \"name\": \"Santa Barbara-San Luis Obispo, CA\"\n          }, \n          \"862\": {\n            \"name\": \"Sacramento-Stockton-Modesto, CA\"\n          }, \n          \"866\": {\n            \"name\": \"Fresno-Visalia, CA\"\n          }, \n          \"868\": {\n            \"name\": \"Chico-Redding, CA\"\n          }\n        }, \n        \"name\": \"California\"\n      }, \n      \"CO\": {\n        \"metros\": {\n          \"751\": {\n            \"name\": \"Denver, CO\"\n          }, \n          \"752\": {\n            \"name\": \"Colorado Springs-Pueblo, CO\"\n          }, \n          \"773\": {\n            \"name\": \"Grand Junction-Montrose, CO\"\n          }\n        }, \n        \"name\": \"Colorado\"\n      }, \n      \"CT\": {\n        \"metros\": {\n          \"533\": {\n            \"name\": \"Hartford & New Haven, CT\"\n          }\n        }, \n        \"name\": \"Connecticut\"\n      }, \n      \"DC\": {\n        \"metros\": {\n          \"511\": {\n            \"name\": \"Washington, DC (Hagerstown, MD)\"\n          }\n        }, \n        \"name\": \"District of Columbia\"\n      }, \n      \"FL\": {\n        \"metros\": {\n          \"528\": {\n            \"name\": \"Miami-Ft. Lauderdale, FL\"\n          }, \n          \"530\": {\n            \"name\": \"Tallahassee, FL-Thomasville, GA\"\n          }, \n          \"534\": {\n            \"name\": \"Orlando-Daytona Beach, FL\"\n          }, \n          \"539\": {\n            \"name\": \"Tampa-St Petersburg (Sarasota), FL\"\n          }, \n          \"548\": {\n            \"name\": \"West Palm Beach-Ft. Pierce, FL\"\n          }, \n          \"561\": {\n            \"name\": \"Jacksonville, FL\"\n          }, \n          \"571\": {\n            \"name\": \"Ft. Myers-Naples, FL\"\n          }, \n          \"592\": {\n            \"name\": \"Gainesville, FL\"\n          }, \n          \"656\": {\n            \"name\": \"Panama City, FL\"\n          }, \n          \"686\": {\n            \"name\": \"Mobile, AL-Pensacola, FL\"\n          }\n        }, \n        \"name\": \"Florida\"\n      }, \n      \"GA\": {\n        \"metros\": {\n          \"503\": {\n            \"name\": \"Macon, GA\"\n          }, \n          \"507\": {\n            \"name\": \"Savannah, GA\"\n          }, \n          \"520\": {\n            \"name\": \"Augusta, GA\"\n          }, \n          \"522\": {\n            \"name\": \"Columbus, GA\"\n          }, \n          \"524\": {\n            \"name\": \"Atlanta, GA\"\n          }, \n          \"525\": {\n            \"name\": \"Albany, GA\"\n          }, \n          \"530\": {\n            \"name\": \"Tallahassee, FL-Thomasville, GA\"\n          }\n        }, \n        \"name\": \"Georgia\"\n      }, \n      \"HI\": {\n        \"metros\": {\n          \"744\": {\n            \"name\": \"Honolulu, HI\"\n          }\n        }, \n        \"name\": \"Hawaii\"\n      }, \n      \"IA\": {\n        \"metros\": {\n          \"611\": {\n            \"name\": \"Rochester-Austin, MN-Mason City, IA\"\n          }, \n          \"624\": {\n            \"name\": \"Sioux City, IA\"\n          }, \n          \"631\": {\n            \"name\": \"Ottumwa, IA-Kirksville, MO\"\n          }, \n          \"637\": {\n            \"name\": \"Cedar Rapids-Waterloo-Iowa City, IA\"\n          }, \n          \"679\": {\n            \"name\": \"Des Moines-Ames, IA\"\n          }, \n          \"682\": {\n            \"name\": \"Davenport,IA-Rock Island-Moline,IL\"\n          }, \n          \"717\": {\n            \"name\": \"Quincy, IL-Hannibal, MO-Keokuk, IA\"\n          }\n        }, \n        \"name\": \"Iowa\"\n      }, \n      \"ID\": {\n        \"metros\": {\n          \"757\": {\n            \"name\": \"Boise, ID\"\n          }, \n          \"758\": {\n            \"name\": \"Idaho Falls-Pocatello, ID\"\n          }, \n          \"760\": {\n            \"name\": \"Twin Falls, ID\"\n          }\n        }, \n        \"name\": \"Idaho\"\n      }, \n      \"IL\": {\n        \"metros\": {\n          \"602\": {\n            \"name\": \"Chicago, IL\"\n          }, \n          \"610\": {\n            \"name\": \"Rockford, IL\"\n          }, \n          \"632\": {\n            \"name\": \"Paducah, KY-Harrisburg, IL\"\n          }, \n          \"648\": {\n            \"name\": \"Champaign & Springfield-Decatur,IL\"\n          }, \n          \"675\": {\n            \"name\": \"Peoria-Bloomington, IL\"\n          }, \n          \"682\": {\n            \"name\": \"Davenport,IA-Rock Island-Moline,IL\"\n          }, \n          \"717\": {\n            \"name\": \"Quincy, IL-Hannibal, MO-Keokuk, IA\"\n          }\n        }, \n        \"name\": \"Illinois\"\n      }, \n      \"IN\": {\n        \"metros\": {\n          \"509\": {\n            \"name\": \"Ft. Wayne, IN\"\n          }, \n          \"527\": {\n            \"name\": \"Indianapolis, IN\"\n          }, \n          \"581\": {\n            \"name\": \"Terre Haute, IN\"\n          }, \n          \"582\": {\n            \"name\": \"Lafayette, IN\"\n          }, \n          \"588\": {\n            \"name\": \"South Bend-Elkhart, IN\"\n          }, \n          \"649\": {\n            \"name\": \"Evansville, IN\"\n          }\n        }, \n        \"name\": \"Indiana\"\n      }, \n      \"KS\": {\n        \"metros\": {\n          \"603\": {\n            \"name\": \"Joplin, MO-Pittsburg, KS\"\n          }, \n          \"605\": {\n            \"name\": \"Topeka, KS\"\n          }, \n          \"678\": {\n            \"name\": \"Wichita-Hutchinson, KS\"\n          }\n        }, \n        \"name\": \"Kansas\"\n      }, \n      \"KY\": {\n        \"metros\": {\n          \"529\": {\n            \"name\": \"Louisville, KY\"\n          }, \n          \"541\": {\n            \"name\": \"Lexington, KY\"\n          }, \n          \"632\": {\n            \"name\": \"Paducah, KY-Harrisburg, IL\"\n          }, \n          \"736\": {\n            \"name\": \"Bowling Green, KY\"\n          }\n        }, \n        \"name\": \"Kentucky\"\n      }, \n      \"LA\": {\n        \"metros\": {\n          \"612\": {\n            \"name\": \"Shreveport, LA\"\n          }, \n          \"622\": {\n            \"name\": \"New Orleans, LA\"\n          }, \n          \"628\": {\n            \"name\": \"Monroe, LA-El Dorado, AR\"\n          }, \n          \"642\": {\n            \"name\": \"Lafayette, LA\"\n          }, \n          \"643\": {\n            \"name\": \"Lake Charles, LA\"\n          }, \n          \"644\": {\n            \"name\": \"Alexandria, LA\"\n          }, \n          \"716\": {\n            \"name\": \"Baton Rouge, LA\"\n          }\n        }, \n        \"name\": \"Louisiana\"\n      }, \n      \"MA\": {\n        \"metros\": {\n          \"506\": {\n            \"name\": \"Boston, MA-Manchester, NH\"\n          }, \n          \"521\": {\n            \"name\": \"Providence, RI-New Bedford, MA\"\n          }, \n          \"543\": {\n            \"name\": \"Springfield-Holyoke, MA\"\n          }\n        }, \n        \"name\": \"Massachusetts\"\n      }, \n      \"MD\": {\n        \"metros\": {\n          \"511\": {\n            \"name\": \"Washington, DC (Hagerstown, MD)\"\n          }, \n          \"512\": {\n            \"name\": \"Baltimore, MD\"\n          }, \n          \"576\": {\n            \"name\": \"Salisbury, MD\"\n          }\n        }, \n        \"name\": \"Maryland\"\n      }, \n      \"ME\": {\n        \"metros\": {\n          \"500\": {\n            \"name\": \"Portland-Auburn, ME\"\n          }, \n          \"537\": {\n            \"name\": \"Bangor, ME\"\n          }, \n          \"552\": {\n            \"name\": \"Presque Isle, ME\"\n          }\n        }, \n        \"name\": \"Maine\"\n      }, \n      \"MI\": {\n        \"metros\": {\n          \"505\": {\n            \"name\": \"Detroit, MI\"\n          }, \n          \"513\": {\n            \"name\": \"Flint-Saginaw-Bay City, MI\"\n          }, \n          \"540\": {\n            \"name\": \"Traverse City-Cadillac, MI\"\n          }, \n          \"551\": {\n            \"name\": \"Lansing, MI\"\n          }, \n          \"553\": {\n            \"name\": \"Marquette, MI\"\n          }, \n          \"563\": {\n            \"name\": \"Grand Rapids-Kalamazoo, MI\"\n          }, \n          \"583\": {\n            \"name\": \"Alpena, MI\"\n          }\n        }, \n        \"name\": \"Michigan\"\n      }, \n      \"MN\": {\n        \"metros\": {\n          \"611\": {\n            \"name\": \"Rochester-Austin, MN-Mason City, IA\"\n          }, \n          \"613\": {\n            \"name\": \"Minneapolis-St. Paul, MN\"\n          }, \n          \"676\": {\n            \"name\": \"Duluth, MN-Superior, WI\"\n          }, \n          \"737\": {\n            \"name\": \"Mankato, MN\"\n          }\n        }, \n        \"name\": \"Minnesota\"\n      }, \n      \"MO\": {\n        \"metros\": {\n          \"603\": {\n            \"name\": \"Joplin, MO-Pittsburg, KS\"\n          }, \n          \"604\": {\n            \"name\": \"Columbia-Jefferson City, MO\"\n          }, \n          \"609\": {\n            \"name\": \"St. Louis, MO\"\n          }, \n          \"616\": {\n            \"name\": \"Kansas City, MO\"\n          }, \n          \"619\": {\n            \"name\": \"Springfield, MO\"\n          }, \n          \"631\": {\n            \"name\": \"Ottumwa, IA-Kirksville, MO\"\n          }, \n          \"638\": {\n            \"name\": \"St. Joseph, MO\"\n          }, \n          \"717\": {\n            \"name\": \"Quincy, IL-Hannibal, MO-Keokuk, IA\"\n          }\n        }, \n        \"name\": \"Missouri\"\n      }, \n      \"MS\": {\n        \"metros\": {\n          \"647\": {\n            \"name\": \"Greenwood-Greenville, MS\"\n          }, \n          \"673\": {\n            \"name\": \"Columbus-Tupelo-West Point, MS\"\n          }, \n          \"710\": {\n            \"name\": \"Hattiesburg-Laurel, MS\"\n          }, \n          \"711\": {\n            \"name\": \"Meridian, MS\"\n          }, \n          \"718\": {\n            \"name\": \"Jackson, MS\"\n          }, \n          \"746\": {\n            \"name\": \"Biloxi-Gulfport, MS\"\n          }\n        }, \n        \"name\": \"Mississippi\"\n      }, \n      \"MT\": {\n        \"metros\": {\n          \"754\": {\n            \"name\": \"Butte-Bozeman, MT\"\n          }, \n          \"755\": {\n            \"name\": \"Great Falls, MT\"\n          }, \n          \"756\": {\n            \"name\": \"Billings, MT\"\n          }, \n          \"762\": {\n            \"name\": \"Missoula, MT\"\n          }, \n          \"766\": {\n            \"name\": \"Helena, MT\"\n          }, \n          \"798\": {\n            \"name\": \"Glendive, MT\"\n          }\n        }, \n        \"name\": \"Montana\"\n      }, \n      \"NC\": {\n        \"metros\": {\n          \"517\": {\n            \"name\": \"Charlotte, NC\"\n          }, \n          \"518\": {\n            \"name\": \"Greensboro-Winston Salem, NC\"\n          }, \n          \"545\": {\n            \"name\": \"Greenville-New Bern-Washington, NC\"\n          }, \n          \"550\": {\n            \"name\": \"Wilmington, NC\"\n          }, \n          \"560\": {\n            \"name\": \"Raleigh-Durham (Fayetteville), NC\"\n          }\n        }, \n        \"name\": \"North Carolina\"\n      }, \n      \"ND\": {\n        \"metros\": {\n          \"687\": {\n            \"name\": \"Minot-Bismarck-Dickinson, ND\"\n          }, \n          \"724\": {\n            \"name\": \"Fargo-Valley City, ND\"\n          }\n        }, \n        \"name\": \"North Dakota\"\n      }, \n      \"NE\": {\n        \"metros\": {\n          \"652\": {\n            \"name\": \"Omaha, NE\"\n          }, \n          \"722\": {\n            \"name\": \"Lincoln & Hastings-Kearney, NE\"\n          }, \n          \"740\": {\n            \"name\": \"North Platte, NE\"\n          }, \n          \"759\": {\n            \"name\": \"Cheyenne, WY-Scottsbluff, NE\"\n          }\n        }, \n        \"name\": \"Nebraska\"\n      }, \n      \"NH\": {\n        \"metros\": {\n          \"506\": {\n            \"name\": \"Boston, MA-Manchester, NH\"\n          }\n        }, \n        \"name\": \"New Hampshire\"\n      }, \n      \"NM\": {\n        \"metros\": {\n          \"790\": {\n            \"name\": \"Albuquerque-Santa Fe, NM\"\n          }\n        }, \n        \"name\": \"New Mexico\"\n      }, \n      \"NV\": {\n        \"metros\": {\n          \"811\": {\n            \"name\": \"Reno, NV\"\n          }, \n          \"839\": {\n            \"name\": \"Las Vegas, NV\"\n          }\n        }, \n        \"name\": \"Nevada\"\n      }, \n      \"NY\": {\n        \"metros\": {\n          \"501\": {\n            \"name\": \"New York, NY\"\n          }, \n          \"502\": {\n            \"name\": \"Binghamton, NY\"\n          }, \n          \"514\": {\n            \"name\": \"Buffalo, NY\"\n          }, \n          \"523\": {\n            \"name\": \"Burlington, VT-Plattsburgh, NY\"\n          }, \n          \"526\": {\n            \"name\": \"Utica, NY\"\n          }, \n          \"532\": {\n            \"name\": \"Albany-Schenectady-Troy, NY\"\n          }, \n          \"538\": {\n            \"name\": \"Rochester, NY\"\n          }, \n          \"549\": {\n            \"name\": \"Watertown, NY\"\n          }, \n          \"555\": {\n            \"name\": \"Syracuse, NY\"\n          }, \n          \"565\": {\n            \"name\": \"Elmira, NY\"\n          }\n        }, \n        \"name\": \"New York\"\n      }, \n      \"OH\": {\n        \"metros\": {\n          \"510\": {\n            \"name\": \"Cleveland-Akron (Canton), OH\"\n          }, \n          \"515\": {\n            \"name\": \"Cincinnati, OH\"\n          }, \n          \"535\": {\n            \"name\": \"Columbus, OH\"\n          }, \n          \"536\": {\n            \"name\": \"Youngstown, OH\"\n          }, \n          \"542\": {\n            \"name\": \"Dayton, OH\"\n          }, \n          \"547\": {\n            \"name\": \"Toledo, OH\"\n          }, \n          \"554\": {\n            \"name\": \"Wheeling, WV-Steubenville, OH\"\n          }, \n          \"558\": {\n            \"name\": \"Lima, OH\"\n          }, \n          \"596\": {\n            \"name\": \"Zanesville, OH\"\n          }\n        }, \n        \"name\": \"Ohio\"\n      }, \n      \"OK\": {\n        \"metros\": {\n          \"627\": {\n            \"name\": \"Wichita Falls, TX & Lawton, OK\"\n          }, \n          \"650\": {\n            \"name\": \"Oklahoma City, OK\"\n          }, \n          \"657\": {\n            \"name\": \"Sherman, TX-Ada, OK\"\n          }, \n          \"671\": {\n            \"name\": \"Tulsa, OK\"\n          }\n        }, \n        \"name\": \"Oklahoma\"\n      }, \n      \"OR\": {\n        \"metros\": {\n          \"801\": {\n            \"name\": \"Eugene, OR\"\n          }, \n          \"813\": {\n            \"name\": \"Medford-Klamath Falls, OR\"\n          }, \n          \"820\": {\n            \"name\": \"Portland, OR\"\n          }, \n          \"821\": {\n            \"name\": \"Bend, OR\"\n          }\n        }, \n        \"name\": \"Oregon\"\n      }, \n      \"PA\": {\n        \"metros\": {\n          \"504\": {\n            \"name\": \"Philadelphia, PA\"\n          }, \n          \"508\": {\n            \"name\": \"Pittsburgh, PA\"\n          }, \n          \"516\": {\n            \"name\": \"Erie, PA\"\n          }, \n          \"566\": {\n            \"name\": \"Harrisburg-Lancaster-York, PA\"\n          }, \n          \"574\": {\n            \"name\": \"Johnstown-Altoona, PA\"\n          }, \n          \"577\": {\n            \"name\": \"Wilkes Barre-Scranton, PA\"\n          }\n        }, \n        \"name\": \"Pennsylvania\"\n      }, \n      \"RI\": {\n        \"metros\": {\n          \"521\": {\n            \"name\": \"Providence, RI-New Bedford, MA\"\n          }\n        }, \n        \"name\": \"Rhode Island\"\n      }, \n      \"SC\": {\n        \"metros\": {\n          \"519\": {\n            \"name\": \"Charleston, SC\"\n          }, \n          \"546\": {\n            \"name\": \"Columbia, SC\"\n          }, \n          \"567\": {\n            \"name\": \"Greenville-Spartanburg, SC\"\n          }, \n          \"570\": {\n            \"name\": \"Florence-Myrtle Beach, SC\"\n          }\n        }, \n        \"name\": \"South Carolina\"\n      }, \n      \"SD\": {\n        \"metros\": {\n          \"725\": {\n            \"name\": \"Sioux Falls(Mitchell), SD\"\n          }, \n          \"764\": {\n            \"name\": \"Rapid City, SD\"\n          }\n        }, \n        \"name\": \"South Dakota\"\n      }, \n      \"TN\": {\n        \"metros\": {\n          \"531\": {\n            \"name\": \"Tri-Cities, TN-VA\"\n          }, \n          \"557\": {\n            \"name\": \"Knoxville, TN\"\n          }, \n          \"575\": {\n            \"name\": \"Chattanooga, TN\"\n          }, \n          \"639\": {\n            \"name\": \"Jackson, TN\"\n          }, \n          \"640\": {\n            \"name\": \"Memphis, TN\"\n          }, \n          \"659\": {\n            \"name\": \"Nashville, TN\"\n          }\n        }, \n        \"name\": \"Tennessee\"\n      }, \n      \"TX\": {\n        \"metros\": {\n          \"600\": {\n            \"name\": \"Corpus Christi, TX\"\n          }, \n          \"618\": {\n            \"name\": \"Houston, TX\"\n          }, \n          \"623\": {\n            \"name\": \"Dallas-Ft. Worth, TX\"\n          }, \n          \"625\": {\n            \"name\": \"Waco-Temple-Bryan, TX\"\n          }, \n          \"626\": {\n            \"name\": \"Victoria, TX\"\n          }, \n          \"627\": {\n            \"name\": \"Wichita Falls, TX & Lawton, OK\"\n          }, \n          \"633\": {\n            \"name\": \"Odessa-Midland, TX\"\n          }, \n          \"634\": {\n            \"name\": \"Amarillo, TX\"\n          }, \n          \"635\": {\n            \"name\": \"Austin, TX\"\n          }, \n          \"636\": {\n            \"name\": \"Harlingen-Weslaco-McAllen, TX\"\n          }, \n          \"641\": {\n            \"name\": \"San Antonio, TX\"\n          }, \n          \"651\": {\n            \"name\": \"Lubbock, TX\"\n          }, \n          \"657\": {\n            \"name\": \"Sherman, TX-Ada, OK\"\n          }, \n          \"661\": {\n            \"name\": \"San Angelo, TX\"\n          }, \n          \"662\": {\n            \"name\": \"Abilene-Sweetwater, TX\"\n          }, \n          \"692\": {\n            \"name\": \"Beaumont-Port Arthur, TX\"\n          }, \n          \"709\": {\n            \"name\": \"Tyler-Longview(Nacogdoches), TX\"\n          }, \n          \"749\": {\n            \"name\": \"Laredo, TX\"\n          }, \n          \"765\": {\n            \"name\": \"El Paso, TX\"\n          }\n        }, \n        \"name\": \"Texas\"\n      }, \n      \"UT\": {\n        \"metros\": {\n          \"770\": {\n            \"name\": \"Salt Lake City, UT\"\n          }\n        }, \n        \"name\": \"Utah\"\n      }, \n      \"VA\": {\n        \"metros\": {\n          \"544\": {\n            \"name\": \"Norfolk-Portsmouth-Newport News,VA\"\n          }, \n          \"556\": {\n            \"name\": \"Richmond-Petersburg, VA\"\n          }, \n          \"569\": {\n            \"name\": \"Harrisonburg, VA\"\n          }, \n          \"573\": {\n            \"name\": \"Roanoke-Lynchburg, VA\"\n          }, \n          \"584\": {\n            \"name\": \"Charlottesville, VA\"\n          }\n        }, \n        \"name\": \"Virginia\"\n      }, \n      \"VT\": {\n        \"metros\": {\n          \"523\": {\n            \"name\": \"Burlington, VT-Plattsburgh, NY\"\n          }\n        }, \n        \"name\": \"Vermont\"\n      }, \n      \"WA\": {\n        \"metros\": {\n          \"810\": {\n            \"name\": \"Yakima-Pasco-Richland-Kennewick, WA\"\n          }, \n          \"819\": {\n            \"name\": \"Seattle-Tacoma, WA\"\n          }, \n          \"881\": {\n            \"name\": \"Spokane, WA\"\n          }\n        }, \n        \"name\": \"Washington\"\n      }, \n      \"WI\": {\n        \"metros\": {\n          \"617\": {\n            \"name\": \"Milwaukee, WI\"\n          }, \n          \"658\": {\n            \"name\": \"Green Bay-Appleton, WI\"\n          }, \n          \"669\": {\n            \"name\": \"Madison, WI\"\n          }, \n          \"676\": {\n            \"name\": \"Duluth, MN-Superior, WI\"\n          }, \n          \"702\": {\n            \"name\": \"La Crosse-Eau Claire, WI\"\n          }, \n          \"705\": {\n            \"name\": \"Wausau-Rhinelander, WI\"\n          }\n        }, \n        \"name\": \"Wisconsin\"\n      }, \n      \"WV\": {\n        \"metros\": {\n          \"554\": {\n            \"name\": \"Wheeling, WV-Steubenville, OH\"\n          }, \n          \"559\": {\n            \"name\": \"Bluefield-Beckley-Oak Hill, WV\"\n          }, \n          \"564\": {\n            \"name\": \"Charleston-Huntington, WV\"\n          }, \n          \"597\": {\n            \"name\": \"Parkersburg, WV\"\n          }, \n          \"598\": {\n            \"name\": \"Clarksburg-Weston, WV\"\n          }\n        }, \n        \"name\": \"West Virginia\"\n      }, \n      \"WY\": {\n        \"metros\": {\n          \"759\": {\n            \"name\": \"Cheyenne, WY-Scottsbluff, NE\"\n          }, \n          \"767\": {\n            \"name\": \"Casper-Riverton, WY\"\n          }\n        }, \n        \"name\": \"Wyoming\"\n      }\n    }\n  }, \n  \"UY\": {\n    \"name\": \"Uruguay\"\n  }, \n  \"UZ\": {\n    \"name\": \"Uzbekistan\"\n  }, \n  \"VA\": {\n    \"name\": \"Holy See (Vatican City State)\"\n  }, \n  \"VC\": {\n    \"name\": \"Saint Vincent and the Grenadines\"\n  }, \n  \"VE\": {\n    \"name\": \"Venezuela\"\n  }, \n  \"VG\": {\n    \"name\": \"Virgin Islands\"\n  }, \n  \"VI\": {\n    \"name\": \"Virgin Islands\"\n  }, \n  \"VN\": {\n    \"name\": \"Vietnam\"\n  }, \n  \"VU\": {\n    \"name\": \"Vanuatu\"\n  }, \n  \"WF\": {\n    \"name\": \"Wallis and Futuna\"\n  }, \n  \"WS\": {\n    \"name\": \"Samoa\"\n  }, \n  \"YE\": {\n    \"name\": \"Yemen\"\n  }, \n  \"YT\": {\n    \"name\": \"Mayotte\"\n  }, \n  \"ZA\": {\n    \"name\": \"South Africa\"\n  }, \n  \"ZM\": {\n    \"name\": \"Zambia\"\n  }, \n  \"ZW\": {\n    \"name\": \"Zimbabwe\"\n  }\n}\n"
  },
  {
    "path": "r2/r2/lib/__init__.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\n"
  },
  {
    "path": "r2/r2/lib/amqp.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\"\"\"Methods and classes for inserting/removing from reddit's queues\n\nThere are three main ways of interacting with this module:\n\nadd_item: Adds a single item to a queue*\nhandle_items: For processing multiple items from a queue\nconsume_items: For processing a queue one item at a time\n\n\n* _add_item (the internal function for adding items to amqp that are\n  added using add_item) might block for an arbitrary amount of time\n  while trying to get a connection to amqp.\n\n\"\"\"\nfrom Queue import Queue\nfrom threading import local, Thread\nfrom datetime import datetime\nimport os\nimport sys\nimport time\nimport errno\nimport socket\nimport itertools\nimport cPickle as pickle\n\nfrom amqplib import client_0_8 as amqp\n\ncfg = None\nworker = None\nconnection_manager = None\n\n\ndef initialize(app_globals):\n    global cfg\n    cfg = Config(app_globals)\n    global worker\n    worker = Worker()\n    global connection_manager\n    connection_manager = ConnectionManager()\n\n\nclass Config(object):\n    def __init__(self, g):\n        self.amqp_host = g.amqp_host\n        self.amqp_user = g.amqp_user\n        self.amqp_pass = g.amqp_pass\n        self.amqp_exchange = 'reddit_exchange'\n        self.log = g.log\n        self.amqp_virtual_host = g.amqp_virtual_host\n        self.amqp_logging = g.amqp_logging\n        self.stats = g.stats\n        self.queues = g.queues\n        self.reset_caches = g.reset_caches\n\n\nclass Worker:\n    def __init__(self):\n        self.q = Queue()\n        self.t = Thread(target=self._handle)\n        self.t.setDaemon(True)\n        self.t.start()\n\n    def _handle(self):\n        while True:\n            cfg.reset_caches()\n\n            fn = self.q.get()\n            try:\n                fn()\n                self.q.task_done()\n            except:\n                import traceback\n                print traceback.format_exc()\n\n    def do(self, fn, *a, **kw):\n        fn1 = lambda: fn(*a, **kw)\n        self.q.put(fn1)\n\n    def join(self):\n        self.q.join()\n\n\nclass ConnectionManager(local):\n    # There should be only two threads that ever talk to AMQP: the\n    # worker thread and the foreground thread (whether consuming queue\n    # items or a shell). This class is just a wrapper to make sure\n    # that they get separate connections\n    def __init__(self):\n        self.connection = None\n        self.channel = None\n        self.have_init = False\n\n    def get_connection(self):\n        while not self.connection:\n            try:\n                self.connection = amqp.Connection(\n                    host=cfg.amqp_host,\n                    userid=cfg.amqp_user,\n                    password=cfg.amqp_pass,\n                    virtual_host=cfg.amqp_virtual_host,\n                    insist=False,\n                )\n            except (socket.error, IOError), e:\n                print ('error connecting to amqp %s @ %s (%r)' %\n                       (cfg.amqp_user, cfg.amqp_host, e))\n                time.sleep(1)\n\n        # don't run init_queue until someone actually needs it. this\n        # allows the app server to start and serve most pages if amqp\n        # isn't running\n        if not self.have_init:\n            self.init_queue()\n            self.have_init = True\n\n        return self.connection\n\n    def get_channel(self, reconnect = False):\n        # Periodic (and increasing with uptime) errors appearing when\n        # connection object is still present, but appears to have been\n        # closed.  This checks that the the connection is still open.\n        if self.connection and self.connection.channels is None:\n            cfg.log.error(\n                \"Error: amqp.py, connection object with no available channels.\"\n                \"  Reconnecting...\")\n            self.connection = None\n\n        if not self.connection or reconnect:\n            self.connection = None\n            self.channel = None\n            self.get_connection()\n\n        if not self.channel:\n            self.channel = self.connection.channel()\n\n        return self.channel\n\n    def init_queue(self):\n        chan = self.get_channel()\n        chan.exchange_declare(exchange=cfg.amqp_exchange,\n                              type=\"direct\",\n                              durable=True,\n                              auto_delete=False)\n\n        for queue in cfg.queues:\n            chan.queue_declare(queue=queue.name,\n                               durable=queue.durable,\n                               exclusive=queue.exclusive,\n                               auto_delete=queue.auto_delete)\n\n        for queue, key in cfg.queues.bindings:\n            chan.queue_bind(routing_key=key,\n                            queue=queue,\n                            exchange=cfg.amqp_exchange)\n\n\n\nDELIVERY_TRANSIENT = 1\nDELIVERY_DURABLE = 2\n\ndef _add_item(routing_key, body, message_id = None,\n              delivery_mode=DELIVERY_DURABLE, headers=None,\n              exchange=None, send_stats=True):\n    \"\"\"adds an item onto a queue. If the connection to amqp is lost it\n    will try to reconnect and then call itself again.\"\"\"\n    if not cfg.amqp_host:\n        cfg.log.error(\"Ignoring amqp message %r to %r\" % (body, routing_key))\n        return\n    if not exchange:\n        exchange = cfg.amqp_exchange\n\n    chan = connection_manager.get_channel()\n    msg = amqp.Message(body,\n                       timestamp = datetime.now(),\n                       delivery_mode = delivery_mode)\n    if message_id:\n        msg.properties['message_id'] = message_id\n\n    if headers:\n        msg.properties[\"application_headers\"] = headers\n\n    event_name = 'amqp.%s' % routing_key\n    try:\n        chan.basic_publish(msg,\n                           exchange=exchange,\n                           routing_key = routing_key)\n    except Exception as e:\n        if send_stats:\n            cfg.stats.event_count(event_name, 'enqueue_failed')\n\n        if e.errno == errno.EPIPE:\n            connection_manager.get_channel(True)\n            add_item(routing_key, body, message_id)\n        else:\n            raise\n    else:\n        if send_stats:\n            cfg.stats.event_count(event_name, 'enqueue')\n\ndef add_item(routing_key, body, message_id=None,\n             delivery_mode=DELIVERY_DURABLE, headers=None,\n             exchange=None, send_stats=True):\n    if cfg.amqp_host and cfg.amqp_logging:\n        cfg.log.debug(\"amqp: adding item %r to %r\", body, routing_key)\n    if exchange is None:\n        exchange = cfg.amqp_exchange\n\n    worker.do(_add_item, routing_key, body, message_id = message_id,\n              delivery_mode=delivery_mode, headers=headers, exchange=exchange,\n              send_stats=send_stats)\n\ndef add_kw(routing_key, **kw):\n    add_item(routing_key, pickle.dumps(kw))\n\ndef consume_items(queue, callback, verbose=True):\n    \"\"\"A lighter-weight version of handle_items that uses AMQP's\n       basic.consume instead of basic.get. Callback is only passed a\n       single items at a time. This is more efficient than\n       handle_items when the queue is likely to be occasionally empty\n       or if batching the received messages is not necessary.\"\"\"\n    from pylons import tmpl_context as c\n\n    chan = connection_manager.get_channel()\n\n    # configure the amount of data rabbit will send down to our buffer before\n    # we're ready for it (to reduce network latency). by default, it will send\n    # as much as our buffers will allow.\n    chan.basic_qos(\n        # size in bytes of prefetch window. zero indicates no preference.\n        prefetch_size=0,\n        # maximum number of prefetched messages.\n        prefetch_count=10,\n        # if global, applies to the whole connection, else just this channel.\n        a_global=False\n    )\n\n    def _callback(msg):\n        if verbose:\n            count_str = ''\n            if 'message_count' in msg.delivery_info:\n                # the count from the last message, if the count is\n                # available\n                count_str = '(%d remaining)' % msg.delivery_info['message_count']\n\n            print \"%s: 1 item %s\" % (queue, count_str)\n\n        cfg.reset_caches()\n        c.use_write_db = {}\n\n        ret = callback(msg)\n        msg.channel.basic_ack(msg.delivery_tag)\n        sys.stdout.flush()\n        return ret\n\n    chan.basic_consume(queue=queue, callback=_callback)\n\n    try:\n        while chan.callbacks:\n            try:\n                chan.wait()\n            except KeyboardInterrupt:\n                break\n    finally:\n        worker.join()\n        if chan.is_open:\n            chan.close()\n\ndef handle_items(queue, callback, ack=True, limit=1, min_size=0,\n                 drain=False, verbose=True, sleep_time=1):\n    \"\"\"Call callback() on every item in a particular queue. If the\n    connection to the queue is lost, it will die. Intended to be\n    used as a long-running process.\"\"\"\n    if limit < min_size:\n        raise ValueError(\"min_size must be less than limit\")\n    from pylons import tmpl_context as c\n\n    chan = connection_manager.get_channel()\n    countdown = None\n\n    while True:\n        # NB: None != 0, so we don't need an \"is not None\" check here\n        if countdown == 0:\n            break\n\n        msg = chan.basic_get(queue)\n        if not msg and drain:\n            return\n        elif not msg:\n            time.sleep(sleep_time)\n            continue\n\n        if countdown is None and drain and 'message_count' in msg.delivery_info:\n            countdown = 1 + msg.delivery_info['message_count']\n\n        cfg.reset_caches()\n        c.use_write_db = {}\n\n        items = [msg]\n\n        while countdown != 0:\n            if countdown is not None:\n                countdown -= 1\n            if len(items) >= limit:\n                break # the innermost loop only\n            msg = chan.basic_get(queue)\n            if msg is None:\n                if len(items) < min_size:\n                    time.sleep(sleep_time)\n                else:\n                    break\n            else:\n                items.append(msg)\n\n        try:\n            count_str = ''\n            if 'message_count' in items[-1].delivery_info:\n                # the count from the last message, if the count is\n                # available\n                count_str = '(%d remaining)' % items[-1].delivery_info['message_count']\n            if verbose:\n                print \"%s: %d items %s\" % (queue, len(items), count_str)\n            callback(items, chan)\n\n            if ack:\n                # ack *all* outstanding messages\n                chan.basic_ack(0, multiple=True)\n\n            # flush any log messages printed by the callback\n            sys.stdout.flush()\n        except:\n            for item in items:\n                # explicitly reject the items that we've not processed\n                chan.basic_reject(item.delivery_tag, requeue = True)\n            raise\n\n\ndef empty_queue(queue):\n    \"\"\"debug function to completely erase the contents of a queue\"\"\"\n    chan = connection_manager.get_channel()\n    chan.queue_purge(queue)\n\n\ndef black_hole(queue):\n    \"\"\"continually empty out a queue as new items are created\"\"\"\n    def _ignore(msg):\n        print 'Ignoring msg: %r' % msg.body\n\n    consume_items(queue, _ignore)\n\ndef dedup_queue(queue, rk = None, limit=None,\n                delivery_mode = DELIVERY_DURABLE):\n    \"\"\"Hackily try to reduce the size of a queue by removing duplicate\n       messages. The consumers of the target queue must consider\n       identical messages to be idempotent. Preserves only message\n       bodies\"\"\"\n    chan = connection_manager.get_channel()\n\n    if rk is None:\n        rk = queue\n\n    bodies = set()\n\n    while True:\n        msg = chan.basic_get(queue)\n\n        if msg is None:\n            break\n\n        if msg.body not in bodies:\n            bodies.add(msg.body)\n\n        if limit is None:\n            limit = msg.delivery_info.get('message_count')\n            if limit is None:\n                default_max = 100*1000\n                print (\"Message count was unavailable, defaulting to %d\"\n                       % (default_max,))\n                limit = default_max\n            else:\n                print \"Grabbing %d messages\" % (limit,)\n        else:\n            limit -= 1\n            if limit <= 0:\n                break\n            elif limit % 1000 == 0:\n                print limit\n\n    print \"Grabbed %d unique bodies\" % (len(bodies),)\n\n    if bodies:\n        for body in bodies:\n            _add_item(rk, body, delivery_mode = delivery_mode)\n\n        worker.join()\n\n        chan.basic_ack(0, multiple=True)\n"
  },
  {
    "path": "r2/r2/lib/app_globals.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom datetime import datetime\nfrom urlparse import urlparse\n\nimport base64\nimport ConfigParser\nimport locale\nimport json\nimport logging\nimport os\nimport re\nimport signal\nimport site\nimport socket\nimport subprocess\nimport sys\n\nfrom sqlalchemy import engine, event\nfrom baseplate import Baseplate, config as baseplate_config\nfrom baseplate.thrift_pool import ThriftConnectionPool\nfrom baseplate.context.thrift import ThriftContextFactory\nfrom baseplate.server import einhorn\n\nimport pkg_resources\nimport pytz\n\nfrom r2.config import queues\nimport r2.lib.amqp\nfrom r2.lib.baseplate_integration import R2BaseplateObserver\nfrom r2.lib.cache import (\n    CacheChain,\n    CL_ONE,\n    CL_QUORUM,\n    CMemcache,\n    HardCache,\n    HardcacheChain,\n    LocalCache,\n    Mcrouter,\n    MemcacheChain,\n    Permacache,\n    SelfEmptyingCache,\n    StaleCacheChain,\n    TransitionalCache,\n)\nfrom r2.lib.configparse import ConfigValue, ConfigValueParser\nfrom r2.lib.contrib import ipaddress\nfrom r2.lib.contrib.activity_thrift import ActivityService\nfrom r2.lib.contrib.activity_thrift.ttypes import ActivityInfo\nfrom r2.lib.eventcollector import EventQueue\nfrom r2.lib.lock import make_lock_factory\nfrom r2.lib.manager import db_manager\nfrom r2.lib.plugin import PluginLoader\nfrom r2.lib.providers import select_provider\nfrom r2.lib.stats import (\n    CacheStats,\n    StaleCacheStats,\n    Stats,\n    StatsCollectingConnectionPool,\n)\nfrom r2.lib.translation import get_active_langs, I18N_PATH\nfrom r2.lib.utils import config_gold_price, thread_dump\nfrom r2.lib.zookeeper import (\n    connect_to_zookeeper,\n    LiveConfig,\n    IPNetworkLiveList,\n)\n\n\nLIVE_CONFIG_NODE = \"/config/live\"\n\nSEARCH_SYNTAXES = {\n        'cloudsearch': ('cloudsearch', 'lucene', 'plain'),\n        'solr': ('solr', 'plain'),\n        }\n\nSECRETS_NODE = \"/config/secrets\"\n\n\ndef extract_live_config(config, plugins):\n    \"\"\"Gets live config out of INI file and validates it according to spec.\"\"\"\n\n    # ConfigParser will include every value in DEFAULT (which paste abuses)\n    # if we do this the way we're supposed to. sorry for the horribleness.\n    live_config = config._sections[\"live_config\"].copy()\n    del live_config[\"__name__\"]  # magic value used by ConfigParser\n\n    # parse the config data including specs from plugins\n    parsed = ConfigValueParser(live_config)\n    parsed.add_spec(Globals.live_config_spec)\n    for plugin in plugins:\n        parsed.add_spec(plugin.live_config)\n\n    return parsed\n\n\ndef _decode_secrets(secrets):\n    return {key: base64.b64decode(value) for key, value in secrets.iteritems()}\n\n\ndef extract_secrets(config):\n    # similarly to the live_config one above, if we just did\n    # .options(\"secrets\") we'd get back all the junk from DEFAULT too. bleh.\n    secrets = config._sections[\"secrets\"].copy()\n    del secrets[\"__name__\"]  # magic value used by ConfigParser\n    return _decode_secrets(secrets)\n\n\ndef fetch_secrets(zk_client):\n    node_data = zk_client.get(SECRETS_NODE)[0]\n    secrets = json.loads(node_data)\n    return _decode_secrets(secrets)\n\n\nPERMISSIONS = {\n    \"admin\": \"admin\",\n    \"sponsor\": \"sponsor\",\n    \"employee\": \"employee\",\n}\n\n\nclass PermissionFilteredEmployeeList(object):\n    def __init__(self, config, type):\n        self.config = config\n        self.type = type\n\n    def __iter__(self):\n        return (username\n                for username, permission in self.config[\"employees\"].iteritems()\n                if permission == self.type)\n\n    def __getitem__(self, key):\n        return list(self)[key]\n\n    def __contains__(self, item):\n        # since we do these permission checks off usernames, make it case\n        # insensitive to relax config file editing pains (safe because\n        # Account._by_name is case-insensitive)\n        return any(item.lower() == username.lower() for username in self)\n\n    def __repr__(self):\n        return \"<PermissionFilteredEmployeeList %r>\" % (list(self),)\n\n\nSHUTDOWN_CALLBACKS = []\ndef on_app_shutdown(arbiter, worker):\n    for callback in SHUTDOWN_CALLBACKS:\n        callback()\n\n\nclass Globals(object):\n    spec = {\n\n        ConfigValue.int: [\n            'db_pool_size',\n            'db_pool_overflow_size',\n            'commentpane_cache_time',\n            'num_mc_clients',\n            'MAX_CAMPAIGNS_PER_LINK',\n            'MIN_DOWN_LINK',\n            'MIN_UP_KARMA',\n            'MIN_DOWN_KARMA',\n            'MIN_RATE_LIMIT_KARMA',\n            'MIN_RATE_LIMIT_COMMENT_KARMA',\n            'HOT_PAGE_AGE',\n            'ADMIN_COOKIE_TTL',\n            'ADMIN_COOKIE_MAX_IDLE',\n            'OTP_COOKIE_TTL',\n            'hsts_max_age',\n            'num_comments',\n            'max_comments',\n            'max_comments_gold',\n            'max_comment_parent_walk',\n            'max_sr_images',\n            'num_serendipity',\n            'comment_visits_period',\n            'butler_max_mentions',\n            'min_membership_create_community',\n            'bcrypt_work_factor',\n            'cassandra_pool_size',\n            'sr_banned_quota',\n            'sr_muted_quota',\n            'sr_wikibanned_quota',\n            'sr_wikicontributor_quota',\n            'sr_moderator_invite_quota',\n            'sr_contributor_quota',\n            'sr_quota_time',\n            'sr_invite_limit',\n            'thumbnail_hidpi_scaling',\n            'wiki_keep_recent_days',\n            'wiki_max_page_length_bytes',\n            'wiki_max_page_name_length',\n            'wiki_max_page_separators',\n            'RL_RESET_MINUTES',\n            'RL_OAUTH_RESET_MINUTES',\n            'comment_karma_display_floor',\n            'link_karma_display_floor',\n            'mobile_auth_gild_time',\n            'default_total_budget_pennies',\n            'min_total_budget_pennies',\n            'max_total_budget_pennies',\n            'default_bid_pennies',\n            'min_bid_pennies',\n            'max_bid_pennies',\n            'frequency_cap_min',\n            'frequency_cap_default',\n            'eu_cookie_max_attempts',\n        ],\n\n        ConfigValue.float: [\n            'statsd_sample_rate',\n            'querycache_prune_chance',\n            'RL_AVG_REQ_PER_SEC',\n            'RL_OAUTH_AVG_REQ_PER_SEC',\n            'RL_LOGIN_AVG_PER_SEC',\n            'RL_LOGIN_IP_AVG_PER_SEC',\n            'RL_SHARE_AVG_PER_SEC',\n            'tracing_sample_rate',\n        ],\n\n        ConfigValue.bool: [\n            'debug',\n            'log_start',\n            'sqlprinting',\n            'template_debug',\n            'reload_templates',\n            'uncompressedJS',\n            'css_killswitch',\n            'db_create_tables',\n            'disallow_db_writes',\n            'disable_ratelimit',\n            'amqp_logging',\n            'read_only_mode',\n            'disable_wiki',\n            'heavy_load_mode',\n            'disable_captcha',\n            'disable_ads',\n            'disable_require_admin_otp',\n            'trust_local_proxies',\n            'shard_commentstree_queues',\n            'shard_author_query_queues',\n            'shard_subreddit_query_queues',\n            'shard_domain_query_queues',\n            'authnet_validate',\n            'ENFORCE_RATELIMIT',\n            'RL_SITEWIDE_ENABLED',\n            'RL_OAUTH_SITEWIDE_ENABLED',\n            'enable_loggedout_experiments',\n        ],\n\n        ConfigValue.tuple: [\n            'plugins',\n            'stalecaches',\n            'lockcaches',\n            'permacache_memcaches',\n            'cassandra_seeds',\n            'automatic_reddits',\n            'hardcache_categories',\n            'case_sensitive_domains',\n            'known_image_domains',\n            'reserved_subdomains',\n            'offsite_subdomains',\n            'TRAFFIC_LOG_HOSTS',\n            'exempt_login_user_agents',\n            'autoexpand_media_types',\n            'media_preview_domain_whitelist',\n            'multi_icons',\n            'hide_subscribers_srs',\n            'mcrouter_addr',\n        ],\n\n        ConfigValue.tuple_of(ConfigValue.int): [\n            'thumbnail_size',\n            'preview_image_max_size',\n            'preview_image_min_size',\n            'mobile_ad_image_size',\n        ],\n\n        ConfigValue.tuple_of(ConfigValue.float): [\n            'ios_versions',\n            'android_versions',\n        ],\n\n        ConfigValue.dict(ConfigValue.str, ConfigValue.int): [\n            'user_agent_ratelimit_regexes',\n        ],\n\n        ConfigValue.str: [\n            'wiki_page_registration_info',\n            'wiki_page_privacy_policy',\n            'wiki_page_user_agreement',\n            'wiki_page_gold_bottlecaps',\n            'fraud_email',\n            'feedback_email',\n            'share_reply',\n            'community_email',\n            'smtp_server',\n            'events_collector_url',\n            'events_collector_test_url',\n            'search_provider',\n        ],\n\n        ConfigValue.choice(ONE=CL_ONE, QUORUM=CL_QUORUM): [\n             'cassandra_rcl',\n             'cassandra_wcl',\n        ],\n\n        ConfigValue.choice(zookeeper=\"zookeeper\", config=\"config\"): [\n            \"liveconfig_source\",\n            \"secrets_source\",\n        ],\n\n        ConfigValue.timeinterval: [\n            'ARCHIVE_AGE',\n            \"vote_queue_grace_period\",\n        ],\n\n        config_gold_price: [\n            'gold_month_price',\n            'gold_year_price',\n            'cpm_selfserve',\n            'cpm_selfserve_geotarget_metro',\n            'cpm_selfserve_geotarget_country',\n            'cpm_selfserve_collection',\n        ],\n\n        ConfigValue.baseplate(baseplate_config.Optional(baseplate_config.Endpoint)): [\n            \"activity_endpoint\",\n            \"tracing_endpoint\",\n        ],\n\n        ConfigValue.dict(ConfigValue.str, ConfigValue.str): [\n            'emr_traffic_tags',\n        ],\n    }\n\n    live_config_spec = {\n        ConfigValue.bool: [\n            'frontend_logging',\n            'mobile_gild_first_login',\n            'precomputed_comment_suggested_sort',\n        ],\n        ConfigValue.int: [\n            'captcha_exempt_comment_karma',\n            'captcha_exempt_link_karma',\n            'create_sr_account_age_days',\n            'create_sr_comment_karma',\n            'create_sr_link_karma',\n            'cflag_min_votes',\n            'ads_popularity_threshold',\n            'precomputed_comment_sort_min_comments',\n            'comment_vote_update_threshold',\n            'comment_vote_update_period',\n        ],\n        ConfigValue.float: [\n            'cflag_lower_bound',\n            'cflag_upper_bound',\n            'spotlight_interest_sub_p',\n            'spotlight_interest_nosub_p',\n            'gold_revenue_goal',\n            'invalid_key_sample_rate',\n            'events_collector_vote_sample_rate',\n            'events_collector_poison_sample_rate',\n            'events_collector_mod_sample_rate',\n            'events_collector_quarantine_sample_rate',\n            'events_collector_modmail_sample_rate',\n            'events_collector_report_sample_rate',\n            'events_collector_submit_sample_rate',\n            'events_collector_comment_sample_rate',\n            'events_collector_use_gzip_chance',\n            'https_cert_testing_probability',\n        ],\n        ConfigValue.tuple: [\n            'fastlane_links',\n            'listing_chooser_sample_multis',\n            'discovery_srs',\n            'proxy_gilding_accounts',\n            'mweb_blacklist_expressions',\n            'global_loid_experiments',\n            'precomputed_comment_sorts',\n            'mailgun_domains',\n        ],\n        ConfigValue.str: [\n            'listing_chooser_gold_multi',\n            'listing_chooser_explore_sr',\n        ],\n        ConfigValue.messages: [\n            'welcomebar_messages',\n            'sidebar_message',\n            'gold_sidebar_message',\n        ],\n        ConfigValue.dict(ConfigValue.str, ConfigValue.int): [\n            'ticket_groups',\n            'ticket_user_fields', \n        ],\n        ConfigValue.dict(ConfigValue.str, ConfigValue.float): [\n            'pennies_per_server_second',\n        ],\n        ConfigValue.dict(ConfigValue.str, ConfigValue.str): [\n            'employee_approved_clients',\n            'modmail_forwarding_email',\n            'modmail_account_map',\n        ],\n        ConfigValue.dict(ConfigValue.str, ConfigValue.choice(**PERMISSIONS)): [\n            'employees',\n        ],\n    }\n\n    def __init__(self, config, global_conf, app_conf, paths, **extra):\n        \"\"\"\n        Globals acts as a container for objects available throughout\n        the life of the application.\n\n        One instance of Globals is created by Pylons during\n        application initialization and is available during requests\n        via the 'g' variable.\n\n        ``config``\n            The PylonsConfig object passed in from ``config/environment.py``\n\n        ``global_conf``\n            The same variable used throughout ``config/middleware.py``\n            namely, the variables from the ``[DEFAULT]`` section of the\n            configuration file.\n\n        ``app_conf``\n            The same ``kw`` dictionary used throughout\n            ``config/middleware.py`` namely, the variables from the\n            section in the config file for your application.\n\n        ``extra``\n            The configuration returned from ``load_config`` in \n            ``config/middleware.py`` which may be of use in the setup of\n            your global variables.\n\n        \"\"\"\n\n        global_conf.setdefault(\"debug\", False)\n\n        # reloading site ensures that we have a fresh sys.path to build our\n        # working set off of. this means that forked worker processes won't get\n        # the sys.path that was current when the master process was spawned\n        # meaning that new plugins will be picked up on regular app reload\n        # rather than having to restart the master process as well.\n        reload(site)\n        self.pkg_resources_working_set = pkg_resources.WorkingSet()\n\n        self.config = ConfigValueParser(global_conf)\n        self.config.add_spec(self.spec)\n        self.plugins = PluginLoader(self.pkg_resources_working_set,\n                                    self.config.get(\"plugins\", []))\n\n        self.stats = Stats(self.config.get('statsd_addr'),\n                           self.config.get('statsd_sample_rate'))\n        self.startup_timer = self.stats.get_timer(\"app_startup\")\n        self.startup_timer.start()\n\n        self.baseplate = Baseplate()\n        self.baseplate.configure_logging()\n        self.baseplate.register(R2BaseplateObserver())\n        self.baseplate.configure_tracing(\n            \"r2\",\n            tracing_endpoint=self.config.get(\"tracing_endpoint\"),\n            sample_rate=self.config.get(\"tracing_sample_rate\"),\n        )\n\n        self.paths = paths\n\n        self.running_as_script = global_conf.get('running_as_script', False)\n        \n        # turn on for language support\n        self.lang = getattr(self, 'site_lang', 'en')\n        self.languages, self.lang_name = get_active_langs(\n            config, default_lang=self.lang)\n\n        all_languages = self.lang_name.keys()\n        all_languages.sort()\n        self.all_languages = all_languages\n        \n        # set default time zone if one is not set\n        tz = global_conf.get('timezone', 'UTC')\n        self.tz = pytz.timezone(tz)\n        \n        dtz = global_conf.get('display_timezone', tz)\n        self.display_tz = pytz.timezone(dtz)\n\n        self.startup_timer.intermediate(\"init\")\n\n    def __getattr__(self, name):\n        if not name.startswith('_') and name in self.config:\n            return self.config[name]\n        else:\n            raise AttributeError(\"g has no attr %r\" % name)\n\n    def setup(self):\n        self.env = ''\n        if (\n            # handle direct invocation of \"nosetests\"\n            \"test\" in sys.argv[0] or\n            # handle \"setup.py test\" and all permutations thereof.\n            \"setup.py\" in sys.argv[0] and \"test\" in sys.argv[1:]\n        ):\n            self.env = \"unit_test\"\n\n        self.queues = queues.declare_queues(self)\n\n        self.extension_subdomains = dict(\n            simple=\"mobile\",\n            i=\"compact\",\n            api=\"api\",\n            rss=\"rss\",\n            xml=\"xml\",\n            json=\"json\",\n        )\n\n        ################# PROVIDERS\n        self.auth_provider = select_provider(\n            self.config,\n            self.pkg_resources_working_set,\n            \"r2.provider.auth\",\n            self.authentication_provider,\n        )\n        self.media_provider = select_provider(\n            self.config,\n            self.pkg_resources_working_set,\n            \"r2.provider.media\",\n            self.media_provider,\n        )\n        self.cdn_provider = select_provider(\n            self.config,\n            self.pkg_resources_working_set,\n            \"r2.provider.cdn\",\n            self.cdn_provider,\n        )\n        self.ticket_provider = select_provider(\n            self.config,\n            self.pkg_resources_working_set,\n            \"r2.provider.support\",\n            # TODO: fix this later, it refuses to pick up \n            # g.config['ticket_provider'] value, so hardcoding for now.\n            # really, the next uncommented line should be:\n            #self.ticket_provider,\n            # instead of:\n            \"zendesk\",\n        )\n        self.image_resizing_provider = select_provider(\n            self.config,\n            self.pkg_resources_working_set,\n            \"r2.provider.image_resizing\",\n            self.image_resizing_provider,\n        )\n        self.email_provider = select_provider(\n            self.config,\n            self.pkg_resources_working_set,\n            \"r2.provider.email\",\n            self.email_provider,\n        )\n        self.startup_timer.intermediate(\"providers\")\n\n        ################# CONFIGURATION\n        # AMQP is required\n        if not self.amqp_host:\n            raise ValueError(\"amqp_host not set in the .ini\")\n\n        if not self.cassandra_seeds:\n            raise ValueError(\"cassandra_seeds not set in the .ini\")\n\n        # heavy load mode is read only mode with a different infobar\n        if self.heavy_load_mode:\n            self.read_only_mode = True\n\n        origin_prefix = self.domain_prefix + \".\" if self.domain_prefix else \"\"\n        self.origin = self.default_scheme + \"://\" + origin_prefix + self.domain\n\n        self.trusted_domains = set([self.domain])\n        if self.https_endpoint:\n            https_url = urlparse(self.https_endpoint)\n            self.trusted_domains.add(https_url.hostname)\n\n        # load the unique hashed names of files under static\n        static_files = os.path.join(self.paths.get('static_files'), 'static')\n        names_file_path = os.path.join(static_files, 'names.json')\n        if os.path.exists(names_file_path):\n            with open(names_file_path) as handle:\n                self.static_names = json.load(handle)\n        else:\n            self.static_names = {}\n\n        # make python warnings go through the logging system\n        logging.captureWarnings(capture=True)\n\n        log = logging.getLogger('reddit')\n\n        # when we're a script (paster run) just set up super simple logging\n        if self.running_as_script:\n            log.setLevel(logging.INFO)\n            log.addHandler(logging.StreamHandler())\n\n        # if in debug mode, override the logging level to DEBUG\n        if self.debug:\n            log.setLevel(logging.DEBUG)\n\n        # attempt to figure out which pool we're in and add that to the\n        # LogRecords.\n        try:\n            with open(\"/etc/ec2_asg\", \"r\") as f:\n                pool = f.read().strip()\n            # clean up the pool name since we're putting stuff after \"-\"\n            pool = pool.partition(\"-\")[0]\n        except IOError:\n            pool = \"reddit-app\"\n        self.log = logging.LoggerAdapter(log, {\"pool\": pool})\n\n        # set locations\n        locations = pkg_resources.resource_stream(__name__,\n                                                  \"../data/locations.json\")\n        self.locations = json.loads(locations.read())\n\n        if not self.media_domain:\n            self.media_domain = self.domain\n        if self.media_domain == self.domain:\n            print >> sys.stderr, (\"Warning: g.media_domain == g.domain. \" +\n                   \"This may give untrusted content access to user cookies\")\n        if self.oauth_domain == self.domain:\n            print >> sys.stderr, (\"Warning: g.oauth_domain == g.domain. \"\n                    \"CORS requests to g.domain will be allowed\")\n\n        for arg in sys.argv:\n            tokens = arg.split(\"=\")\n            if len(tokens) == 2:\n                k, v = tokens\n                self.log.debug(\"Overriding g.%s to %s\" % (k, v))\n                setattr(self, k, v)\n\n        self.reddit_host = socket.gethostname()\n        self.reddit_pid  = os.getpid()\n\n        if hasattr(signal, 'SIGUSR1'):\n            # not all platforms have user signals\n            signal.signal(signal.SIGUSR1, thread_dump)\n\n        locale.setlocale(locale.LC_ALL, self.locale)\n\n        # Pre-calculate ratelimit values\n        self.RL_RESET_SECONDS = self.config[\"RL_RESET_MINUTES\"] * 60\n        self.RL_MAX_REQS = int(self.config[\"RL_AVG_REQ_PER_SEC\"] *\n                                      self.RL_RESET_SECONDS)\n\n        self.RL_OAUTH_RESET_SECONDS = self.config[\"RL_OAUTH_RESET_MINUTES\"] * 60\n        self.RL_OAUTH_MAX_REQS = int(self.config[\"RL_OAUTH_AVG_REQ_PER_SEC\"] *\n                                     self.RL_OAUTH_RESET_SECONDS)\n\n        self.RL_LOGIN_MAX_REQS = int(self.config[\"RL_LOGIN_AVG_PER_SEC\"] *\n                                     self.RL_RESET_SECONDS)\n        self.RL_LOGIN_IP_MAX_REQS = int(self.config[\"RL_LOGIN_IP_AVG_PER_SEC\"] *\n                                        self.RL_RESET_SECONDS)\n        self.RL_SHARE_MAX_REQS = int(self.config[\"RL_SHARE_AVG_PER_SEC\"] *\n                                     self.RL_RESET_SECONDS)\n\n        # Compile ratelimit regexs\n        user_agent_ratelimit_regexes = {}\n        for agent_re, limit in self.user_agent_ratelimit_regexes.iteritems():\n            user_agent_ratelimit_regexes[re.compile(agent_re)] = limit\n        self.user_agent_ratelimit_regexes = user_agent_ratelimit_regexes\n\n        self.startup_timer.intermediate(\"configuration\")\n\n        ################# ZOOKEEPER\n        zk_hosts = self.config[\"zookeeper_connection_string\"]\n        zk_username = self.config[\"zookeeper_username\"]\n        zk_password = self.config[\"zookeeper_password\"]\n        self.zookeeper = connect_to_zookeeper(zk_hosts, (zk_username,\n                                                         zk_password))\n\n        self.throttles = IPNetworkLiveList(\n            self.zookeeper,\n            root=\"/throttles\",\n            reduced_data_node=\"/throttles_reduced\",\n        )\n\n        parser = ConfigParser.RawConfigParser()\n        parser.optionxform = str\n        parser.read([self.config[\"__file__\"]])\n\n        if self.config[\"liveconfig_source\"] == \"zookeeper\":\n            self.live_config = LiveConfig(self.zookeeper, LIVE_CONFIG_NODE)\n        else:\n            self.live_config = extract_live_config(parser, self.plugins)\n\n        if self.config[\"secrets_source\"] == \"zookeeper\":\n            self.secrets = fetch_secrets(self.zookeeper)\n        else:\n            self.secrets = extract_secrets(parser)\n\n        ################# PRIVILEGED USERS\n        self.admins = PermissionFilteredEmployeeList(\n            self.live_config, type=\"admin\")\n        self.sponsors = PermissionFilteredEmployeeList(\n            self.live_config, type=\"sponsor\")\n        self.employees = PermissionFilteredEmployeeList(\n            self.live_config, type=\"employee\")\n\n        # Store which OAuth clients employees may use, the keys are just for\n        # readability.\n        self.employee_approved_clients = \\\n            self.live_config[\"employee_approved_clients\"].values()\n\n        self.startup_timer.intermediate(\"zookeeper\")\n\n        ################# MEMCACHE\n        num_mc_clients = self.num_mc_clients\n\n        # a smaller pool of caches used only for distributed locks.\n        self.lock_cache = CMemcache(\n            \"lock\",\n            self.lockcaches,\n            num_clients=num_mc_clients,\n        )\n        self.make_lock = make_lock_factory(self.lock_cache, self.stats)\n\n        # memcaches used in front of the permacache CF in cassandra.\n        # XXX: this is a legacy thing; permacache was made when C* didn't have\n        # a row cache.\n        permacache_memcaches = CMemcache(\n            \"perma\",\n            self.permacache_memcaches,\n            min_compress_len=1400,\n            num_clients=num_mc_clients,\n        )\n\n        # the stalecache is a memcached local to the current app server used\n        # for data that's frequently fetched but doesn't need to be fresh.\n        if self.stalecaches:\n            stalecaches = CMemcache(\n                \"stale\",\n                self.stalecaches,\n                num_clients=num_mc_clients,\n            )\n        else:\n            stalecaches = None\n\n        self.startup_timer.intermediate(\"memcache\")\n\n        ################# MCROUTER\n        self.mcrouter = Mcrouter(\n            \"mcrouter\",\n            self.mcrouter_addr,\n            min_compress_len=1400,\n            num_clients=num_mc_clients,\n        )\n\n        ################# THRIFT-BASED SERVICES\n        activity_endpoint = self.config.get(\"activity_endpoint\")\n        if activity_endpoint:\n            # make ActivityInfo objects rendercache-key friendly\n            # TODO: figure out a more general solution for this if\n            # we need to do this for other thrift-generated objects\n            ActivityInfo.cache_key = lambda self, style: repr(self)\n\n            activity_pool = ThriftConnectionPool(activity_endpoint, timeout=0.1)\n            self.baseplate.add_to_context(\"activity_service\",\n                ThriftContextFactory(activity_pool, ActivityService.Client))\n\n        self.startup_timer.intermediate(\"thrift\")\n\n        ################# CASSANDRA\n        keyspace = \"reddit\"\n        self.cassandra_pools = {\n            \"main\":\n                StatsCollectingConnectionPool(\n                    keyspace,\n                    stats=self.stats,\n                    logging_name=\"main\",\n                    server_list=self.cassandra_seeds,\n                    pool_size=self.cassandra_pool_size,\n                    timeout=4,\n                    max_retries=3,\n                    prefill=False\n                ),\n        }\n\n        permacache_cf = Permacache._setup_column_family(\n            'permacache',\n            self.cassandra_pools[self.cassandra_default_pool],\n        )\n\n        self.startup_timer.intermediate(\"cassandra\")\n\n        ################# POSTGRES\n        self.dbm = self.load_db_params()\n        self.startup_timer.intermediate(\"postgres\")\n\n        ################# CHAINS\n        # initialize caches. Any cache-chains built here must be added\n        # to cache_chains (closed around by reset_caches) so that they\n        # can properly reset their local components\n        cache_chains = {}\n        localcache_cls = (SelfEmptyingCache if self.running_as_script\n                          else LocalCache)\n\n        if stalecaches:\n            self.gencache = StaleCacheChain(\n                localcache_cls(),\n                stalecaches,\n                self.mcrouter,\n            )\n        else:\n            self.gencache = CacheChain((localcache_cls(), self.mcrouter))\n        cache_chains.update(gencache=self.gencache)\n\n        if stalecaches:\n            self.thingcache = StaleCacheChain(\n                localcache_cls(),\n                stalecaches,\n                self.mcrouter,\n            )\n        else:\n            self.thingcache = CacheChain((localcache_cls(), self.mcrouter))\n        cache_chains.update(thingcache=self.thingcache)\n\n        if stalecaches:\n            self.memoizecache = StaleCacheChain(\n                localcache_cls(),\n                stalecaches,\n                self.mcrouter,\n            )\n        else:\n            self.memoizecache = MemcacheChain(\n                (localcache_cls(), self.mcrouter))\n        cache_chains.update(memoizecache=self.memoizecache)\n\n        if stalecaches:\n            self.srmembercache = StaleCacheChain(\n                localcache_cls(),\n                stalecaches,\n                self.mcrouter,\n            )\n        else:\n            self.srmembercache = MemcacheChain(\n                (localcache_cls(), self.mcrouter))\n        cache_chains.update(srmembercache=self.srmembercache)\n\n        if stalecaches:\n            self.relcache = StaleCacheChain(\n                localcache_cls(),\n                stalecaches,\n                self.mcrouter,\n            )\n        else:\n            self.relcache = MemcacheChain(\n                (localcache_cls(), self.mcrouter))\n        cache_chains.update(relcache=self.relcache)\n\n        self.ratelimitcache = MemcacheChain(\n                (localcache_cls(), self.mcrouter))\n        cache_chains.update(ratelimitcache=self.ratelimitcache)\n\n        # rendercache holds rendered partial templates.\n        self.rendercache = MemcacheChain((\n            localcache_cls(),\n            self.mcrouter,\n        ))\n        cache_chains.update(rendercache=self.rendercache)\n\n        # commentpanecaches hold fully rendered comment panes\n        self.commentpanecache = MemcacheChain((\n            localcache_cls(),\n            self.mcrouter,\n        ))\n        cache_chains.update(commentpanecache=self.commentpanecache)\n\n        # cassandra_local_cache is used for request-local caching in tdb_cassandra\n        self.cassandra_local_cache = localcache_cls()\n        cache_chains.update(cassandra_local_cache=self.cassandra_local_cache)\n\n        if stalecaches:\n            permacache_cache = StaleCacheChain(\n                localcache_cls(),\n                stalecaches,\n                permacache_memcaches,\n            )\n        else:\n            permacache_cache = CacheChain(\n                (localcache_cls(), permacache_memcaches),\n            )\n        cache_chains.update(permacache=permacache_cache)\n\n        self.permacache = Permacache(\n            permacache_cache,\n            permacache_cf,\n            lock_factory=self.make_lock,\n        )\n\n        # hardcache is used for various things that tend to expire\n        # TODO: replace hardcache w/ cassandra stuff\n        self.hardcache = HardcacheChain(\n            (localcache_cls(), HardCache(self)),\n            cache_negative_results=True,\n        )\n        cache_chains.update(hardcache=self.hardcache)\n\n        # I know this sucks, but we need non-request-threads to be\n        # able to reset the caches, so we need them be able to close\n        # around 'cache_chains' without being able to call getattr on\n        # 'g'\n        def reset_caches():\n            for name, chain in cache_chains.iteritems():\n                if isinstance(chain, TransitionalCache):\n                    chain = chain.read_chain\n\n                chain.reset()\n                if isinstance(chain, LocalCache):\n                    continue\n                elif isinstance(chain, StaleCacheChain):\n                    chain.stats = StaleCacheStats(self.stats, name)\n                else:\n                    chain.stats = CacheStats(self.stats, name)\n        self.cache_chains = cache_chains\n\n        self.reset_caches = reset_caches\n        self.reset_caches()\n\n        self.startup_timer.intermediate(\"cache_chains\")\n\n        # try to set the source control revision numbers\n        self.versions = {}\n        r2_root = os.path.dirname(os.path.dirname(self.paths[\"root\"]))\n        r2_gitdir = os.path.join(r2_root, \".git\")\n        self.short_version = self.record_repo_version(\"r2\", r2_gitdir)\n\n        if I18N_PATH:\n            i18n_git_path = os.path.join(os.path.dirname(I18N_PATH), \".git\")\n            self.record_repo_version(\"i18n\", i18n_git_path)\n\n        # Initialize the amqp module globals, start the worker, etc.\n        r2.lib.amqp.initialize(self)\n\n        self.events = EventQueue()\n\n        self.startup_timer.intermediate(\"revisions\")\n\n    def setup_complete(self):\n        self.startup_timer.stop()\n        self.stats.flush()\n\n        if self.log_start:\n            self.log.error(\n                \"%s:%s started %s at %s (took %.02fs)\",\n                self.reddit_host,\n                self.reddit_pid,\n                self.short_version,\n                datetime.now().strftime(\"%H:%M:%S\"),\n                self.startup_timer.elapsed_seconds()\n            )\n\n        if einhorn.is_worker():\n            einhorn.ack_startup()\n\n    def record_repo_version(self, repo_name, git_dir):\n        \"\"\"Get the currently checked out git revision for a given repository,\n        record it in g.versions, and return the short version of the hash.\"\"\"\n        try:\n            subprocess.check_output\n        except AttributeError:\n            # python 2.6 compat\n            pass\n        else:\n            try:\n                revision = subprocess.check_output([\"git\",\n                                                    \"--git-dir\", git_dir,\n                                                    \"rev-parse\", \"HEAD\"])\n            except subprocess.CalledProcessError, e:\n                self.log.warning(\"Unable to fetch git revision: %r\", e)\n            else:\n                self.versions[repo_name] = revision.rstrip()\n                return revision[:7]\n\n        return \"(unknown)\"\n\n    def load_db_params(self):\n        self.databases = tuple(ConfigValue.to_iter(self.config.raw_data['databases']))\n        self.db_params = {}\n        self.predefined_type_ids = {}\n        if not self.databases:\n            return\n\n        if self.env == 'unit_test':\n            from mock import MagicMock\n            return MagicMock()\n\n        dbm = db_manager.db_manager()\n        db_param_names = ('name', 'db_host', 'db_user', 'db_pass', 'db_port',\n                          'pool_size', 'max_overflow')\n        for db_name in self.databases:\n            conf_params = ConfigValue.to_iter(self.config.raw_data[db_name + '_db'])\n            params = dict(zip(db_param_names, conf_params))\n            if params['db_user'] == \"*\":\n                params['db_user'] = self.db_user\n            if params['db_pass'] == \"*\":\n                params['db_pass'] = self.db_pass\n            if params['db_port'] == \"*\":\n                params['db_port'] = self.db_port\n\n            if params['pool_size'] == \"*\":\n                params['pool_size'] = self.db_pool_size\n            if params['max_overflow'] == \"*\":\n                params['max_overflow'] = self.db_pool_overflow_size\n\n            dbm.setup_db(db_name, g_override=self, **params)\n            self.db_params[db_name] = params\n\n        dbm.type_db = dbm.get_engine(self.config.raw_data['type_db'])\n        dbm.relation_type_db = dbm.get_engine(self.config.raw_data['rel_type_db'])\n\n        def split_flags(raw_params):\n            params = []\n            flags = {}\n\n            for param in raw_params:\n                if not param.startswith(\"!\"):\n                    params.append(param)\n                else:\n                    key, sep, value = param[1:].partition(\"=\")\n                    if sep:\n                        flags[key] = value\n                    else:\n                        flags[key] = True\n\n            return params, flags\n\n        prefix = 'db_table_'\n        for k, v in self.config.raw_data.iteritems():\n            if not k.startswith(prefix):\n                continue\n\n            params, table_flags = split_flags(ConfigValue.to_iter(v))\n            name = k[len(prefix):]\n            kind = params[0]\n            server_list = self.config.raw_data[\"db_servers_\" + name]\n            engines, flags = split_flags(ConfigValue.to_iter(server_list))\n\n            typeid = table_flags.get(\"typeid\")\n            if typeid:\n                self.predefined_type_ids[name] = int(typeid)\n\n            if kind == 'thing':\n                dbm.add_thing(name, dbm.get_engines(engines),\n                              **flags)\n            elif kind == 'relation':\n                dbm.add_relation(name, params[1], params[2],\n                                 dbm.get_engines(engines),\n                                 **flags)\n        return dbm\n\n    def __del__(self):\n        \"\"\"\n        Put any cleanup code to be run when the application finally exits \n        here.\n        \"\"\"\n        pass\n\n    @property\n    def search(self):\n        if getattr(self, 'search_provider', None):\n            if type(self.search_provider) == str:\n                self.search_provider = select_provider(self.config,\n                                       self.pkg_resources_working_set,\n                                       \"r2.provider.search\",\n                                       self.search_provider,\n                                       )\n            return  self.search_provider\n        return None\n\n    @property\n    def search_syntaxes(self):\n        return SEARCH_SYNTAXES[self.config.get('search_provider')]\n"
  },
  {
    "path": "r2/r2/lib/authorize/__init__.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom api import *\nfrom interaction import *\n"
  },
  {
    "path": "r2/r2/lib/authorize/api.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\n\"\"\"\nFor talking to authorize.net credit card payments via their XML api.\n\nThis file consists mostly of wrapper classes for dealing with their\nAPI, while the actual useful functions live in interaction.py\n\nNOTE: This is using the Customer Information Manager (CIM) API\nhttp://developer.authorize.net/api/cim/\n\"\"\"\n\nimport re\nfrom httplib import HTTPSConnection\nfrom urlparse import urlparse\n\nfrom BeautifulSoup import BeautifulStoneSoup\nfrom pylons import app_globals as g\nfrom xml.sax.saxutils import escape\n\nfrom r2.lib.export import export\nfrom r2.lib.utils import iters, Storage\n\n__all__ = [\"PROFILE_LIMIT\", \"TRANSACTION_NOT_FOUND\"]\n\nTRANSACTION_NOT_FOUND = 16\n\n# response codes http://www.authorize.net/support/ReportingGuide_XML.pdf\nTRANSACTION_APPROVED = 1\nTRANSACTION_DECLINED = 2\nTRANSACTION_ERROR = 3\nTRANSACTION_IN_REVIEW = 4\n\n# response reason codes\nTRANSACTION_DUPLICATE = 11\n# transactions with identical amount, credit card, and invoice submitted within\n# some time window will raise an error\n# https://support.authorize.net/authkb/index?page=content&id=A425\n\n\n# list of the most common errors.\nErrors = Storage(\n    TRANSACTION_FAIL=\"E00027\",\n    DUPLICATE_RECORD=\"E00039\", \n    RECORD_NOT_FOUND=\"E00040\",\n    TOO_MANY_PAY_PROFILES=\"E00042\",\n    TOO_MANY_SHIP_ADDRESSES=\"E00043\",\n)\n\nPROFILE_LIMIT = 10 # max payment profiles per user allowed by authorize.net\n\n\n@export\nclass AuthorizeNetException(Exception):\n    def __init__(self, msg, code=None):\n        # don't let CC info show up in logs\n        msg = re.sub(\"<cardNumber>\\d+(\\d{4})</cardNumber>\", \n                     \"<cardNumber>...\\g<1></cardNumber>\",\n                     msg)\n        msg = re.sub(\"<cardCode>\\d+</cardCode>\",\n                     \"<cardCode>omitted</cardCode>\",\n                     msg)\n        self.code = code\n        super(AuthorizeNetException, self).__init__(msg)\n\n\nclass TransactionError(Exception):\n    def __init__(self, message):\n        self.message = message\n\n\nclass DuplicateTransactionError(TransactionError):\n    def __init__(self, transaction_id):\n        self.transaction_id = transaction_id\n        message = ('DuplicateTransactionError with transaction_id %d' %\n                   transaction_id)\n        super(DuplicateTransactionError, self).__init__(message)\n\n\nclass AuthorizationHoldNotFound(Exception): pass\n\n\n# xml tags whose content shouldn't be escaped \n_no_escape_list = [\"extraOptions\"]\n\n\nclass SimpleXMLObject(object):\n    \"\"\"\n    All API transactions are done with authorize.net using XML, so\n    here's a class for generating and extracting structured data from\n    XML.\n    \"\"\"\n    _keys = []\n    def __init__(self, **kw):\n        self._used_keys = self._keys if self._keys else kw.keys()\n        for k in self._used_keys:\n            if not hasattr(self, k):\n                setattr(self, k, kw.get(k, \"\"))\n\n    @staticmethod\n    def simple_tag(name, content, **attrs):\n        attrs = \" \".join('%s=\"%s\"' % (k, v) for k, v in attrs.iteritems())\n        if attrs:\n            attrs = \" \" + attrs\n        return (\"<%(name)s%(attrs)s>%(content)s</%(name)s>\" %\n                dict(name=name, content=content, attrs=attrs))\n\n    def toXML(self):\n        content = []\n        def process(k, v):\n            if isinstance(v, SimpleXMLObject):\n                v = v.toXML()\n            elif v is not None:\n                v = unicode(v)\n                if k not in _no_escape_list:\n                    v = escape(v) # escape &, <, and >\n            if v is not None:\n                content.append(self.simple_tag(k, v))\n\n        for k in self._used_keys:\n            v = getattr(self, k)\n            if isinstance(v, iters):\n                for val in v:\n                    process(k, val)\n            else:\n                process(k, v)\n        return self._wrapper(\"\".join(content))\n\n    @classmethod\n    def fromXML(cls, data):\n        kw = {}\n        for k in cls._keys:\n            d = data.find(k.lower())\n            if d and d.contents:\n                kw[k] = unicode(d.contents[0])\n        return cls(**kw)\n\n\n    def __repr__(self):\n        return \"<%s {%s}>\" % (self.__class__.__name__,\n                              \",\".join(\"%s=%s\" % (k, repr(getattr(self, k)))\n                                       for k in self._used_keys))\n\n    def _name(self):\n        name = self.__class__.__name__\n        return name[0].lower() + name[1:]\n    \n    def _wrapper(self, content):\n        return content\n\n\nclass Auth(SimpleXMLObject):\n    _keys = [\"name\", \"transactionKey\"]\n\n\n@export\nclass Address(SimpleXMLObject):\n    _keys = [\"firstName\", \"lastName\", \"company\", \"address\",\n             \"city\", \"state\", \"zip\", \"country\", \"phoneNumber\",\n             \"faxNumber\",\n             \"customerPaymentProfileId\",\n             \"customerAddressId\" ]\n    def __init__(self, **kw):\n        kw['customerPaymentProfileId'] = kw.get(\"customerPaymentProfileId\",\n                                                 None)\n        kw['customerAddressId'] = kw.get(\"customerAddressId\", None)\n        SimpleXMLObject.__init__(self, **kw)\n\n\n@export\nclass CreditCard(SimpleXMLObject):\n    _keys = [\"cardNumber\", \"expirationDate\", \"cardCode\"]\n\n\nclass Profile(SimpleXMLObject):\n    _keys = [\"merchantCustomerId\", \"description\",\n             \"email\", \"customerProfileId\", \"paymentProfiles\", \"validationMode\"]\n\n    def __init__(self, description, merchantCustomerId, customerProfileId,\n                 paymentProfiles, validationMode=None):\n        SimpleXMLObject.__init__(\n            self,\n            merchantCustomerId=merchantCustomerId,\n            description=description,\n            email=\"\",\n            paymentProfiles=paymentProfiles,\n            validationMode=validationMode,\n            customerProfileId=customerProfileId,\n        )\n\n\nclass PaymentProfile(SimpleXMLObject):\n    _keys = [\"billTo\", \"payment\", \"customerPaymentProfileId\", \"validationMode\"]\n    def __init__(self, billTo, card, customerPaymentProfileId=None,\n                 validationMode=None):\n        SimpleXMLObject.__init__(\n            self,\n            billTo=billTo,\n            customerPaymentProfileId=customerPaymentProfileId,\n            payment=SimpleXMLObject(creditCard=card),\n            validationMode=validationMode,\n        )\n\n    @classmethod\n    def fromXML(cls, res):\n        paymentId = int(res.customerpaymentprofileid.contents[0])\n        billTo = Address.fromXML(res.billto)\n        card = CreditCard.fromXML(res.payment)\n        return cls(billTo, card, paymentId)\n\n\n@export\nclass Order(SimpleXMLObject):\n    _keys = [\"invoiceNumber\", \"description\", \"purchaseOrderNumber\"]\n\n\nclass Transaction(SimpleXMLObject):\n    _keys = [\"amount\", \"customerProfileId\", \"customerPaymentProfileId\",\n             \"transId\", \"order\"]\n\n    def __init__(self, amount, customerProfileId, customerPaymentProfileId,\n                 transId=None, order=None):\n        SimpleXMLObject.__init__(\n            self, amount=amount, customerProfileId=customerProfileId,\n            customerPaymentProfileId=customerPaymentProfileId, transId=transId,\n            order=order)\n\n    def _wrapper(self, content):\n        return self.simple_tag(self._name(), content)\n\n\n# only authorize (no charge is made)\n@export\nclass ProfileTransAuthOnly(Transaction): pass\n\n\n# charge only (requires previous auth_only)\n@export\nclass ProfileTransPriorAuthCapture(Transaction): pass\n\n\n# refund a transaction\n@export\nclass ProfileTransRefund(Transaction): pass\n\n\n# void a transaction\n@export\nclass ProfileTransVoid(Transaction): pass\n\n\n#-----\nclass AuthorizeNetRequest(SimpleXMLObject):\n    _keys = [\"merchantAuthentication\"]\n\n    @property\n    def merchantAuthentication(self):\n        return Auth(name=g.secrets['authorizenetname'],\n                    transactionKey=g.secrets['authorizenetkey'])\n\n    def _wrapper(self, content):\n        return ('<?xml version=\"1.0\" encoding=\"utf-8\"?>' +\n                self.simple_tag(self._name(), content,\n                             xmlns=\"AnetApi/xml/v1/schema/AnetApiSchema.xsd\"))\n\n    def make_request(self):\n        u = urlparse(g.authorizenetapi)\n        conn = HTTPSConnection(u.hostname, u.port)\n        conn.request(\"POST\", u.path, self.toXML().encode('utf-8'),\n                     {\"Content-type\": \"text/xml\"})\n        res = conn.getresponse()\n        res = self.handle_response(res.read())\n        conn.close()\n        return res\n\n    def is_error_code(self, res, code):\n        return (res.message.code and res.message.code.contents and\n                res.message.code.contents[0] == code)\n\n    _autoclose_re = re.compile(\"<([^/]+)/>\")\n    def _autoclose_handler(self, m):\n        return \"<%(m)s></%(m)s>\" % dict(m=m.groups()[0])\n\n    def handle_response(self, res):\n        res = self._autoclose_re.sub(self._autoclose_handler, res)\n        res = BeautifulStoneSoup(res, \n                                 markupMassage=False, \n                                 convertEntities=BeautifulStoneSoup.XML_ENTITIES)\n        if res.resultcode.contents[0] == u\"Ok\":\n            return self.process_response(res)\n        else:\n            return self.process_error(res)\n\n    def process_response(self, res):\n        raise NotImplementedError\n\n    def process_error(self, res):\n        raise NotImplementedError\n\n\n# --- real request classes below\n\nclass CreateCustomerProfileRequest(AuthorizeNetRequest):\n    _keys = AuthorizeNetRequest._keys + [\"profile\", \"validationMode\"]\n\n    def __init__(self, profile, validationMode=None):\n        AuthorizeNetRequest.__init__(\n            self, profile=profile, validationMode=validationMode)\n\n    def process_response(self, res):\n        customer_id = int(res.customerprofileid.contents[0])\n        return customer_id\n\n    def process_error(self, res):\n        message_text = res.find(\"text\").contents[0]\n\n        if self.is_error_code(res, Errors.DUPLICATE_RECORD):\n            # authorize.net has a record for this user but we don't. get the id\n            # from the error message\n            matches = re.match(\n                \"A duplicate record with ID (\\d+) already exists\", message_text)\n            if matches:\n                match_groups = matches.groups()\n                customer_id = match_groups[0]\n                return customer_id\n\n        raise AuthorizeNetException(message_text)\n\n\nclass CreateCustomerPaymentProfileRequest(AuthorizeNetRequest):\n    _keys = AuthorizeNetRequest._keys + [\"customerProfileId\", \"paymentProfile\",\n        \"validationMode\"]\n\n    def __init__(self, customerProfileId, paymentProfile, validationMode=None):\n        AuthorizeNetRequest.__init__(\n            self, customerProfileId=customerProfileId,\n            paymentProfile=paymentProfile, validationMode=validationMode)\n\n    def process_response(self, res):\n        pay_id = int(res.customerpaymentprofileid.contents[0])\n        return pay_id\n\n    def process_error(self, res):\n        message_text = res.find(\"text\").contents[0]\n        raise AuthorizeNetException(message_text)\n\n\nclass GetCustomerProfileRequest(AuthorizeNetRequest):\n    _keys = AuthorizeNetRequest._keys + [\"customerProfileId\"]\n\n    def __init__(self, customerProfileId):\n        AuthorizeNetRequest.__init__(\n            self, customerProfileId=customerProfileId)\n\n    def process_response(self, res):\n        merchantCustomerId = res.merchantcustomerid.contents[0]\n        description = res.description.contents[0]\n        profile_id = int(res.customerprofileid.contents[0])\n\n        payment_profiles = []\n        for profile in res.findAll(\"paymentprofiles\"):\n            address = Address.fromXML(profile)\n            credit_card = CreditCard.fromXML(profile.payment)\n            customerPaymentProfileId = int(address.customerPaymentProfileId)\n\n            payment_profile = PaymentProfile(\n                billTo=address,\n                card=credit_card,\n                customerPaymentProfileId=customerPaymentProfileId,\n            )\n            payment_profiles.append(payment_profile)\n\n        profile = Profile(\n            description=description,\n            merchantCustomerId=merchantCustomerId,\n            customerProfileId=profile_id,\n            paymentProfiles=payment_profiles,\n        )\n        return profile\n\n    def process_error(self, res):\n        message_text = res.find(\"text\").contents[0]\n        code = res.find('code').contents[0]\n        raise AuthorizeNetException(message_text, code=code)\n\n\n# TODO: implement\nclass DeleteCustomerPaymentProfileRequest(AuthorizeNetRequest):\n    _keys = AuthorizeNetRequest._keys + [\"customerProfileId\",\n        \"customerPaymentProfileId\"]\n\n    def __init__(self, customerProfileId, customerPaymentProfileId):\n        AuthorizeNetRequest.__init__(\n            self, customerProfileId=customerProfileId,\n            customerPaymentProfileId=customerPaymentProfileId)\n\n    def process_response(self, res):\n        return True\n\n    def process_error(self, res):\n        if self.is_error_code(res, Errors.RECORD_NOT_FOUND):\n            return True\n\n        message_text = res.find(\"text\").contents[0]\n        raise AuthorizeNetException(message_text)\n\n\nclass UpdateCustomerPaymentProfileRequest(AuthorizeNetRequest):\n    _keys = AuthorizeNetRequest._keys + [\"customerProfileId\", \"paymentProfile\",\n        \"validationMode\"]\n\n    def __init__(self, customerProfileId, paymentProfile, validationMode=None):\n        AuthorizeNetRequest.__init__(\n            self, customerProfileId=customerProfileId,\n            paymentProfile=paymentProfile, validationMode=validationMode)\n\n    def process_response(self, res):\n        return self.paymentProfile.customerPaymentProfileId\n\n    def process_error(self, res):\n        message_text = res.find(\"text\").contents[0]\n        raise AuthorizeNetException(message_text)\n\n\nclass CreateCustomerProfileTransactionRequest(AuthorizeNetRequest):\n    _keys = AuthorizeNetRequest._keys + [\"transaction\", \"extraOptions\"]\n\n    # unlike every other response we get back, this api function\n    # returns CSV data of the response with no field labels.  these\n    # are used in package_response to zip this data into a usable\n    # storage.\n    response_keys = (\"response_code\",\n                     \"response_subcode\",\n                     \"response_reason_code\",\n                     \"response_reason_text\",\n                     \"authorization_code\",\n                     \"avs_response\",\n                     \"trans_id\",\n                     \"invoice_number\",\n                     \"description\",\n                     \"amount\", \"method\",\n                     \"transaction_type\",\n                     \"customerID\",\n                     \"firstName\", \"lastName\",\n                     \"company\", \"address\", \"city\", \"state\",\n                     \"zip\", \"country\", \n                     \"phoneNumber\", \"faxNumber\", \"email\",\n                     \"shipTo_firstName\", \"shipTo_lastName\",\n                     \"shipTo_company\", \"shipTo_address\",\n                     \"shipTo_city\", \"shipTo_state\",\n                     \"shipTo_zip\", \"shipTo_country\",\n                     \"tax\", \"duty\", \"freight\",\n                     \"tax_exempt\", \"po_number\", \"md5\",\n                     \"cav_response\")\n\n    # list of casts for the response fields given above\n    response_types = dict(response_code=int,\n                          response_subcode=int,\n                          response_reason_code=int,\n                          trans_id=int)\n\n    def __init__(self, **kw):\n        self._extra = kw.get(\"extraOptions\", {})\n        AuthorizeNetRequest.__init__(self, **kw)\n\n    @property\n    def extraOptions(self):\n        return \"<![CDATA[%s]]>\" % \"&\".join(\"%s=%s\" % x\n                                            for x in self._extra.iteritems())\n\n    def process_response(self, res):\n        return (True, self.package_response(res))\n\n    def process_error(self, res):\n        return (False, self.package_response(res))\n\n    def package_response(self, res):\n        content = res.directresponse.contents[0]\n        s = Storage(zip(self.response_keys, content.split(',')))\n        for name, cast in self.response_types.iteritems():\n            try:\n                s[name] = cast(s[name])\n            except ValueError:\n                pass\n        return s\n\n\nclass GetSettledBatchListRequest(AuthorizeNetRequest):\n    _keys = AuthorizeNetRequest._keys + [\"includeStatistics\", \n                                         \"firstSettlementDate\", \n                                         \"lastSettlementDate\"]\n    def __init__(self, start_date, end_date, **kw):\n        AuthorizeNetRequest.__init__(self, \n                                     includeStatistics=1,\n                                     firstSettlementDate=start_date.isoformat(),\n                                     lastSettlementDate=end_date.isoformat(),\n                                     **kw)\n\n    def process_response(self, res):\n        return res\n\n    def process_error(self, res):\n        message_text = res.find(\"text\").contents[0]\n        raise AuthorizeNetException(message_text)\n\n\ndef create_customer_profile(merchant_customer_id, description):\n    profile = Profile(\n        description=description,\n        merchantCustomerId=merchant_customer_id,\n        paymentProfiles=None,\n        customerProfileId=None,\n    )\n\n    request = CreateCustomerProfileRequest(profile=profile)\n\n    try:\n        customer_id = request.make_request()\n    except AuthorizeNetException:\n        return None\n\n    return customer_id\n\n\ndef get_customer_profile(customer_id):\n    request = GetCustomerProfileRequest(customerProfileId=customer_id)\n\n    try:\n        profile = request.make_request()\n    except AuthorizeNetException:\n        return None\n\n    return profile\n\n\ndef create_payment_profile(customer_id, address, credit_card, validate=False):\n    payment_profile = PaymentProfile(billTo=address, card=credit_card)\n\n    request = CreateCustomerPaymentProfileRequest(\n        customerProfileId=customer_id,\n        paymentProfile=payment_profile,\n        validationMode=\"liveMode\" if validate else None,\n    )\n\n    payment_profile_id = request.make_request()\n\n    return payment_profile_id\n\n\ndef update_payment_profile(customer_id, payment_profile_id, address,\n                           credit_card, validate=False):\n    payment_profile = PaymentProfile(\n        billTo=address,\n        card=credit_card,\n        customerPaymentProfileId=payment_profile_id,\n    )\n\n    request = UpdateCustomerPaymentProfileRequest(\n        customerProfileId=customer_id,\n        paymentProfile=payment_profile,\n        validationMode=\"liveMode\" if validate else None,\n    )\n\n    payment_profile_id = request.make_request()\n\n    return payment_profile_id\n\n\n# TODO: implement\ndef delete_payment_profile(customer_id, payment_profile_id):\n    request = DeleteCustomerPaymentProfileRequest(\n        customerProfileId=customer_id,\n        customerPaymentProfileId=payment_profile_id,\n    )\n\n    try:\n        success = request.make_request()\n    except AuthorizeNetException:\n        return False\n    else:\n        return True\n\n\ndef create_authorization_hold(customer_id, payment_profile_id, amount, invoice,\n                              customer_ip=None):\n    order = Order(invoiceNumber=invoice)\n    transaction = ProfileTransAuthOnly(\n        amount=\"%.2f\" % amount,\n        customerProfileId=customer_id,\n        customerPaymentProfileId=payment_profile_id,\n        transId=None,\n        order=order,\n    )\n    if customer_ip:\n        extra = {\"x_customer_ip\": customer_ip}\n    else:\n        extra = {}\n\n    request = CreateCustomerProfileTransactionRequest(\n        transaction=transaction, extraOptions=extra)\n    success, res = request.make_request()\n\n    if (res.trans_id and\n            res.response_code == TRANSACTION_ERROR and\n            res.response_reason_code == TRANSACTION_DUPLICATE):\n        raise DuplicateTransactionError(res.trans_id)\n\n    if success:\n        return res.trans_id\n    else:\n        raise TransactionError(res.response_reason_text)\n\n\ndef capture_authorization_hold(customer_id, payment_profile_id, amount,\n                               transaction_id):\n    transaction = ProfileTransPriorAuthCapture(\n        amount=\"%.2f\" % amount,\n        customerProfileId=customer_id,\n        customerPaymentProfileId=payment_profile_id,\n        transId=transaction_id,\n    )\n\n    request = CreateCustomerProfileTransactionRequest(\n        transaction=transaction)\n    success, res = request.make_request()\n    response_reason_code = res.get(\"response_reason_code\")\n\n    if success:\n        return\n    elif response_reason_code == TRANSACTION_NOT_FOUND:\n        raise AuthorizationHoldNotFound()\n    else:\n        raise TransactionError(res.response_reason_text)\n\n\ndef void_authorization_hold(customer_id, payment_profile_id, transaction_id):\n    transaction = ProfileTransVoid(\n        amount=None,\n        customerProfileId=customer_id,\n        customerPaymentProfileId=payment_profile_id,\n        transId=transaction_id,\n    )\n\n    request = CreateCustomerProfileTransactionRequest(\n        transaction=transaction)\n    success, res = request.make_request()\n\n    if success:\n        return res.trans_id\n    else:\n        raise TransactionError(res.response_reason_text)\n\n\ndef refund_transaction(customer_id, payment_profile_id, amount, transaction_id):\n    transaction = ProfileTransRefund(\n        amount=\"%.2f\" % amount,\n        customerProfileId=customer_id,\n        customerPaymentProfileId=payment_profile_id,\n        transId=transaction_id,\n    )\n    request = CreateCustomerProfileTransactionRequest(\n        transaction=transaction)\n    success, res = request.make_request()\n\n    if not success:\n        raise TransactionError(res.response_reason_text)\n"
  },
  {
    "path": "r2/r2/lib/authorize/interaction.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom sqlalchemy.orm.exc import MultipleResultsFound\n\nfrom pylons import request\nfrom pylons import app_globals as g\n\nfrom r2.lib.db.thing import NotFound\nfrom r2.lib.utils import Storage\nfrom r2.lib.export import export\nfrom r2.models.bidding import Bid, CustomerID, PayID\nfrom r2.lib.authorize import api\n\n\n__all__ = []\n\n\nFREEBIE_PAYMENT_METHOD_ID = -1\n\n\n@export\ndef get_or_create_customer_profile(user):\n    profile_id = CustomerID.get_id(user._id)\n    if not profile_id:\n        profile_id = api.create_customer_profile(\n            merchant_customer_id=user._fullname, description=user.name)\n        CustomerID.set(user, profile_id)\n\n    profile = api.get_customer_profile(profile_id)\n\n    if not profile or profile.merchantCustomerId != user._fullname:\n        raise ValueError(\"error getting customer profile\")\n\n    for payment_profile in profile.paymentProfiles:\n        PayID.add(user, payment_profile.customerPaymentProfileId)\n\n    return profile\n\n\ndef add_payment_method(user, address, credit_card, validate=False):\n    profile_id = CustomerID.get_id(user._id)\n    payment_method_id = api.create_payment_profile(\n        profile_id, address, credit_card, validate)\n\n    if payment_method_id:\n        PayID.add(user, payment_method_id)\n        return payment_method_id\n\n\ndef update_payment_method(user, payment_method_id, address, credit_card,\n                          validate=False):\n    profile_id = CustomerID.get_id(user._id)\n    payment_method_id = api.update_payment_profile(\n        profile_id, payment_method_id, address, credit_card, validate)\n    return payment_method_id\n\n\n@export\ndef delete_payment_method(user, payment_method_id):\n    profile_id = CustomerID.get_id(user._id)\n    success = api.delete_payment_profile(profile_id, payment_method_id)\n    if success:\n        PayID.delete(user, payment_method_id)\n\n\n@export\ndef add_or_update_payment_method(user, address, credit_card, pay_id=None):\n    if pay_id:\n        return update_payment_method(user, pay_id, address, credit_card,\n                                     validate=True)\n    else:\n        return add_payment_method(user, address, credit_card, validate=True)\n\n\n@export\ndef is_charged_transaction(trans_id, campaign):\n    if not trans_id: return False # trans_id == 0 means no bid\n    try:\n        bid = Bid.one(transaction=trans_id, campaign=campaign)\n    except NotFound:\n        return False\n    except MultipleResultsFound:\n        g.log.error('Multiple bids for trans_id %s' % trans_id)\n        return False\n\n    return bid.is_charged() or bid.is_refund()\n\n\n@export\ndef auth_freebie_transaction(amount, user, link, campaign_id):\n    transaction_id = -link._id\n\n    try:\n        # attempt to update existing freebie transaction\n        bid = Bid.one(thing_id=link._id, transaction=transaction_id,\n                      campaign=campaign_id)\n    except NotFound:\n        bid = Bid._new(transaction_id, user, FREEBIE_PAYMENT_METHOD_ID,\n                       link._id, amount, campaign_id)\n    else:\n        bid.bid = amount\n        bid.auth()\n\n    return transaction_id, \"\"\n\n\n@export\ndef auth_transaction(amount, user, payment_method_id, link, campaign_id):\n    if payment_method_id not in PayID.get_ids(user._id):\n        return None, \"invalid payment method\"\n\n    profile_id = CustomerID.get_id(user._id)\n    invoice = \"T%dC%d\" % (link._id, campaign_id)\n\n    try:\n        transaction_id = api.create_authorization_hold(\n            profile_id, payment_method_id, amount, invoice, request.ip)\n    except api.DuplicateTransactionError as e:\n        transaction_id = e.transaction_id\n        try:\n            bid = Bid.one(transaction_id, campaign=campaign_id)\n        except NotFound:\n            bid = Bid._new(transaction_id, user, payment_method_id, link._id,\n                           amount, campaign_id)\n        g.log.error(\"%s on campaign %d\" % (e.message, campaign_id))\n        return transaction_id, None\n    except api.TransactionError as e:\n        return None, e.message\n\n    bid = Bid._new(transaction_id, user, payment_method_id, link._id, amount,\n                   campaign_id)\n    return transaction_id, None\n\n\n@export\ndef charge_transaction(user, transaction_id, campaign_id):\n    bid = Bid.one(transaction=transaction_id, campaign=campaign_id)\n    if bid.is_charged():\n        return True, None\n\n    if transaction_id < 0:\n        bid.charged()\n        return True, None\n\n    profile_id = CustomerID.get_id(user._id)\n\n    try:\n        api.capture_authorization_hold(\n            customer_id=profile_id,\n            payment_profile_id=bid.pay_id,\n            amount=bid.bid,\n            transaction_id=transaction_id,\n        )\n    except api.AuthorizationHoldNotFound:\n        # authorization hold has expired\n        bid.void()\n        return False, api.TRANSACTION_NOT_FOUND\n    except api.TransactionError as e:\n        return False, e.message\n\n    bid.charged()\n    return True, None\n\n\n@export\ndef void_transaction(user, transaction_id, campaign_id):\n    bid = Bid.one(transaction=transaction_id, campaign=campaign_id)\n\n    if transaction_id <= 0:\n        bid.void()\n        return True, None\n\n    profile_id = CustomerID.get_id(user._id)\n    try:\n        api.void_authorization_hold(profile_id, bid.pay_id, transaction_id)\n    except api.TransactionError as e:\n        return False, e.message\n\n    bid.void()\n    return True, None\n\n\n@export\ndef refund_transaction(user, transaction_id, campaign_id, amount):\n    bid =  Bid.one(transaction=transaction_id, campaign=campaign_id)\n    if transaction_id < 0:\n        bid.refund(amount)\n        return True, None\n\n    profile_id = CustomerID.get_id(user._id)\n    try:\n        api.refund_transaction(\n            customer_id=profile_id,\n            payment_profile_id=bid.pay_id,\n            amount=amount,\n            transaction_id=transaction_id,\n        )\n    except api.TransactionError as e:\n        return False, e.message\n\n    bid.refund(amount)\n    return True, None\n"
  },
  {
    "path": "r2/r2/lib/automoderator.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\n\"\"\"Implements the AutoModerator functionality, a rules system for subreddits.\n\nAutoModerator allows subreddits to define \"rules\", which are conditions\nchecked against submissions and comments in that subreddit. If a rule's\nconditions are satisfied by a post, actions can be automatically taken on it,\nsuch as removing the post, setting flair on it, posting a comment, etc.\n\nA subreddit's rules are defined through YAML on a mod-only page on the\nsubreddit's wiki. This collection of multiple rules is implemented with the\nRuleset class. Each individual rule is a Rule object, which is made up of at\nleast one RuleTarget object, which defines the conditions and/or actions to\napply to an item. Rules may have additional RuleTargets which represent\n\"related items\" that also can have conditions and/or actions. Currently,\nsupport exists for up to two additional RuleTargets, one for the item's\nauthor, and another for the parent link (if the original item was a comment).\n\"\"\"\n\nfrom collections import namedtuple\nfrom datetime import datetime\nfrom hashlib import md5\nimport re\nimport traceback\nimport yaml\n\nfrom pylons import app_globals as g\n\nfrom r2.lib import amqp\nfrom r2.lib.db import queries\nfrom r2.lib.errors import RedditError\nfrom r2.lib.filters import _force_unicode\nfrom r2.lib.menus import CommentSortMenu\nfrom r2.lib.utils import (\n    SimpleSillyStub,\n    TimeoutFunction,\n    TimeoutFunctionException,\n    lowercase_keys_recursively,\n    timeinterval_fromstr,\n    tup,\n)\nfrom r2.lib.validator import VMarkdown\nfrom r2.models import (\n    admintools,\n    Account,\n    Comment,\n    DeletedUser,\n    Frontpage,\n    Inbox,\n    LastModified,\n    Link,\n    Message,\n    ModAction,\n    Report,\n    Subreddit,\n    Thing,\n    WikiPage,\n)\nfrom r2.models.automoderator import PerformedRulesByThing\nfrom r2.models.wiki import wiki_id\n\n\nif g.automoderator_account:\n    ACCOUNT = Account._by_name(g.automoderator_account)\nelse:\n    ACCOUNT = None\n\nDISCLAIMER = \"*I am a bot, and this action was performed automatically. Please [contact the moderators of this subreddit](/message/compose/?to=/r/{{subreddit}}) if you have any questions or concerns.*\"\n\nrules_by_subreddit = {}\n\nunnumbered_placeholders_regex = re.compile(r\"\\{\\{(match(?:-[^\\d-]+?)?)\\}\\}\")\nmatch_placeholders_regex = re.compile(r\"\\{\\{match-(?:([^}]+?)-)?(\\d+)\\}\\}\")\ndef replace_placeholders(string, data, matches):\n    \"\"\"Replace placeholders in the string with appropriate values.\"\"\"\n    item = data[\"item\"]\n    replacements = {\n        \"{{author}}\": data[\"author\"].name,\n        \"{{body}}\": getattr(item, \"body\", \"\"),\n        \"{{subreddit}}\": data[\"subreddit\"].name,\n        \"{{author_flair_text}}\": data[\"author\"].flair_text(\n            data[\"subreddit\"]._id, obey_disabled=True),\n        \"{{author_flair_css_class}}\": data[\"author\"].flair_css_class(\n            data[\"subreddit\"]._id, obey_disabled=True),\n    }\n\n    if isinstance(item, Comment):\n        context = None\n        if item.parent_id:\n            context = 3\n        replacements.update({\n            \"{{kind}}\": \"comment\",\n            \"{{permalink}}\": item.make_permalink_slow(\n                context=context, force_domain=True),\n            \"{{title}}\": data[\"link\"].title,\n        })\n        media_item = data[\"link\"]\n    elif isinstance(item, Link):\n        replacements.update({\n            \"{{kind}}\": \"submission\",\n            \"{{domain}}\": item.link_domain(),\n            \"{{permalink}}\": item.make_permalink_slow(force_domain=True),\n            \"{{title}}\": item.title,\n            \"{{url}}\": item.url,\n        })\n        media_item = item\n\n    if media_item.media_object:\n        oembed = media_item.media_object.get(\"oembed\")\n        if oembed:\n            replacements.update({\n                \"{{media_author}}\": oembed.get(\"author_name\", \"\"),\n                \"{{media_title}}\": oembed.get(\"title\", \"\"),\n                \"{{media_description}}\": oembed.get(\"description\", \"\"),\n                \"{{media_author_url}}\": oembed.get(\"author_url\", \"\"),\n            })\n\n    for placeholder, replacement in replacements.iteritems():\n        string = string.replace(placeholder, _force_unicode(replacement))\n\n    # do the {{match-XX}} and {{match-field-XX}} replacements\n    field_replacements = {}\n    if matches:\n        # replace any unnumbered matches with -1 versions\n        string = unnumbered_placeholders_regex.sub(r\"{{\\1-1}}\", string)\n\n        # find all {{match-field-XX}} placeholders\n        to_replace = match_placeholders_regex.finditer(string)\n        for placeholder in to_replace:\n            if placeholder.group(0) in field_replacements:\n                continue\n\n            source_match_name = placeholder.group(1)\n            if not source_match_name:\n                # they didn't specify a source, so just take one arbitrarily\n                source_match_name = matches.keys()[0]\n\n            source_match = matches.get(source_match_name, None)\n            if not source_match:\n                continue\n\n            try:\n                group_index = int(placeholder.group(2))\n                replacement = source_match.group(group_index)\n            except IndexError:\n                continue\n\n            field_replacements[placeholder.group(0)] = replacement\n\n    for placeholder, replacement in field_replacements.iteritems():\n        string = string.replace(placeholder, _force_unicode(replacement))\n\n    return string\n\n\nclass AutoModeratorSyntaxError(ValueError):\n    def __init__(self, message, yaml):\n        yaml_lines = yaml.splitlines()\n        if len(yaml_lines) > 10:\n            yaml = \"\\n\".join(yaml_lines[:10]) + \"\\n...\"\n        self.message = \"%s in rule:\\n\\n%s\" % (message, yaml)\n\n\nclass AutoModeratorRuleTypeError(AutoModeratorSyntaxError):\n    pass\n\n\n# used in Ruleset.__init__()\nRuleDefinition = namedtuple(\"RuleDefinition\", [\"yaml\", \"values\"])\n\n\nclass Ruleset(object):\n    \"\"\"A subreddit's collection of Rules.\"\"\"\n    def __init__(self, yaml_text=\"\", timer=None):\n        \"\"\"Create a collection of Rules from YAML documents.\"\"\"\n        if timer is None:\n            timer = SimpleSillyStub()\n\n        self.init_time = datetime.now(g.tz)\n        self.rules = []\n\n        if not yaml_text:\n            return\n\n        # We want to maintain the original YAML source sections, so we need\n        # to manually split up the YAML by the document delimiter (line\n        # starting with \"---\") and then try to load each section to see if\n        # it's valid\n        yaml_sections = [section.strip(\"\\r\\n\")\n            for section in re.split(\"^---\", yaml_text, flags=re.MULTILINE)]\n\n        rule_defs = []\n\n        for section_num, section in enumerate(yaml_sections, 1):\n            try:\n                parsed = yaml.safe_load(section)\n            except Exception as e:\n                raise ValueError(\n                    \"YAML parsing error in section %s: %s\" % (section_num, e))\n\n            # only keep the section if the parsed result is a dict (otherwise\n            # it's generally just a comment)\n            if isinstance(parsed, dict):\n                rule_defs.append(RuleDefinition(yaml=section, values=parsed))\n\n        timer.intermediate(\"yaml_parsing\")\n\n        if any(\"standard\" in rule_def.values for rule_def in rule_defs):\n            # load standard rules from wiki page\n            standard_rules = {}\n            try:\n                wp = WikiPage.get(Frontpage, \"automoderator_standards\")\n                standard_defs = yaml.safe_load_all(wp.content)\n                for standard_def in standard_defs:\n                    name = standard_def.pop(\"standard\")\n                    standard_rules[name] = standard_def\n            except Exception as e:\n                g.log.error(\"Error while loading automod standards: %s\", e)\n                standard_rules = None\n\n        timer.intermediate(\"init_standard_rules\")\n\n        for rule_def in rule_defs:\n            # use standard rule as a base if they defined one\n            standard_name = rule_def.values.pop(\"standard\", None)\n            if standard_name:\n                # error while loading the standards, skip this rule\n                if standard_rules is None:\n                    continue\n\n                standard_values = None\n                if isinstance(standard_name, basestring):\n                    standard_values = standard_rules.get(standard_name, None)\n                if not standard_values:\n                    raise AutoModeratorSyntaxError(\n                        \"Invalid standard: `%s`\" % standard_name,\n                        rule_def.yaml,\n                    )\n\n                new_values = standard_values.copy()\n                new_values.update(rule_def.values)\n                rule_def = rule_def._replace(values=new_values)\n\n            type = rule_def.values.get(\"type\", \"any\")\n            if type == \"any\":\n                # try to create two Rules for comments and links\n                rule = None\n                for type_value in (\"comment\", \"submission\"):\n                    rule_def.values[\"type\"] = type_value\n                    try:\n                        rule = Rule(rule_def.values, rule_def.yaml)\n                    except AutoModeratorRuleTypeError as type_error:\n                        continue\n\n                    # only keep the rule if it had any checks\n                    if rule.has_any_checks(targets_only=True):\n                        self.rules.append(rule)\n\n                # if both types hit exceptions we should actually error\n                if not rule:\n                    raise type_error\n            else:\n                self.rules.append(Rule(rule_def.values, rule_def.yaml))\n\n        timer.intermediate(\"init_rules\")\n\n        # drop any rules that don't have a check and an action\n        self.rules = [rule for rule in self.rules\n            if rule.has_any_checks() and rule.has_any_actions()]\n\n        self.rules.sort(key=lambda r: r.priority, reverse=True)\n\n    def __iter__(self):\n        \"\"\"Iterate over the rules in the collection.\"\"\"\n        for rule in self.rules:\n            yield rule\n\n    def __len__(self):\n        return len(self.rules)\n\n    @property\n    def removal_rules(self):\n        \"\"\"Iterate over the rules that could cause removal of their target.\"\"\"\n        for rule in self:\n            if rule.is_removal_rule:\n                yield rule\n\n    @property\n    def nonremoval_rules(self):\n        \"\"\"Iterate over the rules that won't cause removal of their target.\"\"\"\n        for rule in self:\n            if not rule.is_removal_rule:\n                yield rule\n\n    def apply_to_item(self, item):\n        # fetch supplemental data to use throughout\n        data = {}\n        data[\"item\"] = item\n        data[\"subreddit\"] = item.subreddit_slow\n\n        author = item.author_slow\n        if not author._deleted:\n            data[\"author\"] = author\n        else:\n            data[\"author\"] = DeletedUser()\n\n        if isinstance(item, Comment):\n            data[\"link\"] = Link._byID(item.link_id, data=True)\n            link_author = data[\"link\"].author_slow\n            if not link_author._deleted:\n                data[\"link_author\"] = link_author\n            else:\n                data[\"link_author\"] = DeletedUser()\n            data[\"is_submitter\"] = (author == link_author)\n\n        # get the list of rule IDs that have already been performed\n        already_performed = PerformedRulesByThing.get_already_performed(item)\n\n        # stop checking removal rules as soon as one triggers\n        for rule in self.removal_rules:\n            if rule.is_unrepeatable and rule.unique_id in already_performed:\n                continue\n\n            if rule.check_item(item, data):\n                rule.perform_actions(item, data)\n                break\n\n        # check all other rules, regardless of how many trigger\n        for rule in self.nonremoval_rules:\n            if rule.is_unrepeatable and rule.unique_id in already_performed:\n                continue\n\n            if rule.check_item(item, data):\n                rule.perform_actions(item, data)\n\n\nclass RuleComponent(object):\n    \"\"\"Data related to individual key/value components making up a rule.\"\"\"\n\n    def __init__(self, valid_types=None, valid_values=None,\n            valid_regex=None, valid_targets=None, default=None,\n            component_type=None, aliases=None):\n        \"\"\"\n        Keyword arguments:\n        valid_types -- valid value types for this key\n        valid_values -- if present, a set of valid options for this key\n        valid_regex -- if present, a regex the value must satisfy\n        valid_targets -- this key can only be defined for these target types\n        default -- if this key isn't defined, default to this value\n        component_type -- \"action\" or \"check\" if relevant for this key\n        aliases -- other keys you can use (only if \"normal\" one isn't used)\n        \"\"\"\n        self.valid_types = valid_types\n        self.valid_values = valid_values\n        if valid_regex:\n            self.valid_regex = re.compile(valid_regex)\n        else:\n            self.valid_regex = None\n        self.valid_targets = tup(valid_targets)\n        self.default = default\n        self.component_type = component_type\n        self.aliases = aliases or []\n\n    def validate(self, value):\n        \"\"\"Return whether a value satisfies this key's constraints.\"\"\"\n        if self.valid_types:\n            if not isinstance(value, self.valid_types):\n                return False\n        if self.valid_values:\n            if value not in self.valid_values:\n                return False\n        if self.valid_regex:\n            if not self.valid_regex.search(str(value)):\n                return False\n\n        return True\n\n\nclass RuleTarget(object):\n    \"\"\"The conditions and actions that apply to an individual item.\"\"\"\n\n    # valid options for changing how a field value is searched for a match\n    _match_regexes = {\n        \"full-exact\": u\"^%s$\",\n        \"full-text\": ur\"^\\W*%s\\W*$\",\n        \"includes\": u\"%s\",\n        \"includes-word\": ur\"(?:^|\\W|\\b)%s(?:$|\\W|\\b)\",\n        \"starts-with\": u\"^%s\",\n        \"ends-with\": u\"%s$\",\n    }\n\n    # full list of modifiers that can be applied to a match\n    _match_modifiers = set(_match_regexes.keys()) | {\n        \"case-sensitive\",\n        \"regex\",\n    }\n\n    # valid fields to match against for each target object type\n    _match_fields_by_type = {\n        Link: {\n            \"id\",\n            \"title\",\n            \"domain\",\n            \"url\",\n            \"body\",\n            \"media_author\",\n            \"media_author_url\",\n            \"media_title\",\n            \"media_description\",\n            \"flair_text\",\n            \"flair_css_class\",\n        },\n        Comment: {\n            \"id\",\n            \"body\",\n        },\n        Account: {\n            \"id\",\n            \"name\",\n            \"flair_text\",\n            \"flair_css_class\",\n        }\n    }\n\n    # the match regex to default to for particular fields\n    _match_field_defaults = {\n        \"id\": \"full-exact\",\n        \"url\": \"includes\",\n        \"media_author\": \"full-exact\",\n        \"media_author_url\": \"includes\",\n        \"flair_text\": \"full-exact\",\n        \"flair_css_class\": \"full-exact\",\n    }\n\n    _operator_regex = r\"(==?|<|>)\"\n    _oper_int_regex = r\"^(%s)?\\s*-?\\d+$\" % _operator_regex\n    _oper_period_regex = r\"(%s)?\\s*\\d+\\s*((minute|hour|day|week|month|year)s?)?$\" % _operator_regex\n\n    # all the possible components that can be defined for targets\n    _potential_components = {\n        \"reports\": RuleComponent(\n            valid_types=int,\n            valid_targets=(Link, Comment),\n            component_type=\"check\",\n        ),\n        \"body_longer_than\": RuleComponent(\n            valid_types=int,\n            valid_targets=(Link, Comment),\n            component_type=\"check\",\n        ),\n        \"body_shorter_than\": RuleComponent(\n            valid_types=int,\n            valid_targets=(Link, Comment),\n            component_type=\"check\",\n        ),\n        \"ignore_blockquotes\": RuleComponent(\n            valid_types=bool,\n            valid_targets=(Link, Comment),\n        ),\n        \"action\": RuleComponent(\n            valid_values={\"approve\", \"remove\", \"spam\", \"filter\", \"report\"},\n            valid_targets=(Link, Comment),\n            component_type=\"action\",\n        ),\n        \"action_reason\": RuleComponent(\n            valid_types=basestring,\n            valid_targets=(Link, Comment),\n            aliases=[\"report_reason\"],\n        ),\n        \"set_flair\": RuleComponent(\n            valid_types=(basestring, list),\n            valid_targets=(Link, Account),\n            component_type=\"action\",\n        ),\n        \"overwrite_flair\": RuleComponent(\n            valid_types=bool,\n            valid_targets=(Link, Account),\n            default=False,\n        ),\n        \"is_top_level\": RuleComponent(\n            valid_types=bool,\n            valid_targets=Comment,\n            component_type=\"check\",\n        ),\n        \"is_edited\": RuleComponent(\n            valid_types=bool,\n            valid_targets=(Link, Comment),\n            component_type=\"check\",\n        ),\n        \"set_locked\": RuleComponent(\n            valid_types=bool,\n            valid_targets=Link,\n            component_type=\"action\",\n        ),\n        \"set_sticky\": RuleComponent(\n            valid_types=(bool, int),\n            valid_values=set(\n                [True, False] + range(1, Subreddit.MAX_STICKIES+1)),\n            valid_targets=Link,\n            component_type=\"action\",\n        ),\n        \"set_nsfw\": RuleComponent(\n            valid_types=bool,\n            valid_targets=Link,\n            component_type=\"action\",\n        ),\n        \"set_contest_mode\": RuleComponent(\n            valid_types=bool,\n            valid_targets=Link,\n            component_type=\"action\",\n        ),\n        \"set_suggested_sort\": RuleComponent(\n            valid_values=CommentSortMenu.suggested_sort_options + (\"best\",),\n            valid_targets=Link,\n            component_type=\"action\",\n        ),\n        \"comment_karma\": RuleComponent(\n            valid_regex=_oper_int_regex,\n            valid_targets=Account,\n            component_type=\"check\",\n        ),\n        \"post_karma\": RuleComponent(\n            valid_regex=_oper_int_regex,\n            valid_targets=Account,\n            component_type=\"check\",\n            aliases=[\"link_karma\"],\n        ),\n        \"combined_karma\": RuleComponent(\n            valid_regex=_oper_int_regex,\n            valid_targets=Account,\n            component_type=\"check\",\n        ),\n        \"account_age\": RuleComponent(\n            valid_regex=_oper_period_regex,\n            valid_targets=Account,\n            component_type=\"check\",\n        ),\n        \"satisfy_any_threshold\": RuleComponent(\n            valid_types=bool,\n            valid_targets=Account,\n            default=False,\n        ),\n        \"is_gold\": RuleComponent(\n            valid_types=bool,\n            valid_targets=Account,\n            component_type=\"check\",\n        ),\n        \"is_submitter\": RuleComponent(\n            valid_types=bool,\n            valid_targets=Account,\n            component_type=\"check\",\n        ),\n        \"is_contributor\": RuleComponent(\n            valid_types=bool,\n            valid_targets=Account,\n            component_type=\"check\",\n        ),\n        \"is_moderator\": RuleComponent(\n            valid_types=bool,\n            valid_targets=Account,\n            component_type=\"check\",\n        ),\n    }\n\n    def __init__(self, target_type, values, parent, approve_banned=True):\n        \"\"\"Create a RuleTarget that applies to objects of type target_type.\n\n        Keyword arguments:\n        target_type -- the type of object this will apply to\n        values -- a dict of the values for each rule component\n        approve_banned -- whether to approve banned users' posts\n        \n        \"\"\"\n        self.target_type = target_type\n        self.parent = parent\n\n        if not values:\n            values = {}\n        else:\n            values = values.copy()\n\n        self.set_values(values)\n\n        # determine patterns that will be matched against fields\n        self.match_patterns = self.get_match_patterns(values)\n        self.matches = {}\n\n        self.approve_banned = approve_banned\n\n    def set_values(self, values):\n        \"\"\"Set values for all possible rule components on the RuleTarget.\n\n        If a value for a valid component for this target_type was not specified,\n        set to the default value for that component. Set attr values to None for\n        all components that only apply to other types.\"\"\"\n        self.checks = set()\n        self.actions = set()\n\n        for key, component in self._potential_components.iteritems():\n            if self.target_type in component.valid_targets:\n                # pop the key and all aliases out of the values\n                # but only keep the first value we find\n                value = None\n                sources = [key] + component.aliases\n                for source in sources:\n                    from_source = values.pop(source, None)\n                    if value is None:\n                        value = from_source\n\n                if value is not None:\n                    if not component.validate(value):\n                        raise AutoModeratorSyntaxError(\n                            \"invalid value for `%s`: `%s`\" % (key, value),\n                            self.parent.yaml,\n                        )\n                            \n                    setattr(self, key, value)\n\n                    if component.component_type == \"check\":\n                        self.checks.add(key)\n                    elif component.component_type == \"action\":\n                        self.actions.add(key)\n                else:\n                    setattr(self, key, component.default)\n            else:\n                if key in values:\n                    raise AutoModeratorRuleTypeError(\n                        \"Can't use `%s` on this type\" % key,\n                        self.parent.yaml,\n                    )\n\n                setattr(self, key, None)\n\n        # special handling for set_flair\n        if self.set_flair is not None:\n            if isinstance(self.set_flair, basestring):\n                self.set_flair = [self.set_flair, \"\"]\n\n            # handle 0 or 1 item lists\n            while len(self.set_flair) < 2:\n                self.set_flair.append(\"\")\n\n            self.set_flair = {\n                \"text\": self.set_flair[0],\n                \"class\": self.set_flair[1],\n            }\n\n        # ugly hack to allow people to use \"best\" instead of \"confidence\"\n        if self.set_suggested_sort == \"best\":\n            self.set_suggested_sort = \"confidence\"\n\n    _match_field_key_regex = re.compile(r\"^(~?[^\\s(]+)\\s*(?:\\((.+)\\))?$\")\n    def parse_match_fields_key(self, key):\n        \"\"\"Parse a key defining a match against fields into its components.\"\"\"\n        matches = self._match_field_key_regex.match(key)\n        if not matches:\n            raise AutoModeratorSyntaxError(\n                \"Invalid search check: `%s`\" % key,\n                self.parent.yaml,\n            )\n        parsed = {}\n        name = matches.group(1)\n\n        all_valid_fields = set.union(*self._match_fields_by_type.values())\n        fields = [field.strip()\n            for field in name.lstrip(\"~\").partition(\"#\")[0].split(\"+\")]\n        for field in fields:\n            if field not in all_valid_fields:\n                raise AutoModeratorSyntaxError(\n                    \"Unknown field: `%s`\" % field,\n                    self.parent.yaml,\n                )\n\n        valid_fields = self._match_fields_by_type[self.target_type]\n        fields = {field for field in fields if field in valid_fields}\n\n        if not fields:\n            raise AutoModeratorRuleTypeError(\n                \"Can't search `%s` on this type\" % key,\n                self.parent.yaml,\n            )\n\n        modifiers = matches.group(2)\n        if modifiers:\n            modifiers = [mod.strip() for mod in modifiers.split(\",\")]\n        else:\n            modifiers = []\n        for mod in modifiers:\n            if mod not in self._match_modifiers:\n                raise AutoModeratorSyntaxError(\n                    \"Unknown modifier `%s` in `%s`\" % (mod, key),\n                    self.parent.yaml,\n                )\n\n        return {\n            \"name\": name,\n            \"fields\": fields,\n            \"modifiers\": modifiers,\n            \"match_success\": not name.startswith(\"~\"),\n        }\n\n    def get_match_patterns(self, values):\n        \"\"\"Generate the regexes used to match against fields.\"\"\"\n        self.match_fields = set()\n        match_patterns = {}\n        \n        for key in values:\n            parsed_key = self.parse_match_fields_key(key)\n\n            # add fields to the list of fields we're going to check\n            self.match_fields |= parsed_key[\"fields\"]\n\n            match_values = values[key]\n            if not match_values:\n                continue\n            if not isinstance(match_values, list):\n                match_values = list((match_values,))\n            # cast all values to strings in case any numbers were included\n            match_values = [unicode(val) for val in match_values]\n\n            # escape regex special chars unless this is a regex\n            if \"regex\" not in parsed_key[\"modifiers\"]:\n                match_values = [re.escape(val) for val in match_values]\n\n            value_str = u\"(%s)\" % \"|\".join(match_values)\n\n            for mod in parsed_key[\"modifiers\"]:\n                if mod in self._match_regexes:\n                    match_mod = mod\n                    break\n            else:\n                if len(parsed_key[\"fields\"]) == 1:\n                    field = list(parsed_key[\"fields\"])[0]\n                    # default to handling subdomains for checks against domain only\n                    if field == \"domain\":\n                        value_str = ur\"(?:.*?\\.)?\" + value_str\n                    match_mod = self._match_field_defaults.get(\n                        field, \"includes-word\")\n                else:\n                    match_mod = \"includes-word\"\n\n            pattern = self._match_regexes[match_mod] % value_str\n\n            flags = re.DOTALL | re.UNICODE\n            if \"case-sensitive\" not in parsed_key[\"modifiers\"]:\n                flags |= re.IGNORECASE\n\n            try:\n                match_patterns[key] = re.compile(pattern, flags)\n            except Exception as e:\n                raise AutoModeratorSyntaxError(\n                    \"Generated an invalid regex for `%s`: %s\" % (key, e),\n                    self.parent.yaml,\n                )\n\n        return match_patterns\n\n    @property\n    def needs_media_data(self):\n        \"\"\"Whether the component requires data from the media embed.\"\"\"\n        for key in self.match_patterns:\n            fields = self.parse_match_fields_key(key)[\"fields\"]\n            if all(field.startswith(\"media_\") for field in fields):\n                return True\n\n        # check if any of the fields that support placeholders have media ones\n        potential_placeholders = [self.action_reason]\n        if self.set_flair:\n            potential_placeholders.extend(self.set_flair.values())\n        if any(text and \"{{media_\" in text for text in potential_placeholders):\n            return True\n\n        return False\n\n    def check_item(self, item, data):\n        \"\"\"Return whether an item satisfies all of the defined conditions.\"\"\"\n        if not self.check_nonpattern_conditions(item, data):\n            return False\n\n        if isinstance(item, Account):\n            if not self.check_account_thresholds(item, data):\n                return False\n\n        if not self.check_match_patterns(item, data):\n            return False\n            \n        return True\n\n    def check_nonpattern_conditions(self, item, data):\n        \"\"\"Check all the non-regex conditions against the item.\"\"\"\n        # check number of reports if necessary\n        if self.reports and item.reported < self.reports:\n            return False\n\n        if hasattr(item, \"body\"):\n            body = self.get_field_value_from_item(item, data, \"body\")\n\n            # check body length restrictions if necessary\n            if (self.body_longer_than is not None or\n                        self.body_shorter_than is not None):\n                # remove non-word chars on either end of the string\n                pattern = re.compile(r'^\\W+', re.UNICODE)\n                body_text = pattern.sub('', body)\n                pattern = re.compile(r'\\W+$', re.UNICODE)\n                body_text = pattern.sub('', body_text)\n\n                if (self.body_longer_than is not None and\n                        len(body_text) <= self.body_longer_than):\n                    return False\n                if (self.body_shorter_than is not None and\n                        len(body_text) >= self.body_shorter_than):\n                    return False\n\n        # check whether it's a reply or top-level comment if necessary\n        if self.is_top_level is not None:\n            item_is_top_level = (not item.parent_id)\n            if self.is_top_level != item_is_top_level:\n                return False\n\n        # check whether it's been edited if necessary\n        if self.is_edited is not None:\n            if self.is_edited != hasattr(item, \"editted\"):\n                return False\n\n        if self.is_submitter is not None:\n            # default to True in case someone happens to check is_submitter\n            # on a submission's author by accident\n            is_submitter = data.get(\"is_submitter\", True)\n            if self.is_submitter != is_submitter:\n                return False\n\n        if self.is_moderator is not None:\n            is_mod = bool(data[\"subreddit\"].is_moderator(item))\n            if is_mod != self.is_moderator:\n                return False\n\n        if self.is_contributor is not None:\n            is_contrib = bool(data[\"subreddit\"].is_contributor(item))\n            if is_contrib != self.is_contributor:\n                return False\n\n        if self.is_gold is not None:\n            if item.gold != self.is_gold:\n                return False\n\n        return True\n\n    def check_account_thresholds(self, account, data):\n        \"\"\"Check karma/age thresholds against an account.\"\"\"\n        thresholds = [\"comment_karma\", \"post_karma\", \"combined_karma\",\n            \"account_age\"]\n        # figure out which thresholds/values we need to check against\n        checks = {}\n        for threshold in thresholds:\n            compare_value = getattr(self, threshold, None)\n            if compare_value is not None:\n                checks[threshold] = str(compare_value)\n\n        # if we don't need to actually check anything, just return True\n        if not checks:\n            return True\n\n        # banned accounts should never satisfy threshold checks\n        if account._spam:\n            return False\n\n        for check, compare_value in checks.iteritems():\n            match = re.match(self._operator_regex, compare_value)\n            if match:\n                operator = match.group(1)\n                compare_value = compare_value[len(operator):].strip()\n            if not match or operator == \"==\":\n                operator = \"=\"\n\n            # special handling for time period comparison value\n            if check == \"account_age\":\n                # if it's just a number, default to days\n                try:\n                    compare_value = int(compare_value)\n                    compare_value = \"%s days\" % compare_value\n                except ValueError:\n                    pass\n\n                compare_value = timeinterval_fromstr(compare_value)\n            else:\n                compare_value = int(compare_value)\n\n            value = self.get_field_value_from_item(account, data, check)\n\n            if operator == \"<\":\n                result = value < compare_value\n            elif operator == \">\":\n                result = value > compare_value\n            elif operator == \"=\":\n                result = value == compare_value\n\n            # if satisfy_any_threshold is True, we can return True as soon\n            # as a single check is successful. If it's False, they all need\n            # to be satisfied, so we can return False as soon as one fails.\n            if result == self.satisfy_any_threshold:\n                return result\n\n        # if we make it to here, the return statement inside the loop was\n        # never triggered, so that means that if satisfy_any_threshold is\n        # True, all the checks must have been False, and if it's False\n        # they all must have been True\n        return (not self.satisfy_any_threshold)\n\n    def check_match_patterns(self, item, data):\n        \"\"\"Check all the regex patterns against the item's field values.\"\"\"\n        if len(self.match_patterns) == 0:\n            return True\n\n        self.matches = {}\n        checked_anything = False\n        for key, match_pattern in self.match_patterns.iteritems():\n            match = None\n            parsed_key = self.parse_match_fields_key(key)\n\n            if isinstance(item, Link) and not item.is_self:\n                # don't check body for link submissions\n                parsed_key[\"fields\"].discard(\"body\")\n            elif isinstance(item, Link) and item.is_self:\n                # don't check url for text submissions\n                parsed_key[\"fields\"].discard(\"url\")\n\n            for source in parsed_key[\"fields\"]:\n                string = self.get_field_value_from_item(item, data, source)\n                match = match_pattern.search(string)\n                checked_anything = True\n\n                if match:\n                    self.matches[parsed_key[\"name\"]] = match\n                    break\n\n            if bool(match) != parsed_key[\"match_success\"]:\n                return False\n\n        # if we didn't actually check anything, that means that all checks\n        # must have been discarded (for example, url checks when the item\n        # is a self-post). That should be considered a failure.\n        if checked_anything:\n            return True\n        else:\n            return False\n\n    def perform_actions(self, item, data):\n        \"\"\"Execute the defined actions on the item.\"\"\"\n        # only approve if it's currently removed or reported, and hasn't\n        # been removed by a moderator\n        ban_info = getattr(item, \"ban_info\", {})\n        mod_banned = ban_info.get(\"moderator_banned\")\n        should_approve = ((item._spam and not mod_banned) or \n            (self.reports and item.reported))\n        if self.action == \"approve\" and should_approve:\n            approvable_author = not data[\"author\"]._spam or self.approve_banned\n            if approvable_author:\n                # TODO: shouldn't need to set train_spam/insert values\n                was_removed = item._spam\n                admintools.unspam(item, moderator_unbanned=True,\n                    unbanner=ACCOUNT.name, train_spam=True, insert=item._spam)\n\n                log_action = None\n                if isinstance(item, Link):\n                    log_action = \"approvelink\"\n                elif isinstance(item, Comment):\n                    log_action = \"approvecomment\"\n\n                if log_action:\n                    if self.action_reason:\n                        reason = replace_placeholders(\n                            self.action_reason, data, self.parent.matches)\n                    else:\n                        reason = \"unspam\" if was_removed else \"approved\"\n                    ModAction.create(data[\"subreddit\"], ACCOUNT, log_action,\n                        target=item, details=reason)\n\n                g.stats.simple_event(\"automoderator.approve\")\n\n        if self.action in {\"remove\", \"spam\", \"filter\"}:\n            spam = (self.action == \"spam\")\n            keep_in_modqueue = (self.action == \"filter\")\n            admintools.spam(\n                item,\n                auto=keep_in_modqueue,\n                moderator_banned=True,\n                banner=ACCOUNT.name,\n                train_spam=spam,\n            )\n\n            # TODO: shouldn't need to do all of this here\n            log_action = None\n            if isinstance(item, Link):\n                log_action = \"removelink\"\n            elif isinstance(item, Comment):\n                log_action = \"removecomment\"\n                queries.unnotify(item)\n\n            if log_action:\n                if self.action_reason:\n                    reason = replace_placeholders(\n                        self.action_reason, data, self.parent.matches)\n                else:\n                    reason = \"spam\" if spam else \"remove\"\n                ModAction.create(data[\"subreddit\"], ACCOUNT, log_action,\n                    target=item, details=reason)\n\n            g.stats.simple_event(\"automoderator.%s\" % self.action)\n\n        if self.action == \"report\":\n            if self.action_reason:\n                reason = replace_placeholders(\n                    self.action_reason, data, self.parent.matches)\n            else:\n                reason = None\n            Report.new(ACCOUNT, item, reason)\n            admintools.report(item)\n\n            g.stats.simple_event(\"automoderator.report\")\n\n        if self.set_nsfw is not None:\n            if item.over_18 != self.set_nsfw:\n                item.over_18 = self.set_nsfw\n                item._commit()\n                # TODO: shouldn't need to do this here\n                log_details = None\n                if not self.set_nsfw:\n                    log_details = \"remove\"\n                ModAction.create(data[\"subreddit\"], ACCOUNT, \"marknsfw\",\n                    target=item, details=log_details)\n                item.update_search_index()\n\n        if self.set_contest_mode is not None:\n            if item.contest_mode != self.set_contest_mode:\n                item.contest_mode = self.set_contest_mode\n                item._commit()\n\n        if self.set_locked is not None:\n            if item.locked != self.set_locked:\n                item.locked = self.set_locked\n                item._commit()\n\n                log_action = 'lock' if self.set_locked else 'unlock'\n                ModAction.create(data[\"subreddit\"], ACCOUNT, log_action,\n                    target=item)\n\n        if self.set_sticky is not None:\n            if item.is_stickied(data[\"subreddit\"]) != bool(self.set_sticky):\n                if self.set_sticky:\n                    # if set_sticky is a bool, don't specify a slot\n                    if isinstance(self.set_sticky, bool):\n                        num = None\n                    else:\n                        num = self.set_sticky\n\n                    data[\"subreddit\"].set_sticky(item, ACCOUNT, num)\n                else:\n                    data[\"subreddit\"].remove_sticky(item, ACCOUNT)\n\n        if self.set_suggested_sort is not None:\n            if not item.suggested_sort:\n                item.suggested_sort = self.set_suggested_sort\n                item._commit()\n\n                # TODO: shouldn't need to do this here\n                ModAction.create(data[\"subreddit\"], ACCOUNT,\n                    action=\"setsuggestedsort\", target=item)\n\n        if self.set_flair:\n            # don't overwrite existing flair unless that was specified\n            can_update_flair = False\n            if isinstance(item, Link):\n                if item.flair_text or item.flair_css_class:\n                    can_update_flair = self.overwrite_flair\n                else:\n                    can_update_flair = True\n            elif isinstance(item, Account):\n                if data[\"subreddit\"].is_flair(item):\n                    can_update_flair = self.overwrite_flair\n                else:\n                    can_update_flair = True\n\n            if can_update_flair:\n                text = replace_placeholders(\n                    self.set_flair[\"text\"], data, self.parent.matches)\n                cls = replace_placeholders(\n                    self.set_flair[\"class\"], data, self.parent.matches)\n\n                # apply same limits as API to text and class\n                text = text[:64]\n                cls = re.sub(r\"[^\\w -]\", \"\", cls)\n                classes = cls.split()[:10]\n                classes = [cls[:100] for cls in classes]\n                cls = \" \".join(classes)\n\n                if isinstance(item, Link):\n                    item.set_flair(text, cls)\n                elif isinstance(item, Account):\n                    item.set_flair(data[\"subreddit\"], text, cls)\n\n                g.stats.simple_event(\"automoderator.set_flair\")\n\n    def get_field_value_from_item(self, item, data, field):\n        \"\"\"Get a field value from the item to check against.\"\"\"\n        value = ''\n        if field == 'id':\n            value = item._id36\n        elif field == 'body':\n            # pull out the item's body and remove blockquotes if necessary\n            body = item.body\n            if self.ignore_blockquotes:\n                body = '\\n'.join(\n                    line for line in body.splitlines()\n                    if not line.startswith('>') and\n                    len(line) > 0)\n            value = body\n        elif field == 'domain':\n            if not item.is_self:\n                value = item.link_domain()\n            else:\n                value = \"self.\" + data[\"subreddit\"].name\n        elif (field.startswith('media_') and\n                getattr(item, 'media_object', None)):\n            try:\n                if field == 'media_author':\n                    value = item.media_object['oembed']['author_name']\n                elif field == 'media_author_url':\n                    value = item.media_object['oembed']['author_url']\n                elif field == 'media_description':\n                    value = item.media_object['oembed']['description']\n                elif field == 'media_title':\n                    value = item.media_object['oembed']['title']\n            except KeyError:\n                value = ''\n        elif field == \"account_age\":\n            value = item._age\n        elif field == \"post_karma\":\n            value = max(item.link_karma, g.link_karma_display_floor)\n        elif field == \"comment_karma\":\n            value = max(item.comment_karma, g.comment_karma_display_floor)\n        elif field == \"combined_karma\":\n            post = self.get_field_value_from_item(item, data, \"post_karma\")\n            comment = self.get_field_value_from_item(item, data, \"comment_karma\")\n            value = post + comment\n        elif field == \"flair_text\" and isinstance(item, Account):\n            value = item.flair_text(data[\"subreddit\"]._id, obey_disabled=True)\n        elif field == \"flair_css_class\" and isinstance(item, Account):\n            value = item.flair_css_class(\n                data[\"subreddit\"]._id, obey_disabled=True)\n        else:\n            value = getattr(item, field, \"\")\n\n        return value\n\n\nclass Rule(object):\n    \"\"\"The overall rule, made up of 1 or more RuleTargets.\"\"\"\n    _valid_type_map = {\n        \"comment\": Comment,\n        \"submission\": Link,\n        \"link submission\": Link,\n        \"text submission\": Link,\n    }\n\n    _valid_components = {\n        \"type\": RuleComponent(\n            valid_values=set(_valid_type_map.keys() + [\"any\"]),\n            default=\"any\",\n            component_type=\"check\",\n        ),\n        \"priority\": RuleComponent(valid_types=int, default=0),\n        \"moderators_exempt\": RuleComponent(valid_types=bool),\n        \"comment\": RuleComponent(valid_types=basestring, component_type=\"action\"),\n        \"comment_stickied\": RuleComponent(valid_types=bool, default=False),\n        \"modmail\": RuleComponent(valid_types=basestring, component_type=\"action\"),\n        \"modmail_subject\": RuleComponent(\n            valid_types=basestring,\n            default=\"AutoModerator notification\",\n        ),\n        \"message\": RuleComponent(valid_types=basestring, component_type=\"action\"),\n        \"message_subject\": RuleComponent(\n            valid_types=basestring,\n            default=\"AutoModerator notification\",\n        ),\n    }\n\n    def __init__(self, values, yaml_source=None):\n        values = lowercase_keys_recursively(values)\n\n        if yaml_source:\n            self.yaml = yaml_source\n        else:\n            self.yaml = yaml.dump(values)\n\n        self.unique_id = md5(self.yaml.encode(\"utf-8\")).hexdigest()\n\n        self.checks = set()\n        self.actions = set()\n\n        # pop off the values that are special for the top level\n        for key, component in self._valid_components.iteritems():\n            if key in values:\n                value = values.pop(key)\n                if not component.validate(value):\n                    raise AutoModeratorSyntaxError(\n                        \"invalid value for `%s`: `%s`\" % (key, value),\n                        self.yaml,\n                    )\n                setattr(self, key, value)\n\n                if component.component_type == \"check\":\n                    self.checks.add(key)\n                elif component.component_type == \"action\":\n                    self.actions.add(key)\n            else:\n                setattr(self, key, component.default)\n\n        self.base_target_type = self._valid_type_map[self.type]\n\n        self.targets = {}\n\n        author = values.pop(\"author\", None)\n        if not isinstance(author, dict):\n            # if they just specified string(s) for author\n            # that's the same as checking against name\n            if isinstance(author, (list, basestring)):\n                author = {\"name\": author}\n            else:\n                author = {}\n\n        # support string(s) for ~author as well\n        not_author = values.pop(\"~author\", None)\n        if isinstance(not_author, (list, basestring)):\n            author[\"~name\"] = not_author\n\n        approve_banned = False\n        if author:\n            self.targets[\"author\"] = RuleTarget(Account, author, self)\n            # only approve banned users' posts if an author name check is done\n            approve_banned = (\"name\" in self.targets[\"author\"].match_fields)\n\n        parent_submission = values.pop(\"parent_submission\", None)\n        if parent_submission:\n            if self.base_target_type == Comment:\n                target = RuleTarget(Link, parent_submission, self)\n                self.targets[\"parent_submission\"] = target\n            else:\n                raise AutoModeratorRuleTypeError(\n                    \"can't specify `parent_submission` on a submission\",\n                    self.yaml,\n                )\n\n        # TODO: in the future, \"imported\" rules can also exist here as targets\n\n        # send all the remaining values through to the base target\n        self.targets[\"base\"] = RuleTarget(\n            self.base_target_type,\n            values,\n            self,\n            approve_banned=approve_banned,\n        )\n\n    @property\n    def is_removal_rule(self):\n        \"\"\"Whether the rule could result in removing the item.\"\"\"\n        return self.targets[\"base\"].action in {\"spam\", \"remove\", \"filter\"}\n\n    @property\n    def is_inapplicable_to_mods(self):\n        \"\"\"Whether the rule should not be applied to moderators' posts.\"\"\"\n        if self.moderators_exempt is not None:\n            return self.moderators_exempt\n\n        if self.is_removal_rule or self.targets[\"base\"].action == \"report\":\n            return True\n\n        return False\n\n    @property\n    def is_unrepeatable(self):\n        \"\"\"Whether repeating the rule's actions is undesirable.\"\"\"\n        # we don't want to repeatedly execute rules that post or message\n        if self.comment or self.modmail or self.message:\n            return True\n\n        # duplicate reports won't go through anyway\n        if self.targets[\"base\"].action == \"report\":\n            return True\n\n        return False\n\n    @property\n    def needs_media_data(self):\n        \"\"\"Whether the rule requires data from the media embed.\"\"\"\n        for attr in (\"comment\", \"modmail\", \"message\"):\n            text = getattr(self, attr, None)\n            if text and \"{{media_\" in text:\n                return True\n\n        if any(comp.needs_media_data for comp in self.targets.values()):\n            return True\n\n        return False\n\n    @property\n    def matches(self):\n        return self.targets[\"base\"].matches\n\n    def has_any_checks(self, targets_only=False):\n        for target in self.targets.values():\n            if target.checks or target.match_fields:\n                return True\n\n        if targets_only:\n            return False\n\n        return bool(self.checks)\n\n    def has_any_actions(self, targets_only=False):\n        for target in self.targets.values():\n            if target.actions:\n                return True\n\n        if targets_only:\n            return False\n\n        return bool(self.actions)\n\n    def get_target_item(self, item, data, key):\n        \"\"\"Return the subject for a particular target's conditions/actions.\"\"\"\n        if key == \"base\":\n            return item\n        elif key == \"author\":\n            return data[\"author\"]\n        elif key == \"parent_submission\":\n            return data[\"link\"]\n\n    def item_is_correct_type(self, item):\n        \"\"\"Check that the item is the correct type to apply this rule to.\"\"\"\n        if not isinstance(item, self.base_target_type):\n            return False\n\n        if self.type == \"link submission\":\n            return not item.is_self\n        elif self.type == \"text submission\":\n            return item.is_self\n\n        return True\n\n    def should_check_item(self, item, data):\n        \"\"\"Return if it is necessary to check this rule against the item.\"\"\"\n        if not self.item_is_correct_type(item):\n            return False\n\n        # don't check comments made by this account to prevent loops\n        if isinstance(item, Comment) and data[\"author\"] == ACCOUNT:\n            return False\n\n        if (self.is_inapplicable_to_mods and\n                data[\"subreddit\"].is_moderator(data[\"author\"])):\n            return False\n\n        # if the item is already removed by a moderator, no need to\n        # check any rules\n        ban_info = getattr(item, \"ban_info\", {})\n        if item._spam and ban_info.get(\"moderator_banned\"):\n            return False\n\n        if self.is_removal_rule:\n            # don't consider removing items another moderator has\n            # already approved\n            if (getattr(item, \"verdict\", \"\").endswith(\"-approved\") and\n                    ban_info.get(\"unbanner\") != ACCOUNT.name):\n                return False\n\n        if self.needs_media_data:\n            if isinstance(item, Link) and not item.media_object:\n                return False\n            elif isinstance(item, Comment) and not data[\"link\"].media_object:\n                return False\n\n        return True\n\n    def check_item(self, item, data):\n        \"\"\"Return whether the item satisfies all the targets' conditions.\"\"\"\n        if not self.should_check_item(item, data):\n            return False\n\n        g.stats.simple_event(\"automoderator.check_rule\")\n\n        for key, target in self.targets.iteritems():\n            target_item = self.get_target_item(item, data, key)\n            if not target.check_item(target_item, data):\n                return False\n\n        return True\n\n    def perform_actions(self, item, data):\n        \"\"\"Execute all the rule's actions against the item.\"\"\"\n        for key, target in self.targets.iteritems():\n            target_item = self.get_target_item(item, data, key)\n            target.perform_actions(target_item, data)\n\n        if self.comment:\n            comment = self.build_message(self.comment, item, data, disclaimer=True)\n\n            # TODO: shouldn't have to do all this manually\n            if isinstance(item, Comment):\n                link = data[\"link\"]\n                parent_comment = item\n            else:\n                link = item\n                parent_comment = None\n            new_comment, inbox_rel = Comment._new(\n                ACCOUNT, link, parent_comment, comment, None)\n            new_comment.distinguished = \"yes\"\n            new_comment.sendreplies = False\n            new_comment._commit()\n\n            # If the comment isn't going to be put into the user's inbox\n            # due to them having sendreplies disabled, force it. For a normal\n            # mod, distinguishing the comment would do this, but it doesn't\n            # happen here since we're setting .distinguished directly.\n            if isinstance(item, Link) and not inbox_rel:\n                inbox_rel = Inbox._add(data[\"author\"], new_comment, \"selfreply\")\n\n            queries.new_comment(new_comment, inbox_rel)\n\n            if self.comment_stickied:\n                try:\n                    link.set_sticky_comment(new_comment, set_by=ACCOUNT)\n                except RedditError:\n                    # This comment isn't valid to set to sticky, ignore\n                    pass\n\n            g.stats.simple_event(\"automoderator.comment\")\n\n        if self.modmail:\n            message = self.build_message(self.modmail, item, data, permalink=True)\n            subject = replace_placeholders(\n                self.modmail_subject, data, self.matches)\n            subject = subject[:100]\n\n            new_message, inbox_rel = Message._new(ACCOUNT, data[\"subreddit\"],\n                subject, message, None)\n            new_message.distinguished = \"yes\"\n            new_message._commit()\n            queries.new_message(new_message, inbox_rel)\n\n            g.stats.simple_event(\"automoderator.modmail\")\n\n        if self.message and not data[\"author\"]._deleted:\n            message = self.build_message(self.message, item, data,\n                disclaimer=True, permalink=True)\n            subject = replace_placeholders(\n                self.message_subject, data, self.matches)\n            subject = subject[:100]\n\n            new_message, inbox_rel = Message._new(ACCOUNT, data[\"author\"],\n                subject, message, None)\n            queries.new_message(new_message, inbox_rel)\n\n            g.stats.simple_event(\"automoderator.message\")\n\n        PerformedRulesByThing.mark_performed(item, self)\n\n    def build_message(self, text, item, data, disclaimer=False, permalink=False):\n        \"\"\"Generate the text to post as a comment or send as a message.\"\"\"\n        message = text\n        if disclaimer:\n            message = \"%s\\n\\n%s\" % (message, DISCLAIMER)\n        if permalink and \"{{permalink}}\" not in message:\n            message = \"{{permalink}}\\n\\n%s\" % message\n        message = replace_placeholders(message, data, self.matches)\n\n        message = VMarkdown('').run(message)\n\n        return message[:10000]\n\n\ndef run():\n    @g.stats.amqp_processor(\"automoderator_q\")\n    def process_message(msg):\n        if not ACCOUNT:\n            return\n\n        fullname = msg.body\n        with g.make_lock(\"automoderator\", \"automod_\" + fullname, timeout=5):\n            item = Thing._by_fullname(fullname, data=True)\n            if not isinstance(item, (Link, Comment)):\n                return\n\n            subreddit = item.subreddit_slow\n            \n            wiki_page_id = wiki_id(subreddit._id36, \"config/automoderator\")\n            wiki_page_fullname = \"WikiPage_%s\" % wiki_page_id\n            last_edited = LastModified.get(wiki_page_fullname, \"Edit\")\n            if not last_edited:\n                return\n\n            # initialize rules for the subreddit if we haven't already\n            # or if the page has been edited since we last initialized\n            need_to_init = False\n            if subreddit._id not in rules_by_subreddit:\n                need_to_init = True\n            else:\n                rules = rules_by_subreddit[subreddit._id]\n                if last_edited > rules.init_time:\n                    need_to_init = True\n\n            if need_to_init:\n                timer = g.stats.get_timer(\"automoderator.init_ruleset\")\n                timer.start()\n\n                wp = WikiPage.get(subreddit, \"config/automoderator\")\n                timer.intermediate(\"get_wiki_page\")\n\n                try:\n                    rules = Ruleset(wp.content, timer)\n                except (AutoModeratorSyntaxError, AutoModeratorRuleTypeError):\n                    print \"ERROR: Invalid config in /r/%s\" % subreddit.name\n                    return\n\n                rules_by_subreddit[subreddit._id] = rules\n\n                timer.stop()\n\n            if not rules:\n                return\n\n            try:\n                TimeoutFunction(rules.apply_to_item, 2)(item)\n                print \"Checked %s from /r/%s\" % (item, subreddit.name)\n            except TimeoutFunctionException:\n                print \"Timed out on %s from /r/%s\" % (item, subreddit.name)\n            except KeyboardInterrupt:\n                raise\n            except:\n                print \"Error on %s from /r/%s\" % (item, subreddit.name)\n                print traceback.format_exc()\n\n    amqp.consume_items('automoderator_q', process_message, verbose=False)\n"
  },
  {
    "path": "r2/r2/lib/base.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom pylons import request, session, config, response\nfrom pylons import tmpl_context as c\nfrom pylons import app_globals as g\nfrom pylons.controllers import WSGIController\nfrom pylons.i18n import N_, _, ungettext, get_lang\nfrom webob.exc import HTTPException, status_map\nfrom r2.lib.filters import spaceCompress, _force_unicode\nfrom r2.lib.template_helpers import get_domain\nfrom r2.lib.utils import Agent\nfrom utils import string2js, read_http_date\n\nimport re, hashlib\nfrom Cookie import CookieError\nfrom urllib import quote\nimport urllib2\nimport sys\n\n\n#TODO hack\nimport logging\nfrom r2.lib.utils import UrlParser, query_string\nlogging.getLogger('scgi-wsgi').setLevel(logging.CRITICAL)\n\n\ndef is_local_address(ip):\n    # TODO: support the /20 and /24 private networks? make this configurable?\n    return ip.startswith('10.') or ip == \"127.0.0.1\"\n\ndef abort(code_or_exception=None, detail=\"\", headers=None, comment=None,\n          **kwargs):\n    \"\"\"Raise an HTTPException and save it in environ for use by error pages.\"\"\"\n    # Pylons 0.9.6 makes it really hard to get your raised HTTPException,\n    # so this helper implements it manually using a familiar syntax.\n    # FIXME: when we upgrade Pylons, we can replace this with raise\n    #        and access environ['pylons.controller.exception']\n    # NOTE: when we say \"upgrade Pylons\" we mean to 0.10+\n    if isinstance(code_or_exception, HTTPException):\n        exc = code_or_exception\n    else:\n        if type(code_or_exception) is type and issubclass(code_or_exception,\n                                                          HTTPException):\n            exc_cls = code_or_exception\n        else:\n            exc_cls = status_map[code_or_exception]\n        exc = exc_cls(detail, headers, comment, **kwargs)\n    request.environ['r2.controller.exception'] = exc\n    raise exc\n\nclass BaseController(WSGIController):\n    def __before__(self):\n        \"\"\"Perform setup tasks before the controller method/action is executed.\n\n        Called by WSGIController.__call__.\n\n        \"\"\"\n\n        # we override this here to ensure that this header, and only this\n        # header, is trusted to reduce the number of potential\n        # misconfigurations between wsgi application servers (e.g. gunicorn\n        # which trusts three different headers out of the box for this) and\n        # haproxy (which won't clean out bad headers by default)\n        forwarded_proto = request.environ.get(\"HTTP_X_FORWARDED_PROTO\", \"http\")\n        forwarded_proto = forwarded_proto.lower()\n        assert forwarded_proto in (\"http\", \"https\")\n        request.environ[\"wsgi.url_scheme\"] = forwarded_proto\n\n        forwarded_for = request.environ.get('HTTP_X_FORWARDED_FOR', ())\n        remote_addr = request.environ.get('REMOTE_ADDR')\n\n        request.via_cdn = False\n        cdn_ip = g.cdn_provider.get_client_ip(request.environ)\n        if cdn_ip:\n            request.ip = cdn_ip\n            request.via_cdn = True\n        elif (g.trust_local_proxies and\n                forwarded_for and\n                is_local_address(remote_addr)):\n            request.ip = forwarded_for.split(',')[-1]\n        else:\n            request.ip = request.environ['REMOTE_ADDR']\n\n        try:\n            # webob can't handle non utf-8 encoded query strings or paths\n            request.params\n            request.path\n        except UnicodeDecodeError:\n            abort(400)\n\n        #if x-dont-decode is set, pylons won't unicode all the parameters\n        if request.environ.get('HTTP_X_DONT_DECODE'):\n            request.charset = None\n\n        request.referer = request.environ.get('HTTP_REFERER')\n        request.user_agent = request.environ.get('HTTP_USER_AGENT')\n        request.parsed_agent = Agent.parse(request.user_agent)\n        request.fullpath = request.environ.get('FULLPATH', request.path)\n        request.fullurl = request.host_url + request.fullpath\n        request.port = request.environ.get('request_port')\n\n        if_modified_since = request.environ.get('HTTP_IF_MODIFIED_SINCE')\n        if if_modified_since:\n            request.if_modified_since = read_http_date(if_modified_since)\n        else:\n            request.if_modified_since = None\n\n        self.fix_cookie_header()\n        self.pre()\n\n    def __after__(self):\n        self.post()\n\n    def __call__(self, environ, start_response):\n        # as defined by routing rules in in routing.py, a request to\n        # /api/do_something is routed to the ApiController's do_something()\n        # method (action). Rewrite this to include the HTTP verb which is the\n        # real name of the controller method: GET_do_something().\n        action = request.environ['pylons.routes_dict'].get('action')\n        if action:\n            meth = request.method.upper()\n            if meth == 'HEAD':\n                meth = 'GET'\n\n            if (meth == 'OPTIONS' and\n                    self._get_action_handler(action, meth) is None):\n                handler_name = meth\n            else:\n                handler_name = meth + '_' + action\n\n            request.environ['pylons.routes_dict']['action_name'] = action\n            request.environ['pylons.routes_dict']['action'] = handler_name\n\n        # WSGIController.__call__ will run __before__() and then execute the\n        # controller method via environ['pylons.routes_dict']['action']\n        return WSGIController.__call__(self, environ, start_response)\n\n    def pre(self): pass\n    def post(self): pass\n\n    def fix_cookie_header(self):\n        \"\"\"\n        Detect and drop busted `Cookie` headers\n\n        We get all sorts of invalid `Cookie` headers. Just one example:\n\n            Cookie: fo,o=bar; expires=1;\n\n        Normally you'd do this in middleware, but `webob.cookie`'s API\n        is fairly volatile while `webob.request`'s isn't. It's easier to\n        do this once we've got a valid `Request` object.\n        \"\"\"\n        try:\n            # Just accessing this will cause `webob` to attempt a parse,\n            # telling us if the header's broken.\n            request.cookies\n        except (CookieError, KeyError):\n            # Someone sent a janked up cookie header, and `webob` exploded.\n            # just pretend we didn't receive one at all.\n            cookie_val = request.environ.get('HTTP_COOKIE', '')\n            request.environ['HTTP_COOKIE'] = ''\n            g.log.warning(\"Cleared bad cookie header: %r\" % cookie_val)\n            g.stats.simple_event(\"cookie.bad_cookie_header\")\n\n    def _get_action_handler(self, name=None, method=None):\n        name = name or request.environ[\"pylons.routes_dict\"][\"action_name\"]\n        method = method or request.method\n        action = method + \"_\" + name\n        return getattr(self, action, None)\n\n    @classmethod\n    def format_output_url(cls, url, **kw):\n        \"\"\"\n        Helper method used during redirect to ensure that the redirect\n        url (assisted by frame busting code or javasctipt) will point\n        to the correct domain and not have any extra dangling get\n        parameters.  The extensions are also made to match and the\n        resulting url is utf8 encoded.\n\n        Node: for development purposes, also checks that the port\n        matches the request port\n        \"\"\"\n        preserve_extension = kw.pop(\"preserve_extension\", True)\n        u = UrlParser(url)\n\n        if u.is_reddit_url():\n            # make sure to pass the port along if not 80\n            if not kw.has_key('port'):\n                kw['port'] = request.port\n\n            # make sure the extensions agree with the current page\n            if preserve_extension and c.extension:\n                u.set_extension(c.extension)\n\n        # unparse and encode it un utf8\n        rv = _force_unicode(u.unparse()).encode('utf8')\n        if \"\\n\" in rv or \"\\r\" in rv:\n            abort(400)\n        return rv\n\n    @classmethod\n    def intermediate_redirect(cls, form_path, sr_path=True, fullpath=None):\n        \"\"\"\n        Generates a /login or /over18 redirect from the specified or current\n        fullpath, after having properly reformated the path via\n        format_output_url.  The reformatted original url is encoded\n        and added as the \"dest\" parameter of the new url.\n        \"\"\"\n        from r2.lib.template_helpers import add_sr\n        params = dict(dest=cls.format_output_url(fullpath or request.fullurl))\n        if c.extension == \"widget\" and request.GET.get(\"callback\"):\n            params['callback'] = request.GET.get(\"callback\")\n\n        path = add_sr(cls.format_output_url(form_path) +\n                      query_string(params), sr_path=sr_path)\n        abort(302, location=path)\n\n    @classmethod\n    def redirect(cls, dest, code=302, preserve_extension=True):\n        \"\"\"\n        Reformats the new Location (dest) using format_output_url and\n        sends the user to that location with the provided HTTP code.\n        \"\"\"\n        dest = cls.format_output_url(dest or \"/\",\n                                     preserve_extension=preserve_extension)\n        response.status_int = code\n        response.headers['Location'] = dest\n\n\nclass EmbedHandler(urllib2.BaseHandler, urllib2.HTTPHandler,\n                   urllib2.HTTPErrorProcessor, urllib2.HTTPDefaultErrorHandler):\n\n    def http_redirect(self, req, fp, code, msg, hdrs):\n        to = hdrs['Location']\n        h = urllib2.HTTPRedirectHandler()\n        r = h.redirect_request(req, fp, code, msg, hdrs, to)\n        return embedopen.open(r)\n\n    http_error_301 = http_redirect\n    http_error_302 = http_redirect\n    http_error_303 = http_redirect\n    http_error_307 = http_redirect\n\nembedopen = urllib2.OpenerDirector()\nembedopen.add_handler(EmbedHandler())\n\ndef proxyurl(url):\n    r = urllib2.Request(url, None, {})\n    content = embedopen.open(r).read()\n    return content\n"
  },
  {
    "path": "r2/r2/lib/baseplate_integration.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\"\"\"Transitional integration with Baseplate.\n\nThis module provides basic transitional integration with Baseplate. Its intent\nis to integrate baseplate-provided functionality (like thrift clients) into\nr2's existing diagnostics infrastructure. It is not meant to be the last word\non r2+baseplate; ideally r2 will move towards using more of baseplate rather\nthan its own implementations.\n\n\"\"\"\n\nimport functools\nimport sys\n\nfrom baseplate.core import BaseplateObserver, ServerSpanObserver, SpanObserver\nfrom pylons import app_globals as g, tmpl_context as c\n\n\ndef make_server_span(span_name):\n    c.trace = g.baseplate.make_server_span(context=c, name=span_name)\n    return c.trace\n\n\ndef finish_server_span():\n    c.trace.finish()\n\n\ndef with_server_span(name):\n    \"\"\"A decorator for functions that run outside request context.\n\n    This will add a server span which starts just before invocation of the\n    function and ends immediately after. The context (`c`) will have all\n    appropriate baseplate stuff added to it, and metrics will be flushed when\n    the function returns.\n\n    This is useful for functions run in cron jobs or from the shell. Note that\n    you cannot call a function wrapped with this decorator from within an\n    existing server span.\n\n    \"\"\"\n    def with_server_span_decorator(fn):\n        @functools.wraps(fn)\n        def with_server_span_wrapper(*args, **kwargs):\n            assert not c.trace, \"called while already in a server span\"\n\n            try:\n                with make_server_span(name):\n                    return fn(*args, **kwargs)\n            finally:\n                g.stats.flush()\n        return with_server_span_wrapper\n    return with_server_span_decorator\n\n\n# this is just for backwards compatibility\nwith_root_span = with_server_span\n\n\nclass R2BaseplateObserver(BaseplateObserver):\n    def on_server_span_created(self, context, server_span):\n        observer = R2ServerSpanObserver()\n        server_span.register(observer)\n\n\nclass R2ServerSpanObserver(ServerSpanObserver):\n    def on_child_span_created(self, span):\n        observer = R2SpanObserver(span.name)\n        span.register(observer)\n\n\nclass R2SpanObserver(SpanObserver):\n    def __init__(self, span_name):\n        self.metric_name = \"providers.{}\".format(span_name)\n        self.timer = g.stats.get_timer(self.metric_name)\n\n    def on_start(self):\n        self.timer.start()\n\n    def on_finish(self, exc_info):\n        self.timer.stop()\n\n        if exc_info:\n            error = exc_info[1]\n            g.log.warning(\"%s: error: %s\", self.metric_name, error)\n            g.stats.simple_event(\"{}.error\".format(self.metric_name))\n"
  },
  {
    "path": "r2/r2/lib/butler.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom pylons import tmpl_context as c\nfrom pylons import app_globals as g\n\nfrom r2.lib.db import queries\nfrom r2.lib.db.tdb_sql import CreationError\nfrom r2.lib import amqp\nfrom r2.lib.utils import extract_user_mentions\nfrom r2.models import query_cache, Thing, Comment, Account, Inbox, NotFound\n\n\ndef notify_mention(user, thing):\n    try:\n        inbox_rel = Inbox._add(user, thing, \"mention\")\n    except CreationError:\n        # this mention was already inserted, ignore it\n        g.log.error(\"duplicate mention for (%s, %s)\", user, thing)\n        return\n\n    with query_cache.CachedQueryMutator() as m:\n        m.insert(queries.get_inbox_comment_mentions(user), [inbox_rel])\n        queries.set_unread(thing, user, unread=True, mutator=m)\n\n\ndef remove_mention_notification(mention):\n    inbox_owner = mention._thing1\n    thing = mention._thing2\n    with query_cache.CachedQueryMutator() as m:\n        m.delete(queries.get_inbox_comment_mentions(inbox_owner), [mention])\n        queries.set_unread(thing, inbox_owner, unread=False, mutator=m)\n\n\ndef readd_mention_notification(mention):\n    \"\"\"Reinsert into inbox after a comment has been unspammed\"\"\"\n    inbox_owner = mention._thing1\n    thing = mention._thing2\n    with query_cache.CachedQueryMutator() as m:\n        m.insert(queries.get_inbox_comment_mentions(inbox_owner), [mention])\n        unread = getattr(mention, 'unread_preremoval', True)\n        queries.set_unread(thing, inbox_owner, unread=unread, mutator=m)\n\n\ndef monitor_mentions(comment):\n    if comment._spam or comment._deleted:\n        return\n\n    sender = comment.author_slow\n    if getattr(sender, \"butler_ignore\", False):\n        # this is an account that generates false notifications, e.g.\n        # LinkFixer\n        return\n\n    if sender.in_timeout:\n        return\n\n    subreddit = comment.subreddit_slow\n    usernames = extract_user_mentions(comment.body)\n    inbox_class = Inbox.rel(Account, Comment)\n\n    # If more than our allowed number of mentions were passed, don't highlight\n    # any of them.\n    if len(usernames) > g.butler_max_mentions:\n        return\n\n    # Subreddit.can_view stupidly requires this.\n    c.user_is_loggedin = True\n\n    for username in usernames:\n        try:\n            account = Account._by_name(username)\n        except NotFound:\n            continue\n\n        # most people are aware of when they mention themselves.\n        if account == sender:\n            continue\n\n        # bail out if that user has the feature turned off\n        if not account.pref_monitor_mentions:\n            continue\n\n        # don't notify users of things they can't see\n        if not subreddit.can_view(account):\n            continue\n\n        # don't notify users when a person they've blocked mentions them\n        if account.is_enemy(sender):\n            continue\n\n        # ensure this comment isn't already in the user's inbox already\n        rels = inbox_class._fast_query(\n            account,\n            comment,\n            (\"inbox\", \"selfreply\", \"mention\"),\n        )\n        if filter(None, rels.values()):\n            continue\n\n        notify_mention(account, comment)\n\n\ndef run():\n    @g.stats.amqp_processor(\"butler_q\")\n    def process_message(msg):\n        fname = msg.body\n        item = Thing._by_fullname(fname, data=True)\n        monitor_mentions(item)\n\n    amqp.consume_items(\"butler_q\",\n                       process_message,\n                       verbose=True)\n"
  },
  {
    "path": "r2/r2/lib/c/filters.c",
    "content": "/*\n* The contents of this file are subject to the Common Public Attribution\n* License Version 1.0. (the \"License\"); you may not use this file except in\n* compliance with the License. You may obtain a copy of the License at\n* http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n* License Version 1.1, but Sections 14 and 15 have been added to cover use of\n* software over a computer network and provide for limited attribution for the\n* Original Developer. In addition, Exhibit A has been modified to be consistent\n* with Exhibit B.\n*\n* Software distributed under the License is distributed on an \"AS IS\" basis,\n* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n* the specific language governing rights and limitations under the License.\n*\n* The Original Code is reddit.\n*\n* The Original Developer is the Initial Developer.  The Initial Developer of\n* the Original Code is reddit Inc.\n*\n* All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n* Inc. All Rights Reserved.\n******************************************************************************/\n\n#include <Python.h>\n#include <stdio.h>\n#include <string.h>\n\n\nPyObject *unicode_arg(PyObject *args) {\n  PyObject * com;\n  if (!PyArg_ParseTuple(args, \"O\", &com))\n    return NULL;\n  if (!PyUnicode_Check(com)) {\n    PyErr_SetObject(PyExc_TypeError, Py_None);\n    return NULL;\n  }\n  return com;\n}\n\n\n\nstatic PyObject *\nfilters_uwebsafe(PyObject * self, PyObject *args) \n{\n  PyObject * com;\n  Py_UNICODE * input_buffer;\n  Py_UNICODE *buffer;\n  PyObject * res;\n  int ic=0, ib=0;\n  int len;\n  Py_UNICODE c;\n  if (!(com = unicode_arg(args))) return NULL;\n  input_buffer = PyUnicode_AS_UNICODE(com);\n  len = PyUnicode_GetSize(com);\n\n  buffer = (Py_UNICODE*)malloc(6*len*sizeof(Py_UNICODE));\n  if (buffer == NULL) {\n    return PyErr_NoMemory();\n  }\n\n  for(ic = 0, ib = 0; ic < len; ic++, ib++) {\n    c = input_buffer[ic];\n    if (c == '&') {\n      buffer[ib++] = (Py_UNICODE)'&';\n      buffer[ib++] = (Py_UNICODE)'a';\n      buffer[ib++] = (Py_UNICODE)'m';\n      buffer[ib++] = (Py_UNICODE)'p';\n      buffer[ib]   = (Py_UNICODE)';';\n    }\n    else if(c == (Py_UNICODE)'<') {\n      buffer[ib++] = (Py_UNICODE)'&';\n      buffer[ib++] = (Py_UNICODE)'l';\n      buffer[ib++] = (Py_UNICODE)'t';\n      buffer[ib]   = (Py_UNICODE)';';\n    }\n    else if(c == (Py_UNICODE)'>') {\n      buffer[ib++] = (Py_UNICODE)'&';\n      buffer[ib++] = (Py_UNICODE)'g';\n      buffer[ib++] = (Py_UNICODE)'t';\n      buffer[ib]   = (Py_UNICODE)';';\n    }\n    else if(c == (Py_UNICODE)'\"') {\n      buffer[ib++] = (Py_UNICODE)'&';\n      buffer[ib++] = (Py_UNICODE)'q';\n      buffer[ib++] = (Py_UNICODE)'u';\n      buffer[ib++] = (Py_UNICODE)'o';\n      buffer[ib++] = (Py_UNICODE)'t';\n      buffer[ib]   = (Py_UNICODE)';';      \n    }\n    else {\n      buffer[ib] = input_buffer[ic];\n    }\n  }\n  res = PyUnicode_FromUnicode(buffer, ib);\n  free(buffer);\n  return res;\n\n}\n\nstatic PyObject *\nfilters_uwebsafe_json(PyObject * self, PyObject *args) \n{\n  PyObject * com;\n  Py_UNICODE * input_buffer;\n  Py_UNICODE *buffer;\n  PyObject * res;\n  int ic=0, ib=0;\n  int len;\n  Py_UNICODE c;\n  if (!(com = unicode_arg(args))) return NULL;\n  input_buffer = PyUnicode_AS_UNICODE(com);\n  len = PyUnicode_GetSize(com);\n\n  buffer = (Py_UNICODE*)malloc(6*len*sizeof(Py_UNICODE));\n  if (buffer == NULL) {\n    return PyErr_NoMemory();\n  }\n\n  for(ic = 0, ib = 0; ic < len; ic++, ib++) {\n    c = input_buffer[ic];\n    if (c == '&') {\n      buffer[ib++] = (Py_UNICODE)'&';\n      buffer[ib++] = (Py_UNICODE)'a';\n      buffer[ib++] = (Py_UNICODE)'m';\n      buffer[ib++] = (Py_UNICODE)'p';\n      buffer[ib]   = (Py_UNICODE)';';\n    }\n    else if(c == (Py_UNICODE)'<') {\n      buffer[ib++] = (Py_UNICODE)'&';\n      buffer[ib++] = (Py_UNICODE)'l';\n      buffer[ib++] = (Py_UNICODE)'t';\n      buffer[ib]   = (Py_UNICODE)';';\n    }\n    else if(c == (Py_UNICODE)'>') {\n      buffer[ib++] = (Py_UNICODE)'&';\n      buffer[ib++] = (Py_UNICODE)'g';\n      buffer[ib++] = (Py_UNICODE)'t';\n      buffer[ib]   = (Py_UNICODE)';';\n    }\n    else {\n      buffer[ib] = input_buffer[ic];\n    }\n  }\n  res = PyUnicode_FromUnicode(buffer, ib);\n  free(buffer);\n  return res;\n\n}\n\n\nstatic PyObject *\nfilters_websafe(PyObject * self, PyObject *args) \n{\n  const char * input_buffer;\n  char *buffer;\n  PyObject * res;\n  int ic=0, ib=0;\n  int len;\n  char c;\n  if (!PyArg_ParseTuple(args, \"s\", &input_buffer))\n    return NULL;\n  len = strlen(input_buffer);\n  buffer = (char*)malloc(6*len);\n  if (buffer == NULL) {\n    return PyErr_NoMemory();\n  }\n\n  for(ic = 0, ib = 0; ic <= len; ic++, ib++) {\n    c = input_buffer[ic];\n    if (c == '&') {\n      buffer[ib++] = '&';\n      buffer[ib++] = 'a';\n      buffer[ib++] = 'm';\n      buffer[ib++] = 'p';\n      buffer[ib]   = ';';\n    }\n    else if(c == '<') {\n      buffer[ib++] = '&';\n      buffer[ib++] = 'l';\n      buffer[ib++] = 't';\n      buffer[ib]   = ';';\n    }\n    else if(c == '>') {\n      buffer[ib++] = '&';\n      buffer[ib++] = 'g';\n      buffer[ib++] = 't';\n      buffer[ib]   = ';';\n    }\n    else if(c == '\"') {\n      buffer[ib++] = '&';\n      buffer[ib++] = 'q';\n      buffer[ib++] = 'u';\n      buffer[ib++] = 'o';\n      buffer[ib++] = 't';\n      buffer[ib]   = ';';\n    }\n    else {\n      buffer[ib] = input_buffer[ic];\n    }\n  }\n  res =  Py_BuildValue(\"s\", buffer);\n  free(buffer);\n  return res;\n}\n\nvoid print_unicode(Py_UNICODE *c, int len) {\n  int i;\n  for(i = 0; i < len; i++) {\n    printf(\"%d\", (int)c[i]);\n    if(i + 1 != len) printf(\":\");\n  }\n  printf(\"\\n\");\n}\n\nconst char *SC_OFF = \"<!-- SC_OFF -->\";\nconst char *SC_ON  = \"<!-- SC_ON -->\";\nconst Py_UNICODE *SC_OFF_U;\nconst Py_UNICODE *SC_ON_U;\nint SC_OFF_LEN = 0;\nint SC_ON_LEN = 0;\n\n\n\nint whitespace(char c) {\n  return (c == '\\n' || c == '\\r' || c == '\\t' || c == ' ');\n}\n\n\nstatic PyObject *\nfilters_uspace_compress(PyObject * self, PyObject *args) {\n  PyObject * com;\n  PyObject * res;\n  Py_ssize_t len;\n  Py_UNICODE *input_buffer;\n  Py_UNICODE *buffer;\n  Py_UNICODE c;\n  int ic, ib;\n  int gobble = 1;\n  com = unicode_arg(args);\n  if(!com) {\n    return NULL;\n  }\n  input_buffer = PyUnicode_AS_UNICODE(com);\n  len = PyUnicode_GetSize(com);\n  buffer = (Py_UNICODE*)malloc(len * sizeof(Py_UNICODE));\n  if (buffer == NULL) {\n    return PyErr_NoMemory();\n  }\n\n  /* ic -> input buffer index, ib -> output buffer */\n  for(ic = 0, ib = 0; ic <= len; ic++) {\n    c = input_buffer[ic];\n    /* gobble -> we are space compressing */\n    if(gobble) {\n      /* remove spaces if encountered */\n      if(Py_UNICODE_ISSPACE(c)) {\n        /* after this loop, c will be a non-space */\n        while(Py_UNICODE_ISSPACE(c)) { c = input_buffer[++ic]; }\n        /* unless next char is a <, add a single space to account for\n           the multiple spaces that have been removed */\n        if(c != (Py_UNICODE)('<')) {\n          buffer[ib++] = (Py_UNICODE)(' ');\n        }\n      }\n      /* gobble all space after '>' */\n      if(c == (Py_UNICODE)('>')) {\n        buffer[ib++] = c;\n\tc = input_buffer[++ic];\n        while(Py_UNICODE_ISSPACE(c)) { c = input_buffer[++ic]; }\n      }\n      /* does the next part of the string match the SC_OFF label */\n      if (len - ic >= SC_OFF_LEN &&\n          memcmp(&input_buffer[ic], SC_OFF_U, \n                 sizeof(Py_UNICODE)*SC_OFF_LEN) == 0) {\n        /* disable gobbling, and bypass that part of the string */\n        gobble = 0;\n        ic += SC_OFF_LEN;\n        c = input_buffer[ic];\n      }\n    }\n    /* not gobbling, but find the SC_ON tag */\n    else if (len - ic >= SC_ON_LEN &&\n          memcmp(&input_buffer[ic], SC_ON_U, \n                 sizeof(Py_UNICODE)*SC_ON_LEN) == 0) {\n        gobble = 1;\n        ic += SC_ON_LEN;\n        c = input_buffer[ic];\n    }\n    if(c) {\n      buffer[ib++] = c;\n    }\n  }  \n\n  res = PyUnicode_FromUnicode(buffer, ib);\n  free(buffer);\n  return res;\n}\n\nstatic PyMethodDef FilterMethods[] = {\n  {\"websafe\",  filters_websafe, METH_VARARGS,\n   \"make string web safe.\"},\n  {\"uwebsafe\",  filters_uwebsafe, METH_VARARGS,\n   \"make string web safe.\"},\n  {\"uwebsafe_json\",  filters_uwebsafe_json, METH_VARARGS,\n   \"make string web safe, no &quot;.\"},\n  {\"uspace_compress\",  filters_uspace_compress, METH_VARARGS,\n   \"removes spaces around angle brackets. Can be disabled with the use of SC_OFF and SC_ON comments from r2.lib.filters.\"},\n  {NULL, NULL, 0, NULL}        /* Sentinel */\n};\n\nPy_UNICODE *to_unicode(const char *c, int len) {\n  Py_UNICODE *x = (Py_UNICODE *)malloc((len+1) * sizeof(Py_UNICODE));\n  if (x == NULL) {\n    PyErr_NoMemory();\n    return NULL;\n  }\n\n  int i;\n  for(i = 0; i < len; i++) {\n    x[i] = (Py_UNICODE)c[i];\n  }\n  x[len] = (Py_UNICODE)(0);\n  return x;\n}\n\n\nPyMODINIT_FUNC\ninitCfilters(void)\n{\n  SC_OFF_LEN = strlen(SC_OFF);\n  SC_OFF_U = to_unicode(SC_OFF, SC_OFF_LEN);\n  SC_ON_LEN = strlen(SC_ON);\n  SC_ON_U = to_unicode(SC_ON, SC_ON_LEN);\n\n  (void) Py_InitModule(\"Cfilters\", FilterMethods);\n}\n\n"
  },
  {
    "path": "r2/r2/lib/cache.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom threading import local\nfrom hashlib import md5\nimport cPickle as pickle\nfrom copy import copy\nfrom curses.ascii import isgraph\nimport logging\nfrom time import sleep\n\nfrom pylons import app_globals as g\n\nimport pylibmc\nfrom _pylibmc import MemcachedError\n\nimport random\n\nfrom pycassa import ColumnFamily\nfrom pycassa.cassandra.ttypes import ConsistencyLevel\n\nfrom r2.lib.utils import in_chunks, prefix_keys, trace, tup\nfrom r2.lib.hardcachebackend import HardCacheBackend\n\n# This is for use in the health controller\n_CACHE_SERVERS = set()\n\nclass NoneResult(object): pass\n\nclass CacheUtils(object):\n    # Caches that never expire entries should set this to true, so that\n    # CacheChain can properly count hits and misses.\n    permanent = False\n\n    def incr_multi(self, keys, delta=1, prefix=''):\n        for k in keys:\n            try:\n                self.incr(prefix + k, delta)\n            except ValueError:\n                pass\n\n    def add_multi(self, keys, prefix='', time=0):\n        for k,v in keys.iteritems():\n            self.add(prefix+str(k), v, time = time)\n\n    def get_multi(self, keys, prefix='', **kw):\n        return prefix_keys(keys, prefix, lambda k: self.simple_get_multi(k, **kw))\n\n\nclass CMemcache(CacheUtils):\n    def __init__(self,\n                 name,\n                 servers,\n                 debug=False,\n                 noreply=False,\n                 no_block=False,\n                 min_compress_len=512 * 1024,\n                 num_clients=10,\n                 binary=False):\n        self.name = name\n        self.servers = servers\n        self.clients = pylibmc.ClientPool(n_slots = num_clients)\n\n        for x in xrange(num_clients):\n            client = pylibmc.Client(servers, binary=binary)\n            behaviors = {\n                'no_block': no_block, # use async I/O\n                'tcp_nodelay': True, # no nagle\n                '_noreply': int(noreply),\n                'ketama': True, # consistent hashing\n            }\n            if not binary:\n                behaviors['verify_keys'] = True\n\n            client.behaviors.update(behaviors)\n            self.clients.put(client)\n\n        self.min_compress_len = min_compress_len\n\n        _CACHE_SERVERS.update(servers)\n\n    def get(self, key, default = None):\n        with self.clients.reserve() as mc:\n            ret = mc.get(str(key))\n            if ret is None:\n                return default\n            return ret\n\n    def get_multi(self, keys, prefix = ''):\n        str_keys = [str(key) for key in keys]\n        with self.clients.reserve() as mc:\n            return mc.get_multi(str_keys, key_prefix=prefix)\n\n    # simple_get_multi exists so that a cache chain can\n    # single-instance the handling of prefixes for performance, but\n    # pylibmc does this in C which is faster anyway, so CMemcache\n    # implements get_multi itself. But the CacheChain still wants\n    # simple_get_multi to be available for when it's already prefixed\n    # them, so here it is\n    simple_get_multi = get_multi\n\n    def set(self, key, val, time=0):\n        # pylibmc converts this number to an unsigned integer without warning\n        if time < 0:\n            raise ValueError(\"Rejecting negative TTL for key %s\" % key)\n\n        with self.clients.reserve() as mc:\n            return mc.set(str(key), val, time=time,\n                            min_compress_len = self.min_compress_len)\n\n    def set_multi(self, keys, prefix='', time=0):\n        if time < 0:\n            raise ValueError(\"Rejecting negative TTL for key %s\" % key)\n\n        str_keys = {str(k): v for k, v in keys.iteritems()}\n        with self.clients.reserve() as mc:\n            return mc.set_multi(str_keys, key_prefix=prefix, time=time,\n                                min_compress_len=self.min_compress_len)\n\n    def add_multi(self, keys, prefix='', time=0):\n        # pylibmc converts this number to an unsigned integer without warning\n        if time < 0:\n            raise ValueError(\"Rejecting negative TTL for key %s\" % key)\n\n        str_keys = {str(k): v for k, v in keys.iteritems()}\n        with self.clients.reserve() as mc:\n            return mc.add_multi(str_keys, key_prefix=prefix, time=time)\n\n    def incr_multi(self, keys, prefix='', delta=1):\n        str_keys = [str(key) for key in keys]\n        with self.clients.reserve() as mc:\n            return mc.incr_multi(str_keys, key_prefix=prefix, delta=delta)\n\n    def append(self, key, val, time=0):\n        # pylibmc converts this number to an unsigned integer without warning\n        if time < 0:\n            raise ValueError(\"Rejecting negative TTL for key %s\" % key)\n\n        with self.clients.reserve() as mc:\n            return mc.append(str(key), val, time=time)\n\n    def incr(self, key, delta=1, time=0):\n        # ignore the time on these\n        with self.clients.reserve() as mc:\n            return mc.incr(str(key), delta)\n\n    def add(self, key, val, time=0):\n        # pylibmc converts this number to an unsigned integer without warning\n        if time < 0:\n            raise ValueError(\"Rejecting negative TTL for key %s\" % key)\n\n        try:\n            with self.clients.reserve() as mc:\n                return mc.add(str(key), val, time=time)\n        except pylibmc.DataExists:\n            return None\n\n    def delete(self, key, time=0):\n        with self.clients.reserve() as mc:\n            return mc.delete(str(key))\n\n    def delete_multi(self, keys, prefix=''):\n        str_keys = [str(key) for key in keys]\n        with self.clients.reserve() as mc:\n            return mc.delete_multi(str_keys, key_prefix=prefix)\n\n    def __repr__(self):\n        return '<%s(%r)>' % (self.__class__.__name__,\n                             self.servers)\n\n\nclass Mcrouter(CMemcache):\n    \"\"\"Wrapper class to make mcrouter appear like a regular memcached client.\n\n    Expected behavior (benefits of mcrouter):\n    * get() with a cache unresponsive will return `None` to be interpreted as a\n      cache miss rather than raising MemcachedError.\n    * get_multi() with a cache unresponsive returns only the values that were\n      retrieved.\n\n    Error cases:\n    * set() with a cache unresponsive will raise a ServerError.\n    * set_multi() with a cache unresponsive will raise a ServerError. Some of\n      the writes may have succeeded, which is the same behavior in mcrouter and\n      memcached.\n    * add() same as set()\n    * add_multi() same as set_multi()\n\n    In all cases where mcrouter raises a ServerError memcached would raise a\n    MemcachedError. This behavior is acceptable because ServerError inherits\n    from MemcachedError.\n\n    Special cases:\n    * set() if we are using prefix routing and the key doesn't match any routes\n      mcrouter will return `False`. This is converted to a MemcachedError but\n      it's possibly more correct to depend on the client checking the return\n      value and deciding how to proceed.\n\n    Unhandled cases:\n    * delete() with a cache unresponsive will return `False`, but memcached will\n      raise a MemcachedError. This can't be simply interpreted as the error case\n      because `False` is the correct return when deleting a key that doesn't\n      exist. The caller must check the return value.\n    * delete_multi() with a cache unresponsive will return `False`, but\n      memcached will raise a MemcachedError. Same logic follows as delete().\n    * incr() with a cache unresponsive will raise a NotFound exception, which is\n      the same error as attempting to incr an un-set key.\n    * incr_multi() with a cache unresponsive will raise a NotFound exception,\n      but memcached will raise a MemcachedError. This can't be interpreted as\n      being the error case and replaced with a MemcachedError because NotFound\n      is a valid exception when attempting to incr keys that don't exist.\n\n    \"\"\"\n\n    def set(self, key, val, time=0):\n        success = CMemcache.set(self, key, val, time)\n\n        if not success:\n            # If we are using prefix routing and the key doesn't match any\n            # routes mcrouter will return `False`.\n            raise MemcachedError(\"set failed\")\n        else:\n            return True\n\n\nclass HardCache(CacheUtils):\n    backend = None\n    permanent = True\n\n    def __init__(self, gc):\n        self.backend = HardCacheBackend(gc)\n\n    def _split_key(self, key):\n        tokens = key.split(\"-\", 1)\n        if len(tokens) != 2:\n            raise ValueError(\"key %s has no dash\" % key)\n\n        category, ids = tokens\n        return category, ids\n\n    def set(self, key, val, time=0):\n        if val == NoneResult:\n            # NoneResult caching is for other parts of the chain\n            return\n\n        category, ids = self._split_key(key)\n        self.backend.set(category, ids, val, time)\n\n    def simple_get_multi(self, keys):\n        results = {}\n        category_bundles = {}\n        for key in keys:\n            category, ids = self._split_key(key)\n            category_bundles.setdefault(category, []).append(ids)\n\n        for category in category_bundles:\n            idses = category_bundles[category]\n            chunks = in_chunks(idses, size=50)\n            for chunk in chunks:\n                new_results = self.backend.get_multi(category, chunk)\n                results.update(new_results)\n\n        return results\n\n    def set_multi(self, keys, prefix='', time=0):\n        for k,v in keys.iteritems():\n            if v != NoneResult:\n                self.set(prefix+str(k), v, time=time)\n\n    def get(self, key, default=None):\n        category, ids = self._split_key(key)\n        r = self.backend.get(category, ids)\n        if r is None: return default\n        return r\n\n    def delete(self, key, time=0):\n        # Potential optimization: When on a negative-result caching chain,\n        # shove NoneResult throughout the chain when a key is deleted.\n        category, ids = self._split_key(key)\n        self.backend.delete(category, ids)\n\n    def add(self, key, value, time=0):\n        category, ids = self._split_key(key)\n        return self.backend.add(category, ids, value, time=time)\n\n    def incr(self, key, delta=1, time=0):\n        category, ids = self._split_key(key)\n        return self.backend.incr(category, ids, delta=delta, time=time)\n\n\nclass LocalCache(dict, CacheUtils):\n    def __init__(self, *a, **kw):\n        return dict.__init__(self, *a, **kw)\n\n    def _check_key(self, key):\n        if isinstance(key, unicode):\n            key = str(key) # try to convert it first\n        if not isinstance(key, str):\n            raise TypeError('Key is not a string: %r' % (key,))\n\n    def get(self, key, default=None):\n        r = dict.get(self, key)\n        if r is None: return default\n        return r\n\n    def simple_get_multi(self, keys):\n        out = {}\n        for k in keys:\n            try:\n                out[k] = self[k]\n            except KeyError:\n                pass\n        return out\n\n    def set(self, key, val, time = 0):\n        # time is ignored on localcache\n        self._check_key(key)\n        self[key] = val\n\n    def set_multi(self, keys, prefix='', time=0):\n        for k,v in keys.iteritems():\n            self.set(prefix+str(k), v, time=time)\n\n    def add(self, key, val, time = 0):\n        self._check_key(key)\n        was = key in self\n        self.setdefault(key, val)\n        return not was\n\n    def delete(self, key):\n        if self.has_key(key):\n            del self[key]\n\n    def delete_multi(self, keys):\n        for key in keys:\n            if self.has_key(key):\n                del self[key]\n\n    def incr(self, key, delta=1, time=0):\n        if self.has_key(key):\n            self[key] = int(self[key]) + delta\n\n    def decr(self, key, amt=1):\n        if self.has_key(key):\n            self[key] = int(self[key]) - amt\n\n    def append(self, key, val, time = 0):\n        if self.has_key(key):\n            self[key] = str(self[key]) + val\n\n    def prepend(self, key, val, time = 0):\n        if self.has_key(key):\n            self[key] = val + str(self[key])\n\n    def replace(self, key, val, time = 0):\n        if self.has_key(key):\n            self[key] = val\n\n    def flush_all(self):\n        self.clear()\n\n    def reset(self):\n        self.clear()\n\n    def __repr__(self):\n        return \"<LocalCache(%d)>\" % (len(self),)\n\n\nclass TransitionalCache(CacheUtils):\n    \"\"\"A cache \"chain\" for moving keys to a new cluster live.\n\n    `original_cache` is the cache chain previously in use\n    `replacement_cache` is the new place for the keys using this chain to live.\n    `key_transform` is an optional function to translate the key names into\n    different names on the `replacement_cache`\n\n    To use this cache chain, do three separate deployments as follows:\n\n        * start dual-writing to the new pool by putting this chain in place\n          with `read_original=True`.\n        * cut reads over to the new pool after it is sufficiently heated up by\n          deploying `read_original=False`.\n        * remove this cache chain entirely and replace it with\n          `replacement_cache`.\n\n    This ensures that at any point, all apps regardless of their position in\n    the push order will have a consistent view of the data in the cache pool as\n    much as is possible.\n\n    \"\"\"\n\n    def __init__(\n            self, original_cache, replacement_cache, read_original,\n            key_transform=None):\n        self.original = original_cache\n        self.replacement = replacement_cache\n        self.read_original = read_original\n        self.key_transform = key_transform\n\n    @property\n    def stats(self):\n        if self.read_original:\n            return self.original.stats\n        else:\n            return self.replacement.stats\n\n    @property\n    def read_chain(self):\n        if self.read_original:\n            return self.original\n        else:\n            return self.replacement\n\n    @property\n    def caches(self):\n        if self.read_original:\n            return self.original.caches\n        else:\n            return self.replacement.caches\n\n    def transform_memcache_key(self, args, kwargs):\n        \"\"\"Use key_transform to transform keys and prefix.\n\n        key_transform() returns (new_prefix, new_key)\n\n        If \"prefix\" is specified in kwargs, the transformation will look like:\n        key_transform(\"key\", \"old_prefix_\") --> \"new_prefix_\", \"key\"\n\n        If \"prefix\" is not specified in kwargs, it must already be part of the\n        key, and the transformation looks like:\n        key_transform(\"old_prefix_key\") --> \"\", \"new_prefix_key\"\n\n        We don't currently handle multiple gets or sets where the prefix is\n        already prepended to the keys because the return values are different:\n        get([\"old_prefix_A\", \"old_prefix_B\"])\n        old:\n            {\"old_prefix_A\": val, \"old_prefix_B\": val}\n        new:\n            {\"new_prefix_A\": val, \"new_prefix_B\": val}\n\n        They must be looked up with a prefix:\n        get([\"A\", \"B\"], prefix=\"old_prefix_\")\n        old:\n            {\"A\": val, \"B\": val}\n        new (translated to get([\"A\", \"B\"], prefix=\"new_prefix_\"):\n            {\"A\": val, \"B\": val}\n\n        The special case of the above is for a single item lookup, where the\n        return value does not include the key.\n\n        We could handle the general multiple key case by maintaining a mapping\n        of {old_key: new_key} and using that to transform the return value.\n\n        \"\"\"\n\n        if self.key_transform:\n            prefix = kwargs.get(\"prefix\", \"\")\n            new_kwargs = copy(kwargs)\n\n            if isinstance(args[0], dict):\n                assert prefix, \"must include prefix\"\n                new_prefixes = []\n                old_key_dict = args[0]\n                new_key_dict = {}\n\n                for old_key, val in old_key_dict.iteritems():\n                    new_prefix, new_key = self.key_transform(old_key, prefix)\n                    new_key_dict[new_key] = val\n                    new_prefixes.append(new_prefix)\n\n                assert all(p == new_prefixes[0] for p in new_prefixes[1:])\n                new_kwargs[\"prefix\"] = new_prefixes[0]\n                new_args = (new_key_dict,) + args[1:]\n            elif isinstance(args[0], (list, set, tuple)):\n                assert prefix, \"must include prefix\"\n                new_prefixes = []\n                old_key_list = args[0]\n                new_key_list = []\n\n                for old_key in old_key_list:\n                    new_prefix, new_key = self.key_transform(old_key, prefix)\n                    new_key_list.append(new_key)\n                    new_prefixes.append(new_prefix)\n\n                assert all(p == new_prefixes[0] for p in new_prefixes[1:])\n                new_kwargs[\"prefix\"] = new_prefixes[0]\n                new_args = (new_key_list,) + args[1:]\n            else:\n                # single keys can't specify a prefix\n                _, new_key = self.key_transform(args[0])\n                new_args = (new_key,) + args[1:]\n\n            return new_args, new_kwargs\n        else:\n            return args, kwargs\n\n    def make_get_fn(fn_name):\n        def transitional_cache_get_fn(self, *args, **kwargs):\n            if self.read_original:\n                return getattr(self.original, fn_name)(*args, **kwargs)\n            else:\n                new_args, new_kwargs = self.transform_memcache_key(args, kwargs)\n                return getattr(self.replacement, fn_name)(*new_args, **new_kwargs)\n        return transitional_cache_get_fn\n\n    get = make_get_fn(\"get\")\n    get_multi = make_get_fn(\"get_multi\")\n    simple_get_multi = make_get_fn(\"simple_get_multi\")\n\n    def make_set_fn(fn_name):\n        def transitional_cache_set_fn(self, *args, **kwargs):\n            ret_original = getattr(self.original, fn_name)(*args, **kwargs)\n\n            new_args, new_kwargs = self.transform_memcache_key(args, kwargs)\n            ret_replacement = getattr(self.replacement, fn_name)(*new_args, **new_kwargs)\n\n            if self.read_original:\n                return ret_original\n            else:\n                return ret_replacement\n        return transitional_cache_set_fn\n\n    add = make_set_fn(\"add\")\n    set = make_set_fn(\"set\")\n    append = make_set_fn(\"append\")\n    prepend = make_set_fn(\"prepend\")\n    replace = make_set_fn(\"replace\")\n    set_multi = make_set_fn(\"set_multi\")\n    add = make_set_fn(\"add\")\n    add_multi = make_set_fn(\"add_multi\")\n    incr = make_set_fn(\"incr\")\n    incr_multi = make_set_fn(\"incr_multi\")\n    decr = make_set_fn(\"decr\")\n    delete = make_set_fn(\"delete\")\n    delete_multi = make_set_fn(\"delete_multi\")\n    flush_all = make_set_fn(\"flush_all\")\n\n\ndef cache_timer_decorator(fn_name):\n    \"\"\"Use to decorate CacheChain operations so timings will be recorded.\"\"\"\n    def wrap(fn):\n        def timed_fn(self, *a, **kw):\n            use_timer = kw.pop(\"use_timer\", True)\n\n            try:\n                getattr(g, \"log\")\n            except TypeError:\n                # don't have access to g, maybe in a thread?\n                return fn(self, *a, **kw)\n\n            if use_timer and self.stats:\n                publish = random.random() < g.stats.CACHE_SAMPLE_RATE\n                cache_name = self.stats.cache_name\n                timer_name = \"cache.%s.%s\" % (cache_name, fn_name)\n                timer = g.stats.get_timer(timer_name, publish)\n                timer.start()\n            else:\n                timer = None\n\n            result = fn(self, *a, **kw)\n            if timer:\n                timer.stop()\n\n            return result\n        return timed_fn\n    return wrap\n\n\nclass CacheChain(CacheUtils, local):\n    def __init__(self, caches, cache_negative_results=False):\n        self.caches = caches\n        self.cache_negative_results = cache_negative_results\n        self.stats = None\n\n    def make_set_fn(fn_name):\n        @cache_timer_decorator(fn_name)\n        def fn(self, *a, **kw):\n            ret = None\n            for c in self.caches:\n                ret = getattr(c, fn_name)(*a, **kw)\n            return ret\n        return fn\n\n    # note that because of the naive nature of `add' when used on a\n    # cache chain, its return value isn't reliable. if you need to\n    # verify its return value you'll either need to make it smarter or\n    # use the underlying cache directly\n    add = make_set_fn('add')\n\n    set = make_set_fn('set')\n    append = make_set_fn('append')\n    prepend = make_set_fn('prepend')\n    replace = make_set_fn('replace')\n    set_multi = make_set_fn('set_multi')\n    add = make_set_fn('add')\n    add_multi = make_set_fn('add_multi')\n    incr = make_set_fn('incr')\n    incr_multi = make_set_fn('incr_multi')\n    decr = make_set_fn('decr')\n    delete = make_set_fn('delete')\n    delete_multi = make_set_fn('delete_multi')\n    flush_all = make_set_fn('flush_all')\n    cache_negative_results = False\n\n    @cache_timer_decorator(\"get\")\n    def get(self, key, default = None, allow_local = True, stale=None):\n        stat_outcome = False  # assume a miss until a result is found\n        is_localcache = False\n        try:\n            for c in self.caches:\n                is_localcache = isinstance(c, LocalCache)\n                if not allow_local and is_localcache:\n                    continue\n\n                val = c.get(key)\n\n                if val is not None:\n                    if not c.permanent:\n                        stat_outcome = True\n\n                    #update other caches\n                    for d in self.caches:\n                        if c is d:\n                            break # so we don't set caches later in the chain\n                        d.set(key, val)\n\n                    if val == NoneResult:\n                        return default\n                    else:\n                        return val\n\n            if self.cache_negative_results:\n                for c in self.caches[:-1]:\n                    c.set(key, NoneResult)\n\n            return default\n        finally:\n            if self.stats:\n                if stat_outcome:\n                    if not is_localcache:\n                        self.stats.cache_hit()\n                else:\n                    self.stats.cache_miss()\n\n    def get_multi(self, keys, prefix='', allow_local = True, **kw):\n        l = lambda ks: self.simple_get_multi(ks, allow_local = allow_local, **kw)\n        return prefix_keys(keys, prefix, l)\n\n    @cache_timer_decorator(\"get_multi\")\n    def simple_get_multi(self, keys, allow_local = True, stale=None,\n                         stat_subname=None):\n        out = {}\n        need = set(keys)\n        hits = 0\n        local_hits = 0\n        misses = 0\n        for c in self.caches:\n            is_localcache = isinstance(c, LocalCache)\n            if not allow_local and is_localcache:\n                continue\n\n            if c.permanent and not misses:\n                # Once we reach a \"permanent\" cache, we count any outstanding\n                # items as misses.\n                misses = len(need)\n\n            if len(out) == len(keys):\n                # we've found them all\n                break\n\n            r = c.simple_get_multi(need)\n            #update other caches\n            if r:\n                if is_localcache:\n                    local_hits += len(r)\n                elif not c.permanent:\n                    hits += len(r)\n\n                for d in self.caches:\n                    if c is d:\n                        break # so we don't set caches later in the chain\n                    d.set_multi(r)\n                r.update(out)\n                out = r\n                need = need - set(r.keys())\n\n        if need and self.cache_negative_results:\n            d = dict((key, NoneResult) for key in need)\n            for c in self.caches[:-1]:\n                c.set_multi(d)\n\n        out = dict((k, v)\n                   for (k, v) in out.iteritems()\n                   if v != NoneResult)\n\n        if self.stats:\n            if not misses:\n                # If this chain contains no permanent caches, then we need to\n                # count the misses here.\n                misses = len(need)\n            self.stats.cache_hit(hits, subname=stat_subname)\n            self.stats.cache_miss(misses, subname=stat_subname)\n\n        return out\n\n    def __repr__(self):\n        return '<%s %r>' % (self.__class__.__name__,\n                            self.caches)\n\n    def debug(self, key):\n        print \"Looking up [%r]\" % key\n        for i, c in enumerate(self.caches):\n            print \"[%d] %10s has value [%r]\" % (i, c.__class__.__name__,\n                                                c.get(key))\n\n    def reset(self):\n        # the first item in a cache chain is a LocalCache\n        self.caches = (self.caches[0].__class__(),) +  self.caches[1:]\n\nclass MemcacheChain(CacheChain):\n    pass\n\nclass HardcacheChain(CacheChain):\n    def add(self, key, val, time=0):\n        authority = self.caches[-1] # the authority is the hardcache\n                                    # itself\n        added_val = authority.add(key, val, time=time)\n        for cache in self.caches[:-1]:\n            # Calling set() rather than add() to ensure that all caches are\n            # in sync and that de-syncs repair themselves\n            cache.set(key, added_val, time=time)\n\n        return added_val\n\n    def accrue(self, key, time=0, delta=1):\n        auth_value = self.caches[-1].get(key)\n\n        if auth_value is None:\n            auth_value = 0\n\n        try:\n            auth_value = int(auth_value) + delta\n        except ValueError:\n            raise ValueError(\"Can't accrue %s; it's a %s (%r)\" %\n                             (key, auth_value.__class__.__name__, auth_value))\n\n        for c in self.caches:\n            c.set(key, auth_value, time=time)\n\n        return auth_value\n\n    @property\n    def backend(self):\n        # the hardcache is always the last item in a HardCacheChain\n        return self.caches[-1].backend\n\nclass StaleCacheChain(CacheChain):\n    \"\"\"A cache chain of two cache chains. When allowed by `stale`,\n       answers may be returned by a \"closer\" but potentially older\n       cache. Probably doesn't play well with NoneResult cacheing\"\"\"\n    staleness = 30\n\n    def __init__(self, localcache, stalecache, realcache):\n        self.localcache = localcache\n        self.stalecache = stalecache\n        self.realcache = realcache\n        self.caches = (localcache, realcache) # for the other\n                                              # CacheChain machinery\n        self.stats = None\n\n    @cache_timer_decorator(\"get\")\n    def get(self, key, default=None, stale = False, **kw):\n        if kw.get('allow_local', True) and key in self.localcache:\n            return self.localcache[key]\n\n        if stale:\n            stale_value = self._getstale([key]).get(key, None)\n            if stale_value is not None:\n                if self.stats:\n                    self.stats.cache_hit()\n                    self.stats.stale_hit()\n                return stale_value # never return stale data into the\n                                   # LocalCache, or people that didn't\n                                   # say they'll take stale data may\n                                   # get it\n            else:\n                self.stats.stale_miss()\n\n        value = self.realcache.get(key)\n        if value is None:\n            if self.stats:\n                self.stats.cache_miss()\n            return default\n\n        if stale:\n            self.stalecache.set(key, value, time=self.staleness)\n\n        self.localcache.set(key, value)\n\n        if self.stats:\n            self.stats.cache_hit()\n\n        return value\n\n    @cache_timer_decorator(\"get_multi\")\n    def simple_get_multi(self, keys, stale=False, stat_subname=None, **kw):\n        if not isinstance(keys, set):\n            keys = set(keys)\n\n        ret = {}\n        local_hits = 0\n\n        if kw.get('allow_local'):\n            for k in list(keys):\n                if k in self.localcache:\n                    ret[k] = self.localcache[k]\n                    keys.remove(k)\n                    local_hits += 1\n\n        if keys and stale:\n            stale_values = self._getstale(keys)\n            # never put stale data into the localcache\n            for k, v in stale_values.iteritems():\n                ret[k] = v\n                keys.remove(k)\n\n            stale_hits = len(stale_values)\n            stale_misses = len(keys)\n            if self.stats:\n                self.stats.stale_hit(stale_hits, subname=stat_subname)\n                self.stats.stale_miss(stale_misses, subname=stat_subname)\n\n        if keys:\n            values = self.realcache.simple_get_multi(keys)\n            if values and stale:\n                self.stalecache.set_multi(values, time=self.staleness)\n            self.localcache.update(values)\n            ret.update(values)\n\n        if self.stats:\n            misses = len(keys - set(ret.keys()))\n            hits = len(ret) - local_hits\n            self.stats.cache_hit(hits, subname=stat_subname)\n            self.stats.cache_miss(misses, subname=stat_subname)\n\n        return ret\n\n    def _getstale(self, keys):\n        # this is only in its own function to make tapping it for\n        # debugging easier\n        return self.stalecache.simple_get_multi(keys)\n\n    def reset(self):\n        newcache = self.localcache.__class__()\n        self.localcache = newcache\n        self.caches = (newcache,) +  self.caches[1:]\n        if isinstance(self.realcache, CacheChain):\n            assert isinstance(self.realcache.caches[0], LocalCache)\n            self.realcache.caches = (newcache,) + self.realcache.caches[1:]\n\n    def __repr__(self):\n        return '<%s %r>' % (self.__class__.__name__,\n                            (self.localcache, self.stalecache, self.realcache))\n\nCL_ONE = ConsistencyLevel.ONE\nCL_QUORUM = ConsistencyLevel.QUORUM\n\n\nclass Permacache(object):\n    \"\"\"Cassandra key/value column family backend with a cachechain in front.\n    \n    Probably best to not think of this as a cache but rather as a key/value\n    datastore that's faster to access than cassandra because of the cache.\n\n    \"\"\"\n\n    COLUMN_NAME = 'value'\n\n    def __init__(self, cache_chain, column_family, lock_factory):\n        self.cache_chain = cache_chain\n        self.make_lock = lock_factory\n        self.cf = column_family\n\n    @classmethod\n    def _setup_column_family(cls, column_family_name, client):\n        cf = ColumnFamily(client, column_family_name,\n                          read_consistency_level=CL_QUORUM,\n                          write_consistency_level=CL_QUORUM)\n        return cf\n\n    def _backend_get(self, keys):\n        keys, is_single = tup(keys, ret_is_single=True)\n        rows = self.cf.multiget(keys, columns=[self.COLUMN_NAME])\n        ret = {\n            key: pickle.loads(columns[self.COLUMN_NAME])\n            for key, columns in rows.iteritems()\n        }\n        if is_single:\n            if ret:\n                return ret.values()[0]\n            else:\n                return None\n        else:\n            return ret\n\n    def _backend_set(self, key, val):\n        keys = {key: val}\n        ret = self._backend_set_multi(keys)\n        return ret.get(key)\n\n    def _backend_set_multi(self, keys, prefix=''):\n        ret = {}\n        with self.cf.batch():\n            for key, val in keys.iteritems():\n                rowkey = \"%s%s\" % (prefix, key)\n                column = {self.COLUMN_NAME: pickle.dumps(val, protocol=2)}\n                ret[key] = self.cf.insert(rowkey, column)\n        return ret\n\n    def _backend_delete(self, key):\n        self.cf.remove(key)\n\n    def get(self, key, default=None, allow_local=True, stale=False):\n        val = self.cache_chain.get(\n            key, default=None, allow_local=allow_local, stale=stale)\n\n        if val is None:\n            val = self._backend_get(key)\n            if val:\n                self.cache_chain.set(key, val)\n        return val\n\n    def set(self, key, val):\n        self._backend_set(key, val)\n        self.cache_chain.set(key, val)\n\n    def set_multi(self, keys, prefix='', time=None):\n        # time is sent by sgm but will be ignored\n        self._backend_set_multi(keys, prefix=prefix)\n        self.cache_chain.set_multi(keys, prefix=prefix)\n\n    def pessimistically_set(self, key, value):\n        \"\"\"\n        Sets a value in Cassandra but instead of setting it in memcached,\n        deletes it from there instead. This is useful for the mr_top job which\n        sets thousands of keys but almost all of them will never be read out of\n        \"\"\"\n        self._backend_set(key, value)\n        self.cache_chain.delete(key)\n\n    def get_multi(self, keys, prefix='', allow_local=True, stale=False):\n        call_fn = lambda k: self.simple_get_multi(k, allow_local=allow_local,\n                                                  stale=stale)\n        return prefix_keys(keys, prefix, call_fn)\n\n    def simple_get_multi(self, keys, allow_local=True, stale=False):\n        ret = self.cache_chain.simple_get_multi(\n            keys, allow_local=allow_local, stale=stale)\n        still_need = {key for key in keys if key not in ret}\n        if still_need:\n            from_cass = self._backend_get(keys)\n            self.cache_chain.set_multi(from_cass)\n            ret.update(from_cass)\n        return ret\n\n    def delete(self, key):\n        self._backend_delete(key)\n        self.cache_chain.delete(key)\n\n    def mutate(self, key, mutation_fn, default=None, willread=True):\n        \"\"\"Mutate a Cassandra key as atomically as possible\"\"\"\n        with self.make_lock(\"permacache_mutate\", \"mutate_%s\" % key):\n            # This has an edge-case where the cache chain was populated by a ONE\n            # read rather than a QUORUM one just before running this. All reads\n            # should use consistency level QUORUM.\n            if willread:\n                value = self.cache_chain.get(key, allow_local=False)\n                if value is None:\n                    value = self._backend_get(key)\n            else:\n                value = None\n\n            # send in a copy in case they mutate it in-place\n            new_value = mutation_fn(copy(value))\n\n            if not willread or value != new_value:\n                self._backend_set(key, new_value)\n            self.cache_chain.set(key, new_value, use_timer=False)\n        return new_value\n\n    def __repr__(self):\n        return '<%s %r %r>' % (self.__class__.__name__,\n                            self.cache_chain, self.cf.column_family)\n\n\ndef test_cache(cache, prefix=''):\n    #basic set/get\n    cache.set('%s1' % prefix, 1)\n    assert cache.get('%s1' % prefix) == 1\n\n    #python data\n    cache.set('%s2' % prefix, [1,2,3])\n    assert cache.get('%s2' % prefix) == [1,2,3]\n\n    #set multi, no prefix\n    cache.set_multi({'%s3' % prefix:3, '%s4' % prefix: 4})\n    assert cache.get_multi(('%s3' % prefix, '%s4' % prefix)) == {'%s3' % prefix: 3, \n                                                                 '%s4' % prefix: 4}\n\n    #set multi, prefix\n    cache.set_multi({'3':3, '4': 4}, prefix='%sp_' % prefix)\n    assert cache.get_multi(('3', 4), prefix='%sp_' % prefix) == {'3':3, 4: 4}\n    assert cache.get_multi(('%sp_3' % prefix, '%sp_4' % prefix)) == {'%sp_3'%prefix: 3,\n                                                                     '%sp_4'%prefix: 4}\n\n    # delete\n    cache.set('%s1'%prefix, 1)\n    assert cache.get('%s1'%prefix) == 1\n    cache.delete('%s1'%prefix)\n    assert cache.get('%s1'%prefix) is None\n\n    cache.set('%s1'%prefix, 1)\n    cache.set('%s2'%prefix, 2)\n    cache.set('%s3'%prefix, 3)\n    assert cache.get('%s1'%prefix) == 1 and cache.get('%s2'%prefix) == 2\n    cache.delete_multi(['%s1'%prefix, '%s2'%prefix])\n    assert (cache.get('%s1'%prefix) is None\n            and cache.get('%s2'%prefix) is None\n            and cache.get('%s3'%prefix) == 3)\n\n    #incr\n    cache.set('%s5'%prefix, 1)\n    cache.set('%s6'%prefix, 1)\n    cache.incr('%s5'%prefix)\n    assert cache.get('%s5'%prefix) == 2\n    cache.incr('%s5'%prefix,2)\n    assert cache.get('%s5'%prefix) == 4\n    cache.incr_multi(('%s5'%prefix, '%s6'%prefix), 1)\n    assert cache.get('%s5'%prefix) == 5\n    assert cache.get('%s6'%prefix) == 2\n\ndef test_multi(cache):\n    from threading import Thread\n\n    num_threads = 100\n    num_per_thread = 1000\n\n    threads = []\n    for x in range(num_threads):\n        def _fn(prefix):\n            def __fn():\n                for y in range(num_per_thread):\n                    test_cache(cache,prefix=prefix)\n            return __fn\n        t = Thread(target=_fn(str(x)))\n        t.start()\n        threads.append(t)\n\n    for thread in threads:\n        thread.join()\n\n# a cache that occasionally dumps itself to be used for long-running\n# processes\nclass SelfEmptyingCache(LocalCache):\n    def __init__(self, max_size=10*1000):\n        self.max_size = max_size\n\n    def maybe_reset(self):\n        if len(self) > self.max_size:\n            self.clear()\n\n    def set(self, key, val, time=0):\n        self.maybe_reset()\n        return LocalCache.set(self,key,val,time)\n\n    def add(self, key, val, time=0):\n        self.maybe_reset()\n        return LocalCache.add(self, key, val)\n\n\ndef _make_hashable(s):\n    if isinstance(s, str):\n        return s\n    elif isinstance(s, unicode):\n        return s.encode('utf-8')\n    elif isinstance(s, (tuple, list)):\n        return ','.join(_make_hashable(x) for x in s)\n    elif isinstance(s, dict):\n        return ','.join('%s:%s' % (_make_hashable(k), _make_hashable(v))\n                        for (k, v) in sorted(s.iteritems()))\n    else:\n        return str(s)\n\n\ndef make_key_id(*a, **kw):\n    h = md5()\n    h.update(_make_hashable(a))\n    h.update(_make_hashable(kw))\n    return h.hexdigest()\n\n\ndef test_stale():\n    from pylons import app_globals as g\n    ca = g.gencache\n    assert isinstance(ca, StaleCacheChain)\n\n    ca.localcache.clear()\n\n    ca.stalecache.set('foo', 'bar', time=ca.staleness)\n    assert ca.stalecache.get('foo') == 'bar'\n    ca.realcache.set('foo', 'baz')\n    assert ca.realcache.get('foo') == 'baz'\n\n    assert ca.get('foo', stale=True) == 'bar'\n    ca.localcache.clear()\n    assert ca.get('foo', stale=False) == 'baz'\n    ca.localcache.clear()\n\n    assert ca.get_multi(['foo'], stale=True) == {'foo': 'bar'}\n    assert len(ca.localcache) == 0\n    assert ca.get_multi(['foo'], stale=False) == {'foo': 'baz'}\n    ca.localcache.clear()\n"
  },
  {
    "path": "r2/r2/lib/cache_poisoning.py",
    "content": "import hashlib\nimport hmac\nfrom pylons import app_globals as g\n\n# A map of cache policies to their respective cache headers\n# loggedout omitted because loggedout responses are intentionally cacheable\nCACHE_POLICY_DIRECTIVES = {\n    \"loggedin_www\": {\n        \"cache-control\": {\"private\", \"no-cache\"},\n        \"pragma\": {\"no-cache\"},\n        \"expires\": set(),\n    },\n    \"loggedin_www_new\": {\n        \"cache-control\": {\"private\", \"max-age=0\", \"must-revalidate\"},\n        \"pragma\": set(),\n        \"expires\": {\"-1\"},\n    },\n    \"loggedin_mweb\": {\n        \"cache-control\": {\"private\", \"no-cache\"},\n        \"pragma\": set(),\n        \"expires\": set(),\n    },\n}\n\n\ndef make_poisoning_report_mac(\n        poisoner_canary,\n        poisoner_name,\n        poisoner_id,\n        cache_policy,\n        source,\n        # Can't MAC based on URL, some caches don't care about the\n        # order of query params and suchlike.\n        route_name,\n):\n    \"\"\"\n    Make a MAC to send with cache poisoning reports for this page\n    \"\"\"\n    mac_key = g.secrets[\"cache_poisoning\"]\n    mac_data = (\n        poisoner_canary,\n        poisoner_name,\n        str(poisoner_id),\n        cache_policy,\n        source,\n        route_name,\n    )\n    return hmac.new(mac_key, \"|\".join(mac_data), hashlib.sha1).hexdigest()\n\n\ndef cache_headers_valid(policy_name, headers):\n    \"\"\"Check if a response's headers make sense given a cache policy\"\"\"\n\n    policy_headers = CACHE_POLICY_DIRECTIVES[policy_name]\n\n    for header_name, expected_vals in policy_headers.items():\n        # Cache-Control is a little special, you can have multiple directives\n        # in multiple headers\n        found_vals = set(headers.get(header_name, []))\n        if header_name == \"cache-control\":\n            parsed_cache_control = set()\n            for cache_header in found_vals:\n                for split_header in cache_header.split(\",\"):\n                    cache_directive = split_header.strip().lower()\n                    parsed_cache_control.add(cache_directive)\n            if parsed_cache_control != expected_vals:\n                return False\n        elif found_vals != expected_vals:\n            return False\n    return True\n"
  },
  {
    "path": "r2/r2/lib/captcha.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom __future__ import absolute_import\n\nimport random, string\n\nfrom pylons import app_globals as g\n\nfrom Captcha.Base import randomIdentifier\nfrom Captcha.Visual import Text, Backgrounds, Distortions, ImageCaptcha\n\n\nIDEN_LENGTH = 32\nSOL_LENGTH = 6\n\nclass RandCaptcha(ImageCaptcha):\n    defaultSize = (120, 50)\n    fontFactory = Text.FontFactory(18, \"vera/VeraBd.ttf\")\n\n    def getLayers(self, solution=\"blah\"):\n        self.addSolution(solution)\n        return ((Backgrounds.Grid(size=8, foreground=\"white\"),\n                 Distortions.SineWarp(amplitudeRange=(5,9))),\n                (Text.TextLayer(solution,\n                               textColor = 'white',\n                               fontFactory = self.fontFactory),\n                 Distortions.SineWarp()))\n\ndef get_iden():\n    return randomIdentifier(length=IDEN_LENGTH)\n\ndef make_solution():\n    return randomIdentifier(alphabet=string.ascii_letters, length = SOL_LENGTH).upper()\n\ndef get_image(iden):\n    key = \"captcha:%s\" % iden\n    solution = g.gencache.get(key)\n    if not solution:\n        solution = make_solution()\n        g.gencache.set(key, solution, time=300)\n    return RandCaptcha(solution=solution).render()\n\n\ndef valid_solution(iden, solution):\n    key = \"captcha:%s\" % iden\n\n    if (not iden or\n            not solution or\n            len(iden) != IDEN_LENGTH or\n            len(solution) != SOL_LENGTH or\n            solution.upper() != g.gencache.get(key)):\n        # the guess was wrong so make a new solution for the next attempt--the\n        # client will need to refresh the image before guessing again\n        solution = make_solution()\n        g.gencache.set(key, solution, time=300)\n        return False\n    else:\n        g.gencache.delete(key)\n        return True\n"
  },
  {
    "path": "r2/r2/lib/comment_tree.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom collections import defaultdict\nfrom itertools import chain\n\nfrom pylons import tmpl_context as c\nfrom pylons import app_globals as g\n\nfrom r2.lib.sgm import sgm\nfrom r2.lib.utils import tup\nfrom r2.models.comment_tree import CommentTree\nfrom r2.models.link import Comment, Link, CommentScoresByLink\n\nMESSAGE_TREE_SIZE_LIMIT = 15000\n\n\ndef write_comment_scores(link, comments):\n    for sort in (\"_controversy\", \"_confidence\", \"_score\", \"_qa\"):\n        scores = calculate_comment_scores(link, sort, comments)\n        CommentScoresByLink.set_scores(link, sort, scores)\n\n\ndef add_comments(comments):\n    \"\"\"Add comments to the CommentTree and update scores.\"\"\"\n    from r2.models.builder import write_comment_orders\n\n    link_ids = [comment.link_id for comment in tup(comments)]\n    links_by_id = Link._byID(link_ids)\n\n    comments = tup(comments)\n    comments_by_link_id = defaultdict(list)\n    for comment in comments:\n        comments_by_link_id[comment.link_id].append(comment)\n\n    for link_id, link_comments in comments_by_link_id.iteritems():\n        link = links_by_id[link_id]\n\n        timer = g.stats.get_timer('comment_tree.add.1')\n        timer.start()\n\n        write_comment_scores(link, link_comments)\n        timer.intermediate('scores')\n\n        CommentTree.add_comments(link, link_comments)\n        timer.intermediate('update')\n\n        write_comment_orders(link)\n        timer.intermediate('write_order')\n\n        timer.stop()\n\n\ndef calculate_comment_scores(link, sort, comments):\n    if sort in (\"_controversy\", \"_confidence\", \"_score\"):\n        scores = {\n            comment._id36: getattr(comment, sort)\n            for comment in comments\n        }\n    elif sort == \"_qa\":\n        comment_tree = CommentTree.by_link(link)\n        cid_tree = comment_tree.tree\n        scores = _calculate_qa_comment_scores(link, cid_tree, comments)\n    else:\n        raise ValueError(\"unsupported comment sort %s\" % sort)\n\n    return scores\n\n\ndef _calculate_qa_comment_scores(link, cid_tree, comments):\n    \"\"\"Return a dict of comment_id36 -> qa score\"\"\"\n\n    # Responder is usually the OP, but there could be support for adding\n    # other answerers in the future.\n    responder_ids = link.responder_ids\n\n    # An OP response will change the sort value for its parent, so we need\n    # to process the parent, too.\n    parent_cids = []\n    for comment in comments:\n        if comment.author_id in responder_ids and comment.parent_id:\n            parent_cids.append(comment.parent_id)\n    parent_comments = Comment._byID(parent_cids, return_dict=False)\n    comments.extend(parent_comments)\n\n    # Fetch the comments in batch to avoid a bunch of separate calls down\n    # the line.\n    all_child_cids = []\n    for comment in comments:\n        child_cids = cid_tree.get(comment._id, None)\n        if child_cids:\n            all_child_cids.extend(child_cids)\n    all_child_comments = Comment._byID(all_child_cids)\n\n    comment_sorter = {}\n    for comment in comments:\n        child_cids = cid_tree.get(comment._id, ())\n        child_comments = (all_child_comments[cid] for cid in child_cids)\n        sort_value = comment._qa(child_comments, responder_ids)\n        comment_sorter[comment._id36] = sort_value\n\n    return comment_sorter\n\n\ndef get_comment_scores(link, sort, comment_ids, timer):\n    \"\"\"Retrieve cached sort values for all comments on a post.\n\n    Arguments:\n\n    * link_id -- id of the Link containing the comments.\n    * sort -- a string indicating the attribute on the comments to use for\n      generating sort values.\n\n    Returns a dictionary from cid to a numeric sort value.\n\n    \"\"\"\n\n    from r2.lib.db import queries\n    from r2.models import CommentScoresByLink\n\n    if not comment_ids:\n        # no comments means no scores\n        return {}\n\n    if sort == \"_date\":\n        # comment ids are monotonically increasing, so we can use them as a\n        # substitute for creation date\n        scores_by_id = {comment_id: comment_id for comment_id in comment_ids}\n    else:\n        scores_by_id36 = CommentScoresByLink.get_scores(link, sort)\n\n        # we store these id36ed, but there are still bits of the code that\n        # want to deal in integer IDs\n        scores_by_id = {\n            int(id36, 36): score\n            for id36, score in scores_by_id36.iteritems()\n        }\n\n        scores_needed = set(comment_ids) - set(scores_by_id.keys())\n        if scores_needed:\n            # some scores were missing from CommentScoresByLink--lookup the\n            # comments and calculate the scores.\n            g.stats.simple_event('comment_tree_bad_sorter')\n\n            missing = Comment._byID(scores_needed, return_dict=False)\n            scores_by_missing_id36 = calculate_comment_scores(\n                link, sort, missing)\n            scores_by_missing = {\n                int(id36, 36): score\n                for id36, score in scores_by_missing_id36.iteritems()\n            }\n\n            # up to once per minute write the scores to limit writes but\n            # eventually return us to the correct state.\n            if not g.disallow_db_writes:\n                write_key = \"lock:score_{link}{sort}\".format(\n                    link=link._id36,\n                    sort=sort,\n                )\n                should_write = g.lock_cache.add(write_key, \"\", time=60)\n                if should_write:\n                    CommentScoresByLink.set_scores(\n                        link, sort, scores_by_missing_id36)\n\n            scores_by_id.update(scores_by_missing)\n            timer.intermediate('sort')\n\n    return scores_by_id\n\n\n# message conversation functions\ndef messages_key(user_id):\n    return 'message_conversations_' + str(user_id)\n\ndef messages_lock_key(user_id):\n    return 'message_conversations_lock_' + str(user_id)\n\ndef add_message(message, update_recipient=True, update_modmail=True,\n                add_to_user=None):\n    with g.make_lock(\"message_tree\", messages_lock_key(message.author_id)):\n        add_message_nolock(message.author_id, message)\n\n    if (update_recipient and message.to_id and\n            message.to_id != message.author_id):\n        with g.make_lock(\"message_tree\", messages_lock_key(message.to_id)):\n            add_message_nolock(message.to_id, message)\n\n    if update_modmail and message.sr_id:\n        with g.make_lock(\"modmail_tree\", sr_messages_lock_key(message.sr_id)):\n            add_sr_message_nolock(message.sr_id, message)\n\n    if add_to_user and add_to_user._id != message.to_id:\n        with g.make_lock(\"message_tree\", messages_lock_key(add_to_user._id)):\n            add_message_nolock(add_to_user._id, message)\n\ndef _add_message_nolock(key, message):\n    from r2.models import Account, Message\n    trees = g.permacache.get(key)\n    if not trees:\n        # in case an empty list got written at some point, delete it to\n        # force a recompute\n        if trees is not None:\n            g.permacache.delete(key)\n        # no point computing it now.  We'll do it when they go to\n        # their message page.\n        return\n\n    # if it is a new root message, easy enough\n    if message.first_message is None:\n        trees.insert(0, (message._id, []))\n    else:\n        tree_dict = dict(trees)\n\n        # if the tree already has the first message, update the list\n        if message.first_message in tree_dict:\n            if message._id not in tree_dict[message.first_message]:\n                tree_dict[message.first_message].append(message._id)\n                tree_dict[message.first_message].sort()\n        # we have to regenerate the conversation :/\n        else:\n            m = Message._query(Message.c.first_message == message.first_message,\n                               data = True)\n            new_tree = compute_message_trees(m)\n            if new_tree:\n                trees.append(new_tree[0])\n        trees.sort(key = tree_sort_fn, reverse = True)\n\n    # If we have too many messages in the tree, drop the oldest\n    # conversation to avoid the permacache size limit\n    tree_size = len(trees) + sum(len(convo[1]) for convo in trees)\n\n    if tree_size > MESSAGE_TREE_SIZE_LIMIT:\n        del trees[-1]\n\n    # done!\n    g.permacache.set(key, trees)\n\n\ndef add_message_nolock(user_id, message):\n    return _add_message_nolock(messages_key(user_id), message)\n\ndef _conversation(trees, parent):\n    from r2.models import Message\n    if parent._id in trees:\n        convo = trees[parent._id]\n        if convo:\n            m = Message._byID(convo[0], data = True)\n        if not convo or m.first_message == m.parent_id:\n            return [(parent._id, convo)]\n\n    # if we get to this point, either we didn't find the conversation,\n    # or the first child of the result was not the actual first child.\n    # To the database!\n    rules = [Message.c.first_message == parent._id]\n    if c.user_is_admin:\n        rules.append(Message.c._spam == (True, False))\n        rules.append(Message.c._deleted == (True, False))\n    m = Message._query(*rules, data=True)\n    return compute_message_trees([parent] + list(m))\n\ndef conversation(user, parent):\n    trees = dict(user_messages(user))\n    return _conversation(trees, parent)\n\n\ndef user_messages(user, update = False):\n    key = messages_key(user._id)\n    trees = g.permacache.get(key)\n    if not trees or update:\n        trees = user_messages_nocache(user)\n        g.permacache.set(key, trees)\n    return trees\n\n\ndef _load_messages(mlist):\n    from r2.models import Message\n    m = {}\n    ids = [x for x in mlist if not isinstance(x, Message)]\n    if ids:\n        m = Message._by_fullname(ids, return_dict = True, data = True)\n    messages = [m.get(x, x) for x in mlist]\n    return messages\n\ndef user_messages_nocache(user):\n    \"\"\"\n    Just like user_messages, but avoiding the cache\n    \"\"\"\n    from r2.lib.db import queries\n    inbox = queries.get_inbox_messages(user)\n    sent = queries.get_sent(user)\n    messages = _load_messages(list(chain(inbox, sent)))\n    return compute_message_trees(messages)\n\ndef sr_messages_key(sr_id):\n    return 'sr_messages_conversation_' + str(sr_id)\n\ndef sr_messages_lock_key(sr_id):\n    return 'sr_messages_conversation_lock_' + str(sr_id)\n\n\ndef subreddit_messages(sr, update = False):\n    key = sr_messages_key(sr._id)\n    trees = g.permacache.get(key)\n    if not trees or update:\n        trees = subreddit_messages_nocache(sr)\n        g.permacache.set(key, trees)\n    return trees\n\ndef moderator_messages(sr_ids):\n    from r2.models import Subreddit\n\n    srs = Subreddit._byID(sr_ids)\n    sr_ids = [sr_id for sr_id, sr in srs.iteritems()\n              if sr.is_moderator_with_perms(c.user, 'mail')]\n\n    def multi_load_tree(sr_ids):\n        res = {}\n        for sr_id in sr_ids:\n            trees = subreddit_messages_nocache(srs[sr_id])\n            if trees:\n                res[sr_id] = trees\n        return res\n\n    res = sgm(g.permacache, sr_ids, miss_fn = multi_load_tree,\n              prefix = sr_messages_key(\"\"))\n\n    return sorted(chain(*res.values()), key = tree_sort_fn, reverse = True)\n\ndef subreddit_messages_nocache(sr):\n    \"\"\"\n    Just like user_messages, but avoiding the cache\n    \"\"\"\n    from r2.lib.db import queries\n    inbox = queries.get_subreddit_messages(sr)\n    messages = _load_messages(inbox)\n    return compute_message_trees(messages)\n\n\ndef add_sr_message_nolock(sr_id, message):\n    return _add_message_nolock(sr_messages_key(sr_id), message)\n\ndef sr_conversation(sr, parent):\n    trees = dict(subreddit_messages(sr))\n    return _conversation(trees, parent)\n\n\ndef compute_message_trees(messages):\n    from r2.models import Message\n    roots = set()\n    threads = {}\n    mdict = {}\n    messages = sorted(messages, key = lambda m: m._date, reverse = True)\n\n    for m in messages:\n        mdict[m._id] = m\n        if m.first_message:\n            roots.add(m.first_message)\n            threads.setdefault(m.first_message, set()).add(m._id)\n        else:\n            roots.add(m._id)\n\n    # load any top-level messages which are not in the original list\n    missing = [m for m in roots if m not in mdict]\n    if missing:\n        mdict.update(Message._byID(tup(missing),\n                                   return_dict = True, data = True))\n\n    # sort threads in chrono order\n    for k in threads:\n        threads[k] = list(sorted(threads[k]))\n\n    tree = [(root, threads.get(root, [])) for root in roots]\n    tree.sort(key = tree_sort_fn, reverse = True)\n\n    return tree\n\ndef tree_sort_fn(tree):\n    root, threads = tree\n    return threads[-1] if threads else root\n\ndef _populate(after_id = None, estimate=54301242):\n    from r2.models import desc\n    from r2.lib.db import tdb_cassandra\n    from r2.lib import utils\n\n    # larger has a chance to decrease the number of Cassandra writes,\n    # but the probability is low\n    chunk_size = 5000\n\n    q = Comment._query(Comment.c._spam==(True,False),\n                       Comment.c._deleted==(True,False),\n                       sort=desc('_date'))\n\n    if after_id is not None:\n        q._after(Comment._byID(after_id))\n\n    q = utils.fetch_things2(q, chunk_size=chunk_size)\n    q = utils.progress(q, verbosity=chunk_size, estimate = estimate)\n\n    for chunk in utils.in_chunks(q, chunk_size):\n        chunk = filter(lambda x: hasattr(x, 'link_id'), chunk)\n        add_comments(chunk)\n"
  },
  {
    "path": "r2/r2/lib/configparse.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport re\n\n\nclass ConfigValue(object):\n    _bool_map = dict(true=True, false=False)\n\n    @staticmethod\n    def str(v, key=None):\n        return str(v)\n\n    @staticmethod\n    def int(v, key=None):\n        return int(v)\n\n    @staticmethod\n    def float(v, key=None):\n        return float(v)\n\n    @staticmethod\n    def bool(v, key=None):\n        if v in (True, False, None):\n            return bool(v)\n        try:\n            return ConfigValue._bool_map[v.lower()]\n        except KeyError:\n            raise ValueError(\"Unknown value for %r: %r\" % (key, v))\n\n    @staticmethod\n    def tuple(v, key=None):\n        return tuple(ConfigValue.to_iter(v))\n\n    @staticmethod\n    def set(v, key=None):\n        return set(ConfigValue.to_iter(v))\n\n    @staticmethod\n    def set_of(value_type, delim=','):\n        def parse(v, key=None):\n            return set(value_type(x)\n                       for x in ConfigValue.to_iter(v, delim=delim))\n        return parse\n\n    @staticmethod\n    def tuple_of(value_type, delim=','):\n        def parse(v, key=None):\n            return tuple(value_type(x)\n                         for x in ConfigValue.to_iter(v, delim=delim))\n        return parse\n\n    @staticmethod\n    def dict(key_type, value_type, delim=',', kvdelim=':'):\n        def parse(v, key=None):\n            values = (i.partition(kvdelim)\n                      for i in ConfigValue.to_iter(v, delim=delim))\n            return {key_type(x): value_type(y) for x, _,  y in values}\n        return parse\n\n    @staticmethod\n    def choice(**choices):\n        def parse_choice(v, key=None):\n            try:\n                return choices[v]\n            except KeyError:\n                raise ValueError(\"Unknown option for %r: %r not in %r\" % (key, v, choices.keys()))\n        return parse_choice\n\n    @staticmethod\n    def to_iter(v, delim = ','):\n        return (x.strip() for x in v.split(delim) if x)\n\n    @staticmethod\n    def timeinterval(v, key=None):\n        # this import is at function level because it relies on the cythonized\n        # modules being present which is a problem for plugin __init__s that\n        # use this module since they are imported in the early stages of the\n        # makefile\n        from r2.lib.utils import timeinterval_fromstr\n        return timeinterval_fromstr(v)\n\n    messages_re = re.compile(r'\"([^\"]+)\"')\n    @staticmethod\n    def messages(v, key=None):\n        return ConfigValue.messages_re.findall(v.decode(\"string_escape\"))\n\n    @staticmethod\n    def baseplate(baseplate_parser):\n        def adapter(v, key=None):\n            return baseplate_parser(v)\n        return adapter\n\n\nclass ConfigValueParser(dict):\n    def __init__(self, raw_data):\n        dict.__init__(self, raw_data)\n        self.config_keys = {}\n        self.raw_data = raw_data\n\n    def add_spec(self, spec):\n        new_keys = []\n        for parser, keys in spec.iteritems():\n            # keys can be either a list or a dict\n            for key in keys:\n                assert key not in self.config_keys\n                self.config_keys[key] = parser\n                new_keys.append(key)\n        self._update_values(new_keys)\n\n    def _update_values(self, keys):\n        for key in keys:\n            if key not in self.raw_data:\n                continue\n\n            value = self.raw_data[key]\n            if key in self.config_keys:\n                parser = self.config_keys[key]\n                value = parser(value, key)\n            self[key] = value\n"
  },
  {
    "path": "r2/r2/lib/contrib/__init__.py",
    "content": ""
  },
  {
    "path": "r2/r2/lib/contrib/activity.thrift",
    "content": "include \"baseplate.thrift\"\n\n/** A unique identifier for a given \"context\".\n\nA context is an area of the service which a user may be active within, such as\na subreddit or live thread.\n\n*/\ntypedef string ContextID\n\n/** A unique identifier for a given visitor.\n\nA visitor may be a logged-in user's ID or a logged-out user's LOID value. The\nvalue is not actually stored, but only used to update the internal counters.\n\n*/\ntypedef string VisitorID\n\n\n/** A count of visitors active within a context.\n\nIf the count is low enough, some fuzzing is applied to the number. If this\nkicks in, the `is_fuzzed` attribute will be True.\n\n*/\nstruct ActivityInfo {\n    1: optional i32 count;\n    2: optional bool is_fuzzed;\n}\n\n/** A specified context ID was invalid */\nexception InvalidContextIDException {\n}\n\nservice ActivityService extends baseplate.BaseplateService {\n    /** Register a visitor's activity within a given context.\n\n    The visitor's activity will be recorded but will expire over time. If the\n    user continues to be active within the context, this endpoint should be\n    called occasionally to ensure they continue to be counted.\n\n    This method is `oneway`; no indication of success or failure is returned.\n\n    */\n    oneway void record_activity(1: ContextID context_id, 2: VisitorID visitor_id),\n\n    /** Count how many visitors are currently active in a given context.\n\n    The results of this call are cached for a period of time to ensure that if\n    the count is fuzzed, the fuzzing is stable. This prevents repeated requests\n    from revealing the range of fuzzing and therefore the true value.\n\n    */\n    ActivityInfo count_activity(1: ContextID context_id)\n        throws (1: InvalidContextIDException invalid_context_id),\n\n    /** Count how many visitors are active in a number of given contexts.\n\n    This is the same as `count_activity` but allows for querying in batch.\n\n    */\n    map<ContextID, ActivityInfo> count_activity_multi(1: list<ContextID> context_ids)\n        throws (1: InvalidContextIDException invalid_context_id),\n}\n"
  },
  {
    "path": "r2/r2/lib/contrib/dtds/README",
    "content": "This directory provides a minimal DTD defining XHTML entities, for parsing\nHTML generated by markdown.\n"
  },
  {
    "path": "r2/r2/lib/contrib/dtds/allowed_entities.dtd",
    "content": "\n<!--\n     Copyright 1998 - 2011 W3C,\n\n     Use and distribution of this code are permitted under the terms of\n     either of the following two licences:\n\n     1) W3C Software Notice and License.\n        http://www.w3.org/Consortium/Legal/2002/copyright-software-20021231.html\n\n\n     2) The license used for the WHATWG HTML specification,\n        which states, in full:\n            You are granted a license to use, reproduce and create derivative\n            works of this document.\n\n-->\n\n<!--\n    This is a special DTD, mostly collated a version of the\n    following entity files:\n      http://www.w3.org/2003/entities/2007/xhtml1-lat1.ent\n      http://www.w3.org/2003/entities/2007/xhtml1-special.ent\n      http://www.w3.org/2003/entities/2007/xhtml1-symbol.ent\n\n    created on Feb 24 2015 for validating that fragments in an HTML fragment\n    would be safe for use in both XHTML and HTML5\n\n-->\n\n<!-- base XML -->\n\n<!ENTITY amp \"&#38;#38;\">\n<!ENTITY apos \"&#39;\">\n\n<!-- xhtml1-lat1 -->\n\n<!ENTITY aacute           \"&#x000E1;\" ><!--LATIN SMALL LETTER A WITH ACUTE -->\n<!ENTITY Aacute           \"&#x000C1;\" ><!--LATIN CAPITAL LETTER A WITH ACUTE -->\n<!ENTITY acirc            \"&#x000E2;\" ><!--LATIN SMALL LETTER A WITH CIRCUMFLEX -->\n<!ENTITY Acirc            \"&#x000C2;\" ><!--LATIN CAPITAL LETTER A WITH CIRCUMFLEX -->\n<!ENTITY acute            \"&#x000B4;\" ><!--ACUTE ACCENT -->\n<!ENTITY aelig            \"&#x000E6;\" ><!--LATIN SMALL LETTER AE -->\n<!ENTITY AElig            \"&#x000C6;\" ><!--LATIN CAPITAL LETTER AE -->\n<!ENTITY agrave           \"&#x000E0;\" ><!--LATIN SMALL LETTER A WITH GRAVE -->\n<!ENTITY Agrave           \"&#x000C0;\" ><!--LATIN CAPITAL LETTER A WITH GRAVE -->\n<!ENTITY aring            \"&#x000E5;\" ><!--LATIN SMALL LETTER A WITH RING ABOVE -->\n<!ENTITY Aring            \"&#x000C5;\" ><!--LATIN CAPITAL LETTER A WITH RING ABOVE -->\n<!ENTITY atilde           \"&#x000E3;\" ><!--LATIN SMALL LETTER A WITH TILDE -->\n<!ENTITY Atilde           \"&#x000C3;\" ><!--LATIN CAPITAL LETTER A WITH TILDE -->\n<!ENTITY auml             \"&#x000E4;\" ><!--LATIN SMALL LETTER A WITH DIAERESIS -->\n<!ENTITY Auml             \"&#x000C4;\" ><!--LATIN CAPITAL LETTER A WITH DIAERESIS -->\n<!ENTITY brvbar           \"&#x000A6;\" ><!--BROKEN BAR -->\n<!ENTITY ccedil           \"&#x000E7;\" ><!--LATIN SMALL LETTER C WITH CEDILLA -->\n<!ENTITY Ccedil           \"&#x000C7;\" ><!--LATIN CAPITAL LETTER C WITH CEDILLA -->\n<!ENTITY cedil            \"&#x000B8;\" ><!--CEDILLA -->\n<!ENTITY cent             \"&#x000A2;\" ><!--CENT SIGN -->\n<!ENTITY copy             \"&#x000A9;\" ><!--COPYRIGHT SIGN -->\n<!ENTITY curren           \"&#x000A4;\" ><!--CURRENCY SIGN -->\n<!ENTITY deg              \"&#x000B0;\" ><!--DEGREE SIGN -->\n<!ENTITY divide           \"&#x000F7;\" ><!--DIVISION SIGN -->\n<!ENTITY eacute           \"&#x000E9;\" ><!--LATIN SMALL LETTER E WITH ACUTE -->\n<!ENTITY Eacute           \"&#x000C9;\" ><!--LATIN CAPITAL LETTER E WITH ACUTE -->\n<!ENTITY ecirc            \"&#x000EA;\" ><!--LATIN SMALL LETTER E WITH CIRCUMFLEX -->\n<!ENTITY Ecirc            \"&#x000CA;\" ><!--LATIN CAPITAL LETTER E WITH CIRCUMFLEX -->\n<!ENTITY egrave           \"&#x000E8;\" ><!--LATIN SMALL LETTER E WITH GRAVE -->\n<!ENTITY Egrave           \"&#x000C8;\" ><!--LATIN CAPITAL LETTER E WITH GRAVE -->\n<!ENTITY eth              \"&#x000F0;\" ><!--LATIN SMALL LETTER ETH -->\n<!ENTITY ETH              \"&#x000D0;\" ><!--LATIN CAPITAL LETTER ETH -->\n<!ENTITY euml             \"&#x000EB;\" ><!--LATIN SMALL LETTER E WITH DIAERESIS -->\n<!ENTITY Euml             \"&#x000CB;\" ><!--LATIN CAPITAL LETTER E WITH DIAERESIS -->\n<!ENTITY frac12           \"&#x000BD;\" ><!--VULGAR FRACTION ONE HALF -->\n<!ENTITY frac14           \"&#x000BC;\" ><!--VULGAR FRACTION ONE QUARTER -->\n<!ENTITY frac34           \"&#x000BE;\" ><!--VULGAR FRACTION THREE QUARTERS -->\n<!ENTITY iacute           \"&#x000ED;\" ><!--LATIN SMALL LETTER I WITH ACUTE -->\n<!ENTITY Iacute           \"&#x000CD;\" ><!--LATIN CAPITAL LETTER I WITH ACUTE -->\n<!ENTITY icirc            \"&#x000EE;\" ><!--LATIN SMALL LETTER I WITH CIRCUMFLEX -->\n<!ENTITY Icirc            \"&#x000CE;\" ><!--LATIN CAPITAL LETTER I WITH CIRCUMFLEX -->\n<!ENTITY iexcl            \"&#x000A1;\" ><!--INVERTED EXCLAMATION MARK -->\n<!ENTITY igrave           \"&#x000EC;\" ><!--LATIN SMALL LETTER I WITH GRAVE -->\n<!ENTITY Igrave           \"&#x000CC;\" ><!--LATIN CAPITAL LETTER I WITH GRAVE -->\n<!ENTITY iquest           \"&#x000BF;\" ><!--INVERTED QUESTION MARK -->\n<!ENTITY iuml             \"&#x000EF;\" ><!--LATIN SMALL LETTER I WITH DIAERESIS -->\n<!ENTITY Iuml             \"&#x000CF;\" ><!--LATIN CAPITAL LETTER I WITH DIAERESIS -->\n<!ENTITY laquo            \"&#x000AB;\" ><!--LEFT-POINTING DOUBLE ANGLE QUOTATION MARK -->\n<!ENTITY macr             \"&#x000AF;\" ><!--MACRON -->\n<!ENTITY micro            \"&#x000B5;\" ><!--MICRO SIGN -->\n<!ENTITY middot           \"&#x000B7;\" ><!--MIDDLE DOT -->\n<!ENTITY nbsp             \"&#x000A0;\" ><!--NO-BREAK SPACE -->\n<!ENTITY not              \"&#x000AC;\" ><!--NOT SIGN -->\n<!ENTITY ntilde           \"&#x000F1;\" ><!--LATIN SMALL LETTER N WITH TILDE -->\n<!ENTITY Ntilde           \"&#x000D1;\" ><!--LATIN CAPITAL LETTER N WITH TILDE -->\n<!ENTITY oacute           \"&#x000F3;\" ><!--LATIN SMALL LETTER O WITH ACUTE -->\n<!ENTITY Oacute           \"&#x000D3;\" ><!--LATIN CAPITAL LETTER O WITH ACUTE -->\n<!ENTITY ocirc            \"&#x000F4;\" ><!--LATIN SMALL LETTER O WITH CIRCUMFLEX -->\n<!ENTITY Ocirc            \"&#x000D4;\" ><!--LATIN CAPITAL LETTER O WITH CIRCUMFLEX -->\n<!ENTITY ograve           \"&#x000F2;\" ><!--LATIN SMALL LETTER O WITH GRAVE -->\n<!ENTITY Ograve           \"&#x000D2;\" ><!--LATIN CAPITAL LETTER O WITH GRAVE -->\n<!ENTITY ordf             \"&#x000AA;\" ><!--FEMININE ORDINAL INDICATOR -->\n<!ENTITY ordm             \"&#x000BA;\" ><!--MASCULINE ORDINAL INDICATOR -->\n<!ENTITY oslash           \"&#x000F8;\" ><!--LATIN SMALL LETTER O WITH STROKE -->\n<!ENTITY Oslash           \"&#x000D8;\" ><!--LATIN CAPITAL LETTER O WITH STROKE -->\n<!ENTITY otilde           \"&#x000F5;\" ><!--LATIN SMALL LETTER O WITH TILDE -->\n<!ENTITY Otilde           \"&#x000D5;\" ><!--LATIN CAPITAL LETTER O WITH TILDE -->\n<!ENTITY ouml             \"&#x000F6;\" ><!--LATIN SMALL LETTER O WITH DIAERESIS -->\n<!ENTITY Ouml             \"&#x000D6;\" ><!--LATIN CAPITAL LETTER O WITH DIAERESIS -->\n<!ENTITY para             \"&#x000B6;\" ><!--PILCROW SIGN -->\n<!ENTITY plusmn           \"&#x000B1;\" ><!--PLUS-MINUS SIGN -->\n<!ENTITY pound            \"&#x000A3;\" ><!--POUND SIGN -->\n<!ENTITY raquo            \"&#x000BB;\" ><!--RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK -->\n<!ENTITY reg              \"&#x000AE;\" ><!--REGISTERED SIGN -->\n<!ENTITY sect             \"&#x000A7;\" ><!--SECTION SIGN -->\n<!ENTITY shy              \"&#x000AD;\" ><!--SOFT HYPHEN -->\n<!ENTITY sup1             \"&#x000B9;\" ><!--SUPERSCRIPT ONE -->\n<!ENTITY sup2             \"&#x000B2;\" ><!--SUPERSCRIPT TWO -->\n<!ENTITY sup3             \"&#x000B3;\" ><!--SUPERSCRIPT THREE -->\n<!ENTITY szlig            \"&#x000DF;\" ><!--LATIN SMALL LETTER SHARP S -->\n<!ENTITY thorn            \"&#x000FE;\" ><!--LATIN SMALL LETTER THORN -->\n<!ENTITY THORN            \"&#x000DE;\" ><!--LATIN CAPITAL LETTER THORN -->\n<!ENTITY times            \"&#x000D7;\" ><!--MULTIPLICATION SIGN -->\n<!ENTITY uacute           \"&#x000FA;\" ><!--LATIN SMALL LETTER U WITH ACUTE -->\n<!ENTITY Uacute           \"&#x000DA;\" ><!--LATIN CAPITAL LETTER U WITH ACUTE -->\n<!ENTITY ucirc            \"&#x000FB;\" ><!--LATIN SMALL LETTER U WITH CIRCUMFLEX -->\n<!ENTITY Ucirc            \"&#x000DB;\" ><!--LATIN CAPITAL LETTER U WITH CIRCUMFLEX -->\n<!ENTITY ugrave           \"&#x000F9;\" ><!--LATIN SMALL LETTER U WITH GRAVE -->\n<!ENTITY Ugrave           \"&#x000D9;\" ><!--LATIN CAPITAL LETTER U WITH GRAVE -->\n<!ENTITY uml              \"&#x000A8;\" ><!--DIAERESIS -->\n<!ENTITY uuml             \"&#x000FC;\" ><!--LATIN SMALL LETTER U WITH DIAERESIS -->\n<!ENTITY Uuml             \"&#x000DC;\" ><!--LATIN CAPITAL LETTER U WITH DIAERESIS -->\n<!ENTITY yacute           \"&#x000FD;\" ><!--LATIN SMALL LETTER Y WITH ACUTE -->\n<!ENTITY Yacute           \"&#x000DD;\" ><!--LATIN CAPITAL LETTER Y WITH ACUTE -->\n<!ENTITY yen              \"&#x000A5;\" ><!--YEN SIGN -->\n<!ENTITY yuml             \"&#x000FF;\" ><!--LATIN SMALL LETTER Y WITH DIAERESIS -->\n\n<!-- xhtml1-special -->\n\n<!ENTITY bdquo            \"&#x0201E;\" ><!--DOUBLE LOW-9 QUOTATION MARK -->\n<!ENTITY circ             \"&#x002C6;\" ><!--MODIFIER LETTER CIRCUMFLEX ACCENT -->\n<!ENTITY dagger           \"&#x02020;\" ><!--DAGGER -->\n<!ENTITY Dagger           \"&#x02021;\" ><!--DOUBLE DAGGER -->\n<!ENTITY emsp             \"&#x02003;\" ><!--EM SPACE -->\n<!ENTITY ensp             \"&#x02002;\" ><!--EN SPACE -->\n<!ENTITY euro             \"&#x020AC;\" ><!--EURO SIGN -->\n<!ENTITY gt               \"&#x0003E;\" ><!--GREATER-THAN SIGN -->\n<!ENTITY ldquo            \"&#x0201C;\" ><!--LEFT DOUBLE QUOTATION MARK -->\n<!ENTITY lrm              \"&#x0200E;\" ><!--LEFT-TO-RIGHT MARK -->\n<!ENTITY lsaquo           \"&#x02039;\" ><!--SINGLE LEFT-POINTING ANGLE QUOTATION MARK -->\n<!ENTITY lsquo            \"&#x02018;\" ><!--LEFT SINGLE QUOTATION MARK -->\n<!ENTITY lt               \"&#38;#60;\" ><!--LESS-THAN SIGN -->\n<!ENTITY mdash            \"&#x02014;\" ><!--EM DASH -->\n<!ENTITY ndash            \"&#x02013;\" ><!--EN DASH -->\n<!ENTITY oelig            \"&#x00153;\" ><!--LATIN SMALL LIGATURE OE -->\n<!ENTITY OElig            \"&#x00152;\" ><!--LATIN CAPITAL LIGATURE OE -->\n<!ENTITY permil           \"&#x02030;\" ><!--PER MILLE SIGN -->\n<!ENTITY quot             \"&#x00022;\" ><!--QUOTATION MARK -->\n<!ENTITY rdquo            \"&#x0201D;\" ><!--RIGHT DOUBLE QUOTATION MARK -->\n<!ENTITY rlm              \"&#x0200F;\" ><!--RIGHT-TO-LEFT MARK -->\n<!ENTITY rsaquo           \"&#x0203A;\" ><!--SINGLE RIGHT-POINTING ANGLE QUOTATION MARK -->\n<!ENTITY rsquo            \"&#x02019;\" ><!--RIGHT SINGLE QUOTATION MARK -->\n<!ENTITY sbquo            \"&#x0201A;\" ><!--SINGLE LOW-9 QUOTATION MARK -->\n<!ENTITY scaron           \"&#x00161;\" ><!--LATIN SMALL LETTER S WITH CARON -->\n<!ENTITY Scaron           \"&#x00160;\" ><!--LATIN CAPITAL LETTER S WITH CARON -->\n<!ENTITY thinsp           \"&#x02009;\" ><!--THIN SPACE -->\n<!ENTITY tilde            \"&#x002DC;\" ><!--SMALL TILDE -->\n<!ENTITY Yuml             \"&#x00178;\" ><!--LATIN CAPITAL LETTER Y WITH DIAERESIS -->\n<!ENTITY zwj              \"&#x0200D;\" ><!--ZERO WIDTH JOINER -->\n<!ENTITY zwnj             \"&#x0200C;\" ><!--ZERO WIDTH NON-JOINER -->\n\n<!-- xhtml1-symbol -->\n\n<!ENTITY alefsym          \"&#x02135;\" ><!--ALEF SYMBOL -->\n<!ENTITY alpha            \"&#x003B1;\" ><!--GREEK SMALL LETTER ALPHA -->\n<!ENTITY Alpha            \"&#x00391;\" ><!--GREEK CAPITAL LETTER ALPHA -->\n<!ENTITY and              \"&#x02227;\" ><!--LOGICAL AND -->\n<!ENTITY ang              \"&#x02220;\" ><!--ANGLE -->\n<!ENTITY asymp            \"&#x02248;\" ><!--ALMOST EQUAL TO -->\n<!ENTITY beta             \"&#x003B2;\" ><!--GREEK SMALL LETTER BETA -->\n<!ENTITY Beta             \"&#x00392;\" ><!--GREEK CAPITAL LETTER BETA -->\n<!ENTITY bull             \"&#x02022;\" ><!--BULLET -->\n<!ENTITY cap              \"&#x02229;\" ><!--INTERSECTION -->\n<!ENTITY chi              \"&#x003C7;\" ><!--GREEK SMALL LETTER CHI -->\n<!ENTITY Chi              \"&#x003A7;\" ><!--GREEK CAPITAL LETTER CHI -->\n<!ENTITY clubs            \"&#x02663;\" ><!--BLACK CLUB SUIT -->\n<!ENTITY cong             \"&#x02245;\" ><!--APPROXIMATELY EQUAL TO -->\n<!ENTITY crarr            \"&#x021B5;\" ><!--DOWNWARDS ARROW WITH CORNER LEFTWARDS -->\n<!ENTITY cup              \"&#x0222A;\" ><!--UNION -->\n<!ENTITY darr             \"&#x02193;\" ><!--DOWNWARDS ARROW -->\n<!ENTITY dArr             \"&#x021D3;\" ><!--DOWNWARDS DOUBLE ARROW -->\n<!ENTITY delta            \"&#x003B4;\" ><!--GREEK SMALL LETTER DELTA -->\n<!ENTITY Delta            \"&#x00394;\" ><!--GREEK CAPITAL LETTER DELTA -->\n<!ENTITY diams            \"&#x02666;\" ><!--BLACK DIAMOND SUIT -->\n<!ENTITY empty            \"&#x02205;\" ><!--EMPTY SET -->\n<!ENTITY epsilon          \"&#x003B5;\" ><!--GREEK SMALL LETTER EPSILON -->\n<!ENTITY Epsilon          \"&#x00395;\" ><!--GREEK CAPITAL LETTER EPSILON -->\n<!ENTITY equiv            \"&#x02261;\" ><!--IDENTICAL TO -->\n<!ENTITY eta              \"&#x003B7;\" ><!--GREEK SMALL LETTER ETA -->\n<!ENTITY Eta              \"&#x00397;\" ><!--GREEK CAPITAL LETTER ETA -->\n<!ENTITY exist            \"&#x02203;\" ><!--THERE EXISTS -->\n<!ENTITY fnof             \"&#x00192;\" ><!--LATIN SMALL LETTER F WITH HOOK -->\n<!ENTITY forall           \"&#x02200;\" ><!--FOR ALL -->\n<!ENTITY frasl            \"&#x02044;\" ><!--FRACTION SLASH -->\n<!ENTITY gamma            \"&#x003B3;\" ><!--GREEK SMALL LETTER GAMMA -->\n<!ENTITY Gamma            \"&#x00393;\" ><!--GREEK CAPITAL LETTER GAMMA -->\n<!ENTITY ge               \"&#x02265;\" ><!--GREATER-THAN OR EQUAL TO -->\n<!ENTITY harr             \"&#x02194;\" ><!--LEFT RIGHT ARROW -->\n<!ENTITY hArr             \"&#x021D4;\" ><!--LEFT RIGHT DOUBLE ARROW -->\n<!ENTITY hearts           \"&#x02665;\" ><!--BLACK HEART SUIT -->\n<!ENTITY hellip           \"&#x02026;\" ><!--HORIZONTAL ELLIPSIS -->\n<!ENTITY image            \"&#x02111;\" ><!--BLACK-LETTER CAPITAL I -->\n<!ENTITY infin            \"&#x0221E;\" ><!--INFINITY -->\n<!ENTITY int              \"&#x0222B;\" ><!--INTEGRAL -->\n<!ENTITY iota             \"&#x003B9;\" ><!--GREEK SMALL LETTER IOTA -->\n<!ENTITY Iota             \"&#x00399;\" ><!--GREEK CAPITAL LETTER IOTA -->\n<!ENTITY isin             \"&#x02208;\" ><!--ELEMENT OF -->\n<!ENTITY kappa            \"&#x003BA;\" ><!--GREEK SMALL LETTER KAPPA -->\n<!ENTITY Kappa            \"&#x0039A;\" ><!--GREEK CAPITAL LETTER KAPPA -->\n<!ENTITY lambda           \"&#x003BB;\" ><!--GREEK SMALL LETTER LAMDA -->\n<!ENTITY Lambda           \"&#x0039B;\" ><!--GREEK CAPITAL LETTER LAMDA -->\n<!ENTITY lang             \"&#x027E8;\" ><!--MATHEMATICAL LEFT ANGLE BRACKET -->\n<!ENTITY larr             \"&#x02190;\" ><!--LEFTWARDS ARROW -->\n<!ENTITY lArr             \"&#x021D0;\" ><!--LEFTWARDS DOUBLE ARROW -->\n<!ENTITY lceil            \"&#x02308;\" ><!--LEFT CEILING -->\n<!ENTITY le               \"&#x02264;\" ><!--LESS-THAN OR EQUAL TO -->\n<!ENTITY lfloor           \"&#x0230A;\" ><!--LEFT FLOOR -->\n<!ENTITY lowast           \"&#x02217;\" ><!--ASTERISK OPERATOR -->\n<!ENTITY loz              \"&#x025CA;\" ><!--LOZENGE -->\n<!ENTITY minus            \"&#x02212;\" ><!--MINUS SIGN -->\n<!ENTITY mu               \"&#x003BC;\" ><!--GREEK SMALL LETTER MU -->\n<!ENTITY Mu               \"&#x0039C;\" ><!--GREEK CAPITAL LETTER MU -->\n<!ENTITY nabla            \"&#x02207;\" ><!--NABLA -->\n<!ENTITY ne               \"&#x02260;\" ><!--NOT EQUAL TO -->\n<!ENTITY ni               \"&#x0220B;\" ><!--CONTAINS AS MEMBER -->\n<!ENTITY notin            \"&#x02209;\" ><!--NOT AN ELEMENT OF -->\n<!ENTITY nsub             \"&#x02284;\" ><!--NOT A SUBSET OF -->\n<!ENTITY nu               \"&#x003BD;\" ><!--GREEK SMALL LETTER NU -->\n<!ENTITY Nu               \"&#x0039D;\" ><!--GREEK CAPITAL LETTER NU -->\n<!ENTITY oline            \"&#x0203E;\" ><!--OVERLINE -->\n<!ENTITY omega            \"&#x003C9;\" ><!--GREEK SMALL LETTER OMEGA -->\n<!ENTITY Omega            \"&#x003A9;\" ><!--GREEK CAPITAL LETTER OMEGA -->\n<!ENTITY omicron          \"&#x003BF;\" ><!--GREEK SMALL LETTER OMICRON -->\n<!ENTITY Omicron          \"&#x0039F;\" ><!--GREEK CAPITAL LETTER OMICRON -->\n<!ENTITY oplus            \"&#x02295;\" ><!--CIRCLED PLUS -->\n<!ENTITY or               \"&#x02228;\" ><!--LOGICAL OR -->\n<!ENTITY otimes           \"&#x02297;\" ><!--CIRCLED TIMES -->\n<!ENTITY part             \"&#x02202;\" ><!--PARTIAL DIFFERENTIAL -->\n<!ENTITY perp             \"&#x022A5;\" ><!--UP TACK -->\n<!ENTITY phi              \"&#x003C6;\" ><!--GREEK SMALL LETTER PHI -->\n<!ENTITY Phi              \"&#x003A6;\" ><!--GREEK CAPITAL LETTER PHI -->\n<!ENTITY pi               \"&#x003C0;\" ><!--GREEK SMALL LETTER PI -->\n<!ENTITY Pi               \"&#x003A0;\" ><!--GREEK CAPITAL LETTER PI -->\n<!ENTITY piv              \"&#x003D6;\" ><!--GREEK PI SYMBOL -->\n<!ENTITY prime            \"&#x02032;\" ><!--PRIME -->\n<!ENTITY Prime            \"&#x02033;\" ><!--DOUBLE PRIME -->\n<!ENTITY prod             \"&#x0220F;\" ><!--N-ARY PRODUCT -->\n<!ENTITY prop             \"&#x0221D;\" ><!--PROPORTIONAL TO -->\n<!ENTITY psi              \"&#x003C8;\" ><!--GREEK SMALL LETTER PSI -->\n<!ENTITY Psi              \"&#x003A8;\" ><!--GREEK CAPITAL LETTER PSI -->\n<!ENTITY radic            \"&#x0221A;\" ><!--SQUARE ROOT -->\n<!ENTITY rang             \"&#x027E9;\" ><!--MATHEMATICAL RIGHT ANGLE BRACKET -->\n<!ENTITY rarr             \"&#x02192;\" ><!--RIGHTWARDS ARROW -->\n<!ENTITY rArr             \"&#x021D2;\" ><!--RIGHTWARDS DOUBLE ARROW -->\n<!ENTITY rceil            \"&#x02309;\" ><!--RIGHT CEILING -->\n<!ENTITY real             \"&#x0211C;\" ><!--BLACK-LETTER CAPITAL R -->\n<!ENTITY rfloor           \"&#x0230B;\" ><!--RIGHT FLOOR -->\n<!ENTITY rho              \"&#x003C1;\" ><!--GREEK SMALL LETTER RHO -->\n<!ENTITY Rho              \"&#x003A1;\" ><!--GREEK CAPITAL LETTER RHO -->\n<!ENTITY sdot             \"&#x022C5;\" ><!--DOT OPERATOR -->\n<!ENTITY sigma            \"&#x003C3;\" ><!--GREEK SMALL LETTER SIGMA -->\n<!ENTITY Sigma            \"&#x003A3;\" ><!--GREEK CAPITAL LETTER SIGMA -->\n<!ENTITY sigmaf           \"&#x003C2;\" ><!--GREEK SMALL LETTER FINAL SIGMA -->\n<!ENTITY sim              \"&#x0223C;\" ><!--TILDE OPERATOR -->\n<!ENTITY spades           \"&#x02660;\" ><!--BLACK SPADE SUIT -->\n<!ENTITY sub              \"&#x02282;\" ><!--SUBSET OF -->\n<!ENTITY sube             \"&#x02286;\" ><!--SUBSET OF OR EQUAL TO -->\n<!ENTITY sum              \"&#x02211;\" ><!--N-ARY SUMMATION -->\n<!ENTITY sup              \"&#x02283;\" ><!--SUPERSET OF -->\n<!ENTITY supe             \"&#x02287;\" ><!--SUPERSET OF OR EQUAL TO -->\n<!ENTITY tau              \"&#x003C4;\" ><!--GREEK SMALL LETTER TAU -->\n<!ENTITY Tau              \"&#x003A4;\" ><!--GREEK CAPITAL LETTER TAU -->\n<!ENTITY there4           \"&#x02234;\" ><!--THEREFORE -->\n<!ENTITY theta            \"&#x003B8;\" ><!--GREEK SMALL LETTER THETA -->\n<!ENTITY Theta            \"&#x00398;\" ><!--GREEK CAPITAL LETTER THETA -->\n<!ENTITY thetasym         \"&#x003D1;\" ><!--GREEK THETA SYMBOL -->\n<!ENTITY trade            \"&#x02122;\" ><!--TRADE MARK SIGN -->\n<!ENTITY uarr             \"&#x02191;\" ><!--UPWARDS ARROW -->\n<!ENTITY uArr             \"&#x021D1;\" ><!--UPWARDS DOUBLE ARROW -->\n<!ENTITY upsih            \"&#x003D2;\" ><!--GREEK UPSILON WITH HOOK SYMBOL -->\n<!ENTITY upsilon          \"&#x003C5;\" ><!--GREEK SMALL LETTER UPSILON -->\n<!ENTITY Upsilon          \"&#x003A5;\" ><!--GREEK CAPITAL LETTER UPSILON -->\n<!ENTITY weierp           \"&#x02118;\" ><!--SCRIPT CAPITAL P -->\n<!ENTITY xi               \"&#x003BE;\" ><!--GREEK SMALL LETTER XI -->\n<!ENTITY Xi               \"&#x0039E;\" ><!--GREEK CAPITAL LETTER XI -->\n<!ENTITY zeta             \"&#x003B6;\" ><!--GREEK SMALL LETTER ZETA -->\n<!ENTITY Zeta             \"&#x00396;\" ><!--GREEK CAPITAL LETTER ZETA -->\n"
  },
  {
    "path": "r2/r2/lib/contrib/ipaddress.py",
    "content": "#!/usr/bin/python3\n#\n# Copyright 2007 Google Inc.\n#  Licensed to PSF under a Contributor Agreement.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n# implied. See the License for the specific language governing\n# permissions and limitations under the License.\n\n\"\"\"A fast, lightweight IPv4/IPv6 manipulation library in Python.\n\nThis library is used to create/poke/manipulate IPv4 and IPv6 addresses\nand networks.\n\n\"\"\"\n\n__version__ = '1.0'\n\nimport struct\n\nIPV4LENGTH = 32\nIPV6LENGTH = 128\n\n\nclass AddressValueError(ValueError):\n    \"\"\"A Value Error related to the address.\"\"\"\n\n\nclass NetmaskValueError(ValueError):\n    \"\"\"A Value Error related to the netmask.\"\"\"\n\n\ndef ip_address(address, version=None):\n    \"\"\"Take an IP string/int and return an object of the correct type.\n\n    Args:\n        address: A string or integer, the IP address.  Either IPv4 or\n          IPv6 addresses may be supplied; integers less than 2**32 will\n          be considered to be IPv4 by default.\n        version: An Integer, 4 or 6. If set, don't try to automatically\n          determine what the IP address type is. important for things\n          like ip_address(1), which could be IPv4, '192.0.2.1',  or IPv6,\n          '2001:db8::1'.\n\n    Returns:\n        An IPv4Address or IPv6Address object.\n\n    Raises:\n        ValueError: if the string passed isn't either a v4 or a v6\n          address.\n\n    \"\"\"\n    if version:\n        if version == 4:\n            return IPv4Address(address)\n        elif version == 6:\n            return IPv6Address(address)\n\n    try:\n        return IPv4Address(address)\n    except (AddressValueError, NetmaskValueError):\n        pass\n\n    try:\n        return IPv6Address(address)\n    except (AddressValueError, NetmaskValueError):\n        pass\n\n    raise ValueError('%r does not appear to be an IPv4 or IPv6 address' %\n                     address)\n\n\ndef ip_network(address, version=None, strict=True):\n    \"\"\"Take an IP string/int and return an object of the correct type.\n\n    Args:\n        address: A string or integer, the IP network.  Either IPv4 or\n          IPv6 networks may be supplied; integers less than 2**32 will\n          be considered to be IPv4 by default.\n        version: An Integer, if set, don't try to automatically\n          determine what the IP address type is. important for things\n          like ip_network(1), which could be IPv4, '192.0.2.1/32', or IPv6,\n          '2001:db8::1/128'.\n\n    Returns:\n        An IPv4Network or IPv6Network object.\n\n    Raises:\n        ValueError: if the string passed isn't either a v4 or a v6\n          address. Or if the network has host bits set.\n\n    \"\"\"\n    if version:\n        if version == 4:\n            return IPv4Network(address, strict)\n        elif version == 6:\n            return IPv6Network(address, strict)\n\n    try:\n        return IPv4Network(address, strict)\n    except (AddressValueError, NetmaskValueError):\n        pass\n\n    try:\n        return IPv6Network(address, strict)\n    except (AddressValueError, NetmaskValueError):\n        pass\n\n    raise ValueError('%r does not appear to be an IPv4 or IPv6 network' %\n                     address)\n\n\ndef ip_interface(address, version=None):\n    \"\"\"Take an IP string/int and return an object of the correct type.\n\n    Args:\n        address: A string or integer, the IP address.  Either IPv4 or\n          IPv6 addresses may be supplied; integers less than 2**32 will\n          be considered to be IPv4 by default.\n        version: An Integer, if set, don't try to automatically\n          determine what the IP address type is. important for things\n          like ip_network(1), which could be IPv4, '192.0.2.1/32', or IPv6,\n          '2001:db8::1/128'.\n\n    Returns:\n        An IPv4Network or IPv6Network object.\n\n    Raises:\n        ValueError: if the string passed isn't either a v4 or a v6\n          address.\n\n    Notes:\n        The IPv?Interface classes describe an Address on a particular\n        Network, so they're basically a combination of both the Address\n        and Network classes.\n    \"\"\"\n    if version:\n        if version == 4:\n            return IPv4Interface(address)\n        elif version == 6:\n            return IPv6Interface(address)\n\n    try:\n        return IPv4Interface(address)\n    except (AddressValueError, NetmaskValueError):\n        pass\n\n    try:\n        return IPv6Interface(address)\n    except (AddressValueError, NetmaskValueError):\n        pass\n\n    raise ValueError('%r does not appear to be an IPv4 or IPv6 network' %\n                     address)\n\n\ndef v4_int_to_packed(address):\n    \"\"\"The binary representation of this address.\n\n    Args:\n        address: An integer representation of an IPv4 IP address.\n\n    Returns:\n        The binary representation of this address.\n\n    Raises:\n        ValueError: If the integer is too large to be an IPv4 IP\n          address.\n    \"\"\"\n    if address > _BaseV4._ALL_ONES:\n        raise ValueError('Address too large for IPv4')\n    return struct.pack('!I', address)\n\n\ndef v6_int_to_packed(address):\n    \"\"\"The binary representation of this address.\n\n    Args:\n        address: An integer representation of an IPv6 IP address.\n\n    Returns:\n        The binary representation of this address.\n    \"\"\"\n    return struct.pack('!QQ', address >> 64, address & (2**64 - 1))\n\n\ndef _find_address_range(addresses):\n    \"\"\"Find a sequence of addresses.\n\n    Args:\n        addresses: a list of IPv4 or IPv6 addresses.\n\n    Returns:\n        A tuple containing the first and last IP addresses in the sequence.\n\n    \"\"\"\n    first = last = addresses[0]\n    for ip in addresses[1:]:\n        if ip._ip == last._ip + 1:\n            last = ip\n        else:\n            break\n    return (first, last)\n\ndef _get_prefix_length(number1, number2, bits):\n    \"\"\"Get the number of leading bits that are same for two numbers.\n\n    Args:\n        number1: an integer.\n        number2: another integer.\n        bits: the maximum number of bits to compare.\n\n    Returns:\n        The number of leading bits that are the same for two numbers.\n\n    \"\"\"\n    for i in range(bits):\n        if number1 >> i == number2 >> i:\n            return bits - i\n    return 0\n\ndef _count_righthand_zero_bits(number, bits):\n    \"\"\"Count the number of zero bits on the right hand side.\n\n    Args:\n        number: an integer.\n        bits: maximum number of bits to count.\n\n    Returns:\n        The number of zero bits on the right hand side of the number.\n\n    \"\"\"\n    if number == 0:\n        return bits\n    for i in range(bits):\n        if (number >> i) % 2:\n            return i\n\n\ndef summarize_address_range(first, last):\n    \"\"\"Summarize a network range given the first and last IP addresses.\n\n    Example:\n        >>> summarize_address_range(IPv4Address('192.0.2.0'),\n            IPv4Address('192.0.2.130'))\n        [IPv4Network('192.0.2.0/25'), IPv4Network('192.0.2.128/31'),\n        IPv4Network('192.0.2.130/32')]\n\n    Args:\n        first: the first IPv4Address or IPv6Address in the range.\n        last: the last IPv4Address or IPv6Address in the range.\n\n    Returns:\n        An iterator of the summarized IPv(4|6) network objects.\n\n    Raise:\n        TypeError:\n            If the first and last objects are not IP addresses.\n            If the first and last objects are not the same version.\n        ValueError:\n            If the last object is not greater than the first.\n            If the version is not 4 or 6.\n\n    \"\"\"\n    if not (isinstance(first, _BaseAddress) and isinstance(last, _BaseAddress)):\n        raise TypeError('first and last must be IP addresses, not networks')\n    if first.version != last.version:\n        raise TypeError(\"%s and %s are not of the same version\" % (\n                str(first), str(last)))\n    if first > last:\n        raise ValueError('last IP address must be greater than first')\n\n    networks = []\n\n    if first.version == 4:\n        ip = IPv4Network\n    elif first.version == 6:\n        ip = IPv6Network\n    else:\n        raise ValueError('unknown IP version')\n\n    ip_bits = first._max_prefixlen\n    first_int = first._ip\n    last_int = last._ip\n    while first_int <= last_int:\n        nbits = _count_righthand_zero_bits(first_int, ip_bits)\n        current = None\n        while nbits >= 0:\n            addend = 2**nbits - 1\n            current = first_int + addend\n            nbits -= 1\n            if current <= last_int:\n                break\n        prefix = _get_prefix_length(first_int, current, ip_bits)\n        net = ip('%s/%d' % (str(first), prefix))\n        yield net\n        #networks.append(net)\n        if current == ip._ALL_ONES:\n            break\n        first_int = current + 1\n        first = ip_address(first_int, version=first._version)\n\ndef _collapse_addresses_recursive(addresses):\n    \"\"\"Loops through the addresses, collapsing concurrent netblocks.\n\n    Example:\n\n        ip1 = IPv4Network('192.0.2.0/26')\n        ip2 = IPv4Network('192.0.2.64/26')\n        ip3 = IPv4Network('192.0.2.128/26')\n        ip4 = IPv4Network('192.0.2.192/26')\n\n        _collapse_addresses_recursive([ip1, ip2, ip3, ip4]) ->\n          [IPv4Network('192.0.2.0/24')]\n\n        This shouldn't be called directly; it is called via\n          collapse_addresses([]).\n\n    Args:\n        addresses: A list of IPv4Network's or IPv6Network's\n\n    Returns:\n        A list of IPv4Network's or IPv6Network's depending on what we were\n        passed.\n\n    \"\"\"\n    ret_array = []\n    optimized = False\n\n    for cur_addr in addresses:\n        if not ret_array:\n            ret_array.append(cur_addr)\n            continue\n        if (cur_addr.network_address >= ret_array[-1].network_address and\n            cur_addr.broadcast_address <= ret_array[-1].broadcast_address):\n            optimized = True\n        elif cur_addr == list(ret_array[-1].supernet().subnets())[1]:\n            ret_array.append(ret_array.pop().supernet())\n            optimized = True\n        else:\n            ret_array.append(cur_addr)\n\n    if optimized:\n        return _collapse_addresses_recursive(ret_array)\n\n    return ret_array\n\n\ndef collapse_addresses(addresses):\n    \"\"\"Collapse a list of IP objects.\n\n    Example:\n        collapse_addresses([IPv4Network('192.0.2.0/25'),\n                            IPv4Network('192.0.2.128/25')]) ->\n                           [IPv4Network('192.0.2.0/24')]\n\n    Args:\n        addresses: An iterator of IPv4Network or IPv6Network objects.\n\n    Returns:\n        An iterator of the collapsed IPv(4|6)Network objects.\n\n    Raises:\n        TypeError: If passed a list of mixed version objects.\n\n    \"\"\"\n    i = 0\n    addrs = []\n    ips = []\n    nets = []\n\n    # split IP addresses and networks\n    for ip in addresses:\n        if isinstance(ip, _BaseAddress):\n            if ips and ips[-1]._version != ip._version:\n                raise TypeError(\"%s and %s are not of the same version\" % (\n                        str(ip), str(ips[-1])))\n            ips.append(ip)\n        elif ip._prefixlen == ip._max_prefixlen:\n            if ips and ips[-1]._version != ip._version:\n                raise TypeError(\"%s and %s are not of the same version\" % (\n                        str(ip), str(ips[-1])))\n            try:\n                ips.append(ip.ip)\n            except AttributeError:\n                ips.append(ip.network_address)\n        else:\n            if nets and nets[-1]._version != ip._version:\n                raise TypeError(\"%s and %s are not of the same version\" % (\n                        str(ip), str(nets[-1])))\n            nets.append(ip)\n\n    # sort and dedup\n    ips = sorted(set(ips))\n    nets = sorted(set(nets))\n\n    while i < len(ips):\n        (first, last) = _find_address_range(ips[i:])\n        i = ips.index(last) + 1\n        addrs.extend(summarize_address_range(first, last))\n\n    return iter(_collapse_addresses_recursive(sorted(\n        addrs + nets, key=_BaseNetwork._get_networks_key)))\n\n\ndef get_mixed_type_key(obj):\n    \"\"\"Return a key suitable for sorting between networks and addresses.\n\n    Address and Network objects are not sortable by default; they're\n    fundamentally different so the expression\n\n        IPv4Address('192.0.2.0') <= IPv4Network('192.0.2.0/24')\n\n    doesn't make any sense.  There are some times however, where you may wish\n    to have ipaddress sort these for you anyway. If you need to do this, you\n    can use this function as the key= argument to sorted().\n\n    Args:\n      obj: either a Network or Address object.\n    Returns:\n      appropriate key.\n\n    \"\"\"\n    if isinstance(obj, _BaseNetwork):\n        return obj._get_networks_key()\n    elif isinstance(obj, _BaseAddress):\n        return obj._get_address_key()\n    return NotImplemented\n\n\nclass _IPAddressBase(object):\n\n    \"\"\"The mother class.\"\"\"\n\n    @property\n    def exploded(self):\n        \"\"\"Return the longhand version of the IP address as a string.\"\"\"\n        return self._explode_shorthand_ip_string()\n\n    @property\n    def compressed(self):\n        \"\"\"Return the shorthand version of the IP address as a string.\"\"\"\n        return str(self)\n\n    def _ip_int_from_prefix(self, prefixlen=None):\n        \"\"\"Turn the prefix length netmask into a int for comparison.\n\n        Args:\n            prefixlen: An integer, the prefix length.\n\n        Returns:\n            An integer.\n\n        \"\"\"\n        if not prefixlen and prefixlen != 0:\n            prefixlen = self._prefixlen\n        return self._ALL_ONES ^ (self._ALL_ONES >> prefixlen)\n\n    def _prefix_from_ip_int(self, ip_int, mask=32):\n        \"\"\"Return prefix length from the decimal netmask.\n\n        Args:\n            ip_int: An integer, the IP address.\n            mask: The netmask.  Defaults to 32.\n\n        Returns:\n            An integer, the prefix length.\n\n        \"\"\"\n        while mask:\n            if ip_int & 1 == 1:\n                break\n            ip_int >>= 1\n            mask -= 1\n\n        return mask\n\n    def _ip_string_from_prefix(self, prefixlen=None):\n        \"\"\"Turn a prefix length into a dotted decimal string.\n\n        Args:\n            prefixlen: An integer, the netmask prefix length.\n\n        Returns:\n            A string, the dotted decimal netmask string.\n\n        \"\"\"\n        if not prefixlen:\n            prefixlen = self._prefixlen\n        return self._string_from_ip_int(self._ip_int_from_prefix(prefixlen))\n\n\nclass _BaseAddress(_IPAddressBase):\n\n    \"\"\"A generic IP object.\n\n    This IP class contains the version independent methods which are\n    used by single IP addresses.\n\n    \"\"\"\n\n    def __init__(self, address):\n        if (not isinstance(address, bytes)\n            and '/' in str(address)):\n            raise AddressValueError(address)\n\n    def __index__(self):\n        return self._ip\n\n    def __int__(self):\n        return self._ip\n\n    def __hex__(self):\n        return hex(self._ip)\n\n    def __eq__(self, other):\n        try:\n            return (self._ip == other._ip\n                    and self._version == other._version)\n        except AttributeError:\n            return NotImplemented\n\n    def __ne__(self, other):\n        eq = self.__eq__(other)\n        if eq is NotImplemented:\n            return NotImplemented\n        return not eq\n\n    def __le__(self, other):\n        gt = self.__gt__(other)\n        if gt is NotImplemented:\n            return NotImplemented\n        return not gt\n\n    def __ge__(self, other):\n        lt = self.__lt__(other)\n        if lt is NotImplemented:\n            return NotImplemented\n        return not lt\n\n    def __lt__(self, other):\n        if self._version != other._version:\n            raise TypeError('%s and %s are not of the same version' % (\n                    str(self), str(other)))\n        if not isinstance(other, _BaseAddress):\n            raise TypeError('%s and %s are not of the same type' % (\n                    str(self), str(other)))\n        if self._ip != other._ip:\n            return self._ip < other._ip\n        return False\n\n    def __gt__(self, other):\n        if self._version != other._version:\n            raise TypeError('%s and %s are not of the same version' % (\n                    str(self), str(other)))\n        if not isinstance(other, _BaseAddress):\n            raise TypeError('%s and %s are not of the same type' % (\n                    str(self), str(other)))\n        if self._ip != other._ip:\n            return self._ip > other._ip\n        return False\n\n    # Shorthand for Integer addition and subtraction. This is not\n    # meant to ever support addition/subtraction of addresses.\n    def __add__(self, other):\n        if not isinstance(other, int):\n            return NotImplemented\n        return ip_address(int(self) + other, version=self._version)\n\n    def __sub__(self, other):\n        if not isinstance(other, int):\n            return NotImplemented\n        return ip_address(int(self) - other, version=self._version)\n\n    def __repr__(self):\n        return '%s(%r)' % (self.__class__.__name__, str(self))\n\n    def __str__(self):\n        return  '%s' % self._string_from_ip_int(self._ip)\n\n    def __hash__(self):\n        return hash(hex(int(self._ip)))\n\n    def _get_address_key(self):\n        return (self._version, self)\n\n    @property\n    def version(self):\n        raise NotImplementedError('BaseIP has no version')\n\n\nclass _BaseNetwork(_IPAddressBase):\n\n    \"\"\"A generic IP object.\n\n    This IP class contains the version independent methods which are\n    used by networks.\n\n    \"\"\"\n\n    def __init__(self, address):\n        self._cache = {}\n\n    def __index__(self):\n        return int(self.network_address) ^ self.prefixlen\n\n    def __int__(self):\n        return int(self.network_address)\n\n    def __repr__(self):\n        return '%s(%r)' % (self.__class__.__name__, str(self))\n\n    def hosts(self):\n        \"\"\"Generate Iterator over usable hosts in a network.\n\n           This is like __iter__ except it doesn't return the network\n           or broadcast addresses.\n\n        \"\"\"\n        cur = int(self.network_address) + 1\n        bcast = int(self.broadcast_address) - 1\n        while cur <= bcast:\n            cur += 1\n            yield ip_address(cur - 1, version=self._version)\n\n    def __iter__(self):\n        cur = int(self.network_address)\n        bcast = int(self.broadcast_address)\n        while cur <= bcast:\n            cur += 1\n            yield ip_address(cur - 1, version=self._version)\n\n    def __getitem__(self, n):\n        network = int(self.network_address)\n        broadcast = int(self.broadcast_address)\n        if n >= 0:\n            if network + n > broadcast:\n                raise IndexError\n            return ip_address(network + n, version=self._version)\n        else:\n            n += 1\n            if broadcast + n < network:\n                raise IndexError\n            return ip_address(broadcast + n, version=self._version)\n\n    def __lt__(self, other):\n        if self._version != other._version:\n            raise TypeError('%s and %s are not of the same version' % (\n                    str(self), str(other)))\n        if not isinstance(other, _BaseNetwork):\n            raise TypeError('%s and %s are not of the same type' % (\n                    str(self), str(other)))\n        if self.network_address != other.network_address:\n            return self.network_address < other.network_address\n        if self.netmask != other.netmask:\n            return self.netmask < other.netmask\n        return False\n\n    def __gt__(self, other):\n        if self._version != other._version:\n            raise TypeError('%s and %s are not of the same version' % (\n                    str(self), str(other)))\n        if not isinstance(other, _BaseNetwork):\n            raise TypeError('%s and %s are not of the same type' % (\n                    str(self), str(other)))\n        if self.network_address != other.network_address:\n            return self.network_address > other.network_address\n        if self.netmask != other.netmask:\n            return self.netmask > other.netmask\n        return False\n\n    def __le__(self, other):\n        gt = self.__gt__(other)\n        if gt is NotImplemented:\n            return NotImplemented\n        return not gt\n\n    def __ge__(self, other):\n        lt = self.__lt__(other)\n        if lt is NotImplemented:\n            return NotImplemented\n        return not lt\n\n    def __eq__(self, other):\n        if not isinstance(other, _BaseNetwork):\n            raise TypeError('%s and %s are not of the same type' % (\n                    str(self), str(other)))\n        return (self._version == other._version and\n                self.network_address == other.network_address and\n                int(self.netmask) == int(other.netmask))\n\n    def __ne__(self, other):\n        eq = self.__eq__(other)\n        if eq is NotImplemented:\n            return NotImplemented\n        return not eq\n\n    def __str__(self):\n        return  '%s/%s' % (str(self.ip),\n                           str(self._prefixlen))\n\n    def __hash__(self):\n        return hash(int(self.network_address) ^ int(self.netmask))\n\n    def __contains__(self, other):\n        # always false if one is v4 and the other is v6.\n        if self._version != other._version:\n          return False\n        # dealing with another network.\n        if isinstance(other, _BaseNetwork):\n            return False\n        # dealing with another address\n        else:\n            # address\n            return (int(self.network_address) <= int(other._ip) <=\n                    int(self.broadcast_address))\n\n    def overlaps(self, other):\n        \"\"\"Tell if self is partly contained in other.\"\"\"\n        return self.network_address in other or (\n            self.broadcast_address in other or (\n                other.network_address in self or (\n                    other.broadcast_address in self)))\n\n    @property\n    def broadcast_address(self):\n        x = self._cache.get('broadcast_address')\n        if x is None:\n            x = ip_address(int(self.network_address) | int(self.hostmask),\n                           version=self._version)\n            self._cache['broadcast_address'] = x\n        return x\n\n    @property\n    def hostmask(self):\n        x = self._cache.get('hostmask')\n        if x is None:\n            x = ip_address(int(self.netmask) ^ self._ALL_ONES,\n                          version=self._version)\n            self._cache['hostmask'] = x\n        return x\n\n    @property\n    def network(self):\n        return ip_network('%s/%d' % (str(self.network_address),\n                                     self.prefixlen))\n\n    @property\n    def with_prefixlen(self):\n        return '%s/%d' % (str(self.ip), self._prefixlen)\n\n    @property\n    def with_netmask(self):\n        return '%s/%s' % (str(self.ip), str(self.netmask))\n\n    @property\n    def with_hostmask(self):\n        return '%s/%s' % (str(self.ip), str(self.hostmask))\n\n    @property\n    def num_addresses(self):\n        \"\"\"Number of hosts in the current subnet.\"\"\"\n        return int(self.broadcast_address) - int(self.network_address) + 1\n\n    @property\n    def version(self):\n        raise NotImplementedError('BaseNet has no version')\n\n    @property\n    def prefixlen(self):\n        return self._prefixlen\n\n    def address_exclude(self, other):\n        \"\"\"Remove an address from a larger block.\n\n        For example:\n\n            addr1 = ip_network('192.0.2.0/28')\n            addr2 = ip_network('192.0.2.1/32')\n            addr1.address_exclude(addr2) =\n                [IPv4Network('192.0.2.0/32'), IPv4Network('192.0.2.2/31'),\n                IPv4Network('192.0.2.4/30'), IPv4Network('192.0.2.8/29')]\n\n        or IPv6:\n\n            addr1 = ip_network('2001:db8::1/32')\n            addr2 = ip_network('2001:db8::1/128')\n            addr1.address_exclude(addr2) =\n                [ip_network('2001:db8::1/128'),\n                ip_network('2001:db8::2/127'),\n                ip_network('2001:db8::4/126'),\n                ip_network('2001:db8::8/125'),\n                ...\n                ip_network('2001:db8:8000::/33')]\n\n        Args:\n            other: An IPv4Network or IPv6Network object of the same type.\n\n        Returns:\n            An iterator of the the IPv(4|6)Network objects which is self\n            minus other.\n\n        Raises:\n            TypeError: If self and other are of difffering address\n              versions, or if other is not a network object.\n            ValueError: If other is not completely contained by self.\n\n        \"\"\"\n        if not self._version == other._version:\n            raise TypeError(\"%s and %s are not of the same version\" % (\n                str(self), str(other)))\n\n        if not isinstance(other, _BaseNetwork):\n            raise TypeError(\"%s is not a network object\" % str(other))\n\n        if not (other.network_address >= self.network_address and\n                other.broadcast_address <= self.broadcast_address):\n            raise ValueError('%s not contained in %s' % (str(other), str(self)))\n\n        if other == self:\n            raise StopIteration\n\n        ret_addrs = []\n\n        # Make sure we're comparing the network of other.\n        other = ip_network('%s/%s' % (str(other.network_address),\n                                      str(other.prefixlen)),\n                           version=other._version)\n\n        s1, s2 = self.subnets()\n        while s1 != other and s2 != other:\n            if (other.network_address >= s1.network_address and\n                other.broadcast_address <= s1.broadcast_address):\n                yield s2\n                s1, s2 = s1.subnets()\n            elif (other.network_address >= s2.network_address and\n                  other.broadcast_address <= s2.broadcast_address):\n                yield s1\n                s1, s2 = s2.subnets()\n            else:\n                # If we got here, there's a bug somewhere.\n                raise AssertionError('Error performing exclusion: '\n                                     's1: %s s2: %s other: %s' %\n                                     (str(s1), str(s2), str(other)))\n        if s1 == other:\n            yield s2\n        elif s2 == other:\n            yield s1\n        else:\n            # If we got here, there's a bug somewhere.\n            raise AssertionError('Error performing exclusion: '\n                                 's1: %s s2: %s other: %s' %\n                                 (str(s1), str(s2), str(other)))\n\n    def compare_networks(self, other):\n        \"\"\"Compare two IP objects.\n\n        This is only concerned about the comparison of the integer\n        representation of the network addresses.  This means that the\n        host bits aren't considered at all in this method.  If you want\n        to compare host bits, you can easily enough do a\n        'HostA._ip < HostB._ip'\n\n        Args:\n            other: An IP object.\n\n        Returns:\n            If the IP versions of self and other are the same, returns:\n\n            -1 if self < other:\n              eg: IPv4Network('192.0.2.0/25') < IPv4Network('192.0.2.128/25')\n              IPv6Network('2001:db8::1000/124') <\n                  IPv6Network('2001:db8::2000/124')\n            0 if self == other\n              eg: IPv4Network('192.0.2.0/24') == IPv4Network('192.0.2.0/24')\n              IPv6Network('2001:db8::1000/124') ==\n                  IPv6Network('2001:db8::1000/124')\n            1 if self > other\n              eg: IPv4Network('192.0.2.128/25') > IPv4Network('192.0.2.0/25')\n                  IPv6Network('2001:db8::2000/124') >\n                      IPv6Network('2001:db8::1000/124')\n\n          Raises:\n              TypeError if the IP versions are different.\n\n        \"\"\"\n        # does this need to raise a ValueError?\n        if self._version != other._version:\n            raise TypeError('%s and %s are not of the same type' % (\n                    str(self), str(other)))\n        # self._version == other._version below here:\n        if self.network_address < other.network_address:\n            return -1\n        if self.network_address > other.network_address:\n            return 1\n        # self.network_address == other.network_address below here:\n        if self.netmask < other.netmask:\n            return -1\n        if self.netmask > other.netmask:\n            return 1\n        return 0\n\n    def _get_networks_key(self):\n        \"\"\"Network-only key function.\n\n        Returns an object that identifies this address' network and\n        netmask. This function is a suitable \"key\" argument for sorted()\n        and list.sort().\n\n        \"\"\"\n        return (self._version, self.network_address, self.netmask)\n\n    def subnets(self, prefixlen_diff=1, new_prefix=None):\n        \"\"\"The subnets which join to make the current subnet.\n\n        In the case that self contains only one IP\n        (self._prefixlen == 32 for IPv4 or self._prefixlen == 128\n        for IPv6), yield an iterator with just ourself.\n\n        Args:\n            prefixlen_diff: An integer, the amount the prefix length\n              should be increased by. This should not be set if\n              new_prefix is also set.\n            new_prefix: The desired new prefix length. This must be a\n              larger number (smaller prefix) than the existing prefix.\n              This should not be set if prefixlen_diff is also set.\n\n        Returns:\n            An iterator of IPv(4|6) objects.\n\n        Raises:\n            ValueError: The prefixlen_diff is too small or too large.\n                OR\n            prefixlen_diff and new_prefix are both set or new_prefix\n              is a smaller number than the current prefix (smaller\n              number means a larger network)\n\n        \"\"\"\n        if self._prefixlen == self._max_prefixlen:\n            yield self\n            return\n\n        if new_prefix is not None:\n            if new_prefix < self._prefixlen:\n                raise ValueError('new prefix must be longer')\n            if prefixlen_diff != 1:\n                raise ValueError('cannot set prefixlen_diff and new_prefix')\n            prefixlen_diff = new_prefix - self._prefixlen\n\n        if prefixlen_diff < 0:\n            raise ValueError('prefix length diff must be > 0')\n        new_prefixlen = self._prefixlen + prefixlen_diff\n\n        if not self._is_valid_netmask(str(new_prefixlen)):\n            raise ValueError(\n                'prefix length diff %d is invalid for netblock %s' % (\n                    new_prefixlen, str(self)))\n\n        first = ip_network('%s/%s' % (str(self.network_address),\n                                     str(self._prefixlen + prefixlen_diff)),\n                         version=self._version)\n\n        yield first\n        current = first\n        while True:\n            broadcast = current.broadcast_address\n            if broadcast == self.broadcast_address:\n                return\n            new_addr = ip_address(int(broadcast) + 1, version=self._version)\n            current = ip_network('%s/%s' % (str(new_addr), str(new_prefixlen)),\n                                version=self._version)\n\n            yield current\n\n    def masked(self):\n        \"\"\"Return the network object with the host bits masked out.\"\"\"\n        return ip_network('%s/%d' % (self.network_address, self._prefixlen),\n                         version=self._version)\n\n    def supernet(self, prefixlen_diff=1, new_prefix=None):\n        \"\"\"The supernet containing the current network.\n\n        Args:\n            prefixlen_diff: An integer, the amount the prefix length of\n              the network should be decreased by.  For example, given a\n              /24 network and a prefixlen_diff of 3, a supernet with a\n              /21 netmask is returned.\n\n        Returns:\n            An IPv4 network object.\n\n        Raises:\n            ValueError: If self.prefixlen - prefixlen_diff < 0. I.e., you have a\n              negative prefix length.\n                OR\n            If prefixlen_diff and new_prefix are both set or new_prefix is a\n              larger number than the current prefix (larger number means a\n              smaller network)\n\n        \"\"\"\n        if self._prefixlen == 0:\n            return self\n\n        if new_prefix is not None:\n            if new_prefix > self._prefixlen:\n                raise ValueError('new prefix must be shorter')\n            if prefixlen_diff != 1:\n                raise ValueError('cannot set prefixlen_diff and new_prefix')\n            prefixlen_diff = self._prefixlen - new_prefix\n\n\n        if self.prefixlen - prefixlen_diff < 0:\n            raise ValueError(\n                'current prefixlen is %d, cannot have a prefixlen_diff of %d' %\n                (self.prefixlen, prefixlen_diff))\n        # TODO (pmoody): optimize this.\n        t = ip_network('%s/%d' % (str(self.network_address),\n                                    self.prefixlen - prefixlen_diff),\n                         version=self._version, strict=False)\n        return ip_network('%s/%d' % (str(t.network_address), t.prefixlen),\n                          version=t._version)\n\n\nclass _BaseV4(object):\n\n    \"\"\"Base IPv4 object.\n\n    The following methods are used by IPv4 objects in both single IP\n    addresses and networks.\n\n    \"\"\"\n\n    # Equivalent to 255.255.255.255 or 32 bits of 1's.\n    _ALL_ONES = (2**IPV4LENGTH) - 1\n    _DECIMAL_DIGITS = frozenset('0123456789')\n\n    def __init__(self, address):\n        self._version = 4\n        self._max_prefixlen = IPV4LENGTH\n\n    def _explode_shorthand_ip_string(self):\n        return str(self)\n\n    def _ip_int_from_string(self, ip_str):\n        \"\"\"Turn the given IP string into an integer for comparison.\n\n        Args:\n            ip_str: A string, the IP ip_str.\n\n        Returns:\n            The IP ip_str as an integer.\n\n        Raises:\n            AddressValueError: if ip_str isn't a valid IPv4 Address.\n\n        \"\"\"\n        octets = ip_str.split('.')\n        if len(octets) != 4:\n            raise AddressValueError(ip_str)\n\n        packed_ip = 0\n        for oc in octets:\n            try:\n                packed_ip = (packed_ip << 8) | self._parse_octet(oc)\n            except ValueError:\n                raise AddressValueError(ip_str)\n        return packed_ip\n\n    def _parse_octet(self, octet_str):\n        \"\"\"Convert a decimal octet into an integer.\n\n        Args:\n            octet_str: A string, the number to parse.\n\n        Returns:\n            The octet as an integer.\n\n        Raises:\n            ValueError: if the octet isn't strictly a decimal from [0..255].\n\n        \"\"\"\n        # Whitelist the characters, since int() allows a lot of bizarre stuff.\n        if not self._DECIMAL_DIGITS.issuperset(octet_str):\n            raise ValueError\n        octet_int = int(octet_str, 10)\n        # Disallow leading zeroes, because no clear standard exists on\n        # whether these should be interpreted as decimal or octal.\n        if octet_int > 255 or (octet_str[0] == '0' and len(octet_str) > 1):\n            raise ValueError\n        return octet_int\n\n    def _string_from_ip_int(self, ip_int):\n        \"\"\"Turns a 32-bit integer into dotted decimal notation.\n\n        Args:\n            ip_int: An integer, the IP address.\n\n        Returns:\n            The IP address as a string in dotted decimal notation.\n\n        \"\"\"\n        octets = []\n        for _ in range(4):\n            octets.insert(0, str(ip_int & 0xFF))\n            ip_int >>= 8\n        return '.'.join(octets)\n\n    @property\n    def max_prefixlen(self):\n        return self._max_prefixlen\n\n    @property\n    def version(self):\n        return self._version\n\n    @property\n    def is_reserved(self):\n       \"\"\"Test if the address is otherwise IETF reserved.\n\n        Returns:\n            A boolean, True if the address is within the\n            reserved IPv4 Network range.\n\n       \"\"\"\n       reserved_network = IPv4Network('240.0.0.0/4')\n       if isinstance(self, _BaseAddress):\n           return self in reserved_network\n       return (self.network_address in reserved_network and\n               self.broadcast_address in reserved_network)\n\n    @property\n    def is_private(self):\n        \"\"\"Test if this address is allocated for private networks.\n\n        Returns:\n            A boolean, True if the address is reserved per RFC 1918.\n\n        \"\"\"\n        private_10 = IPv4Network('10.0.0.0/8')\n        private_172 = IPv4Network('172.16.0.0/12')\n        private_192 = IPv4Network('192.168.0.0/16')\n        if isinstance(self, _BaseAddress):\n            return (self in private_10 or self in private_172 or\n                    self in private_192)\n        else:\n            return ((self.network_address in private_10 and\n                     self.broadcast_address in private_10) or\n                    (self.network_address in private_172 and\n                     self.broadcast_address in private_172) or\n                    (self.network_address in private_192 and\n                     self.broadcast_address in private_192))\n\n    @property\n    def is_multicast(self):\n        \"\"\"Test if the address is reserved for multicast use.\n\n        Returns:\n            A boolean, True if the address is multicast.\n            See RFC 3171 for details.\n\n        \"\"\"\n        multicast_network = IPv4Network('224.0.0.0/4')\n        if isinstance(self, _BaseAddress):\n            return self in IPv4Network('224.0.0.0/4')\n        return (self.network_address in multicast_network and\n                self.broadcast_address in multicast_network)\n\n    @property\n    def is_unspecified(self):\n        \"\"\"Test if the address is unspecified.\n\n        Returns:\n            A boolean, True if this is the unspecified address as defined in\n            RFC 5735 3.\n\n        \"\"\"\n        unspecified_address = IPv4Address('0.0.0.0')\n        if isinstance(self, _BaseAddress):\n            return self in unspecified_address\n        return (self.network_address == self.broadcast_address ==\n                unspecified_address)\n\n    @property\n    def is_loopback(self):\n        \"\"\"Test if the address is a loopback address.\n\n        Returns:\n            A boolean, True if the address is a loopback per RFC 3330.\n\n        \"\"\"\n        loopback_address = IPv4Network('127.0.0.0/8')\n        if isinstance(self, _BaseAddress):\n            return self in loopback_address\n\n        return (self.network_address in loopback_address and\n                self.broadcast_address in loopback_address)\n\n    @property\n    def is_link_local(self):\n        \"\"\"Test if the address is reserved for link-local.\n\n        Returns:\n            A boolean, True if the address is link-local per RFC 3927.\n\n        \"\"\"\n        linklocal_network = IPv4Network('169.254.0.0/16')\n        if isinstance(self, _BaseAddress):\n            return self in linklocal_network\n        return (self.network_address in linklocal_network and\n                self.broadcast_address in linklocal_network)\n\n\nclass IPv4Address(_BaseV4, _BaseAddress):\n\n    \"\"\"Represent and manipulate single IPv4 Addresses.\"\"\"\n\n    def __init__(self, address):\n\n        \"\"\"\n        Args:\n            address: A string or integer representing the IP\n\n              Additionally, an integer can be passed, so\n              IPv4Address('192.0.2.1') == IPv4Address(3221225985).\n              or, more generally\n              IPv4Address(int(IPv4Address('192.0.2.1'))) ==\n                IPv4Address('192.0.2.1')\n\n        Raises:\n            AddressValueError: If ipaddressisn't a valid IPv4 address.\n\n        \"\"\"\n        _BaseAddress.__init__(self, address)\n        _BaseV4.__init__(self, address)\n\n        # Efficient constructor from integer.\n        if isinstance(address, int):\n            self._ip = address\n            if address < 0 or address > self._ALL_ONES:\n                raise AddressValueError(address)\n            return\n\n        # Constructing from a packed address\n        if (not isinstance(address, str) and\n            isinstance(address, bytes) and len(address) == 4):\n            self._ip = struct.unpack('!I', address)[0]\n            return\n\n        # Assume input argument to be string or any object representation\n        # which converts into a formatted IP string.\n        addr_str = str(address)\n        self._ip = self._ip_int_from_string(addr_str)\n\n    @property\n    def packed(self):\n        \"\"\"The binary representation of this address.\"\"\"\n        return v4_int_to_packed(self._ip)\n\n\nclass IPv4Interface(IPv4Address):\n\n    # the valid octets for host and netmasks. only useful for IPv4.\n    _valid_mask_octets = set((255, 254, 252, 248, 240, 224, 192, 128, 0))\n\n    def __init__(self, address):\n        if isinstance(address, (bytes, int)):\n            IPv4Address.__init__(self, address)\n            self.network = IPv4Network(self._ip)\n            self._prefixlen = self._max_prefixlen\n            return\n\n        addr = str(address).split('/')\n        if len(addr) > 2:\n            raise AddressValueError(address)\n        IPv4Address.__init__(self, addr[0])\n\n        self.network = IPv4Network(address, strict=False)\n        self._prefixlen = self.network._prefixlen\n\n        self.netmask = self.network.netmask\n        self.hostmask = self.network.hostmask\n\n\n    def __str__(self):\n        return '%s/%d' % (self._string_from_ip_int(self._ip),\n                          self.network.prefixlen)\n\n    def __eq__(self, other):\n        try:\n            return (IPv4Address.__eq__(self, other) and\n                    self.network == other.network)\n        except AttributeError:\n            return NotImplemented\n\n    def __hash__(self):\n        return self._ip ^ self._prefixlen ^ int(self.network.network_address)\n\n    def _is_valid_netmask(self, netmask):\n        \"\"\"Verify that the netmask is valid.\n\n        Args:\n            netmask: A string, either a prefix or dotted decimal\n              netmask.\n\n        Returns:\n            A boolean, True if the prefix represents a valid IPv4\n            netmask.\n\n        \"\"\"\n        mask = netmask.split('.')\n        if len(mask) == 4:\n            if [x for x in mask if int(x) not in self._valid_mask_octets]:\n                return False\n            if [y for idx, y in enumerate(mask) if idx > 0 and\n                y > mask[idx - 1]]:\n                return False\n            return True\n        try:\n            netmask = int(netmask)\n        except ValueError:\n            return False\n        return 0 <= netmask <= self._max_prefixlen\n\n    def _is_hostmask(self, ip_str):\n        \"\"\"Test if the IP string is a hostmask (rather than a netmask).\n\n        Args:\n            ip_str: A string, the potential hostmask.\n\n        Returns:\n            A boolean, True if the IP string is a hostmask.\n\n        \"\"\"\n        bits = ip_str.split('.')\n        try:\n            parts = [int(x) for x in bits if int(x) in self._valid_mask_octets]\n        except ValueError:\n            return False\n        if len(parts) != len(bits):\n            return False\n        if parts[0] < parts[-1]:\n            return True\n        return False\n\n\n    @property\n    def prefixlen(self):\n        return self._prefixlen\n\n    @property\n    def ip(self):\n        return IPv4Address(self._ip)\n\n    @property\n    def with_prefixlen(self):\n        return self\n\n    @property\n    def with_netmask(self):\n        return '%s/%s' % (self._string_from_ip_int(self._ip),\n                          self.netmask)\n    @property\n    def with_hostmask(self):\n        return '%s/%s' % (self._string_from_ip_int(self._ip),\n                          self.hostmask)\n\n\nclass IPv4Network(_BaseV4, _BaseNetwork):\n\n    \"\"\"This class represents and manipulates 32-bit IPv4 network + addresses..\n\n    Attributes: [examples for IPv4Network('192.0.2.0/27')]\n        .network_address: IPv4Address('192.0.2.0')\n        .hostmask: IPv4Address('0.0.0.31')\n        .broadcast_address: IPv4Address('192.0.2.32')\n        .netmask: IPv4Address('255.255.255.224')\n        .prefixlen: 27\n\n    \"\"\"\n\n    # the valid octets for host and netmasks. only useful for IPv4.\n    _valid_mask_octets = set((255, 254, 252, 248, 240, 224, 192, 128, 0))\n\n    def __init__(self, address, strict=True):\n\n        \"\"\"Instantiate a new IPv4 network object.\n\n        Args:\n            address: A string or integer representing the IP [& network].\n              '192.0.2.0/24'\n              '192.0.2.0/255.255.255.0'\n              '192.0.0.2/0.0.0.255'\n              are all functionally the same in IPv4. Similarly,\n              '192.0.2.1'\n              '192.0.2.1/255.255.255.255'\n              '192.0.2.1/32'\n              are also functionaly equivalent. That is to say, failing to\n              provide a subnetmask will create an object with a mask of /32.\n\n              If the mask (portion after the / in the argument) is given in\n              dotted quad form, it is treated as a netmask if it starts with a\n              non-zero field (e.g. /255.0.0.0 == /8) and as a hostmask if it\n              starts with a zero field (e.g. 0.255.255.255 == /8), with the\n              single exception of an all-zero mask which is treated as a\n              netmask == /0. If no mask is given, a default of /32 is used.\n\n              Additionally, an integer can be passed, so\n              IPv4Network('192.0.2.1') == IPv4Network(3221225985)\n              or, more generally\n              IPv4Interface(int(IPv4Interface('192.0.2.1'))) ==\n                IPv4Interface('192.0.2.1')\n\n        Raises:\n            AddressValueError: If ipaddressisn't a valid IPv4 address.\n            NetmaskValueError: If the netmask isn't valid for\n              an IPv4 address.\n            ValueError: If strict was True and a network address was not\n              supplied.\n\n        \"\"\"\n\n        _BaseV4.__init__(self, address)\n        _BaseNetwork.__init__(self, address)\n\n        # Constructing from a packed address\n        if isinstance(address, bytes) and len(address) == 4:\n            self.network_address = IPv4Address(\n                struct.unpack('!I', address)[0])\n            self._prefixlen = self._max_prefixlen\n            self.netmask = IPv4Address(self._ALL_ONES)\n            #fixme: address/network test here\n            return\n\n        # Efficient constructor from integer.\n        if isinstance(address, int):\n            self._prefixlen = self._max_prefixlen\n            self.netmask = IPv4Address(self._ALL_ONES)\n            if address < 0 or address > self._ALL_ONES:\n                raise AddressValueError(address)\n            self.network_address = IPv4Address(address)\n            #fixme: address/network test here.\n            return\n\n        # Assume input argument to be string or any object representation\n        # which converts into a formatted IP prefix string.\n        addr = str(address).split('/')\n        self.network_address = IPv4Address(self._ip_int_from_string(addr[0]))\n\n        if len(addr) > 2:\n            raise AddressValueError(address)\n\n        if len(addr) == 2:\n            mask = addr[1].split('.')\n\n            if len(mask) == 4:\n                # We have dotted decimal netmask.\n                if self._is_valid_netmask(addr[1]):\n                    self.netmask = IPv4Address(self._ip_int_from_string(\n                            addr[1]))\n                elif self._is_hostmask(addr[1]):\n                    self.netmask = IPv4Address(\n                        self._ip_int_from_string(addr[1]) ^ self._ALL_ONES)\n                else:\n                    raise NetmaskValueError('%s is not a valid netmask'\n                                                     % addr[1])\n\n                self._prefixlen = self._prefix_from_ip_int(int(self.netmask))\n            else:\n                # We have a netmask in prefix length form.\n                if not self._is_valid_netmask(addr[1]):\n                    raise NetmaskValueError(addr[1])\n                self._prefixlen = int(addr[1])\n                self.netmask = IPv4Address(self._ip_int_from_prefix(\n                    self._prefixlen))\n        else:\n            self._prefixlen = self._max_prefixlen\n            self.netmask = IPv4Address(self._ip_int_from_prefix(\n                self._prefixlen))\n\n        if strict:\n            if (IPv4Address(int(self.network_address) & int(self.netmask)) !=\n                self.network_address):\n                raise ValueError('%s has host bits set' % self)\n        self.network_address = IPv4Address(int(self.network_address) &\n                                           int(self.netmask))\n\n        if self._prefixlen == (self._max_prefixlen - 1):\n            self.hosts = self.__iter__\n\n    @property\n    def packed(self):\n        \"\"\"The binary representation of this address.\"\"\"\n        return v4_int_to_packed(self.network_address)\n\n    def __str__(self):\n        return '%s/%d' % (str(self.network_address),\n                          self.prefixlen)\n\n    def _is_valid_netmask(self, netmask):\n        \"\"\"Verify that the netmask is valid.\n\n        Args:\n            netmask: A string, either a prefix or dotted decimal\n              netmask.\n\n        Returns:\n            A boolean, True if the prefix represents a valid IPv4\n            netmask.\n\n        \"\"\"\n        mask = netmask.split('.')\n        if len(mask) == 4:\n            if [x for x in mask if int(x) not in self._valid_mask_octets]:\n                return False\n            if [y for idx, y in enumerate(mask) if idx > 0 and\n                y > mask[idx - 1]]:\n                return False\n            return True\n        try:\n            netmask = int(netmask)\n        except ValueError:\n            return False\n        return 0 <= netmask <= self._max_prefixlen\n\n    def _is_hostmask(self, ip_str):\n        \"\"\"Test if the IP string is a hostmask (rather than a netmask).\n\n        Args:\n            ip_str: A string, the potential hostmask.\n\n        Returns:\n            A boolean, True if the IP string is a hostmask.\n\n        \"\"\"\n        bits = ip_str.split('.')\n        try:\n            parts = [int(x) for x in bits if int(x) in self._valid_mask_octets]\n        except ValueError:\n            return False\n        if len(parts) != len(bits):\n            return False\n        if parts[0] < parts[-1]:\n            return True\n        return False\n\n    @property\n    def with_prefixlen(self):\n        return '%s/%d' % (str(self.network_address), self._prefixlen)\n\n    @property\n    def with_netmask(self):\n        return '%s/%s' % (str(self.network_address), str(self.netmask))\n\n    @property\n    def with_hostmask(self):\n        return '%s/%s' % (str(self.network_address), str(self.hostmask))\n\n\nclass _BaseV6(object):\n\n    \"\"\"Base IPv6 object.\n\n    The following methods are used by IPv6 objects in both single IP\n    addresses and networks.\n\n    \"\"\"\n\n    _ALL_ONES = (2**IPV6LENGTH) - 1\n    _HEXTET_COUNT = 8\n    _HEX_DIGITS = frozenset('0123456789ABCDEFabcdef')\n\n    def __init__(self, address):\n        self._version = 6\n        self._max_prefixlen = IPV6LENGTH\n\n    def _ip_int_from_string(self, ip_str):\n        \"\"\"Turn an IPv6 ip_str into an integer.\n\n        Args:\n            ip_str: A string, the IPv6 ip_str.\n\n        Returns:\n            An int, the IPv6 address\n\n        Raises:\n            AddressValueError: if ip_str isn't a valid IPv6 Address.\n\n        \"\"\"\n        parts = ip_str.split(':')\n\n        # An IPv6 address needs at least 2 colons (3 parts).\n        if len(parts) < 3:\n            raise AddressValueError(ip_str)\n\n        # If the address has an IPv4-style suffix, convert it to hexadecimal.\n        if '.' in parts[-1]:\n            ipv4_int = IPv4Address(parts.pop())._ip\n            parts.append('%x' % ((ipv4_int >> 16) & 0xFFFF))\n            parts.append('%x' % (ipv4_int & 0xFFFF))\n\n        # An IPv6 address can't have more than 8 colons (9 parts).\n        if len(parts) > self._HEXTET_COUNT + 1:\n            raise AddressValueError(ip_str)\n\n        # Disregarding the endpoints, find '::' with nothing in between.\n        # This indicates that a run of zeroes has been skipped.\n        try:\n            skip_index, = (\n                [i for i in range(1, len(parts) - 1) if not parts[i]] or\n                [None])\n        except ValueError:\n            # Can't have more than one '::'\n            raise AddressValueError(ip_str)\n\n        # parts_hi is the number of parts to copy from above/before the '::'\n        # parts_lo is the number of parts to copy from below/after the '::'\n        if skip_index is not None:\n            # If we found a '::', then check if it also covers the endpoints.\n            parts_hi = skip_index\n            parts_lo = len(parts) - skip_index - 1\n            if not parts[0]:\n                parts_hi -= 1\n                if parts_hi:\n                    raise AddressValueError(ip_str)  # ^: requires ^::\n            if not parts[-1]:\n                parts_lo -= 1\n                if parts_lo:\n                    raise AddressValueError(ip_str)  # :$ requires ::$\n            parts_skipped = self._HEXTET_COUNT - (parts_hi + parts_lo)\n            if parts_skipped < 1:\n                raise AddressValueError(ip_str)\n        else:\n            # Otherwise, allocate the entire address to parts_hi.  The endpoints\n            # could still be empty, but _parse_hextet() will check for that.\n            if len(parts) != self._HEXTET_COUNT:\n                raise AddressValueError(ip_str)\n            parts_hi = len(parts)\n            parts_lo = 0\n            parts_skipped = 0\n\n        try:\n            # Now, parse the hextets into a 128-bit integer.\n            ip_int = 0\n            for i in range(parts_hi):\n                ip_int <<= 16\n                ip_int |= self._parse_hextet(parts[i])\n            ip_int <<= 16 * parts_skipped\n            for i in range(-parts_lo, 0):\n                ip_int <<= 16\n                ip_int |= self._parse_hextet(parts[i])\n            return ip_int\n        except ValueError:\n            raise AddressValueError(ip_str)\n\n    def _parse_hextet(self, hextet_str):\n        \"\"\"Convert an IPv6 hextet string into an integer.\n\n        Args:\n            hextet_str: A string, the number to parse.\n\n        Returns:\n            The hextet as an integer.\n\n        Raises:\n            ValueError: if the input isn't strictly a hex number from [0..FFFF].\n\n        \"\"\"\n        # Whitelist the characters, since int() allows a lot of bizarre stuff.\n        if not self._HEX_DIGITS.issuperset(hextet_str):\n            raise ValueError\n        if len(hextet_str) > 4:\n            raise ValueError\n        hextet_int = int(hextet_str, 16)\n        if hextet_int > 0xFFFF:\n            raise ValueError\n        return hextet_int\n\n    def _compress_hextets(self, hextets):\n        \"\"\"Compresses a list of hextets.\n\n        Compresses a list of strings, replacing the longest continuous\n        sequence of \"0\" in the list with \"\" and adding empty strings at\n        the beginning or at the end of the string such that subsequently\n        calling \":\".join(hextets) will produce the compressed version of\n        the IPv6 address.\n\n        Args:\n            hextets: A list of strings, the hextets to compress.\n\n        Returns:\n            A list of strings.\n\n        \"\"\"\n        best_doublecolon_start = -1\n        best_doublecolon_len = 0\n        doublecolon_start = -1\n        doublecolon_len = 0\n        for index in range(len(hextets)):\n            if hextets[index] == '0':\n                doublecolon_len += 1\n                if doublecolon_start == -1:\n                    # Start of a sequence of zeros.\n                    doublecolon_start = index\n                if doublecolon_len > best_doublecolon_len:\n                    # This is the longest sequence of zeros so far.\n                    best_doublecolon_len = doublecolon_len\n                    best_doublecolon_start = doublecolon_start\n            else:\n                doublecolon_len = 0\n                doublecolon_start = -1\n\n        if best_doublecolon_len > 1:\n            best_doublecolon_end = (best_doublecolon_start +\n                                    best_doublecolon_len)\n            # For zeros at the end of the address.\n            if best_doublecolon_end == len(hextets):\n                hextets += ['']\n            hextets[best_doublecolon_start:best_doublecolon_end] = ['']\n            # For zeros at the beginning of the address.\n            if best_doublecolon_start == 0:\n                hextets = [''] + hextets\n\n        return hextets\n\n    def _string_from_ip_int(self, ip_int=None):\n        \"\"\"Turns a 128-bit integer into hexadecimal notation.\n\n        Args:\n            ip_int: An integer, the IP address.\n\n        Returns:\n            A string, the hexadecimal representation of the address.\n\n        Raises:\n            ValueError: The address is bigger than 128 bits of all ones.\n\n        \"\"\"\n        if not ip_int and ip_int != 0:\n            ip_int = int(self._ip)\n\n        if ip_int > self._ALL_ONES:\n            raise ValueError('IPv6 address is too large')\n\n        hex_str = '%032x' % ip_int\n        hextets = []\n        for x in range(0, 32, 4):\n            hextets.append('%x' % int(hex_str[x:x+4], 16))\n\n        hextets = self._compress_hextets(hextets)\n        return ':'.join(hextets)\n\n    def _explode_shorthand_ip_string(self):\n        \"\"\"Expand a shortened IPv6 address.\n\n        Args:\n            ip_str: A string, the IPv6 address.\n\n        Returns:\n            A string, the expanded IPv6 address.\n\n        \"\"\"\n        if isinstance(self, IPv6Network):\n            ip_str = str(self.network_address)\n        elif isinstance(self, IPv6Interface):\n            ip_str = str(self.ip)\n        else:\n            ip_str = str(self)\n\n        ip_int = self._ip_int_from_string(ip_str)\n        parts = []\n        for i in range(self._HEXTET_COUNT):\n            parts.append('%04x' % (ip_int & 0xFFFF))\n            ip_int >>= 16\n        parts.reverse()\n        if isinstance(self, (_BaseNetwork, IPv6Interface)):\n            return '%s/%d' % (':'.join(parts), self.prefixlen)\n        return ':'.join(parts)\n\n    @property\n    def max_prefixlen(self):\n        return self._max_prefixlen\n\n    @property\n    def packed(self):\n        \"\"\"The binary representation of this address.\"\"\"\n        return v6_int_to_packed(self._ip)\n\n    @property\n    def version(self):\n        return self._version\n\n    @property\n    def is_multicast(self):\n        \"\"\"Test if the address is reserved for multicast use.\n\n        Returns:\n            A boolean, True if the address is a multicast address.\n            See RFC 2373 2.7 for details.\n\n        \"\"\"\n        multicast_network = IPv6Network('ff00::/8')\n        if isinstance(self, _BaseAddress):\n            return self in multicast_network\n        return (self.network_address in multicast_network and\n                self.broadcast_address in multicast_network)\n\n    @property\n    def is_reserved(self):\n        \"\"\"Test if the address is otherwise IETF reserved.\n\n        Returns:\n            A boolean, True if the address is within one of the\n            reserved IPv6 Network ranges.\n\n        \"\"\"\n        reserved_networks = [IPv6Network('::/8'), IPv6Network('100::/8'),\n                             IPv6Network('200::/7'), IPv6Network('400::/6'),\n                             IPv6Network('800::/5'), IPv6Network('1000::/4'),\n                             IPv6Network('4000::/3'), IPv6Network('6000::/3'),\n                             IPv6Network('8000::/3'), IPv6Network('A000::/3'),\n                             IPv6Network('C000::/3'), IPv6Network('E000::/4'),\n                             IPv6Network('F000::/5'), IPv6Network('F800::/6'),\n                             IPv6Network('FE00::/9')]\n\n        if isinstance(self, _BaseAddress):\n            return len([x for x in reserved_networks if self in x]) > 0\n        return len([x for x in reserved_networks if self.network_address in x\n                    and self.broadcast_address in x]) > 0\n\n    @property\n    def is_link_local(self):\n        \"\"\"Test if the address is reserved for link-local.\n\n        Returns:\n            A boolean, True if the address is reserved per RFC 4291.\n\n        \"\"\"\n        linklocal_network = IPv6Network('fe80::/10')\n        if isinstance(self, _BaseAddress):\n            return self in linklocal_network\n        return (self.network_address in linklocal_network and\n                self.broadcast_address in linklocal_network)\n\n    @property\n    def is_site_local(self):\n        \"\"\"Test if the address is reserved for site-local.\n\n        Note that the site-local address space has been deprecated by RFC 3879.\n        Use is_private to test if this address is in the space of unique local\n        addresses as defined by RFC 4193.\n\n        Returns:\n            A boolean, True if the address is reserved per RFC 3513 2.5.6.\n\n        \"\"\"\n        sitelocal_network = IPv6Network('fec0::/10')\n        if isinstance(self, _BaseAddress):\n            return self in sitelocal_network\n        return (self.network_address in sitelocal_network and\n                self.broadcast_address in sitelocal_network)\n\n    @property\n    def is_private(self):\n        \"\"\"Test if this address is allocated for private networks.\n\n        Returns:\n            A boolean, True if the address is reserved per RFC 4193.\n\n        \"\"\"\n        private_network = IPv6Network('fc00::/7')\n        if isinstance(self, _BaseAddress):\n            return self in private_network\n        return (self.network_address in private_network and\n                self.broadcast_address in private_network)\n\n\n    @property\n    def ipv4_mapped(self):\n        \"\"\"Return the IPv4 mapped address.\n\n        Returns:\n            If the IPv6 address is a v4 mapped address, return the\n            IPv4 mapped address. Return None otherwise.\n\n        \"\"\"\n        if (self._ip >> 32) != 0xFFFF:\n            return None\n        return IPv4Address(self._ip & 0xFFFFFFFF)\n\n    @property\n    def teredo(self):\n        \"\"\"Tuple of embedded teredo IPs.\n\n        Returns:\n            Tuple of the (server, client) IPs or None if the address\n            doesn't appear to be a teredo address (doesn't start with\n            2001::/32)\n\n        \"\"\"\n        if (self._ip >> 96) != 0x20010000:\n            return None\n        return (IPv4Address((self._ip >> 64) & 0xFFFFFFFF),\n                IPv4Address(~self._ip & 0xFFFFFFFF))\n\n    @property\n    def sixtofour(self):\n        \"\"\"Return the IPv4 6to4 embedded address.\n\n        Returns:\n            The IPv4 6to4-embedded address if present or None if the\n            address doesn't appear to contain a 6to4 embedded address.\n\n        \"\"\"\n        if (self._ip >> 112) != 0x2002:\n            return None\n        return IPv4Address((self._ip >> 80) & 0xFFFFFFFF)\n\n    @property\n    def is_unspecified(self):\n        \"\"\"Test if the address is unspecified.\n\n        Returns:\n            A boolean, True if this is the unspecified address as defined in\n            RFC 2373 2.5.2.\n\n        \"\"\"\n        if isinstance(self, (IPv6Network, IPv6Interface)):\n            return int(self.network_address) == 0 and getattr(\n                self, '_prefixlen', 128) == 128\n        return self._ip == 0\n\n    @property\n    def is_loopback(self):\n        \"\"\"Test if the address is a loopback address.\n\n        Returns:\n            A boolean, True if the address is a loopback address as defined in\n            RFC 2373 2.5.3.\n\n        \"\"\"\n        if isinstance(self, IPv6Network):\n            return int(self.network) == 1 and getattr(\n                self, '_prefixlen', 128) == 128\n        elif isinstance(self, IPv6Interface):\n            return int(self.network.network_address) == 1 and getattr(\n                self, '_prefixlen', 128) == 128\n        return self._ip == 1\n\n\nclass IPv6Address(_BaseV6, _BaseAddress):\n\n    \"\"\"Represent and manipulate single IPv6 Addresses.\n    \"\"\"\n\n    def __init__(self, address):\n        \"\"\"Instantiate a new IPv6 address object.\n\n        Args:\n            address: A string or integer representing the IP\n\n              Additionally, an integer can be passed, so\n              IPv6Address('2001:db8::') ==\n                IPv6Address(42540766411282592856903984951653826560)\n              or, more generally\n              IPv6Address(int(IPv6Address('2001:db8::'))) ==\n                IPv6Address('2001:db8::')\n\n        Raises:\n            AddressValueError: If address isn't a valid IPv6 address.\n\n        \"\"\"\n        _BaseAddress.__init__(self, address)\n        _BaseV6.__init__(self, address)\n\n        # Efficient constructor from integer.\n        if isinstance(address, int):\n            self._ip = address\n            if address < 0 or address > self._ALL_ONES:\n                raise AddressValueError(address)\n            return\n\n        # Constructing from a packed address\n        if (not isinstance(address, str) and\n            isinstance(address, bytes) and len(address) == 16):\n            tmp = struct.unpack('!QQ', address)\n            self._ip = (tmp[0] << 64) | tmp[1]\n            return\n\n        # Assume input argument to be string or any object representation\n        # which converts into a formatted IP string.\n        addr_str = str(address)\n        if not addr_str:\n            raise AddressValueError('')\n\n        self._ip = self._ip_int_from_string(addr_str)\n\n\nclass IPv6Interface(IPv6Address):\n\n    def __init__(self, address):\n        if isinstance(address, (bytes, int)):\n            IPv6Address.__init__(self, address)\n            self.network = IPv6Network(self._ip)\n            self._prefixlen = self._max_prefixlen\n            return\n\n        addr = str(address).split('/')\n        IPv6Address.__init__(self, addr[0])\n        self.network = IPv6Network(address, strict=False)\n        self.netmask = self.network.netmask\n        self._prefixlen = self.network._prefixlen\n        self.hostmask = self.network.hostmask\n\n\n    def __str__(self):\n        return '%s/%d' % (self._string_from_ip_int(self._ip),\n                          self.network.prefixlen)\n\n    def __eq__(self, other):\n        try:\n            return (IPv6Address.__eq__(self, other) and\n                    self.network == other.network)\n        except AttributeError:\n            return NotImplemented\n\n    def __hash__(self):\n        return self._ip ^ self._prefixlen ^ int(self.network.network_address)\n\n    @property\n    def prefixlen(self):\n        return self._prefixlen\n    @property\n    def ip(self):\n        return IPv6Address(self._ip)\n\n    @property\n    def with_prefixlen(self):\n        return self\n\n    @property\n    def with_netmask(self):\n        return self.with_prefixlen\n    @property\n    def with_hostmask(self):\n        return '%s/%s' % (self._string_from_ip_int(self._ip),\n                          self.hostmask)\n\n\nclass IPv6Network(_BaseV6, _BaseNetwork):\n\n    \"\"\"This class represents and manipulates 128-bit IPv6 networks.\n\n    Attributes: [examples for IPv6('2001:db8::1000/124')]\n        .network_address: IPv6Address('2001:db8::1000')\n        .hostmask: IPv6Address('::f')\n        .broadcast_address: IPv6Address('2001:db8::100f')\n        .netmask: IPv6Address('ffff:ffff:ffff:ffff:ffff:ffff:ffff:fff0')\n        .prefixlen: 124\n\n    \"\"\"\n\n    def __init__(self, address, strict=True):\n        \"\"\"Instantiate a new IPv6 Network object.\n\n        Args:\n            address: A string or integer representing the IPv6 network or the IP\n              and prefix/netmask.\n              '2001:db8::/128'\n              '2001:db8:0000:0000:0000:0000:0000:0000/128'\n              '2001:db8::'\n              are all functionally the same in IPv6.  That is to say,\n              failing to provide a subnetmask will create an object with\n              a mask of /128.\n\n              Additionally, an integer can be passed, so\n              IPv6Network('2001:db8::') ==\n                IPv6Network(42540766411282592856903984951653826560)\n              or, more generally\n              IPv6Network(int(IPv6Network('2001:db8::'))) ==\n                IPv6Network('2001:db8::')\n\n            strict: A boolean. If true, ensure that we have been passed\n              A true network address, eg, 2001:db8::1000/124 and not an\n              IP address on a network, eg, 2001:db8::1/124.\n\n        Raises:\n            AddressValueError: If address isn't a valid IPv6 address.\n            NetmaskValueError: If the netmask isn't valid for\n              an IPv6 address.\n            ValueError: If strict was True and a network address was not\n              supplied.\n\n        \"\"\"\n        _BaseV6.__init__(self, address)\n        _BaseNetwork.__init__(self, address)\n\n        # Efficient constructor from integer.\n        if isinstance(address, int):\n            if address < 0 or address > self._ALL_ONES:\n                raise AddressValueError(address)\n            self.network_address = IPv6Address(address)\n            self._prefixlen = self._max_prefixlen\n            self.netmask = IPv6Address(self._ALL_ONES)\n            if strict:\n                if (IPv6Address(int(self.network_address) &\n                                int(self.netmask)) != self.network_address):\n                    raise ValueError('%s has host bits set' % str(self))\n            self.network_address = IPv6Address(int(self.network_address) &\n                                               int(self.netmask))\n            return\n\n        # Constructing from a packed address\n        if isinstance(address, bytes) and len(address) == 16:\n            tmp = struct.unpack('!QQ', address)\n            self.network_address = IPv6Address((tmp[0] << 64) | tmp[1])\n            self._prefixlen = self._max_prefixlen\n            self.netmask = IPv6Address(self._ALL_ONES)\n            if strict:\n                if (IPv6Address(int(self.network_address) &\n                                int(self.netmask)) != self.network_address):\n                    raise ValueError('%s has host bits set' % str(self))\n                self.network_address = IPv6Address(int(self.network_address) &\n                                                   int(self.netmask))\n                return\n\n        # Assume input argument to be string or any object representation\n        # which converts into a formatted IP prefix string.\n        addr = str(address).split('/')\n\n        if len(addr) > 2:\n            raise AddressValueError(address)\n\n        self.network_address = IPv6Address(self._ip_int_from_string(addr[0]))\n\n        if len(addr) == 2:\n            if self._is_valid_netmask(addr[1]):\n                self._prefixlen = int(addr[1])\n            else:\n                raise NetmaskValueError(addr[1])\n        else:\n            self._prefixlen = self._max_prefixlen\n\n        self.netmask = IPv6Address(self._ip_int_from_prefix(self._prefixlen))\n        if strict:\n            if (IPv6Address(int(self.network_address) & int(self.netmask)) !=\n                self.network_address):\n                raise ValueError('%s has host bits set' % str(self))\n        self.network_address = IPv6Address(int(self.network_address) &\n                                           int(self.netmask))\n\n        if self._prefixlen == (self._max_prefixlen - 1):\n            self.hosts = self.__iter__\n\n    def __str__(self):\n        return '%s/%d' % (str(self.network_address),\n                          self.prefixlen)\n\n    def _is_valid_netmask(self, prefixlen):\n        \"\"\"Verify that the netmask/prefixlen is valid.\n\n        Args:\n            prefixlen: A string, the netmask in prefix length format.\n\n        Returns:\n            A boolean, True if the prefix represents a valid IPv6\n            netmask.\n\n        \"\"\"\n        try:\n            prefixlen = int(prefixlen)\n        except ValueError:\n            return False\n        return 0 <= prefixlen <= self._max_prefixlen\n\n    @property\n    def with_netmask(self):\n        return self.with_prefixlen\n\n    @property\n    def with_prefixlen(self):\n        return '%s/%d' % (str(self.network_address), self._prefixlen)\n\n    @property\n    def with_netmask(self):\n        return '%s/%s' % (str(self.network_address), str(self.netmask))\n\n    @property\n    def with_hostmask(self):\n        return '%s/%s' % (str(self.network_address), str(self.hostmask))\n"
  },
  {
    "path": "r2/r2/lib/contrib/rcssmin.py",
    "content": "#!/usr/bin/env python\n# -*- coding: ascii -*-\n#\n# Copyright 2011, 2012\n# Andr\\xe9 Malo or his licensors, as applicable\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\nr\"\"\"\n==============\n CSS Minifier\n==============\n\nCSS Minifier.\n\nThe minifier is based on the semantics of the `YUI compressor`_\\, which itself\nis based on `the rule list by Isaac Schlueter`_\\.\n\nThis module is a re-implementation aiming for speed instead of maximum\ncompression, so it can be used at runtime (rather than during a preprocessing\nstep). RCSSmin does syntactical compression only (removing spaces, comments\nand possibly semicolons). It does not provide semantic compression (like\nremoving empty blocks, collapsing redundant properties etc). It does, however,\nsupport various CSS hacks (by keeping them working as intended).\n\nHere's a feature list:\n\n- Strings are kept, except that escaped newlines are stripped\n- Space/Comments before the very end or before various characters are\n  stripped: ``:{});=>+],!`` (The colon (``:``) is a special case, a single\n  space is kept if it's outside a ruleset.)\n- Space/Comments at the very beginning or after various characters are\n  stripped: ``{}(=:>+[,!``\n- Optional space after unicode escapes is kept, resp. replaced by a simple\n  space\n- whitespaces inside ``url()`` definitions are stripped\n- Comments starting with an exclamation mark (``!``) can be kept optionally.\n- All other comments and/or whitespace characters are replaced by a single\n  space.\n- Multiple consecutive semicolons are reduced to one\n- The last semicolon within a ruleset is stripped\n- CSS Hacks supported:\n\n  - IE7 hack (``>/**/``)\n  - Mac-IE5 hack (``/*\\*/.../**/``)\n  - The boxmodelhack is supported naturally because it relies on valid CSS2\n    strings\n  - Between ``:first-line`` and the following comma or curly brace a space is\n    inserted. (apparently it's needed for IE6)\n  - Same for ``:first-letter``\n\nrcssmin.c is a reimplementation of rcssmin.py in C and improves runtime up to\nfactor 50 or so (depending on the input).\n\nBoth python 2 (>= 2.4) and python 3 are supported.\n\n.. _YUI compressor: https://github.com/yui/yuicompressor/\n\n.. _the rule list by Isaac Schlueter: https://github.com/isaacs/cssmin/tree/\n\"\"\"\n__author__ = \"Andr\\xe9 Malo\"\n__author__ = getattr(__author__, 'decode', lambda x: __author__)('latin-1')\n__docformat__ = \"restructuredtext en\"\n__license__ = \"Apache License, Version 2.0\"\n__version__ = '1.0.1'\n__all__ = ['cssmin']\n\nimport re as _re\n\n\ndef _make_cssmin(python_only=False):\n    \"\"\"\n    Generate CSS minifier.\n\n    :Parameters:\n      `python_only` : ``bool``\n        Use only the python variant. If true, the c extension is not even\n        tried to be loaded.\n\n    :Return: Minifier\n    :Rtype: ``callable``\n    \"\"\"\n    # pylint: disable = W0612\n    # (\"unused\" variables)\n\n    # pylint: disable = R0911, R0912, R0914, R0915\n    # (too many anything)\n\n    if not python_only:\n        try:\n            import _rcssmin\n        except ImportError:\n            pass\n        else:\n            return _rcssmin.cssmin\n\n    nl = r'(?:[\\n\\f]|\\r\\n?)' # pylint: disable = C0103\n    spacechar = r'[\\r\\n\\f\\040\\t]'\n\n    unicoded = r'[0-9a-fA-F]{1,6}(?:[\\040\\n\\t\\f]|\\r\\n?)?'\n    escaped = r'[^\\n\\r\\f0-9a-fA-F]'\n    escape = r'(?:\\\\(?:%(unicoded)s|%(escaped)s))' % locals()\n\n    nmchar = r'[^\\000-\\054\\056\\057\\072-\\100\\133-\\136\\140\\173-\\177]'\n    #nmstart = r'[^\\000-\\100\\133-\\136\\140\\173-\\177]'\n    #ident = (r'(?:'\n    #    r'-?(?:%(nmstart)s|%(escape)s)%(nmchar)s*(?:%(escape)s%(nmchar)s*)*'\n    #r')') % locals()\n\n    comment = r'(?:/\\*[^*]*\\*+(?:[^/*][^*]*\\*+)*/)'\n\n    # only for specific purposes. The bang is grouped:\n    _bang_comment = r'(?:/\\*(!?)[^*]*\\*+(?:[^/*][^*]*\\*+)*/)'\n\n    string1 = \\\n        r'(?:\\047[^\\047\\\\\\r\\n\\f]*(?:\\\\[^\\r\\n\\f][^\\047\\\\\\r\\n\\f]*)*\\047)'\n    string2 = r'(?:\"[^\"\\\\\\r\\n\\f]*(?:\\\\[^\\r\\n\\f][^\"\\\\\\r\\n\\f]*)*\")'\n    strings = r'(?:%s|%s)' % (string1, string2)\n\n    nl_string1 = \\\n        r'(?:\\047[^\\047\\\\\\r\\n\\f]*(?:\\\\(?:[^\\r]|\\r\\n?)[^\\047\\\\\\r\\n\\f]*)*\\047)'\n    nl_string2 = r'(?:\"[^\"\\\\\\r\\n\\f]*(?:\\\\(?:[^\\r]|\\r\\n?)[^\"\\\\\\r\\n\\f]*)*\")'\n    nl_strings = r'(?:%s|%s)' % (nl_string1, nl_string2)\n\n    uri_nl_string1 = r'(?:\\047[^\\047\\\\]*(?:\\\\(?:[^\\r]|\\r\\n?)[^\\047\\\\]*)*\\047)'\n    uri_nl_string2 = r'(?:\"[^\"\\\\]*(?:\\\\(?:[^\\r]|\\r\\n?)[^\"\\\\]*)*\")'\n    uri_nl_strings = r'(?:%s|%s)' % (uri_nl_string1, uri_nl_string2)\n\n    nl_escaped = r'(?:\\\\%(nl)s)' % locals()\n\n    space = r'(?:%(spacechar)s|%(comment)s)' % locals()\n\n    ie7hack = r'(?:>/\\*\\*/)'\n\n    uri = (r'(?:'\n        r'(?:[^\\000-\\040\"\\047()\\\\\\177]*'\n            r'(?:%(escape)s[^\\000-\\040\"\\047()\\\\\\177]*)*)'\n        r'(?:'\n            r'(?:%(spacechar)s+|%(nl_escaped)s+)'\n            r'(?:'\n                r'(?:[^\\000-\\040\"\\047()\\\\\\177]|%(escape)s|%(nl_escaped)s)'\n                r'[^\\000-\\040\"\\047()\\\\\\177]*'\n                r'(?:%(escape)s[^\\000-\\040\"\\047()\\\\\\177]*)*'\n            r')+'\n        r')*'\n    r')') % locals()\n\n    nl_unesc_sub = _re.compile(nl_escaped).sub\n\n    uri_space_sub = _re.compile((\n        r'(%(escape)s+)|%(spacechar)s+|%(nl_escaped)s+'\n    ) % locals()).sub\n    uri_space_subber = lambda m: m.groups()[0] or ''\n\n    space_sub_simple = _re.compile((\n        r'[\\r\\n\\f\\040\\t;]+|(%(comment)s+)'\n    ) % locals()).sub\n    space_sub_banged = _re.compile((\n        r'[\\r\\n\\f\\040\\t;]+|(%(_bang_comment)s+)'\n    ) % locals()).sub\n\n    post_esc_sub = _re.compile(r'[\\r\\n\\f\\t]+').sub\n\n    main_sub = _re.compile((\n        r'([^\\\\\"\\047u>@\\r\\n\\f\\040\\t/;:{}]+)'\n        r'|(?<=[{}(=:>+[,!])(%(space)s+)'\n        r'|^(%(space)s+)'\n        r'|(%(space)s+)(?=(([:{});=>+\\],!])|$)?)'\n        r'|;(%(space)s*(?:;%(space)s*)*)(?=(\\})?)'\n        r'|(\\{)'\n        r'|(\\})'\n        r'|(%(strings)s)'\n        r'|(?<!%(nmchar)s)url\\(%(spacechar)s*('\n                r'%(uri_nl_strings)s'\n                r'|%(uri)s'\n            r')%(spacechar)s*\\)'\n        r'|(@[mM][eE][dD][iI][aA])(?!%(nmchar)s)'\n        r'|(%(ie7hack)s)(%(space)s*)'\n        r'|(:[fF][iI][rR][sS][tT]-[lL]'\n            r'(?:[iI][nN][eE]|[eE][tT][tT][eE][rR]))'\n            r'(%(space)s*)(?=[{,])'\n        r'|(%(nl_strings)s)'\n        r'|(%(escape)s[^\\\\\"\\047u>@\\r\\n\\f\\040\\t/;:{}]*)'\n    ) % locals()).sub\n\n    #print main_sub.__self__.pattern\n\n    def main_subber(keep_bang_comments):\n        \"\"\" Make main subber \"\"\"\n        in_macie5, in_rule, at_media = [0], [0], [0]\n\n        if keep_bang_comments:\n            space_sub = space_sub_banged\n            def space_subber(match):\n                \"\"\" Space|Comment subber \"\"\"\n                if match.lastindex:\n                    group1, group2 = match.group(1, 2)\n                    if group2:\n                        if group1.endswith(r'\\*/'):\n                            in_macie5[0] = 1\n                        else:\n                            in_macie5[0] = 0\n                        return group1\n                    elif group1:\n                        if group1.endswith(r'\\*/'):\n                            if in_macie5[0]:\n                                return ''\n                            in_macie5[0] = 1\n                            return r'/*\\*/'\n                        elif in_macie5[0]:\n                            in_macie5[0] = 0\n                            return '/**/'\n                return ''\n        else:\n            space_sub = space_sub_simple\n            def space_subber(match):\n                \"\"\" Space|Comment subber \"\"\"\n                if match.lastindex:\n                    if match.group(1).endswith(r'\\*/'):\n                        if in_macie5[0]:\n                            return ''\n                        in_macie5[0] = 1\n                        return r'/*\\*/'\n                    elif in_macie5[0]:\n                        in_macie5[0] = 0\n                        return '/**/'\n                return ''\n\n        def fn_space_post(group):\n            \"\"\" space with token after \"\"\"\n            if group(5) is None or (\n                    group(6) == ':' and not in_rule[0] and not at_media[0]):\n                return ' ' + space_sub(space_subber, group(4))\n            return space_sub(space_subber, group(4))\n\n        def fn_semicolon(group):\n            \"\"\" ; handler \"\"\"\n            return ';' + space_sub(space_subber, group(7))\n\n        def fn_semicolon2(group):\n            \"\"\" ; handler \"\"\"\n            if in_rule[0]:\n                return space_sub(space_subber, group(7))\n            return ';' + space_sub(space_subber, group(7))\n\n        def fn_open(group):\n            \"\"\" { handler \"\"\"\n            # pylint: disable = W0613\n            if at_media[0]:\n                at_media[0] -= 1\n            else:\n                in_rule[0] = 1\n            return '{'\n\n        def fn_close(group):\n            \"\"\" } handler \"\"\"\n            # pylint: disable = W0613\n            in_rule[0] = 0\n            return '}'\n\n        def fn_media(group):\n            \"\"\" @media handler \"\"\"\n            at_media[0] += 1\n            return group(13)\n\n        def fn_ie7hack(group):\n            \"\"\" IE7 Hack handler \"\"\"\n            if not in_rule[0] and not at_media[0]:\n                in_macie5[0] = 0\n                return group(14) + space_sub(space_subber, group(15))\n            return '>' + space_sub(space_subber, group(15))\n\n        table = (\n            None,\n            None,\n            None,\n            None,\n            fn_space_post,                      # space with token after\n            fn_space_post,                      # space with token after\n            fn_space_post,                      # space with token after\n            fn_semicolon,                       # semicolon\n            fn_semicolon2,                      # semicolon\n            fn_open,                            # {\n            fn_close,                           # }\n            lambda g: g(11),                    # string\n            lambda g: 'url(%s)' % uri_space_sub(uri_space_subber, g(12)),\n                                                # url(...)\n            fn_media,                           # @media\n            None,\n            fn_ie7hack,                         # ie7hack\n            None,\n            lambda g: g(16) + ' ' + space_sub(space_subber, g(17)),\n                                                # :first-line|letter followed\n                                                # by [{,] (apparently space\n                                                # needed for IE6)\n            lambda g: nl_unesc_sub('', g(18)),  # nl_string\n            lambda g: post_esc_sub(' ', g(19)), # escape\n        )\n\n        def func(match):\n            \"\"\" Main subber \"\"\"\n            idx, group = match.lastindex, match.group\n            if idx > 3:\n                return table[idx](group)\n\n            # shortcuts for frequent operations below:\n            elif idx == 1:     # not interesting\n                return group(1)\n            #else: # space with token before or at the beginning\n            return space_sub(space_subber, group(idx))\n\n        return func\n\n    def cssmin(style, keep_bang_comments=False): # pylint: disable = W0621\n        \"\"\"\n        Minify CSS.\n\n        :Parameters:\n          `style` : ``str``\n            CSS to minify\n\n          `keep_bang_comments` : ``bool``\n            Keep comments starting with an exclamation mark? (``/*!...*/``)\n\n        :Return: Minified style\n        :Rtype: ``str``\n        \"\"\"\n        return main_sub(main_subber(keep_bang_comments), style)\n\n    return cssmin\n\ncssmin = _make_cssmin()\n\n\nif __name__ == '__main__':\n    def main():\n        \"\"\" Main \"\"\"\n        import sys as _sys\n        keep_bang_comments = (\n            '-b' in _sys.argv[1:]\n            or '-bp' in _sys.argv[1:]\n            or '-pb' in _sys.argv[1:]\n        )\n        if '-p' in _sys.argv[1:] or '-bp' in _sys.argv[1:] \\\n                or '-pb' in _sys.argv[1:]:\n            global cssmin # pylint: disable = W0603\n            cssmin = _make_cssmin(python_only=True)\n        _sys.stdout.write(cssmin(\n            _sys.stdin.read(), keep_bang_comments=keep_bang_comments\n        ))\n    main()\n"
  },
  {
    "path": "r2/r2/lib/contrib/simpleflake.py",
    "content": "# ========================================================================\n# simple-flake - https://github.com/SawdustSoftware/simple-flake\n# ========================================================================\n\n# Copyright (c) 2013 CustomMade Ventures\n\n# Permission is hereby granted, free of charge, to any person obtaining a copy\n# of this software and associated documentation files (the \"Software\"), to deal\n# in the Software without restriction, including without limitation the rights\n# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n# copies of the Software, and to permit persons to whom the Software is\n# furnished to do so, subject to the following conditions:\n\n# The above copyright notice and this permission notice shall be included in\n# all copies or substantial portions of the Software.\n\n# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n# THE SOFTWARE.\n\nimport time\nimport random\nimport collections\n\n#: Epoch for simpleflake timestamps, starts at the year 2000\nSIMPLEFLAKE_EPOCH = 946702800\n\n#field lengths in bits\nSIMPLEFLAKE_TIMESTAMP_LENGTH = 41\nSIMPLEFLAKE_RANDOM_LENGTH = 23\n\n#left shift amounts\nSIMPLEFLAKE_RANDOM_SHIFT = 0\nSIMPLEFLAKE_TIMESTAMP_SHIFT = 23\n\nsimpleflake_struct = collections.namedtuple(\"SimpleFlake\",\n                                            [\"timestamp\", \"random_bits\"])\n\n# ===================== Utility ====================\n\n\ndef pad_bytes_to_64(string):\n    return format(string, \"064b\")\n\n\ndef binary(num, padding=True):\n    \"\"\"Show binary digits of a number, pads to 64 bits unless specified.\"\"\"\n    binary_digits = \"{0:b}\".format(int(num))\n    if not padding:\n        return binary_digits\n    return pad_bytes_to_64(int(num))\n\n\ndef extract_bits(data, shift, length):\n    \"\"\"Extract a portion of a bit string. Similar to substr().\"\"\"\n    bitmask = ((1 << length) - 1) << shift\n    return ((data & bitmask) >> shift)\n\n# ==================================================\n\n\ndef simpleflake(timestamp=None, random_bits=None, epoch=SIMPLEFLAKE_EPOCH):\n    \"\"\"Generate a 64 bit, roughly-ordered, globally-unique ID.\"\"\"\n    second_time = timestamp if timestamp is not None else time.time()\n    second_time -= epoch\n    millisecond_time = int(second_time * 1000)\n\n    randomness = random.SystemRandom().getrandbits(SIMPLEFLAKE_RANDOM_LENGTH)\n    randomness = random_bits if random_bits is not None else randomness\n\n    flake = (millisecond_time << SIMPLEFLAKE_TIMESTAMP_SHIFT) + randomness\n\n    return flake\n\n\ndef parse_simpleflake(flake):\n    \"\"\"Parses a simpleflake and returns a named tuple with the parts.\"\"\"\n    timestamp = SIMPLEFLAKE_EPOCH\\\n        + extract_bits(flake,\n                       SIMPLEFLAKE_TIMESTAMP_SHIFT,\n                       SIMPLEFLAKE_TIMESTAMP_LENGTH) / 1000.0\n    random = extract_bits(flake,\n                          SIMPLEFLAKE_RANDOM_SHIFT,\n                          SIMPLEFLAKE_RANDOM_LENGTH)\n    return simpleflake_struct(timestamp, random)\n"
  },
  {
    "path": "r2/r2/lib/cookies.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom datetime import datetime, timedelta\n\nfrom pylons import tmpl_context as c\nfrom pylons import app_globals as g\nfrom pylons import request\n\nfrom r2.lib import utils\nfrom r2.models import COOKIE_TIMESTAMP_FORMAT\n\nNEVER = datetime(2037, 12, 31, 23, 59, 59)\nDELETE = datetime(1970, 01, 01, 0, 0, 1)\n\n\nclass Cookies(dict):\n    def add(self, name, value, *k, **kw):\n        name = name.encode('utf-8')\n        self[name] = Cookie(value, *k, **kw)\n\n\nclass Cookie(object):\n    def __init__(self, value, expires=None, domain=None,\n                 dirty=True, secure=None, httponly=False):\n        self.value = value\n        self.expires = expires\n        self.dirty = dirty\n        self.secure = secure\n        self.httponly = httponly\n        if domain:\n            self.domain = domain\n        else:\n            self.domain = g.domain\n\n    @staticmethod\n    def classify(cookie_name):\n        if cookie_name == g.login_cookie:\n            return \"session\"\n        elif cookie_name == g.admin_cookie:\n            return \"admin\"\n        elif cookie_name == \"reddit_first\":\n            return \"first\"\n        elif cookie_name == \"over18\":\n            return \"over18\"\n        elif cookie_name == \"secure_session\":\n            return \"secure_session\"\n        elif cookie_name.endswith(\"_last_thing\"):\n            return \"last_thing\"\n        elif cookie_name.endswith(\"_options\"):\n            return \"options\"\n        elif cookie_name.endswith(\"_recentclicks2\"):\n            return \"clicks\"\n        elif cookie_name.startswith(\"__utm\"):\n            return \"ga\"\n        elif cookie_name.startswith(\"beta_\"):\n            return \"beta\"\n        else:\n            return \"other\"\n\n    def __repr__(self):\n        return (\"Cookie(value=%r, expires=%r, domain=%r, dirty=%r)\"\n                % (self.value, self.expires, self.domain, self.dirty))\n\n\n# Cookies that might need the secure flag toggled\nPRIVATE_USER_COOKIES = [\"recentclicks2\"]\nPRIVATE_SESSION_COOKIES = [g.login_cookie, g.admin_cookie, \"_options\"]\n\n\ndef have_secure_session_cookie():\n    cookie = c.cookies.get(\"secure_session\", None)\n    return cookie and cookie.value == \"1\"\n\n\ndef change_user_cookie_security(secure, remember):\n    \"\"\"Mark a user's cookies as either secure or insecure.\n\n    (Un)set the secure flag on sensitive cookies, and add / remove\n    the cookie marking the session as HTTPS-only\n    \"\"\"\n    if secure:\n        set_secure_session_cookie(remember)\n    else:\n        delete_secure_session_cookie()\n\n    if not c.user_is_loggedin:\n        return\n\n    user_name = c.user.name\n    securable = (PRIVATE_SESSION_COOKIES +\n                 [user_name + \"_\" + c_name for c_name in PRIVATE_USER_COOKIES])\n    for name, cookie in c.cookies.iteritems():\n        if name in securable:\n            cookie.secure = secure\n            if name in PRIVATE_SESSION_COOKIES:\n                if name != \"_options\":\n                    cookie.httponly = True\n                # TODO: need a way to tell if a session is supposed to last\n                # forever. We don't get to see the expiry date of a cookie\n                if remember and name == g.login_cookie:\n                    cookie.expires = NEVER\n            cookie.dirty = True\n\n\ndef set_secure_session_cookie(remember=False):\n    expires = NEVER if remember else None\n    c.cookies[\"secure_session\"] = Cookie(\n        value=\"1\",\n        httponly=True,\n        expires=expires,\n        secure=False,\n    )\n\n\ndef delete_secure_session_cookie():\n    c.cookies[\"secure_session\"] = Cookie(\n        value=\"\",\n        httponly=True,\n        expires=DELETE,\n    )\n\n\ndef upgrade_cookie_security():\n    # We only upgrade on POSTs over HTTPS to prevent cookies from being cached\n    # by bad proxies\n    if not c.secure or request.method != \"POST\":\n        return\n\n    # There's likely not any cookies we need to upgrade\n    if not c.user_is_loggedin or c.oauth_user or have_secure_session_cookie():\n        return\n\n    # If they authed using a feedhash they might not even have this cookie\n    if g.login_cookie not in c.cookies:\n        return\n\n    sess_split = c.cookies[g.login_cookie].value.split(\",\")\n    if len(sess_split) != 3:\n        return\n\n    # If the cookie's old enough, just pretend we know they had \"remember me\"\n    # ticked.\n    sess_start_time = datetime.strptime(sess_split[1], COOKIE_TIMESTAMP_FORMAT)\n    rem = (datetime.now() - sess_start_time > timedelta(days=30))\n    change_user_cookie_security(secure=True, remember=rem)\n"
  },
  {
    "path": "r2/r2/lib/count.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom r2.models import Link, Subreddit\nfrom r2.lib import utils\nfrom r2.lib.db.operators import desc\nfrom pylons import config\nfrom pylons import app_globals as g\n\n\ncount_period = g.rising_period\n\n#stubs\n\ndef incr_counts(wrapped):\n    pass\n\ndef get_link_counts(period = count_period):\n    links = Link._query(Link.c._date >= utils.timeago(period),\n                        limit=50, data = True)\n    return dict((l._fullname, (0, l.sr_id)) for l in links)\n\ndef get_sr_counts():\n    srs = utils.fetch_things2(Subreddit._query(sort=desc(\"_date\")))\n\n    return dict((sr._fullname, sr._ups) for sr in srs)\n\n\nif config['r2.import_private']:\n    from r2admin.lib.count import *\n"
  },
  {
    "path": "r2/r2/lib/csrf.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\n\nclass CSRFPreventionException(Exception):\n    pass\n\n\ndef csrf_exempt(fn):\n    \"\"\"Mark an endpoint as exempt from CSRF prevention checks\"\"\"\n    fn.handles_csrf = True\n    return fn\n\n\ndef check_controller_csrf_prevention(controller):\n    \"\"\"Check that the a controller and its handlers are properly protected\n       from CSRF attacks\"\"\"\n    if getattr(controller, 'handles_csrf', False):\n        return\n\n    # We're only interested in handlers that might mutate data\n    mutating_methods = {\"POST\", \"PUT\", \"PATCH\", \"DELETE\"}\n\n    for name, func in controller.__dict__.iteritems():\n        method, sep, action = name.partition('_')\n        if not action:\n            continue\n        if method not in mutating_methods:\n            continue\n\n        # Check if the handler has specified how it deals with CSRF\n        if not getattr(func, 'handles_csrf', False):\n            endpoint_name = ':'.join((controller.__name__, name))\n            msg = (\"Handlers that might mutate data must be \"\n                   \"explicit about CSRF prevention: %s\" % endpoint_name)\n            raise CSRFPreventionException(msg)\n"
  },
  {
    "path": "r2/r2/lib/cssfilter.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\"\"\"Parse and validate a safe subset of CSS.\n\nThe goal of this validation is not to ensure functionally correct stylesheets\nbut rather that the stylesheet is safe to show to downstream users.  This\nincludes:\n\n    * not generating requests to third party hosts (information leak)\n    * xss via strange syntax in buggy browsers\n\nBeyond that, every effort is made to allow the full gamut of modern CSS.\n\n\"\"\"\n\nimport itertools\nimport re\nimport unicodedata\n\nimport tinycss2\n\nfrom pylons.i18n import N_\n\nfrom r2.lib.contrib import rcssmin\nfrom r2.lib.utils import tup\n\n\n__all__ = [\"validate_css\"]\n\n\nSIMPLE_TOKEN_TYPES = {\n    \"dimension\",\n    \"hash\",\n    \"ident\",\n    \"literal\",\n    \"number\",\n    \"percentage\",\n    \"string\",\n    \"whitespace\",\n}\n\n\nVENDOR_PREFIXES = {\n    \"-apple-\",\n    \"-khtml-\",\n    \"-moz-\",\n    \"-ms-\",\n    \"-o-\",\n    \"-webkit-\",\n}\nassert all(prefix == prefix.lower() for prefix in VENDOR_PREFIXES)\n\n\nSAFE_PROPERTIES = {\n    \"align-content\",\n    \"align-items\",\n    \"align-self\",\n    \"animation\",\n    \"animation-delay\",\n    \"animation-direction\",\n    \"animation-duration\",\n    \"animation-fill-mode\",\n    \"animation-iteration-count\",\n    \"animation-name\",\n    \"animation-play-state\",\n    \"animation-timing-function\",\n    \"appearance\",\n    \"backface-visibility\",\n    \"background\",\n    \"background-attachment\",\n    \"background-blend-mode\",\n    \"background-clip\",\n    \"background-color\",\n    \"background-image\",\n    \"background-origin\",\n    \"background-position\",\n    \"background-position-x\",\n    \"background-position-y\",\n    \"background-repeat\",\n    \"background-size\",\n    \"border\",\n    \"border-bottom\",\n    \"border-bottom-color\",\n    \"border-bottom-left-radius\",\n    \"border-bottom-right-radius\",\n    \"border-bottom-style\",\n    \"border-bottom-width\",\n    \"border-collapse\",\n    \"border-color\",\n    \"border-image\",\n    \"border-image-outset\",\n    \"border-image-repeat\",\n    \"border-image-slice\",\n    \"border-image-source\",\n    \"border-image-width\",\n    \"border-left\",\n    \"border-left-color\",\n    \"border-left-style\",\n    \"border-left-width\",\n    \"border-radius\",\n    \"border-radius-bottomleft\",\n    \"border-radius-bottomright\",\n    \"border-radius-topleft\",\n    \"border-radius-topright\",\n    \"border-right\",\n    \"border-right-color\",\n    \"border-right-style\",\n    \"border-right-width\",\n    \"border-spacing\",\n    \"border-style\",\n    \"border-top\",\n    \"border-top-color\",\n    \"border-top-left-radius\",\n    \"border-top-right-radius\",\n    \"border-top-style\",\n    \"border-top-width\",\n    \"border-width\",\n    \"bottom\",\n    \"box-shadow\",\n    \"box-sizing\",\n    \"caption-side\",\n    \"clear\",\n    \"clip\",\n    \"clip-path\",\n    \"color\",\n    \"content\",\n    \"counter-increment\",\n    \"counter-reset\",\n    \"cue\",\n    \"cue-after\",\n    \"cue-before\",\n    \"cursor\",\n    \"direction\",\n    \"display\",\n    \"elevation\",\n    \"empty-cells\",\n    # the \"filter\" property cannot be safely added while IE9 is allowed to\n    # use subreddit stylesheets. see explanation here:\n    # https://github.com/reddit/reddit/pull/1058#issuecomment-76466180\n    # \"filter\",\n    \"flex\",\n    \"flex-align\",\n    \"flex-basis\",\n    \"flex-direction\",\n    \"flex-flow\",\n    \"flex-grow\",\n    \"flex-item-align\",\n    \"flex-line-pack\",\n    \"flex-order\",\n    \"flex-pack\",\n    \"flex-shrink\",\n    \"flex-wrap\",\n    \"float\",\n    \"font\",\n    \"font-family\",\n    \"font-size\",\n    \"font-style\",\n    \"font-variant\",\n    \"font-weight\",\n    \"grid\",\n    \"grid-area\",\n    \"grid-auto-columns\",\n    \"grid-auto-flow\",\n    \"grid-auto-position\",\n    \"grid-auto-rows\",\n    \"grid-column\",\n    \"grid-column-start\",\n    \"grid-column-end\",\n    \"grid-row\",\n    \"grid-row-start\",\n    \"grid-row-end\",\n    \"grid-template\",\n    \"grid-template-areas\",\n    \"grid-template-rows\",\n    \"grid-template-columns\",\n    \"hanging-punctuation\",\n    \"height\",\n    \"hyphens\",\n    \"image-orientation\",\n    \"image-rendering\",\n    \"image-resolution\",\n    \"justify-content\",\n    \"left\",\n    \"letter-spacing\",\n    \"line-break\",\n    \"line-height\",\n    \"list-style\",\n    \"list-style-image\",\n    \"list-style-position\",\n    \"list-style-type\",\n    \"margin\",\n    \"margin-bottom\",\n    \"margin-left\",\n    \"margin-right\",\n    \"margin-top\",\n    \"max-height\",\n    \"max-width\",\n    \"mask\",\n    \"mask-border\",\n    \"mask-border-mode\",\n    \"mask-border-outset\",\n    \"mask-border-repeat\",\n    \"mask-border-source\",\n    \"mask-border-slice\",\n    \"mask-border-width\",\n    \"mask-clip\",\n    \"mask-composite\",\n    \"mask-image\",\n    \"mask-mode\",\n    \"mask-origin\",\n    \"mask-position\",\n    \"mask-repeat\",\n    \"mask-size\",\n    \"min-height\",\n    \"min-width\",\n    \"mix-blend-mode\",\n    \"opacity\",\n    \"order\",\n    \"orphans\",\n    \"outline\",\n    \"outline-color\",\n    \"outline-offset\",\n    \"outline-style\",\n    \"outline-width\",\n    \"overflow\",\n    \"overflow-wrap\",\n    \"overflow-x\",\n    \"overflow-y\",\n    \"padding\",\n    \"padding-bottom\",\n    \"padding-left\",\n    \"padding-right\",\n    \"padding-top\",\n    \"page-break-after\",\n    \"page-break-before\",\n    \"page-break-inside\",\n    \"pause\",\n    \"pause-after\",\n    \"pause-before\",\n    \"perspective\",\n    \"perspective-origin\",\n    \"pitch\",\n    \"pitch-range\",\n    \"play-during\",\n    \"pointer-events\",\n    \"position\",\n    \"quotes\",\n    \"resize\",\n    \"richness\",\n    \"right\",\n    \"speak\",\n    \"speak-header\",\n    \"speak-numeral\",\n    \"speak-punctuation\",\n    \"speech-rate\",\n    \"stress\",\n    \"table-layout\",\n    \"tab-size\",\n    \"text-align\",\n    \"text-align-last\",\n    \"text-decoration\",\n    \"text-decoration-color\",\n    \"text-decoration-line\",\n    \"text-decoration-skip\",\n    \"text-decoration-style\",\n    \"text-indent\",\n    \"text-justify\",\n    \"text-overflow\",\n    \"text-rendering\",\n    \"text-shadow\",\n    \"text-size-adjust\",\n    \"text-space-collapse\",\n    \"text-transform\",\n    \"text-underline-position\",\n    \"text-wrap\",\n    \"top\",\n    \"transform\",\n    \"transform-origin\",\n    \"transform-style\",\n    \"transition\",\n    \"transition-delay\",\n    \"transition-duration\",\n    \"transition-property\",\n    \"transition-timing-function\",\n    \"unicode-bidi\",\n    \"vertical-align\",\n    \"visibility\",\n    \"voice-family\",\n    \"volume\",\n    \"white-space\",\n    \"widows\",\n    \"width\",\n    \"will-change\",\n    \"word-break\",\n    \"word-spacing\",\n    \"z-index\",\n}\nassert all(property == property.lower() for property in SAFE_PROPERTIES)\n\n\nSAFE_FUNCTIONS = {\n    \"attr\",\n    \"calc\",\n    \"circle\",\n    \"counter\",\n    \"counters\",\n    \"cubic-bezier\",\n    \"ellipse\",\n    \"hsl\",\n    \"hsla\",\n    \"lang\",\n    \"line\",\n    \"linear-gradient\",\n    \"matrix\",\n    \"matrix3d\",\n    \"not\",\n    \"nth-child\",\n    \"nth-last-child\",\n    \"nth-last-of-type\",\n    \"nth-of-type\",\n    \"perspective\",\n    \"polygon\",\n    \"polyline\",\n    \"radial-gradient\",\n    \"rect\",\n    \"repeating-linear-gradient\",\n    \"repeating-radial-gradient\",\n    \"rgb\",\n    \"rgba\",\n    \"rotate\",\n    \"rotate3d\",\n    \"rotatex\",\n    \"rotatey\",\n    \"rotatez\",\n    \"scale\",\n    \"scale3d\",\n    \"scalex\",\n    \"scaley\",\n    \"scalez\",\n    \"skewx\",\n    \"skewy\",\n    \"steps\",\n    \"translate\",\n    \"translate3d\",\n    \"translatex\",\n    \"translatey\",\n    \"translatez\",\n}\nassert all(function == function.lower() for function in SAFE_FUNCTIONS)\n\n\nERROR_MESSAGES = {\n    \"IMAGE_NOT_FOUND\": N_('no image found with name \"%(name)s\"'),\n    \"NON_PLACEHOLDER_URL\": N_(\"only uploaded images are allowed; reference \"\n                              \"them with the %%%%imagename%%%% system\"),\n    \"SYNTAX_ERROR\": N_(\"syntax error: %(message)s\"),\n    \"UNKNOWN_AT_RULE\": N_(\"@%(keyword)s is not allowed\"),\n    \"UNKNOWN_PROPERTY\": N_('unknown property \"%(name)s\"'),\n    \"UNKNOWN_FUNCTION\": N_('unknown function \"%(function)s\"'),\n    \"UNEXPECTED_TOKEN\": N_('unexpected token \"%(token)s\"'),\n    \"BACKSLASH\": N_(\"backslashes are not allowed\"),\n    \"CONTROL_CHARACTER\": N_(\"control characters are not allowed\"),\n    \"TOO_BIG\": N_(\"the stylesheet is too big. maximum size: %(size)d KiB\"),\n}\n\n\nMAX_SIZE_KIB = 100\nSUBREDDIT_IMAGE_URL_PLACEHOLDER = re.compile(r\"\\A%%([a-zA-Z0-9\\-]+)%%\\Z\")\n\n\ndef strip_vendor_prefix(identifier):\n    for prefix in VENDOR_PREFIXES:\n        if identifier.startswith(prefix):\n            return identifier[len(prefix):]\n    return identifier\n\n\nclass ValidationError(object):\n    def __init__(self, line_number, error_code, message_params=None):\n        self.line = line_number\n        self.error_code = error_code\n        self.message_params = message_params or {}\n        # note: _source_lines is added to these objects by the parser\n\n    @property\n    def offending_line(self):\n        return self._source_lines[self.line - 1]\n\n    @property\n    def message_key(self):\n        return ERROR_MESSAGES[self.error_code]\n\n\nclass StylesheetValidator(object):\n    def __init__(self, images):\n        self.images = images\n\n    def validate_url(self, url_node):\n        m = SUBREDDIT_IMAGE_URL_PLACEHOLDER.match(url_node.value)\n        if not m:\n            return ValidationError(url_node.source_line, \"NON_PLACEHOLDER_URL\")\n\n        image_name = m.group(1)\n        if image_name not in self.images:\n            return ValidationError(url_node.source_line, \"IMAGE_NOT_FOUND\",\n                                   {\"name\": image_name})\n\n        # rewrite the url value to the actual url of the image\n        url_node.value = self.images[image_name]\n\n    def validate_function(self, function_node):\n        function_name = strip_vendor_prefix(function_node.lower_name)\n\n        if function_name not in SAFE_FUNCTIONS:\n            return ValidationError(function_node.source_line,\n                                   \"UNKNOWN_FUNCTION\",\n                                   {\"function\": function_node.name})\n        # property: attr(something url)\n        # https://developer.mozilla.org/en-US/docs/Web/CSS/attr\n        elif function_name == \"attr\":\n            for argument in function_node.arguments:\n                if argument.type == \"ident\" and argument.lower_value == \"url\":\n                    return ValidationError(argument.source_line,\n                                           \"NON_PLACEHOLDER_URL\")\n\n        return self.validate_component_values(function_node.arguments)\n\n    def validate_block(self, block):\n        return self.validate_component_values(block.content)\n\n    def validate_component_values(self, component_values):\n        return self.validate_list(component_values, {\n            # {} blocks are technically part of component values but i don't\n            # know of any actual valid uses for them in selectors etc. and they\n            # can cause issues with e.g.\n            # Safari 5: p[foo=bar{}*{background:green}]{background:red}\n            \"[] block\": self.validate_block,\n            \"() block\": self.validate_block,\n            \"url\": self.validate_url,\n            \"function\": self.validate_function,\n        }, ignored_types=SIMPLE_TOKEN_TYPES)\n\n    def validate_declaration(self, declaration):\n        if strip_vendor_prefix(declaration.lower_name) not in SAFE_PROPERTIES:\n            return ValidationError(declaration.source_line, \"UNKNOWN_PROPERTY\",\n                                   {\"name\": declaration.name})\n        return self.validate_component_values(declaration.value)\n\n    def validate_declaration_list(self, declarations):\n        return self.validate_list(declarations, {\n            \"at-rule\": self.validate_at_rule,\n            \"declaration\": self.validate_declaration,\n        })\n\n    def validate_qualified_rule(self, rule):\n        prelude_errors = self.validate_component_values(rule.prelude)\n        declarations = tinycss2.parse_declaration_list(rule.content)\n        declaration_errors = self.validate_declaration_list(declarations)\n        return itertools.chain(prelude_errors, declaration_errors)\n\n    def validate_at_rule(self, rule):\n        prelude_errors = self.validate_component_values(rule.prelude)\n\n        keyword = strip_vendor_prefix(rule.lower_at_keyword)\n\n        if keyword in (\"media\", \"keyframes\"):\n            rules = tinycss2.parse_rule_list(rule.content)\n            rule_errors = self.validate_rule_list(rules)\n        elif keyword == \"page\":\n            rule_errors = self.validate_qualified_rule(rule)\n        else:\n            return ValidationError(rule.source_line, \"UNKNOWN_AT_RULE\",\n                                   {\"keyword\": rule.at_keyword})\n\n        return itertools.chain(prelude_errors, rule_errors)\n\n    def validate_rule_list(self, rules):\n        return self.validate_list(rules, {\n            \"qualified-rule\": self.validate_qualified_rule,\n            \"at-rule\": self.validate_at_rule,\n        })\n\n    def validate_list(self, nodes, validators_by_type, ignored_types=None):\n        for node in nodes:\n            if node.type == \"error\":\n                yield ValidationError(node.source_line, \"SYNTAX_ERROR\",\n                                      {\"message\": node.message})\n                continue\n            elif node.type == \"literal\":\n                if node.value == \";\":\n                    # if we're seeing a semicolon as a literal, it's in a place\n                    # that doesn't fit naturally in the syntax.\n                    # Safari 5 will treat this as two color properties:\n                    # color: calc(;color:red;);\n                    message = \"semicolons are not allowed in this context\"\n                    yield ValidationError(node.source_line, \"SYNTAX_ERROR\",\n                                          {\"message\": message})\n                    continue\n\n            validator = validators_by_type.get(node.type)\n\n            if validator:\n                for error in tup(validator(node)):\n                    if error:\n                        yield error\n            else:\n                if not ignored_types or node.type not in ignored_types:\n                    yield ValidationError(node.source_line,\n                                          \"UNEXPECTED_TOKEN\",\n                                          {\"token\": node.type})\n\n    def check_for_evil_codepoints(self, source_lines):\n        for line_number, line_text in enumerate(source_lines, start=1):\n            for codepoint in line_text:\n                # IE<8: *{color: expression\\28 alert\\28 1 \\29 \\29 }\n                if codepoint == \"\\\\\":\n                    yield ValidationError(line_number, \"BACKSLASH\")\n                    break\n                # accept these characters that get classified as control\n                elif codepoint in (\"\\t\", \"\\n\", \"\\r\"):\n                    continue\n                # Safari: *{font-family:'foobar\\x03;background:url(evil);';}\n                elif unicodedata.category(codepoint).startswith(\"C\"):\n                    yield ValidationError(line_number, \"CONTROL_CHARACTER\")\n                    break\n\n    def parse_and_validate(self, stylesheet_source):\n        if len(stylesheet_source) > (MAX_SIZE_KIB * 1024):\n            return \"\", [ValidationError(0, \"TOO_BIG\", {\"size\": MAX_SIZE_KIB})]\n\n        nodes = tinycss2.parse_stylesheet(stylesheet_source)\n\n        source_lines = stylesheet_source.splitlines()\n\n        backslash_errors = self.check_for_evil_codepoints(source_lines)\n        validation_errors = self.validate_rule_list(nodes)\n\n        errors = []\n        for error in itertools.chain(backslash_errors, validation_errors):\n            error._source_lines = source_lines\n            errors.append(error)\n        errors.sort(key=lambda e: e.line)\n\n        if not errors:\n            serialized = rcssmin.cssmin(tinycss2.serialize(nodes))\n        else:\n            serialized = \"\"\n\n        return serialized.encode(\"utf-8\"), errors\n\n\ndef validate_css(stylesheet, images):\n    \"\"\"Validate and re-serialize the user submitted stylesheet.\n\n    images is a mapping of subreddit image names to their URLs.  The\n    re-serialized stylesheet will have %%name%% tokens replaced with their\n    appropriate URLs.\n\n    The return value is a two-tuple of the re-serialized (and minified)\n    stylesheet and a list of errors.  If the list is empty, the stylesheet is\n    valid.\n\n    \"\"\"\n    assert isinstance(stylesheet, unicode)\n    validator = StylesheetValidator(images)\n    return validator.parse_and_validate(stylesheet)\n"
  },
  {
    "path": "r2/r2/lib/db/__init__.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\n"
  },
  {
    "path": "r2/r2/lib/db/_sorts.pyx",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport math\nfrom datetime import datetime, timedelta\nfrom pylons import app_globals as g\n\n\ncdef extern from \"math.h\":\n    double log10(double)\n    double sqrt(double)\n\nepoch = datetime(1970, 1, 1, tzinfo = g.tz)\n\ncpdef double epoch_seconds(date):\n    \"\"\"Returns the number of seconds from the epoch to date. Should\n       match the number returned by the equivalent function in\n       postgres.\"\"\"\n    td = date - epoch\n    return td.days * 86400 + td.seconds + (float(td.microseconds) / 1000000)\n\ncpdef long score(long ups, long downs):\n    return ups - downs\n\ncpdef double hot(long ups, long downs, date):\n    return _hot(ups, downs, epoch_seconds(date))\n\ncpdef double _hot(long ups, long downs, double date):\n    \"\"\"The hot formula. Should match the equivalent function in postgres.\"\"\"\n    s = score(ups, downs)\n    order = log10(max(abs(s), 1))\n    if s > 0:\n        sign = 1\n    elif s < 0:\n        sign = -1\n    else:\n        sign = 0\n    seconds = date - 1134028003\n    return round(sign * order + seconds / 45000, 7)\n\ncpdef double controversy(long ups, long downs):\n    \"\"\"The controversy sort.\"\"\"\n    if downs <= 0 or ups <= 0:\n        return 0\n\n    magnitude = ups + downs\n    balance = float(downs) / ups if ups > downs else float(ups) / downs\n\n    return magnitude ** balance\n\ncpdef double _confidence(int ups, int downs):\n    \"\"\"The confidence sort.\n       http://www.evanmiller.org/how-not-to-sort-by-average-rating.html\"\"\"\n    cdef float n = ups + downs\n\n    if n == 0:\n        return 0\n\n    cdef float z = 1.281551565545 # 80% confidence\n    cdef float p = float(ups) / n\n\n    left = p + 1/(2*n)*z*z\n    right = z*sqrt(p*(1-p)/n + z*z/(4*n*n))\n    under = 1+1/n*z*z\n\n    return (left - right) / under\n\ncdef int up_range = 400\ncdef int down_range = 100\ncdef list _confidences = []\nfor ups in xrange(up_range):\n    for downs in xrange(down_range):\n        _confidences.append(_confidence(ups, downs))\ndef confidence(int ups, int downs):\n    if ups + downs == 0:\n        return 0\n    elif ups < up_range and downs < down_range:\n        return _confidences[downs + ups * down_range]\n    else:\n        return _confidence(ups, downs)\n\ncpdef double qa(int question_ups, int question_downs, int question_length,\n                op_children):\n    \"\"\"The Q&A-type sort.\n\n    Similar to the \"best\" (confidence) sort, but specially designed for\n    Q&A-type threads to highlight good question/answer pairs.\n    \"\"\"\n    question_score = confidence(question_ups, question_downs)\n\n    if not op_children:\n        return _qa(question_score, question_length)\n\n    # Only take into account the \"best\" answer from OP.\n    best_score = None\n    for answer in op_children:\n        score = confidence(answer._ups, answer._downs)\n        if best_score is None or score > best_score:\n            best_score = score\n            answer_length = len(answer.body)\n    return _qa(question_score, question_length, best_score, answer_length)\n\ncpdef double _qa(double question_score, int question_length,\n                 double answer_score=0, int answer_length=1):\n    score_modifier = question_score + answer_score\n\n    # Give more weight to longer posts, but count longer text less and less to\n    # avoid artificially high rankings for long-spam posts.\n    length_modifier = log10(question_length + answer_length)\n\n    # Add together the weighting from the scores and lengths, but emphasize\n    # score more.\n    return score_modifier + (length_modifier / 5)\n"
  },
  {
    "path": "r2/r2/lib/db/alter_db.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport tdb_sql\nimport sqlalchemy as sa\n\ndef thing_tables():\n    for type in tdb_sql.types_id.values():\n        yield type.thing_table\n\n    for table in tdb_sql.extra_thing_tables.values():\n        yield table\n\ndef rel_tables():\n    for type in tdb_sql.rel_types_id.values():\n        yield type.rel_table[0]\n\ndef dtables():\n    for type in tdb_sql.types_id.values():\n        yield type.data_table[0]\n\n    for type in tdb_sql.rel_types_id.values():\n        yield type.rel_table[3]\n\ndef exec_all(command, data=False, rel = False, print_only = False):\n    if data:\n        tables = dtables()\n    elif rel:\n        tables = rel_tables()\n    else:\n        tables = thing_tables()\n\n    for tt in tables:\n        #print tt\n        engine = tt.bind\n        if print_only:\n            print command % dict(type=tt.name)\n        else:\n            try:\n                engine.execute(command % dict(type=tt.name))\n            except:\n                print \"FAILED!\"\n\n\"alter table %(type)s add primary key (thing_id, key)\"\n\"drop index idx_thing_id_%(type)s\"\n\n\"create index concurrently idx_thing1_name_date_%(type)s on %(type)s (thing1_id, name, date);\"\n"
  },
  {
    "path": "r2/r2/lib/db/operators.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nclass BooleanOp(object):\n    def __init__(self, *ops):\n        self.ops = ops\n\n    def __repr__(self):\n        return '<%s_ %s>' % (self.__class__.__name__, str(self.ops))\n\nclass or_(BooleanOp): pass\nclass and_(BooleanOp): pass\nclass not_(BooleanOp): pass\n\nclass op(object):\n    def __init__(self, lval, lval_name, rval):\n        self.lval = lval\n        self.rval = rval\n        self.lval_name = lval_name\n\n    def __repr__(self):\n        return '<%s: %s, %s>' % (self.__class__.__name__, self.lval, self.rval)\n\n    # sorts in a consistent order, required for Query._cache_key()\n    def __cmp__(self, other):\n        return cmp(repr(self), repr(other))\n\nclass eq(op): pass\nclass ne(op): pass\nclass lt(op): pass\nclass lte(op): pass\nclass gt(op): pass\nclass gte(op): pass\nclass in_(op): pass\n\nclass Slot(object):\n    def __init__(self, lval):\n        if isinstance(lval, Slot):\n            self.name = lval.name\n            self.lval = lval\n        else:\n            self.name = lval\n\n    def __repr__(self):\n        return '<%s: %s>' % (self.__class__.__name__, self.name)\n\n    def __eq__(self, other):\n        return eq(self, self.name, other)\n\n    def __ne__(self, other):\n        return ne(self, self.name, other)\n\n    def __lt__(self, other):\n        return lt(self, self.name, other)\n\n    def __le__(self, other):\n        return lte(self, self.name, other)\n\n    def __gt__(self, other):\n        return gt(self, self.name, other)\n\n    def __ge__(self, other):\n        return gte(self, self.name, other)\n\n    def in_(self, other):\n        return in_(self, self.name, other)\n\nclass Slots(object):\n    def __getattr__(self, attr):\n        return Slot(attr)\n\n    def __getitem__(self, attr):\n        return Slot(attr)\n        \ndef op_iter(ops):\n    for o in ops:\n        if isinstance(o, op):\n            yield o\n        elif isinstance(o, BooleanOp):\n            for p in op_iter(o.ops):\n                yield p\n\nclass query_func(Slot): pass\nclass lower(query_func): pass\nclass ip_network(query_func): pass\nclass base_url(query_func): pass\nclass domain(query_func): pass\nclass year_func(query_func): pass\n\nclass timeago(object):\n    def __init__(self, interval):\n        self.interval = interval\n\n    def __repr__(self):\n        return '<interval: %s>' % self.interval\n\nclass sort(object):\n    def __init__(self, col):\n        self.col = col\n\n    def __repr__(self):\n        return '<sort:%s %s>' % (self.__class__.__name__, str(self.col))\n\n    def __eq__(self, other):\n        return self.__class__ == other.__class__ and self.col == other.col\n\n    def __ne__(self, other):\n        return not self.__eq__(other)\n\n\nclass asc(sort): pass\nclass desc(sort):pass\nclass shuffled(desc): pass\n"
  },
  {
    "path": "r2/r2/lib/db/queries.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport collections\nfrom copy import deepcopy, copy\nimport cPickle as pickle\nfrom datetime import datetime\nfrom functools import partial\nimport hashlib\nimport itertools\nimport pytz\nfrom time import mktime\n\nfrom pylons import app_globals as g\nfrom pylons import tmpl_context as c\nfrom pylons import request\n\nfrom r2.lib import amqp\nfrom r2.lib import filters\nfrom r2.lib.comment_tree import add_comments\nfrom r2.lib.db import tdb_cassandra\nfrom r2.lib.db.operators import and_, or_\nfrom r2.lib.db.operators import asc, desc, timeago\nfrom r2.lib.db.sorts import epoch_seconds\nfrom r2.lib.db.thing import Thing, Merge\nfrom r2.lib import utils\nfrom r2.lib.utils import in_chunks, is_subdomain, SimpleSillyStub\nfrom r2.lib.utils import fetch_things2, tup, UniqueIterator\nfrom r2.lib.voting import prequeued_vote_key\nfrom r2.models import (\n    Account,\n    Comment,\n    Inbox,\n    Link,\n    LinksByAccount,\n    Message,\n    ModContribSR,\n    ModeratorInbox,\n    MultiReddit,\n    PromoCampaign,\n    Report,\n    Subreddit,\n    VotesByAccount,\n)\nfrom r2.models.last_modified import LastModified\nfrom r2.models.promo import PROMOTE_STATUS, PromotionLog\nfrom r2.models.query_cache import (\n    cached_query,\n    CachedQuery,\n    CachedQueryMutator,\n    filter_thing,\n    FakeQuery,\n    merged_cached_query,\n    MergedCachedQuery,\n    SubredditQueryCache,\n    ThingTupleComparator,\n    UserQueryCache,\n)\nfrom r2.models.vote import Vote\n\n\nprecompute_limit = 1000\n\ndb_sorts = dict(hot = (desc, '_hot'),\n                new = (desc, '_date'),\n                top = (desc, '_score'),\n                controversial = (desc, '_controversy'))\n\ndef db_sort(sort):\n    cls, col = db_sorts[sort]\n    return cls(col)\n\ndb_times = dict(all = None,\n                hour = Thing.c._date >= timeago('1 hour'),\n                day = Thing.c._date >= timeago('1 day'),\n                week = Thing.c._date >= timeago('1 week'),\n                month = Thing.c._date >= timeago('1 month'),\n                year = Thing.c._date >= timeago('1 year'))\n\n# sorts for which there can be a time filter (by day, by week,\n# etc). All of these but 'all' are done in mr_top, who knows about the\n# structure of the stored CachedResults (so changes here may warrant\n# changes there)\ntime_filtered_sorts = set(('top', 'controversial'))\n\n#we need to define the filter functions here so cachedresults can be pickled\ndef filter_identity(x):\n    return x\n\ndef filter_thing2(x):\n    \"\"\"A filter to apply to the results of a relationship query returns\n    the object of the relationship.\"\"\"\n    return x._thing2\n\n\nclass CachedResults(object):\n    \"\"\"Given a query returns a list-like object that will lazily look up\n    the query from the persistent cache. \"\"\"\n    def __init__(self, query, filter):\n        self.query = query\n        self.query._limit = precompute_limit\n        self.filter = filter\n        self.iden = self.get_query_iden(query)\n        self.sort_cols = [s.col for s in self.query._sort]\n        self.data = []\n        self._fetched = False\n\n    @classmethod\n    def get_query_iden(cls, query):\n        # previously in Query._iden()\n        i = str(query._sort) + str(query._kind) + str(query._limit)\n\n        if query._offset:\n            i += str(query._offset)\n\n        if query._rules:\n            rules = copy(query._rules)\n            rules.sort()\n            for r in rules:\n                i += str(r)\n\n        return hashlib.sha1(i).hexdigest()\n\n    @property\n    def sort(self):\n        return self.query._sort\n\n    def fetch(self, force=False, stale=False):\n        \"\"\"Loads the query from the cache.\"\"\"\n        self.fetch_multi([self], force=force, stale=stale)\n\n    @classmethod\n    def fetch_multi(cls, crs, force=False, stale=False):\n        unfetched = filter(lambda cr: force or not cr._fetched, crs)\n        if not unfetched:\n            return\n\n        keys = [cr.iden for cr in unfetched]\n        cached = g.permacache.get_multi(\n            keys=keys,\n            allow_local=not force,\n            stale=stale,\n        )\n        for cr in unfetched:\n            cr.data = cached.get(cr.iden) or []\n            cr._fetched = True\n\n    def make_item_tuple(self, item):\n        \"\"\"Given a single 'item' from the result of a query build the tuple\n        that will be stored in the query cache. It is effectively the\n        fullname of the item after passing through the filter plus the\n        columns of the unfiltered item to sort by.\"\"\"\n        filtered_item = self.filter(item)\n        lst = [filtered_item._fullname]\n        for col in self.sort_cols:\n            #take the property of the original \n            attr = getattr(item, col)\n            #convert dates to epochs to take less space\n            if isinstance(attr, datetime):\n                attr = epoch_seconds(attr)\n            lst.append(attr)\n        return tuple(lst)\n\n    def can_insert(self):\n        \"\"\"True if a new item can just be inserted rather than\n           rerunning the query.\"\"\"\n         # This is only true in some circumstances: queries where\n         # eligibility in the list is determined only by its sort\n         # value (e.g. hot) and where addition/removal from the list\n         # incurs an insertion/deletion event called on the query. So\n         # the top hottest items in X some subreddit where the query\n         # is notified on every submission/banning/unbanning/deleting\n         # will work, but for queries with a time-component or some\n         # other eligibility factor, it cannot be inserted this way.\n        if self.query._sort in ([desc('_date')],\n                                [desc('_hot'), desc('_date')],\n                                [desc('_score'), desc('_date')],\n                                [desc('_controversy'), desc('_date')]):\n            if not any(r for r in self.query._rules\n                       if r.lval.name == '_date'):\n                # if no time-rule is specified, then it's 'all'\n                return True\n        return False\n\n    def can_delete(self):\n        \"True if a item can be removed from the listing, always true for now.\"\n        return True\n\n    def _mutate(self, fn, willread=True):\n        self.data = g.permacache.mutate(\n            key=self.iden,\n            mutation_fn=fn,\n            default=[],\n            willread=willread,\n        )\n        self._fetched=True\n\n    def insert(self, items):\n        \"\"\"Inserts the item into the cached data. This only works\n           under certain criteria, see can_insert.\"\"\"\n        self._insert_tuples([self.make_item_tuple(item) for item in tup(items)])\n\n    def _insert_tuples(self, tuples):\n        def _mutate(data):\n            data = data or []\n            item_tuples = tuples or []\n\n            existing_fnames = {item[0] for item in data}\n            new_fnames = {item[0] for item in item_tuples}\n\n            mutated_length = len(existing_fnames.union(new_fnames))\n            would_truncate = mutated_length >= precompute_limit\n            if would_truncate and data:\n                # only insert items that are already stored or new items\n                # that are large enough that they won't be immediately truncated\n                # out of storage\n                # item structure is (name, sortval1[, sortval2, ...])\n                smallest = data[-1]\n                item_tuples = [item for item in item_tuples\n                                    if (item[0] in existing_fnames or\n                                        item[1:] >= smallest[1:])]\n\n            if not item_tuples:\n                return data\n\n            # insert the items, remove the duplicates (keeping the\n            # one being inserted over the stored value if applicable),\n            # and sort the result\n            data = filter(lambda x: x[0] not in new_fnames, data)\n            data.extend(item_tuples)\n            data.sort(reverse=True, key=lambda x: x[1:])\n            if len(data) > precompute_limit:\n                data = data[:precompute_limit]\n            return data\n\n        self._mutate(_mutate)\n\n    def delete(self, items):\n        \"\"\"Deletes an item from the cached data.\"\"\"\n        fnames = set(self.filter(x)._fullname for x in tup(items))\n\n        def _mutate(data):\n            data = data or []\n            return filter(lambda x: x[0] not in fnames,\n                          data)\n\n        self._mutate(_mutate)\n\n    def _replace(self, tuples, lock=True):\n        \"\"\"Take pre-rendered tuples from mr_top and replace the\n           contents of the query outright. This should be considered a\n           private API\"\"\"\n        if lock:\n            def _mutate(data):\n                return tuples\n            self._mutate(_mutate, willread=False)\n        else:\n            self._fetched = True\n            self.data = tuples\n            g.permacache.pessimistically_set(self.iden, tuples)\n\n    def update(self):\n        \"\"\"Runs the query and stores the result in the cache. This is\n           only run by hand.\"\"\"\n        self.data = [self.make_item_tuple(i) for i in self.query]\n        self._fetched = True\n        g.permacache.set(self.iden, self.data)\n\n    def __repr__(self):\n        return '<CachedResults %s %s>' % (self.query._rules, self.query._sort)\n\n    def __iter__(self):\n        self.fetch()\n\n        for x in self.data:\n            yield x[0]\n\nclass MergedCachedResults(object):\n    \"\"\"Given two CachedResults, merges their lists based on the sorts\n       of their queries.\"\"\"\n    # normally we'd do this by having a superclass of CachedResults,\n    # but we have legacy pickled CachedResults that we don't want to\n    # break\n\n    def __init__(self, results):\n        self.cached_results = results\n        CachedResults.fetch_multi([r for r in results\n                                   if isinstance(r, CachedResults)])\n        CachedQuery._fetch_multi([r for r in results\n                                   if isinstance(r, CachedQuery)])\n        self._fetched = True\n\n        self.sort = results[0].sort\n        comparator = ThingTupleComparator(self.sort)\n        # make sure they're all the same\n        assert all(r.sort == self.sort for r in results[1:])\n\n        all_items = []\n        for cr in results:\n            all_items.extend(cr.data)\n        all_items.sort(cmp=comparator)\n        self.data = all_items\n\n\n    def __repr__(self):\n        return '<MergedCachedResults %r>' % (self.cached_results,)\n\n    def __iter__(self):\n        for x in self.data:\n            yield x[0]\n\n    def update(self):\n        for x in self.cached_results:\n            x.update()\n\ndef make_results(query, filter = filter_identity):\n    return CachedResults(query, filter)\n\ndef merge_results(*results):\n    if not results:\n        return []\n    return MergedCachedResults(results)\n\ndef migrating_cached_query(model, filter_fn=filter_identity):\n    \"\"\"Returns a CachedResults object that has a new-style cached query\n    attached as \"new_query\". This way, reads will happen from the old\n    query cache while writes can be made to go to both caches until a\n    backfill migration is complete.\"\"\"\n\n    decorator = cached_query(model, filter_fn)\n    def migrating_cached_query_decorator(fn):\n        wrapped = decorator(fn)\n        def migrating_cached_query_wrapper(*args):\n            new_query = wrapped(*args)\n            old_query = make_results(new_query.query, filter_fn)\n            old_query.new_query = new_query\n            return old_query\n        return migrating_cached_query_wrapper\n    return migrating_cached_query_decorator\n\n\n@cached_query(UserQueryCache)\ndef get_deleted_links(user_id):\n    return Link._query(Link.c.author_id == user_id,\n                       Link.c._deleted == True,\n                       Link.c._spam == (True, False),\n                       sort=db_sort('new'))\n\n\n@cached_query(UserQueryCache)\ndef get_deleted_comments(user_id):\n    return Comment._query(Comment.c.author_id == user_id,\n                          Comment.c._deleted == True,\n                          Comment.c._spam == (True, False),\n                          sort=db_sort('new'))\n\n\n@merged_cached_query\ndef get_deleted(user):\n    return [get_deleted_links(user),\n            get_deleted_comments(user)]\n\n\ndef get_links(sr, sort, time):\n    return _get_links(sr._id, sort, time)\n\ndef _get_links(sr_id, sort, time):\n    \"\"\"General link query for a subreddit.\"\"\"\n    q = Link._query(Link.c.sr_id == sr_id,\n                    sort = db_sort(sort),\n                    data = True)\n\n    if time != 'all':\n        q._filter(db_times[time])\n\n    res = make_results(q)\n\n    return res\n\n@cached_query(SubredditQueryCache)\ndef get_spam_links(sr_id):\n    return Link._query(Link.c.sr_id == sr_id,\n                       Link.c._spam == True,\n                       sort = db_sort('new'))\n\n@cached_query(SubredditQueryCache)\ndef get_spam_comments(sr_id):\n    return Comment._query(Comment.c.sr_id == sr_id,\n                          Comment.c._spam == True,\n                          sort = db_sort('new'))\n\n\n@cached_query(SubredditQueryCache)\ndef get_edited_comments(sr_id):\n    return FakeQuery(sort=[desc(\"editted\")])\n\n\n@cached_query(SubredditQueryCache)\ndef get_edited_links(sr_id):\n    return FakeQuery(sort=[desc(\"editted\")])\n\n\n@merged_cached_query\ndef get_edited(sr, user=None, include_links=True, include_comments=True):\n    sr_ids = moderated_srids(sr, user)\n    queries = []\n\n    if include_links:\n        queries.append(get_edited_links)\n    if include_comments:\n        queries.append(get_edited_comments)\n    return [query(sr_id) for sr_id, query in itertools.product(sr_ids, queries)]\n\n\ndef moderated_srids(sr, user):\n    if isinstance(sr, (ModContribSR, MultiReddit)):\n        srs = Subreddit._byID(sr.sr_ids, return_dict=False)\n        if user:\n            srs = [sr for sr in srs\n                   if sr.is_moderator_with_perms(user, 'posts')]\n        return [sr._id for sr in srs]\n    else:\n        return [sr._id]\n\n@merged_cached_query\ndef get_spam(sr, user=None, include_links=True, include_comments=True):\n    sr_ids = moderated_srids(sr, user)\n    queries = []\n\n    if include_links:\n        queries.append(get_spam_links)\n    if include_comments:\n        queries.append(get_spam_comments)\n    return [query(sr_id) for sr_id, query in itertools.product(sr_ids, queries)]\n\n@cached_query(SubredditQueryCache)\ndef get_spam_filtered_links(sr_id):\n    \"\"\" NOTE: This query will never run unless someone does an \"update\" on it,\n        but that will probably timeout. Use insert_spam_filtered_links.\"\"\"\n    return Link._query(Link.c.sr_id == sr_id,\n                       Link.c._spam == True,\n                       Link.c.verdict != 'mod-removed',\n                       sort = db_sort('new'))\n\n@cached_query(SubredditQueryCache)\ndef get_spam_filtered_comments(sr_id):\n    return Comment._query(Comment.c.sr_id == sr_id,\n                          Comment.c._spam == True,\n                          Comment.c.verdict != 'mod-removed',\n                          sort = db_sort('new'))\n\n@merged_cached_query\ndef get_spam_filtered(sr):\n    return [get_spam_filtered_links(sr),\n            get_spam_filtered_comments(sr)]\n\n@cached_query(SubredditQueryCache)\ndef get_reported_links(sr_id):\n    q = Link._query(Link.c.reported != 0,\n                    Link.c._spam == False,\n                    sort = db_sort('new'))\n    if sr_id is not None:\n        q._filter(Link.c.sr_id == sr_id)\n    return q\n\n@cached_query(SubredditQueryCache)\ndef get_reported_comments(sr_id):\n    q = Comment._query(Comment.c.reported != 0,\n                          Comment.c._spam == False,\n                          sort = db_sort('new'))\n\n    if sr_id is not None:\n        q._filter(Comment.c.sr_id == sr_id)\n    return q\n\n@merged_cached_query\ndef get_reported(sr, user=None, include_links=True, include_comments=True):\n    sr_ids = moderated_srids(sr, user)\n    queries = []\n\n    if include_links:\n        queries.append(get_reported_links)\n    if include_comments:\n        queries.append(get_reported_comments)\n    return [query(sr_id) for sr_id, query in itertools.product(sr_ids, queries)]\n\n@cached_query(SubredditQueryCache)\ndef get_unmoderated_links(sr_id):\n    q = Link._query(Link.c.sr_id == sr_id,\n                    Link.c._spam == (True, False),\n                    sort = db_sort('new'))\n\n    # Doesn't really work because will not return Links with no verdict\n    q._filter(or_(and_(Link.c._spam == True, Link.c.verdict != 'mod-removed'),\n                  and_(Link.c._spam == False, Link.c.verdict != 'mod-approved')))\n    return q\n\n@merged_cached_query\ndef get_modqueue(sr, user=None, include_links=True, include_comments=True):\n    sr_ids = moderated_srids(sr, user)\n    queries = []\n\n    if include_links:\n        queries.append(get_reported_links)\n        queries.append(get_spam_filtered_links)\n    if include_comments:\n        queries.append(get_reported_comments)\n        queries.append(get_spam_filtered_comments)\n    return [query(sr_id) for sr_id, query in itertools.product(sr_ids, queries)]\n\n@merged_cached_query\ndef get_unmoderated(sr, user=None):\n    sr_ids = moderated_srids(sr, user)\n    queries = [get_unmoderated_links]\n    return [query(sr_id) for sr_id, query in itertools.product(sr_ids, queries)]\n\ndef get_domain_links(domain, sort, time):\n    from r2.lib.db import operators\n    q = Link._query(operators.domain(Link.c.url) == filters._force_utf8(domain),\n                    sort = db_sort(sort),\n                    data = True)\n    if time != \"all\":\n        q._filter(db_times[time])\n\n    return make_results(q)\n\ndef user_query(kind, user_id, sort, time):\n    \"\"\"General profile-page query.\"\"\"\n    q = kind._query(kind.c.author_id == user_id,\n                    kind.c._spam == (True, False),\n                    sort = db_sort(sort))\n    if time != 'all':\n        q._filter(db_times[time])\n    return make_results(q)\n\ndef get_all_comments():\n    \"\"\"the master /comments page\"\"\"\n    q = Comment._query(sort = desc('_date'))\n    return make_results(q)\n\ndef get_sr_comments(sr):\n    return _get_sr_comments(sr._id)\n\ndef _get_sr_comments(sr_id):\n    \"\"\"the subreddit /r/foo/comments page\"\"\"\n    q = Comment._query(Comment.c.sr_id == sr_id,\n                       sort = desc('_date'))\n    return make_results(q)\n\ndef _get_comments(user_id, sort, time):\n    return user_query(Comment, user_id, sort, time)\n\ndef get_comments(user, sort, time):\n    return _get_comments(user._id, sort, time)\n\ndef _get_submitted(user_id, sort, time):\n    return user_query(Link, user_id, sort, time)\n\ndef get_submitted(user, sort, time):\n    return _get_submitted(user._id, sort, time)\n\n\ndef get_user_actions(user, sort, time):\n    results = []\n    unique_ids = set()\n\n    # Order is important as a listing will only have the action_type of the\n    # first occurrance (aka: posts trump comments which trump likes)\n    actions_by_type = ((get_submitted(user, sort, time), 'submit'),\n                       (get_comments(user, sort, time), 'comment'),\n                       (get_liked(user), 'like'))\n\n    for cached_result, action_type in actions_by_type:\n        cached_result.fetch()\n        for thing in cached_result.data:\n            if thing[0] not in unique_ids:\n                results.append(thing + (action_type,))\n                unique_ids.add(thing[0])\n\n    return sorted(results, key=lambda x: x[1], reverse=True)\n\n\ndef get_overview(user, sort, time):\n    return merge_results(get_comments(user, sort, time),\n                         get_submitted(user, sort, time))\n\ndef rel_query(rel, thing_id, name, filters = []):\n    \"\"\"General relationship query.\"\"\"\n\n    q = rel._query(rel.c._thing1_id == thing_id,\n                   rel.c._t2_deleted == False,\n                   rel.c._name == name,\n                   sort = desc('_date'),\n                   eager_load = True,\n                   )\n    if filters:\n        q._filter(*filters)\n\n    return q\n\ncached_userrel_query = cached_query(UserQueryCache, filter_thing2)\ncached_srrel_query = cached_query(SubredditQueryCache, filter_thing2)\n\n@cached_query(UserQueryCache, filter_thing)\ndef get_liked(user):\n    return FakeQuery(sort=[desc(\"date\")])\n\n@cached_query(UserQueryCache, filter_thing)\ndef get_disliked(user):\n    return FakeQuery(sort=[desc(\"date\")])\n\n@cached_query(UserQueryCache)\ndef get_hidden_links(user_id):\n    return FakeQuery(sort=[desc(\"action_date\")])\n\ndef get_hidden(user):\n    return get_hidden_links(user)\n\n@cached_query(UserQueryCache)\ndef get_categorized_saved_links(user_id, sr_id, category):\n    return FakeQuery(sort=[desc(\"action_date\")])\n\n@cached_query(UserQueryCache)\ndef get_categorized_saved_comments(user_id, sr_id, category):\n    return FakeQuery(sort=[desc(\"action_date\")])\n\n@cached_query(UserQueryCache)\ndef get_saved_links(user_id, sr_id):\n    return FakeQuery(sort=[desc(\"action_date\")])\n\n@cached_query(UserQueryCache)\ndef get_saved_comments(user_id, sr_id):\n    return FakeQuery(sort=[desc(\"action_date\")])\n\ndef get_saved(user, sr_id=None, category=None):\n    sr_id = sr_id or 'none'\n    if not category:\n        queries = [get_saved_links(user, sr_id),\n                   get_saved_comments(user, sr_id)]\n    else:\n        queries = [get_categorized_saved_links(user, sr_id, category),\n                   get_categorized_saved_comments(user, sr_id, category)]\n    return MergedCachedQuery(queries)\n\n@cached_srrel_query\ndef get_subreddit_messages(sr):\n    return rel_query(ModeratorInbox, sr, 'inbox')\n\n@cached_srrel_query\ndef get_unread_subreddit_messages(sr):\n    return rel_query(ModeratorInbox, sr, 'inbox',\n                          filters = [ModeratorInbox.c.new == True])\n\ndef get_unread_subreddit_messages_multi(srs):\n    if not srs:\n        return []\n    queries = [get_unread_subreddit_messages(sr) for sr in srs]\n    return MergedCachedQuery(queries)\n\ninbox_message_rel = Inbox.rel(Account, Message)\n@cached_userrel_query\ndef get_inbox_messages(user):\n    return rel_query(inbox_message_rel, user, 'inbox')\n\n@cached_userrel_query\ndef get_unread_messages(user):\n    return rel_query(inbox_message_rel, user, 'inbox',\n                          filters = [inbox_message_rel.c.new == True])\n\ninbox_comment_rel = Inbox.rel(Account, Comment)\n@cached_userrel_query\ndef get_inbox_comments(user):\n    return rel_query(inbox_comment_rel, user, 'inbox')\n\n@cached_userrel_query\ndef get_unread_comments(user):\n    return rel_query(inbox_comment_rel, user, 'inbox',\n                          filters = [inbox_comment_rel.c.new == True])\n\n@cached_userrel_query\ndef get_inbox_selfreply(user):\n    return rel_query(inbox_comment_rel, user, 'selfreply')\n\n@cached_userrel_query\ndef get_unread_selfreply(user):\n    return rel_query(inbox_comment_rel, user, 'selfreply',\n                          filters = [inbox_comment_rel.c.new == True])\n\n\n@cached_userrel_query\ndef get_inbox_comment_mentions(user):\n    return rel_query(inbox_comment_rel, user, \"mention\")\n\n\n@cached_userrel_query\ndef get_unread_comment_mentions(user):\n    return rel_query(inbox_comment_rel, user, \"mention\",\n                     filters=[inbox_comment_rel.c.new == True])\n\n\ndef get_inbox(user):\n    return merge_results(get_inbox_comments(user),\n                         get_inbox_messages(user),\n                         get_inbox_comment_mentions(user),\n                         get_inbox_selfreply(user))\n\n@cached_query(UserQueryCache)\ndef get_sent(user_id):\n    return Message._query(Message.c.author_id == user_id,\n                          Message.c._spam == (True, False),\n                          sort = desc('_date'))\n\ndef get_unread_inbox(user):\n    return merge_results(get_unread_comments(user),\n                         get_unread_messages(user),\n                         get_unread_comment_mentions(user),\n                         get_unread_selfreply(user))\n\ndef _user_reported_query(user_id, thing_cls):\n    rel_cls = Report.rel(Account, thing_cls)\n    return rel_query(rel_cls, user_id, ('-1', '0', '1'))\n    # -1: rejected report\n    # 0: unactioned report\n    # 1: accepted report\n\n@cached_userrel_query\ndef get_user_reported_links(user_id):\n    return _user_reported_query(user_id, Link)\n\n@cached_userrel_query\ndef get_user_reported_comments(user_id):\n    return _user_reported_query(user_id, Comment)\n\n@cached_userrel_query\ndef get_user_reported_messages(user_id):\n    return _user_reported_query(user_id, Message)\n\n@merged_cached_query\ndef get_user_reported(user_id):\n    return [get_user_reported_links(user_id),\n            get_user_reported_comments(user_id),\n            get_user_reported_messages(user_id)]\n\n\ndef set_promote_status(link, promote_status):\n    all_queries = [promote_query(link.author_id) for promote_query in \n                   (get_unpaid_links, get_unapproved_links, \n                    get_rejected_links, get_live_links, get_accepted_links,\n                    get_edited_live_links)]\n    all_queries.extend([get_all_unpaid_links(), get_all_unapproved_links(),\n                        get_all_rejected_links(), get_all_live_links(),\n                        get_all_accepted_links(), get_all_edited_live_links()])\n\n    if promote_status == PROMOTE_STATUS.unpaid:\n        inserts = [get_unpaid_links(link.author_id), get_all_unpaid_links()]\n    elif promote_status == PROMOTE_STATUS.unseen:\n        inserts = [get_unapproved_links(link.author_id),\n                   get_all_unapproved_links()]\n    elif promote_status == PROMOTE_STATUS.rejected:\n        inserts = [get_rejected_links(link.author_id), get_all_rejected_links()]\n    elif promote_status == PROMOTE_STATUS.promoted:\n        inserts = [get_live_links(link.author_id), get_all_live_links()]\n    elif promote_status == PROMOTE_STATUS.edited_live:\n        inserts = [\n            get_edited_live_links(link.author_id),\n            get_all_edited_live_links()\n        ]\n    elif promote_status in (PROMOTE_STATUS.accepted, PROMOTE_STATUS.pending,\n                            PROMOTE_STATUS.finished):\n        inserts = [get_accepted_links(link.author_id), get_all_accepted_links()]\n\n    deletes = list(set(all_queries) - set(inserts))\n    with CachedQueryMutator() as m:\n        for q in inserts:\n            m.insert(q, [link])\n        for q in deletes:\n            m.delete(q, [link])\n\n    link.promote_status = promote_status\n    link._commit()\n\n    text = \"set promote status to '%s'\" % PROMOTE_STATUS.name[promote_status]\n    PromotionLog.add(link, text)\n\n\ndef _promoted_link_query(user_id, status):\n    STATUS_CODES = {'unpaid': PROMOTE_STATUS.unpaid,\n                    'unapproved': PROMOTE_STATUS.unseen,\n                    'rejected': PROMOTE_STATUS.rejected,\n                    'live': PROMOTE_STATUS.promoted,\n                    'accepted': (PROMOTE_STATUS.accepted,\n                                 PROMOTE_STATUS.pending,\n                                 PROMOTE_STATUS.finished),\n                    'edited_live': PROMOTE_STATUS.edited_live}\n\n    q = Link._query(Link.c.sr_id == Subreddit.get_promote_srid(),\n                    Link.c._spam == (True, False),\n                    Link.c._deleted == (True, False),\n                    Link.c.promote_status == STATUS_CODES[status],\n                    sort=db_sort('new'))\n    if user_id:\n        q._filter(Link.c.author_id == user_id)\n    return q\n\n\n@cached_query(UserQueryCache)\ndef get_unpaid_links(user_id):\n    return _promoted_link_query(user_id, 'unpaid')\n\n\n@cached_query(UserQueryCache)\ndef get_all_unpaid_links():\n    return _promoted_link_query(None, 'unpaid')\n\n\n@cached_query(UserQueryCache)\ndef get_unapproved_links(user_id):\n    return _promoted_link_query(user_id, 'unapproved')\n\n\n@cached_query(UserQueryCache)\ndef get_all_unapproved_links():\n    return _promoted_link_query(None, 'unapproved')\n\n\n@cached_query(UserQueryCache)\ndef get_rejected_links(user_id):\n    return _promoted_link_query(user_id, 'rejected')\n\n\n@cached_query(UserQueryCache)\ndef get_all_rejected_links():\n    return _promoted_link_query(None, 'rejected')\n\n\n@cached_query(UserQueryCache)\ndef get_live_links(user_id):\n    return _promoted_link_query(user_id, 'live')\n\n\n@cached_query(UserQueryCache)\ndef get_all_live_links():\n    return _promoted_link_query(None, 'live')\n\n\n@cached_query(UserQueryCache)\ndef get_accepted_links(user_id):\n    return _promoted_link_query(user_id, 'accepted')\n\n\n@cached_query(UserQueryCache)\ndef get_all_accepted_links():\n    return _promoted_link_query(None, 'accepted')\n\n\n@cached_query(UserQueryCache)\ndef get_edited_live_links(user_id):\n    return _promoted_link_query(user_id, 'edited_live')\n\n\n@cached_query(UserQueryCache)\ndef get_all_edited_live_links():\n    return _promoted_link_query(None, 'edited_live')\n\n\n@cached_query(UserQueryCache)\ndef get_payment_flagged_links():\n    return FakeQuery(sort=[desc(\"_date\")])\n\n\ndef set_payment_flagged_link(link):\n    with CachedQueryMutator() as m:\n        q = get_payment_flagged_links()\n        m.insert(q, [link])\n\n\ndef unset_payment_flagged_link(link):\n    with CachedQueryMutator() as m:\n        q = get_payment_flagged_links()\n        m.delete(q, [link])\n\n\n@cached_query(UserQueryCache)\ndef get_underdelivered_campaigns():\n    return FakeQuery(sort=[desc(\"_date\")])\n\n\ndef set_underdelivered_campaigns(campaigns):\n    campaigns = tup(campaigns)\n    with CachedQueryMutator() as m:\n        q = get_underdelivered_campaigns()\n        m.insert(q, campaigns)\n\n\ndef unset_underdelivered_campaigns(campaigns):\n    campaigns = tup(campaigns)\n    with CachedQueryMutator() as m:\n        q = get_underdelivered_campaigns()\n        m.delete(q, campaigns)\n\n\n@merged_cached_query\ndef get_promoted_links(user_id):\n    queries = [get_unpaid_links(user_id), get_unapproved_links(user_id),\n               get_rejected_links(user_id), get_live_links(user_id),\n               get_accepted_links(user_id), get_edited_live_links(user_id)]\n    return queries\n\n\n@merged_cached_query\ndef get_all_promoted_links():\n    queries = [get_all_unpaid_links(), get_all_unapproved_links(),\n               get_all_rejected_links(), get_all_live_links(),\n               get_all_accepted_links(), get_all_edited_live_links()]\n    return queries\n\n\n@cached_query(SubredditQueryCache, filter_fn=filter_thing)\ndef get_all_gilded_comments():\n    return FakeQuery(sort=[desc(\"date\")])\n\n\n@cached_query(SubredditQueryCache, filter_fn=filter_thing)\ndef get_all_gilded_links():\n    return FakeQuery(sort=[desc(\"date\")])\n\n\n@merged_cached_query\ndef get_all_gilded():\n    return [get_all_gilded_comments(), get_all_gilded_links()]\n\n\n@cached_query(SubredditQueryCache, filter_fn=filter_thing)\ndef get_gilded_comments(sr_id):\n    return FakeQuery(sort=[desc(\"date\")])\n\n\n@cached_query(SubredditQueryCache, filter_fn=filter_thing)\ndef get_gilded_links(sr_id):\n    return FakeQuery(sort=[desc(\"date\")])\n\n\n@merged_cached_query\ndef get_gilded(sr_ids):\n    queries = [get_gilded_links, get_gilded_comments]\n    return [query(sr_id)\n            for sr_id, query in itertools.product(tup(sr_ids), queries)]\n\n\n@cached_query(UserQueryCache, filter_fn=filter_thing)\ndef get_gilded_user_comments(user_id):\n    return FakeQuery(sort=[desc(\"date\")])\n\n\n@cached_query(UserQueryCache, filter_fn=filter_thing)\ndef get_gilded_user_links(user_id):\n    return FakeQuery(sort=[desc(\"date\")])\n\n\n@merged_cached_query\ndef get_gilded_users(user_ids):\n    queries = [get_gilded_user_links, get_gilded_user_comments]\n    return [query(user_id)\n            for user_id, query in itertools.product(tup(user_ids), queries)]\n\n\n@cached_query(UserQueryCache, filter_fn=filter_thing)\ndef get_user_gildings(user_id):\n    return FakeQuery(sort=[desc(\"date\")])\n\n\n@merged_cached_query\ndef get_gilded_user(user):\n    return [get_gilded_user_comments(user), get_gilded_user_links(user)]\n\n\ndef add_queries(queries, insert_items=None, delete_items=None):\n    \"\"\"Adds multiple queries to the query queue. If insert_items or\n       delete_items is specified, the query may not need to be\n       recomputed against the database.\"\"\"\n\n    for q in queries:\n        if insert_items and q.can_insert():\n            g.log.debug(\"Inserting %s into query %s\" % (insert_items, q))\n            with g.stats.get_timer('permacache.foreground.insert'):\n                q.insert(insert_items)\n        elif delete_items and q.can_delete():\n            g.log.debug(\"Deleting %s from query %s\" % (delete_items, q))\n            with g.stats.get_timer('permacache.foreground.delete'):\n                q.delete(delete_items)\n        else:\n            raise Exception(\"Cannot update query %r!\" % (q,))\n\n    # dual-write any queries that are being migrated to the new query cache\n    with CachedQueryMutator() as m:\n        new_queries = [getattr(q, 'new_query') for q in queries if hasattr(q, 'new_query')]\n\n        if insert_items:\n            for query in new_queries:\n                m.insert(query, tup(insert_items))\n\n        if delete_items:\n            for query in new_queries:\n                m.delete(query, tup(delete_items))\n\n#can be rewritten to be more efficient\ndef all_queries(fn, obj, *param_lists):\n    \"\"\"Given a fn and a first argument 'obj', calls the fn(obj, *params)\n    for every permutation of the parameters in param_lists\"\"\"\n    results = []\n    params = [[obj]]\n    for pl in param_lists:\n        new_params = []\n        for p in pl:\n            for c in params:\n                new_param = list(c)\n                new_param.append(p)\n                new_params.append(new_param)\n        params = new_params\n\n    results = [fn(*p) for p in params]\n    return results\n\n## The following functions should be called after their respective\n## actions to update the correct listings.\ndef new_link(link):\n    \"Called on the submission and deletion of links\"\n    sr = Subreddit._byID(link.sr_id)\n    author = Account._byID(link.author_id)\n\n    # just update \"new\" here, new_vote will handle hot/top/controversial\n    results = [get_links(sr, 'new', 'all')]\n    results.append(get_submitted(author, 'new', 'all'))\n\n    for domain in utils.UrlParser(link.url).domain_permutations():\n        results.append(get_domain_links(domain, 'new', \"all\"))\n\n    with CachedQueryMutator() as m:\n        if link._spam:\n            m.insert(get_spam_links(sr), [link])\n        if not (sr.exclude_banned_modqueue and author._spam):\n            m.insert(get_unmoderated_links(sr), [link])\n\n    add_queries(results, insert_items = link)\n    amqp.add_item('new_link', link._fullname)\n\n\ndef add_to_commentstree_q(comment):\n    if utils.to36(comment.link_id) in g.live_config[\"fastlane_links\"]:\n        amqp.add_item('commentstree_fastlane_q', comment._fullname)\n    elif g.shard_commentstree_queues:\n        amqp.add_item('commentstree_%d_q' % (comment.link_id % 10),\n                      comment._fullname)\n    else:\n        amqp.add_item('commentstree_q', comment._fullname)\n\n\ndef update_comment_notifications(comment, inbox_rels):\n    is_visible = not comment._deleted and not comment._spam\n\n    with CachedQueryMutator() as mutator:\n        for inbox_rel in tup(inbox_rels):\n            inbox_owner = inbox_rel._thing1\n            unread = (is_visible and\n                getattr(inbox_rel, 'unread_preremoval', True))\n\n            if inbox_rel._name == \"inbox\":\n                query = get_inbox_comments(inbox_owner)\n            elif inbox_rel._name == \"selfreply\":\n                query = get_inbox_selfreply(inbox_owner)\n            else:\n                raise ValueError(\"wtf is \" + inbox_rel._name)\n\n            # mentions happen in butler_q\n\n            if is_visible:\n                mutator.insert(query, [inbox_rel])\n            else:\n                mutator.delete(query, [inbox_rel])\n\n            set_unread(comment, inbox_owner, unread=unread, mutator=mutator)\n\n\ndef new_comment(comment, inbox_rels):\n    author = Account._byID(comment.author_id)\n\n    # just update \"new\" here, new_vote will handle hot/top/controversial\n    job = [get_comments(author, 'new', 'all')]\n\n    sr = Subreddit._byID(comment.sr_id)\n\n    if comment._deleted:\n        job_key = \"delete_items\"\n        job.append(get_sr_comments(sr))\n        job.append(get_all_comments())\n    else:\n        job_key = \"insert_items\"\n        if comment._spam:\n            with CachedQueryMutator() as m:\n                m.insert(get_spam_comments(sr), [comment])\n                if (was_spam_filtered(comment) and\n                        not (sr.exclude_banned_modqueue and author._spam)):\n                    m.insert(get_spam_filtered_comments(sr), [comment])\n\n        amqp.add_item('new_comment', comment._fullname)\n        add_to_commentstree_q(comment)\n\n    job_dict = { job_key: comment }\n    add_queries(job, **job_dict)\n\n    # note that get_all_comments() is updated by the amqp process\n    # r2.lib.db.queries.run_new_comments (to minimise lock contention)\n\n    if inbox_rels:\n        update_comment_notifications(comment, inbox_rels)\n\n\ndef new_subreddit(sr):\n    \"no precomputed queries here yet\"\n    amqp.add_item('new_subreddit', sr._fullname)\n\n\ndef new_message(message, inbox_rels, add_to_sent=True, update_modmail=True):\n    from r2.lib.comment_tree import add_message\n\n    from_user = Account._byID(message.author_id)\n\n    # check if the from_user is exempt from ever adding to sent\n    if not from_user.update_sent_messages:\n        add_to_sent = False\n\n    if message.display_author:\n        add_to_sent = False\n\n    modmail_rel_included = False\n    update_recipient = False\n    add_to_user = None\n\n    with CachedQueryMutator() as m:\n        if add_to_sent:\n            m.insert(get_sent(from_user), [message])\n\n        for inbox_rel in tup(inbox_rels):\n            to = inbox_rel._thing1\n\n            if isinstance(inbox_rel, ModeratorInbox):\n                m.insert(get_subreddit_messages(to), [inbox_rel])\n                modmail_rel_included = True\n                set_sr_unread(message, to, unread=True, mutator=m)\n            else:\n                m.insert(get_inbox_messages(to), [inbox_rel])\n                update_recipient = True\n                # make sure we add this message to the user's inbox\n                add_to_user = to\n                set_unread(message, to, unread=True, mutator=m)\n\n    update_modmail = update_modmail and modmail_rel_included\n\n    amqp.add_item('new_message', message._fullname)\n    add_message(message, update_recipient=update_recipient,\n                update_modmail=update_modmail, add_to_user=add_to_user)\n    \n    # light up the modmail icon for all other mods with mail access\n    if update_modmail:\n        mod_perms = message.subreddit_slow.moderators_with_perms()\n        mod_ids = [mod_id for mod_id, perms in mod_perms.iteritems()\n            if mod_id != from_user._id and perms.get('mail', False)]\n        moderators = Account._byID(mod_ids, data=True, return_dict=False)\n        for mod in moderators:\n            if not mod.modmsgtime:\n                mod.modmsgtime = message._date\n                mod._commit()\n\n\ndef set_unread(messages, user, unread, mutator=None):\n    messages = tup(messages)\n\n    inbox_rels = Inbox.get_rels(user, messages)\n    Inbox.set_unread(inbox_rels, unread)\n\n    update_unread_queries(inbox_rels, insert=unread, mutator=mutator)\n\n\ndef update_unread_queries(inbox_rels, insert=True, mutator=None):\n    \"\"\"Update all the cached queries related to the inbox relations\"\"\"\n    if not mutator:\n        m = CachedQueryMutator()\n    else:\n        m = mutator\n\n    inbox_rels = tup(inbox_rels)\n    for inbox_rel in inbox_rels:\n        thing = inbox_rel._thing2\n        user = inbox_rel._thing1\n\n        if isinstance(thing, Comment):\n            if inbox_rel._name == \"inbox\":\n                query = get_unread_comments(user._id)\n            elif inbox_rel._name == \"selfreply\":\n                query = get_unread_selfreply(user._id)\n            elif inbox_rel._name == \"mention\":\n                query = get_unread_comment_mentions(user._id)\n        elif isinstance(thing, Message):\n            query = get_unread_messages(user._id)\n        else:\n            raise ValueError(\"can't handle %s\" % thing.__class__.__name__)\n\n        if insert:\n            m.insert(query, [inbox_rel])\n        else:\n            m.delete(query, [inbox_rel])\n\n    if not mutator:\n        m.send()\n\n\ndef set_sr_unread(messages, sr, unread, mutator=None):\n    messages = tup(messages)\n\n    inbox_rels = ModeratorInbox.get_rels(sr, messages)\n    ModeratorInbox.set_unread(inbox_rels, unread)\n\n    update_unread_sr_queries(inbox_rels, insert=unread, mutator=mutator)\n\n\ndef update_unread_sr_queries(inbox_rels, insert=True, mutator=None):\n    if not mutator:\n        m = CachedQueryMutator()\n    else:\n        m = mutator\n\n    inbox_rels = tup(inbox_rels)\n    for inbox_rel in inbox_rels:\n        sr = inbox_rel._thing1\n        query = get_unread_subreddit_messages(sr)\n\n        if insert:\n            m.insert(query, [inbox_rel])\n        else:\n            m.delete(query, [inbox_rel])\n\n    if not mutator:\n        m.send()\n\n\ndef unread_handler(things, user, unread):\n    \"\"\"Given a user and Things of varying types, set their unread state.\"\"\"\n    sr_messages = collections.defaultdict(list)\n    comments = []\n    messages = []\n    # Group things by subreddit or type\n    for thing in things:\n        if isinstance(thing, Message):\n            if getattr(thing, 'sr_id', False):\n                sr_messages[thing.sr_id].append(thing)\n            else:\n                messages.append(thing)\n        else:\n            comments.append(thing)\n\n    if sr_messages:\n        mod_srs = Subreddit.reverse_moderator_ids(user)\n        srs = Subreddit._byID(sr_messages.keys())\n    else:\n        mod_srs = []\n\n    with CachedQueryMutator() as m:\n        for sr_id, things in sr_messages.items():\n            # Remove the item(s) from the user's inbox\n            set_unread(things, user, unread, mutator=m)\n\n            if sr_id in mod_srs:\n                # Only moderators can change the read status of that\n                # message in the modmail inbox\n                sr = srs[sr_id]\n                set_sr_unread(things, sr, unread, mutator=m)\n\n        if comments:\n            set_unread(comments, user, unread, mutator=m)\n\n        if messages:\n            set_unread(messages, user, unread, mutator=m)\n\n\ndef unnotify(thing, possible_recipients=None):\n    \"\"\"Given a Thing, remove any notifications to its possible recipients.\n\n    `possible_recipients` is a list of account IDs to unnotify. If not passed,\n    deduce all possible recipients and remove their notifications.\n    \"\"\"\n    from r2.lib import butler\n    error_message = (\"Unable to unnotify thing of type: %r\" % thing)\n    notification_handler(thing,\n        notify_function=butler.remove_mention_notification,\n        error_message=error_message,\n        possible_recipients=possible_recipients,\n    )\n\n\ndef renotify(thing, possible_recipients=None):\n    \"\"\"Given a Thing, reactivate notifications for possible recipients.\n\n    `possible_recipients` is a list of account IDs to renotify. If not passed,\n    deduce all possible recipients and add their notifications.\n    This is used when unspamming comments.\n    \"\"\"\n    from r2.lib import butler\n    error_message = (\"Unable to renotify thing of type: %r\" % thing)\n    notification_handler(thing,\n        notify_function=butler.readd_mention_notification,\n        error_message=error_message,\n        possible_recipients=possible_recipients,\n    )\n\n\ndef notification_handler(thing, notify_function,\n        error_message, possible_recipients=None):\n    if not possible_recipients:\n        possible_recipients = Inbox.possible_recipients(thing)\n\n    if not possible_recipients:\n        return\n\n    accounts = Account._byID(\n        possible_recipients,\n        return_dict=False,\n        ignore_missing=True,\n    )\n\n    if isinstance(thing, Comment):\n        rels = Inbox._fast_query(\n            accounts,\n            thing,\n            (\"inbox\", \"selfreply\", \"mention\"),\n        )\n\n        # if the comment has been spammed, remember the previous\n        # new value in case it becomes unspammed\n        if thing._spam:\n            for (tupl, rel) in rels.iteritems():\n                if rel:\n                    rel.unread_preremoval = rel.new\n                    rel._commit()\n\n        replies, mentions = utils.partition(\n            lambda r: r._name == \"mention\",\n            filter(None, rels.values()),\n        )\n\n        for mention in mentions:\n            notify_function(mention)\n\n        replies = list(replies)\n        if replies:\n            update_comment_notifications(thing, replies)\n    else:\n        raise ValueError(error_message)\n\n\ndef _by_srid(things, srs=True):\n    \"\"\"Takes a list of things and returns them in a dict separated by\n       sr_id, in addition to the looked-up subreddits\"\"\"\n    ret = {}\n\n    for thing in tup(things):\n        if getattr(thing, 'sr_id', None) is not None:\n            ret.setdefault(thing.sr_id, []).append(thing)\n\n    if srs:\n        _srs = Subreddit._byID(ret.keys(), return_dict=True) if ret else {}\n        return ret, _srs\n    else:\n        return ret\n\n\ndef _by_author(things, authors=True):\n    ret = collections.defaultdict(list)\n\n    for thing in tup(things):\n        author_id = getattr(thing, 'author_id')\n        if author_id:\n            ret[author_id].append(thing)\n\n    if authors:\n        _authors = Account._byID(ret.keys(), return_dict=True) if ret else {}\n        return ret, _authors\n    else:\n        return ret\n\ndef _by_thing1_id(rels):\n    ret = {}\n    for rel in tup(rels):\n        ret.setdefault(rel._thing1_id, []).append(rel)\n    return ret\n\n\ndef was_spam_filtered(thing):\n    if (thing._spam and not thing._deleted and\n        getattr(thing, 'verdict', None) != 'mod-removed'):\n        return True\n    else:\n        return False\n\n\ndef delete(things):\n    query_cache_inserts, query_cache_deletes = _common_del_ban(things)\n    by_srid, srs = _by_srid(things)\n    by_author, authors = _by_author(things)\n\n    for sr_id, sr_things in by_srid.iteritems():\n        sr = srs[sr_id]\n        links = [x for x in sr_things if isinstance(x, Link)]\n        comments = [x for x in sr_things if isinstance(x, Comment)]\n\n        if links:\n            query_cache_deletes.append((get_spam_links(sr), links))\n            query_cache_deletes.append((get_spam_filtered_links(sr), links))\n            query_cache_deletes.append((get_unmoderated_links(sr_id),\n                                            links))\n            query_cache_deletes.append((get_edited_links(sr_id), links))\n        if comments:\n            query_cache_deletes.append((get_spam_comments(sr), comments))\n            query_cache_deletes.append((get_spam_filtered_comments(sr),\n                                        comments))\n            query_cache_deletes.append((get_edited_comments(sr), comments))\n\n    for author_id, a_things in by_author.iteritems():\n        author = authors[author_id]\n        links = [x for x in a_things if isinstance(x, Link)]\n        comments = [x for x in a_things if isinstance(x, Comment)]\n\n        if links:\n            results = [get_submitted(author, 'hot', 'all'),\n                       get_submitted(author, 'new', 'all')]\n            for sort in time_filtered_sorts:\n                for time in db_times.keys():\n                    results.append(get_submitted(author, sort, time))\n            add_queries(results, delete_items=links)\n            query_cache_inserts.append((get_deleted_links(author_id), links))\n        if comments:\n            results = [get_comments(author, 'hot', 'all'),\n                       get_comments(author, 'new', 'all')]\n            for sort in time_filtered_sorts:\n                for time in db_times.keys():\n                    results.append(get_comments(author, sort, time))\n            add_queries(results, delete_items=comments)\n            query_cache_inserts.append((get_deleted_comments(author_id),\n                                        comments))\n\n    with CachedQueryMutator() as m:\n        for q, inserts in query_cache_inserts:\n            m.insert(q, inserts)\n        for q, deletes in query_cache_deletes:\n            m.delete(q, deletes)\n\n    for thing in tup(things):\n        thing.update_search_index()\n\n\ndef edit(thing):\n    if isinstance(thing, Link):\n        query = get_edited_links\n    elif isinstance(thing, Comment):\n        query = get_edited_comments\n\n    with CachedQueryMutator() as m:\n        m.delete(query(thing.sr_id), [thing])\n        m.insert(query(thing.sr_id), [thing])\n\n\ndef ban(things, filtered=True):\n    query_cache_inserts, query_cache_deletes = _common_del_ban(things)\n    by_srid = _by_srid(things, srs=False)\n\n    for sr_id, sr_things in by_srid.iteritems():\n        links = []\n        modqueue_links = []\n        comments = []\n        modqueue_comments = []\n        for item in sr_things:\n            # don't add posts by banned users if subreddit prefs exclude them\n            add_to_modqueue = (filtered and\n                       not (item.subreddit_slow.exclude_banned_modqueue and\n                            item.author_slow._spam))\n\n            if isinstance(item, Link):\n                links.append(item)\n                if add_to_modqueue:\n                    modqueue_links.append(item)\n            elif isinstance(item, Comment):\n                comments.append(item)\n                if add_to_modqueue:\n                    modqueue_comments.append(item)\n\n        if links:\n            query_cache_inserts.append((get_spam_links(sr_id), links))\n            if not filtered:\n                query_cache_deletes.append(\n                        (get_spam_filtered_links(sr_id), links))\n                query_cache_deletes.append(\n                        (get_unmoderated_links(sr_id), links))\n\n        if modqueue_links:\n            query_cache_inserts.append(\n                    (get_spam_filtered_links(sr_id), modqueue_links))\n\n        if comments:\n            query_cache_inserts.append((get_spam_comments(sr_id), comments))\n            if not filtered:\n                query_cache_deletes.append(\n                        (get_spam_filtered_comments(sr_id), comments))\n\n        if modqueue_comments:\n            query_cache_inserts.append(\n                    (get_spam_filtered_comments(sr_id), modqueue_comments))\n\n    with CachedQueryMutator() as m:\n        for q, inserts in query_cache_inserts:\n            m.insert(q, inserts)\n        for q, deletes in query_cache_deletes:\n            m.delete(q, deletes)\n\n    for thing in tup(things):\n        thing.update_search_index()\n\n\ndef _common_del_ban(things):\n    query_cache_inserts = []\n    query_cache_deletes = []\n    by_srid, srs = _by_srid(things)\n\n    for sr_id, sr_things in by_srid.iteritems():\n        sr = srs[sr_id]\n        links = [x for x in sr_things if isinstance(x, Link)]\n        comments = [x for x in sr_things if isinstance(x, Comment)]\n\n        if links:\n            results = [get_links(sr, 'hot', 'all'), get_links(sr, 'new', 'all')]\n            for sort in time_filtered_sorts:\n                for time in db_times.keys():\n                    results.append(get_links(sr, sort, time))\n            add_queries(results, delete_items=links)\n            query_cache_deletes.append([get_reported_links(sr), links])\n            query_cache_deletes.append([get_reported_links(None), links])\n        if comments:\n            query_cache_deletes.append([get_reported_comments(sr), comments])\n            query_cache_deletes.append([get_reported_comments(None), comments])\n\n    return query_cache_inserts, query_cache_deletes\n\n\ndef unban(things, insert=True):\n    query_cache_deletes = []\n\n    by_srid, srs = _by_srid(things)\n    if not by_srid:\n        return\n\n    for sr_id, things in by_srid.iteritems():\n        sr = srs[sr_id]\n        links = [x for x in things if isinstance(x, Link)]\n        comments = [x for x in things if isinstance(x, Comment)]\n\n        if insert and links:\n            # put it back in the listings\n            results = [get_links(sr, 'hot', 'all'),\n                       get_links(sr, 'top', 'all'),\n                       get_links(sr, 'controversial', 'all'),\n                       ]\n            # the time-filtered listings will have to wait for the\n            # next mr_top run\n            add_queries(results, insert_items=links)\n\n            # Check if link is being unbanned and should be put in\n            # 'new' with current time\n            new_links = []\n            for l in links:\n                ban_info = l.ban_info\n                if ban_info.get('reset_used', True) == False and \\\n                    ban_info.get('auto', False):\n                    l_copy = deepcopy(l)\n                    l_copy._date = ban_info['unbanned_at']\n                    new_links.append(l_copy)\n                else:\n                    new_links.append(l)\n            add_queries([get_links(sr, 'new', 'all')], insert_items=new_links)\n            query_cache_deletes.append([get_spam_links(sr), links])\n\n        if insert and comments:\n            add_queries([get_all_comments(), get_sr_comments(sr)],\n                        insert_items=comments)\n            query_cache_deletes.append([get_spam_comments(sr), comments])\n\n        if links:\n            query_cache_deletes.append((get_unmoderated_links(sr), links))\n            query_cache_deletes.append([get_spam_filtered_links(sr), links])\n\n        if comments:\n            query_cache_deletes.append([get_spam_filtered_comments(sr), comments])\n\n    with CachedQueryMutator() as m:\n        for q, deletes in query_cache_deletes:\n            m.delete(q, deletes)\n\n    for thing in tup(things):\n        thing.update_search_index()\n\ndef new_report(thing, report_rel):\n    reporter_id = report_rel._thing1_id\n\n    # determine if the report is for spam so we can update the global\n    # report queue as well as the per-subreddit one\n    reason = getattr(report_rel, \"reason\", None)\n    is_spam_report = reason and \"spam\" in reason.lower()\n\n    with CachedQueryMutator() as m:\n        if isinstance(thing, Link):\n            m.insert(get_reported_links(thing.sr_id), [thing])\n            if is_spam_report:\n                m.insert(get_reported_links(None), [thing])\n            m.insert(get_user_reported_links(reporter_id), [report_rel])\n        elif isinstance(thing, Comment):\n            m.insert(get_reported_comments(thing.sr_id), [thing])\n            if is_spam_report:\n                m.insert(get_reported_comments(None), [thing])\n            m.insert(get_user_reported_comments(reporter_id), [report_rel])\n        elif isinstance(thing, Message):\n            m.insert(get_user_reported_messages(reporter_id), [report_rel])\n\n    amqp.add_item(\"new_report\", thing._fullname)\n\n\ndef clear_reports(things, rels):\n    query_cache_deletes = []\n\n    by_srid = _by_srid(things, srs=False)\n\n    for sr_id, sr_things in by_srid.iteritems():\n        links = [ x for x in sr_things if isinstance(x, Link) ]\n        comments = [ x for x in sr_things if isinstance(x, Comment) ]\n\n        if links:\n            query_cache_deletes.append([get_reported_links(sr_id), links])\n            query_cache_deletes.append([get_reported_links(None), links])\n        if comments:\n            query_cache_deletes.append([get_reported_comments(sr_id), comments])\n            query_cache_deletes.append([get_reported_comments(None), comments])\n\n    # delete from user_reported if the report was correct\n    rels = [r for r in rels if r._name == '1']\n    if rels:\n        link_rels = [r for r in rels if r._type2 == Link]\n        comment_rels = [r for r in rels if r._type2 == Comment]\n        message_rels = [r for r in rels if r._type2 == Message]\n\n        rels_to_query = ((link_rels, get_user_reported_links),\n                         (comment_rels, get_user_reported_comments),\n                         (message_rels, get_user_reported_messages))\n\n        for thing_rels, query in rels_to_query:\n            if not thing_rels:\n                continue\n\n            by_thing1_id = _by_thing1_id(thing_rels)\n            for reporter_id, reporter_rels in by_thing1_id.iteritems():\n                query_cache_deletes.append([query(reporter_id), reporter_rels])\n\n    with CachedQueryMutator() as m:\n        for q, deletes in query_cache_deletes:\n            m.delete(q, deletes)\n\n\ndef add_all_srs():\n    \"\"\"Recalculates every listing query for every subreddit. Very,\n       very slow.\"\"\"\n    q = Subreddit._query(sort = asc('_date'))\n    for sr in fetch_things2(q):\n        for q in all_queries(get_links, sr, ('hot', 'new'), ['all']):\n            q.update()\n        for q in all_queries(get_links, sr, time_filtered_sorts, db_times.keys()):\n            q.update()\n        get_spam_links(sr).update()\n        get_spam_comments(sr).update()\n        get_reported_links(sr).update()\n        get_reported_comments(sr).update()\n\ndef update_user(user):\n    if isinstance(user, str):\n        user = Account._by_name(user)\n    elif isinstance(user, int):\n        user = Account._byID(user)\n\n    results = [get_inbox_messages(user),\n               get_inbox_comments(user),\n               get_inbox_selfreply(user),\n               get_sent(user),\n               get_liked(user),\n               get_disliked(user),\n               get_submitted(user, 'new', 'all'),\n               get_comments(user, 'new', 'all')]\n    for q in results:\n        q.update()\n\ndef add_all_users():\n    q = Account._query(sort = asc('_date'))\n    for user in fetch_things2(q):\n        update_user(user)\n\n# amqp queue processing functions\n\ndef run_new_comments(limit=1000):\n    \"\"\"Add new incoming comments to the /comments page\"\"\"\n    # this is done as a queue because otherwise the contention for the\n    # lock on the query would be very high\n\n    @g.stats.amqp_processor('newcomments_q')\n    def _run_new_comments(msgs, chan):\n        fnames = [msg.body for msg in msgs]\n\n        comments = Comment._by_fullname(fnames, data=True, return_dict=False)\n        add_queries([get_all_comments()],\n                    insert_items=comments)\n\n        bysrid = _by_srid(comments, False)\n        for srid, sr_comments in bysrid.iteritems():\n            add_queries([_get_sr_comments(srid)],\n                        insert_items=sr_comments)\n\n    amqp.handle_items('newcomments_q', _run_new_comments, limit=limit)\n\ndef run_commentstree(qname=\"commentstree_q\", limit=400):\n    \"\"\"Add new incoming comments to their respective comments trees\"\"\"\n\n    @g.stats.amqp_processor(qname)\n    def _run_commentstree(msgs, chan):\n        comments = Comment._by_fullname([msg.body for msg in msgs],\n                                        data = True, return_dict = False)\n        print 'Processing %r' % (comments,)\n\n        if comments:\n            add_comments(comments)\n\n    # High velocity threads put additional pressure on Cassandra.\n    if qname == \"commentstree_fastlane_q\":\n        limit = max(1000, limit)\n    amqp.handle_items(qname, _run_commentstree, limit=limit)\n\n\ndef _by_type(items):\n    by_type = collections.defaultdict(list)\n    for item in items:\n        by_type[item.__class__].append(item)\n    return by_type\n\n\ndef get_stored_votes(user, things):\n    if not user or not things:\n        return {}\n\n    results = {}\n    things_by_type = _by_type(things)\n\n    for thing_class, items in things_by_type.iteritems():\n        if not thing_class.is_votable:\n            continue\n\n        rel_class = VotesByAccount.rel(thing_class)\n        votes = rel_class.fast_query(user, items)\n        for cross, direction in votes.iteritems():\n            results[cross] = Vote.deserialize_direction(int(direction))\n\n    return results\n\n\ndef get_likes(user, requested_items):\n    if not user or not requested_items:\n        return {}\n\n    res = {}\n\n    try:\n        last_modified = LastModified._byID(user._fullname)\n    except tdb_cassandra.NotFound:\n        last_modified = None\n\n    items_in_grace_period = {}\n    items_by_type = _by_type(requested_items)\n    for type_, items in items_by_type.iteritems():\n        if not type_.is_votable:\n            # these items can't be voted on. just mark 'em as None and skip.\n            for item in items:\n                res[(user, item)] = None\n            continue\n\n        rel_cls = VotesByAccount.rel(type_)\n        last_vote = getattr(last_modified, rel_cls._last_modified_name, None)\n        if last_vote:\n            time_since_last_vote = datetime.now(pytz.UTC) - last_vote\n\n        # only do prequeued_vote lookups if we've voted within the grace period\n        # and therefore might have votes in flight in the queues.\n        if last_vote and time_since_last_vote < g.vote_queue_grace_period:\n            too_new = 0\n\n            for item in items:\n                if item._age > time_since_last_vote:\n                    key = prequeued_vote_key(user, item)\n                    items_in_grace_period[key] = (user, item)\n                else:\n                    # the item is newer than our last vote, we can't have\n                    # possibly voted on it.\n                    res[(user, item)] = None\n                    too_new += 1\n\n            if too_new:\n                g.stats.simple_event(\"vote.prequeued.too-new\", delta=too_new)\n        else:\n            g.stats.simple_event(\"vote.prequeued.graceless\", delta=len(items))\n\n    # look up votes in memcache for items that could have been voted on\n    # but not processed by a queue processor yet.\n    if items_in_grace_period:\n        g.stats.simple_event(\n            \"vote.prequeued.fetch\", delta=len(items_in_grace_period))\n        r = g.gencache.get_multi(items_in_grace_period.keys())\n        for key, v in r.iteritems():\n            res[items_in_grace_period[key]] = Vote.deserialize_direction(v)\n\n    cassavotes = get_stored_votes(\n        user, [i for i in requested_items if (user, i) not in res])\n    res.update(cassavotes)\n\n    return res\n\n\ndef consume_mark_all_read():\n    @g.stats.amqp_processor('markread_q')\n    def process_mark_all_read(msg):\n        user = Account._by_fullname(msg.body)\n        inbox_fullnames = get_unread_inbox(user)\n        for inbox_chunk in in_chunks(inbox_fullnames, size=100):\n            things = Thing._by_fullname(inbox_chunk, return_dict=False)\n            unread_handler(things, user, unread=False)\n\n    amqp.consume_items('markread_q', process_mark_all_read)\n\n\ndef consume_deleted_accounts():\n    @g.stats.amqp_processor('del_account_q')\n    def process_deleted_accounts(msg):\n        account = Thing._by_fullname(msg.body)\n        assert isinstance(account, Account)\n\n        if account.has_stripe_subscription:\n            from r2.controllers.ipn import cancel_stripe_subscription\n            cancel_stripe_subscription(account.gold_subscr_id)\n\n        # Mark their link submissions for updating on cloudsearch\n        query = LinksByAccount._cf.xget(account._id36)\n        for link_id36, unused in query:\n            fullname = Link._fullname_from_id36(link_id36)\n            msg = pickle.dumps({\"fullname\": fullname})\n            amqp.add_item(\"search_changes\", msg, message_id=fullname,\n                delivery_mode=amqp.DELIVERY_TRANSIENT)\n\n    amqp.consume_items('del_account_q', process_deleted_accounts)\n"
  },
  {
    "path": "r2/r2/lib/db/sorts.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom r2.lib.db._sorts import epoch_seconds, score, hot, _hot\nfrom r2.lib.db._sorts import controversy, confidence, qa\n"
  },
  {
    "path": "r2/r2/lib/db/tdb_cassandra.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport json\nimport inspect\nimport pytz\nfrom datetime import datetime\n\nfrom pylons import app_globals as g\n\nfrom pycassa import ColumnFamily\nfrom pycassa.pool import MaximumRetryException\nfrom pycassa.cassandra.ttypes import ConsistencyLevel, NotFoundException\nfrom pycassa.system_manager import (\n    ASCII_TYPE,\n    COUNTER_COLUMN_TYPE,\n    DATE_TYPE,\n    INT_TYPE,\n    SystemManager,\n    TIME_UUID_TYPE,\n    UTF8_TYPE,\n)\nfrom pycassa.types import DateType\nfrom pycassa.util import convert_uuid_to_time\nfrom r2.lib.utils import tup, Storage\nfrom r2.lib.sgm import sgm\nfrom uuid import uuid1, UUID\nfrom itertools import chain\nimport cPickle as pickle\nfrom pycassa.util import OrderedDict\nimport base64\n\nconnection_pools = g.cassandra_pools\ndefault_connection_pool = g.cassandra_default_pool\n\nkeyspace = 'reddit'\ndisallow_db_writes = g.disallow_db_writes\ntz = g.tz\nlog = g.log\nread_consistency_level = g.cassandra_rcl\nwrite_consistency_level = g.cassandra_wcl\ndebug = g.debug\nmake_lock = g.make_lock\ndb_create_tables = g.db_create_tables\n\nthing_types = {}\n\nTRANSIENT_EXCEPTIONS = (MaximumRetryException,)\n\n# The available consistency levels\nCL = Storage(ANY    = ConsistencyLevel.ANY,\n             ONE    = ConsistencyLevel.ONE,\n             QUORUM = ConsistencyLevel.QUORUM,\n             ALL    = ConsistencyLevel.ALL)\n\n# the greatest number of columns that we're willing to accept over the\n# wire for a given row (this should be increased if we start working\n# with classes with lots of columns, like Account which has lots of\n# karma_ rows, or we should not do that)\nmax_column_count = 10000\n\n# the pycassa date serializer, for use when we can't set the right metadata\n# to get pycassa to serialize dates for us\ndate_serializer = DateType()\n\nclass CassandraException(Exception):\n    \"\"\"Base class for Exceptions in tdb_cassandra\"\"\"\n    pass\n\nclass InvariantException(CassandraException):\n    \"\"\"Exceptions that can only be caused by bugs in tdb_cassandra\"\"\"\n    pass\n\nclass ConfigurationException(CassandraException):\n    \"\"\"Exceptions that are caused by incorrect configuration on the\n       Cassandra server\"\"\"\n    pass\n\nclass TdbException(CassandraException):\n    \"\"\"Exceptions caused by bugs in our callers or subclasses\"\"\"\n    pass\n\nclass NotFound(CassandraException, NotFoundException):\n    \"\"\"Someone asked us for an ID that isn't stored in the DB at\n       all. This is probably an end-user's fault.\"\"\"\n    pass\n\ndef will_write(fn):\n    \"\"\"Decorator to indicate that a given function intends to write\n       out to Cassandra\"\"\"\n    def _fn(*a, **kw):\n        if disallow_db_writes:\n            raise CassandraException(\"Not so fast! DB writes have been disabled\")\n        return fn(*a, **kw)\n    return _fn\n\ndef get_manager(seeds):\n    # n.b. does not retry against multiple servers\n    server = seeds[0]\n    return SystemManager(server)\n\nclass ThingMeta(type):\n    def __init__(cls, name, bases, dct):\n        type.__init__(cls, name, bases, dct)\n\n        if hasattr(cls, '_ttl') and hasattr(cls._ttl, 'total_seconds'):\n            cls._ttl = cls._ttl.total_seconds()\n\n        if cls._use_db:\n            if cls._type_prefix is None:\n                # default to the class name\n                cls._type_prefix = name\n\n            if '_' in cls._type_prefix:\n                raise TdbException(\"Cannot have _ in type prefix %r (for %r)\"\n                                   % (cls._type_prefix, name))\n\n            if cls._type_prefix in thing_types:\n                raise InvariantException(\"Redefining type %r?\" % (cls._type_prefix))\n\n            # if we weren't given a specific _cf_name, we can use the\n            # classes's name\n            cf_name = cls._cf_name or name\n\n            thing_types[cls._type_prefix] = cls\n\n            if not getattr(cls, \"_read_consistency_level\", None):\n                cls._read_consistency_level = read_consistency_level\n            if not getattr(cls, \"_write_consistency_level\", None):\n                cls._write_consistency_level = write_consistency_level\n\n            pool_name = getattr(cls, \"_connection_pool\", default_connection_pool)\n            connection_pool = connection_pools[pool_name]\n            cassandra_seeds = connection_pool.server_list\n\n            try:\n                cls._cf = ColumnFamily(connection_pool,\n                                       cf_name,\n                                       read_consistency_level = cls._read_consistency_level,\n                                       write_consistency_level = cls._write_consistency_level)\n            except NotFoundException:\n                if not db_create_tables:\n                    raise\n\n                manager = get_manager(cassandra_seeds)\n\n                # allow subclasses to add creation args or override base class ones\n                extra_creation_arguments = {}\n                for c in reversed(inspect.getmro(cls)):\n                    creation_args = getattr(c, \"_extra_schema_creation_args\", {})\n                    extra_creation_arguments.update(creation_args)\n\n                log.warning(\"Creating Cassandra Column Family %s\" % (cf_name,))\n                with make_lock(\"cassandra_schema\", 'cassandra_schema'):\n                    manager.create_column_family(keyspace, cf_name,\n                                                 comparator_type = cls._compare_with,\n                                                 super=getattr(cls, '_super', False),\n                                                 **extra_creation_arguments\n                                                 )\n                log.warning(\"Created Cassandra Column Family %s\" % (cf_name,))\n\n                # try again to look it up\n                cls._cf = ColumnFamily(connection_pool,\n                                       cf_name,\n                                       read_consistency_level = cls._read_consistency_level,\n                                       write_consistency_level = cls._write_consistency_level)\n\n        cls._kind = name\n\n    def __repr__(cls):\n        return '<thing: %s>' % cls.__name__\n\n\nclass ThingBase(object):\n    # base class for Thing\n\n    __metaclass__ = ThingMeta\n\n    _cf_name = None # the name of the ColumnFamily; defaults to the\n                    # name of the class\n\n    # subclasses must replace these\n\n    _type_prefix = None # this must be present for classes with _use_db==True\n\n    _use_db = False\n\n    # the Cassandra column-comparator (internally orders column\n    # names). In real life you can't change this without some changes\n    # to tdb_cassandra to support other attr types\n    _compare_with = UTF8_TYPE\n\n    _value_type = None # if set, overrides all of the _props types\n                       # below. Used for Views. One of 'int', 'float',\n                       # 'bool', 'pickle', 'json', 'date', 'bytes', 'str'\n\n    _int_props = ()\n    _float_props = () # note that we can lose resolution on these\n    _bool_props = ()\n    _pickle_props = ()\n    _json_props = ()\n    _date_props = () # note that we can lose resolution on these\n    _bytes_props = ()\n    _str_props = () # at present we never actually read out of here\n                    # since it's the default if none of the previous\n                    # matches\n\n    # the value that we assume a property to have if it is not found\n    # in the DB. Note that we don't do type-checking here, so if you\n    # want a default to be a boolean and want it to be storable you'll\n    # also have to set it in _bool_props\n    _defaults = {}\n\n    # The default TTL in seconds to add to all columns. Note: if an\n    # entire object is expected to have a TTL, it should be considered\n    # immutable! (You don't want to write out an object with an author\n    # and date, then go update author or add a new column, then have\n    # the original columns expire. Then when you go to look it up, the\n    # inherent properties author and/or date will be gone and only the\n    # updated columns will be present.) This is an expected convention\n    # and is not enforced.\n    _ttl = None\n    _warn_on_partial_ttl = True\n\n    # A per-class dictionary of default TTLs that new columns of this\n    # class should have\n    _default_ttls = {}\n\n    # A per-instance property defining the TTL of individual columns\n    # (that must also appear in self._dirties)\n    _column_ttls = {}\n\n    # a timestamp property that will automatically be added to newly\n    # created Things (disable by setting to None)\n    _timestamp_prop = None\n\n    # a per-instance property indicating that this object was\n    # partially loaded: i.e. only some properties were requested from\n    # the DB\n    _partial = None\n\n    # a per-instance property that specifies that the columns backing\n    # these attributes are to be removed on _commit()\n    _deletes = set()\n\n    # thrift will materialize the entire result set for a slice range\n    # in memory, meaning that we need to limit the maximum number of columns\n    # we receive in a single get to avoid hurting the server. if this\n    # value is true, we will make sure to do extra gets to retrieve all of\n    # the columns in a row when there are more than the per-call maximum.\n    _fetch_all_columns = False\n\n    # request-local cache to avoid duplicate lookups from hitting C*\n    _local_cache = g.cassandra_local_cache\n\n    def __init__(self, _id = None, _committed = False, _partial = None, **kw):\n        # things that have changed\n        self._dirties = kw.copy()\n\n        # what the original properties were when we went to Cassandra to\n        # get them\n        self._orig = {}\n\n        self._defaults = self._defaults.copy()\n\n        # whether this item has ever been created\n        self._committed = _committed\n\n        self._partial = None if _partial is None else frozenset(_partial)\n\n        self._deletes = set()\n        self._column_ttls = {}\n\n        # our row key\n        self._id = _id\n\n        if not self._use_db:\n            raise TdbException(\"Cannot make instances of %r\" % (self.__class__,))\n\n    @classmethod\n    def _byID(cls, ids, return_dict=True, properties=None):\n        ids, is_single = tup(ids, True)\n\n        if properties is not None:\n            asked_properties = frozenset(properties)\n            willask_properties = set(properties)\n\n        if not len(ids):\n            if is_single:\n                raise InvariantException(\"whastis?\")\n            return {}\n\n        # all keys must be strings or directly convertable to strings\n        assert all(isinstance(_id, basestring) or str(_id) for _id in ids)\n\n        def reject_bad_partials(cached, still_need):\n            # tell sgm that the match it found in the cache isn't good\n            # enough if it's a partial that doesn't include our\n            # properties. we still need to look those items up to get\n            # the properties that we're after\n            stillfind = set()\n\n            for k, v in cached.iteritems():\n                if properties is None:\n                    if v._partial is not None:\n                        # there's a partial in the cache but we're not\n                        # looking for partials\n                        stillfind.add(k)\n                elif v._partial is not None and not asked_properties.issubset(v._partial):\n                    # we asked for a partial, and this is a partial,\n                    # but it doesn't have all of the properties that\n                    # we need\n                    stillfind.add(k)\n\n                    # other callers in our request are now expecting\n                    # to find the properties that were on that\n                    # partial, so we'll have to preserve them\n                    for prop in v._partial:\n                        willask_properties.add(prop)\n\n            for k in stillfind:\n                del cached[k]\n                still_need.add(k)\n\n        def lookup(l_ids):\n            if properties is None:\n                rows = cls._cf.multiget(l_ids, column_count=max_column_count)\n\n                # if we got max_column_count columns back for a row, it was\n                # probably clipped. in this case, we should fetch the remaining\n                # columns for that row and add them to the result.\n                if cls._fetch_all_columns:\n                    for key, row in rows.iteritems():\n                        if len(row) == max_column_count:\n                            last_column_seen = next(reversed(row))\n                            cols = cls._cf.xget(key,\n                                                column_start=last_column_seen,\n                                                buffer_size=max_column_count)\n                            row.update(cols)\n            else:\n                rows = cls._cf.multiget(l_ids, columns = willask_properties)\n\n            l_ret = {}\n            for t_id, row in rows.iteritems():\n                t = cls._from_serialized_columns(t_id, row)\n                if properties is not None:\n                    # make sure that the item is marked as a _partial\n                    t._partial = willask_properties\n                l_ret[t._id] = t\n\n            return l_ret\n\n        ret = sgm(\n            cache=cls._local_cache,\n            keys=ids,\n            miss_fn=lookup,\n            prefix=cls._cache_prefix(),\n            found_fn=reject_bad_partials,\n        )\n\n        if is_single and not ret:\n            raise NotFound(\"<%s %r>\" % (cls.__name__,\n                                        ids[0]))\n        elif is_single:\n            assert len(ret) == 1\n            return ret.values()[0]\n        elif return_dict:\n            return ret\n        else:\n            return filter(None, (ret.get(i) for i in ids))\n\n    @property\n    def _fullname(self):\n        if self._type_prefix is None:\n            raise TdbException(\"%r has no _type_prefix, so fullnames cannot be generated\"\n                               % self.__class__)\n\n        return '%s_%s' % (self._type_prefix, self._id)\n\n    @classmethod\n    def _by_fullname(cls, fnames, return_dict=True, ignore_missing=False):\n        if ignore_missing:\n            raise NotImplementedError\n        ids, is_single = tup(fnames, True)\n\n        by_cls = {}\n        for i in ids:\n            typ, underscore, _id = i.partition('_')\n            assert underscore == '_'\n\n            by_cls.setdefault(thing_types[typ], []).append(_id)\n\n        items = []\n        for typ, ids in by_cls.iteritems():\n            items.extend(typ._byID(ids).values())\n\n        if is_single:\n            try:\n                return items[0]\n            except IndexError:\n                raise NotFound(\"<%s %r>\" % (cls.__name__, ids[0]))\n        elif return_dict:\n            return dict((x._fullname, x) for x in items)\n        else:\n            d = dict((x._fullname, x) for x in items)\n            return [d[fullname] for fullname in fnames]\n\n    @classmethod\n    def _cache_prefix(cls):\n        return 'tdbcassandra_' + cls._type_prefix + '_'\n\n    def _cache_key(self):\n        if not self._id:\n            raise TdbException('no cache key for uncommitted %r' % (self,))\n\n        return self._cache_key_id(self._id)\n\n    @classmethod\n    def _cache_key_id(cls, t_id):\n        return cls._cache_prefix() + t_id\n\n    @classmethod\n    def _wcl(cls, wcl, default = None):\n        if wcl is not None:\n            return wcl\n        elif default is not None:\n            return default\n        return cls._write_consistency_level\n\n    def _rcl(cls, rcl, default = None):\n        if rcl is not None:\n            return rcl\n        elif default is not None:\n            return default\n        return cls._read_consistency_level\n\n    @classmethod\n    def _get_column_validator(cls, colname):\n        return cls._cf.column_validators.get(colname,\n                                             cls._cf.default_validation_class)\n\n    @classmethod\n    def _deserialize_column(cls, attr, val):\n        if attr in cls._int_props or (cls._value_type and cls._value_type == 'int'):\n            try:\n                return int(val)\n            except ValueError:\n                return long(val)\n        elif attr in cls._float_props or (cls._value_type and cls._value_type == 'float'):\n            return float(val)\n        elif attr in cls._bool_props or (cls._value_type and cls._value_type == 'bool'):\n            # note that only the string \"1\" is considered true!\n            return val == '1'\n        elif attr in cls._pickle_props or (cls._value_type and cls._value_type == 'pickle'):\n            return pickle.loads(val)\n        elif attr in cls._json_props or (cls._value_type and cls._value_type == 'json'):\n            return json.loads(val)\n        elif attr in cls._date_props or attr == cls._timestamp_prop or (cls._value_type and cls._value_type == 'date'):\n            return cls._deserialize_date(val)\n        elif attr in cls._bytes_props or (cls._value_type and cls._value_type == 'bytes'):\n            return val\n\n        # otherwise we'll assume that it's a utf-8 string\n        return val if isinstance(val, unicode) else val.decode('utf-8')\n\n    @classmethod\n    def _serialize_column(cls, attr, val):\n        if (attr in chain(cls._int_props, cls._float_props) or\n            (cls._value_type and cls._value_type in ('float', 'int'))):\n            return str(val)\n        elif attr in cls._bool_props or (cls._value_type and cls._value_type == 'bool'):\n            # n.b. we \"truncate\" this to a boolean, so truthy but\n            # non-boolean values are discarded\n            return '1' if val else '0'\n        elif attr in cls._pickle_props or (cls._value_type and cls._value_type == 'pickle'):\n            return pickle.dumps(val)\n        elif attr in cls._json_props or (cls._value_type and cls._value_type == 'json'):\n            return json.dumps(val)\n        elif (attr in cls._date_props or attr == cls._timestamp_prop or\n              (cls._value_type and cls._value_type == 'date')):\n            # the _timestamp_prop is handled in _commit(), not here\n            validator = cls._get_column_validator(attr)\n            if validator in (\"DateType\", \"TimeUUIDType\"):\n                # pycassa will take it from here\n                return val\n            else:\n                return cls._serialize_date(val)\n        elif attr in cls._bytes_props or (cls._value_type and cls._value_type == 'bytes'):\n            return val\n\n        return unicode(val).encode('utf-8')\n\n    @classmethod\n    def _serialize_date(cls, date):\n        return date_serializer.pack(date)\n\n    @classmethod\n    def _deserialize_date(cls, val):\n        if isinstance(val, datetime):\n            date = val\n        elif isinstance(val, UUID):\n            return convert_uuid_to_time(val)\n        elif len(val) == 8: # cassandra uses 8-byte integer format for this\n            date = date_serializer.unpack(val)\n        else: # it's probably the old-style stringified seconds since epoch\n            as_float = float(val)\n            date = datetime.utcfromtimestamp(as_float)\n\n        return date.replace(tzinfo=pytz.utc)\n\n    @classmethod\n    def _from_serialized_columns(cls, t_id, columns):\n        d_columns = dict((attr, cls._deserialize_column(attr, val))\n                         for (attr, val)\n                         in columns.iteritems())\n        return cls._from_columns(t_id, d_columns)\n\n    @classmethod\n    def _from_columns(cls, t_id, columns):\n        \"\"\"Given a dictionary of freshly deserialized columns\n           construct an instance of cls\"\"\"\n        t = cls()\n        t._orig = columns\n        t._id = t_id\n        t._committed = True\n        return t\n\n    @property\n    def _dirty(self):\n        return len(self._dirties) or len(self._deletes) or not self._committed\n\n    @will_write\n    def _commit(self, write_consistency_level = None):\n        if not self._dirty:\n            return\n\n        if self._id is None:\n            raise TdbException(\"Can't commit %r without an ID\" % (self,))\n\n        if self._committed and self._ttl and self._warn_on_partial_ttl:\n            log.warning(\"Using a full-TTL object %r in a mutable fashion\"\n                        % (self,))\n\n        if not self._committed:\n            # if this has never been committed we should also consider\n            # the _orig columns as dirty (but \"less dirty\" than the\n            # _dirties)\n            upd = self._orig.copy()\n            self._orig.clear()\n            upd.update(self._dirties)\n            self._dirties = upd\n\n        # Cassandra values are untyped byte arrays, so we need to\n        # serialize everything while filtering out anything that's\n        # been dirtied but doesn't actually differ from what's already\n        # in the DB\n        updates = dict((attr, self._serialize_column(attr, val))\n                       for (attr, val)\n                       in self._dirties.iteritems()\n                       if (attr not in self._orig or\n                           val != self._orig[attr]))\n\n        # n.b. deleted columns are applied *after* the updates. our\n        # __setattr__/__delitem__ tries to make sure that this always\n        # works\n\n        if not self._committed and self._timestamp_prop and self._timestamp_prop not in updates:\n            # auto-create timestamps on classes that request them\n\n            # this serialize/deserialize is a bit funny: the process\n            # of storing and retrieving causes us to lose some\n            # resolution because of the floating-point representation,\n            # so this is just to make sure that we have the same value\n            # that the DB does after writing it out. Note that this is\n            # the only property munged this way: other timestamp and\n            # floating point properties may lose resolution\n            s_now = self._serialize_date(datetime.now(tz))\n            now = self._deserialize_date(s_now)\n\n            timestamp_is_typed = self._get_column_validator(self._timestamp_prop) == \"DateType\"\n            updates[self._timestamp_prop] = now if timestamp_is_typed else s_now\n            self._dirties[self._timestamp_prop] = now\n\n        if not updates and not self._deletes:\n            self._dirties.clear()\n            return\n\n        # actually write out the changes to the CF\n        wcl = self._wcl(write_consistency_level)\n        with self._cf.batch(write_consistency_level = wcl) as b:\n            if updates:\n                for k, v in updates.iteritems():\n                    b.insert(self._id,\n                             {k: v},\n                             ttl=self._column_ttls.get(k, self._ttl))\n            if self._deletes:\n                b.remove(self._id, self._deletes)\n\n        self._orig.update(self._dirties)\n        self._column_ttls.clear()\n        self._dirties.clear()\n        for k in self._deletes:\n            try:\n                del self._orig[k]\n            except KeyError:\n                pass\n        self._deletes.clear()\n\n        if not self._committed:\n            self._on_create()\n        else:\n            self._on_commit()\n\n        self._committed = True\n\n        self.__class__._local_cache.set(self._cache_key(), self)\n\n    def _revert(self):\n        if not self._committed:\n            raise TdbException(\"Revert to what?\")\n\n        self._dirties.clear()\n        self._deletes.clear()\n        self._column_ttls.clear()\n\n    def _destroy(self):\n        self._cf.remove(self._id,\n                        write_consistency_level=self._write_consistency_level)\n\n    def __getattr__(self, attr):\n        if isinstance(attr, basestring) and attr.startswith('_'):\n            # TODO: I bet this interferes with Views whose column names can\n            # start with a _\n            try:\n                return self.__dict__[attr]\n            except KeyError:\n                raise AttributeError, attr\n\n        if attr in self._deletes:\n            raise AttributeError(\"%r has no %r because you deleted it\", (self, attr))\n        elif attr in self._dirties:\n            return self._dirties[attr]\n        elif attr in self._orig:\n            return self._orig[attr]\n        elif attr in self._defaults:\n            return self._defaults[attr]\n        elif self._partial is not None and attr not in self._partial:\n            raise AttributeError(\"%r has no %r but you didn't request it\" % (self, attr))\n        else:\n            raise AttributeError('%r has no %r' % (self, attr))\n\n    def __setattr__(self, attr, val):\n        if attr == '_id' and self._committed:\n            raise ValueError('cannot change _id on a committed %r' % (self.__class__))\n\n        if isinstance(attr, basestring) and attr.startswith('_'):\n            # TODO: I bet this interferes with Views whose column names can\n            # start with a _\n            return object.__setattr__(self, attr, val)\n\n        try:\n            self._deletes.remove(attr)\n        except KeyError:\n            pass\n        self._dirties[attr] = val\n        if attr in self._default_ttls:\n            self._column_ttls[attr] = self._default_ttls[attr]\n\n    def __eq__(self, other):\n        if self.__class__ != other.__class__:\n            return False\n\n        if self._partial or other._partial and self._partial != other._partial:\n            raise ValueError(\"Can't compare incompatible partials\")\n\n        return self._id == other._id and self._t == other._t\n\n    def __ne__(self, other):\n        return not (self == other)\n\n    @property\n    def _t(self):\n        \"\"\"Emulate the _t property from tdb_sql: a dictionary of all\n           values that are or will be stored in the database, (not\n           including _defaults or unrequested properties on\n           partials)\"\"\"\n        ret = self._orig.copy()\n        ret.update(self._dirties)\n        for k in self._deletes:\n            try:\n                del ret[k]\n            except KeyError:\n                pass\n        return ret\n\n    # allow the dictionary mutation syntax; it makes working some some\n    # keys a bit easier. Go through our regular\n    # __getattr__/__setattr__ functions where all of the appropriate\n    # work is done\n    def __getitem__(self, key):\n        return self.__getattr__(key)\n\n    def __setitem__(self, key, value):\n        return self.__setattr__(key, value)\n\n    def __delitem__(self, key):\n        try:\n            del self._dirties[key]\n        except KeyError:\n            pass\n        try:\n            del self._column_ttls[key]\n        except KeyError:\n            pass\n        self._deletes.add(key)\n\n    def _get(self, key, default = None):\n        try:\n            return self.__getattr__(key)\n        except AttributeError:\n            if self._partial is not None and key not in self._partial:\n                raise AttributeError(\"_get on unrequested key from partial\")\n            return default\n\n    def _set_ttl(self, key, ttl):\n        assert key in self._dirties\n        assert isinstance(ttl, (long, int))\n        self._column_ttls[key] = ttl\n\n    def _on_create(self):\n        \"\"\"A hook executed on creation, good for creation of static\n           Views. Subclasses should call their parents' hook(s) as\n           well\"\"\"\n        pass\n\n    def _on_commit(self):\n        \"\"\"Executed on _commit other than creation.\"\"\"\n        pass\n\n    @classmethod\n    def _all(cls):\n        # returns a query object yielding every single item in a\n        # column family. it probably shouldn't be used except in\n        # debugging\n        return Query(cls, limit=None)\n\n    def __repr__(self):\n        # it's safe for subclasses to override this to e.g. put a Link\n        # title or Account name in the repr(), but they must be\n        # careful to check hasattr for the properties that they read\n        # out, as __getattr__ itself may call __repr__ in constructing\n        # its error messages\n        id_str = self._id\n        comm_str = '' if self._committed else ' (uncommitted)'\n        part_str = '' if self._partial is None else ' (partial)'\n        return \"<%s %r%s%s>\" % (self.__class__.__name__,\n                              id_str,\n                              comm_str, part_str)\n\n    if debug:\n        # we only want this with g.debug because overriding __del__ can play\n        # hell with memory leaks\n        def __del__(self):\n            if not self._committed:\n                # normally we'd log this with g.log or something, but we can't\n                # guarantee that the thread destructing us has access to g\n                print \"Warning: discarding uncomitted %r; this is usually a bug\" % (self,)\n            elif self._dirty:\n                print (\"Warning: discarding dirty %r; this is usually a bug (_dirties=%r, _deletes=%r)\"\n                       % (self,self._dirties,self._deletes))\n\nclass Thing(ThingBase):\n    _timestamp_prop = 'date'\n\n    # alias _date property for consistency with tdb_sql things.\n    @property\n    def _date(self):\n        return self.date\n\nclass UuidThing(ThingBase):\n    _timestamp_prop = 'date'\n    _extra_schema_creation_args = {\n        'key_validation_class': TIME_UUID_TYPE\n    }\n\n    def __init__(self, **kw):\n        ThingBase.__init__(self, _id=uuid1(), **kw)\n\n    @classmethod\n    def _byID(cls, ids, **kw):\n        ids, is_single = tup(ids, ret_is_single=True)\n\n        #Convert string ids to UUIDs before retrieving\n        uuids = [UUID(id) if not isinstance(id, UUID) else id for id in ids]\n\n        if len(uuids) == 0:\n            return {}\n        elif is_single:\n            assert len(uuids) == 1\n            uuids = uuids[0]\n\n        return super(UuidThing, cls)._byID(uuids, **kw)\n\n    @classmethod\n    def _cache_key_id(cls, t_id):\n        return cls._cache_prefix() + str(t_id)\n\n\ndef view_of(cls):\n    \"\"\"Register a class as a view of a Thing.\n\n    When a Thing is created or destroyed the appropriate View method must be\n    called to update the View. This can be done using Thing._on_create() for\n    general Thing classes or create()/destroy() for DenormalizedRelation\n    classes.\n\n    \"\"\"\n    def view_of_decorator(view_cls):\n        cls._views.append(view_cls)\n        view_cls._view_of = cls\n        return view_cls\n    return view_of_decorator\n\n\n\nclass DenormalizedRelation(object):\n    \"\"\"A model of many-to-many relationships, indexed by thing1.\n\n    Each thing1 is represented by a row. The relationships from that thing1 to\n    a number of thing2s are represented by columns in that row. To query if\n    relationships exist and what its value is (\"name\" in the PG model), we\n    fetch the thing1's row, telling C* we're only interested in the columns\n    representing the thing2s we are interested in. This allows negative lookups\n    to be very fast because of the row-level bloom filter.\n\n    This data model will generate VERY wide rows. Any column family based on\n    it should have its row cache disabled.\n\n    \"\"\"\n    __metaclass__ = ThingMeta\n    _use_db = False\n    _cf_name = None\n    _compare_with = ASCII_TYPE\n    _type_prefix = None\n    _last_modified_name = None\n    _write_last_modified = True\n    _extra_schema_creation_args = dict(key_validation_class=ASCII_TYPE,\n                                       default_validation_class=UTF8_TYPE)\n    _ttl = None\n\n    @classmethod\n    def value_for(cls, thing1, thing2, **kw):\n        \"\"\"Return a value to store for a relationship between thing1/thing2.\"\"\"\n        raise NotImplementedError()\n\n    @classmethod\n    @will_write\n    def create(cls, thing1, thing2s, **kw):\n        \"\"\"Create a relationship between thing1 and thing2s.\n\n        If there are any other views of this data, they will be updated as\n        well.\n\n        Takes kwargs which can be used by views\n        or value_for to get additional information.\n\n        \"\"\"\n        thing2s = tup(thing2s)\n        values = {thing2._id36 : cls.value_for(thing1, thing2, **kw)\n                  for thing2 in thing2s}\n        cls._cf.insert(thing1._id36, values, ttl=cls._ttl)\n\n        for view in cls._views:\n            view.create(thing1, thing2s, **kw)\n\n        if cls._write_last_modified:\n            from r2.models.last_modified import LastModified\n            LastModified.touch(thing1._fullname, cls._last_modified_name)\n\n    @classmethod\n    @will_write\n    def destroy(cls, thing1, thing2s):\n        \"\"\"Destroy relationships between thing1 and some thing2s.\"\"\"\n        thing2s = tup(thing2s)\n        cls._cf.remove(thing1._id36, (thing2._id36 for thing2 in thing2s))\n\n        for view in cls._views:\n            view.destroy(thing1, thing2s)\n\n    @classmethod\n    def fast_query(cls, thing1, thing2s):\n        \"\"\"Find relationships between thing1 and various thing2s.\"\"\"\n        thing2s, thing2s_is_single = tup(thing2s, ret_is_single=True)\n\n        if not thing1:\n            return {}\n\n        # don't bother looking up relationships for items that were created\n        # since the last time the thing1 created a relationship of this type\n        if cls._last_modified_name:\n            from r2.models.last_modified import LastModified\n            timestamp = LastModified.get(thing1._fullname,\n                                         cls._last_modified_name)\n            if timestamp:\n                thing2s = [thing2 for thing2 in thing2s\n                           if thing2._date <= timestamp]\n            else:\n                thing2s = []\n\n        if not thing2s:\n            return {}\n\n        # fetch the row from cassandra. if it doesn't exist, thing1 has no\n        # relation of this type to any thing2!\n        try:\n            columns = [thing2._id36 for thing2 in thing2s]\n            results = cls._cf.get(thing1._id36, columns)\n        except NotFoundException:\n            results = {}\n\n        # return the data in the expected format\n        if not thing2s_is_single:\n            # {(thing1, thing2) : value}\n            thing2s_by_id = {thing2._id36 : thing2 for thing2 in thing2s}\n            return {(thing1, thing2s_by_id[k]) : v\n                    for k, v in results.iteritems()}\n        else:\n            if results:\n                assert len(results) == 1\n                return results.values()[0]\n            else:\n                raise NotFound(\"<%s %r>\" % (cls.__name__, (thing1._id36,\n                                                           thing2._id36)))\n\n\nclass ColumnQuery(object):\n    \"\"\"\n    A query across a row of a CF.\n    \"\"\"\n    _chunk_size = 100\n\n    def __init__(self, cls, rowkeys, column_start=\"\", column_finish=\"\",\n                 column_count=100, column_reversed=True,\n                 column_to_obj=None,\n                 obj_to_column=None):\n        self.cls = cls\n        self.rowkeys = rowkeys\n        self.column_start = column_start\n        self.column_finish = column_finish\n        self._limit = column_count\n        self.column_reversed = column_reversed\n        self.column_to_obj = column_to_obj or self.default_column_to_obj\n        self.obj_to_column = obj_to_column or self.default_obj_to_column\n        self._rules = []    # dummy parameter to mimic tdb_sql queries\n\n        # Sorting for TimeUuid objects\n        if self.cls._compare_with == TIME_UUID_TYPE:\n            def sort_key(i):\n                return i.time\n        else:\n            def sort_key(i):\n                return i\n        self.sort_key = sort_key\n\n    @staticmethod\n    def combine(queries):\n        raise NotImplementedError\n\n    @staticmethod\n    def default_column_to_obj(columns):\n        \"\"\"\n        Mapping from column --> object.\n\n        This default doesn't actually return the underlying object but we don't\n        know how to do that without more information.\n        \"\"\"\n        return columns\n\n    @staticmethod\n    def default_obj_to_column(objs):\n        \"\"\"\n        Mapping from object --> column\n        \"\"\"\n        objs, is_single = tup(objs, ret_is_single=True)\n        columns = [{obj._id: obj._id} for obj in objs]\n\n        if is_single:\n            return columns[0]\n        else:\n            return columns\n\n    def _after(self, thing):\n        if thing:\n            column_name = self.obj_to_column(thing).keys()[0]\n            self.column_start = column_name\n        else:\n            self.column_start = \"\"\n\n    def _after_id(self, column_name):\n        self.column_start = column_name\n\n    def _reverse(self):\n        # Logic of standard reddit query is opposite of cassandra\n        self.column_reversed = False\n\n    def __iter__(self, yield_column_names=False):\n        retrieved = 0\n        column_start = self.column_start\n        while retrieved < self._limit:\n            try:\n                column_count = min(self._chunk_size, self._limit - retrieved)\n                if column_start:\n                    column_count += 1   # cassandra includes column_start\n                r = self.cls._cf.multiget(self.rowkeys,\n                                          column_start=column_start,\n                                          column_finish=self.column_finish,\n                                          column_count=column_count,\n                                          column_reversed=self.column_reversed)\n\n                # multiget returns OrderedDict {rowkey: {column_name: column_value}}\n                # combine into single OrderedDict of {column_name: column_value}\n                nrows = len(r.keys())\n                if nrows == 0:\n                    return\n                elif nrows == 1:\n                    columns = r.values()[0]\n                else:\n                    r_combined = {}\n                    for d in r.values():\n                        r_combined.update(d)\n                    columns = OrderedDict(sorted(r_combined.items(),\n                                                 key=lambda t: self.sort_key(t[0]),\n                                                 reverse=self.column_reversed))\n            except NotFoundException:\n                return\n\n            retrieved += self._chunk_size\n\n            if column_start:\n                try:\n                    del columns[column_start]\n                except KeyError:\n                    # This can happen when a timezone-aware datetime is\n                    # passed in as a column_start, but non-timezone-aware\n                    # datetimes are returned from cassandra, causing `del` to\n                    # fail.\n                    #\n                    # Reversed queries include column_start in the results,\n                    # while non-reversed queries do not.\n                    if self.column_reversed:\n                        columns.popitem(last=False)\n\n            if not columns:\n                return\n\n            # Convert to list of columns\n            l_columns = [{col_name: columns[col_name]} for col_name in columns]\n\n            column_start = l_columns[-1].keys()[0]\n            objs = self.column_to_obj(l_columns)\n\n            if yield_column_names:\n                column_names = [column.keys()[0] for column in l_columns]\n                if len(column_names) == 1:\n                    ret = (column_names[0], objs),\n                else:\n                    ret = zip(column_names, objs)\n            else:\n                ret = objs\n\n            ret, is_single = tup(ret, ret_is_single=True)\n            for r in ret:\n                yield r\n\n    def __repr__(self):\n        return \"<%s(%s-%r)>\" % (self.__class__.__name__, self.cls.__name__,\n                                self.rowkeys)\n\nclass MultiColumnQuery(object):\n    def __init__(self, queries, num, sort_key=None):\n        self.num = num\n        self._queries = queries\n        self.sort_key = sort_key    # python doesn't sort UUID1's correctly, need to pass in a sorter\n        self._rules = []            # dummy parameter to mimic tdb_sql queries\n\n    def _after(self, thing):\n        for q in self._queries:\n            q._after(thing)\n\n    def _reverse(self):\n        for q in self._queries:\n            q._reverse()\n\n    def __setattr__(self, attr, val):\n        # Catch _limit to set on all queries\n        if attr == '_limit':\n             for q in self._queries:\n                 q._limit = val\n        else:\n            object.__setattr__(self, attr, val)\n\n    def __iter__(self):\n\n        if self.sort_key:\n            def sort_key(tup):\n                # Need to point the supplied sort key at the correct item in\n                # the (sortable, item, generator) tuple\n                return self.sort_key(tup[0])\n        else:\n            def sort_key(tup):\n                return tup[0]\n\n        top_items = []\n        for q in self._queries:\n            try:\n                gen = q.__iter__(yield_column_names=True)\n                column_name, item = gen.next()\n                top_items.append((column_name, item, gen))\n            except StopIteration:\n                pass\n        top_items.sort(key=sort_key)\n\n        def _update(top_items):\n            # Remove the first item from combined query and update the list\n            head = top_items.pop(0)\n            item = head[1]\n            gen = head[2]\n\n            # Try to get a new item from the query that gave us the current one\n            try:\n                column_name, item = gen.next()\n                top_items.append((column_name, item, gen)) # if multiple queues have the same item value the sort is somewhat undefined\n                top_items.sort(key=sort_key)\n            except StopIteration:\n                pass\n\n        num_ret = 0\n        while top_items and num_ret < self.num:\n            yield top_items[0][1]\n            _update(top_items)\n            num_ret += 1\n\nclass Query(object):\n    \"\"\"A query across a CF. Note that while you can query rows from a\n       CF that has a RandomPartitioner, you won't get them in any sort\n       of order\"\"\"\n    def __init__(self, cls, after=None, properties=None, limit=100,\n                 chunk_size=100, _max_column_count = max_column_count):\n        self.cls = cls\n        self.after = after\n        self.properties = properties\n        self.limit = limit\n        self.chunk_size = chunk_size\n        self.max_column_count = _max_column_count\n\n    def __copy__(self):\n        return Query(self.cls, after=self.after,\n                     properties = self.properties,\n                     limit=self.limit,\n                     chunk_size=self.chunk_size,\n                     _max_column_count = self.max_column_count)\n    copy = __copy__\n\n    def _dump(self):\n        q = self.copy()\n        q.after = q.limit = None\n\n        for row in q:\n            print row\n            for col, val in row._t.iteritems():\n                print '\\t%s: %r' % (col, val)\n\n    def __iter__(self):\n        # n.b.: we aren't caching objects that we find this way in the\n        # LocalCache. This may will need to be changed if we ever\n        # start using OPP in Cassandra (since otherwise these types of\n        # queries aren't useful for anything but debugging anyway)\n        after = '' if self.after is None else self.after._id\n        limit = self.limit\n\n        if self.properties is None:\n            r = self.cls._cf.get_range(start=after, row_count=limit,\n                                       column_count = self.max_column_count)\n        else:\n            r = self.cls._cf.get_range(start=after, row_count=limit,\n                                       columns = self.properties)\n\n        for t_id, columns in r:\n            if not columns:\n                # a ghost row\n                continue\n\n            t = self.cls._from_serialized_columns(t_id, columns)\n            yield t\n\nclass View(ThingBase):\n    # Views are Things like any other, but may have special key\n    # characteristics. Uses ColumnQuery for queries across a row.\n\n    _timestamp_prop = None\n    _value_type = 'str'\n\n    _compare_with = UTF8_TYPE   # Type of the columns - should match _key_validation_class of _view_of class\n    _view_of = None\n    _write_consistency_level = CL.ONE   # Is this necessary?\n    _query_cls = ColumnQuery\n\n    @classmethod\n    def _rowkey(cls, obj):\n        \"\"\"Mapping from _view_of object --> view rowkey. No default\n        implementation is provided because this is the fundamental aspect of the\n        view.\"\"\"\n        raise NotImplementedError\n\n    @classmethod\n    def _obj_to_column(cls, objs):\n        \"\"\"Mapping from _view_of object --> view column. Returns a\n        single item dict {column name:column value} or list of dicts.\"\"\"\n        objs, is_single = tup(objs, ret_is_single=True)\n\n        columns = [{obj._id: obj._id} for obj in objs]\n\n        if len(columns) == 1:\n            return columns[0]\n        else:\n            return columns\n\n    @classmethod\n    def _column_to_obj(cls, columns):\n        \"\"\"Mapping from view column --> _view_of object. Must be complement to\n        _obj_to_column().\"\"\"\n        columns, is_single = tup(columns, ret_is_single=True)\n\n        ids = [column.keys()[0] for column in columns]\n\n        if len(ids) == 1:\n            ids = ids[0]\n        return cls._view_of._byID(ids, return_dict=False)\n\n    @classmethod\n    def add_object(cls, obj, **kw):\n        \"\"\"Add a lookup to the view\"\"\"\n        rowkey = cls._rowkey(obj)\n        column = cls._obj_to_column(obj)\n        cls._set_values(rowkey, column, **kw)\n\n    @classmethod\n    def query(cls, rowkeys, after=None, reverse=False, count=1000):\n        \"\"\"Return a query to get objects from the underlying _view_of class.\"\"\"\n\n        column_reversed = not reverse   # Reverse convention for cassandra is opposite\n\n        q = cls._query_cls(cls, rowkeys, column_count=count,\n                           column_reversed=column_reversed,\n                           column_to_obj=cls._column_to_obj,\n                           obj_to_column=cls._obj_to_column)\n        q._after(after)\n        return q\n\n    def _values(self):\n        \"\"\"Retrieve the entire contents of the view\"\"\"\n        # TODO: at present this only grabs max_column_count columns\n        return self._t\n\n    @classmethod\n    def get_time_sorted_columns(cls, rowkey, limit=None):\n        q = cls._cf.xget(rowkey, include_timestamp=True)\n        r = sorted(q, key=lambda i: i[1][1]) # (col_name, (col_val, timestamp))\n        if limit:\n            r = r[:limit]\n        return OrderedDict([(i[0], i[1][0]) for i in r])\n\n    @classmethod\n    @will_write\n    def _set_values(cls, row_key, col_values,\n                    write_consistency_level = None,\n                    batch=None,\n                    ttl=None):\n        \"\"\"Set a set of column values in a row of a view without\n           looking up the whole row first\"\"\"\n        # col_values =:= dict(col_name -> col_value)\n\n        updates = dict((col_name, cls._serialize_column(col_name, col_val))\n                       for (col_name, col_val) in col_values.iteritems())\n\n        # if they didn't give us a TTL, use the default TTL for the\n        # class. This will be further overwritten below per-column\n        # based on the _default_ttls class dict. Note! There is no way\n        # to use this API to express that you don't want a TTL if\n        # there is a default set on either the row or the column\n        default_ttl = ttl or cls._ttl\n\n        def do_inserts(b):\n            for k, v in updates.iteritems():\n                b.insert(row_key, {k: v},\n                         ttl=cls._default_ttls.get(k, default_ttl))\n\n        if batch is None:\n            batch = cls._cf.batch(write_consistency_level = cls._wcl(write_consistency_level))\n            with batch as b:\n                do_inserts(b)\n        else:\n            do_inserts(batch)\n\n        # can we be smarter here?\n        cls._local_cache.delete(cls._cache_key_id(row_key))\n\n    @classmethod\n    @will_write\n    def _remove(cls, key, columns):\n        cls._cf.remove(key, columns)\n        cls._local_cache.delete(cls._cache_key_id(key))\n\nclass DenormalizedView(View):\n    \"\"\"Store the entire underlying object inside the View column.\"\"\"\n\n    @classmethod\n    def is_date_prop(cls, attr):\n        view_cls = cls._view_of\n        return (view_cls._value_type == 'date' or\n                attr in view_cls._date_props or\n                view_cls._timestamp_prop and attr == view_cls._timestamp_prop)\n\n    @classmethod\n    def _thing_dumper(cls, thing):\n        serialize_fn = cls._view_of._serialize_column\n        serialized_columns = dict((attr, serialize_fn(attr, val)) for\n            (attr, val) in thing._orig.iteritems())\n\n        # Encode date props which may be binary\n        for attr, val in serialized_columns.items():\n            if cls.is_date_prop(attr):\n                serialized_columns[attr] = base64.b64encode(val)\n\n        dump = json.dumps(serialized_columns)\n        return dump\n\n    @classmethod\n    def _thing_loader(cls, _id, dump):\n        serialized_columns = json.loads(dump)\n\n        # Decode date props\n        for attr, val in serialized_columns.items():\n            if cls.is_date_prop(attr):\n                serialized_columns[attr] = base64.b64decode(val)\n\n        obj = cls._view_of._from_serialized_columns(_id, serialized_columns)\n        return obj\n\n    @classmethod\n    def _obj_to_column(cls, objs):\n        objs = tup(objs)\n        columns = []\n        for o in objs:\n            _id = o._id\n            dump = cls._thing_dumper(o)\n            columns.append({_id: dump})\n\n        if len(columns) == 1:\n            return columns[0]\n        else:\n            return columns\n\n    @classmethod\n    def _column_to_obj(cls, columns):\n        columns = tup(columns)\n        objs = []\n        for column in columns:\n            _id, dump = column.items()[0]\n            obj = cls._thing_loader(_id, dump)\n            objs.append(obj)\n\n        if len(objs) == 1:\n            return objs[0]\n        else:\n            return objs\n"
  },
  {
    "path": "r2/r2/lib/db/tdb_lite.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport sqlalchemy as sa\nimport cPickle as pickle\n\nclass tdb_lite(object):\n    def __init__(self, gc):\n        self.gc = gc\n\n    def make_metadata(self, engine):\n        metadata = sa.MetaData(engine)\n        metadata.bind.echo = self.gc.sqlprinting\n        return metadata\n\n    def index_str(self, table, name, on, where = None):\n        index_str = 'create index idx_%s_' % name\n        index_str += table.name\n        index_str += ' on '+ table.name + ' (%s)' % on\n        if where:\n            index_str += ' where %s' % where\n        return index_str\n\n    def create_table(self, table, index_commands=None):\n        t = table\n        if self.gc.db_create_tables:\n            #@@hackish?\n            if not t.bind.has_table(t.name):\n                t.create(checkfirst = False)\n                if index_commands:\n                    for i in index_commands:\n                        t.bind.execute(i)\n\n    def py2db(self, val, return_kind=False):\n        if isinstance(val, bool):\n            val = 't' if val else 'f'\n            kind = 'bool'\n        elif isinstance(val, (str, unicode)):\n            kind = 'str'\n        elif isinstance(val, (int, float, long)):\n            kind = 'num'\n        elif val is None:\n            kind = 'none'\n        else:\n            kind = 'pickle'\n            val = pickle.dumps(val)\n\n        if return_kind:\n            return (val, kind)\n        else:\n            return val\n\n    def db2py(self, val, kind):\n        if kind == 'bool':\n            val = True if val is 't' else False\n        elif kind == 'num':\n            try:\n                val = int(val)\n            except ValueError:\n                val = float(val)\n        elif kind == 'none':\n            val = None\n        elif kind == 'pickle':\n            val = pickle.loads(val)\n\n        return val\n"
  },
  {
    "path": "r2/r2/lib/db/tdb_sql.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom copy import deepcopy\nfrom datetime import datetime\nimport cPickle as pickle\nimport logging\nimport operators\nimport re\nimport threading\n\nfrom pylons import request\nfrom pylons import tmpl_context as c\nfrom pylons import app_globals as g\n\nimport sqlalchemy as sa\n\nfrom r2.lib import filters\nfrom r2.lib.utils import (\n    iters,\n    Results,\n    simple_traceback,\n    storage,\n    tup,\n)\n\n\ndbm = g.dbm\npredefined_type_ids = g.predefined_type_ids\nlog_format = logging.Formatter('sql: %(message)s')\nmax_val_len = 1000\n\n\nclass TransactionSet(threading.local):\n    \"\"\"A manager for SQL transactions.\n\n    This implements a thread local meta-transaction which may span multiple\n    databases.  The existing tdb_sql code calls add_engine before executing\n    writes.  If thing.py calls begin then these calls will actually kick in\n    and start a transaction that must be committed or rolled back by thing.py.\n\n    Because this involves creating transactions at the connection level, this\n    system implicitly relies on using the threadlocal strategy for the\n    sqlalchemy engines.\n\n    This system is a bit awkward, and should be replaced with something that\n    doesn't use module-globals when doing a cleanup of tdb_sql.\n\n    \"\"\"\n\n    def __init__(self):\n        self.transacting_engines = set()\n        self.transaction_begun = False\n\n    def begin(self):\n        \"\"\"Indicate that a transaction has begun.\"\"\"\n        self.transaction_begun = True\n\n    def add_engine(self, engine):\n        \"\"\"Add a database connection to the meta-transaction if active.\"\"\"\n        if not self.transaction_begun:\n            return\n\n        if engine not in self.transacting_engines:\n            engine.begin()\n            self.transacting_engines.add(engine)\n\n    def commit(self):\n        \"\"\"Commit the meta-transaction.\"\"\"\n        try:\n            for engine in self.transacting_engines:\n                engine.commit()\n        finally:\n            self._clear()\n\n    def rollback(self):\n        \"\"\"Roll back the meta-transaction.\"\"\"\n        try:\n            for engine in self.transacting_engines:\n                engine.rollback()\n        finally:\n            self._clear()\n\n    def _clear(self):\n        self.transacting_engines.clear()\n        self.transaction_begun = False\n\n\ntransactions = TransactionSet()\n\nMAX_THING_ID = 9223372036854775807 # http://www.postgresql.org/docs/8.3/static/datatype-numeric.html\nMIN_THING_ID = 0\n\ndef make_metadata(engine):\n    metadata = sa.MetaData(engine)\n    metadata.bind.echo = g.sqlprinting\n    return metadata\n\ndef create_table(table, index_commands=None):\n    t = table\n    if g.db_create_tables:\n        #@@hackish?\n        if not t.bind.has_table(t.name):\n            t.create(checkfirst = False)\n            if index_commands:\n                for i in index_commands:\n                    t.bind.execute(i)\n\ndef index_str(table, name, on, where = None, unique = False):\n    if unique:\n        index_str = 'create unique index'\n    else:\n        index_str = 'create index'\n    index_str += ' idx_%s_' % name\n    index_str += table.name\n    index_str += ' on '+ table.name + ' (%s)' % on\n    if where:\n        index_str += ' where %s' % where\n    return index_str\n\n\ndef index_commands(table, type):\n    commands = []\n\n    if type == 'thing':\n        commands.append(index_str(table, 'date', 'date'))\n        commands.append(index_str(table, 'deleted_spam', 'deleted, spam'))\n        commands.append(index_str(table, 'hot', 'hot(ups, downs, date), date'))\n        commands.append(index_str(table, 'score', 'score(ups, downs), date'))\n        commands.append(index_str(table, 'controversy', 'controversy(ups, downs), date'))\n    elif type == 'data':\n        commands.append(index_str(table, 'key_value', 'key, substring(value, 1, %s)' \\\n                                  % max_val_len))\n\n        #lower name\n        commands.append(index_str(table, 'lower_key_value', 'key, lower(value)',\n                                  where = \"key = 'name'\"))\n        #ip\n        commands.append(index_str(table, 'ip_network', 'ip_network(value)',\n                                  where = \"key = 'ip'\"))\n        #base_url\n        commands.append(index_str(table, 'base_url', 'base_url(lower(value))',\n                                  where = \"key = 'url'\"))\n    elif type == 'rel':\n        commands.append(index_str(table, 'thing1_name_date', 'thing1_id, name, date'))\n        commands.append(index_str(table, 'thing2_name_date', 'thing2_id, name, date'))\n        commands.append(index_str(table, 'thing1_id', 'thing1_id'))\n        commands.append(index_str(table, 'thing2_id', 'thing2_id'))\n        commands.append(index_str(table, 'name', 'name'))\n        commands.append(index_str(table, 'date', 'date'))\n    else:\n        print \"unknown index_commands() type %s\" % type\n\n    return commands\n\ndef get_type_table(metadata):\n    table = sa.Table(g.db_app_name + '_type', metadata,\n                     sa.Column('id', sa.Integer, primary_key = True),\n                     sa.Column('name', sa.String, nullable = False))\n    return table\n\ndef get_rel_type_table(metadata):\n    table = sa.Table(g.db_app_name + '_type_rel', metadata,\n                     sa.Column('id', sa.Integer, primary_key = True),\n                     sa.Column('type1_id', sa.Integer, nullable = False),\n                     sa.Column('type2_id', sa.Integer, nullable = False),\n                     sa.Column('name', sa.String, nullable = False))\n    return table\n\ndef get_thing_table(metadata, name):\n    table = sa.Table(g.db_app_name + '_thing_' + name, metadata,\n                     sa.Column('thing_id', sa.BigInteger, primary_key = True),\n                     sa.Column('ups', sa.Integer, default = 0, nullable = False),\n                     sa.Column('downs',\n                               sa.Integer,\n                               default = 0,\n                               nullable = False),\n                     sa.Column('deleted',\n                               sa.Boolean,\n                               default = False,\n                               nullable = False),\n                     sa.Column('spam',\n                               sa.Boolean,\n                               default = False,\n                               nullable = False),\n                     sa.Column('date',\n                               sa.DateTime(timezone = True),\n                               default = sa.func.now(),\n                               nullable = False))\n    table.thing_name = name\n    return table\n\ndef get_data_table(metadata, name):\n    data_table = sa.Table(g.db_app_name + '_data_' + name, metadata,\n                          sa.Column('thing_id', sa.BigInteger, nullable = False,\n                                    primary_key = True),\n                          sa.Column('key', sa.String, nullable = False,\n                                    primary_key = True),\n                          sa.Column('value', sa.String),\n                          sa.Column('kind', sa.String))\n    return data_table\n\ndef get_rel_table(metadata, name):\n    rel_table = sa.Table(g.db_app_name + '_rel_' + name, metadata,\n                         sa.Column('rel_id', sa.BigInteger, primary_key = True),\n                         sa.Column('thing1_id', sa.BigInteger, nullable = False),\n                         sa.Column('thing2_id', sa.BigInteger, nullable = False),\n                         sa.Column('name', sa.String, nullable = False),\n                         sa.Column('date', sa.DateTime(timezone = True),\n                                   default = sa.func.now(), nullable = False),\n                         sa.UniqueConstraint('thing1_id', 'thing2_id', 'name'))\n    rel_table.rel_name = name\n    return rel_table\n\n#get/create the type tables\ndef make_type_table():\n    metadata = make_metadata(dbm.type_db)\n    table = get_type_table(metadata)\n    create_table(table)\n    return table\ntype_table = make_type_table()\n\ndef make_rel_type_table():\n    metadata = make_metadata(dbm.relation_type_db)\n    table = get_rel_type_table(metadata)\n    create_table(table)\n    return table\nrel_type_table = make_rel_type_table()\n\n#lookup dicts\ntypes_id = {}\ntypes_name = {}\nrel_types_id = {}\nrel_types_name = {}\n\nclass ConfigurationError(Exception):\n    pass\n\ndef check_type(table, name, insert_vals):\n    # before hitting the db, check if we can get the type id from\n    # the ini file\n    type_id = predefined_type_ids.get(name)\n    if type_id:\n        return type_id\n    elif len(predefined_type_ids) > 0:\n        # flip the hell out if only *some* of the type ids are defined\n        raise ConfigurationError(\"Expected typeid for %s\" % name)\n\n    # check for type in type table, create if not existent\n    r = table.select(table.c.name == name).execute().fetchone()\n    if not r:\n        r = table.insert().execute(**insert_vals)\n        type_id = r.inserted_primary_key[0]\n    else:\n        type_id = r.id\n    return type_id\n\n#make the thing tables\ndef build_thing_tables():\n    for name, engines in dbm.things_iter():\n        type_id = check_type(type_table,\n                             name,\n                             dict(name = name))\n\n        tables = []\n        for engine in engines:\n            metadata = make_metadata(engine)\n\n            #make thing table\n            thing_table = get_thing_table(metadata, name)\n            create_table(thing_table,\n                         index_commands(thing_table, 'thing'))\n\n            #make data tables\n            data_table = get_data_table(metadata, name)\n            create_table(data_table,\n                         index_commands(data_table, 'data'))\n\n            tables.append((thing_table, data_table))\n\n        thing = storage(type_id = type_id,\n                        name = name,\n                        avoid_master_reads = dbm.avoid_master_reads.get(name),\n                        tables = tables)\n\n        types_id[type_id] = thing\n        types_name[name] = thing\nbuild_thing_tables()\n\n#make relation tables\ndef build_rel_tables():\n    for name, (type1_name, type2_name, engines) in dbm.rels_iter():\n        type1_id = types_name[type1_name].type_id\n        type2_id = types_name[type2_name].type_id\n        type_id = check_type(rel_type_table,\n                             name,\n                             dict(name = name,\n                                  type1_id = type1_id,\n                                  type2_id = type2_id))\n\n        tables = []\n        for engine in engines:\n            metadata = make_metadata(engine)\n\n            #relation table\n            rel_table = get_rel_table(metadata, name)\n            create_table(rel_table, index_commands(rel_table, 'rel'))\n\n            #make thing tables\n            rel_t1_table = get_thing_table(metadata, type1_name)\n            if type1_name == type2_name:\n                rel_t2_table = rel_t1_table\n            else:\n                rel_t2_table = get_thing_table(metadata, type2_name)\n\n            #build the data\n            rel_data_table = get_data_table(metadata, 'rel_' + name)\n            create_table(rel_data_table,\n                         index_commands(rel_data_table, 'data'))\n\n            tables.append((rel_table,\n                           rel_t1_table,\n                           rel_t2_table,\n                           rel_data_table))\n\n        rel = storage(type_id = type_id,\n                      type1_id = type1_id,\n                      type2_id = type2_id,\n                      avoid_master_reads = dbm.avoid_master_reads.get(name),\n                      name = name,\n                      tables = tables)\n\n        rel_types_id[type_id] = rel\n        rel_types_name[name] = rel\nbuild_rel_tables()\n\ndef get_write_table(tables):\n    if g.disallow_db_writes:\n        raise Exception(\"not so fast! writes are not allowed on this app.\")\n    else:\n        return tables[0]\n\ndef add_request_info(select):\n    def sanitize(txt):\n        return \"\".join(x if x.isalnum() else \".\"\n                       for x in filters._force_utf8(txt))\n\n    tb = simple_traceback(limit=12)\n    try:\n        if (hasattr(request, 'path') and\n            hasattr(request, 'ip') and\n            hasattr(request, 'user_agent')):\n            comment = '/*\\n%s\\n%s\\n%s\\n*/' % (\n                tb or \"\", \n                sanitize(request.fullpath),\n                sanitize(request.ip))\n            return select.prefix_with(comment)\n    except UnicodeDecodeError:\n        pass\n\n    return select\n\n\ndef get_table(kind, action, tables, avoid_master_reads = False):\n    if action == 'write':\n        #if this is a write, store the kind in the c.use_write_db dict\n        #so that all future requests use the write db\n        if not isinstance(c.use_write_db, dict):\n            c.use_write_db = {}\n        c.use_write_db[kind] = True\n\n        return get_write_table(tables)\n    elif action == 'read':\n        #check to see if we're supposed to use the write db again\n        if c.use_write_db and c.use_write_db.has_key(kind):\n            return get_write_table(tables)\n        else:\n            if avoid_master_reads and len(tables) > 1:\n                return dbm.get_read_table(tables[1:])\n            return dbm.get_read_table(tables)\n\n\ndef get_thing_table(type_id, action = 'read' ):\n    return get_table('t' + str(type_id), action,\n                     types_id[type_id].tables,\n                     avoid_master_reads = types_id[type_id].avoid_master_reads)\n\ndef get_rel_table(rel_type_id, action = 'read'):\n    return get_table('r' + str(rel_type_id), action,\n                     rel_types_id[rel_type_id].tables,\n                     avoid_master_reads = rel_types_id[rel_type_id].avoid_master_reads)\n\n\n#TODO does the type actually exist?\ndef make_thing(type_id, ups, downs, date, deleted, spam, id=None):\n    table = get_thing_table(type_id, action = 'write')[0]\n\n    params = dict(ups = ups, downs = downs,\n                  date = date, deleted = deleted, spam = spam)\n\n    if id:\n        params['thing_id'] = id\n\n    def do_insert(t):\n        transactions.add_engine(t.bind)\n        r = t.insert().execute(**params)\n        new_id = r.inserted_primary_key[0]\n        new_r = r.last_inserted_params()\n        for k, v in params.iteritems():\n            if new_r[k] != v:\n                raise CreationError, (\"There's shit in the plumbing. \" +\n                                      \"expected %s, got %s\" % (params,  new_r))\n        return new_id\n\n    try:\n        id = do_insert(table)\n        params['thing_id'] = id\n        return id\n    except sa.exc.DBAPIError, e:\n        if not 'IntegrityError' in e.message:\n            raise\n        # wrap the error to prevent db layer bleeding out\n        raise CreationError, \"Thing exists (%s)\" % str(params)\n\n\ndef set_thing_props(type_id, thing_id, **props):\n    table = get_thing_table(type_id, action = 'write')[0]\n\n    if not props:\n        return\n\n    #use real columns\n    def do_update(t):\n        transactions.add_engine(t.bind)\n        new_props = dict((t.c[prop], val) for prop, val in props.iteritems())\n        u = t.update(t.c.thing_id == thing_id, values = new_props)\n        u.execute()\n\n    do_update(table)\n\ndef incr_thing_prop(type_id, thing_id, prop, amount):\n    table = get_thing_table(type_id, action = 'write')[0]\n    \n    def do_update(t):\n        transactions.add_engine(t.bind)\n        u = t.update(t.c.thing_id == thing_id,\n                     values={t.c[prop] : t.c[prop] + amount})\n        u.execute()\n\n    do_update(table)\n\nclass CreationError(Exception): pass\n\n#TODO does the type exist?\n#TODO do the things actually exist?\ndef make_relation(rel_type_id, thing1_id, thing2_id, name, date=None):\n    table = get_rel_table(rel_type_id, action = 'write')[0]\n    transactions.add_engine(table.bind)\n    \n    if not date: date = datetime.now(g.tz)\n    try:\n        r = table.insert().execute(thing1_id = thing1_id,\n                                   thing2_id = thing2_id,\n                                   name = name, \n                                   date = date)\n        return r.inserted_primary_key[0]\n    except sa.exc.DBAPIError, e:\n        if not 'IntegrityError' in e.message:\n            raise\n        # wrap the error to prevent db layer bleeding out\n        raise CreationError, \"Relation exists (%s, %s, %s)\" % (name, thing1_id, thing2_id)\n        \n\ndef set_rel_props(rel_type_id, rel_id, **props):\n    t = get_rel_table(rel_type_id, action = 'write')[0]\n\n    if not props:\n        return\n\n    #use real columns\n    transactions.add_engine(t.bind)\n    new_props = dict((t.c[prop], val) for prop, val in props.iteritems())\n    u = t.update(t.c.rel_id == rel_id, values = new_props)\n    u.execute()\n\n\ndef py2db(val, return_kind=False):\n    if isinstance(val, bool):\n        val = 't' if val else 'f'\n        kind = 'bool'\n    elif isinstance(val, (str, unicode)):\n        kind = 'str'\n    elif isinstance(val, (int, float, long)):\n        kind = 'num'\n    elif val is None:\n        kind = 'none'\n    else:\n        kind = 'pickle'\n        val = pickle.dumps(val)\n\n    if return_kind:\n        return (val, kind)\n    else:\n        return val\n\ndef db2py(val, kind):\n    if kind == 'bool':\n        val = True if val is 't' else False\n    elif kind == 'num':\n        try:\n            val = int(val)\n        except ValueError:\n            val = float(val)\n    elif kind == 'none':\n        val = None\n    elif kind == 'pickle':\n        val = pickle.loads(val)\n\n    return val\n\n\ndef update_data(table, thing_id, **vals):\n    transactions.add_engine(table.bind)\n\n    u = table.update(sa.and_(table.c.thing_id == thing_id,\n                             table.c.key == sa.bindparam('_key')))\n\n    inserts = []\n    for key, val in vals.iteritems():\n        val, kind = py2db(val, return_kind=True)\n\n        uresult = u.execute(_key = key, value = val, kind = kind)\n        if not uresult.rowcount:\n            inserts.append({'key':key, 'value':val, 'kind': kind})\n\n    #do one insert\n    if inserts:\n        i = table.insert(values = dict(thing_id = thing_id))\n        i.execute(*inserts)\n\n\ndef create_data(table, thing_id, **vals):\n    transactions.add_engine(table.bind)\n\n    inserts = []\n    for key, val in vals.iteritems():\n        val, kind = py2db(val, return_kind=True)\n        inserts.append(dict(key=key, value=val, kind=kind))\n\n    if inserts:\n        i = table.insert(values=dict(thing_id=thing_id))\n        i.execute(*inserts)\n\n\ndef incr_data_prop(table, type_id, thing_id, prop, amount):\n    t = table\n    transactions.add_engine(t.bind)\n    u = t.update(sa.and_(t.c.thing_id == thing_id,\n                         t.c.key == prop),\n                 values={t.c.value : sa.cast(t.c.value, sa.Float) + amount})\n    u.execute()\n\ndef fetch_query(table, id_col, thing_id):\n    \"\"\"pull the columns from the thing/data tables for a list or single\n    thing_id\"\"\"\n    single = False\n\n    if not isinstance(thing_id, iters):\n        single = True\n        thing_id = (thing_id,)\n    \n    s = sa.select([table], id_col.in_(thing_id))\n\n    try:\n        r = add_request_info(s).execute().fetchall()\n    except Exception, e:\n        dbm.mark_dead(table.bind)\n        # this thread must die so that others may live\n        raise\n    return (r, single)\n\n#TODO specify columns to return?\ndef get_data(table, thing_id):\n    r, single = fetch_query(table, table.c.thing_id, thing_id)\n\n    #if single, only return one storage, otherwise make a dict\n    res = storage() if single else {}\n    for row in r:\n        val = db2py(row.value, row.kind)\n        stor = res if single else res.setdefault(row.thing_id, storage())\n        if single and row.thing_id != thing_id:\n            raise ValueError, (\"tdb_sql.py: there's shit in the plumbing.\" \n                               + \" got %s, wanted %s\" % (row.thing_id,\n                                                         thing_id))\n        stor[row.key] = val\n\n    return res\n\ndef set_thing_data(type_id, thing_id, brand_new_thing, **vals):\n    table = get_thing_table(type_id, action = 'write')[1]\n\n    if brand_new_thing:\n        return create_data(table, thing_id, **vals)\n    else:\n        return update_data(table, thing_id, **vals)\n\ndef incr_thing_data(type_id, thing_id, prop, amount):\n    table = get_thing_table(type_id, action = 'write')[1]\n    return incr_data_prop(table, type_id, thing_id, prop, amount)    \n\ndef get_thing_data(type_id, thing_id):\n    table = get_thing_table(type_id)[1]\n    return get_data(table, thing_id)\n\ndef get_thing(type_id, thing_id):\n    table = get_thing_table(type_id)[0]\n    r, single = fetch_query(table, table.c.thing_id, thing_id)\n\n    #if single, only return one storage, otherwise make a dict\n    res = {} if not single else None\n    for row in r:\n        stor = storage(ups = row.ups,\n                       downs = row.downs,\n                       date = row.date,\n                       deleted = row.deleted,\n                       spam = row.spam)\n        if single:\n            res = stor\n            # check that we got what we asked for\n            if row.thing_id != thing_id:\n                raise ValueError, (\"tdb_sql.py: there's shit in the plumbing.\" \n                                    + \" got %s, wanted %s\" % (row.thing_id,\n                                                              thing_id))\n        else:\n            res[row.thing_id] = stor\n    return res\n\ndef set_rel_data(rel_type_id, thing_id, brand_new_thing, **vals):\n    table = get_rel_table(rel_type_id, action = 'write')[3]\n\n    if brand_new_thing:\n        return create_data(table, thing_id, **vals)\n    else:\n        return update_data(table, thing_id, **vals)\n\ndef incr_rel_data(rel_type_id, thing_id, prop, amount):\n    table = get_rel_table(rel_type_id, action = 'write')[3]\n    return incr_data_prop(table, rel_type_id, thing_id, prop, amount)\n\ndef get_rel_data(rel_type_id, rel_id):\n    table = get_rel_table(rel_type_id)[3]\n    return get_data(table, rel_id)\n\ndef get_rel(rel_type_id, rel_id):\n    r_table = get_rel_table(rel_type_id)[0]\n    r, single = fetch_query(r_table, r_table.c.rel_id, rel_id)\n    \n    res = {} if not single else None\n    for row in r:\n        stor = storage(thing1_id = row.thing1_id,\n                       thing2_id = row.thing2_id,\n                       name = row.name,\n                       date = row.date)\n        if single:\n            res = stor\n        else:\n            res[row.rel_id] = stor\n    return res\n\ndef del_rel(rel_type_id, rel_id):\n    tables = get_rel_table(rel_type_id, action = 'write')\n    table = tables[0]\n    data_table = tables[3]\n\n    transactions.add_engine(table.bind)\n    transactions.add_engine(data_table.bind)\n\n    table.delete(table.c.rel_id == rel_id).execute()\n    data_table.delete(data_table.c.thing_id == rel_id).execute()\n\ndef sa_op(op):\n    #if BooleanOp\n    if isinstance(op, operators.or_):\n        return sa.or_(*[sa_op(o) for o in op.ops])\n    elif isinstance(op, operators.and_):\n        return sa.and_(*[sa_op(o) for o in op.ops])\n    elif isinstance(op, operators.not_):\n        return sa.not_(*[sa_op(o) for o in op.ops])\n\n    #else, assume op is an instance of op\n    if isinstance(op, operators.eq):\n        fn = lambda x,y: x == y\n    elif isinstance(op, operators.ne):\n        fn = lambda x,y: x != y\n    elif isinstance(op, operators.gt):\n        fn = lambda x,y: x > y\n    elif isinstance(op, operators.lt):\n        fn = lambda x,y: x < y\n    elif isinstance(op, operators.gte):\n        fn = lambda x,y: x >= y\n    elif isinstance(op, operators.lte):\n        fn = lambda x,y: x <= y\n    elif isinstance(op, operators.in_):\n        return sa.or_(op.lval.in_(op.rval))\n\n    rval = tup(op.rval)\n\n    if not rval:\n        return '2+2=5'\n    else:\n        return sa.or_(*[fn(op.lval, v) for v in rval])\n\ndef translate_sort(table, column_name, lval = None, rewrite_name = True):\n    if isinstance(lval, operators.query_func):\n        fn_name = lval.__class__.__name__\n        sa_func = getattr(sa.func, fn_name)\n        return sa_func(translate_sort(table,\n                                      column_name,\n                                      lval.lval,\n                                      rewrite_name))\n\n    if rewrite_name:\n        if column_name == 'id':\n            return table.c.thing_id\n        elif column_name == 'hot':\n            return sa.func.hot(table.c.ups, table.c.downs, table.c.date)\n        elif column_name == 'score':\n            return sa.func.score(table.c.ups, table.c.downs)\n        elif column_name == 'controversy':\n            return sa.func.controversy(table.c.ups, table.c.downs)\n    #else\n    return table.c[column_name]\n\n#TODO - only works with thing tables\ndef add_sort(sort, t_table, select):\n    sort = tup(sort)\n\n    prefixes = t_table.keys() if isinstance(t_table, dict) else None\n    #sort the prefixes so the longest come first\n    prefixes.sort(key = lambda x: len(x))\n    cols = []\n\n    def make_sa_sort(s):\n        orig_col = s.col\n\n        col = orig_col\n        if prefixes:\n            table = None\n            for k in prefixes:\n                if k and orig_col.startswith(k):\n                    table = t_table[k]\n                    col = orig_col[len(k):]\n            if table is None:\n                table = t_table[None]\n        else:\n            table = t_table\n\n        real_col = translate_sort(table, col)\n\n        #TODO a way to avoid overlap?\n        #add column for the sort parameter using the sorted name\n        select.append_column(real_col.label(orig_col))\n\n        #avoids overlap temporarily\n        select.use_labels = True\n\n        #keep track of which columns we added so we can add joins later\n        cols.append((real_col, table))\n\n        #default to asc\n        return (sa.desc(real_col) if isinstance(s, operators.desc)\n                else sa.asc(real_col))\n        \n    sa_sort = [make_sa_sort(s) for s in sort]\n\n    s = select.order_by(*sa_sort)\n\n    return s, cols\n\ndef translate_thing_value(rval):\n    if isinstance(rval, operators.timeago):\n        return sa.text(\"current_timestamp - interval '%s'\" % rval.interval)\n    else:\n        return rval\n\n#will assume parameters start with a _ for consistency\ndef find_things(type_id, sort, limit, offset, constraints):\n    table = get_thing_table(type_id)[0]\n    constraints = deepcopy(constraints)\n\n    s = sa.select([table.c.thing_id.label('thing_id')])\n    \n    for op in operators.op_iter(constraints):\n        #assume key starts with _\n        #if key.startswith('_'):\n        key = op.lval_name\n        op.lval = translate_sort(table, key[1:], op.lval)\n        op.rval = translate_thing_value(op.rval)\n\n    for op in constraints:\n        s.append_whereclause(sa_op(op))\n\n    if sort:\n        s, cols = add_sort(sort, {'_': table}, s)\n\n    if limit:\n        s = s.limit(limit)\n\n    if offset:\n        s = s.offset(offset)\n\n    try:\n        r = add_request_info(s).execute()\n    except Exception, e:\n        dbm.mark_dead(table.bind)\n        # this thread must die so that others may live\n        raise\n    return Results(r, lambda(row): row.thing_id)\n\ndef translate_data_value(alias, op):\n    lval = op.lval\n    need_substr = False if isinstance(lval, operators.query_func) else True\n    lval = translate_sort(alias, 'value', lval, False)\n\n    #add the substring func\n    if need_substr:\n        lval = sa.func.substring(lval, 1, max_val_len)\n    \n    op.lval = lval\n        \n    #convert the rval to db types\n    #convert everything to strings for pg8.3\n    op.rval = tuple(str(py2db(v)) for v in tup(op.rval))\n\n#TODO sort by data fields\n#TODO sort by id wants thing_id\ndef find_data(type_id, sort, limit, offset, constraints):\n    t_table, d_table = get_thing_table(type_id)\n    constraints = deepcopy(constraints)\n\n    used_first = False\n    s = None\n    need_join = False\n    have_data_rule = False\n    first_alias = d_table.alias()\n    s = sa.select([first_alias.c.thing_id.label('thing_id')])#, distinct=True)\n\n    for op in operators.op_iter(constraints):\n        key = op.lval_name\n        vals = tup(op.rval)\n\n        if key == '_id':\n            op.lval = first_alias.c.thing_id\n        elif key.startswith('_'):\n            need_join = True\n            op.lval = translate_sort(t_table, key[1:], op.lval)\n            op.rval = translate_thing_value(op.rval)\n        else:\n            have_data_rule = True\n            id_col = None\n            if not used_first:\n                alias = first_alias\n                used_first = True\n            else:\n                alias = d_table.alias()\n                id_col = first_alias.c.thing_id\n\n            if id_col is not None:\n                s.append_whereclause(id_col == alias.c.thing_id)\n            \n            s.append_column(alias.c.value.label(key))\n            s.append_whereclause(alias.c.key == key)\n            \n            #add the substring constraint if no other functions are there\n            translate_data_value(alias, op)\n\n    for op in constraints:\n        s.append_whereclause(sa_op(op))\n\n    if not have_data_rule:\n        raise Exception('Data queries must have at least one data rule.')\n\n    #TODO in order to sort by data columns, this is going to need to be smarter\n    if sort:\n        need_join = True\n        s, cols = add_sort(sort, {'_':t_table}, s)\n            \n    if need_join:\n        s.append_whereclause(first_alias.c.thing_id == t_table.c.thing_id)\n\n    if limit:\n        s = s.limit(limit)\n\n    if offset:\n        s = s.offset(offset)\n\n    try:\n        r = add_request_info(s).execute()\n    except Exception, e:\n        dbm.mark_dead(t_table.bind)\n        # this thread must die so that others may live\n        raise\n\n    return Results(r, lambda(row): row.thing_id)\n\n\ndef sort_thing_ids_by_data_value(type_id, thing_ids, value_name,\n        limit=None, desc=False):\n    \"\"\"Order thing_ids by the value of a data column.\"\"\"\n\n    thing_table, data_table = get_thing_table(type_id)\n\n    join = thing_table.join(data_table,\n        data_table.c.thing_id == thing_table.c.thing_id)\n\n    query = (sa.select(\n            [thing_table.c.thing_id],\n            sa.and_(\n                thing_table.c.thing_id.in_(thing_ids),\n                thing_table.c.deleted == False,\n                thing_table.c.spam == False,\n                data_table.c.key == value_name,\n            )\n        )\n        .select_from(join)\n    )\n\n    sort_column = data_table.c.value\n    if desc:\n        sort_column = sa.desc(sort_column)\n    query = query.order_by(sort_column)\n\n    if limit:\n        query = query.limit(limit)\n\n    rows = query.execute()\n\n    return Results(rows, lambda(row): row.thing_id)\n\n\ndef find_rels(ret_props, rel_type_id, sort, limit, offset, constraints):\n    tables = get_rel_table(rel_type_id)\n    r_table, t1_table, t2_table, d_table = tables\n    constraints = deepcopy(constraints)\n\n    t1_table, t2_table = t1_table.alias(), t2_table.alias()\n\n    prop_to_column = {\n        \"_rel_id\": r_table.c.rel_id.label('rel_id'),\n        \"_thing1_id\": r_table.c.thing1_id.label('thing1_id'),\n        \"_thing2_id\": r_table.c.thing2_id.label('thing2_id'),\n        \"_name\": r_table.c.name.label('name'),\n        \"_date\": r_table.c.date.label('date'),\n    }\n\n    if not ret_props:\n        valid_props = ', '.join(prop_to_column.keys())\n        raise ValueError(\"ret_props must contain at least one of \" + valid_props)\n\n    columns = []\n    for prop in ret_props:\n        if prop not in prop_to_column:\n            raise ValueError(\"ret_props got unrecognized %s\" % prop)\n\n        columns.append(prop_to_column[prop])\n\n    s = sa.select(columns)\n    need_join1 = ('thing1_id', t1_table)\n    need_join2 = ('thing2_id', t2_table)\n    joins_needed = set()\n\n    for op in operators.op_iter(constraints):\n        #vals = con.rval\n        key = op.lval_name\n        prefix = key[:4]\n        \n        if prefix in ('_t1_', '_t2_'):\n            #not a thing attribute\n            key = key[4:]\n\n            if prefix == '_t1_':\n                join = need_join1\n                joins_needed.add(join)\n            elif prefix == '_t2_':\n                join = need_join2\n                joins_needed.add(join)\n\n            table = join[1]\n            op.lval = translate_sort(table, key, op.lval)\n            op.rval = translate_thing_value(op.rval)\n            #ors = [sa_op(con, key, v) for v in vals]\n            #s.append_whereclause(sa.or_(*ors))\n\n        elif prefix.startswith('_'):\n            op.lval = r_table.c[key[1:]]\n\n        else:\n            alias = d_table.alias()\n            s.append_whereclause(r_table.c.rel_id == alias.c.thing_id)\n            s.append_column(alias.c.value.label(key))\n            s.append_whereclause(alias.c.key == key)\n\n            translate_data_value(alias, op)\n\n    for op in constraints:\n        s.append_whereclause(sa_op(op))\n\n    if sort:\n        s, cols = add_sort(\n            sort=sort,\n            t_table={'_': r_table, '_t1_': t1_table, '_t2_': t2_table},\n            select=s,\n        )\n\n        #do we need more joins?\n        for (col, table) in cols:\n            if table == need_join1[1]:\n                joins_needed.add(need_join1)\n            elif table == need_join2[1]:\n                joins_needed.add(need_join2)\n\n    for j in joins_needed:\n        col, table = j\n        s.append_whereclause(r_table.c[col] == table.c.thing_id)\n\n    if limit:\n        s = s.limit(limit)\n\n    if offset:\n        s = s.offset(offset)\n\n    try:\n        r = add_request_info(s).execute()\n    except Exception, e:\n        dbm.mark_dead(r_table.bind)\n        # this thread must die so that others may live\n        raise\n\n    def build_fn(row):\n        # return Storage objects with just the requested props\n        props = {}\n        for prop in ret_props:\n            db_prop = prop[1:]  # column name doesn't have _ prefix\n            props[prop] = getattr(row, db_prop)\n        return storage(**props)\n\n    return Results(sa_ResultProxy=r, build_fn=build_fn)\n\n\nif logging.getLogger('sqlalchemy').handlers:\n    logging.getLogger('sqlalchemy').handlers[0].formatter = log_format\n\n\n#inconsitencies:\n\n#relationships assume their thing and data tables are in the same\n#database. things don't make that assumption. in practice thing/data\n#tables always go together.\n#\n#we create thing tables for a relationship's things that aren't on the\n#same database as the relationship, although they're never used in\n#practice. we could remove a healthy chunk of code if we removed that.\n"
  },
  {
    "path": "r2/r2/lib/db/thing.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom copy import copy, deepcopy\nimport cPickle as pickle\nfrom datetime import datetime, timedelta\nimport hashlib\nimport itertools\nimport new\nimport sys\n\nfrom _pylibmc import MemcachedError\nfrom pylons import app_globals as g\n\nfrom r2.lib import amqp, hooks\nfrom r2.lib.db import tdb_sql as tdb, sorts, operators\nfrom r2.lib.sgm import sgm\nfrom r2.lib.utils import class_property, Results, tup, to36\n\n\nclass NotFound(Exception):\n    pass\n\n\nCreationError = tdb.CreationError\n\nthing_types = {}\nrel_types = {}\n\n\nclass SafeSetAttr:\n    def __init__(self, cls):\n        self.cls = cls\n\n    def __enter__(self):\n        self.cls.__safe__ = True\n\n    def __exit__(self, type, value, tb):\n        self.cls.__safe__ = False\n\n\nclass TdbTransactionContext(object):\n    def __enter__(self):\n        tdb.transactions.begin()\n\n    def __exit__(self, exc_type, exc_value, traceback):\n        if exc_type:\n            tdb.transactions.rollback()\n            raise\n        else:\n            tdb.transactions.commit()\n\n\nclass DataThing(object):\n    _base_props = ()\n    _int_props = ()\n    _data_int_props = ()\n    _int_prop_suffix = None\n    _defaults = {}\n    _essentials = ()\n    c = operators.Slots()\n    __safe__ = False\n    _cache = None\n    _cache_ttl = int(timedelta(hours=12).total_seconds())\n\n    def __init__(self):\n        safe_set_attr = SafeSetAttr(self)\n        with safe_set_attr:\n            self.safe_set_attr = safe_set_attr\n            self._dirties = {}\n            self._t = {}\n            self._created = False\n\n    #TODO some protection here?\n    def __setattr__(self, attr, val, make_dirty=True):\n        if attr.startswith('__') or self.__safe__:\n            object.__setattr__(self, attr, val)\n            return \n\n        if attr.startswith('_'):\n            #assume baseprops has the attr\n            if make_dirty and hasattr(self, attr):\n                old_val = getattr(self, attr)\n            object.__setattr__(self, attr, val)\n            if not attr in self._base_props:\n                return\n        else:\n            old_val = self._t.get(attr, self._defaults.get(attr))\n            self._t[attr] = val\n        if make_dirty and val != old_val:\n            self._dirties[attr] = (old_val, val)\n\n    def __setstate__(self, state):\n        # pylibmc's automatic unpicking will call __setstate__ if it exists.\n        # if we don't implement __setstate__ the check for existence will fail\n        # in an atypical (and not properly handled) way because we override\n        # __getattr__. the implementation provided here is identical to what\n        # would happen in the default unimplemented case.\n        self.__dict__ = state\n\n    def __getattr__(self, attr):\n        try:\n            return self._t[attr]\n        except KeyError:\n            try:\n                return self._defaults[attr]\n            except KeyError:\n                # attr didn't exist--continue on to error recovery below\n                pass\n\n        try:\n            _id = object.__getattribute__(self, \"_id\")\n        except AttributeError:\n            _id = \"???\"\n\n        try:\n            class_name = object.__getattribute__(self, \"__class__\").__name__\n        except AttributeError:\n            class_name = \"???\"\n\n        try:\n            id_str = \"%d\" % _id\n        except TypeError:\n            id_str = \"%r\" % _id\n\n        error_msg = \"{cls}({id}).{attr} not found\".format(\n            cls=class_name,\n            id=_id,\n            attr=attr,\n        )\n\n        raise AttributeError, error_msg\n\n    @classmethod\n    def _cache_prefix(cls):\n        return cls.__name__ + '_'\n\n    def _cache_key(self):\n        prefix = self._cache_prefix()\n        return \"{prefix}{id}\".format(prefix=prefix, id=self._id)\n\n    @classmethod\n    def get_things_from_db(cls, ids):\n        \"\"\"Read props from db and return id->thing dict.\"\"\"\n        raise NotImplementedError\n\n    @classmethod\n    def get_things_from_cache(cls, ids, stale=False, allow_local=True):\n        \"\"\"Read things from cache and return id->thing dict.\"\"\"\n        cache = cls._cache\n        prefix = cls._cache_prefix()\n        things_by_id = cache.get_multi(\n            ids, prefix=prefix, stale=stale, allow_local=allow_local,\n            stat_subname=cls.__name__)\n        return things_by_id\n\n    @classmethod\n    def write_things_to_cache(cls, things_by_id):\n        \"\"\"Write id->thing dict to cache.\n\n        Used to populate the cache after a cache miss/db read. To ensure we\n        don't clobber a write by another process (we don't have a lock) we use\n        add_multi to only set the values that don't exist.\n\n        \"\"\"\n\n        cache = cls._cache\n        prefix = cls._cache_prefix()\n        try:\n            cache.add_multi(things_by_id, prefix=prefix, time=cls._cache_ttl)\n        except MemcachedError as e:\n            g.log.warning(\"write_things_to_cache error: %s\", e)\n\n    def get_read_modify_write_lock(self):\n        \"\"\"Return the lock to be used when doing a read-modify-write.\n\n        When modifying a Thing we must read its current version from cache and\n        update that to avoid clobbering modifications made by other processes\n        after we first read the Thing.\n\n        \"\"\"\n\n        return g.make_lock(\"thing_commit\", 'commit_' + self._fullname)\n\n    def write_new_thing_to_db(self):\n        \"\"\"Write the new thing to db and return its id.\"\"\"\n        raise NotImplementedError\n\n    def write_props_to_db(self, props, data_props, brand_new_thing):\n        \"\"\"Write the props to db.\"\"\"\n        raise NotImplementedError\n\n    def write_changes_to_db(self, changes, brand_new_thing=False):\n        \"\"\"Write changes to db.\"\"\"\n        if not changes:\n            return\n\n        data_props = {}\n        props = {}\n        for prop, (old_value, new_value) in changes.iteritems():\n            if prop.startswith('_'):\n                props[prop[1:]] = new_value\n            else:\n                data_props[prop] = new_value\n\n        self.write_props_to_db(props, data_props, brand_new_thing)\n\n    def write_thing_to_cache(self, lock, brand_new_thing=False):\n        \"\"\"After modifying a thing write the entire object to cache.\n\n        The caller must either pass in the read_modify_write lock or be acting\n        for a newly created thing (that has therefore never been cached before).\n\n        \"\"\"\n\n        assert brand_new_thing or lock.have_lock\n\n        cache = self.__class__._cache\n        key = self._cache_key()\n        cache.set(key, self, time=self.__class__._cache_ttl)\n\n    def update_from_cache(self, lock):\n        \"\"\"Read the current value of thing from cache and update self.\n\n        To be used before writing cache to avoid clobbering changes made by a\n        different process. Must be called under write lock.\n\n        \"\"\"\n\n        assert lock.have_lock\n\n        # disallow reading from local cache because we want to pull in changes\n        # made by other processes since we first read this thing.\n        other_selfs = self.__class__.get_things_from_cache(\n            [self._id], allow_local=False)\n        if not other_selfs:\n            return\n        other_self = other_selfs[self._id]\n\n        # update base_props\n        for base_prop in self._base_props:\n            other_self_val = getattr(other_self, base_prop)\n            self.__setattr__(base_prop, other_self_val, make_dirty=False)\n\n        # update data_props\n        self._t = other_self._t\n\n        # reapply changes made to self\n        self_changes = self._dirties\n        self._dirties = {}\n        for data_prop, (old_val, new_val) in self_changes.iteritems():\n            setattr(self, data_prop, new_val)\n\n    @classmethod\n    def record_cache_write(cls, event, delta=1):\n        raise NotImplementedError\n\n    def _commit(self):\n        \"\"\"Write changes to db and write the full object to cache.\n\n        When writing to postgres we write only the changes. The data in postgres\n        is the canonical version.\n\n        For a few reasons (speed, decreased load on postgres, postgres\n        replication lag) we want to keep a perfectly consistent copy of the\n        thing in cache.\n\n        To achieve this we read the current value of the thing from cache to\n        pull in any changes made by other processes, apply our changes to the\n        thing, and finally set it in cache. This is done under lock to ensure\n        read/write safety.\n\n        If the cached thing is evicted or expires we must read from postgres.\n\n        Failure cases:\n        * Write to cache fails. The cache now contains stale/incorrect data. To\n          ensure we recover quickly TTLs should be set as low as possible\n          without overloading postgres.\n        * There is long replication lag and high cache pressure. When an object\n          is modified it is written to cache, but quickly evicted, The next\n          lookup might read from a postgres secondary before the changes have\n          been replicated there. To protect against this replication lag and\n          cache pressure should be monitored and kept at acceptable levels.\n        * Near simultaneous writes that create a logical inconsistency. Say\n          request 1 and request 2 both read state 0 of a Thing. Request 1\n          changes Thing.prop from True to False and writes to cache and\n          postgres. Request 2 examines the value of Thing.prop, sees that it is\n          True, and due to logic in the app sets Thing.prop_is_true to True and\n          writes to cache and postgres. Request 2 didn't clobber the change\n          made by request 1, but it made a logically incorrect change--the\n          resulting state is Thing.prop = False and Thing.prop_is_true = True.\n          Logic like this should be identified and avoided wherever possible, or\n          protected against using locks.\n\n        \"\"\"\n\n        if not self._created:\n            with TdbTransactionContext():\n                _id = self.write_new_thing_to_db()\n                self._id = _id\n                self._created = True\n\n                changes = self._dirties.copy()\n                self.write_changes_to_db(changes, brand_new_thing=True)\n                self._dirties.clear()\n\n            self.write_thing_to_cache(lock=None, brand_new_thing=True)\n            self.record_cache_write(event=\"create\")\n        else:\n            with self.get_read_modify_write_lock() as lock:\n                self.update_from_cache(lock)\n                if not self._dirty:\n                    return\n\n                with TdbTransactionContext():\n                    changes = self._dirties.copy()\n                    self.write_changes_to_db(changes, brand_new_thing=False)\n                    self._dirties.clear()\n\n                self.write_thing_to_cache(lock)\n                self.record_cache_write(event=\"modify\")\n\n        hooks.get_hook(\"thing.commit\").call(thing=self, changes=changes)\n\n    def _incr(self, prop, amt=1):\n        raise NotImplementedError\n\n    @property\n    def _id36(self):\n        return to36(self._id)\n\n    @class_property\n    def _fullname_prefix(cls):\n        return cls._type_prefix + to36(cls._type_id)\n\n    @classmethod\n    def _fullname_from_id36(cls, id36):\n        return cls._fullname_prefix + '_' + id36\n\n    @property\n    def _fullname(self):\n        return self._fullname_from_id36(self._id36)\n\n    @classmethod\n    def _byID(cls, ids, data=True, return_dict=True, stale=False,\n              ignore_missing=False):\n        # data props are ALWAYS loaded, data keyword is meaningless\n        ids, single = tup(ids, ret_is_single=True)\n\n        for x in ids:\n            if not isinstance(x, (int, long)):\n                raise ValueError('non-integer thing_id in %r' % ids)\n            if x > tdb.MAX_THING_ID:\n                raise NotFound('huge thing_id in %r' % ids)\n            elif x < tdb.MIN_THING_ID:\n                raise NotFound('negative thing_id in %r' % ids)\n\n        if not single and not ids:\n            if return_dict:\n                return {}\n            else:\n                return []\n\n        things_by_id = cls.get_things_from_cache(ids, stale=stale)\n        missing_ids = [_id\n            for _id in ids\n            if _id not in things_by_id\n        ]\n\n        if missing_ids:\n            from_db_by_id = cls.get_things_from_db(missing_ids)\n        else:\n            from_db_by_id = {}\n\n        if from_db_by_id:\n            cls.write_things_to_cache(from_db_by_id)\n            cls.record_cache_write(event=\"cache\", delta=len(from_db_by_id))\n\n        things_by_id.update(from_db_by_id)\n\n        # Check to see if we found everything we asked for\n        missing = [_id for _id in ids if _id not in things_by_id]\n        if missing and not ignore_missing:\n            raise NotFound, '%s %s' % (cls.__name__, missing)\n\n        if missing:\n            ids = [_id for _id in ids if _id not in missing]\n\n        if single:\n            return things_by_id[ids[0]] if ids else None\n        elif return_dict:\n            return things_by_id\n        else:\n            return filter(None, (things_by_id.get(_id) for _id in ids))\n\n    @classmethod\n    def _byID36(cls, id36s, return_dict = True, **kw):\n\n        id36s, single = tup(id36s, True)\n\n        # will fail if it's not a string\n        ids = [ int(x, 36) for x in id36s ]\n\n        things = cls._byID(ids, return_dict=True, **kw)\n        things = {thing._id36: thing for thing in things.itervalues()}\n\n        if single:\n            return things.values()[0]\n        elif return_dict:\n            return things\n        else:\n            return filter(None, (things.get(i) for i in id36s))\n\n    @classmethod\n    def _by_fullname(cls, names,\n                     return_dict = True, \n                     ignore_missing=False,\n                     **kw):\n        names, single = tup(names, True)\n\n        table = {}\n        lookup = {}\n        # build id list by type\n        for fullname in names:\n            try:\n                real_type, thing_id = fullname.split('_')\n                #distinguish between things and realtions\n                if real_type[0] == 't':\n                    type_dict = thing_types\n                elif real_type[0] == 'r':\n                    type_dict = rel_types\n                else:\n                    raise NotFound\n                real_type = type_dict[int(real_type[1:], 36)]\n                thing_id = int(thing_id, 36)\n                lookup[fullname] = (real_type, thing_id)\n                table.setdefault(real_type, []).append(thing_id)\n            except (KeyError, ValueError):\n                if single:\n                    raise NotFound\n\n        # lookup ids for each type\n        identified = {}\n        for real_type, thing_ids in table.iteritems():\n            i = real_type._byID(thing_ids, ignore_missing=ignore_missing, **kw)\n            identified[real_type] = i\n\n        # interleave types in original order of the name\n        res = []\n        for fullname in names:\n            if lookup.has_key(fullname):\n                real_type, thing_id = lookup[fullname]\n                thing = identified.get(real_type, {}).get(thing_id)\n                if not thing and ignore_missing:\n                    continue\n                res.append((fullname, thing))\n\n        if single:\n            return res[0][1] if res else None\n        elif return_dict:\n            return dict(res)\n        else:\n            return [x for i, x in res]\n\n    @property\n    def _dirty(self):\n        return bool(len(self._dirties))\n\n    @classmethod\n    def _query(cls, *a, **kw):\n        raise NotImplementedError()\n\n\nclass ThingMeta(type):\n    def __init__(cls, name, bases, dct):\n        if name == 'Thing' or hasattr(cls, '_nodb') and cls._nodb: return\n        if g.env == 'unit_test':\n            return\n\n        #TODO exceptions\n        cls._type_name = name.lower()\n        try:\n            cls._type_id = tdb.types_name[cls._type_name].type_id\n        except KeyError:\n            raise KeyError, 'is the thing database %s defined?' % name\n\n        global thing_types\n        thing_types[cls._type_id] = cls\n\n        super(ThingMeta, cls).__init__(name, bases, dct)\n    \n    def __repr__(cls):\n        return '<thing: %s>' % cls._type_name\n\nclass Thing(DataThing):\n    __metaclass__ = ThingMeta\n    _base_props = ('_ups', '_downs', '_date', '_deleted', '_spam')\n    _int_props = ('_ups', '_downs')\n    _type_prefix = 't'\n\n    is_votable = False\n\n    def __init__(self, ups = 0, downs = 0, date = None, deleted = False,\n                 spam = False, id = None, **attrs):\n        DataThing.__init__(self)\n\n        with self.safe_set_attr:\n            if id:\n                self._id = id\n                self._created = True\n\n            if not date:\n                date = datetime.now(g.tz)\n            else:\n                date = date.astimezone(g.tz)\n\n            self._ups = ups\n            self._downs = downs\n            self._date = date\n            self._deleted = deleted\n            self._spam = spam\n\n        #new way\n        for k, v in attrs.iteritems():\n            self.__setattr__(k, v, not self._created)\n\n    @classmethod\n    def record_cache_write(cls, event, delta=1):\n        name = cls.__name__.lower()\n        event_name = \"thing.{event}.{name}\".format(event=event, name=name)\n        g.stats.simple_event(event_name, delta)\n\n    def __repr__(self):\n        return '<%s %s>' % (self.__class__.__name__,\n                            self._id if self._created else '[unsaved]')\n\n    @classmethod\n    def get_things_from_db(cls, ids):\n        \"\"\"Read props from db and return id->thing dict.\"\"\"\n        props_by_id = tdb.get_thing(cls._type_id, ids)\n        data_props_by_id = tdb.get_thing_data(cls._type_id, ids)\n\n        things_by_id = {}\n        for _id, props in props_by_id.iteritems():\n            data_props = data_props_by_id.get(_id, {})\n            thing = cls(\n                ups=props.ups,\n                downs=props.downs,\n                date=props.date,\n                deleted=props.deleted,\n                spam=props.spam,\n                id=_id,\n            )\n            thing._t.update(data_props)\n\n            if not all(data_prop in thing._t for data_prop in cls._essentials):\n                # a Thing missing an essential prop is invalid\n                # this can happen if a process looks up the Thing as it's\n                # created but between when the props and the data props are\n                # written\n                g.log.error(\"%s missing essentials, got %s\", thing, thing._t)\n                g.stats.simple_event(\"thing.load.missing_essentials\")\n                continue\n\n            things_by_id[_id] = thing\n\n        return things_by_id\n\n    def write_new_thing_to_db(self):\n        \"\"\"Write the new thing to db and return its id.\"\"\"\n        assert not self._created\n\n        _id = tdb.make_thing(\n            type_id=self.__class__._type_id,\n            ups=self._ups,\n            downs=self._downs,\n            date=self._date,\n            deleted=self._deleted,\n            spam=self._spam,\n        )\n        return _id\n\n    def write_props_to_db(self, props, data_props, brand_new_thing):\n        \"\"\"Write the props to db.\"\"\"\n        if data_props:\n            tdb.set_thing_data(\n                type_id=self.__class__._type_id,\n                thing_id=self._id,\n                brand_new_thing=brand_new_thing,\n                **data_props\n            )\n\n        if props and not brand_new_thing:\n            # if the thing is brand new its props have just been written by\n            # write_new_thing_to_db\n            tdb.set_thing_props(\n                type_id=self.__class__._type_id,\n                thing_id=self._id,\n                **props\n            )\n\n    def _incr(self, prop, amt=1):\n        \"\"\"Increment self.prop.\"\"\"\n        assert not self._dirty\n\n        is_base_prop = prop in self._int_props\n        is_data_prop = (prop in self._data_int_props or\n            self._int_prop_suffix and prop.endswith(self._int_prop_suffix))\n        db_prop = prop[1:] if is_base_prop else prop\n\n        assert is_base_prop or is_data_prop\n\n        with self.get_read_modify_write_lock() as lock:\n            self.update_from_cache(lock)\n            old_val = getattr(self, prop)\n            new_val = old_val + amt\n\n            self.__setattr__(prop, new_val, make_dirty=False)\n\n            with TdbTransactionContext():\n                if is_base_prop:\n                    # can just incr a base prop because it must have been set\n                    # when the object was created\n                    tdb.incr_thing_prop(\n                        type_id=self.__class__._type_id,\n                        thing_id=self._id,\n                        prop=db_prop,\n                        amount=amt,\n                    )\n                elif (prop in self.__class__._defaults and\n                        self.__class__._defaults[prop] == old_val):\n                    # when updating a data prop from the default value assume\n                    # the value was never actually set so it's not safe to incr\n                    tdb.set_thing_data(\n                        type_id=self.__class__._type_id,\n                        thing_id=self._id,\n                        brand_new_thing=False,\n                        **{db_prop: new_val}\n                    )\n                else:\n                    tdb.incr_thing_data(\n                        type_id=self.__class__._type_id,\n                        thing_id=self._id,\n                        prop=db_prop,\n                        amount=amt,\n                    )\n\n                # write to cache within the transaction context so an exception\n                # will cause a transaction rollback\n                self.write_thing_to_cache(lock)\n        self.record_cache_write(event=\"incr\")\n\n    @property\n    def _age(self):\n        return datetime.now(g.tz) - self._date\n\n    @property\n    def _hot(self):\n        return sorts.hot(self._ups, self._downs, self._date)\n\n    @property\n    def _score(self):\n        return sorts.score(self._ups, self._downs)\n\n    @property\n    def _controversy(self):\n        return sorts.controversy(self._ups, self._downs)\n\n    @property\n    def _confidence(self):\n        return sorts.confidence(self._ups, self._downs)\n\n    @property\n    def num_votes(self):\n        return self._ups + self._downs\n\n    @property\n    def is_distinguished(self):\n        \"\"\"Return whether this Thing has a special flag on it (mod, admin, etc).\n\n        Done this way because distinguish is implemented in such a way where it\n        does not exist by default, but can also be set to a string of 'no',\n        which also means it is not distinguished.\n        \"\"\"\n        return getattr(self, 'distinguished', 'no') != 'no'\n\n    @classmethod\n    def _query(cls, *all_rules, **kw):\n        need_deleted = True\n        need_spam = True\n        #add default spam/deleted\n        rules = []\n        optimize_rules = kw.pop('optimize_rules', False)\n        for r in all_rules:\n            if not isinstance(r, operators.op):\n                continue\n            if r.lval_name == '_deleted':\n                need_deleted = False\n                # if the caller is explicitly unfiltering based on this column,\n                # we don't need this rule at all. taking this out can save us a\n                # join that is very expensive on pg9.\n                if optimize_rules and r.rval == (True, False):\n                    continue\n            elif r.lval_name == '_spam':\n                need_spam = False\n                # see above for explanation\n                if optimize_rules and r.rval == (True, False):\n                    continue\n            rules.append(r)\n\n        if need_deleted:\n            rules.append(cls.c._deleted == False)\n\n        if need_spam:\n            rules.append(cls.c._spam == False)\n\n        return Things(cls, *rules, **kw)\n\n    @classmethod\n    def sort_ids_by_data_value(cls, thing_ids, value_name,\n            limit=None, desc=False):\n        return tdb.sort_thing_ids_by_data_value(\n            cls._type_id, thing_ids, value_name, limit, desc)\n\n    def update_search_index(self, boost_only=False):\n        msg = {'fullname': self._fullname}\n        if boost_only:\n            msg['boost_only'] = True\n\n        amqp.add_item('search_changes', pickle.dumps(msg),\n                      message_id=self._fullname,\n                      delivery_mode=amqp.DELIVERY_TRANSIENT)\n\n\nclass RelationMeta(type):\n    def __init__(cls, name, bases, dct):\n        if name == 'RelationCls': return\n        #print \"checking relation\", name\n\n        if g.env == 'unit_test':\n            return\n\n        cls._type_name = name.lower()\n        try:\n            cls._type_id = tdb.rel_types_name[cls._type_name].type_id\n        except KeyError:\n            raise KeyError, 'is the relationship database %s defined?' % name\n\n        global rel_types\n        rel_types[cls._type_id] = cls\n\n        super(RelationMeta, cls).__init__(name, bases, dct)\n\n    def __repr__(cls):\n        return '<relation: %s>' % cls._type_name\n\ndef Relation(type1, type2):\n    class RelationCls(DataThing):\n        __metaclass__ = RelationMeta\n        if not (issubclass(type1, Thing) and issubclass(type2, Thing)):\n                raise TypeError('Relation types must be subclass of %s' % Thing)\n\n        _type1 = type1\n        _type2 = type2\n\n        _base_props = ('_thing1_id', '_thing2_id', '_name', '_date')\n        _type_prefix = Relation._type_prefix\n\n        _cache_ttl = int(timedelta(hours=1).total_seconds())\n\n        _enable_fast_query = True\n        _rel_cache = g.relcache\n        _rel_cache_ttl = int(timedelta(hours=1).total_seconds())\n\n        @classmethod\n        def get_things_from_db(cls, ids):\n            \"\"\"Read props from db and return id->rel dict.\"\"\"\n            props_by_id = tdb.get_rel(cls._type_id, ids)\n            data_props_by_id = tdb.get_rel_data(cls._type_id, ids)\n\n            rels_by_id = {}\n            for _id, props in props_by_id.iteritems():\n                data_props = data_props_by_id.get(_id, {})\n                rel = cls(\n                    thing1=props.thing1_id,\n                    thing2=props.thing2_id,\n                    name=props.name,\n                    date=props.date,\n                    id=_id,\n                )\n                rel._t.update(data_props)\n\n                if not all(data_prop in rel._t for data_prop in cls._essentials):\n                    # a Relation missing an essential prop is invalid\n                    # this can happen if a process looks up the Relation as it's\n                    # created but between when the props and the data props are\n                    # written\n                    g.log.error(\"%s missing essentials, got %s\", rel, rel._t)\n                    g.stats.simple_event(\"thing.load.missing_essentials\")\n                    continue\n\n                rels_by_id[_id] = rel\n\n            return rels_by_id\n\n        def write_new_thing_to_db(self):\n            \"\"\"Write the new rel to db and return its id.\"\"\"\n            assert not self._created\n\n            _id = tdb.make_relation(\n                rel_type_id=self.__class__._type_id,\n                thing1_id=self._thing1_id,\n                thing2_id=self._thing2_id,\n                name=self._name,\n                date=self._date,\n            )\n            return _id\n\n        def write_props_to_db(self, props, data_props, brand_new_thing):\n            \"\"\"Write the props to db.\"\"\"\n            if data_props:\n                tdb.set_rel_data(\n                    rel_type_id=self.__class__._type_id,\n                    thing_id=self._id,\n                    brand_new_thing=brand_new_thing,\n                    **data_props\n                )\n\n            if props and not brand_new_thing:\n                tdb.set_rel_props(\n                    rel_type_id=self.__class__._type_id,\n                    rel_id=self._id,\n                    **props\n                )\n\n        # eager_load means, do you load thing1 and thing2 immediately. It calls\n        # _byID(xxx).\n        @classmethod\n        def _byID_rel(cls, ids, data=True, return_dict=True,\n                      eager_load=False, thing_data=True, thing_stale=False,\n                      ignore_missing=False):\n            # data props are ALWAYS loaded, data and thing_data keywords are\n            # meaningless\n\n            ids, single = tup(ids, True)\n\n            bases = cls._byID(\n                ids, return_dict=True, ignore_missing=ignore_missing)\n\n            values = bases.values()\n\n            if values and eager_load:\n                load_things(values, stale=thing_stale)\n\n            if single:\n                return bases[ids[0]]\n            elif return_dict:\n                return bases\n            else:\n                return filter(None, (bases.get(i) for i in ids))\n\n        def __init__(self, thing1, thing2, name, date = None, id = None, **attrs):\n            DataThing.__init__(self)\n\n            def id_and_obj(in_thing):\n                if isinstance(in_thing, (int, long)):\n                    return in_thing\n                else:\n                    return in_thing._id\n\n            with self.safe_set_attr:\n                if id:\n                    self._id = id\n                    self._created = True\n\n                if not date:\n                    date = datetime.now(g.tz)\n                else:\n                    date = date.astimezone(g.tz)\n\n                #store the id, and temporarily store the actual object\n                #because we may need it later\n                self._thing1_id = id_and_obj(thing1)\n                self._thing2_id = id_and_obj(thing2)\n                self._name = name\n                self._date = date\n\n            for k, v in attrs.iteritems():\n                self.__setattr__(k, v, not self._created)\n\n        @classmethod\n        def record_cache_write(cls, event, delta=1):\n            name = cls.__name__.lower()\n            event_name = \"rel.{event}.{name}\".format(event=event, name=name)\n            g.stats.simple_event(event_name, delta)\n\n        def __getattr__(self, attr):\n            if attr == '_thing1':\n                return self._type1._byID(self._thing1_id)\n            elif attr == '_thing2':\n                return self._type2._byID(self._thing2_id)\n            elif attr.startswith('_t1'):\n                return getattr(self._thing1, attr[3:])\n            elif attr.startswith('_t2'):\n                return getattr(self._thing2, attr[3:])\n            else:\n                return DataThing.__getattr__(self, attr)\n\n        def __repr__(self):\n            return ('<%s %s: <%s %s> - <%s %s> %s>' %\n                    (self.__class__.__name__, self._name,\n                     self._type1.__name__, self._thing1_id,\n                     self._type2.__name__,self._thing2_id,\n                     '[unsaved]' if not self._created else '\\b'))\n\n        @classmethod\n        def _rel_cache_prefix(cls):\n            return \"rel:\"\n\n        @classmethod\n        def _rel_cache_key_from_parts(cls, thing1_id, thing2_id, name):\n            key = \"{prefix}{cls}_{t1}_{t2}_{name}\".format(\n                prefix=cls._rel_cache_prefix(),\n                cls=cls.__name__,\n                t1=str(thing1_id),\n                t2=str(thing2_id),\n                name=name,\n            )\n            return key\n\n        def _rel_cache_key(self):\n            return self._rel_cache_key_from_parts(\n                self._thing1_id,\n                self._thing2_id,\n                self._name,\n            )\n\n        def _commit(self):\n            DataThing._commit(self)\n\n            if self.__class__._enable_fast_query:\n                ttl = self.__class__._rel_cache_ttl\n                self._rel_cache.set(self._rel_cache_key(), self._id, time=ttl)\n\n        def _delete(self):\n            tdb.del_rel(self._type_id, self._id)\n\n            self._cache.delete(self._cache_key())\n\n            if self.__class__._enable_fast_query:\n                ttl = self.__class__._rel_cache_ttl\n                self._rel_cache.set(self._rel_cache_key(), None, time=ttl)\n\n            # temporarily set this property so the rest of this request\n            # knows it's deleted. save -> unsave, hide -> unhide\n            self._name = 'un' + self._name\n\n        @classmethod\n        def _fast_query(cls, thing1s, thing2s, name, data=True, eager_load=True,\n                        thing_data=True, thing_stale=False):\n            \"\"\"looks up all the relationships between thing1_ids and\n               thing2_ids and caches them\"\"\"\n\n            if not cls._enable_fast_query:\n                raise ValueError(\"%s._fast_query is disabled\" % cls.__name__)\n\n            cache_key_lookup = dict()\n\n            # We didn't find these keys in the cache, look them up in the\n            # database\n            def lookup_rel_ids(uncached_keys):\n                rel_ids = {}\n\n                # Lookup thing ids and name from cache key\n                t1_ids = set()\n                t2_ids = set()\n                names = set()\n                for cache_key in uncached_keys:\n                    (thing1, thing2, name) = cache_key_lookup[cache_key]\n                    t1_ids.add(thing1._id)\n                    t2_ids.add(thing2._id)\n                    names.add(name)\n\n                q = cls._query(\n                        cls.c._thing1_id == t1_ids,\n                        cls.c._thing2_id == t2_ids,\n                        cls.c._name == names)\n\n                for rel in q:\n                    rel_cache_key = cls._rel_cache_key_from_parts(\n                        rel._thing1_id,\n                        rel._thing2_id,\n                        str(rel._name),\n                    )\n                    rel_ids[rel_cache_key] = rel._id\n\n                for cache_key in uncached_keys:\n                    if cache_key not in rel_ids:\n                        rel_ids[cache_key] = None\n\n                return rel_ids\n\n            # make lookups for thing ids and names\n            thing1_dict = dict((t._id, t) for t in tup(thing1s))\n            thing2_dict = dict((t._id, t) for t in tup(thing2s))\n\n            names = map(str, tup(name))\n\n            # permute all of the pairs via cartesian product\n            rel_tuples = itertools.product(\n                thing1_dict.values(),\n                thing2_dict.values(),\n                names)\n\n            # create cache keys for all permutations and initialize lookup\n            for t in rel_tuples:\n                thing1, thing2, name = t\n                rel_cache_key = cls._rel_cache_key_from_parts(\n                    thing1._id,\n                    thing2._id,\n                    name,\n                )\n                cache_key_lookup[rel_cache_key] = t\n\n            # get the relation ids from the cache or query the db\n            res = sgm(\n                cache=cls._rel_cache,\n                keys=cache_key_lookup.keys(),\n                miss_fn=lookup_rel_ids,\n                time=cls._rel_cache_ttl,\n                ignore_set_errors=True,\n            )\n\n            # get the relation objects\n            rel_ids = {rel_id for rel_id in res.itervalues()\n                              if rel_id is not None}\n            rels = cls._byID_rel(\n                rel_ids,\n                eager_load=eager_load,\n                thing_stale=thing_stale)\n\n            # Takes aggregated results from cache and db (res) and transforms\n            # the values from ids to Relations.\n            res_obj = {}\n            for cache_key, rel_id in res.iteritems():\n                t = cache_key_lookup[cache_key]\n                rel = rels[rel_id] if rel_id is not None else None\n                res_obj[t] = rel\n\n            return res_obj\n\n        @classmethod\n        def _query(cls, *a, **kw):\n            return Relations(cls, *a, **kw)\n\n        @classmethod\n        def _simple_query(cls, props, *rules, **kw):\n            \"\"\"Return only the requested props rather than Relation objects.\"\"\"\n            return RelationsPropsOnly(cls, props, *rules, **kw)\n\n    return RelationCls\nRelation._type_prefix = 'r'\n\n\nclass Query(object):\n    def __init__(self, kind, *rules, **kw):\n        self._rules = []\n        self._kind = kind\n\n        self._read_cache = kw.get('read_cache')\n        self._write_cache = kw.get('write_cache')\n        self._cache_time = kw.get('cache_time', 86400)\n        self._limit = kw.get('limit')\n        self._offset = kw.get('offset')\n        self._stale = kw.get('stale', False)\n        self._sort = kw.get('sort', ())\n        self._filter_primary_sort_only = kw.get('filter_primary_sort_only', False)\n\n        self._filter(*rules)\n\n    def _setsort(self, sorts):\n        sorts = tup(sorts)\n        #make sure sorts are wrapped in a Sort obj\n        have_date = False\n        op_sorts = []\n        for s in sorts:\n            if not isinstance(s, operators.sort):\n                s = operators.asc(s)\n            op_sorts.append(s)\n            if s.col.endswith('_date'):\n                have_date = True\n        if op_sorts and not have_date:\n            op_sorts.append(operators.desc('_date'))\n\n        self._sort_param = op_sorts\n        return self\n\n    def _getsort(self):\n        return self._sort_param\n\n    _sort = property(_getsort, _setsort)\n\n    def _reverse(self):\n        for s in self._sort:\n            if isinstance(s, operators.asc):\n                s.__class__ = operators.desc\n            else:\n                s.__class__ = operators.asc\n\n    def _list(self, data=True):\n        return list(self)\n\n    def _dir(self, thing, reverse):\n        ors = []\n\n        # this fun hack lets us simplify the query on /r/all \n        # for postgres-9 compatibility. please remove it when\n        # /r/all is precomputed.\n        sorts = range(len(self._sort))\n        if self._filter_primary_sort_only:\n            sorts = [0]\n\n        #for each sort add and a comparison operator\n        for i in sorts:\n            s = self._sort[i]\n\n            if isinstance(s, operators.asc):\n                op = operators.gt\n            else:\n                op = operators.lt\n\n            if reverse:\n                op = operators.gt if op == operators.lt else operators.lt\n\n            #remember op takes lval and lval_name\n            ands = [op(s.col, s.col, getattr(thing, s.col))]\n\n            #for each sort up to the last add an equals operator\n            for j in range(0, i):\n                s = self._sort[j]\n                ands.append(thing.c[s.col] == getattr(thing, s.col))\n\n            ors.append(operators.and_(*ands))\n\n        return self._filter(operators.or_(*ors))\n\n    def _before(self, thing):\n        return self._dir(thing, True)\n\n    def _after(self, thing):\n        return self._dir(thing, False)\n\n    def _filter(*a, **kw):\n        raise NotImplementedError\n\n    def _cursor(*a, **kw):\n        raise NotImplementedError\n\n    def _cache_key(self):\n        fingerprint = str(self._sort) + str(self._limit) + str(self._offset)\n        if self._rules:\n            rules = copy(self._rules)\n            rules.sort()\n            for rule in rules:\n                fingerprint += str(rule)\n\n        cache_key = \"query:{kind}.{id}\".format(\n            kind=self._kind.__name__,\n            id=hashlib.sha1(fingerprint).hexdigest()\n        )\n        return cache_key\n\n    def _get_results(self):\n        things = self._cursor().fetchall()\n        return things\n\n    def get_from_cache(self, allow_local=True):\n        thing_fullnames = g.gencache.get(\n            self._cache_key(), allow_local=allow_local)\n        if thing_fullnames:\n            things = Thing._by_fullname(thing_fullnames, return_dict=False,\n                                        stale=self._stale)\n            return things\n\n    def set_to_cache(self, things):\n        thing_fullnames = [thing._fullname for thing in things]\n        g.gencache.set(self._cache_key(), thing_fullnames, self._cache_time)\n\n    def __iter__(self):\n        if self._read_cache:\n            things = self.get_from_cache()\n        else:\n            things = None\n\n        if things is None and not self._write_cache:\n            things = self._get_results()\n        elif things is None:\n            # it's not in the cache, and we have the power to\n            # update it, which we should do in a lock to prevent\n            # concurrent requests for the same data\n            with g.make_lock(\"thing_query\", \"lock_%s\" % self._cache_key()):\n                # see if it was set while we were waiting for our\n                # lock\n                if self._read_cache:\n                    things = self.get_from_cache(allow_local=False)\n                else:\n                    things = None\n\n                if things is None:\n                    things = self._get_results()\n                    self.set_to_cache(things)\n\n        for thing in things:\n            yield thing\n\n\nclass Things(Query):\n    def __init__(self, kind, *rules, **kw):\n        self._use_data = False\n        Query.__init__(self, kind, *rules, **kw)\n\n    def _filter(self, *rules):\n        for op in operators.op_iter(rules):\n            if not op.lval_name.startswith('_'):\n                self._use_data = True\n\n        self._rules += rules\n        return self\n\n    def _cursor(self):\n        if self._use_data:\n            find_fn = tdb.find_data\n        else:\n            find_fn = tdb.find_things\n\n        cursor = find_fn(\n            type_id=self._kind._type_id,\n            sort=self._sort,\n            limit=self._limit,\n            offset=self._offset,\n            constraints=self._rules,\n        )\n\n        #called on a bunch of rows to fetch their properties in batch\n        def row_fn(ids):\n            return self._kind._byID(ids, return_dict=False, stale=self._stale)\n\n        return Results(cursor, row_fn, do_batch=True)\n\ndef load_things(rels, stale=False):\n    rels = tup(rels)\n    kind = rels[0].__class__\n\n    t1_ids = set()\n    if kind._type1 == kind._type2:\n        t2_ids = t1_ids\n    else:\n        t2_ids = set()\n\n    for rel in rels:\n        t1_ids.add(rel._thing1_id)\n        t2_ids.add(rel._thing2_id)\n\n    kind._type1._byID(t1_ids, stale=stale)\n    if kind._type1 != kind._type2:\n        t2_items = kind._type2._byID(t2_ids, stale=stale)\n\nclass Relations(Query):\n    def __init__(self, kind, *rules, **kw):\n        self._eager_load = kw.get('eager_load')\n        self._thing_stale = kw.get('thing_stale')\n        Query.__init__(self, kind, *rules, **kw)\n\n    def _filter(self, *rules):\n        self._rules += rules\n        return self\n\n    def _eager(self, eager):\n        #load the things (id, ups, down, etc.)\n        self._eager_load = eager\n        return self\n\n    def _make_rel(self, rows):\n        rel_ids = [row._rel_id for row in rows]\n        rels = self._kind._byID(rel_ids, return_dict=False)\n        if rels and self._eager_load:\n            load_things(rels, stale=self._thing_stale)\n        return rels\n\n    def _cursor(self):\n        c = tdb.find_rels(\n            ret_props=[\"_rel_id\"],\n            rel_type_id=self._kind._type_id,\n            sort=self._sort,\n            limit=self._limit,\n            offset=self._offset,\n            constraints=self._rules,\n        )\n        return Results(c, self._make_rel, do_batch=True)\n\n\nclass RelationsPropsOnly(Relations):\n    def __init__(self, kind, props, *rules, **kw):\n        self.props = props\n        Relations.__init__(self, kind, *rules, **kw)\n\n    def _cursor(self):\n        c = tdb.find_rels(\n            ret_props=self.props,\n            rel_type_id=self._kind._type_id,\n            sort=self._sort,\n            limit=self._limit,\n            offset=self._offset,\n            constraints=self._rules,\n        )\n        return c\n\n    def _cache_key(self):\n        fingerprint = str(self._sort) + str(self._limit) + str(self._offset)\n        if self._rules:\n            rules = copy(self._rules)\n            rules.sort()\n            for rule in rules:\n                fingerprint += str(rule)\n        fingerprint += '|'.join(sorted(self.props))\n\n        cache_key = \"query:{kind}.{id}\".format(\n            kind=self._kind.__name__,\n            id=hashlib.sha1(fingerprint).hexdigest()\n        )\n        return cache_key\n\n    def _get_results(self):\n        rows = self._cursor().fetchall()\n        return rows\n\n    def get_from_cache(self, allow_local=True):\n        return g.gencache.get(self._cache_key(), allow_local=allow_local)\n\n    def set_to_cache(self, rows):\n        g.gencache.set(self._cache_key(), rows, self._cache_time)\n\n\nclass MultiCursor(object):\n    def __init__(self, *execute_params):\n        self._execute_params = execute_params\n        self._cursor = None\n\n    def fetchone(self):\n        if not self._cursor:\n            self._cursor = self._execute(*self._execute_params)\n            \n        return self._cursor.next()\n                \n    def fetchall(self):\n        if not self._cursor:\n            self._cursor = self._execute(*self._execute_params)\n\n        return [i for i in self._cursor]\n\nclass MergeCursor(MultiCursor):\n    def _execute(self, cursors, sorts):\n        #a \"pair\" is a (cursor, item, done) tuple\n        def safe_next(c):\n            try:\n                #hack to keep searching even if fetching a thing returns notfound\n                while True:\n                    try:\n                        return [c, c.fetchone(), False]\n                    except NotFound:\n                        #skips the broken item\n                        pass\n            except StopIteration:\n                return c, None, True\n\n        def undone(pairs):\n            return [p for p in pairs if not p[2]]\n\n        pairs = undone(safe_next(c) for c in cursors)\n\n        while pairs:\n            #only one query left, just dump it\n            if len(pairs) == 1:\n                c, item, done = pair = pairs[0]\n                while not done:\n                    yield item\n                    c, item, done = safe_next(c)\n                    pair[:] = c, item, done\n            else:\n                #by default, yield the first item\n                yield_pair = pairs[0]\n                for s in sorts:\n                    col = s.col\n                    #sort direction?\n                    max_fn = min if isinstance(s, operators.asc) else max\n\n                    #find the max (or min) val\n                    vals = [(getattr(i[1], col), i) for i in pairs]\n                    max_pair = vals[0]\n                    all_equal = True\n                    for pair in vals[1:]:\n                        if all_equal and pair[0] != max_pair[0]:\n                            all_equal = False\n                        max_pair = max_fn(max_pair, pair, key=lambda x: x[0])\n\n                    if not all_equal:\n                        yield_pair = max_pair[1]\n                        break\n\n                c, item, done = yield_pair\n                yield item\n                yield_pair[:] = safe_next(c)\n\n            pairs = undone(pairs)\n        raise StopIteration\n\n\nclass MultiQuery(Query):\n    def __init__(self, queries, *rules, **kw):\n        self._queries = queries\n        Query.__init__(self, None, *rules, **kw)\n\n        assert not self._read_cache\n        assert not self._write_cache\n\n    def get_from_cache(self):\n        raise NotImplementedError()\n\n    def set_to_cache(self):\n        raise NotImplementedError()\n\n    def _cursor(self):\n        raise NotImplementedError()\n\n    def _reverse(self):\n        for q in self._queries:\n            q._reverse()\n\n    def _setdata(self, data):\n        return\n\n    def _getdata(self):\n        return True\n\n    _data = property(_getdata, _setdata)\n\n    def _setsort(self, sorts):\n        for q in self._queries:\n            q._sort = deepcopy(sorts)\n\n    def _getsort(self):\n        if self._queries:\n            return self._queries[0]._sort\n\n    _sort = property(_getsort, _setsort)\n\n    def _filter(self, *rules):\n        for q in self._queries:\n            q._filter(*rules)\n\n    def _getrules(self):\n        return [q._rules for q in self._queries]\n\n    def _setrules(self, rules):\n        for q,r in zip(self._queries, rules):\n            q._rules = r\n\n    _rules = property(_getrules, _setrules)\n\n    def _getlimit(self):\n        return self._queries[0]._limit\n\n    def _setlimit(self, limit):\n        for q in self._queries:\n            q._limit = limit\n\n    _limit = property(_getlimit, _setlimit)\n\n\nclass Merge(MultiQuery):\n    def _cursor(self):\n        if (any(q._sort for q in self._queries) and\n            not reduce(lambda x,y: (x == y) and x,\n                      (q._sort for q in self._queries))):\n            raise \"The sorts should be the same\"\n\n        return MergeCursor((q._cursor() for q in self._queries),\n                           self._sort)\n\ndef MultiRelation(name, *relations):\n    rels_tmp = {}\n    for rel in relations:\n        t1, t2 = rel._type1, rel._type2\n        clsname = name + '_' + t1.__name__.lower() + '_' + t2.__name__.lower()\n        cls = new.classobj(clsname, (rel,), {'__module__':t1.__module__})\n        setattr(sys.modules[t1.__module__], clsname, cls)\n        rels_tmp[(t1, t2)] = cls\n\n    class MultiRelationCls(object):\n        c = operators.Slots()\n        rels = rels_tmp\n\n        def __init__(self, thing1, thing2, *a, **kw):\n            r = self.rel(thing1, thing2)\n            self.__class__ = r\n            self.__init__(thing1, thing2, *a, **kw)\n\n        @classmethod\n        def rel(cls, thing1, thing2):\n            t1 = thing1 if isinstance(thing1, ThingMeta) else thing1.__class__\n            t2 = thing2 if isinstance(thing2, ThingMeta) else thing2.__class__\n            return cls.rels[(t1, t2)]\n\n        @classmethod\n        def _query(cls, *rules, **kw):\n            #TODO it should be possible to send the rules and kw to\n            #the merge constructor\n            queries = [r._query(*rules, **kw) for r in cls.rels.values()]\n            if \"sort\" in kw:\n                print \"sorting MultiRelations is not supported\"\n            return Merge(queries)\n\n        @classmethod\n        def _fast_query(cls, sub, obj, name, data=True, eager_load=True,\n                        thing_data=True):\n            #divide into types\n            def type_dict(items):\n                types = {}\n                for i in items:\n                    types.setdefault(i.__class__, []).append(i)\n                return types\n\n            sub_dict = type_dict(tup(sub))\n            obj_dict = type_dict(tup(obj))\n\n            #for each pair of types, see if we have a query to send\n            res = {}\n            for types, rel in cls.rels.iteritems():\n                t1, t2 = types\n                if sub_dict.has_key(t1) and obj_dict.has_key(t2):\n                    res.update(rel._fast_query(\n                        sub_dict[t1], obj_dict[t2], name, eager_load=eager_load))\n\n            return res\n\n    return MultiRelationCls\n"
  },
  {
    "path": "r2/r2/lib/db/userrel.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport functools\nimport types\n\nfrom r2.lib.db.thing import CreationError\nfrom r2.lib.memoize import memoize\n\n\nclass UserRelManager(object):\n    \"\"\"Manages access to a relation between a type of thing and users.\"\"\"\n\n    def __init__(self, name, relation, permission_class):\n        self.name = name\n        self.relation = relation\n        self.permission_class = permission_class\n\n    def get(self, thing, user):\n        if user:\n            q = self.relation._fast_query([thing], [user], self.name)\n            rel = q.get((thing, user, self.name))\n            if rel:\n                rel._permission_class = self.permission_class\n            return rel\n\n    def add(self, thing, user, permissions=None, **attrs):\n        if self.get(thing, user):\n            return None\n        r = self.relation(thing, user, self.name, **attrs)\n        if permissions is not None:\n            r.set_permissions(permissions)\n\n        try:\n            r._commit()\n        except CreationError:\n            return None\n\n        r._permission_class = self.permission_class\n        return r\n\n    def remove(self, thing, user):\n        r = self.get(thing, user)\n        if r:\n            r._delete()\n            return True\n        return False\n\n    def mutate(self, thing, user, **attrs):\n        r = self.get(thing, user)\n        if r:\n            for k, v in attrs.iteritems():\n                setattr(r, k, v)\n            r._commit()\n            r._permission_class = self.permission_class\n            return r\n        else:\n            return self.add(thing, user, **attrs)\n\n    def ids(self, thing):\n        q = self.relation._simple_query(\n            [\"_thing2_id\"],\n            self.relation.c._thing1_id == thing._id,\n            self.relation.c._name == self.name,\n            sort='_date',\n        )\n        return [r._thing2_id for r in q]\n\n    def reverse_ids(self, user):\n        q = self.relation._simple_query(\n            [\"_thing1_id\"],\n            self.relation.c._thing2_id == user._id,\n            self.relation.c._name == self.name,\n        )\n        return [r._thing1_id for r in q]\n\n    def by_thing(self, thing):\n        q = self.relation._query(\n            self.relation.c._thing1_id == thing._id,\n            self.relation.c._name == self.name,\n            sort='_date',\n            data=True,\n        )\n\n        for r in q:\n            r._permission_class = self.permission_class\n            yield r\n\n\nclass MemoizedUserRelManager(UserRelManager):\n    \"\"\"Memoized manager for a relation to users.\"\"\"\n\n    def __init__(self, name, relation, permission_class,\n                 disable_ids_fn=False, disable_reverse_ids_fn=False):\n        super(MemoizedUserRelManager, self).__init__(\n            name, relation, permission_class)\n\n        self.disable_ids_fn = disable_ids_fn\n        self.disable_reverse_ids_fn = disable_reverse_ids_fn\n        self.ids_fn_name = self.name + '_ids'\n        self.reverse_ids_fn_name = 'reverse_' + self.name + '_ids'\n\n        sup = super(MemoizedUserRelManager, self)\n        self.ids = memoize(self.ids_fn_name)(sup.ids)\n        self.reverse_ids = memoize(self.reverse_ids_fn_name)(sup.reverse_ids)\n        self.add = self._update_caches_on_success(sup.add)\n        self.remove = self._update_caches_on_success(sup.remove)\n\n    def _update_caches(self, thing, user):\n        if not self.disable_ids_fn:\n            self.ids(thing, _update=True)\n        if not self.disable_reverse_ids_fn:\n            self.reverse_ids(user, _update=True)\n\n    def _update_caches_on_success(self, method):\n        @functools.wraps(method)\n        def wrapper(thing, user, *args, **kwargs):\n            try:\n                result = method(thing, user, *args, **kwargs)\n            except:\n                raise\n            else:\n                self._update_caches(thing, user)\n            return result\n        return wrapper\n\n\ndef UserRel(name, relation, disable_ids_fn=False, disable_reverse_ids_fn=False,\n            permission_class=None):\n    \"\"\"Mixin for Thing subclasses for managing a relation to users.\n\n    Provides the following suite of methods for a relation named \"<relation>\":\n\n      - is_<relation>(self, user) - whether user is related to self\n      - add_<relation>(self, user) - relates user to self\n      - remove_<relation>(self, user) - dissolves relation of user to self\n\n    This suite also usually includes (unless explicitly disabled):\n\n      - <relation>_ids(self) - list of user IDs related to self\n      - (static) reverse_<relation>_ids(user) - list of thing IDs user is\n          related to\n    \"\"\"\n    mgr = MemoizedUserRelManager(\n        name, relation, permission_class,\n        disable_ids_fn, disable_reverse_ids_fn)\n\n    class UR:\n        @classmethod\n        def _bind(cls, fn):\n            return types.UnboundMethodType(fn, None, cls)\n\n    setattr(UR, 'is_' + name, UR._bind(mgr.get))\n    setattr(UR, 'get_' + name, UR._bind(mgr.get))\n    setattr(UR, 'add_' + name, UR._bind(mgr.add))\n    setattr(UR, 'remove_' + name, UR._bind(mgr.remove))\n    setattr(UR, 'each_' + name, UR._bind(mgr.by_thing))\n    setattr(UR, name + '_permission_class', permission_class)\n    if not disable_ids_fn:\n        setattr(UR, mgr.ids_fn_name, UR._bind(mgr.ids))\n    if not disable_reverse_ids_fn:\n        setattr(UR, mgr.reverse_ids_fn_name, staticmethod(mgr.reverse_ids))\n\n    return UR\n\n\ndef MigratingUserRel(name, relation, disable_ids_fn=False,\n                     disable_reverse_ids_fn=False, permission_class=None):\n    \"\"\"\n    Replacement for UserRel to be used during migrations away from the system.\n\n    The resulting \"UserRel\" classes generated are to be used as standalones and\n    not included in Subreddit.__bases__.\n\n    \"\"\"\n\n    mgr = MemoizedUserRelManager(\n        name, relation, permission_class,\n        disable_ids_fn, disable_reverse_ids_fn)\n\n    class URM: pass\n\n    setattr(URM, 'is_' + name, mgr.get)\n    setattr(URM, 'get_' + name, mgr.get)\n    setattr(URM, 'add_' + name, staticmethod(mgr.add))\n    setattr(URM, 'remove_' + name, staticmethod(mgr.remove))\n    setattr(URM, 'each_' + name, mgr.by_thing)\n    setattr(URM, name + '_permission_class', permission_class)\n\n    if not disable_ids_fn:\n        setattr(URM, mgr.ids_fn_name, mgr.ids)\n\n    if not disable_reverse_ids_fn:\n        setattr(URM, mgr.reverse_ids_fn_name, staticmethod(mgr.reverse_ids))\n\n    return URM\n"
  },
  {
    "path": "r2/r2/lib/einhorn.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2016 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\"\"\"Run a Gunicorn WSGI container under Einhorn.\n\n[Einhorn] is a language/protocol-agnostic socket and worker manager. We're\nusing it elsewhere for Baseplate services (where something WSGI-specific\nwouldn't work on Thrift-based services) and its graceful reload logic is more\nfriendly than Gunicorn's*. However, for non-gevent WSGI, we still need\nsomething to parse HTTP and provide a WSGI container. Gunicorn is excellent at\nthis. This module adapts Gunicorn to work under Einhorn as a single worker\nprocess.\n\nTo run a paste-based application (like r2) under Einhorn using Gunicorn as the\nWSGI container, run this module. All of gunicorn's command line arguments are\nsupported, though some may be meaningless (like worker count) because Gunicorn\nisn't managing workers.\n\n    einhorn -n 4 -b 0.0.0.0:8080 python -m r2.lib.einhorn example.ini\n\n[Einhorn]: https://github.com/stripe/einhorn\n\n*: In particular, when told to gracefully reload, Gunicorn will gracefully\nterminate all workers immediately and then replace them. Einhorn starts up a\nnew worker, waits for it to acknowledge it is up and running, and then reaps an\nold worker.\n\n\"\"\"\nimport os\nimport signal\nimport sys\n\nfrom baseplate.server import einhorn\nfrom gunicorn import util\nfrom gunicorn.app.pasterapp import PasterApplication\nfrom gunicorn.workers.sync import SyncWorker\n\n\nclass EinhornSyncWorker(SyncWorker):\n    def __init__(self, cfg, app):\n        listener = einhorn.get_socket()\n        super(EinhornSyncWorker, self).__init__(\n            age=0,\n            ppid=os.getppid(),\n            sockets=[listener],\n            app=app,\n            timeout=None,\n            cfg=cfg,\n            log=cfg.logger_class(cfg),\n        )\n\n    def init_signals(self):\n        # reset signal handlers to defaults\n        [signal.signal(s, signal.SIG_DFL) for s in self.SIGNALS]\n\n        # einhorn will send SIGUSR2 to request a graceful shutdown\n        signal.signal(signal.SIGUSR2, self.start_graceful_shutdown)\n        signal.siginterrupt(signal.SIGUSR2, False)\n\n    def start_graceful_shutdown(self, signal_number, frame):\n        # gunicorn changed the meaning of its signals in 8124190. by being\n        # explicit what we mean here, we avoid woes when upgrading later.\n        self.alive = False\n\n\ndef run_gunicorn_worker():\n    if not einhorn.is_worker():\n        print >> sys.stderr, \"This process does not appear to be running under Einhorn.\"\n        sys.exit(1)\n\n    app = PasterApplication()\n    util._setproctitle(\"worker [%s]\" % app.cfg.proc_name)\n    worker = EinhornSyncWorker(app.cfg, app)\n    worker.init_process()\n\n\nif __name__ == \"__main__\":\n    run_gunicorn_worker()\n"
  },
  {
    "path": "r2/r2/lib/emailer.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom email import encoders\nfrom email.MIMEBase import MIMEBase\nfrom email.MIMEText import MIMEText\nfrom email.MIMEMultipart import MIMEMultipart\nfrom email.errors import HeaderParseError\nimport datetime\nimport traceback, sys, smtplib\n\nfrom pylons import tmpl_context as c\nfrom pylons import app_globals as g\nimport simplejson as json\n\nfrom r2.config import feature\nfrom r2.lib import hooks\nfrom r2.lib.ratelimit import SimpleRateLimit\nfrom r2.lib.utils import timeago\nfrom r2.models import Comment, Email, DefaultSR, Account, Award\nfrom r2.models.token import EmailVerificationToken, PasswordResetToken\n\n\ntrylater_hooks = hooks.HookRegistrar()\n\ndef _system_email(email, plaintext_body, kind, reply_to=\"\",\n        thing=None, from_address=g.feedback_email,\n        html_body=\"\", list_unsubscribe_header=\"\", user=None,\n        suppress_username=False):\n    \"\"\"\n    For sending email from the system to a user (reply address will be\n    feedback and the name will be reddit.com)\n    \"\"\"\n    if suppress_username:\n        user = None\n    elif user is None and c.user_is_loggedin:\n        user = c.user\n\n    Email.handler.add_to_queue(user,\n        email, g.domain, from_address, kind,\n        body=plaintext_body, reply_to=reply_to, thing=thing,\n    )\n\n\ndef _ads_email(body, from_name, kind):\n    \"\"\"\n    For sending email to ads\n    \"\"\"\n    Email.handler.add_to_queue(None, g.ads_email, from_name, g.ads_email,\n                               kind, body=body)\n\ndef _fraud_email(body, kind):\n    \"\"\"\n    For sending email to the fraud mailbox\n    \"\"\"\n    Email.handler.add_to_queue(None, g.fraud_email, g.domain, g.fraud_email,\n                               kind, body=body)\n\ndef _community_email(body, kind):\n    \"\"\"\n    For sending email to the community mailbox\n    \"\"\"\n    Email.handler.add_to_queue(c.user, g.community_email, g.domain, g.community_email,\n                               kind, body=body)\n\ndef verify_email(user, dest=None):\n    \"\"\"\n    For verifying an email address\n    \"\"\"\n    from r2.lib.pages import VerifyEmail\n    user.email_verified = False\n    user._commit()\n    Award.take_away(\"verified_email\", user)\n\n    token = EmailVerificationToken._new(user)\n    base = g.https_endpoint or g.origin\n    emaillink = base + '/verification/' + token._id\n    if dest:\n        emaillink += '?dest=%s' % dest\n    g.log.debug(\"Generated email verification link: \" + emaillink)\n\n    _system_email(user.email,\n                  VerifyEmail(user=user,\n                              emaillink = emaillink).render(style='email'),\n                  Email.Kind.VERIFY_EMAIL)\n\ndef password_email(user):\n    \"\"\"\n    For resetting a user's password.\n    \"\"\"\n    from r2.lib.pages import PasswordReset\n\n    user_reset_ratelimit = SimpleRateLimit(\n        name=\"email_reset_count_%s\" % user._id36,\n        seconds=int(datetime.timedelta(hours=12).total_seconds()),\n        limit=3,\n    )\n    if not user_reset_ratelimit.record_and_check():\n        return False\n\n    global_reset_ratelimit = SimpleRateLimit(\n        name=\"email_reset_count_global\",\n        seconds=int(datetime.timedelta(hours=1).total_seconds()),\n        limit=1000,\n    )\n    if not global_reset_ratelimit.record_and_check():\n        raise ValueError(\"password reset ratelimit exceeded\")\n\n    token = PasswordResetToken._new(user)\n    base = g.https_endpoint or g.origin\n    passlink = base + '/resetpassword/' + token._id\n    g.log.info(\"Generated password reset link: \" + passlink)\n    _system_email(user.email,\n                  PasswordReset(user=user,\n                                passlink=passlink).render(style='email'),\n                  Email.Kind.RESET_PASSWORD,\n                  user=user,\n                  )\n    return True\n\n@trylater_hooks.on('trylater.message_notification_email')\ndef message_notification_email(data):\n    \"\"\"Queues a system email for a new message notification.\"\"\"\n    from r2.lib.pages import MessageNotificationEmail\n\n    MAX_EMAILS_PER_DAY = 1000\n    MESSAGE_THROTTLE_KEY = 'message_notification_emails'\n\n    # If our counter's expired, initialize it again.\n    g.cache.add(MESSAGE_THROTTLE_KEY, 0, time=24*60*60)\n\n    for datum in data.itervalues():\n        datum = json.loads(datum)\n        user = Account._byID36(datum['to'], data=True)\n        comment = Comment._by_fullname(datum['comment'], data=True)\n\n        # In case a user has enabled the preference while it was enabled for\n        # them, but we've since turned it off.  We need to explicitly state the\n        # user because we're not in the context of an HTTP request from them.\n        if not feature.is_enabled('orangereds_as_emails', user=user):\n            continue\n\n        if g.cache.get(MESSAGE_THROTTLE_KEY) > MAX_EMAILS_PER_DAY:\n            raise Exception(\n                    'Message notification emails: safety limit exceeded!')\n\n        mac = generate_notification_email_unsubscribe_token(\n                datum['to'], user_email=user.email,\n                user_password_hash=user.password)\n        base = g.https_endpoint or g.origin\n        unsubscribe_link = base + '/mail/unsubscribe/%s/%s' % (datum['to'], mac)\n\n        templateData = {\n            'sender_username': datum.get('from', ''),\n            'comment': comment,\n            'permalink': datum['permalink'],\n            'unsubscribe_link': unsubscribe_link,\n        }\n        _system_email(user.email,\n                      MessageNotificationEmail(**templateData).render(style='email'),\n                      Email.Kind.MESSAGE_NOTIFICATION,\n                      from_address=g.notification_email)\n\n        g.stats.simple_event('email.message_notification.queued')\n        g.cache.incr(MESSAGE_THROTTLE_KEY)\n\ndef generate_notification_email_unsubscribe_token(user_id36, user_email=None,\n                                                  user_password_hash=None):\n    \"\"\"Generate a token used for one-click unsubscribe links for notification\n    emails.\n\n    user_id36: A base36-encoded user id.\n    user_email: The user's email.  Looked up if not provided.\n    user_password_hash: The hash of the user's password.  Looked up if not\n                        provided.\n    \"\"\"\n    import hashlib\n    import hmac\n\n    if (not user_email) or (not user_password_hash):\n        user = Account._byID36(user_id36, data=True)\n        if not user_email:\n            user_email = user.email\n        if not user_password_hash:\n            user_password_hash = user.password\n\n    return hmac.new(\n        g.secrets['email_notifications'],\n        user_id36 + user_email + user_password_hash,\n        hashlib.sha256).hexdigest()\n\ndef password_change_email(user):\n    \"\"\"Queues a system email for a password change notification.\"\"\"\n    from r2.lib.pages import PasswordChangeEmail\n\n    return _system_email(user.email,\n                         PasswordChangeEmail(user=user).render(style='email'),\n                         Email.Kind.PASSWORD_CHANGE,\n                         user=user,\n                         )\n\ndef email_change_email(user):\n    \"\"\"Queues a system email for a email change notification.\"\"\"\n    from r2.lib.pages import EmailChangeEmail\n\n    return _system_email(user.email,\n                         EmailChangeEmail(user=user).render(style='email'),\n                         Email.Kind.EMAIL_CHANGE)\n\ndef community_email(body, kind):\n    return _community_email(body, kind)\n\n\ndef ads_email(body, from_name=g.domain):\n    \"\"\"Queues an email to the Sales team.\"\"\"\n    return _ads_email(body, from_name, Email.Kind.ADS_ALERT)\n\ndef share(link, emails, from_name = \"\", reply_to = \"\", body = \"\"):\n    \"\"\"Queues a 'share link' email.\"\"\"\n    now = datetime.datetime.now(g.tz)\n    ival = now - timeago(g.new_link_share_delay)\n    date = max(now,link._date + ival)\n    Email.handler.add_to_queue(c.user, emails, from_name, g.share_reply,\n                               Email.Kind.SHARE, date = date,\n                               body = body, reply_to = reply_to,\n                               thing = link)\n\ndef send_queued_mail(test = False):\n    \"\"\"sends mail from the mail queue to smtplib for delivery.  Also,\n    on successes, empties the mail queue and adds all emails to the\n    sent_mail list.\"\"\"\n    from r2.lib.pages import Share, Mail_Opt\n    now = datetime.datetime.now(g.tz)\n    if not c.site:\n        c.site = DefaultSR()\n\n    clear = False\n    if not test:\n        session = smtplib.SMTP(g.smtp_server)\n    def sendmail(email):\n        try:\n            mimetext = email.to_MIMEText()\n            if mimetext is None:\n                print (\"Got None mimetext for email from %r and to %r\"\n                       % (email.fr_addr, email.to_addr))\n            if test:\n                print mimetext.as_string()\n            else:\n                session.sendmail(email.fr_addr, email.to_addr,\n                                 mimetext.as_string())\n                email.set_sent(rejected = False)\n        # exception happens only for local recipient that doesn't exist\n        except (smtplib.SMTPRecipientsRefused, smtplib.SMTPSenderRefused,\n                UnicodeDecodeError, AttributeError, HeaderParseError):\n            # handle error and print, but don't stall the rest of the queue\n            print \"Handled error sending mail (traceback to follow)\"\n            traceback.print_exc(file = sys.stdout)\n            email.set_sent(rejected = True)\n\n\n    try:\n        for email in Email.get_unsent(now):\n            clear = True\n\n            should_queue = email.should_queue()\n            # check only on sharing that the mail is invalid\n            if email.kind == Email.Kind.SHARE:\n                if should_queue:\n                    email.body = Share(username = email.from_name(),\n                                       msg_hash = email.msg_hash,\n                                       link = email.thing,\n                                       body =email.body).render(style = \"email\")\n                else:\n                    email.set_sent(rejected = True)\n                    continue\n            elif email.kind == Email.Kind.OPTOUT:\n                email.body = Mail_Opt(msg_hash = email.msg_hash,\n                                      leave = True).render(style = \"email\")\n            elif email.kind == Email.Kind.OPTIN:\n                email.body = Mail_Opt(msg_hash = email.msg_hash,\n                                      leave = False).render(style = \"email\")\n            # handle unknown types here\n            elif not email.body:\n                print (\"Rejecting email with an empty body from %r and to %r\"\n                       % (email.fr_addr, email.to_addr))\n                email.set_sent(rejected = True)\n                continue\n            sendmail(email)\n\n    finally:\n        if not test:\n            session.quit()\n\n    # clear is true if anything was found and processed above\n    if clear:\n        Email.handler.clear_queue(now)\n\n\n\ndef opt_out(msg_hash):\n    \"\"\"Queues an opt-out email (i.e., a confirmation that the email\n    address has been opted out of receiving any future mail)\"\"\"\n    email, added =  Email.handler.opt_out(msg_hash)\n    if email and added:\n        _system_email(email, \"\", Email.Kind.OPTOUT)\n    return email, added\n\ndef opt_in(msg_hash):\n    \"\"\"Queues an opt-in email (i.e., that the email has been removed\n    from our opt out list)\"\"\"\n    email, removed =  Email.handler.opt_in(msg_hash)\n    if email and removed:\n        _system_email(email, \"\", Email.Kind.OPTIN)\n    return email, removed\n\n\ndef _promo_email(thing, kind, body = \"\", **kw):\n    from r2.lib.pages import Promo_Email\n    a = Account._byID(thing.author_id, True)\n\n    if not a.email:\n        return\n\n    body = Promo_Email(link = thing, kind = kind,\n                       body = body, **kw).render(style = \"email\")\n    return _system_email(a.email, body, kind, thing = thing,\n                         reply_to = g.selfserve_support_email,\n                         suppress_username=True)\n\n\ndef new_promo(thing):\n    return _promo_email(thing, Email.Kind.NEW_PROMO)\n\ndef promo_total_budget(thing, total_budget_dollars, start_date):\n    return _promo_email(thing, Email.Kind.BID_PROMO,\n        total_budget_dollars = total_budget_dollars, start_date = start_date)\n\ndef accept_promo(thing):\n    return _promo_email(thing, Email.Kind.ACCEPT_PROMO)\n\ndef reject_promo(thing, reason = \"\"):\n    return _promo_email(thing, Email.Kind.REJECT_PROMO, reason)\n\ndef edited_live_promo(thing):\n    return _promo_email(thing, Email.Kind.EDITED_LIVE_PROMO)\n\ndef queue_promo(thing, total_budget_dollars, trans_id):\n    return _promo_email(thing, Email.Kind.QUEUED_PROMO,\n        total_budget_dollars=total_budget_dollars, trans_id = trans_id)\n\ndef live_promo(thing):\n    return _promo_email(thing, Email.Kind.LIVE_PROMO)\n\ndef finished_promo(thing):\n    return _promo_email(thing, Email.Kind.FINISHED_PROMO)\n\n\ndef refunded_promo(thing):\n    return _promo_email(thing, Email.Kind.REFUNDED_PROMO)\n\n\ndef void_payment(thing, campaign, total_budget_dollars, reason):\n    return _promo_email(thing, Email.Kind.VOID_PAYMENT, campaign=campaign,\n                        total_budget_dollars=total_budget_dollars,\n                        reason=reason)\n\n\ndef fraud_alert(body):\n    return _fraud_email(body, Email.Kind.FRAUD_ALERT)\n\ndef suspicious_payment(user, link):\n    from r2.lib.pages import SuspiciousPaymentEmail\n\n    body = SuspiciousPaymentEmail(user, link).render(style=\"email\")\n    kind = Email.Kind.SUSPICIOUS_PAYMENT\n\n    return _fraud_email(body, kind)\n\n\ndef send_html_email(to_addr, from_addr, subject, html,\n        subtype=\"html\", attachments=None):\n    from r2.lib.filters import _force_utf8\n    if not attachments:\n        attachments = []\n\n    msg = MIMEMultipart()\n    msg.attach(MIMEText(_force_utf8(html), subtype))\n    msg[\"Subject\"] = subject\n    msg[\"From\"] = from_addr\n    msg[\"To\"] = to_addr\n\n    for attachment in attachments:\n        part = MIMEBase('application', \"octet-stream\")\n        part.set_payload(attachment['contents'])\n        encoders.encode_base64(part)\n        part.add_header('Content-Disposition', 'attachment',\n            filename=attachment['name'])\n        msg.attach(part)\n\n    session = smtplib.SMTP(g.smtp_server)\n    session.sendmail(from_addr, to_addr, msg.as_string())\n    session.quit()\n\ntrylater_hooks.register_all()\n"
  },
  {
    "path": "r2/r2/lib/embeds.py",
    "content": "from datetime import datetime\nimport math\nfrom pylons import request\nfrom pylons import tmpl_context as c\nfrom pylons import app_globals as g\nfrom pylons.controllers.util import abort\nimport pytz\n\nfrom r2.controllers.reddit_base import UnloggedUser\nfrom r2.lib import js\nfrom r2.models import Account, NotFound\nfrom r2.models.subreddit import Subreddit\n\n# Note: This template is shared between python and javascript. See underscore\n# templating in embed.js for more info. (Specific note: Only %()s is supported\n# presently to use underscore templating.)\n_COMMENT_EMBED_TEMPLATE = (\n    '<div class=\"reddit-embed\" '\n        'data-embed-media=\"%(media)s\" '\n        'data-embed-parent=\"%(parent)s\" '\n        'data-embed-live=\"%(live)s\" '\n        'data-embed-created=\"%(created)s\">'\n        '<a href=\"%(comment)s\">Comment</a> '\n        'from discussion '\n        '<a href=\"%(link)s\">%(title)s</a>.'\n    '</div>'\n)\n\n\ndef get_inject_template(omitscript=False):\n    template = _COMMENT_EMBED_TEMPLATE\n    if not omitscript:\n        script_urls = js.src(\"comment-embed\", absolute=True, mangle_name=False)\n        scripts = \"\".join('<script%s src=\"%s\"></script>' % (\n            ' async' if len(script_urls) == 1 else '',\n            script_url\n        ) for script_url in script_urls)\n        template += scripts\n    return template\n\n\ndef edited_after(thing, iso_timestamp, showedits):\n    if not thing:\n        return False\n\n    if not isinstance(getattr(thing, \"editted\", False), datetime):\n        return False\n\n    try:\n        created = datetime.strptime(iso_timestamp, \"%Y-%m-%dT%H:%M:%S.%fZ\")\n    except ValueError:\n        return not showedits\n\n    created = created.replace(tzinfo=pytz.utc)\n\n    return created < thing.editted\n\n\ndef prepare_embed_request():\n    \"\"\"Given a request, determine if we are embedding. If so, prepare the\n    request for embedding.\n\n    Returns the value of the embed GET parameter.\n    \"\"\"\n    is_embed = request.GET.get('embed')\n\n    if not is_embed:\n        return None\n\n    if request.host != g.media_domain:\n        # don't serve up untrusted content except on our\n        # specifically untrusted domain\n        abort(404)\n\n    c.allow_framing = True\n\n    return is_embed\n\n\ndef set_up_comment_embed(sr, thing, showedits):\n    try:\n        author = Account._byID(thing.author_id) if thing.author_id else None\n    except NotFound:\n        author = None\n\n    iso_timestamp = request.GET.get(\"created\", \"\")\n\n    c.embed_config = {\n        \"eventtracker_url\": g.eventtracker_url or \"\",\n        \"anon_eventtracker_url\": g.anon_eventtracker_url or \"\",\n        \"event_clicktracker_url\": g.event_clicktracker_url or \"\",\n        \"created\": iso_timestamp,\n        \"showedits\": showedits,\n        \"thing\": {\n            \"id\": thing._id,\n            \"sr_id\": sr._id,\n            \"sr_name\": sr.name,\n            \"edited\": edited_after(thing, iso_timestamp, showedits),\n            \"deleted\": thing.deleted or author._deleted,\n        },\n        \"comment_max_height\": 200,\n    }\n\n    c.render_style = \"iframe\"\n    c.user = UnloggedUser([c.lang])\n    c.user_is_loggedin = False\n    c.forced_loggedout = True\n\n\ndef is_embed():\n    return c.render_style == \"iframe\"\n"
  },
  {
    "path": "r2/r2/lib/emr_helpers.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom copy import copy\n\nfrom pylons import app_globals as g\n\nfrom r2.lib.memoize import memoize\n\nLIVE_STATES = ['RUNNING', 'STARTING', 'WAITING', 'BOOTSTRAPPING']\nCOMPLETED = 'COMPLETED'\nPENDING = 'PENDING'\nNOTFOUND = 'NOTFOUND'\n\n\ndef get_live_clusters(emr_connection):\n    ret = emr_connection.list_clusters(cluster_states=LIVE_STATES)\n    return ret.clusters or []\n\n\n@memoize('get_step_states', time=60, timeout=60)\ndef get_step_states(emr_connection, jobflowid):\n    \"\"\"Return the names and states of all steps in the jobflow.\n\n    Memoized to prevent ratelimiting.\n\n    \"\"\"\n\n    ret = emr_connection.list_steps(jobflowid)\n    steps = []\n    steps.extend(ret.steps)\n    while hasattr(ret, \"marker\"):\n        ret = emr_connection.list_steps(jobflowid, marker=ret.marker)\n        steps.extend(ret.steps)\n\n    ret = []\n    for step in steps:\n        start_str = step.status.timeline.creationdatetime\n        ret.append((step.name, step.status.state, start_str))\n    return ret\n\n\ndef get_step_state(emr_connection, jobflowid, step_name, update=False):\n    \"\"\"Return the state of a step.\n\n    If jobflowid/step_name combination is not unique this will return the state\n    of the most recent step.\n\n    \"\"\"\n\n    g.reset_caches()\n    steps = get_step_states(emr_connection, jobflowid, _update=update)\n\n    for name, state, start in sorted(steps, key=lambda t: t[2], reverse=True):\n        if name == step_name:\n            return state\n    else:\n        return NOTFOUND\n\n\ndef get_jobflow_id(emr_connection, name):\n    \"\"\"Return id of the live cluster with specified name.\"\"\"\n    ret = emr_connection.list_clusters(cluster_states=LIVE_STATES)\n    clusters = ret.clusters\n\n    try:\n        # clusters appear to be ordered by creation time\n        return [cluster.id for cluster in clusters if cluster.name == name][0]\n    except IndexError:\n        return\n\n\ndef terminate_jobflow(emr_connection, jobflow_name):\n    jobflow_id = get_jobflow_id(emr_connection, jobflow_name)\n    if jobflow_id:\n        emr_connection.terminate_jobflow(jobflow_id)\n\n\ndef modify_slave_count(emr_connection, jobflow_name, num_slaves=1):\n    jobflow_id = get_jobflow_id(emr_connection, jobflow_name)\n    if not jobflow_id:\n        return\n\n    ret = emr_connection.list_instance_groups(jobflow_id)\n\n    try:\n        instancegroup = [i for i in ret.instancegroups if i.name == \"slave\"][0]\n    except IndexError:\n        # no slave instance group\n        return\n\n    if instancegroup.requestedinstancecount != num_slaves:\n        return\n\n    msg = 'Modifying slave instance count of %s (%s -> %s)'\n    print msg % (jobflow_name, instancegroup.requestedinstancecount, num_slaves)\n    emr_connection.modify_instance_groups(instancegroup.id, num_slaves)\n\n\nclass EmrJob(object):\n    def __init__(self, emr_connection, name, steps=[], setup_steps=[],\n                 bootstrap_actions=[], log_uri=None, keep_alive=True,\n                 ec2_keyname=None, hadoop_version='1.0.3',\n                 ami_version='latest', master_instance_type='m1.small',\n                 slave_instance_type='m1.small', num_slaves=1,\n                 visible_to_all_users=True, job_flow_role=None,\n                 service_role=None, tags=None):\n\n        self.jobflowid = None\n        self.conn = emr_connection\n        self.name = name\n        self.steps = steps\n        self.setup_steps = setup_steps\n        self.bootstrap_actions = bootstrap_actions\n        self.log_uri = log_uri\n        self.enable_debugging = bool(log_uri)\n        self.keep_alive = keep_alive\n        self.ec2_keyname = ec2_keyname\n        self.hadoop_version = hadoop_version\n        self.ami_version = ami_version\n        self.master_instance_type = master_instance_type\n        self.slave_instance_type = slave_instance_type\n        self.num_instances = num_slaves + 1\n        self.visible_to_all_users = visible_to_all_users\n        self.job_flow_role = job_flow_role\n        self.service_role = service_role\n        self.tags = tags or {}\n\n    def run(self):\n        steps = copy(self.setup_steps)\n        steps.extend(self.steps)\n\n        job_flow_args = dict(name=self.name,\n            steps=steps, bootstrap_actions=self.bootstrap_actions,\n            keep_alive=self.keep_alive, ec2_keyname=self.ec2_keyname,\n            hadoop_version=self.hadoop_version, ami_version=self.ami_version,\n            master_instance_type=self.master_instance_type,\n            slave_instance_type=self.slave_instance_type,\n            num_instances=self.num_instances,\n            enable_debugging=self.enable_debugging,\n            log_uri=self.log_uri,\n            visible_to_all_users=self.visible_to_all_users,\n            job_flow_role=self.job_flow_role, service_role=self.service_role)\n\n        self.jobflowid = self.conn.run_jobflow(**job_flow_args)\n        if self.tags:\n            assert isinstance(self.tags, dict)\n            # The EMR Cluster name is distinct from the Name tag. The latter\n            # is exportable as a cost allocation tag.\n            self.tags[\"Name\"] = self.name\n            self.conn.add_tags(self.jobflowid, self.tags)\n\n    def terminate(self):\n        terminate_jobflow(self.conn, self.name)\n\n    def modify_slave_count(self, num_slaves=1):\n        modify_slave_count(self.conn, self.name, num_slaves)\n\n\nclass EmrException(Exception):\n    def __init__(self, msg):\n        self.msg = msg\n\n    def __str__(self):\n        return self.msg\n"
  },
  {
    "path": "r2/r2/lib/errors.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom webob.exc import HTTPBadRequest, HTTPForbidden, status_map\nfrom r2.lib.utils import Storage, tup\nfrom pylons import request\nfrom pylons import app_globals as g\nfrom pylons.i18n import _\nfrom copy import copy\n\n\nerror_list = dict((\n        ('USER_REQUIRED', _(\"Please log in to do that.\")),\n        ('MOD_REQUIRED', _(\"You must be a moderator to do that.\")),\n        ('HTTPS_REQUIRED', _(\"this page must be accessed using https\")),\n        ('WRONG_DOMAIN', _(\"you can't do that on this domain\")),\n        ('VERIFIED_USER_REQUIRED', _(\"you need to set a valid email address to do that.\")),\n        ('NO_URL', _('a url is required')),\n        ('BAD_URL', _('you should check that url')),\n        ('INVALID_SCHEME', _('URI scheme must be one of: %(schemes)s')),\n        ('BAD_CAPTCHA', _('care to try these again?')),\n        ('BAD_USERNAME', _('invalid user name')),\n        ('USERNAME_TOO_SHORT', _('username must be between %(min)d and %(max)d characters')),\n        ('USERNAME_INVALID_CHARACTERS', _('username must contain only letters, numbers, \"-\", and \"_\"')),\n        ('USERNAME_TAKEN', _('that username is already taken')),\n        ('USERNAME_TAKEN_DEL', _('that username is taken by a deleted account')),\n        ('USER_BLOCKED', _(\"you can't send to a user that you have blocked\")),\n        ('NO_THING_ID', _('id not specified')),\n        ('TOO_MANY_THING_IDS', _('you provided too many ids')),\n        ('NOT_AUTHOR', _(\"you can't do that\")),\n        ('NOT_USER', _(\"You are not logged in as that user.\")),\n        ('NOT_FRIEND', _(\"you are not friends with that user\")),\n        ('LOGGED_IN', _(\"You are already logged in.\")),\n        ('DELETED_COMMENT', _('that comment has been deleted')),\n        ('DELETED_THING', _('that element has been deleted')),\n        ('SHORT_PASSWORD', _('the password must be at least %(chars)d characters')),\n        ('BAD_PASSWORD', _('that password is unacceptable')),\n        ('WRONG_PASSWORD', _('wrong password')),\n        ('BAD_PASSWORD_MATCH', _('passwords do not match')),\n        ('NO_NAME', _('please enter a name')),\n        ('NO_EMAIL', _('please enter an email address')),\n        ('NO_EMAIL_FOR_USER', _('no email address for that user')),\n        ('NO_VERIFIED_EMAIL', _('no verified email address for that user')),\n        ('NO_TO_ADDRESS', _('send it to whom?')),\n        ('NO_SUBJECT', _('please enter a subject')),\n        ('USER_DOESNT_EXIST', _(\"that user doesn't exist\")),\n        ('NO_USER', _('please enter a username')),\n        ('INVALID_PREF', _(\"that preference isn't valid\")),\n        ('NON_PREFERENCE', _(\"'%(choice)s' is not a user preference field\")),\n        ('INVALID_LANG', _(\"that language is not available\")),\n        ('BAD_NUMBER', _(\"that number isn't in the right range (%(range)s)\")),\n        ('BAD_STRING', _(\"you used a character here that we can't handle\")),\n        ('BAD_BUDGET', _(\"your budget must be at least $%(min)d and no more than $%(max)d.\")),\n        ('BAD_BID', _('your bid must be at least $%(min)s and no more than $%(max)s.')),\n        ('ALREADY_SUB', _(\"that link has already been submitted\")),\n        ('SUBREDDIT_EXISTS', _('that subreddit already exists')),\n        ('SUBREDDIT_NOEXIST', _('that subreddit doesn\\'t exist')),\n        ('SUBREDDIT_NOTALLOWED', _(\"you aren't allowed to post there.\")),\n        ('SUBREDDIT_NO_ACCESS', _(\"you aren't allowed access to this subreddit\")),\n        ('SUBREDDIT_REQUIRED', _('you must specify a subreddit')),\n        ('SUBREDDIT_DISABLED_ADS', _('this subreddit has chosen to disable their ads at this time')),\n        ('BAD_SR_NAME', _('that name isn\\'t going to work')),\n        ('COLLECTION_NOEXIST', _('that collection doesn\\'t exist')),\n        ('INVALID_TARGET', _('that target type is not valid')),\n        ('INVALID_NSFW_TARGET', _('nsfw ads must target nsfw content')),\n        ('INVALID_OS_VERSION', _('that version range is not valid')),\n        ('RATELIMIT', _('you are doing that too much. try again in %(time)s.')),\n        ('SUBREDDIT_RATELIMIT', _(\"you are doing that too much. try again later.\")),\n        ('EXPIRED', _('your session has expired')),\n        ('DRACONIAN', _('you must accept the terms first')),\n        ('BANNED_IP', \"IP banned\"),\n        ('BAD_CNAME', \"that domain isn't going to work\"),\n        ('USED_CNAME', \"that domain is already in use\"),\n        ('INVALID_OPTION', _('that option is not valid')),\n        ('BAD_EMAIL', _('that email is invalid')),\n        ('BAD_EMAILS', _('the following emails are invalid: %(emails)s')),\n        ('NO_EMAILS', _('please enter at least one email address')),\n        ('TOO_MANY_EMAILS', _('please only share to %(num)s emails at a time.')),\n        ('NEWSLETTER_NO_EMAIL', _('where should we send that weekly newsletter?')),\n        ('SPONSOR_NO_EMAIL', _('advertisers are required to supply an email')),\n        ('NEWSLETTER_EMAIL_UNACCEPTABLE', _('That email could not be added. Check your email for an existing confirmation email.')),\n        ('OVERSOLD', _('that subreddit has already been oversold on %(start)s to %(end)s. Please pick another subreddit or date.')),\n        ('OVERSOLD_DETAIL', _(\"We have insufficient inventory to fulfill your requested budget, target, and dates. Only %(available)s impressions available on %(target)s from %(start)s to %(end)s.\")),\n        ('BAD_DATE', _('please provide a date of the form mm/dd/yyyy')),\n        ('BAD_DATE_RANGE', _('the dates need to be in order and not identical')),\n        ('DATE_TOO_LATE', _('please enter a date %(day)s or earlier')),\n        ('DATE_TOO_EARLY', _('please enter a date %(day)s or later')),\n        ('START_DATE_CANNOT_CHANGE', _('start date cannot be changed')),\n        ('COST_BASIS_CANNOT_CHANGE', _('this campaign was created prior to auction and cannot be edited')),\n        ('BAD_ADDRESS', _('address problem: %(message)s')),\n        ('BAD_CARD', _('card problem: %(message)s')),\n        ('TOO_LONG', _(\"this is too long (max: %(max_length)s)\")),\n        ('NO_TEXT', _('we need something here')),\n        ('TOO_SHORT', _(\"this is too short (min: %(min_length)s)\")),\n        ('INVALID_CODE', _(\"we've never seen that code before\")),\n        ('CLAIMED_CODE', _(\"that code has already been claimed -- perhaps by you?\")),\n        ('NO_SELFS', _(\"that subreddit doesn't allow text posts\")),\n        ('NO_LINKS', _(\"that subreddit only allows text posts\")),\n        ('TOO_OLD', _(\"that's a piece of history now; it's too late to reply to it\")),\n        ('THREAD_LOCKED', _(\"Comments are locked.\")),\n        ('BAD_CSS_NAME', _('invalid css name')),\n        ('BAD_CSS', _('invalid css')),\n        ('BAD_COLOR', _('invalid color')),\n        ('BAD_REVISION', _('invalid revision ID')),\n        ('TOO_MUCH_FLAIR_CSS', _('too many flair css classes')),\n        ('BAD_FLAIR_TARGET', _('not a valid flair target')),\n        ('OAUTH2_INVALID_CLIENT', _('invalid client id')),\n        ('OAUTH2_INVALID_REDIRECT_URI', _('invalid redirect_uri parameter')),\n        ('OAUTH2_INVALID_RESPONSE_TYPE', _('invalid response type')),\n        ('OAUTH2_INVALID_SCOPE', _('invalid scope requested')),\n        ('OAUTH2_INVALID_REFRESH_TOKEN', _('invalid refresh token')),\n        ('OAUTH2_ACCESS_DENIED', _('access denied by the user')),\n        ('OAUTH2_NO_REFRESH_TOKENS_ALLOWED', _('refresh tokens are not allowed for this response_type')),\n        ('OAUTH2_CONFIDENTIAL_TOKEN', _('confidential clients can not request tokens directly')),\n        ('CONFIRM', _(\"please confirm the form\")),\n        ('CONFLICT', _(\"conflict error while saving\")),\n        ('NO_API', _('cannot perform this action via the API')),\n        ('DOMAIN_BANNED', _('%(domain)s is not allowed on reddit: %(reason)s')),\n        ('NO_OTP_SECRET', _('you must enable two-factor authentication')),\n        ('OTP_ALREADY_ENABLED', _('two-factor authentication is already enabled')),\n        ('BAD_IMAGE', _('image problem')),\n        ('DEVELOPER_ALREADY_ADDED', _('already added')),\n        ('TOO_MANY_DEVELOPERS', _('too many developers')),\n        ('DEVELOPER_FIRST_PARTY_APP', _('this app can not be modified from this interface')),\n        ('DEVELOPER_PRIVILEGED_ACCOUNT', _('you cannot add this account from this interface')),\n        ('INVALID_MODHASH', _(\"invalid modhash\")),\n        ('ALREADY_MODERATOR', _('that user is already a moderator')),\n        ('CANT_RESTRICT_MODERATOR', _(\"You can't perform that action because that user is a moderator.\")),\n        ('NO_INVITE_FOUND', _('there is no pending invite for that subreddit')),\n        ('BUDGET_LIVE', _('you cannot edit the budget of a live ad')),\n        ('TOO_MANY_CAMPAIGNS', _('you have too many campaigns for that promotion')),\n        ('BAD_JSONP_CALLBACK', _('that jsonp callback contains invalid characters')),\n        ('INVALID_PERMISSION_TYPE', _(\"permissions don't apply to that type of user\")),\n        ('INVALID_PERMISSIONS', _('invalid permissions string')),\n        ('BAD_MULTI_PATH', _('invalid multi path')),\n        ('BAD_MULTI_NAME', _('%(reason)s')),\n        ('MULTI_NOT_FOUND', _('that multireddit doesn\\'t exist')),\n        ('MULTI_EXISTS', _('that multireddit already exists')),\n        ('MULTI_CANNOT_EDIT', _('you can\\'t change that multireddit')),\n        ('MULTI_TOO_MANY_SUBREDDITS', _('no more space for subreddits in that multireddit')),\n        ('MULTI_SPECIAL_SUBREDDIT', _(\"can't add special subreddit %(path)s\")),\n        ('TOO_MANY_SUBREDDITS', _('maximum %(max)s subreddits')),\n        ('JSON_PARSE_ERROR', _('unable to parse JSON data')),\n        ('JSON_INVALID', _('unexpected JSON structure')),\n        ('JSON_MISSING_KEY', _('JSON missing key: \"%(key)s\"')),\n        ('NO_CHANGE_KIND', _(\"can't change post type\")),\n        ('INVALID_LOCATION', _(\"invalid location\")),\n        ('INVALID_FREQUENCY_CAP', _(\"invalid values for frequency cap\")),\n        ('FREQUENCY_CAP_TOO_LOW', _('frequency cap must be at least %(min)d')),\n        ('BANNED_FROM_SUBREDDIT', _('that user is banned from the subreddit')),\n        ('IN_TIMEOUT', _(\"You can't do that while suspended.\")),\n        ('GOLD_REQUIRED', _('you must have an active reddit gold subscription to do that')),\n        ('INSUFFICIENT_CREDDITS', _(\"insufficient creddits\")),\n        ('GILDING_NOT_ALLOWED', _(\"gilding is not allowed in this subreddit\")),\n        ('SCRAPER_ERROR', _(\"unable to scrape provided url\")),\n        ('NO_SR_TO_SR_MESSAGE', _(\"can't send a message from a subreddit to another subreddit\")),\n        ('USER_BLOCKED_MESSAGE', _(\"can't send message to that user\")),\n        ('ADMIN_REQUIRED', _(\"you must be in admin mode for this\")),\n        ('CANT_CONVERT_TO_GOLD_ONLY', _(\"to convert an existing subreddit to gold only, send a message to %(admin_modmail)s\") \n            % dict(admin_modmail=g.admin_message_acct)),\n        ('GOLD_ONLY_SR_REQUIRED', _(\"this subreddit must be 'gold only' to select this\")),\n        ('CANT_CREATE_SR', _(\"your account is too new or you do not have enough karma to create a subreddit. please contact the admins to request an exemption.\")),\n        ('BAD_PROMO_MOBILE_OS', _(\"you must select at least one mobile OS to target\")),\n        ('BAD_PROMO_MOBILE_DEVICE', _(\"you must select at least one device per OS to target\")),\n        ('USER_MUTED', _(\"You have been muted from this subreddit.\")),\n        ('MUTED_FROM_SUBREDDIT', _(\"This user has been muted from the subreddit.\")),\n        ('COMMENT_NOT_STICKYABLE', _(\"This comment is not stickyable. Ensure that it is a top level comment.\")),\n        ('SR_RULE_EXISTS', _(\"A subreddit rule by that name already exists.\")),\n        ('SR_RULE_DOESNT_EXIST', _(\"No subreddit rule by that name exists.\")),\n        ('SR_RULE_TOO_MANY', _(\"This subreddit already has the maximum number of rules.\")),\n        ('COMMENT_NOT_ACCESSIBLE', _(\"Cannot access this comment.\")),\n        ('POST_NOT_ACCESSIBLE', _(\"Cannot access this post.\")),\n    ))\n\nerrors = Storage([(e, e) for e in error_list.keys()])\n\n\ndef add_error_codes(new_codes):\n    \"\"\"Add error codes to the error enumeration.\n\n    It is assumed that the incoming messages are marked for translation but not\n    yet translated, so they can be declared before pylons.i18n is ready.\n\n    \"\"\"\n    for code, message in new_codes.iteritems():\n        error_list[code] = _(message)\n        errors[code] = code\n\n\nclass RedditError(Exception):\n    name = None\n    fields = None\n    code = None\n\n    def __init__(self, name=None, msg_params=None, fields=None, code=None):\n        Exception.__init__(self)\n\n        if name is not None:\n            self.name = name\n\n        self.i18n_message = error_list.get(self.name)\n        self.msg_params = msg_params or {}\n\n        if fields is not None:\n            # list of fields in the original form that caused the error\n            self.fields = tup(fields)\n\n        if code is not None:\n            self.code = code\n\n    @property\n    def message(self):\n        return _(self.i18n_message) % self.msg_params\n\n    def __iter__(self):\n        yield ('name', self.name)\n        yield ('message', _(self.message))\n\n    def __repr__(self):\n        return '<RedditError: %s>' % self.name\n\n    def __str__(self):\n        return repr(self)\n\n\nclass ErrorSet(object):\n    def __init__(self):\n        self.errors = {}\n\n    def __contains__(self, pair):\n        \"\"\"Expects an (error_name, field_name) tuple and checks to\n        see if it's in the errors list.\"\"\"\n        return self.errors.has_key(pair)\n\n    def get(self, name, default=None):\n        return self.errors.get(name, default)\n\n    def get_first(self, field_name, *error_names):\n        error = None\n\n        for error_name in error_names:\n            error = self.get((error_name, field_name))\n            if error:\n                return error\n\n    def __getitem__(self, name):\n        return self.errors[name]\n\n    def __repr__(self):\n        return \"<ErrorSet %s>\" % list(self)\n\n    def __iter__(self):\n        for x in self.errors:\n            yield x\n\n    def __len__(self):\n        return len(self.errors)\n\n    def add(self, error_name, msg_params=None, field=None, code=None):\n        for field_name in tup(field):\n            e = RedditError(error_name, msg_params, fields=field_name,\n                            code=code)\n            self.add_error(e)\n\n    def add_error(self, error):\n        for field_name in tup(error.fields):\n            self.errors[(error.name, field_name)] = error\n\n    def remove(self, pair):\n        \"\"\"Expects an (error_name, field_name) tuple and removes it\n        from the errors list.\"\"\"\n        if self.errors.has_key(pair):\n            del self.errors[pair]\n\n\nclass ForbiddenError(HTTPForbidden):\n    def __init__(self, error_name):\n        HTTPForbidden.__init__(self)\n        self.explanation = error_list[error_name]\n\n\nclass BadRequestError(HTTPBadRequest):\n    def __init__(self, error_name):\n        HTTPBadRequest.__init__(self)\n        self.error_data = {\n            'reason': error_name,\n            'explanation': error_list[error_name],\n        }\n        self.explanation = error_list[error_name]\n\n\ndef reddit_http_error(code=400, error_name='UNKNOWN_ERROR', **data):\n    exc = status_map[code]()\n\n    data['reason'] = exc.explanation = error_name\n    if 'explanation' not in data and error_name in error_list:\n        data['explanation'] = exc.explanation = error_list[error_name]\n\n    # omit 'fields' json attribute if it is empty\n    if 'fields' in data and not data['fields']:\n        del data['fields']\n\n    exc.error_data = data\n    return exc\n\n\nclass UserRequiredException(RedditError):\n    name = errors.USER_REQUIRED\n    code = 403\n\n\nclass VerifiedUserRequiredException(RedditError):\n    name = errors.VERIFIED_USER_REQUIRED\n    code = 403\n\n\nclass MessageError(Exception): pass\n"
  },
  {
    "path": "r2/r2/lib/eventcollector.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\nfrom cStringIO import StringIO\nimport datetime\nimport gzip\nimport hashlib\nimport hmac\nimport itertools\nimport json\nimport pytz\nimport random\nimport requests\nimport time\n\nimport httpagentparser\nimport time\n\nfrom pylons import app_globals as g\nfrom uuid import uuid4\nfrom wsgiref.handlers import format_date_time\n\nfrom r2.lib import amqp, hooks\nfrom r2.lib.language import charset_summary\nfrom r2.lib.geoip import (\n    get_request_location,\n    location_by_ips,\n)\nfrom r2.lib.cache_poisoning import cache_headers_valid\nfrom r2.lib.utils import (\n    domain,\n    to_epoch_milliseconds,\n    sampled,\n    squelch_exceptions,\n    to36,\n)\n\n\ndef _make_http_date(when=None):\n    if when is None:\n        when = datetime.datetime.now(pytz.UTC)\n    return format_date_time(time.mktime(when.timetuple()))\n\n\n# XXX External dependencies!\n_datetime_to_millis = to_epoch_milliseconds\n\n\ndef parse_agent(ua):\n    agent_summary = {}\n    parsed = httpagentparser.detect(ua)\n    for attr in (\"browser\", \"os\", \"platform\"):\n        d = parsed.get(attr)\n        if d:\n            for subattr in (\"name\", \"version\"):\n                if subattr in d:\n                    key = \"%s_%s\" % (attr, subattr)\n                    agent_summary[key] = d[subattr]\n\n    agent_summary['bot'] = parsed.get('bot')\n\n    return agent_summary\n\n\nclass EventQueue(object):\n    def __init__(self, queue=amqp):\n        self.queue = queue\n\n    def save_event(self, event):\n        if event.testing:\n            queue_name = \"event_collector_test\"\n        else:\n            queue_name = \"event_collector\"\n\n        # send info about truncatable field as a header, separate from the\n        # actual event data\n        headers = None\n        if event.truncatable_field:\n            headers = {\"truncatable_field\": event.truncatable_field}\n\n        self.queue.add_item(queue_name, event.dump(), headers=headers)\n\n    @squelch_exceptions\n    @sampled(\"events_collector_vote_sample_rate\")\n    def vote_event(self, vote):\n        \"\"\"Create a 'vote' event for event-collector\n\n        vote: An r2.models.vote Vote object\n        \"\"\"\n\n        # For mapping vote directions to readable names used by data team\n        def get_vote_direction_name(vote):\n            if vote.is_upvote:\n                return \"up\"\n            elif vote.is_downvote:\n                return \"down\"\n            else:\n                return \"clear\"\n\n        event = Event(\n            topic=\"vote_server\",\n            event_type=\"server_vote\",\n            time=vote.date,\n            data=vote.event_data[\"context\"],\n            obfuscated_data=vote.event_data[\"sensitive\"],\n        )\n\n        event.add(\"vote_direction\", get_vote_direction_name(vote))\n\n        if vote.previous_vote:\n            event.add(\"prev_vote_direction\",\n                get_vote_direction_name(vote.previous_vote))\n            event.add(\n                \"prev_vote_ts\",\n                to_epoch_milliseconds(vote.previous_vote.date)\n            )\n\n        if vote.is_automatic_initial_vote:\n            event.add(\"auto_self_vote\", True)\n\n        for name, value in vote.effects.serializable_data.iteritems():\n            # rename the \"notes\" field to \"details_text\" for the event\n            if name == \"notes\":\n                name = \"details_text\"\n\n            event.add(name, value)\n\n        # add the note codes separately as \"process_notes\"\n        event.add(\"process_notes\", \", \".join(vote.effects.note_codes))\n\n        event.add_subreddit_fields(vote.thing.subreddit_slow)\n        event.add_target_fields(vote.thing)\n\n        # add the rank of the vote if we have it (passed in through the API)\n        rank = vote.data.get('rank')\n        if rank:\n            event.add(\"target_rank\", rank)\n\n        self.save_event(event)\n\n    @squelch_exceptions\n    @sampled(\"events_collector_submit_sample_rate\")\n    def submit_event(self, new_post, request=None, context=None):\n        \"\"\"Create a 'submit' event for event-collector\n\n        new_post: An r2.models.Link object\n        request, context: Should be pylons.request & pylons.c respectively\n\n        \"\"\"\n        event = Event(\n            topic=\"submit_events\",\n            event_type=\"ss.submit\",\n            time=new_post._date,\n            request=request,\n            context=context,\n            truncatable_field=\"post_body\",\n        )\n\n        event.add(\"post_id\", new_post._id)\n        event.add(\"post_fullname\", new_post._fullname)\n        event.add_text(\"post_title\", new_post.title)\n\n        event.add(\"user_neutered\", new_post.author_slow._spam)\n\n        if new_post.is_self:\n            event.add(\"post_type\", \"self\")\n            event.add_text(\"post_body\", new_post.selftext)\n        else:\n            event.add(\"post_type\", \"link\")\n            event.add(\"post_target_url\", new_post.url)\n            event.add(\"post_target_domain\", new_post.link_domain())\n\n        event.add_subreddit_fields(new_post.subreddit_slow)\n\n        self.save_event(event)\n\n    @squelch_exceptions\n    @sampled(\"events_collector_comment_sample_rate\")\n    def comment_event(self, new_comment, request=None, context=None):\n        \"\"\"Create a 'comment' event for event-collector.\n\n        new_comment: An r2.models.Comment object\n        request, context: Should be pylons.request & pylons.c respectively\n        \"\"\"\n        from r2.models import Comment, Link\n\n        event = Event(\n            topic=\"comment_events\",\n            event_type=\"ss.comment\",\n            time=new_comment._date,\n            request=request,\n            context=context,\n            truncatable_field=\"comment_body\",\n        )\n\n        event.add(\"comment_id\", new_comment._id)\n        event.add(\"comment_fullname\", new_comment._fullname)\n\n        event.add_text(\"comment_body\", new_comment.body)\n\n        post = Link._byID(new_comment.link_id)\n        event.add(\"post_id\", post._id)\n        event.add(\"post_fullname\", post._fullname)\n        event.add(\"post_created_ts\", to_epoch_milliseconds(post._date))\n        if post.promoted:\n            event.add(\"post_is_promoted\", bool(post.promoted))\n\n        if new_comment.parent_id:\n            parent = Comment._byID(new_comment.parent_id)\n        else:\n            # If this is a top-level comment, parent is the same as the post\n            parent = post\n        event.add(\"parent_id\", parent._id)\n        event.add(\"parent_fullname\", parent._fullname)\n        event.add(\"parent_created_ts\", to_epoch_milliseconds(parent._date))\n\n        event.add(\"user_neutered\", new_comment.author_slow._spam)\n\n        event.add_subreddit_fields(new_comment.subreddit_slow)\n\n        self.save_event(event)\n\n    @squelch_exceptions\n    @sampled(\"events_collector_poison_sample_rate\")\n    def cache_poisoning_event(self, poison_info, request=None, context=None):\n        \"\"\"Create a 'cache_poisoning_server' event for event-collector\n\n        poison_info: Details from the client about the poisoning event\n        request, context: Should be pylons.request & pylons.c respectively\n\n        \"\"\"\n        poisoner_name = poison_info.pop(\"poisoner_name\")\n\n        event = Event(\n            topic=\"cache_poisoning_events\",\n            event_type=\"ss.cache_poisoning\",\n            request=request,\n            context=context,\n            data=poison_info,\n            truncatable_field=\"resp_headers\",\n        )\n\n        event.add(\"poison_blame_guess\", \"proxy\")\n\n        resp_headers = poison_info[\"resp_headers\"]\n        if resp_headers:\n            # Check if the caching headers we got back match the current policy\n            cache_policy = poison_info[\"cache_policy\"]\n            headers_valid = cache_headers_valid(cache_policy, resp_headers)\n\n            event.add(\"cache_headers_valid\", headers_valid)\n\n        # try to determine what kind of poisoning we're dealing with\n\n        if poison_info[\"source\"] == \"web\":\n            # Do we think they logged in the usual way, or do we think they\n            # got poisoned with someone else's session cookie?\n            valid_login_hook = hooks.get_hook(\"poisoning.guess_valid_login\")\n            if valid_login_hook.call_until_return(poisoner_name=poisoner_name):\n                # Maybe a misconfigured local Squid proxy + multiple\n                # clients?\n                event.add(\"poison_blame_guess\", \"local_proxy\")\n                event.add(\"poison_credentialed_guess\", False)\n            elif (context.user_is_loggedin and\n                  context.user.name == poisoner_name):\n                # Guess we got poisoned with a cookie-bearing response.\n                event.add(\"poison_credentialed_guess\", True)\n            else:\n                event.add(\"poison_credentialed_guess\", False)\n        elif poison_info[\"source\"] == \"mweb\":\n            # All mweb responses contain an OAuth token, so we have to assume\n            # whoever got this response can perform actions as the poisoner\n            event.add(\"poison_credentialed_guess\", True)\n        else:\n            raise Exception(\"Unsupported source in cache_poisoning_event\")\n\n        # Check if the CF-Cache-Status header is present (this header is not\n        # present if caching is disallowed.) If it is, the CDN caching rules\n        # are all jacked up.\n        if resp_headers and \"cf-cache-status\" in resp_headers:\n            event.add(\"poison_blame_guess\", \"cdn\")\n\n        self.save_event(event)\n\n    @squelch_exceptions\n    def muted_forbidden_event(self, details_text, subreddit=None,\n            parent_message=None, target=None, request=None, context=None):\n        \"\"\"Create a mute-related 'forbidden_event' for event-collector.\n\n        details_text: \"muted\" if a muted user is trying to message the\n            subreddit or \"muted mod\" if the subreddit mod is attempting\n            to message the muted user\n        subreddit: The Subreddit of the mod messaging the muted user\n        parent_message: Message that is being responded to\n        target: The intended recipient (Subreddit or Account)\n        request, context: Should be pylons.request & pylons.c respectively;\n\n        \"\"\"\n        event = Event(\n            topic=\"forbidden_actions\",\n            event_type=\"ss.forbidden_message_attempt\",\n            request=request,\n            context=context,\n        )\n        event.add(\"details_text\", details_text)\n\n        if parent_message:\n            event.add(\"parent_message_id\", parent_message._id)\n            event.add(\"parent_message_fullname\", parent_message._fullname)\n\n        event.add_subreddit_fields(subreddit)\n        event.add_target_fields(target)\n\n        self.save_event(event)\n\n    @squelch_exceptions\n    def timeout_forbidden_event(self, action_name, details_text,\n            target=None, target_fullname=None, subreddit=None,\n            request=None, context=None):\n        \"\"\"Create a timeout-related 'forbidden_actions' for event-collector.\n\n        action_name: the action taken by a user in timeout\n        details_text: this provides more details about the action\n        target: The intended item the action was to be taken on\n        target_fullname: The fullname used to convert to a target\n        subreddit: The Subreddit the action was taken in. If target is of the\n            type Subreddit, then this won't be passed in\n        request, context: Should be pylons.request & pylons.c respectively;\n\n        \"\"\"\n        if not action_name:\n            request_vars = request.environ[\"pylons.routes_dict\"]\n            action_name = request_vars.get('action_name')\n\n            # type of vote\n            if action_name == \"vote\":\n                direction = int(request.POST.get(\"dir\", 0))\n                if direction == 1:\n                    action_name = \"upvote\"\n                elif direction == -1:\n                    action_name = \"downvote\"\n                else:\n                    action_name = \"clearvote\"\n            # set or unset for contest mode and subreddit sticky\n            elif action_name in (\"set_contest_mode\", \"set_subreddit_sticky\"):\n                action_name = action_name.replace(\"_\", \"\")\n                if request.POST.get('state') == \"False\":\n                    action_name = \"un\" + action_name\n            # set or unset for suggested sort\n            elif action_name == \"set_suggested_sort\":\n                action_name = action_name.replace(\"_\", \"\")\n                if request.POST.get(\"sort\") in (\"\", \"clear\"):\n                    action_name = \"un\" + action_name\n            # action for viewing /about/reports, /about/spam, /about/modqueue\n            elif action_name == \"spamlisting\":\n                action_name = \"pageview\"\n                details_text = request_vars.get(\"location\")\n            elif action_name == \"clearflairtemplates\":\n                action_name = \"editflair\"\n                details_text = \"flair_clear_template\"\n            elif action_name in (\"flairconfig\", \"flaircsv\", \"flairlisting\"):\n                details_text = action_name.replace(\"flair\", \"flair_\")\n                action_name = \"editflair\"\n\n        if not target:\n            if not target_fullname:\n                if action_name in (\"wiki_settings\", \"wiki_edit\"):\n                    target = context.site\n                elif action_name in (\"wiki_allow_editor\"):\n                    target = Account._by_name(request.POST.get(\"username\"))\n                elif action_name in (\"delete_sr_header\", \"delete_sr_icon\",\n                        \"delete_sr_banner\"):\n                    details_text = \"%s\" % action_name.replace(\"ete_sr\", \"\")\n                    action_name = \"editsettings\"\n                    target = context.site\n                elif action_name in (\"bannedlisting\", \"mutedlisting\",\n                        \"wikibannedlisting\", \"wikicontributorslisting\"):\n                    target = context.site\n\n            if target_fullname:\n                from r2.models import Thing\n                target = Thing._by_fullname(\n                    target_fullname,\n                    return_dict=False,\n                    data=True,\n            )\n\n        event = Event(\n            topic=\"forbidden_actions\",\n            event_type=\"ss.forbidden_%s\" % action_name,\n            request=request,\n            context=context,\n        )\n        event.add(\"details_text\", details_text)\n        event.add(\"process_notes\", \"IN_TIMEOUT\")\n\n        from r2.models import Comment, Link, Subreddit\n        if not subreddit:\n            if isinstance(context.site, Subreddit):\n                subreddit = context.site\n            elif isinstance(target, (Comment, Link)):\n                subreddit = target.subreddit_slow\n            elif isinstance(target, Subreddit):\n                subreddit = target\n\n        event.add_subreddit_fields(subreddit)\n        event.add_target_fields(target)\n\n        self.save_event(event)\n\n    @squelch_exceptions\n    @sampled(\"events_collector_mod_sample_rate\")\n    def mod_event(self, modaction, subreddit, mod, target=None,\n            request=None, context=None):\n        \"\"\"Create a 'mod' event for event-collector.\n\n        modaction: An r2.models.ModAction object\n        subreddit: The Subreddit the mod action is being performed in\n        mod: The Account that is performing the mod action\n        target: The Thing the mod action was applied to\n        request, context: Should be pylons.request & pylons.c respectively\n\n        \"\"\"\n        event = Event(\n            topic=\"mod_events\",\n            event_type=modaction.action,\n            time=modaction.date,\n            uuid=modaction._id,\n            request=request,\n            context=context,\n        )\n\n        event.add(\"details_text\", modaction.details_text)\n\n        # Some jobs that perform mod actions (for example, AutoModerator) are\n        # run without actually logging into the account that performs the\n        # the actions. In that case, set the user data based on the mod that's\n        # performing the action.\n        if not event.get(\"user_id\"):\n            event[\"user_id\"] = mod._id\n            event[\"user_name\"] = mod.name\n\n        event.add_subreddit_fields(subreddit)\n        event.add_target_fields(target)\n\n        self.save_event(event)\n\n    @squelch_exceptions\n    @sampled(\"events_collector_report_sample_rate\")\n    def report_event(self, reason=None, details_text=None,\n            subreddit=None, target=None, request=None, context=None,\n                     event_type=\"ss.report\"):\n        \"\"\"Create a 'report' event for event-collector.\n\n        process_notes: Type of rule (pre-defined report reasons or custom)\n        details_text: The report reason\n        subreddit: The Subreddit the action is being performed in\n        target: The Thing the action was applied to\n        request, context: Should be pylons.request & pylons.c respectively\n\n        \"\"\"\n        from r2.models.rules import OLD_SITEWIDE_RULES, SITEWIDE_RULES, SubredditRules\n\n        event = Event(\n            topic=\"report_events\",\n            event_type=event_type,\n            request=request,\n            context=context,\n        )\n        if reason in OLD_SITEWIDE_RULES or reason in SITEWIDE_RULES:\n            process_notes = \"SITE_RULES\"\n        else:\n            if subreddit and SubredditRules.get_rule(subreddit, reason):\n                process_notes = \"SUBREDDIT_RULES\"\n            else:\n                process_notes = \"CUSTOM\"\n\n        event.add(\"process_notes\", process_notes)\n        event.add(\"details_text\", details_text)\n\n        event.add_subreddit_fields(subreddit)\n        event.add_target_fields(target)\n\n        self.save_event(event)\n\n    @squelch_exceptions\n    @sampled(\"events_collector_quarantine_sample_rate\")\n    def quarantine_event(self, event_type, subreddit,\n            request=None, context=None):\n        \"\"\"Create a 'quarantine' event for event-collector.\n\n        event_type: quarantine_interstitial_view, quarantine_opt_in,\n            quarantine_opt_out, quarantine_interstitial_dismiss\n        subreddit: The quarantined subreddit\n        request, context: Should be pylons.request & pylons.c respectively\n\n        \"\"\"\n        event = Event(\n            topic=\"quarantine\",\n            event_type=event_type,\n            request=request,\n            context=context,\n        )\n\n        if context:\n            if context.user_is_loggedin:\n                event.add(\"verified_email\", context.user.email_verified)\n            else:\n                event.add(\"verified_email\", False)\n\n        # Due to the redirect, the request object being sent isn't the\n        # original, so referrer and action data is missing for certain events\n        if request and (event_type == \"quarantine_interstitial_view\" or\n                 event_type == \"quarantine_opt_out\"):\n            request_vars = request.environ[\"pylons.routes_dict\"]\n            event.add(\"sr_action\", request_vars.get(\"action\", None))\n\n            # The thing_id the user is trying to view is a comment\n            if request.environ[\"pylons.routes_dict\"].get(\"comment\", None):\n                thing_id36 = request_vars.get(\"comment\", None)\n            # The thing_id is a link\n            else:\n                thing_id36 = request_vars.get(\"article\", None)\n\n            if thing_id36:\n                event.add(\"thing_id\", int(thing_id36, 36))\n\n        event.add_subreddit_fields(subreddit)\n\n        self.save_event(event)\n\n    @squelch_exceptions\n    @sampled(\"events_collector_modmail_sample_rate\")\n    def modmail_event(self, message, request=None, context=None):\n        \"\"\"Create a 'modmail' event for event-collector.\n\n        message: An r2.models.Message object\n        request: pylons.request of the request that created the message\n        context: pylons.tmpl_context of the request that created the message\n\n        \"\"\"\n\n        from r2.models import Account, Message\n\n        sender = message.author_slow\n        sr = message.subreddit_slow\n        sender_is_moderator = sr.is_moderator_with_perms(sender, \"mail\")\n\n        if message.first_message:\n            first_message = Message._byID(message.first_message, data=True)\n        else:\n            first_message = message\n\n        event = Event(\n            topic=\"message_events\",\n            event_type=\"ss.send_message\",\n            time=message._date,\n            request=request,\n            context=context,\n            data={\n                # set these manually rather than allowing them to be set from\n                # the request context because the loggedin user might not\n                # be the message sender\n                \"user_id\": sender._id,\n                \"user_name\": sender.name,\n            },\n        )\n\n        if sender == Account.system_user():\n            sender_type = \"automated\"\n        elif sender_is_moderator:\n            sender_type = \"moderator\"\n        else:\n            sender_type = \"user\"\n\n        event.add(\"sender_type\", sender_type)\n        event.add(\"sr_id\", sr._id)\n        event.add(\"sr_name\", sr.name)\n        event.add(\"message_id\", message._id)\n        event.add(\"message_kind\", \"modmail\")\n        event.add(\"message_fullname\", message._fullname)\n\n        event.add_text(\"message_body\", message.body)\n        event.add_text(\"message_subject\", message.subject)\n\n        event.add(\"first_message_id\", first_message._id)\n        event.add(\"first_message_fullname\", first_message._fullname)\n\n        if request and request.POST.get(\"source\", None):\n            source = request.POST[\"source\"]\n            if source in {\"compose\", \"permalink\", \"modmail\", \"usermail\"}:\n                event.add(\"page\", source)\n\n        if message.sent_via_email:\n            event.add(\"is_third_party\", True)\n            event.add(\"third_party_metadata\", \"mailgun\")\n\n        if not message.to_id:\n            target = sr\n        else:\n            target = Account._byID(message.to_id, data=True)\n\n        event.add_target_fields(target)\n\n        self.save_event(event)\n\n    @squelch_exceptions\n    @sampled(\"events_collector_message_sample_rate\")\n    def message_event(self, message, event_type=\"ss.send_message\",\n                      request=None, context=None):\n        \"\"\"Create a 'message' event for event-collector.\n\n        message: An r2.models.Message object\n        request: pylons.request of the request that created the message\n        context: pylons.tmpl_context of the request that created the message\n\n        \"\"\"\n\n        from r2.models import Account, Message\n\n        sender = message.author_slow\n\n        if message.first_message:\n            first_message = Message._byID(message.first_message, data=True)\n        else:\n            first_message = message\n\n        event = Event(\n            topic=\"message_events\",\n            event_type=event_type,\n            time=message._date,\n            request=request,\n            context=context,\n            data={\n                # set these manually rather than allowing them to be set from\n                # the request context because the loggedin user might not\n                # be the message sender\n                \"user_id\": sender._id,\n                \"user_name\": sender.name,\n            },\n        )\n\n        if sender == Account.system_user():\n            sender_type = \"automated\"\n        else:\n            sender_type = \"user\"\n\n        event.add(\"sender_type\", sender_type)\n        event.add(\"message_kind\", \"message\")\n        event.add(\"message_id\", message._id)\n        event.add(\"message_fullname\", message._fullname)\n\n        event.add_text(\"message_body\", message.body)\n        event.add_text(\"message_subject\", message.subject)\n\n        event.add(\"first_message_id\", first_message._id)\n        event.add(\"first_message_fullname\", first_message._fullname)\n\n        if request and request.POST.get(\"source\", None):\n            source = request.POST[\"source\"]\n            if source in {\"compose\", \"permalink\", \"usermail\"}:\n                event.add(\"page\", source)\n\n        if message.sent_via_email:\n            event.add(\"is_third_party\", True)\n            event.add(\"third_party_metadata\", \"mailgun\")\n\n        target = Account._byID(message.to_id, data=True)\n\n        event.add_target_fields(target)\n\n        self.save_event(event)\n\n    def loid_event(self, loid, action_name, request=None, context=None):\n        \"\"\"Create a 'loid' event for event-collector.\n\n        loid: the created/modified loid\n        action_name: create_loid (only allowed value currently)\n        \"\"\"\n        event = Event(\n            topic=\"loid_events\",\n            event_type='ss.%s' % action_name,\n            request=request,\n            context=context,\n        )\n        event.add(\"request_url\", request.fullpath)\n        for k, v in loid.to_dict().iteritems():\n            event.add(k, v)\n        self.save_event(event)\n\n    def login_event(self, action_name, error_msg,\n                    user_name=None, email=None,\n                    remember_me=None, newsletter=None, email_verified=None,\n                    signature=None, request=None, context=None):\n        \"\"\"Create a 'login' event for event-collector.\n\n        action_name: login_attempt, register_attempt, password_reset\n        error_msg: error message string if there was an error\n        user_name: user entered username string\n        email: user entered email string (register, password reset)\n        remember_me:  boolean state of remember me checkbox (login, register)\n        newsletter: boolean state of newsletter checkbox (register only)\n        email_verified: boolean value for email verification state, requires\n            email (password reset only)\n        request, context: Should be pylons.request & pylons.c respectively\n\n        \"\"\"\n        event = Event(\n            topic=\"login_events\",\n            event_type='ss.%s' % action_name,\n            request=request,\n            context=context,\n        )\n\n        if error_msg:\n            event.add('successful', False)\n            event.add('process_notes', error_msg)\n        else:\n            event.add('successful', True)\n\n        event.add('user_name', user_name)\n        event.add('email', email)\n        event.add('remember_me', remember_me)\n        event.add('newsletter', newsletter)\n        event.add('email_verified', email_verified)\n        if signature:\n            event.add(\"signed\", True)\n            event.add(\"signature_platform\", signature.platform)\n            event.add(\"signature_version\", signature.version)\n            event.add(\"signature_valid\", signature.is_valid())\n            sigerror = \", \".join(\n                \"%s_%s\" % (field, code) for code, field in signature.errors\n            )\n            event.add(\"signature_errors\", sigerror)\n            if signature.epoch:\n                event.add(\"signature_age\", int(time.time()) - signature.epoch)\n\n        self.save_event(event)\n\n    def bucketing_event(\n        self, experiment_id, experiment_name, variant, user, loid\n    ):\n        \"\"\"Send an event recording an experiment bucketing.\n\n        experiment_id: an integer representing the experiment\n        experiment_name: a human-readable name representing the experiment\n        variant: a string representing the variant name\n        user: the Account that has been put into the variant\n        \"\"\"\n        event = Event(\n            topic='bucketing_events',\n            event_type='bucket',\n        )\n        event.add('experiment_id', experiment_id)\n        event.add('experiment_name', experiment_name)\n        event.add('variant', variant)\n        # if the user is logged out, we won't have a user_id or name\n        if user is not None:\n            event.add('user_id', user._id)\n            event.add('user_name', user.name)\n        if loid:\n            for k, v in loid.to_dict().iteritems():\n                event.add(k, v)\n        self.save_event(event)\n\n    def page_bucketing_event(\n        self, experiment_id, experiment_name, variant, content_id,\n        request, context=None\n    ):\n        \"\"\"Send an event recording bucketing of a page for a page-based\n        experiment.\n\n        experiment_id: an integer representing the experiment\n        experiment_name: a human-readable name representing the experiment\n        variant: a string representing the variant name\n        content_id: the primary content fullname for the page being bucketed\n        \"\"\"\n        event = Event(\n            topic='bucketing_events',\n            event_type='bucket_page',\n            request=request,\n            context=context,\n        )\n        event.add('experiment_id', experiment_id)\n        event.add('experiment_name', experiment_name)\n        event.add('variant', variant)\n        event.add('bucketing_fullname', content_id)\n        event.add('crawler_name', g.pool_name)\n        event.add('url', request.fullurl)\n        self.save_event(event)\n\n\nclass Event(object):\n    def __init__(self, topic, event_type,\n            time=None, uuid=None, request=None, context=None, testing=False,\n            data=None, obfuscated_data=None, truncatable_field=None):\n        \"\"\"Create a new event for event-collector.\n\n        topic: Used to filter events into appropriate streams for processing\n        event_type: Used for grouping and sub-categorizing events\n        time: Should be a datetime.datetime object in UTC timezone\n        uuid: Should be a UUID object\n        request, context: Should be pylons.request & pylons.c respectively\n        testing: Whether to send the event to the test endpoint\n        data: A dict of field names/values to initialize the payload with\n        obfuscated_data: Same as `data`, but fields that need obfuscation\n        truncatable_field: Field to truncate if the event is too large\n        \"\"\"\n        self.topic = topic\n        self.event_type = event_type\n        self.testing = testing or g.debug\n        self.truncatable_field = truncatable_field\n\n        if not time:\n            time = datetime.datetime.now(pytz.UTC)\n        self.timestamp = _datetime_to_millis(time)\n\n        if not uuid:\n            uuid = uuid4()\n        self.uuid = str(uuid)\n\n        self.payload = {}\n        if data:\n            self.payload.update(data)\n        self.obfuscated_data = {}\n        if obfuscated_data:\n            self.obfuscated_data.update(obfuscated_data)\n\n        if context and request:\n            # Since we don't want to override any of these values that callers\n            # might have set, we have to do a bit of finagling to filter out\n            # the values that've already been set. Variety of other solutions\n            # here: http://stackoverflow.com/q/6354436/120999\n            context_data = self.get_context_data(request, context)\n            new_context_data = {k: v for (k, v) in context_data.items()\n                                if k not in self.payload}\n            self.payload.update(new_context_data)\n\n            context_data = self.get_sensitive_context_data(request, context)\n            new_context_data = {k: v for (k, v) in context_data.items()\n                                if k not in self.obfuscated_data}\n            self.obfuscated_data.update(new_context_data)\n\n    def add(self, field, value, obfuscate=False):\n        # There's no need to send null/empty values, the collector will act\n        # the same whether they're sent or not. Zeros are important though,\n        # so we can't use a simple boolean truth check here.\n        if value is None or value == \"\":\n            return\n\n        if obfuscate:\n            self.obfuscated_data[field] = value\n        else:\n            self.payload[field] = value\n\n    def add_text(self, key, value, obfuscate=False):\n        self.add(key, value, obfuscate=obfuscate)\n        for k, v in charset_summary(value).iteritems():\n            self.add(\"{}_{}\".format(key, k), v)\n\n    def add_target_fields(self, target):\n        if not target:\n            return\n        from r2.models import Comment, Link, Message\n\n        self.add(\"target_id\", target._id)\n        self.add(\"target_fullname\", target._fullname)\n        self.add(\"target_age_seconds\", target._age.total_seconds())\n\n        target_type = target.__class__.__name__.lower()\n        if target_type == \"link\" and target.is_self:\n            target_type = \"self\"\n        self.add(\"target_type\", target_type)\n\n        # If the target is an Account or Subreddit (or has a \"name\" attr),\n        # add the target_name\n        if hasattr(target, \"name\"):\n            self.add(\"target_name\", target.name)\n\n        # Add info about the target's author for comments, links, & messages\n        if isinstance(target, (Comment, Link, Message)):\n            author = target.author_slow\n            if target._deleted or author._deleted:\n                self.add(\"target_author_id\", 0)\n                self.add(\"target_author_name\", \"[deleted]\")\n            else:\n                self.add(\"target_author_id\", author._id)\n                self.add(\"target_author_name\", author.name)\n\n        # Add info about the url being linked to for link posts\n        if isinstance(target, Link):\n            self.add_text(\"target_title\", target.title)\n            if not target.is_self:\n                self.add(\"target_url\", target.url)\n                self.add(\"target_url_domain\", target.link_domain())\n\n        # Add info about the link being commented on for comments\n        if isinstance(target, Comment):\n            link_fullname = Link._fullname_from_id36(to36(target.link_id))\n            self.add(\"link_id\", target.link_id)\n            self.add(\"link_fullname\", link_fullname)\n\n        # Add info about when target was originally posted for links/comments\n        if isinstance(target, (Comment, Link)):\n            self.add(\"target_created_ts\", to_epoch_milliseconds(target._date))\n\n        hooks.get_hook(\"eventcollector.add_target_fields\").call(\n            event=self,\n            target=target,\n        )\n\n    def add_subreddit_fields(self, subreddit):\n        if not subreddit:\n            return\n\n        self.add(\"sr_id\", subreddit._id)\n        self.add(\"sr_name\", subreddit.name)\n\n    def get(self, field, obfuscated=False):\n        if obfuscated:\n            return self.obfuscated_data.get(field, None)\n        else:\n            return self.payload.get(field, None)\n\n    @classmethod\n    def get_context_data(self, request, context):\n        \"\"\"Extract common data from the current request and context\n\n        This is generally done explicitly in `__init__`, but is done by hand for\n        votes before the request context is lost by the queuing.\n\n        request, context: Should be pylons.request & pylons.c respectively\n        \"\"\"\n        data = {}\n\n        if context.user_is_loggedin:\n            data[\"user_id\"] = context.user._id\n            data[\"user_name\"] = context.user.name\n        else:\n            if context.loid:\n                data.update(context.loid.to_dict())\n\n        oauth2_client = getattr(context, \"oauth2_client\", None)\n        if oauth2_client:\n            data[\"oauth2_client_id\"] = oauth2_client._id\n            data[\"oauth2_client_name\"] = oauth2_client.name\n            data[\"oauth2_client_app_type\"] = oauth2_client.app_type\n\n        data[\"geoip_country\"] = get_request_location(request, context)\n        data[\"domain\"] = request.host\n        data[\"user_agent\"] = request.user_agent\n        data[\"user_agent_parsed\"] = request.parsed_agent.to_dict()\n\n        http_referrer = request.headers.get(\"Referer\", None)\n        if http_referrer:\n            data[\"referrer_url\"] = http_referrer\n            data[\"referrer_domain\"] = domain(http_referrer)\n\n        hooks.get_hook(\"eventcollector.context_data\").call(\n            data=data,\n            user=context.user,\n            request=request,\n            context=context,\n        )\n\n        return data\n\n    @classmethod\n    def get_sensitive_context_data(self, request, context):\n        data = {}\n        ip = getattr(request, \"ip\", None)\n        if ip:\n            data[\"client_ip\"] = ip\n            # since we obfuscate IP addresses in the DS pipeline, we can't\n            # extract the subnet for analysis after this step. So, pre-generate\n            # (and separately obfuscate) the subnets.\n            if \".\" in ip:\n                octets = ip.split(\".\")\n                data[\"client_ipv4_24\"] = \".\".join(octets[:3])\n                data[\"client_ipv4_16\"] = \".\".join(octets[:2])\n\n        return data\n\n    def dump(self):\n        \"\"\"Returns the JSON representation of the event.\"\"\"\n        data = {\n            \"event_topic\": self.topic,\n            \"event_type\": self.event_type,\n            \"event_ts\": self.timestamp,\n            \"uuid\": self.uuid,\n            \"payload\": self.payload,\n        }\n        if self.obfuscated_data:\n            data[\"payload\"][\"obfuscated_data\"] = self.obfuscated_data\n\n        return json.dumps(data)\n\n\nclass PublishableEvent(object):\n    def __init__(self, data, truncatable_field=None):\n        self.data = data\n        self.truncatable_field = truncatable_field\n\n    def __len__(self):\n        return len(self.data)\n\n    def truncate_data(self, target_len):\n        if not self.truncatable_field:\n            return\n\n        if len(self.data) <= target_len:\n            return\n\n        # this will over-truncate with unicode characters, but it shouldn't be\n        # important to cut it as close as possible\n        oversize_by = len(self.data) - target_len\n\n        # make space for the is_truncated field we're going to add\n        oversize_by += len('\"is_truncated\": true, ')\n\n        deserialized_data = json.loads(self.data)\n\n        original = deserialized_data[\"payload\"][self.truncatable_field]\n        truncated = original[:-oversize_by]\n        deserialized_data[\"payload\"][self.truncatable_field] = truncated\n        deserialized_data[\"payload\"][\"is_truncated\"] = True\n\n        self.data = json.dumps(deserialized_data)\n\n        g.stats.simple_event(\"eventcollector.oversize_truncated\")\n\n\nclass EventPublisher(object):\n    # The largest JSON string for a single event in bytes (but it's encoded\n    # to ASCII, so this is the same as character length)\n    MAX_EVENT_SIZE = 100 * 1024\n\n    # The largest combined total JSON string that can be sent (multiple events)\n    MAX_CONTENT_LENGTH = 500 * 1024\n\n    def __init__(self, url, signature_key, secret, user_agent, stats,\n            max_event_size=MAX_EVENT_SIZE, max_content_length=MAX_CONTENT_LENGTH,\n            timeout=None):\n        self.url = url\n        self.signature_key = signature_key\n        self.secret = secret\n        self.user_agent = user_agent\n        self.timeout = timeout\n        self.stats = stats\n        self.max_event_size = max_event_size\n        self.max_content_length = max_content_length\n\n        self.session = requests.Session()\n\n    def _make_signature(self, payload):\n        mac = hmac.new(self.secret, payload, hashlib.sha256).hexdigest()\n        return \"key={key}, mac={mac}\".format(key=self.signature_key, mac=mac)\n\n    def _publish(self, events):\n        # Note: If how the JSON payload is created is changed,\n        # update the content-length estimations in `_chunk_events`\n        data = \"[\" + \", \".join(events) + \"]\"\n\n        headers = {\n            \"Date\": _make_http_date(),\n            \"User-Agent\": self.user_agent,\n            \"Content-Type\": \"application/json\",\n            \"X-Signature\": self._make_signature(data),\n        }\n\n        # Gzip body\n        use_gzip = (g.live_config.get(\"events_collector_use_gzip_chance\", 0) >\n                    random.random())\n        if use_gzip:\n            f = StringIO()\n            gzip.GzipFile(fileobj=f, mode='wb').write(data)\n            data = f.getvalue()\n            headers[\"Content-Encoding\"] = \"gzip\"\n\n        # Post events\n        with self.stats.get_timer(\"providers.event_collector\"):\n            resp = self.session.post(self.url, data=data,\n                                     headers=headers, timeout=self.timeout)\n            return resp\n\n    def _chunk_events(self, events):\n        \"\"\"Break a PublishableEvent list into chunks to obey size limits.\n\n        Note that this yields lists of strings (the serialized data) to\n        publish directly, not PublishableEvent objects.\n\n        \"\"\"\n        to_send = []\n        send_size = 0\n\n        for event in events:\n            # make sure the event is inside the size limit, and drop it if\n            # truncation wasn't possible (or didn't make it small enough)\n            event.truncate_data(self.max_event_size)\n            if len(event) > self.max_event_size:\n                g.log.warning(\"Event too large (%s); dropping\", len(event))\n                g.log.warning(\"%r\", event.data)\n                g.stats.simple_event(\"eventcollector.oversize_dropped\")\n                continue\n\n            # increase estimated content-length by length of message,\n            # plus the length of the `, ` used to join the events JSON\n            # if there will be more than one event in the list\n            send_size += len(event)\n            if len(to_send) > 0:\n                send_size += len(\", \")\n\n            # If adding this event would put us over the batch limit, yield\n            # the current set of events first. Note that we add 2 chars to the\n            # send_size to account for the square brackets around the list of\n            # events when serialized to JSON\n            if send_size + 2 >= self.max_content_length:\n                yield to_send\n                to_send = []\n                send_size = len(event)\n\n            to_send.append(event.data)\n\n        if to_send:\n            yield to_send\n\n    def publish(self, events):\n        for some_events in self._chunk_events(events):\n            resp = self._publish(some_events)\n            # read from resp.content, so that the connection can be re-used\n            # http://docs.python-requests.org/en/latest/user/advanced/#keep-alive\n            ignored = resp.content\n            yield resp, some_events\n\n\ndef _get_reason(response):\n    return (getattr(response, \"reason\", None) or\n            getattr(response.raw, \"reason\", \"{unknown}\"))\n\n\ndef process_events(g, timeout=5.0, **kw):\n    publisher = EventPublisher(\n        g.events_collector_url,\n        g.secrets[\"events_collector_key\"],\n        g.secrets[\"events_collector_secret\"],\n        g.useragent,\n        g.stats,\n        timeout=timeout,\n    )\n    test_publisher = EventPublisher(\n        g.events_collector_test_url,\n        g.secrets[\"events_collector_key\"],\n        g.secrets[\"events_collector_secret\"],\n        g.useragent,\n        g.stats,\n        timeout=timeout,\n    )\n\n    @g.stats.amqp_processor(\"event_collector\")\n    def processor(msgs, chan):\n        events = []\n        test_events = []\n\n        for msg in msgs:\n            headers = msg.properties.get(\"application_headers\", {})\n            truncatable_field = headers.get(\"truncatable_field\")\n\n            event = PublishableEvent(msg.body, truncatable_field)\n            if msg.delivery_info[\"routing_key\"] == \"event_collector_test\":\n                test_events.append(event)\n            else:\n                events.append(event)\n\n        to_publish = itertools.chain(\n            publisher.publish(events),\n            test_publisher.publish(test_events),\n        )\n        for response, sent in to_publish:\n            if response.ok:\n                g.log.info(\"Published %s events\", len(sent))\n            else:\n                g.log.warning(\n                    \"Event send failed %s - %s\",\n                    response.status_code,\n                    _get_reason(response),\n                )\n                g.log.warning(\"Response headers: %r\", response.headers)\n\n                # if the events were too large, move them into a separate\n                # queue to get them out of here, since they'll always fail\n                if response.status_code == 413:\n                    for event in sent:\n                        amqp.add_item(\"event_collector_failed\", event)\n                else:\n                    response.raise_for_status()\n\n    amqp.handle_items(\"event_collector\", processor, **kw)\n"
  },
  {
    "path": "r2/r2/lib/export.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport sys\n\n__all__ = [\"export\", \"ExportError\"]\n\n\nclass ExportError(Exception):\n    def __init__(self, module):\n        msg = \"Missing __all__ declaration in module %s.  \" \\\n              \"@export cannot be used without declaring __all__ \" \\\n              \"in that module.\" % (module)\n        Exception.__init__(self, msg)\n\n\ndef export(exported_entity):\n    \"\"\"Use a decorator to avoid retyping function/class names.\n  \n    * Based on an idea by Duncan Booth:\n    http://groups.google.com/group/comp.lang.python/msg/11cbb03e09611b8a\n    * Improved via a suggestion by Dave Angel:\n    http://groups.google.com/group/comp.lang.python/msg/3d400fb22d8a42e1\n    * Copied from Stack Overflow\n    http://stackoverflow.com/questions/6206089/is-it-a-good-practice-to-add-names-to-all-using-a-decorator\n    \"\"\"\n    all_var = sys.modules[exported_entity.__module__].__dict__.get('__all__')\n    if all_var is None:\n        raise ExportError(exported_entity.__module__)\n    if exported_entity.__name__ not in all_var:  # Prevent duplicates if run from an IDE.\n        all_var.append(exported_entity.__name__)\n    return exported_entity\n\nexport(export)  # Emulate decorating ourself\n\n"
  },
  {
    "path": "r2/r2/lib/filters.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport cgi\nimport json\nimport re\n\nfrom collections import Counter\n\nimport snudown\n\nfrom BeautifulSoup import BeautifulSoup, Tag\nfrom pylons import tmpl_context as c\nfrom pylons import app_globals as g\n\nfrom r2.lib.souptest import (\n    souptest_fragment,\n    SoupError,\n    SoupUnsupportedEntityError,\n)\nfrom r2.lib.unicode import _force_utf8, _force_unicode\n\nSC_OFF = \"<!-- SC_OFF -->\"\nSC_ON = \"<!-- SC_ON -->\"\n\nMD_START = '<div class=\"md\">'\nMD_END = '</div>'\n\nWIKI_MD_START = '<div class=\"md wiki\">'\nWIKI_MD_END = '</div>'\n\ncustom_img_url = re.compile(r'\\A%%([a-zA-Z0-9\\-]+)%%$')\n\ndef python_websafe(text):\n    return text.replace('&', \"&amp;\").replace(\"<\", \"&lt;\").replace(\">\", \"&gt;\").replace('\"', \"&quot;\")\n\ndef python_websafe_json(text):\n    return text.replace('&', \"&amp;\").replace(\"<\", \"&lt;\").replace(\">\", \"&gt;\")\n\ntry:\n    from Cfilters import uwebsafe as c_websafe, uspace_compress, \\\n        uwebsafe_json as c_websafe_json\n    def spaceCompress(text):\n        try:\n            text = unicode(text, 'utf-8')\n        except TypeError:\n            text = unicode(text)\n        return uspace_compress(text)\nexcept ImportError:\n    c_websafe      = python_websafe\n    c_websafe_json = python_websafe_json\n    _between_tags1 = re.compile('> +')\n    _between_tags2 = re.compile(' +<')\n    _spaces = re.compile('[\\s]+')\n    _ignore = re.compile('(' + SC_OFF + '|' + SC_ON + ')', re.S | re.I)\n    def spaceCompress(content):\n        res = ''\n        sc = True\n        for p in _ignore.split(content):\n            if p == SC_ON:\n                sc = True\n            elif p == SC_OFF:\n                sc = False\n            elif sc:\n                p = _spaces.sub(' ', p)\n                p = _between_tags1.sub('>', p)\n                p = _between_tags2.sub('<', p)\n                res += p\n            else:\n                res += p\n\n        return res\n\n\nclass _Unsafe(unicode):\n    # Necessary so Wrapped instances with these can get cached\n    def cache_key(self, style):\n        return unicode(self)\n\n\ndef unsafe(text=''):\n    return _Unsafe(_force_unicode(text))\n\ndef websafe_json(text=\"\"):\n    return c_websafe_json(_force_unicode(text))\n\ndef double_websafe(text=\"\"):\n    # RSS requires double escaping on fields that could be interpreted as HTML\n    return unsafe(python_websafe(python_websafe(text)))\n\ndef conditional_websafe(text = ''):\n    from wrapped import Templated, CacheStub\n\n    if text.__class__ == _Unsafe:\n        return text\n    elif isinstance(text, Templated):\n        return _Unsafe(text.render())\n    elif isinstance(text, CacheStub):\n        return _Unsafe(text)\n    elif text is None:\n        return \"\"\n    elif text.__class__ != unicode:\n        text = _force_unicode(text)\n    return c_websafe(text)\n\n\ndef mako_websafe(text=''):\n    \"\"\"Wrapper for conditional_websafe so cached templates don't explode\"\"\"\n    return conditional_websafe(text)\n\n\ndef websafe(text=''):\n    if text.__class__ != unicode:\n        text = _force_unicode(text)\n    #wrap the response in _Unsafe so make_websafe doesn't unescape it\n    return _Unsafe(c_websafe(text))\n\n\n# From https://github.com/django/django/blob/master/django/utils/html.py\n_js_escapes = {\n    ord('\\\\'): u'\\\\u005C',\n    ord('\\''): u'\\\\u0027',\n    ord('\"'): u'\\\\u0022',\n    ord('>'): u'\\\\u003E',\n    ord('<'): u'\\\\u003C',\n    ord('&'): u'\\\\u0026',\n    ord('='): u'\\\\u003D',\n    ord('-'): u'\\\\u002D',\n    ord(';'): u'\\\\u003B',\n    ord(u'\\u2028'): u'\\\\u2028',\n    ord(u'\\u2029'): u'\\\\u2029',\n}\n# Escape every ASCII character with a value less than 32.\n_js_escapes.update((ord('%c' % z), u'\\\\u%04X' % z) for z in range(32))\n\n\ndef jssafe(text=u''):\n    \"\"\"Prevents text from breaking outside of string literals in JS\"\"\"\n    if text.__class__ != unicode:\n        text = _force_unicode(text)\n    # wrap the response in _Unsafe so conditional_websafe doesn't touch it\n    return _Unsafe(text.translate(_js_escapes))\n\n\n_json_escapes = {\n    ord('>'): u'\\\\u003E',\n    ord('<'): u'\\\\u003C',\n    ord('&'): u'\\\\u0026',\n}\n\n\ndef scriptsafe_dumps(obj, **kwargs):\n    \"\"\"\n    Like `json.dumps()`, but safe for use in `<script>` blocks.\n\n    Also nice for response bodies that might be consumed by terrible browsers!\n\n    You should avoid using this to template data into inline event handlers.\n    When possible, you should do something like this instead:\n    ```\n    <button\n      onclick=\"console.log($(this).data('json-thing'))\"\n      data-json-thing=\"${json_thing}\">\n    </button>\n    ```\n    \"\"\"\n    text = _force_unicode(json.dumps(obj, **kwargs))\n    # wrap the response in _Unsafe so conditional_websafe doesn't touch it\n    # TODO: this might be a hot path soon, C-ify it?\n    return _Unsafe(text.translate(_json_escapes))\n\n\ndef markdown_souptest(text, nofollow=False, target=None, renderer='reddit'):\n    if not text:\n        return text\n    \n    if renderer == 'reddit':\n        smd = safemarkdown(text, nofollow=nofollow, target=target)\n    elif renderer == 'wiki':\n        smd = wikimarkdown(text)\n\n    souptest_fragment(smd)\n\n    return smd\n\ndef safemarkdown(text, nofollow=False, wrap=True, **kwargs):\n    if not text:\n        return None\n\n    target = kwargs.get(\"target\", None)\n    text = snudown.markdown(_force_utf8(text), nofollow, target)\n\n    if wrap:\n        return SC_OFF + MD_START + text + MD_END + SC_ON\n    else:\n        return SC_OFF + text + SC_ON\n\ndef wikimarkdown(text, include_toc=True, target=None):\n    from r2.lib.template_helpers import make_url_protocol_relative\n\n    # this hard codes the stylesheet page for now, but should be parameterized\n    # in the future to allow per-page images.\n    from r2.models.wiki import ImagesByWikiPage\n    from r2.lib.utils import UrlParser\n    from r2.lib.template_helpers import add_sr\n    page_images = ImagesByWikiPage.get_images(c.site, \"config/stylesheet\")\n    \n    def img_swap(tag):\n        name = tag.get('src')\n        name = custom_img_url.search(name)\n        name = name and name.group(1)\n        if name and name in page_images:\n            url = page_images[name]\n            url = make_url_protocol_relative(url)\n            tag['src'] = url\n        else:\n            tag.extract()\n    \n    nofollow = True\n    \n    text = snudown.markdown(_force_utf8(text), nofollow, target,\n                            renderer=snudown.RENDERER_WIKI)\n    \n    # TODO: We should test how much of a load this adds to the app\n    soup = BeautifulSoup(text.decode('utf-8'))\n    images = soup.findAll('img')\n    \n    if images:\n        [img_swap(image) for image in images]\n\n    def add_ext_to_link(link):\n        url = UrlParser(link.get('href'))\n        if url.is_reddit_url():\n            link['href'] = add_sr(link.get('href'), sr_path=False)\n\n    if c.render_style == 'compact':\n        links = soup.findAll('a')\n        [add_ext_to_link(a) for a in links]\n\n    if include_toc:\n        tocdiv = generate_table_of_contents(soup, prefix=\"wiki\")\n        if tocdiv:\n            soup.insert(0, tocdiv)\n    \n    text = str(soup)\n    \n    return SC_OFF + WIKI_MD_START + text + WIKI_MD_END + SC_ON\n\ntitle_re = re.compile('[^\\w.-]')\nheader_re = re.compile('^h[1-6]$')\ndef generate_table_of_contents(soup, prefix):\n    header_ids = Counter()\n    headers = soup.findAll(header_re)\n    if not headers:\n        return\n    tocdiv = Tag(soup, \"div\", [(\"class\", \"toc\")])\n    parent = Tag(soup, \"ul\")\n    parent.level = 0\n    tocdiv.append(parent)\n    level = 0\n    previous = 0\n    for header in headers:\n        contents = u''.join(header.findAll(text=True))\n        \n        # In the event of an empty header, skip\n        if not contents:\n            continue\n        \n        # Convert html entities to avoid ugly header ids\n        aid = unicode(BeautifulSoup(contents, convertEntities=BeautifulSoup.XML_ENTITIES))\n        # Prefix with PREFIX_ to avoid ID conflict with the rest of the page\n        aid = u'%s_%s' % (prefix, aid.replace(\" \", \"_\").lower())\n        # Convert down to ascii replacing special characters with hex\n        aid = str(title_re.sub(lambda c: '.%X' % ord(c.group()), aid))\n        \n        # Check to see if a tag with the same ID exists\n        id_num = header_ids[aid] + 1\n        header_ids[aid] += 1\n        # Only start numbering ids with the second instance of an id\n        if id_num > 1:\n            aid = '%s%d' % (aid, id_num)\n        \n        header['id'] = aid\n        \n        li = Tag(soup, \"li\", [(\"class\", aid)])\n        a = Tag(soup, \"a\", [(\"href\", \"#%s\" % aid)])\n        a.string = contents\n        li.append(a)\n        \n        thislevel = int(header.name[-1])\n        \n        if previous and thislevel > previous:\n            newul = Tag(soup, \"ul\")\n            newul.level = thislevel\n            newli = Tag(soup, \"li\", [(\"class\", \"toc_child\")])\n            newli.append(newul)\n            parent.append(newli)\n            parent = newul\n            level += 1\n        elif level and thislevel < previous:\n            while level and parent.level > thislevel:\n                parent = parent.findParent(\"ul\")\n                level -= 1\n        \n        previous = thislevel\n        parent.append(li)\n    \n    return tocdiv\n\n\ndef keep_space(text):\n    text = websafe(text)\n    for i in \" \\n\\r\\t\":\n        text=text.replace(i,'&#%02d;' % ord(i))\n    return unsafe(text)\n\n\ndef unkeep_space(text):\n    return text.replace('&#32;', ' ').replace('&#10;', '\\n').replace('&#09;', '\\t')\n"
  },
  {
    "path": "r2/r2/lib/generate_strings.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\n\nfunny_translatable_strings = {\n    \"500_page\": [\"Funny 500 page message %d\" % i for i in xrange(1, 11)],\n    \"create_subreddit\": [\n        \"Reason to create a reddit %d\" % i for i in xrange(1, 21)],\n}\n\ndef generate_strings():\n    \"\"\"Print out automatically generated strings for translation.\"\"\"\n\n    # used by error pages and in the sidebar for why to create a subreddit\n    for category, strings in funny_translatable_strings.iteritems():\n        for string in strings:\n            print \"# TRANSLATORS: Do not translate literally. Come up with a funny/relevant phrase (see the English version for ideas.) Accepts markdown formatting.\"\n            print \"print _('\" + string + \"')\"\n\n    # these are used in r2.lib.pages.trafficpages\n    INTERVALS = (\"hour\", \"day\", \"month\")\n    TYPES = (\"uniques\", \"pageviews\", \"traffic\", \"impressions\", \"clicks\")\n    for interval in INTERVALS:\n        for type in TYPES:\n            print \"print _('%s by %s')\" % (type, interval)\n\n\nif __name__ == \"__main__\":\n    generate_strings()\n"
  },
  {
    "path": "r2/r2/lib/geoip.py",
    "content": "#!/usr/bin/python\n# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport datetime\nimport httplib\nimport json\nimport os\nimport socket\nimport urllib2\n\nfrom pylons import app_globals as g\n\nfrom r2.lib.sgm import sgm\nfrom r2.lib.utils import in_chunks, tup\n\n# If the geoip service has nginx in front of it there is a default limit of 8kb:\n#   http://wiki.nginx.org/NginxHttpCoreModule#large_client_header_buffers\n# >>> len('GET /geoip/' + '+'.join(['255.255.255.255'] * 500) + ' HTTP/1.1')\n# 8019\nMAX_IPS_PER_GROUP = 500\n\nGEOIP_CACHE_TIME = datetime.timedelta(days=7).total_seconds()\n\ndef _location_by_ips(ips):\n    if not hasattr(g, 'geoip_location'):\n        g.log.warning(\"g.geoip_location not set. skipping GeoIP lookup.\")\n        return {}\n\n    ret = {}\n    for batch in in_chunks(ips, MAX_IPS_PER_GROUP):\n        ip_string = '+'.join(batch)\n        url = os.path.join(g.geoip_location, 'geoip', ip_string)\n\n        try:\n            response = urllib2.urlopen(url=url, timeout=3)\n            json_data = response.read()\n        except (urllib2.URLError, httplib.HTTPException, socket.error) as e:\n            g.log.warning(\"Failed to fetch GeoIP information: %r\" % e)\n            continue\n\n        try:\n            ret.update(json.loads(json_data))\n        except ValueError, e:\n            g.log.warning(\"Invalid JSON response for GeoIP lookup: %r\" % e)\n            continue\n    return ret\n\n\ndef _organization_by_ips(ips):\n    if not hasattr(g, 'geoip_location'):\n        g.log.warning(\"g.geoip_location not set. skipping GeoIP lookup.\")\n        return {}\n\n    ip_string = '+'.join(set(ips))\n    url = os.path.join(g.geoip_location, 'org', ip_string)\n\n    try:\n        response = urllib2.urlopen(url=url, timeout=3)\n        json_data = response.read()\n    except urllib2.URLError, e:\n        g.log.warning(\"Failed to fetch GeoIP information: %r\" % e)\n        return {}\n\n    try:\n        return json.loads(json_data)\n    except ValueError, e:\n        g.log.warning(\"Invalid JSON response for GeoIP lookup: %r\" % e)\n        return {}\n\n\ndef location_by_ips(ips):\n    ips, is_single = tup(ips, ret_is_single=True)\n    location_by_ip = sgm(\n        cache=g.gencache,\n        keys=ips,\n        miss_fn=_location_by_ips,\n        prefix='geoip:loc_',\n        time=GEOIP_CACHE_TIME,\n        ignore_set_errors=True,\n    )\n    if is_single and location_by_ip:\n        return location_by_ip[ips[0]]\n    else:\n        return location_by_ip\n\n\ndef organization_by_ips(ips):\n    ips, is_single = tup(ips, ret_is_single=True)\n    organization_by_ip = sgm(\n        cache=g.gencache,\n        keys=ips,\n        miss_fn=_organization_by_ips,\n        prefix='geoip:org_',\n        time=GEOIP_CACHE_TIME,\n        ignore_set_errors=True,\n    )\n    if is_single and organization_by_ip:\n        return organization_by_ip[ips[0]]\n    else:\n        return organization_by_ip\n\n\ndef get_request_location(request, context):\n    \"\"\"Determine country of origin of the `request` for the given `context`\n\n    This is done by:\n     * checking the CDN headers for country of origin if set\n     * falling back on geocoding request.ip address against the geocoder service\n    The resulting location is memoized on context on `context.location`\n\n    request, context: Should be pylons.request & pylons.c respectively;\n    \"\"\"\n    if context.location != '':\n        # unset context attributes have the value ''\n        return context.location\n\n    context.location = None\n\n    if getattr(request, 'via_cdn', False):\n        g.stats.simple_event('geoip.cdn_request')\n        cdn_geoinfo = g.cdn_provider.get_client_location(request.environ)\n        if cdn_geoinfo:\n            context.location = cdn_geoinfo\n    elif getattr(request, 'ip', None):\n        g.stats.simple_event('geoip.non_cdn_request')\n        timer = g.stats.get_timer(\"providers.geoip.location_by_ips\")\n        timer.start()\n        location = location_by_ips(request.ip)\n        if location:\n            context.location = location.get('country_code', None)\n        timer.stop()\n\n    return context.location\n"
  },
  {
    "path": "r2/r2/lib/gzipper.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport cStringIO\nimport gzip\nimport wsgiref.headers\n\nfrom paste.util.mimeparse import parse_mime_type, desired_matches\n\n\nENCODABLE_CONTENT_TYPES = {\n    \"application/json\",\n    \"application/javascript\",\n    \"application/xml\",\n    \"text/css\",\n    \"text/csv\",\n    \"text/html\",\n    \"text/javascript\",\n    \"text/plain\",\n    \"text/xml\",\n}\n\n\nclass GzipMiddleware(object):\n    \"\"\"A middleware that transparently compresses content with gzip.\n\n    Note: this middleware deliberately violates PEP-333 in three ways:\n\n        - it disables the use of the \"write()\" callable.\n        - it does content encoding which is a \"hop-by-hop\" feature.\n        - it does not \"yield at least one value each time its underlying\n          application yields a value\".\n\n    None of these are an issue for the reddit application, but use at your\n    own risk.\n\n    \"\"\"\n\n    def __init__(self, app, compression_level, min_size):\n        self.app = app\n        self.compression_level = compression_level\n        self.min_size = min_size\n\n    def _start_response(self, status, response_headers, exc_info=None):\n        self.status = status\n        self.headers = response_headers\n        self.exc_info = exc_info\n        return self._write_not_implemented\n\n    @staticmethod\n    def _write_not_implemented(*args, **kwargs):\n        \"\"\"Raise an exception.\n\n        This middleware doesn't work with the write callable.\n\n        \"\"\"\n        raise NotImplementedError\n\n    @staticmethod\n    def content_length(headers, app_iter):\n        \"\"\"Return the content-length of this response as best as we can tell.\n\n        If the application returned a Content-Length header we will trust it.\n        If not, we are allowed by PEP-333 to attempt to determine the length of\n        the app's iterable and if it's 1, use the length of the only chunk as\n        the content-length.\n\n        \"\"\"\n        content_length_header = headers[\"Content-Length\"]\n\n        if content_length_header:\n            return int(content_length_header)\n\n        try:\n            app_iter_len = len(app_iter)\n        except ValueError:\n            return None  # streaming response; we're done here.\n\n        if app_iter_len == 1:\n            return len(app_iter[0])\n        return None\n\n    def should_gzip_response(self, headers, app_iter):\n        # this middleware isn't smart enough to deal with stuff like ETags or\n        # content ranges at the moment. let's just bail out. (this prevents\n        # issues with pylons/paste's static file middleware)\n        if \"ETag\" in headers:\n            return False\n\n        # here we are, violating pep-333 by looking at a hop-by-hop header\n        # within the middleware chain. but this will prevent us from overriding\n        # encoding done lower down in the app, if present. so it goes.\n        if \"Content-Encoding\" in headers:\n            return False\n\n        # bail if we can't figure out how big it is or it's too small\n        content_length = self.content_length(headers, app_iter)\n        if not content_length or content_length < self.min_size:\n            return False\n\n        # make sure this is one of the content-types we're allowed to encode\n        content_type = headers[\"Content-Type\"]\n        type, subtype, params = parse_mime_type(content_type)\n        if \"%s/%s\" % (type, subtype) not in ENCODABLE_CONTENT_TYPES:\n            return False\n\n        return True\n\n    @staticmethod\n    def update_vary_header(headers):\n        vary_headers = headers.get_all(\"Vary\")\n        del headers[\"Vary\"]\n\n        varies = []\n        for vary_header in vary_headers:\n            varies.extend(field.strip().lower()\n                          for field in vary_header.split(\",\"))\n\n        if \"*\" in varies:\n            varies = [\"*\"]\n        elif \"accept-encoding\" not in varies:\n            varies.append(\"accept-encoding\")\n\n        headers[\"Vary\"] = \", \".join(varies)\n\n    @staticmethod\n    def request_accepts_gzip(environ):\n        accept_encoding = environ.get(\"HTTP_ACCEPT_ENCODING\", \"identity\")\n        return \"gzip\" in desired_matches([\"gzip\"], accept_encoding)\n\n    def __call__(self, environ, start_response):\n        app_iter = self.app(environ, self._start_response)\n        headers = wsgiref.headers.Headers(self.headers)\n\n        response_compressible = self.should_gzip_response(headers, app_iter)\n        if response_compressible:\n            # this means that the sole factor left in determining whether or\n            # not to gzip is the Accept-Encoding header; we should let\n            # downstream caches know that this is the case with the Vary header\n            self.update_vary_header(headers)\n\n        if response_compressible and self.request_accepts_gzip(environ):\n            headers[\"Content-Encoding\"] = \"gzip\"\n\n            response_buffer = cStringIO.StringIO()\n            gzipper = gzip.GzipFile(fileobj=response_buffer, mode=\"wb\",\n                                    compresslevel=self.compression_level)\n            try:\n                for chunk in app_iter:\n                    gzipper.write(chunk)\n            finally:\n                if hasattr(app_iter, \"close\"):\n                    app_iter.close()\n\n            gzipper.close()\n            new_response = response_buffer.getvalue()\n            encoded_app_iter = [new_response]\n            response_buffer.close()\n\n            headers[\"Content-Length\"] = str(len(new_response))\n        else:\n            encoded_app_iter = app_iter\n\n        # send the response\n        start_response(self.status, self.headers, self.exc_info)\n        return encoded_app_iter\n\n\ndef make_gzip_middleware(app, global_conf=None, compress_level=9, min_size=0):\n    \"\"\"Return a gzip-compressing middleware.\"\"\"\n    return GzipMiddleware(app, int(compress_level), int(min_size))\n"
  },
  {
    "path": "r2/r2/lib/hadoop_decompress.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\n\nimport snappy\nimport struct\n\n\nclass HadoopStreamDecompressor(object):\n    \"\"\"This class implements the decompressor-side of the hadoop framing\n    format.\n\n    Hadoop fraiming format consists of one or more blocks, each of which is\n    composed of one or more compressed subblocks. The block size is the\n    uncompressed size, while the subblock size is the size of the compressed\n    data.\n\n    https://github.com/andrix/python-snappy/pull/35/files\n    \"\"\"\n\n    __slots__ = [\"_buf\", \"_block_size\", \"_block_read\", \"_subblock_size\"]\n\n    def __init__(self):\n        self._buf = b\"\"\n        self._block_size = None\n        self._block_read = 0\n        self._subblock_size = None\n\n    def decompress(self, data):\n        self._buf += data\n        output = b\"\"\n        while True:\n            # decompress block will attempt to decompress  any subblocks if it\n            # has already read the block size and subblock size.\n            buf = self._decompress_block()\n            if len(buf) > 0:\n                output += buf\n            else:\n                break\n        return output\n\n    def _decompress_block(self):\n        if self._block_size is None:\n            if len(self._buf) <= 4:\n                return b\"\"\n            self._block_size = struct.unpack(\">i\", self._buf[:4])[0]\n            self._buf = self._buf[4:]\n        output = b\"\"\n        while self._block_read < self._block_size:\n            buf = self._decompress_subblock()\n            if len(buf) > 0:\n                output += buf\n            else:\n                # Buffer doesn't contain full subblock\n                break\n        if self._block_read == self._block_size:\n            # We finished reading this block, so reinitialize.\n            self._block_read = 0\n            self._block_size = None\n        return output\n\n    def _decompress_subblock(self):\n        if self._subblock_size is None:\n            if len(self._buf) <= 4:\n                return b\"\"\n            self._subblock_size = struct.unpack(\">i\", self._buf[:4])[0]\n            self._buf = self._buf[4:]\n        # Only attempt to decompress complete subblocks.\n        if len(self._buf) < self._subblock_size:\n            return b\"\"\n        compressed = self._buf[:self._subblock_size]\n        self._buf = self._buf[self._subblock_size:]\n        uncompressed = snappy.uncompress(compressed)\n        self._block_read += len(uncompressed)\n        self._subblock_size = None\n        return uncompressed\n\n    def flush(self):\n        if self._buf != b\"\":\n            raise snappy.UncompressError(\"chunk truncated\")\n        return b\"\"\n\n    def copy(self):\n        copy = HadoopStreamDecompressor()\n        copy._buf = self._buf\n        copy._block_size = self._block_size\n        copy._block_read = self._block_read\n        copy._subblock_size = self._subblock_size\n        return copy\n\n\ndef hadoop_decompress(src, dst, blocksize=snappy._STREAM_TO_STREAM_BLOCK_SIZE):\n    decompressor = HadoopStreamDecompressor()\n    while True:\n        buf = src.read(blocksize)\n        if not buf:\n            break\n        buf = decompressor.decompress(buf)\n        if buf:\n            dst.write(buf)\n    decompressor.flush()  # makes sure the stream ended well\n"
  },
  {
    "path": "r2/r2/lib/hardcachebackend.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom pylons import app_globals as g\nfrom datetime import timedelta as timedelta\nfrom datetime import datetime\nimport sqlalchemy as sa\nfrom r2.lib.db.tdb_lite import tdb_lite\nimport pytz\nimport random\n\nCOUNT_CATEGORY = 'hc_count'\nELAPSED_CATEGORY = 'hc_elapsed'\nTZ = pytz.timezone(\"MST\")\n\ndef expiration_from_time(time):\n    if time <= 0:\n        raise ValueError (\"HardCache items *must* have an expiration time\")\n    return datetime.now(TZ) + timedelta(0, time)\n\nclass HardCacheBackend(object):\n    def __init__(self, gc):\n        self.tdb = tdb_lite(gc)\n        self.profile_categories = {}\n        TZ = gc.display_tz\n\n        def _table(metadata):\n            return sa.Table(gc.db_app_name + '_hardcache', metadata,\n                            sa.Column('category', sa.String, nullable = False,\n                                      primary_key = True),\n                            sa.Column('ids', sa.String, nullable = False,\n                                      primary_key = True),\n                            sa.Column('value', sa.String, nullable = False),\n                            sa.Column('kind', sa.String, nullable = False),\n                            sa.Column('expiration',\n                                      sa.DateTime(timezone = True),\n                                      nullable = False)\n                            )\n        enginenames_by_category = {}\n        all_enginenames = set()\n        for item in gc.hardcache_categories:\n            chunks = item.split(\":\")\n            if len(chunks) < 2:\n                raise ValueError(\"Invalid hardcache_overrides\")\n            category = chunks.pop(0)\n            enginenames_by_category[category] = []\n            for c in chunks:\n                if c == '!profile':\n                    self.profile_categories[category] = True\n                elif c.startswith(\"!\"):\n                    raise ValueError(\"WTF is [%s] in hardcache_overrides?\" % c)\n                else:\n                    all_enginenames.add(c)\n                    enginenames_by_category[category].append(c)\n\n        assert('*' in enginenames_by_category.keys())\n\n        engines_by_enginename = {}\n        for enginename in all_enginenames:\n            engine = gc.dbm.get_engine(enginename)\n            md = self.tdb.make_metadata(engine)\n            table = _table(md)\n            indstr = self.tdb.index_str(table, 'expiration', 'expiration')\n            self.tdb.create_table(table, [ indstr ])\n            engines_by_enginename[enginename] = table\n\n        self.mapping = {}\n        for category, enginenames in enginenames_by_category.iteritems():\n            self.mapping[category] = [ engines_by_enginename[e]\n                                       for e in enginenames]\n\n    def engine_by_category(self, category, type=\"master\"):\n        if category not in self.mapping:\n            category = '*'\n        engines = self.mapping[category]\n        if type == 'master':\n            return engines[0]\n        elif type == 'readslave':\n            return random.choice(engines[1:])\n        else:\n            raise ValueError(\"invalid type %s\" % type)\n\n    def profile_start(self, operation, category):\n        if category == COUNT_CATEGORY:\n            return None\n\n        if category == ELAPSED_CATEGORY:\n            return None\n\n        if category in self.mapping:\n            effective_category = category\n        else:\n            effective_category = '*'\n\n        if effective_category not in self.profile_categories:\n            return None\n\n        return (datetime.now(TZ), operation, category)\n\n    def profile_stop(self, t):\n        if t is None:\n            return\n\n        start_time, operation, category = t\n\n        end_time = datetime.now(TZ)\n\n        period = end_time.strftime(\"%Y/%m/%d_%H:%M\")[:-1] + 'x'\n\n        elapsed = end_time - start_time\n        msec = elapsed.seconds * 1000 + elapsed.microseconds / 1000\n\n        ids = \"-\".join((operation, category, period))\n\n        self.add(COUNT_CATEGORY, ids, 0, time=86400)\n        self.add(ELAPSED_CATEGORY, ids, 0, time=86400)\n\n        self.incr(COUNT_CATEGORY, ids, time=86400)\n        self.incr(ELAPSED_CATEGORY, ids, time=86400, delta=msec)\n\n\n    def set(self, category, ids, val, time):\n\n        self.delete(category, ids) # delete it if it already exists\n\n        value, kind = self.tdb.py2db(val, True)\n\n        expiration = expiration_from_time(time)\n\n        prof = self.profile_start('set', category)\n\n        engine = self.engine_by_category(category, \"master\")\n\n        engine.insert().execute(\n            category=category,\n            ids=ids,\n            value=value,\n            kind=kind,\n            expiration=expiration\n            )\n\n        self.profile_stop(prof)\n\n    def add(self, category, ids, val, time=0):\n        self.delete_if_expired(category, ids)\n\n        expiration = expiration_from_time(time)\n\n        value, kind = self.tdb.py2db(val, True)\n\n        prof = self.profile_start('add', category)\n\n        engine = self.engine_by_category(category, \"master\")\n\n        try:\n            rp = engine.insert().execute(\n                category=category,\n                ids=ids,\n                value=value,\n                kind=kind,\n                expiration=expiration\n                )\n            self.profile_stop(prof)\n            return value\n\n        except sa.exc.IntegrityError, e:\n            self.profile_stop(prof)\n            return self.get(category, ids, force_write_table=True)\n\n    def incr(self, category, ids, time=0, delta=1):\n        self.delete_if_expired(category, ids)\n\n        expiration = expiration_from_time(time)\n\n        prof = self.profile_start('incr', category)\n\n        engine = self.engine_by_category(category, \"master\")\n\n        rp = engine.update(sa.and_(engine.c.category==category,\n                                   engine.c.ids==ids,\n                                   engine.c.kind=='num'),\n                           values = {\n                                   engine.c.value:\n                                           sa.cast(\n                                           sa.cast(engine.c.value, sa.Integer)\n                                           + delta, sa.String),\n                                   engine.c.expiration: expiration\n                                   }\n                           ).execute()\n\n        self.profile_stop(prof)\n\n        if rp.rowcount == 1:\n            return self.get(category, ids, force_write_table=True)\n        elif rp.rowcount == 0:\n            existing_value = self.get(category, ids, force_write_table=True)\n            if existing_value is None:\n                raise ValueError(\"[%s][%s] can't be incr()ed -- it's not set\" %\n                                 (category, ids))\n            else:\n                raise ValueError(\"[%s][%s] has non-integer value %r\" %\n                                 (category, ids, existing_value))\n        else:\n            raise ValueError(\"Somehow %d rows got updated\" % rp.rowcount)\n\n    def get(self, category, ids, force_write_table=False):\n        if force_write_table:\n            type = \"master\"\n        else:\n            type = \"readslave\"\n\n        engine = self.engine_by_category(category, type)\n\n        prof = self.profile_start('get', category)\n\n        s = sa.select([engine.c.value,\n                       engine.c.kind,\n                       engine.c.expiration],\n                      sa.and_(engine.c.category==category,\n                              engine.c.ids==ids),\n                      limit = 1)\n        rows = s.execute().fetchall()\n\n        self.profile_stop(prof)\n\n        if len(rows) < 1:\n            return None\n        elif rows[0].expiration < datetime.now(TZ):\n            return None\n        else:\n            return self.tdb.db2py(rows[0].value, rows[0].kind)\n\n    def get_multi(self, category, idses):\n        prof = self.profile_start('get_multi', category)\n\n        engine = self.engine_by_category(category, \"readslave\")\n\n        s = sa.select([engine.c.ids,\n                       engine.c.value,\n                       engine.c.kind,\n                       engine.c.expiration],\n                      sa.and_(engine.c.category==category,\n                              sa.or_(*[engine.c.ids==ids\n                                       for ids in idses])))\n        rows = s.execute().fetchall()\n\n        self.profile_stop(prof)\n\n        results = {}\n\n        for row in rows:\n          if row.expiration >= datetime.now(TZ):\n              k = \"%s-%s\" % (category, row.ids)\n              results[k] = self.tdb.db2py(row.value, row.kind)\n\n        return results\n\n    def delete(self, category, ids):\n        prof = self.profile_start('delete', category)\n        engine = self.engine_by_category(category, \"master\")\n        engine.delete(\n            sa.and_(engine.c.category==category,\n                    engine.c.ids==ids)).execute()\n        self.profile_stop(prof)\n\n    def ids_by_category(self, category, limit=1000):\n        prof = self.profile_start('ids_by_category', category)\n        engine = self.engine_by_category(category, \"readslave\")\n        s = sa.select([engine.c.ids],\n                      sa.and_(engine.c.category==category,\n                              engine.c.expiration > datetime.now(TZ)),\n                      limit = limit)\n        rows = s.execute().fetchall()\n        self.profile_stop(prof)\n        return [ r.ids for r in rows ]\n\n    def clause_from_expiration(self, engine, expiration):\n        if expiration is None:\n            return True\n        elif expiration == \"now\":\n            return engine.c.expiration < datetime.now(TZ)\n        else:\n            return engine.c.expiration < expiration\n\n    def expired(self, engine, expiration_clause, limit=1000):\n        s = sa.select([engine.c.category,\n                       engine.c.ids,\n                       engine.c.expiration],\n                      expiration_clause,\n                      limit = limit,\n                      order_by = engine.c.expiration\n                      )\n        rows = s.execute().fetchall()\n        return [ (r.expiration, r.category, r.ids) for r in rows ]\n\n    def delete_if_expired(self, category, ids, expiration=\"now\"):\n        prof = self.profile_start('delete_if_expired', category)\n        engine = self.engine_by_category(category, \"master\")\n        expiration_clause = self.clause_from_expiration(engine, expiration)\n        engine.delete(sa.and_(engine.c.category==category,\n                              engine.c.ids==ids,\n                              expiration_clause)).execute()\n        self.profile_stop(prof)\n\n\ndef delete_expired(expiration=\"now\", limit=5000):\n    # the following depends on the structure of g.hardcache not changing\n    backend = g.hardcache.caches[1].backend\n    # localcache = g.hardcache.caches[0]\n\n    masters = set()\n\n    for engines in backend.mapping.values():\n        masters.add(engines[0])\n\n    for engine in masters:\n        expiration_clause = backend.clause_from_expiration(engine, expiration)\n\n        # Get all the expired keys\n        rows = backend.expired(engine, expiration_clause, limit)\n\n        if len(rows) == 0:\n            continue\n\n        # Delete from the backend.\n        engine.delete(expiration_clause).execute()\n"
  },
  {
    "path": "r2/r2/lib/helpers.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\n\"\"\"\nHelper functions\n\nAll names available in this module will be available under the Pylons h object.\n\"\"\"\n"
  },
  {
    "path": "r2/r2/lib/hooks.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\"\"\"A very simple system for event hooks for plugins etc.\n\nIn general, you will probably want to use a ``HookRegistrar`` to manage your\nhooks.  The file that contains the code you want to hook into will look\nsomething like this::\n\n    from r2.lib import hooks\n    \n    def foo(spam):\n        # Do a little bit of this and a little bit of that.\n        eggs = this(spam)\n        baked_beans = that(eggs)\n    \n        hooks.get_hook('foo').call(ingredient=baked_beans)\n\nThen, any place you want to hook into it, just throw on a decorator::\n\n    from r2.lib.hooks import HookRegistrar\n    hooks = HookRegistrar()\n    \n    @hooks.on('foo')\n    def bar(ingredient):\n        print ingredient\n\n    hooks.register_all()\n\"\"\"\n\n\n_HOOKS = {}\n\n\ndef all_hooks():\n    \"\"\"Return all registered hooks.\"\"\"\n    return _HOOKS\n\n\nclass Hook(object):\n    \"\"\"A single hook that can be listened for.\"\"\"\n    def __init__(self):\n        self.handlers = []\n\n    def register_handler(self, handler):\n        \"\"\"Register a handler to call from this hook.\"\"\"\n        self.handlers.append(handler)\n\n    def call(self, **kwargs):\n        \"\"\"Call handlers and return their results.\n\n        Handlers will be called in the same order they were registered and\n        their results will be returned in the same order as well.\n\n        \"\"\"\n        return [handler(**kwargs) for handler in self.handlers]\n\n    def call_until_return(self, **kwargs):\n        \"\"\"Call handlers until one returns a non-None value.\n\n        As with call, handlers are called in the same order they are\n        registered.  Only the return value of the first non-None handler is\n        returned.\n\n        \"\"\"\n        for handler in self.handlers:\n            ret = handler(**kwargs)\n            if ret is not None:\n                return ret\n\n\ndef get_hook(name):\n    \"\"\"Return the named hook `name` creating it if necessary.\"\"\"\n    # this should be atomic as long as `name`'s __hash__ isn't python code\n    # or for all types after the fixes in python#13521 are merged into 2.7.\n    return _HOOKS.setdefault(name, Hook())\n\n\nclass HookRegistrar(object):\n    \"\"\"A registry for deferring module-scope hook registrations.\n\n    This registry allows us to use module-level decorators but not actually\n    register with global hooks unless we're told to.\n\n    \"\"\"\n    def __init__(self):\n        self.registered = False\n        self.connections = []\n\n    def on(self, name):\n        \"\"\"Return a decorator that registers the wrapped function.\"\"\"\n\n        hook = get_hook(name)\n\n        def hook_decorator(fn):\n            if self.registered:\n                hook.register_handler(fn)\n            else:\n                self.connections.append((hook, fn))\n            return fn\n        return hook_decorator\n\n    def register_all(self):\n        \"\"\"Complete all deferred registrations.\"\"\"\n        for hook, handler in self.connections:\n            hook.register_handler(handler)\n        self.registered = True\n"
  },
  {
    "path": "r2/r2/lib/inventory.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\n\nfrom collections import defaultdict, OrderedDict\nfrom datetime import datetime, timedelta\nimport re\n\nfrom itertools import chain\nfrom sqlalchemy import func\n\nfrom r2.lib.inventory_optimization import get_maximized_pageviews\nfrom r2.lib.memoize import memoize\nfrom r2.lib.utils import to_date, tup\nfrom r2.models import (\n    Bid,\n    FakeSubreddit,\n    LocalizedDefaultSubreddits,\n    Location,\n    NO_TRANSACTION,\n    PromoCampaign,\n    PromotionWeights,\n    Subreddit,\n    traffic,\n)\nfrom r2.models.promo_metrics import LocationPromoMetrics, PromoMetrics\nfrom r2.models.subreddit import DefaultSR\n\nNDAYS_TO_QUERY = 14  # how much history to use in the estimate\nMIN_DAILY_CASS_KEY = 'min_daily_pageviews.GET_listing'\nPAGEVIEWS_REGEXP = re.compile('(.*)-GET_listing')\nINVENTORY_FACTOR = 1.00\nDEFAULT_INVENTORY_FACTOR = 5.00\n# For `PERCENT_MOBILE`:\n# if `0`, 100% of inventory will be displayed no matter the platform;\n# if not `0`:\n# - `all` is 100% of inventory\n# - `mobile` is `all` * (PERCENT_MOBILE / 100)\n# - `desktop` is `all` - `mobile`\nPERCENT_MOBILE = 0\n\n\ndef update_prediction_data():\n    \"\"\"Fetch prediction data and write it to cassandra.\"\"\"\n    min_daily_by_sr = _min_daily_pageviews_by_sr(NDAYS_TO_QUERY)\n\n    # combine front page values (sometimes frontpage gets '' for its name)\n    if '' in min_daily_by_sr:\n        fp = DefaultSR.name.lower()\n        min_daily_by_sr[fp] = min_daily_by_sr.get(fp, 0) + min_daily_by_sr['']\n        del min_daily_by_sr['']\n\n    filtered = {sr_name: num for sr_name, num in min_daily_by_sr.iteritems()\n                if num > 100}\n    PromoMetrics.set(MIN_DAILY_CASS_KEY, filtered)\n\n\ndef _min_daily_pageviews_by_sr(ndays=NDAYS_TO_QUERY, end_date=None):\n    \"\"\"Return dict mapping sr_name to min_pageviews over the last ndays.\"\"\"\n    if not end_date:\n        last_modified = traffic.get_traffic_last_modified()\n        end_date = last_modified - timedelta(days=1)\n    stop = end_date\n    start = stop - timedelta(ndays)\n    time_points = traffic.get_time_points('day', start, stop)\n    cls = traffic.PageviewsBySubredditAndPath\n    q = (traffic.Session.query(cls.srpath, func.min(cls.pageview_count))\n                               .filter(cls.interval == 'day')\n                               .filter(cls.date.in_(time_points))\n                               .filter(cls.srpath.like('%-GET_listing'))\n                               .group_by(cls.srpath))\n\n    # row looks like: ('lightpainting-GET_listing', 16)\n    retval = {}\n    for row in q:\n        m = PAGEVIEWS_REGEXP.match(row[0])\n        if m:\n            retval[m.group(1)] = row[1]\n    return retval\n\n\ndef get_date_range(start, end):\n    start, end = map(to_date, [start, end])\n    dates = [start + timedelta(i) for i in xrange((end - start).days)]\n    return dates\n\n\ndef get_campaigns_by_date(srs, start, end, ignore=None):\n    srs = tup(srs)\n    sr_names = [sr.name for sr in srs]\n    campaign_ids = PromotionWeights.get_campaign_ids(\n        start, end=end, sr_names=sr_names)\n    if ignore:\n        campaign_ids.discard(ignore._id)\n    campaigns = PromoCampaign._byID(campaign_ids, data=True, return_dict=False)\n\n    # filter out deleted campaigns that didn't have their PromotionWeights\n    # deleted\n    campaigns = filter(lambda camp: not camp._deleted, campaigns)\n\n    transaction_ids = {camp.trans_id for camp in campaigns\n                                     if camp.trans_id != NO_TRANSACTION}\n\n    if transaction_ids:\n        transactions = Bid.query().filter(Bid.transaction.in_(transaction_ids))\n        # index transactions by transaction and campaign id because freebies\n        # reuse the same transaction id (they always use -link id)\n        transaction_by_id = {\n            (bid.transaction, bid.campaign): bid for bid in transactions}\n    else:\n        transaction_by_id = {}\n\n    dates = set(get_date_range(start, end))\n    ret = {date: set() for date in dates}\n    for camp in campaigns:\n        if camp.trans_id == NO_TRANSACTION:\n            continue\n\n        if camp.impressions <= 0:\n            # pre-CPM campaign\n            continue\n\n        transaction = transaction_by_id[(camp.trans_id, camp._id)]\n        if not (transaction.is_auth() or transaction.is_charged()):\n            continue\n\n        camp_dates = set(get_date_range(camp.start_date, camp.end_date))\n        for date in camp_dates.intersection(dates):\n            ret[date].add(camp)\n    return ret\n\n\ndef get_predicted_pageviews(srs, location=None):\n    \"\"\"\n    Return predicted number of pageviews for sponsored headlines.\n\n    Predicted geotargeted impressions are estimated as:\n\n    geotargeted impressions = (predicted untargeted impressions) *\n                                 (fp impressions for location / fp impressions)\n\n    \"\"\"\n\n    srs, is_single = tup(srs, ret_is_single=True)\n    sr_names = [sr.name for sr in srs]\n\n    # default subreddits require a different inventory factor\n    default_srids = LocalizedDefaultSubreddits.get_global_defaults()\n\n    if location:\n        no_location = Location(None)\n        r = LocationPromoMetrics.get(DefaultSR, [no_location, location])\n        location_pageviews = r[(DefaultSR, location)]\n        all_pageviews = r[(DefaultSR, no_location)]\n        if all_pageviews:\n            location_factor = float(location_pageviews) / float(all_pageviews)\n        else:\n            location_factor = 0.\n    else:\n        location_factor = 1.0\n\n    # prediction does not vary by date\n    daily_inventory = PromoMetrics.get(MIN_DAILY_CASS_KEY, sr_names=sr_names)\n    ret = {}\n    for sr in srs:\n        if not isinstance(sr, FakeSubreddit) and sr._id in default_srids:\n            default_factor = DEFAULT_INVENTORY_FACTOR\n        else:\n            default_factor = INVENTORY_FACTOR\n        base_pageviews = daily_inventory.get(sr.name, 0)\n        ret[sr.name] = int(base_pageviews * default_factor * location_factor)\n\n    if is_single:\n        return ret[srs[0].name]\n    else:\n        return ret\n\n\ndef make_target_name(target):\n    name = (\"collection: %s\" % target.collection.name if target.is_collection\n                                           else target.subreddit_name)\n    return name\n\n\ndef find_campaigns(srs, start, end, ignore):\n    \"\"\"Get all campaigns in srs and pull in campaigns in other targeted srs.\"\"\"\n    all_sr_names = set()\n    all_campaigns = set()\n    srs = set(srs)\n\n    while srs:\n        all_sr_names |= {sr.name for sr in srs}\n        new_campaigns_by_date = get_campaigns_by_date(srs, start, end, ignore)\n        new_campaigns = set(chain.from_iterable(\n            new_campaigns_by_date.itervalues()))\n        all_campaigns.update(new_campaigns)\n        new_sr_names = set(chain.from_iterable(\n            campaign.target.subreddit_names for campaign in new_campaigns\n        ))\n        new_sr_names -= all_sr_names\n        srs = set(Subreddit._by_name(new_sr_names).values())\n    return all_campaigns\n\n\ndef get_available_pageviews(targets, start, end, location=None, datestr=False,\n                            ignore=None, platform='all'):\n    \"\"\"\n    Return the available pageviews by date for the targets and location.\n\n    Available pageviews depends on all equal and higher level locations:\n    A location is: subreddit > country > metro\n\n    e.g. if a campaign is targeting /r/funny in USA/Boston we need to check that\n    there's enough inventory in:\n    * /r/funny (all campaigns targeting /r/funny regardless of location)\n    * /r/funny + USA (all campaigns targeting /r/funny and USA with or without\n      metro level targeting)\n    * /r/funny + USA + Boston (all campaigns targeting /r/funny and USA and\n      Boston)\n    The available inventory is the smallest of these values.\n\n    \"\"\"\n\n    # assemble levels of location targeting, None means untargeted\n    locations = [None]\n    if location:\n        locations.append(location)\n\n        if location.metro:\n            locations.append(Location(country=location.country))\n\n    # get all the campaigns directly and indirectly involved in our target\n    targets, is_single = tup(targets, ret_is_single=True)\n    target_srs = list(chain.from_iterable(\n        target.subreddits_slow for target in targets))\n    all_campaigns = find_campaigns(target_srs, start, end, ignore)\n\n    # get predicted pageviews for each subreddit and location\n    all_sr_names = set(sr.name for sr in target_srs)\n    all_sr_names |= set(chain.from_iterable(\n        campaign.target.subreddit_names for campaign in all_campaigns\n    ))\n    all_srs = Subreddit._by_name(all_sr_names).values()\n    pageviews_dict = {location: get_predicted_pageviews(all_srs, location)\n                          for location in locations}\n\n    # determine booked impressions by target and location for each day\n    dates = set(get_date_range(start, end))\n    booked_dict = {}\n    for date in dates:\n        booked_dict[date] = {}\n        for location in locations:\n            booked_dict[date][location] = defaultdict(int)\n\n    for campaign in all_campaigns:\n        camp_dates = set(get_date_range(campaign.start_date, campaign.end_date))\n        sr_names = tuple(sorted(campaign.target.subreddit_names))\n        daily_impressions = campaign.impressions / campaign.ndays\n\n        for location in locations:\n            if location and not location.contains(campaign.location):\n                # campaign's location is less specific than location\n                continue\n\n            for date in camp_dates.intersection(dates):\n                booked_dict[date][location][sr_names] += daily_impressions\n\n    # calculate inventory for each target and location on each date\n    datekey = lambda dt: dt.strftime('%m/%d/%Y') if datestr else dt\n\n    ret = {}\n    for target in targets:\n        name = make_target_name(target)\n        subreddit_names = target.subreddit_names\n        ret[name] = {}\n        for date in dates:\n            pageviews_by_location = {}\n            for location in locations:\n                # calculate available impressions for each location\n                booked_by_target = booked_dict[date][location]\n                pageviews_by_sr_name = pageviews_dict[location]\n                pageviews_by_location[location] = get_maximized_pageviews(\n                    subreddit_names, booked_by_target, pageviews_by_sr_name)\n            # available pageviews is the minimum from all locations\n            min_pageviews = min(pageviews_by_location.values())\n            if PERCENT_MOBILE != 0:\n                mobile_pageviews = min_pageviews * (float(PERCENT_MOBILE) / 100)\n                if platform == 'mobile':\n                    min_pageviews = mobile_pageviews\n                if platform == 'desktop':\n                    min_pageviews = min_pageviews - mobile_pageviews\n            ret[name][datekey(date)] = max(0, min_pageviews)\n\n    if is_single:\n        name = make_target_name(targets[0])\n        return ret[name]\n    else:\n        return ret\n\n\ndef get_oversold(target, start, end, daily_request, ignore=None, location=None):\n    available_by_date = get_available_pageviews(target, start, end, location,\n                                                datestr=True, ignore=ignore)\n    oversold = {}\n    for datestr, available in available_by_date.iteritems():\n        if available < daily_request:\n            oversold[datestr] = available\n    return oversold\n"
  },
  {
    "path": "r2/r2/lib/inventory_optimization.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom collections import defaultdict\nfrom itertools import chain\n\n\nclass SimpleCampaign(object):\n    def __init__(self, name, target_names, impressions):\n        self.name = name\n        self.target_names = target_names\n        self.impressions = impressions\n\n    def __repr__(self):\n        s = \"<%s %s: %s impressions in %s>\"\n        return s % (self.__class__.__name__, self.name, self.impressions,\n                    ', '.join(self.target_names))\n\n\nclass SimpleTarget(object):\n    def __init__(self, name, impressions):\n        self.name = name\n        self.impressions = impressions\n\n    def __repr__(self):\n        return \"<%s %s: %s impressions>\" % (self.__class__.__name__, self.name,\n                                            self.impressions)\n\n\nclass System(object):\n    \"\"\"Take a set of campaigns and a set of targets and allocate the\n    inventory of each target to the campaigns in such a way to maximize\n    the free inventory in the priority target or targets.\n\n    \"\"\"\n\n    def __init__(self, campaigns, targets, priority_target_names):\n        self.priority_target_names = priority_target_names\n        self.campaigns, self.targets = self.simplify(campaigns, targets)\n\n    def __repr__(self):\n        max_names = ', '.join(self.priority_target_names)\n        all_names = ', '.join(\"%s (%s)\" % (target.name, target.impressions)\n                              for target in self.targets)\n        return \"<%s: max %s in %s>\" % (self.__class__.__name__, max_names,\n                                       all_names)\n\n    def combine_campaigns(self, campaigns):\n        \"\"\"Combine campaigns with the same target.\"\"\"\n        campaigns_by_target = defaultdict(list)\n        for campaign in campaigns:\n            target_names_tuple = tuple(sorted(campaign.target_names))\n            campaigns_by_target[target_names_tuple].append(campaign)\n\n        combined_campaigns = []\n        changed = False\n        for target_names_tuple, campaigns in campaigns_by_target.iteritems():\n            if len(campaigns) > 1:\n                changed = True\n                name = ','.join(camp.name for camp in campaigns)\n                target_names = list(target_names_tuple)\n                impressions = sum(camp.impressions for camp in campaigns)\n                combined = SimpleCampaign(name, target_names, impressions)\n                combined_campaigns.append(combined)\n            else:\n                combined_campaigns.extend(campaigns)\n        return changed, combined_campaigns\n\n    def reduce_campaigns(self, campaigns, targets):\n        \"\"\"Remove campaigns.\n\n        Find campaigns with only a single target and subtract their required\n        impressions from the target and remove the campaign.\n\n        \"\"\"\n\n        targets_by_name = {target.name: target for target in targets}\n        changed = False\n        reduced_campaigns = []\n        for campaign in campaigns:\n            if len(campaign.target_names) == 1:\n                changed = True\n                target_name = campaign.target_names[0]\n                target_impressions = targets_by_name[target_name].impressions\n                target_impressions -= campaign.impressions\n                new_target = SimpleTarget(target_name, target_impressions)\n                targets_by_name[target_name] = new_target\n            else:\n                reduced_campaigns.append(campaign)\n        reduced_targets = targets_by_name.values()\n        return changed, reduced_campaigns, reduced_targets\n\n    def reduce_targets(self, campaigns, targets):\n        \"\"\"Remove targets.\n\n        Remove non-priority targets that have only a single campaign or that\n        have enough inventory to satisfy all their campaigns. As a result may\n        end up removing campaigns if they're fully satisfied.\n\n        \"\"\"\n\n        campaign_names_by_target = defaultdict(list)\n        for campaign in campaigns:\n            for target_name in campaign.target_names:\n                if target_name not in self.priority_target_names:\n                    campaign_names_by_target[target_name].append(campaign.name)\n\n        campaigns_by_name = {campaign.name: campaign for campaign in campaigns}\n        targets_by_name = {target.name: target for target in targets}\n        changed = False\n        for target_name, campaign_names in campaign_names_by_target.iteritems():\n            target = targets_by_name[target_name]\n            campaign_impressions = sum(\n                campaigns_by_name[name].impressions for name in campaign_names)\n            fully_satisfied = campaign_impressions <= target.impressions\n            single_campaign = len(campaign_names) == 1\n\n            if not (fully_satisfied or single_campaign):\n                continue\n\n            changed = True\n            for campaign_name in campaign_names:\n                campaign = campaigns_by_name[campaign_name]\n                if fully_satisfied:\n                    # the target has enough impressions to cover all campaigns\n                    impressions = 0\n                else:\n                    # assign all of target's inventory to its single campaign\n                    target_impressions = max(0, target.impressions)\n                    impressions = campaign_impressions - target_impressions\n                target_names = campaign.target_names[:]\n                target_names.remove(target_name)\n\n                # update (rewrite) the SimpleCampaign object in the lookup dict\n                campaigns_by_name[campaign_name] = SimpleCampaign(\n                    campaign_name, target_names, impressions)\n\n            # no need to adjust the inventory of the target--it's not one\n            # we care about and there are no campaigns targeting it any more\n            # so we can just delete it\n            del targets_by_name[target_name]\n\n        reduced_campaigns = []\n        for campaign in campaigns_by_name.itervalues():\n            if campaign.impressions > 0:\n                reduced_campaigns.append(campaign)\n        reduced_targets = targets_by_name.values()\n        return changed, reduced_campaigns, reduced_targets\n\n    def simplify(self, campaigns, targets):\n        changed = False\n        first_run = True\n\n        while changed or first_run:\n            first_run = False\n            changed_1, campaigns = self.combine_campaigns(campaigns)\n            changed_2, campaigns, targets = self.reduce_campaigns(\n                campaigns, targets)\n            changed_3, campaigns, targets = self.reduce_targets(\n                campaigns, targets)\n\n            # only re-run if changed_2 or changed_2 because then we need to\n            # repeat the earlier steps\n            changed = changed_2 or changed_3\n\n        return campaigns, targets\n\n    def get_free_impressions(self):\n        \"\"\"Run through algorithm to solve for maximum free impressions.\n\n        Choose how to allocate inventory to each campaign by first mapping out\n        the distance of each target from the targets we're trying to maximize\n        inventory of, and then assigning inventory to each campaign\n        preferring to choose the targets that are farthest away.\n\n        \"\"\"\n\n        campaigns_by_target = defaultdict(list)\n        for campaign in self.campaigns:\n            for target_name in campaign.target_names:\n                campaigns_by_target[target_name].append(campaign)\n\n        # map out distance from targets we want to maximize\n        level = 0\n        level_by_target_name = {}\n        next_level_target_names = set(self.priority_target_names)\n        while next_level_target_names:\n            target_names = next_level_target_names\n\n            for target_name in target_names:\n                level_by_target_name[target_name] = level\n\n            campaigns = chain.from_iterable(\n                campaigns_by_target[target_name] for target_name in target_names\n            )\n            next_level_target_names = {\n                target_name for campaign in campaigns\n                            for target_name in campaign.target_names\n                            # skip any targets we've already seen\n                            if target_name not in level_by_target_name\n            }\n            level += 1\n\n        # assign any unconnected targets maximum level (although they should\n        # probably have been excluded before getting to the optimization)\n        for target in self.targets:\n            if target.name not in level_by_target_name:\n                level_by_target_name[target.name] = level\n\n        target_names_by_level = defaultdict(list)\n        for target_name, level in level_by_target_name.iteritems():\n            target_names_by_level[level].append(target_name)\n\n        # iterate over targets, starting at the highest level, and assign\n        # their inventory to campaigns\n        unassigned_by_campaign = {\n            campaign.name: campaign.impressions for campaign in self.campaigns}\n        impressions_by_target = {\n            target.name: target.impressions for target in self.targets}\n\n        for level in sorted(target_names_by_level.iterkeys(), reverse=True):\n            target_names = target_names_by_level[level]\n            campaigns = chain.from_iterable(\n                campaigns_by_target[target_name] for target_name in target_names\n            )\n\n            def sort_val(campaign):\n                # campaigns can have a maximum of 2 levels of targets,\n                # prioritize those with lower level and fewer targets\n                val = sum(\n                    level_by_target_name[name] + 1\n                    for name in campaign.target_names\n                    if level_by_target_name[name] <= level\n                )\n                return val\n\n            for campaign in sorted(campaigns, key=sort_val):\n                campaign_targets = [name for name in target_names\n                                    if name in campaign.target_names]\n                for target_name in campaign_targets:\n                    unassigned = unassigned_by_campaign[campaign.name]\n\n                    if unassigned > 0:\n                        available = max(0, impressions_by_target[target_name])\n                        assigned = min(unassigned, available)\n                        unassigned_by_campaign[campaign.name] -= assigned\n                        impressions_by_target[target_name] -= assigned\n\n        # check if any campaigns didn't get fully allocated (this means a target\n        # is oversold)\n        penalty = 0\n        for campaign in self.campaigns:\n            unassigned = unassigned_by_campaign[campaign.name]\n            if unassigned > 0:\n                if not campaign.target_names:\n                    # all the campaign's targets were already allocated\n                    continue\n\n                # allocate inventory from the lowest level target\n                target_name = min(campaign.target_names,\n                                  key=lambda name: level_by_target_name[name])\n                unassigned_by_campaign[campaign.name] -= unassigned\n                impressions_by_target[target_name] -= unassigned\n\n                # we screwed up, reduce the free impressions. using the penalty\n                # may end up underestimating the free impressions, but better\n                # safe than sorry.\n                penalty += unassigned\n\n        free_impressions = sum(\n            impressions_by_target[target_name] for target_name\n                                               in self.priority_target_names)\n        return free_impressions - penalty\n\n\ndef campaign_to_simple_campaign(campaign):\n    name = campaign._fullname\n    target_names = campaign.target.subreddit_names\n    impressions = campaign.impressions / campaign.ndays\n    return SimpleCampaign(name, target_names, impressions)\n\n\ndef get_maximized_pageviews(priority_sr_names, booked_by_target,\n                            pageviews_by_sr_name):\n    targets = [SimpleTarget(sr_name, pageviews) for sr_name, pageviews\n                                                in pageviews_by_sr_name.iteritems()]\n    campaigns = [\n        SimpleCampaign(', '.join(sr_names), list(sr_names), impressions)\n        for sr_names, impressions in booked_by_target.iteritems()\n    ]\n    system = System(campaigns, targets, priority_sr_names)\n    return system.get_free_impressions()\n\n\ndef run_tests():\n    # example 1: maximize impressions in a subreddit that is also targeted\n    # by a collection\n    pageviews_by_sr_name = {\n        'leagueoflegends': 50000,\n        'dota2': 50000,\n        'hearthstone': 50000,\n        'games': 50000,\n    }\n    targets = [SimpleTarget(sr_name, pageviews) for sr_name, pageviews\n               in pageviews_by_sr_name.iteritems()]\n\n    campaigns = [\n        SimpleCampaign('c1', ['leagueoflegends'], 20000),\n        SimpleCampaign('c2', ['dota2'], 40000),\n        SimpleCampaign('c3', ['games'], 40000),\n        SimpleCampaign('c4', ['hearthstone'], 40000),\n        SimpleCampaign('c5',\n            ['leagueoflegends', 'dota2', 'hearthstone', 'games'], 20000),\n    ]\n    priority_target_names = ['leagueoflegends']\n    system = System(campaigns, targets, priority_target_names)\n    impressions = system.get_free_impressions()\n    assert impressions == 30000\n\n    # example 2: maximize impressions of a collection\n    priority_target_names = ['leagueoflegends', 'dota2', 'hearthstone', 'games']\n    system = System(campaigns, targets, priority_target_names)\n    impressions = system.get_free_impressions()\n    assert impressions == 40000\n\n    # example 3: branching--we don't solve this \"correctly\", must rely on\n    # penalty (algorithm is greedy, see following notes)\n    pageviews_by_sr_name = {\n        'leagueoflegends': 25000,\n        'dota2': 25000,\n        'hearthstone': 25000,\n        'games': 25000,\n        'smashbros': 50000,\n    }\n    targets = [SimpleTarget(sr_name, pageviews) for sr_name, pageviews\n               in pageviews_by_sr_name.iteritems()]\n    campaigns = [\n        SimpleCampaign('c1', ['leagueoflegends', 'dota2'], 25000),\n        SimpleCampaign('c2', ['hearthstone', 'games'], 25000),\n        SimpleCampaign('c3', ['dota2', 'smashbros'], 50000),\n        SimpleCampaign('c4', ['games', 'smashbros'], 50000),\n    ]\n    priority_target_names = ['leagueoflegends', 'hearthstone']\n\n    \"\"\"\n    optimal distribution:\n    c4: 25000 from smashbros, 25000 from games\n    c3: 25000 from smashbros, 25000 from dota2\n    c2: 25000 from hearthstone\n    c2: 25000 from leagueoflegends\n\n    Current algorithm can't split smashbros because it's too greedy, the first\n    of c4 or c3 to be allocated will get all 50000\n\n    Subsequent improvements to the algorithm should allow splitting a target\n    and should prioritize campaigns for which the target is their lowest\n    level target. Also for campaigns for which the target is their highest\n    level target the algorithm should look forward to their lowest level target\n    and determin whether that has any chance of satisfying the campaign.\n\n    \"\"\"\n\n    system = System(campaigns, targets, priority_target_names)\n    impressions = system.get_free_impressions()\n    assert impressions == 0\n"
  },
  {
    "path": "r2/r2/lib/ip_events.py",
    "content": "from collections import Counter\n\nfrom r2.lib.geoip import location_by_ips, organization_by_ips\nfrom r2.lib.utils import tup\nfrom r2.models.ip import IPsByAccount, AccountsByIP\n\n\ndef ips_by_account_id(account_id, limit=None):\n    ips = IPsByAccount.get(account_id, column_count=limit or 1000)\n    flattened_ips = [j for i in ips for j in i.iteritems()]\n    locations = location_by_ips(set(ip for _, ip in flattened_ips))\n    orgs = organization_by_ips(set(ip for _, ip in flattened_ips))\n\n    # Deduplicate and summarize total usage time\n    counts = Counter((ip for _, ip in flattened_ips))\n    seen = set()\n    results = []\n    for visit_time, ip in flattened_ips:\n        if ip in seen:\n            continue\n        results.append(\n            (ip, visit_time, locations.get(ip) or {},\n                orgs.get(ip), counts.get(ip))\n        )\n        seen.add(ip)\n    return results\n\n\ndef account_ids_by_ip(ip, after=None, before=None, limit=1000):\n    \"\"\"Get a list of account IDs that an IP has accessed.\n\n    Parameters:\n    after -- a `datetime.datetime` from which results should start\n    before -- a `datetime.datetime` from which results should end.  If `after`\n        is specified, this will be ignored.\n    limit -- number of results to return\n    \"\"\"\n    ips = tup(ip)\n    results = []\n    flattened_accounts = {}\n    for ip in ips:\n        if before and not after:\n            # One less result will be returned for `before` queries, so we\n            # increase the limit by one.\n            account_ip = AccountsByIP.get(\n                ip, column_start=before, column_count=limit + 1,\n                column_reversed=False)\n            account_ip = sorted(account_ip, reverse=True)\n        else:\n            account_ip = AccountsByIP.get(\n                ip, column_start=after, column_count=limit)\n        flattened_account_ip = [j for i in account_ip\n                                for j in i.iteritems()]\n        flattened_accounts[ip] = flattened_account_ip\n\n    for ip, flattened_account_ip in flattened_accounts.iteritems():\n        for last_visit, account in flattened_account_ip:\n            results.append((account, last_visit, [ip]))\n    return results\n"
  },
  {
    "path": "r2/r2/lib/js.py",
    "content": "#!/usr/bin/env python\n# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport inspect\nimport sys\nimport os.path\nimport re\nimport subprocess\nimport json\n\nfrom pylons import app_globals as g\nfrom pylons import tmpl_context as c\n\nfrom r2.config.paths import get_built_statics_path\nfrom r2.lib.permissions import ModeratorPermissionSet\nfrom r2.lib.plugin import PluginLoader\nfrom r2.lib.static import locate_static_file\nfrom r2.lib.translation import (\n    extract_javascript_msgids,\n    get_catalog,\n    iter_langs,\n    validate_plural_forms,\n)\n\n\nscript_tag = '<script type=\"text/javascript\" src=\"{src}\"></script>\\n'\ninline_script_tag = '<script type=\"text/javascript\">{content}</script>'\n\n\nclass Uglify(object):\n    def compile(self, data, dest):\n        process = subprocess.Popen(\n            [\"/usr/bin/uglifyjs\", \"-nc\"],\n            stdin=subprocess.PIPE,\n            stdout=dest,\n        )\n\n        process.communicate(input=data)\n\n        if process.returncode != 0:\n            raise subprocess.CalledProcessError(process.returncode, \"uglifyjs\")\n\n\nclass Source(object):\n    \"\"\"An abstract collection of JavaScript code.\"\"\"\n    def get_source(self, **kwargs):\n        \"\"\"Return the full JavaScript source code.\"\"\"\n        raise NotImplementedError\n\n    def use(self, **kwargs):\n        \"\"\"Return HTML to insert the JavaScript source inside a template.\"\"\"\n        raise NotImplementedError\n\n    @property\n    def dependencies(self):\n        raise NotImplementedError\n\n    @property\n    def outputs(self):\n        raise NotImplementedError\n\n\nclass FileSource(Source):\n    \"\"\"A JavaScript source file on disk.\"\"\"\n    def __init__(self, name):\n        self.name = name\n\n    def __eq__(self, other):\n        return type(self) is type(other) and self.name == other.name\n\n    def get_source(self, use_built_statics=False):\n        if use_built_statics:\n            # we are in the build system so we have already copied all files\n            # into the static build directory\n            built_statics_path = get_built_statics_path()\n            path = os.path.join(built_statics_path, \"static\", \"js\", self.name)\n        else:\n            # we are in request so we need to check the pylons static_files\n            # path and the static paths for all plugins\n            path = locate_static_file(os.path.join(\"static\", \"js\", self.name))\n\n        with open(path) as f:\n            return f.read()\n\n    def url(self, absolute=False, mangle_name=False):\n        from r2.lib.template_helpers import static\n        path = [g.static_path, self.name]\n        if g.uncompressedJS:\n            path.insert(1, \"js\")\n\n        return static(os.path.join(*path), absolute, mangle_name)\n\n    def use(self, **kwargs):\n        return script_tag.format(src=self.url(**kwargs))\n\n    @property\n    def dependencies(self):\n        built_statics_path = get_built_statics_path()\n        path = os.path.join(built_statics_path, \"static\", \"js\", self.name)\n        return [path]\n\n\nclass Module(Source):\n    \"\"\"A module of JS code consisting of a collection of sources.\"\"\"\n    def __init__(self, name, *sources, **kwargs):\n        self.name = name\n        self.should_compile = kwargs.get('should_compile', True)\n        self.wrap = kwargs.get('wrap')\n        self.sources = []\n        filter_module = kwargs.get('filter_module')\n        if isinstance(filter_module, Module):\n            self.filter_sources = filter_module.get_flattened_sources([])\n        else:\n            self.filter_sources = None\n        sources = sources or (name,)\n        for source in sources:\n            if not isinstance(source, Source):\n                if 'prefix' in kwargs:\n                    source = os.path.join(kwargs['prefix'], source)\n                source = self.get_default_source(source)\n            self.sources.append(source)\n\n    def get_default_source(self, source):\n        return FileSource(source)\n\n    def get_flattened_sources(self, flattened_sources):\n        for s in self.sources:\n            if s in flattened_sources:\n                continue\n            elif isinstance(s, Module):\n                s.get_flattened_sources(flattened_sources)\n            else:\n                flattened_sources.append(s)\n        if self.filter_sources:\n            flattened_sources = [s for s in flattened_sources\n                                 if s not in self.filter_sources]\n        return flattened_sources\n\n    def get_source(self, use_built_statics=False):\n        sources = self.get_flattened_sources([])\n        return \";\".join(\n            s.get_source(use_built_statics=use_built_statics)\n            for s in sources\n        )\n\n    def extend(self, module):\n        self.sources.extend(module.sources)\n\n    @property\n    def destination_path(self):\n        built_statics_path = get_built_statics_path()\n        return os.path.join(built_statics_path, \"static\", self.name)\n\n    def build(self, minifier):\n        with open(self.destination_path, \"w\") as out:\n            source = self.get_source(use_built_statics=True)\n            if self.wrap:\n                source = self.wrap.format(content=source, name=self.name)\n\n            if self.should_compile:\n                print >> sys.stderr, \"Compiling {0}...\".format(self.name),\n                minifier.compile(source, out)\n            else:\n                print >> sys.stderr, \"Concatenating {0}...\".format(self.name),\n                out.write(source)\n        print >> sys.stderr, \" done.\"\n\n    def url(self, absolute=False, mangle_name=True):\n        from r2.lib.template_helpers import static\n        if g.uncompressedJS:\n            return [source.url(absolute=absolute, mangle_name=mangle_name) for source in self.sources]\n        else:\n            return static(self.name, absolute=absolute, mangle_name=mangle_name)\n\n    def use(self, **kwargs):\n        if g.uncompressedJS:\n            sources = self.get_flattened_sources([])\n            return \"\".join(source.use(**kwargs) for source in sources)\n        else:\n            return script_tag.format(src=self.url(**kwargs))\n\n    @property\n    def dependencies(self):\n        deps = []\n        for source in self.sources:\n            deps.extend(source.dependencies)\n        return deps\n\n    @property\n    def outputs(self):\n        return [self.destination_path]\n\n\nclass DataSource(Source):\n    \"\"\"A generated source consisting of wrapped JSON data.\"\"\"\n    def __init__(self, wrap, data=None):\n        self.wrap = wrap\n        self.data = data\n\n    def get_content(self, **kw):\n        return self.data\n\n    def get_source(self, use_built_statics=False):\n        content = self.get_content(use_built_statics=use_built_statics)\n        json_data = json.dumps(content)\n        return self.wrap.format(content=json_data) + \"\\n\"\n\n    def use(self):\n        from r2.lib.filters import SC_OFF, SC_ON, websafe_json\n        escaped_json = websafe_json(self.get_source())\n        return (SC_OFF + inline_script_tag.format(content=escaped_json) +\n                SC_ON + \"\\n\")\n\n    @property\n    def dependencies(self):\n        return []\n\n\nclass PermissionsDataSource(DataSource):\n    \"\"\"DataSource for PermissionEditor configuration data.\"\"\"\n\n    def __init__(self, permission_sets):\n        self.permission_sets = permission_sets\n\n    @classmethod\n    def _make_marked_json(cls, obj):\n        \"\"\"Return serialized psuedo-JSON with translation support.\n\n        Strings are marked for extraction with r.N_. Dictionaries are\n        serialized to JSON objects as normal.\n\n        \"\"\"\n        if isinstance(obj, dict):\n            props = []\n            for key, value in obj.iteritems():\n                value_encoded = cls._make_marked_json(value)\n                props.append(\"%s: %s\" % (key, value_encoded))\n            return \"{%s}\" % \",\".join(props)\n        elif isinstance(obj, basestring):\n            return \"r.N_(%s)\" % json.dumps(obj)\n        else:\n            raise ValueError, \"unsupported type\"\n\n    def get_source(self, **kw):\n        permission_set_info = {k: v.info for k, v in\n                               self.permission_sets.iteritems()}\n        permissions = self._make_marked_json(permission_set_info)\n        return \"r.permissions = _.extend(r.permissions || {}, %s)\" % permissions\n\n    @property\n    def dependencies(self):\n        dependencies = set(super(PermissionsDataSource, self).dependencies)\n        for permission_set in self.permission_sets.itervalues():\n            dependencies.add(inspect.getsourcefile(permission_set))\n        return list(dependencies)\n\n\nclass TemplateFileSource(DataSource, FileSource):\n    \"\"\"A JavaScript template file on disk.\"\"\"\n    def __init__(self, name, wrap=\"r.templates.set({content})\"):\n        DataSource.__init__(self, wrap)\n        FileSource.__init__(self, name)\n        self.name = name\n\n    def get_content(self, use_built_statics=False):\n        name, style = os.path.splitext(self.name)\n\n        if use_built_statics:\n            built_statics_path = get_built_statics_path()\n            path = os.path.join(built_statics_path, 'static', 'js', self.name)\n        else:\n            path = locate_static_file(os.path.join('static', 'js', self.name))\n\n        with open(path) as f:\n            return [{\n                \"name\": name,\n                \"style\": style.lstrip('.'),\n                \"template\": f.read(),\n            }]\n\n\nclass LocaleSpecificSource(object):\n    def get_localized_source(self, lang):\n        raise NotImplementedError\n\n\nclass StringsSource(LocaleSpecificSource):\n    \"\"\"Translations sourced from a gettext catalog.\"\"\"\n\n    def __init__(self, keys):\n        self.keys = keys\n\n    invalid_formatting_specifier_re = re.compile(r\"(?<!%)%\\w|(?<!%)%\\(\\w+\\)[^s]\")\n    def _check_formatting_specifiers(self, string):\n        if not isinstance(string, basestring):\n            return\n\n        if self.invalid_formatting_specifier_re.search(string):\n            raise ValueError(\"Invalid string formatting specifier: %r\" % string)\n\n    def get_localized_source(self, lang):\n        catalog = get_catalog(lang)\n\n        # relies on pyx files, so it can't be imported at global scope\n        from r2.lib.utils import tup\n\n        data = {}\n        for key in self.keys:\n            key = tup(key)[0]  # because the key for plurals is (sing, plur)\n            self._check_formatting_specifiers(key)\n            msg = catalog[key]\n\n            if not msg or not msg.string:\n                continue\n\n            # jed expects to ignore the first value in the translations array\n            # so we'll just make it null\n            strings = tup(msg.string)\n            data[key] = [None] + list(strings)\n        return \"r.i18n.addMessages(%s)\" % json.dumps(data)\n\n\nclass PluralForms(LocaleSpecificSource):\n    def get_localized_source(self, lang):\n        catalog = get_catalog(lang)\n        validate_plural_forms(catalog.plural_expr)\n        return \"r.i18n.setPluralForms('%s')\" % catalog.plural_expr\n\n\nclass LocalizedModule(Module):\n    \"\"\"A module that generates localized code for each language.\n\n    Strings marked for translation with one of the functions in i18n.js (viz.\n    r._, r.P_, and r.N_) are extracted from the source and their translations\n    are built into the compiled source.\n\n    \"\"\"\n\n    def __init__(self, *args, **kwargs):\n        self.localized_appendices = kwargs.pop(\"localized_appendices\", [])\n        Module.__init__(self, *args, **kwargs)\n\n        for source in self.sources:\n            if isinstance(source, LocalizedModule):\n                self.localized_appendices.extend(source.localized_appendices)\n\n    @staticmethod\n    def languagize_path(path, lang):\n        path_name, path_ext = os.path.splitext(path)\n        return path_name + \".\" + lang + path_ext\n\n    def build(self, minifier):\n        Module.build(self, minifier)\n\n        with open(self.destination_path) as f:\n            reddit_source = f.read()\n\n        localized_appendices = self.localized_appendices\n        msgids = extract_javascript_msgids(reddit_source)\n        if msgids:\n            localized_appendices = localized_appendices + [StringsSource(msgids)]\n\n        print >> sys.stderr, \"Creating language-specific files:\"\n        for lang, unused in iter_langs():\n            lang_path = LocalizedModule.languagize_path(\n                self.destination_path, lang)\n\n            # make sure we're not rewriting a different mangled file\n            # via symlink\n            if os.path.islink(lang_path):\n                os.unlink(lang_path)\n\n            with open(lang_path, \"w\") as out:\n                print >> sys.stderr, \"  \" + lang_path\n                out.write(reddit_source)\n                for appendix in localized_appendices:\n                    out.write(appendix.get_localized_source(lang) + \";\")\n\n    def use(self, **kwargs):\n        from pylons.i18n import get_lang\n        from r2.lib.template_helpers import static\n        from r2.lib.filters import SC_OFF, SC_ON\n\n        if g.uncompressedJS:\n            if c.lang == \"en\" or c.lang not in g.all_languages:\n                # in this case, the msgids *are* the translated strings and we\n                # can save ourselves the pricey step of lexing the js source\n                return Module.use(self, **kwargs)\n\n            msgids = extract_javascript_msgids(Module.get_source(self))\n            localized_appendices = self.localized_appendices + [StringsSource(msgids)]\n\n            lines = [Module.use(self, **kwargs)]\n            for appendix in localized_appendices:\n                line = SC_OFF + inline_script_tag.format(\n                    content=appendix.get_localized_source(c.lang)) + SC_ON\n                lines.append(line)\n            return \"\\n\".join(lines)\n        else:\n            langs = get_lang() or [g.lang]\n            url = LocalizedModule.languagize_path(self.name, langs[0])\n            return script_tag.format(src=static(url), **kwargs)\n\n    @property\n    def outputs(self):\n        for lang, unused in iter_langs():\n            yield LocalizedModule.languagize_path(self.destination_path, lang)\n\n\n_submodule = {}\nmodule = {}\n\ncatch_errors = \"try {{ {content} }} catch (err) {{ r.sendError('Error running module', '{name}', ':', err.toString()) }}\"\n\n\n_submodule[\"config\"] = Module(\"_setup.js\",\n    \"base.js\",\n    \"setup.js\",\n    \"hooks.js\",\n)\n\n_submodule[\"utils\"] = Module(\"_utils.js\",\n    \"base.js\",\n    _submodule[\"config\"],\n    \"utils.js\",\n)\n\n_submodule[\"uibase\"] = Module(\"_uibase.js\",\n    \"base.js\",\n    \"i18n.js\",\n    _submodule[\"utils\"],\n    \"uibase.js\",\n)\n\n_submodule[\"analytics\"] = Module(\"_analytics.js\",\n    \"base.js\",\n    _submodule[\"config\"],\n    _submodule[\"utils\"],\n    \"events.js\",\n    \"analytics.js\",\n)\n\n_submodule[\"errors\"] = Module(\"_errors.js\",\n    \"base.js\",\n    \"i18n.js\",\n    \"errors.js\",\n)\n\n_submodule[\"gate-popup\"] = Module(\"_gate-popup.js\",\n    \"base.js\",\n    _submodule[\"uibase\"],\n    _submodule[\"errors\"],\n    \"gate-popup.js\",\n)\n\n_submodule[\"timeouts\"] = Module(\"_timeouts.js\",\n    \"base.js\",\n    _submodule[\"config\"],\n    _submodule[\"analytics\"],\n    _submodule[\"gate-popup\"],\n    \"access.js\",\n    \"timeouts.js\",\n)\n\n_submodule[\"locked\"] = Module(\"_locked.js\",\n    \"base.js\",\n    \"access.js\",\n    _submodule[\"gate-popup\"],\n    \"locked.js\",\n)\n\n_submodule[\"archived\"] = Module(\"_archived.js\",\n    \"base.js\",\n    \"hooks.js\",\n    _submodule[\"gate-popup\"],\n    \"archived.js\",\n)\n\nmodule[\"gtm-jail\"] = Module(\"gtm-jail.js\",\n    \"lib/json2.js\",\n    \"custom-event.js\",\n    \"frames.js\",\n    \"google-tag-manager/gtm-jail-listener.js\",\n)\n\n\nmodule[\"gtm\"] = Module(\"gtm.js\",\n    \"lib/json2.js\",\n    \"custom-event.js\",\n    \"frames.js\",\n    \"google-tag-manager/gtm-listener.js\",\n)\n\n\nmodule[\"reddit-embed-base\"] = Module(\"reddit-embed-base.js\",\n    \"lib/es5-shim.js\",\n    \"lib/json2.js\",\n    \"base.js\",\n    \"uuid.js\",\n    \"custom-event.js\",\n    \"frames.js\",\n    \"embed/utils.js\",\n    \"embed/pixel-tracking.js\",\n)\n\n\nmodule[\"reddit-embed\"] = Module(\"reddit-embed.js\",\n    module[\"reddit-embed-base\"],\n    \"embed/embed.js\",\n)\n\n\nmodule[\"comment-embed\"] = Module(\"comment-embed.js\",\n    module[\"reddit-embed-base\"],\n    \"embed/comment-embed.js\",\n)\n\n\nmodule[\"reddit-init-base\"] = LocalizedModule(\"reddit-init-base.js\",\n    \"lib/modernizr.js\",\n    \"lib/json2.js\",\n    \"lib/underscore-1.4.4-1.js\",\n    \"lib/store.js\",\n    \"lib/jed.js\",\n    \"lib/bootstrap.modal.js\",\n    \"lib/bootstrap.transition.js\",\n    \"lib/bootstrap.tooltip.js\",\n    \"lib/reddit-client-lib.js\",\n    \"lib/jquery.cookie.js\",\n    \"lib/event-tracker.js\",\n    \"lib/hmac-sha256.js\",\n    \"do-not-track.js\",\n    \"bootstrap.tooltip.extension.js\",\n    \"base.js\",\n    \"uuid.js\",\n    \"hooks.js\",\n    \"setup.js\",\n    \"migrate-global-reddit.js\",\n    \"ajax.js\",\n    \"safe-store.js\",\n    \"preload.js\",\n    \"logging.js\",\n    \"client-error-logger.js\",\n    \"voting.js\",\n    \"uibase.js\",\n    \"i18n.js\",\n    \"utils.js\",\n    \"analytics.js\",\n    \"events.js\",\n    \"access.js\",\n    \"reddit-init-hook.js\",\n    \"jquery.reddit.js\",\n    \"stateify.js\",\n    \"validator.js\",\n    \"strength-meter.js\",\n    \"toggles.js\",\n    \"reddit.js\",\n    \"sr-autocomplete.js\",\n    \"spotlight.js\",\n    localized_appendices=[\n        PluralForms(),\n    ],\n)\n\nmodule[\"reddit-init-legacy\"] = LocalizedModule(\"reddit-init-legacy.js\",\n    \"lib/html5shiv.js\",\n    \"lib/jquery-1.11.1.js\",\n    \"lib/es5-shim.js\",\n    \"lib/es5-sham.js\",\n    module[\"reddit-init-base\"],\n    wrap=catch_errors,\n)\n\nmodule[\"reddit-init\"] = LocalizedModule(\"reddit-init.js\",\n    \"lib/jquery-2.1.1.js\",\n    \"lib/es5-shim.js\",\n    module[\"reddit-init-base\"],\n    wrap=catch_errors,\n)\n\nmodule[\"expando-nsfw-flow\"] = Module(\"expando-nsfw-flow.js\",\n    TemplateFileSource('ui/formbar.html'),\n    \"ui/formbar.js\",\n    TemplateFileSource('expando/nsfwgate.html'),\n    \"expando/nsfwflow.js\",\n)\n\nmodule[\"reddit\"] = LocalizedModule(\"reddit.js\",\n    \"lib/jquery.url.js\",\n    \"lib/backbone-1.0.0.js\",\n    \"custom-event.js\",\n    \"frames.js\",\n    \"embed/utils.js\",\n    \"embed/pixel-tracking.js\",\n    \"embed/comment-embed.js\",\n    \"google-tag-manager/gtm.js\",\n    \"backbone-init.js\",\n    \"timings.js\",\n    \"templates.js\",\n    \"scrollupdater.js\",\n    \"timetext.js\",\n    \"ui.js\",\n    \"popup.js\",\n    \"login.js\",\n    _submodule[\"locked\"],\n    _submodule[\"timeouts\"],\n    _submodule[\"archived\"],\n    \"newsletter.js\",\n    \"flair.js\",\n    \"report.js\",\n    \"interestbar.js\",\n    \"visited.js\",\n    \"wiki.js\",\n    \"apps.js\",\n    \"gold.js\",\n    \"multi.js\",\n    \"filter.js\",\n    \"recommender.js\",\n    \"action-forms.js\",\n    \"embed.js\",\n    \"post-sharing.js\",\n    \"expando.js\",\n    # inline expando-nsfw-flow.js module here when unflagged\n    \"saved.js\",\n    \"cache-poisoning-detection.js\",\n    \"messages.js\",\n    \"reddit-hook.js\",\n    \"link-click-tracking.js\",\n    \"warn-on-unload.js\",\n    PermissionsDataSource({\n        \"moderator\": ModeratorPermissionSet,\n        \"moderator_invite\": ModeratorPermissionSet,\n    }),\n    wrap=catch_errors,\n    filter_module=module[\"reddit-init-base\"],\n)\n\nmodule[\"modtools\"] = Module(\"modtools.js\",\n    \"errors.js\",\n    \"models/validators.js\",\n    \"models/subreddit-rule.js\",\n    \"edit-subreddit-rules.js\",\n    wrap=catch_errors,\n)\n\nmodule[\"admin\"] = Module(\"admin.js\",\n    # include Backbone and timings so they are available early to render admin bar fast.\n    \"lib/backbone-1.0.0.js\",\n    \"timings.js\",\n    \"adminbar.js\",\n)\n\nmodule[\"mobile\"] = LocalizedModule(\"mobile.js\",\n    module[\"reddit\"],\n    \"lib/jquery.lazyload.js\",\n    \"compact.js\",\n    filter_module=module[\"reddit-init-base\"],\n)\n\n\nmodule[\"policies\"] = Module(\"policies.js\",\n    \"policies.js\",\n)\n\n\nmodule[\"sponsored\"] = LocalizedModule(\"sponsored.js\",\n    \"lib/ui.core.js\",\n    \"lib/ui.datepicker.js\",\n    \"lib/react-with-addons-0.11.2.js\",\n    \"image-upload.js\",\n    \"sponsored.js\"\n)\n\n\nmodule[\"timeseries\"] = Module(\"timeseries.js\",\n    \"lib/jquery.flot.js\",\n    \"lib/jquery.flot.time.js\",\n    \"timeseries.js\",\n)\n\n\nmodule[\"timeseries-ie\"] = Module(\"timeseries-ie.js\",\n    \"lib/excanvas.min.js\",\n    module[\"timeseries\"],\n)\n\n\nmodule[\"traffic\"] = LocalizedModule(\"traffic.js\",\n    \"traffic.js\",\n)\n\n\nmodule[\"qrcode\"] = Module(\"qrcode.js\",\n    \"lib/jquery.qrcode.min.js\",\n    \"qrcode.js\",\n)\n\n\nmodule[\"highlight\"] = Module(\"highlight.js\",\n    \"lib/highlight.pack.js\",\n    \"highlight.js\",\n)\n\nmodule[\"messagecompose\"] = Module(\"messagecompose.js\",\n    # jquery, hooks, ajax, preload\n    \"messagecompose.js\")\n\nmodule[\"less\"] = Module('less.js',\n    'lib/less-1.4.2.js',\n    should_compile=False,\n)\n\n# This needs to be separate module because we need it to load on old / bad\n# browsers that choke on reddit.js\nmodule[\"https-tester\"] = Module(\"https-tester.js\",\n    \"base.js\",\n    \"uuid.js\",\n    \"https-tester.js\"\n)\n\ndef src(*names, **kwargs):\n    sources = []\n\n    for name in names:\n        urls = module[name].url(**kwargs)\n\n        if isinstance(urls, str) or isinstance(urls, unicode):\n            sources.append(urls)\n        else:\n            for url in list(urls):\n                if isinstance(url, list):\n                    sources.extend(url)\n                else:\n                    sources.append(url)\n\n    return sources\n\ndef use(*names, **kwargs):\n    return \"\\n\".join(module[name].use(**kwargs) for name in names)\n\n\ndef load_plugin_modules(plugins=None):\n    if not plugins:\n        plugins = PluginLoader()\n    for plugin in plugins:\n        plugin.add_js(module)\n\n\ncommands = {}\ndef build_command(fn):\n    def wrapped(*args):\n        load_plugin_modules()\n        fn(*args)\n    commands[fn.__name__] = wrapped\n    return wrapped\n\n\n@build_command\ndef enumerate_modules():\n    for name, m in module.iteritems():\n        print name\n\n\n@build_command\ndef dependencies(name):\n    for dep in module[name].dependencies:\n        print dep\n\n\n@build_command\ndef enumerate_outputs(*names):\n    if names:\n        modules = [module[name] for name in names]\n    else:\n        modules = module.itervalues()\n\n    for m in modules:\n        for output in m.outputs:\n            print output\n\n\n@build_command\ndef build_module(name):\n    minifier = Uglify()\n    module[name].build(minifier)\n\n\nif __name__ == \"__main__\":\n    commands[sys.argv[1]](*sys.argv[2:])\n"
  },
  {
    "path": "r2/r2/lib/jsonresponse.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom r2.config.extensions import get_api_subtype\nfrom r2.lib.utils import tup\nfrom r2.lib.captcha import get_iden\nfrom r2.lib.wrapped import Wrapped, StringTemplate\nfrom r2.lib.filters import websafe_json, spaceCompress\nfrom r2.lib.base import BaseController\nfrom r2.lib.pages.things import wrap_links\nfrom r2.models import IDBuilder, Listing\n\nimport simplejson\nfrom pylons import tmpl_context as c\nfrom pylons import app_globals as g\n\n\nclass JsonResponse(object):\n    \"\"\"\n    Simple Api response handler, returning a list of errors generated\n    in the api func's validators, as well as blobs of data set by the\n    api func.\n    \"\"\"\n\n    content_type = 'application/json'\n\n    def __init__(self):\n        self._clear()\n\n    def _clear(self):\n        self._errors = set()\n        self._new_captcha = False\n        self._ratelimit = False\n        self._data = {}\n\n    def send_failure(self, error):\n        c.errors.add(error)\n        self._clear()\n        self._errors.add((error, None))\n\n    def __call__(self, *a, **kw):\n        return self\n\n    def __getattr__(self, key):\n        return self\n\n    def make_response(self):\n        res = {}\n        if self._data:\n            res['data'] = self._data\n        if self._new_captcha:\n            res['captcha'] = get_iden()\n        if self._ratelimit:\n            res['ratelimit'] = self._ratelimit\n        res['errors'] = [(e[0], c.errors[e].message, e[1]) for e in self._errors]\n        return {\"json\": res}\n\n    def set_error(self, error_name, field_name):\n        self._errors.add((error_name, field_name))\n\n    def has_error(self):\n        return bool(self._errors)\n\n    def has_errors(self, field_name, *errors, **kw):\n        have_error = False\n        field_name = tup(field_name)\n        for error_name in errors:\n            for fname in field_name:\n                if (error_name, fname) in c.errors:\n                    self.set_error(error_name, fname)\n                    have_error = True\n        return have_error\n\n    def process_rendered(self, res):\n        return res\n\n    def _things(self, things, action, *a, **kw):\n        \"\"\"\n        function for inserting/replacing things in listings.\n        \"\"\"\n        things = tup(things)\n        if not all(isinstance(t, Wrapped) for t in things):\n            wrap = kw.pop('wrap', Wrapped)\n            things = wrap_links(things, wrapper = wrap)\n        data = [self.process_rendered(t.render()) for t in things]\n\n        if kw:\n            for d in data:\n                if d.has_key('data'):\n                    d['data'].update(kw)\n\n        self._data['things'] = data\n        return data\n\n    def insert_things(self, things, append = False, **kw):\n        return self._things(things, \"insert_things\", append, **kw)\n\n    def replace_things(self, things, keep_children = False,\n                       reveal = False, stubs = False, **kw):\n        return self._things(things, \"replace_things\",\n                            keep_children, reveal, stubs, **kw)\n\n    def _send_data(self, **kw):\n        self._data.update(kw)\n\n    def new_captcha(self):\n        self._new_captcha = True\n\n    def ratelimit(self, seconds):\n        self._ratelimit = seconds\n\n\nclass JQueryResponse(JsonResponse):\n    \"\"\"\n    class which mimics the jQuery in javascript for allowing Dom\n    manipulations on the client side.\n\n    An instantiated JQueryResponse acts just like the \"$\" function on\n    the JS layer with the exception of the ability to run arbitrary\n    code on the client.  Selectors and method functions evaluate to\n    new JQueryResponse objects, and the transformations are cataloged\n    by the original object which can be iterated and sent across the\n    wire.\n    \"\"\"\n    def __init__(self, top_node = None):\n        if top_node:\n            self.top_node = top_node\n        else:\n            self.top_node = self\n        JsonResponse.__init__(self)\n        self._clear()\n\n    def _clear(self):\n        if self.top_node == self:\n            self.objs = {self: 0}\n            self.ops  = []\n        else:\n            self.objs = None\n            self.ops  = None\n        JsonResponse._clear(self)\n\n    def process_rendered(self, res):\n        if 'data' in res:\n            if 'content' in res['data']:\n                res['data']['content'] = spaceCompress(res['data']['content'])\n        return res\n\n    def send_failure(self, error):\n        c.errors.add(error)\n        self._clear()\n        self._errors.add((self, error, None))\n        self.refresh()\n\n    def __call__(self, *a):\n        return self.top_node.transform(self, \"call\", a)\n\n    def __getattr__(self, key):\n        if not key.startswith(\"__\"):\n            return self.top_node.transform(self, \"attr\", key)\n\n    def transform(self, obj, op, args):\n        new = self.__class__(self)\n        newi = self.objs[new] = len(self.objs)\n        self.ops.append([self.objs[obj], newi, op, args])\n        return new\n\n    def set_error(self, error_name, field_name):\n        #self is the form that had the error checked, but we need to\n        #add this error to the top_node of this response and give it a\n        #reference to the form.\n        self.top_node._errors.add((self, error_name, field_name))\n\n    def has_error(self):\n        return bool(self.top_node._errors)\n\n    def make_response(self):\n        #add the error messages\n        for (form, error_name, field_name) in self._errors:\n            selector = \".error.\" + error_name\n            if field_name:\n                selector += \".field-\" + field_name\n            message = c.errors[(error_name, field_name)].message\n            form.find(selector).show().text(message).end()\n        return {\"jquery\": self.ops,\n                \"success\": not self.has_error()}\n\n    # thing methods\n    #--------------\n\n    def _things(self, things, action, *a, **kw):\n        data = JsonResponse._things(self, things, action, *a, **kw)\n        new = self.__getattr__(action)\n        return new(data, *a)\n\n    def insert_table_rows(self, rows, index = -1):\n        new = self.__getattr__(\"insert_table_rows\")\n        return new([row.render(style='html') for row in tup(rows)], index)\n\n\n    # convenience methods:\n    # --------------------\n    #def _mark_error(self, e, field):\n    #    self.find(\".\" + e).show().html(c.errors[e].message).end()\n    #\n    #def _unmark_error(self, e):\n    #    self.find(\".\" + e).html(\"\").end()\n\n    def new_captcha(self):\n        if not self._new_captcha:\n            self.captcha(get_iden())\n            self._new_captcha = True\n        \n    def get_input(self, name):\n        return self.find(\"*[name=%s]\" % name)\n\n    def set_inputs(self, **kw):\n        for k, v in kw.iteritems():\n            # Using 'val' instead of setting the 'value' attribute allows this\n            # To work for non-textbox inputs, like textareas\n            self.get_input(k).val(v).end()\n        return self\n\n    def focus_input(self, name):\n        return self.get_input(name).focus().end()\n\n    def set_html(self, selector, value):\n        if value:\n            return self.find(selector).show().html(value).end()\n        return self.find(selector).hide().html(\"\").end()\n\n    def set_text(self, selector, value):\n        if value:\n            return self.find(selector).show().text(value).end()\n        return self.find(selector).hide().html(\"\").end()\n\n    def set(self, **kw):\n        obj = self\n        for k, v in kw.iteritems():\n            obj = obj.attr(k, v)\n        return obj\n\n    def refresh(self):\n        return self.top_node.transform(self, \"refresh\", [])\n\n"
  },
  {
    "path": "r2/r2/lib/jsontemplates.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport calendar\nfrom collections import defaultdict\n\nfrom utils import to36, tup, iters\nfrom wrapped import Wrapped, StringTemplate, CacheStub, Templated\nfrom mako.template import Template\nfrom r2.config import feature\nfrom r2.config.extensions import get_api_subtype\nfrom r2.lib import hooks\nfrom r2.lib.filters import spaceCompress, safemarkdown, _force_unicode\nfrom r2.models import (\n    Account,\n    Comment,\n    Link,\n    Report,\n    Subreddit,\n    SubredditUserRelations,\n    Trophy,\n)\nfrom r2.models.token import OAuth2Scope, extra_oauth2_scope\nimport time, pytz\nfrom pylons import response\nfrom pylons import tmpl_context as c\nfrom pylons import app_globals as g\nfrom pylons.i18n import _\n\nfrom r2.models.wiki import ImagesByWikiPage\n\n\ndef make_typename(typ):\n    return 't%s' % to36(typ._type_id)\n\ndef make_fullname(typ, _id):\n    return '%s_%s' % (make_typename(typ), to36(_id))\n\n\nclass ObjectTemplate(StringTemplate):\n    def __init__(self, d):\n        self.d = d\n\n    def update(self, kw):\n        def _update(obj):\n            if isinstance(obj, (str, unicode)):\n                return _force_unicode(obj)\n            elif isinstance(obj, dict):\n                return dict((k, _update(v)) for k, v in obj.iteritems())\n            elif isinstance(obj, (list, tuple)):\n                return map(_update, obj)\n            elif isinstance(obj, CacheStub) and kw.has_key(obj.name):\n                return kw[obj.name]\n            else:\n                return obj\n        res = _update(self.d)\n        return ObjectTemplate(res)\n\n    def finalize(self, kw = {}):\n        return self.update(kw).d\n\n\nclass JsonTemplate(Template):\n    def __init__(self): pass\n\n    def render(self, thing = None, *a, **kw):\n        return ObjectTemplate({})\n\n\nclass TakedownJsonTemplate(JsonTemplate):\n    def render(self, thing = None, *a, **kw):\n        return thing.explanation\n\n\nclass ThingTemplate(object):\n    @classmethod\n    def render(cls, thing):\n        \"\"\"\n        Return a JSON representation of a Wrapped Thing object.\n\n        The Thing object should be Wrapped and been run through add_props just\n        like is required for regular HTML rendering. The return value is an\n        ObjectTemplate wrapped dictionary.\n\n        \"\"\"\n\n        api_subtype = get_api_subtype()\n\n        # the argument is named `thing` due to specifics of wrapped\n        item = thing\n\n        if api_subtype:\n            # special handling for rendering a nested template as a different\n            # style (usually html)\n            data = cls.get_rendered(item, render_style=api_subtype)\n        else:\n            data = cls.get_json(item)\n\n        d = {\n            \"kind\": cls.get_kind(item),\n            \"data\": data,\n        }\n        return ObjectTemplate(d)\n\n    @classmethod\n    def get_kind(cls, item):\n        thing = item.lookups[0]\n        return make_typename(thing.__class__)\n\n    @classmethod\n    def get_json(cls, item):\n        data = {\n            \"created\": time.mktime(item._date.timetuple()),\n            \"created_utc\": time.mktime(\n                item._date.astimezone(pytz.UTC).timetuple()) - time.timezone,\n            \"id\": item._id36,\n            \"name\": item._fullname,\n        }\n        return data\n\n    @classmethod\n    def get_rendered(cls, item, render_style):\n        data = {\n            \"id\": item._fullname,\n            \"content\": item.render(style=render_style),\n        }\n        return data\n\n\nclass ThingJsonTemplate(JsonTemplate):\n    _data_attrs_ = dict(\n        created=\"created\",\n        created_utc=\"created_utc\",\n        id=\"_id36\",\n        name=\"_fullname\",\n    )\n\n    @classmethod\n    def data_attrs(cls, **kw):\n        d = cls._data_attrs_.copy()\n        d.update(kw)\n        return d\n    \n    def kind(self, wrapped):\n        \"\"\"\n        Returns a string literal which identifies the type of this\n        thing.  For subclasses of Thing, it will be 't's + kind_id.\n        \"\"\"\n        _thing = wrapped.lookups[0] if isinstance(wrapped, Wrapped) else wrapped\n        return make_typename(_thing.__class__)\n\n    def rendered_data(self, thing):\n        \"\"\"\n        Called only when get_api_type is non-None (i.e., a JSON\n        request has been made with partial rendering of the object to\n        be returned)\n\n        Canonical Thing data representation for JS, which is currently\n        a dictionary of three elements (translated into a JS Object\n        when sent out).  The elements are:\n\n         * id : Thing _fullname of thing.\n         * content : rendered  representation of the thing by\n           calling render on it using the style of get_api_subtype().\n        \"\"\"\n        res =  dict(id = thing._fullname,\n                    content = thing.render(style=get_api_subtype()))\n        return res\n\n    def raw_data(self, thing):\n        \"\"\"\n        Complement to rendered_data.  Called when a dictionary of\n        thing data attributes is to be sent across the wire.\n        \"\"\"\n        attrs = dict(self._data_attrs_)\n        if hasattr(self, \"_optional_data_attrs\"):\n            for attr, attrv in self._optional_data_attrs.iteritems():\n                if hasattr(thing, attr):\n                    attrs[attr] = attrv\n\n        return dict((k, self.thing_attr(thing, v))\n                    for k, v in attrs.iteritems())\n\n    def thing_attr(self, thing, attr):\n        \"\"\"\n        For the benefit of subclasses, to lookup attributes which may\n        require more work than a simple getattr (for example, 'author'\n        which has to be gotten from the author_id attribute on most\n        things).\n        \"\"\"\n        if attr == \"author\":\n            if thing.author._deleted:\n                return \"[deleted]\"\n            return thing.author.name\n\n        if attr == \"created\":\n            return time.mktime(thing._date.timetuple())\n        elif attr == \"created_utc\":\n            return (time.mktime(thing._date.astimezone(pytz.UTC).timetuple())\n                    - time.timezone)\n        elif attr == \"child\":\n            child = getattr(thing, \"child\", None)\n            if child:\n                return child.render()\n            else:\n                return \"\"\n\n        if attr == 'distinguished':\n            distinguished = getattr(thing, attr, 'no')\n            if distinguished == 'no':\n                return None\n            return distinguished\n\n        return getattr(thing, attr, None)\n\n    def data(self, thing):\n        if get_api_subtype():\n            return self.rendered_data(thing)\n        else:\n            return self.raw_data(thing)\n\n    def render(self, thing = None, action = None, *a, **kw):\n        return ObjectTemplate(dict(kind = self.kind(thing),\n                                   data = self.data(thing)))\n\nclass SubredditJsonTemplate(ThingJsonTemplate):\n    _data_attrs_ = ThingJsonTemplate.data_attrs(\n        accounts_active=\"accounts_active_count\",\n        banner_img=\"banner_img\",\n        banner_size=\"banner_size\",\n        collapse_deleted_comments=\"collapse_deleted_comments\",\n        comment_score_hide_mins=\"comment_score_hide_mins\",\n        community_rules=\"community_rules\",\n        description=\"description\",\n        description_html=\"description_html\",\n        display_name=\"name\",\n        header_img=\"header\",\n        header_size=\"header_size\",\n        header_title=\"header_title\",\n        icon_img=\"icon_img\",\n        icon_size=\"icon_size\",\n        # key_color=\"key_color\",\n        lang=\"lang\",\n        over18=\"over_18\",\n        public_description=\"public_description\",\n        public_description_html=\"public_description_html\",\n        public_traffic=\"public_traffic\",\n        # related_subreddits=\"related_subreddits\",\n        hide_ads=\"hide_ads\",\n        quarantine=\"quarantine\",\n        show_media=\"show_media\",\n        show_media_preview=\"show_media_preview\",\n        submission_type=\"link_type\",\n        submit_link_label=\"submit_link_label\",\n        submit_text_label=\"submit_text_label\",\n        submit_text=\"submit_text\",\n        submit_text_html=\"submit_text_html\",\n        subreddit_type=\"type\",\n        subscribers=\"_ups\",\n        suggested_comment_sort=\"suggested_comment_sort\",\n        title=\"title\",\n        url=\"path\",\n        user_is_banned=\"is_banned\",\n        user_is_muted=\"is_muted\",\n        user_is_contributor=\"is_contributor\",\n        user_is_moderator=\"is_moderator\",\n        user_is_subscriber=\"is_subscriber\",\n        user_sr_theme_enabled=\"user_sr_style_enabled\",\n        wiki_enabled=\"wiki_enabled\",\n    )\n\n    # subreddit *attributes* (right side of the equals)\n    # that are accessible even if the user can't view the subreddit\n    _public_attrs = {\n        \"_id36\",\n        # subreddit ID with prefix\n        \"_fullname\",\n        # Creation date\n        \"created\",\n        \"created_utc\",\n        # Canonically-cased subreddit name\n        \"name\",\n        # Canonical subreddit URL, relative to reddit.com\n        \"path\",\n        # Text shown on the access denied page\n        \"public_description\",\n        \"public_description_html\",\n        # Title shown in search\n        \"title\",\n        # Type of subreddit, so people know that it's private\n        \"type\",\n    }\n\n    def raw_data(self, thing):\n        data = ThingJsonTemplate.raw_data(self, thing)\n\n        # remove this when feature is enabled and use _data_attrs instead\n        if feature.is_enabled('mobile_settings'):\n            data['key_color'] = self.thing_attr(thing, 'key_color')\n        if feature.is_enabled('related_subreddits'):\n            data['related_subreddits'] = self.thing_attr(thing, 'related_subreddits')\n\n        permissions = getattr(thing, 'mod_permissions', None)\n        if permissions:\n            permissions = [perm for perm, has in permissions.iteritems() if has]\n            data['mod_permissions'] = permissions\n\n        return data\n\n    def thing_attr(self, thing, attr):\n        if attr not in self._public_attrs and not thing.can_view(c.user):\n            return None\n\n        if (attr == \"_ups\" and\n                (thing.hide_subscribers or thing.hide_num_users_info)):\n            return 0\n        elif attr == 'description_html':\n            return safemarkdown(thing.description)\n        elif attr == 'public_description_html':\n            return safemarkdown(thing.public_description)\n        elif attr == \"is_moderator\":\n            if c.user_is_loggedin:\n                return thing.moderator\n            return None\n        elif attr == \"is_contributor\":\n            if c.user_is_loggedin:\n                return thing.contributor\n            return None\n        elif attr == \"is_subscriber\":\n            if c.user_is_loggedin:\n                return thing.subscriber\n            return None\n        elif attr == 'is_banned':\n            if c.user_is_loggedin:\n                return thing.banned\n            return None\n        elif attr == 'is_muted':\n            if c.user_is_loggedin:\n                return thing.muted\n            return None\n        elif attr == 'submit_text_html':\n            return safemarkdown(thing.submit_text)\n        elif attr == 'user_sr_style_enabled':\n            if c.user_is_loggedin:\n                return c.user.use_subreddit_style(thing)\n            else:\n                return True\n        elif attr == 'wiki_enabled':\n            is_admin_or_mod = c.user_is_loggedin and (\n                c.user_is_admin or thing.is_moderator_with_perms(c.user, 'wiki')\n            )\n\n            return thing.wikimode == 'anyone' or (thing.wikimode == 'modonly' and is_admin_or_mod)\n        else:\n            return ThingJsonTemplate.thing_attr(self, thing, attr)\n\n\nclass LabeledMultiDescriptionJsonTemplate(ThingJsonTemplate):\n    _data_attrs_ = dict(\n        body_html=\"description_html\",\n        body_md=\"description_md\",\n    )\n\n    def kind(self, wrapped):\n        return \"LabeledMultiDescription\"\n\n    def thing_attr(self, thing, attr):\n        if attr == \"description_html\":\n            # if safemarkdown is passed a falsy string it returns None :/\n            description_html = safemarkdown(thing.description_md) or ''\n            return description_html\n        else:\n            return ThingJsonTemplate.thing_attr(self, thing, attr)\n\n\nclass LabeledMultiJsonTemplate(LabeledMultiDescriptionJsonTemplate):\n    _data_attrs_ = ThingJsonTemplate.data_attrs(\n        can_edit=\"can_edit\",\n        copied_from=\"copied_from\",\n        description_html=\"description_html\",\n        description_md=\"description_md\",\n        display_name=\"display_name\",\n        key_color=\"key_color\",\n        icon_name=\"icon_id\",\n        icon_url=\"icon_url\",\n        name=\"name\",\n        path=\"path\",\n        subreddits=\"srs\",\n        visibility=\"visibility\",\n        weighting_scheme=\"weighting_scheme\",\n    )\n    del _data_attrs_[\"id\"]\n\n    def __init__(self, expand_srs=False):\n        super(LabeledMultiJsonTemplate, self).__init__()\n        self.expand_srs = expand_srs\n\n    def kind(self, wrapped):\n        return \"LabeledMulti\"\n\n    @classmethod\n    def sr_props(cls, thing, srs, expand=False):\n        sr_props = dict(thing.sr_props)\n        if expand:\n            sr_dicts = get_trimmed_sr_dicts(srs, c.user)\n            for sr in srs:\n                sr_props[sr._id][\"data\"] = sr_dicts[sr._id]\n        return [dict(sr_props[sr._id], name=sr.name) for sr in srs]\n\n    def thing_attr(self, thing, attr):\n        if attr == \"srs\":\n            return self.sr_props(thing, thing.srs, expand=self.expand_srs)\n        elif attr == \"can_edit\":\n            return c.user_is_loggedin and thing.can_edit(c.user)\n        elif attr == \"copied_from\":\n            if thing.can_edit(c.user):\n                return thing.copied_from\n            else:\n                return None\n        elif attr == \"display_name\":\n            return thing.display_name or thing.name\n        else:\n            super_ = super(LabeledMultiJsonTemplate, self)\n            return super_.thing_attr(thing, attr)\n\n\ndef get_trimmed_sr_dicts(srs, user):\n    if c.user_is_loggedin:\n        sr_user_relations = Subreddit.get_sr_user_relations(user, srs)\n    else:\n        # backwards compatibility: for loggedout users don't return boolean,\n        # instead return None for all relations.\n        NO_SR_USER_RELATIONS = SubredditUserRelations(\n            subscriber=None,\n            moderator=None,\n            contributor=None,\n            banned=None,\n            muted=None,\n        )\n        sr_user_relations = defaultdict(lambda: NO_SR_USER_RELATIONS)\n\n    ret = {}\n    for sr in srs:\n        relations = sr_user_relations[sr._id]\n        can_view = sr.can_view(user)\n        subscribers = sr._ups if not sr.hide_subscribers else 0\n\n        data = dict(\n            name=sr._fullname,\n            display_name=sr.name,\n            url=sr.path,\n            banner_img=sr.banner_img if can_view else None,\n            banner_size=sr.banner_size if can_view else None,\n            header_img=sr.header if can_view else None,\n            header_size=sr.header_size if can_view else None,\n            icon_img=sr.icon_img if can_view else None,\n            icon_size=sr.icon_size if can_view else None,\n            key_color=sr.key_color if can_view else None,\n            subscribers=subscribers if can_view else None,\n            user_is_banned=relations.banned if can_view else None,\n            user_is_muted=relations.muted if can_view else None,\n            user_is_contributor=relations.contributor if can_view else None,\n            user_is_moderator=relations.moderator if can_view else None,\n            user_is_subscriber=relations.subscriber if can_view else None,\n        )\n\n        if feature.is_enabled('mobile_settings'):\n            data[\"key_color\"] = sr.key_color if can_view else None\n\n        ret[sr._id] = data\n    return ret\n\n\nclass IdentityJsonTemplate(ThingJsonTemplate):\n    _data_attrs_ = ThingJsonTemplate.data_attrs(\n        comment_karma=\"comment_karma\",\n        has_verified_email=\"email_verified\",\n        is_gold=\"gold\",\n        is_mod=\"is_mod\",\n        link_karma=\"link_karma\",\n        name=\"name\",\n        hide_from_robots=\"pref_hide_from_robots\",\n    )\n    _private_data_attrs = dict(\n        inbox_count=\"inbox_count\",\n        over_18=\"pref_over_18\",\n        gold_creddits=\"gold_creddits\",\n        gold_expiration=\"gold_expiration\",\n        is_suspended=\"in_timeout\",\n        suspension_expiration_utc=\"timeout_expiration_utc\",\n        features=\"features\",\n    )\n    _public_attrs = {\n        \"name\",\n        \"is_suspended\",\n    }\n\n    def raw_data(self, thing):\n        viewable = True\n        attrs = self._data_attrs_.copy()\n        if c.user_is_loggedin and thing._id == c.user._id:\n            attrs.update(self._private_data_attrs)\n        # Add a public indication when a user is permanently in timeout.\n        elif (thing.in_timeout and thing.timeout_expiration is None):\n            attrs.update({\"is_suspended\": \"in_timeout\"})\n            viewable = False\n\n        if thing.pref_hide_from_robots:\n            response.headers['X-Robots-Tag'] = 'noindex, nofollow'\n\n        data = {k: self.thing_attr(thing, v) for k, v in attrs.iteritems()\n                if viewable or k in self._public_attrs}\n        try:\n            self.add_message_data(data, thing)\n        except OAuth2Scope.InsufficientScopeError:\n            # No access to privatemessages, but the rest of\n            # the identity information is sufficient.\n            pass\n\n        # Add as private data attributes states about this user. This is used\n        # for feature flagging by user state on first-party API clients.\n        if c.user_is_loggedin and thing._id == c.user._id:\n            data['is_employee'] = thing.employee\n            data['in_beta'] = thing.pref_beta\n\n        return data\n\n    @extra_oauth2_scope(\"privatemessages\")\n    def add_message_data(self, data, thing):\n        if c.user_is_loggedin and thing._id == c.user._id:\n            data['has_mail'] = self.thing_attr(thing, 'has_mail')\n            data['has_mod_mail'] = self.thing_attr(thing, 'has_mod_mail')\n\n    def thing_attr(self, thing, attr):\n        from r2.lib.template_helpers import (\n            display_comment_karma, display_link_karma)\n        if attr == \"is_mod\":\n            t = thing.lookups[0] if isinstance(thing, Wrapped) else thing\n            return t.is_moderator_somewhere\n        elif attr == \"has_mail\":\n            return bool(c.have_messages)\n        elif attr == \"has_mod_mail\":\n            return bool(c.have_mod_messages)\n        elif attr == \"comment_karma\":\n            return display_comment_karma(thing.comment_karma)\n        elif attr == \"link_karma\":\n            return display_link_karma(thing.link_karma)\n        elif attr == \"gold_expiration\":\n            if not thing.gold:\n                return None\n            return calendar.timegm(thing.gold_expiration.utctimetuple())\n        elif attr == \"timeout_expiration_utc\":\n            expiration_date = thing.timeout_expiration\n            if not expiration_date:\n                return None\n\n            return calendar.timegm(expiration_date.utctimetuple())\n        elif attr == \"features\":\n            return feature.all_enabled(c.user)\n\n        return ThingJsonTemplate.thing_attr(self, thing, attr)\n\n\nclass AccountJsonTemplate(IdentityJsonTemplate):\n    _data_attrs_ = IdentityJsonTemplate.data_attrs(is_friend=\"is_friend\")\n    _private_data_attrs = dict(\n        modhash=\"modhash\",\n        **IdentityJsonTemplate._private_data_attrs\n    )\n\n    def thing_attr(self, thing, attr):\n        if attr == \"is_friend\":\n            return c.user_is_loggedin and thing._id in c.user.friends\n        elif attr == \"modhash\":\n            return c.modhash\n        return IdentityJsonTemplate.thing_attr(self, thing, attr)\n\n\n\nclass PrefsJsonTemplate(ThingJsonTemplate):\n    _data_attrs_ = dict((k[len(\"pref_\"):], k) for k in\n            Account._preference_attrs)\n\n    def __init__(self, fields=None):\n        if fields is not None:\n            _data_attrs_ = {}\n            for field in fields:\n                if field not in self._data_attrs_:\n                    raise KeyError(field)\n                _data_attrs_[field] = self._data_attrs_[field]\n            self._data_attrs_ = _data_attrs_\n\n    def thing_attr(self, thing, attr):\n        if attr == \"pref_clickgadget\":\n            return bool(thing.pref_clickgadget)\n        return ThingJsonTemplate.thing_attr(self, thing, attr)\n\n\ndef get_mod_attributes(item):\n    data = {}\n    if c.user_is_loggedin and item.can_ban:\n        data[\"num_reports\"] = item.reported\n        data[\"report_reasons\"] = Report.get_reasons(item)\n\n        ban_info = getattr(item, \"ban_info\", {})\n        if item._spam:\n            data[\"approved_by\"] = None\n            if ban_info.get('moderator_banned'):\n                data[\"banned_by\"] = ban_info.get(\"banner\")\n            else:\n                data[\"banned_by\"] = True\n        else:\n            data[\"approved_by\"] = ban_info.get(\"unbanner\")\n            data[\"banned_by\"] = None\n    else:\n        data[\"num_reports\"] = None\n        data[\"report_reasons\"] = None\n        data[\"approved_by\"] = None\n        data[\"banned_by\"] = None\n    return data\n\n\ndef get_author_attributes(item):\n    data = {}\n    if not item.author._deleted:\n        author = item.author\n        sr_id = item.subreddit._id\n\n        data[\"author\"] = author.name\n\n        if author.flair_enabled_in_sr(sr_id):\n            flair_text = getattr(author, 'flair_%s_text' % sr_id, None)\n            flair_css = getattr(author, 'flair_%s_css_class' % sr_id, None)\n        else:\n            flair_text = None\n            flair_css = None\n        data[\"author_flair_text\"] = flair_text\n        data[\"author_flair_css_class\"] = flair_css\n\n    else:\n        data[\"author\"] = \"[deleted]\"\n        data[\"author_flair_text\"] = None\n        data[\"author_flair_css_class\"] = None\n    return data\n\n\ndef get_distinguished_attributes(item):\n    data = {}\n    distinguished = getattr(item, \"distinguished\", \"no\")\n    data[\"distinguished\"] = distinguished if distinguished != \"no\" else None\n    return data\n\n\ndef get_edited_attributes(item):\n    data = {}\n    if isinstance(item.editted, bool):\n        data[\"edited\"] = item.editted\n    else:\n        editted_timetuple = item.editted.astimezone(pytz.UTC).timetuple()\n        data[\"edited\"] = time.mktime(editted_timetuple) - time.timezone\n    return data\n\n\ndef get_report_reason_attributes(item):\n    if c.user_is_loggedin and c.user.in_timeout:\n        data = {\n            \"user_reports\": [],\n            \"mod_reports\": [],\n        }\n    else:\n        data = {\n            \"user_reports\": item.user_reports,\n            \"mod_reports\": item.mod_reports,\n        }\n    return data\n\n\ndef get_removal_reason_attributes(item):\n    data = {}\n    if getattr(item, \"admin_takedown\", None):\n        data[\"removal_reason\"] = \"legal\"\n    else:\n        data[\"removal_reason\"] = None\n    return data\n\n\ndef get_media_embed_attributes(item):\n    from r2.lib.media import get_media_embed\n\n    data = {\n        \"media_embed\": {},\n        \"secure_media_embed\": {},\n    }\n\n    media_object = item.media_object\n    if media_object and not isinstance(media_object, basestring):\n        media_embed = get_media_embed(media_object)\n        if media_embed:\n            data[\"media_embed\"] = {\n                \"scrolling\": media_embed.scrolling,\n                \"width\": media_embed.width,\n                \"height\": media_embed.height,\n                \"content\": media_embed.content,\n            }\n\n    secure_media_object = item.secure_media_object\n    if secure_media_object and not isinstance(secure_media_object, basestring):\n        secure_media_embed = get_media_embed(secure_media_object)\n        if secure_media_embed:\n            data[\"secure_media_embed\"] = {\n                \"scrolling\": secure_media_embed.scrolling,\n                \"width\": secure_media_embed.width,\n                \"height\": secure_media_embed.height,\n                \"content\": secure_media_embed.content,\n            }\n    return data\n\n\ndef get_selftext_attributes(item):\n    data = {}\n    if not item.expunged:\n        data[\"selftext\"] = item.selftext\n        data[\"selftext_html\"] = safemarkdown(item.selftext)\n    else:\n        data[\"selftext\"] = \"[removed]\"\n        data[\"selftext_html\"] = safemarkdown(_(\"[removed]\"))\n    return data\n\n\ndef generate_image_links(preview_object, file_type=None, censor_nsfw=False):\n    PREVIEW_RESOLUTIONS = (108, 216, 320, 640, 960, 1080)\n    PREVIEW_MAX_RATIO = 2\n\n    # Determine which previews would be feasible with our given dims\n    source_width = preview_object['width']\n    source_height = preview_object['height']\n    source_ratio = float(source_height) / source_width\n\n    # previews with a ratio above the max will be cropped to a lower ratio\n    max_ratio = float(PREVIEW_MAX_RATIO)\n    preview_ratio = min(source_ratio, max_ratio)\n\n    preview_resolutions = []\n    for w in PREVIEW_RESOLUTIONS:\n        if w > source_width:\n            continue\n\n        url = g.image_resizing_provider.resize_image(\n            preview_object,\n            w,\n            file_type=file_type,\n            censor_nsfw=censor_nsfw,\n            max_ratio=PREVIEW_MAX_RATIO,\n        )\n        h = int(w * preview_ratio)\n        preview_resolutions.append({\n            \"url\": url,\n            \"width\": w,\n            \"height\": h,\n        })\n\n    url = g.image_resizing_provider.resize_image(\n        preview_object,\n        file_type=file_type,\n        censor_nsfw=censor_nsfw,\n    )\n\n    return {\n        \"source\": {\n            \"url\": url,\n            \"width\": source_width,\n            \"height\": source_height,\n        },\n        \"resolutions\": preview_resolutions,\n    }\n\n\nclass LinkJsonTemplate(ThingTemplate):\n    @classmethod\n    def get_json(cls, item):\n        data = ThingTemplate.get_json(item)\n\n        data.update(get_mod_attributes(item))\n        data.update(get_author_attributes(item))\n        data.update(get_distinguished_attributes(item))\n        data.update(get_edited_attributes(item))\n        data.update(get_media_embed_attributes(item))\n        data.update(get_report_reason_attributes(item))\n        data.update(get_removal_reason_attributes(item))\n        data.update(get_selftext_attributes(item))\n\n        data.update({\n            \"archived\": not item.votable,\n            \"visited\": item.visited,\n            \"clicked\": False,\n            \"contest_mode\": item.contest_mode,\n            \"domain\": item.domain,\n            \"downs\": 0,\n            \"gilded\": item.gildings,\n            \"hidden\": item.hidden,\n            \"hide_score\": item.hide_score,\n            \"is_self\": item.is_self,\n            \"likes\": item.likes,\n            \"link_flair_css_class\": item.flair_css_class,\n            \"link_flair_text\": item.flair_text,\n            \"locked\": item.locked,\n            \"media\": item.media_object,\n            \"secure_media\": item.secure_media_object,\n            \"num_comments\": item.num_comments,\n            \"over_18\": item.over_18,\n            \"quarantine\": item.quarantine,\n            \"permalink\": item.permalink,\n            \"saved\": item.saved,\n            \"score\": item.score,\n            \"stickied\": item.stickied,\n            \"subreddit\": item.subreddit.name,\n            \"subreddit_id\": item.subreddit._fullname,\n            \"suggested_sort\": item.sort_if_suggested(sr=item.subreddit),\n            \"thumbnail\": item.thumbnail,\n            \"title\": item.title,\n            \"ups\": item.score,\n            \"url\": item.url,\n        })\n\n        if hasattr(item, \"action_type\"):\n            data[\"action_type\"] = item.action_type\n\n        if hasattr(item, \"sr_detail\"):\n            data[\"sr_detail\"] = item.sr_detail\n\n        if hasattr(item, \"show_media\"):\n            data[\"show_media\"] = item.show_media\n\n        if c.permalink_page:\n            data[\"upvote_ratio\"] = item.upvote_ratio\n\n        preview_object = item.preview_image\n        if preview_object:\n            preview_is_gif = preview_object.get('url', '').endswith('.gif')\n            data['preview'] = {}\n            data['post_hint'] = item.post_hint\n            # For gifs, the default preview should be a static image, with the\n            # full gif as a variant\n            if preview_is_gif:\n                images = generate_image_links(preview_object, file_type=\"jpg\")\n            else:\n                images = generate_image_links(preview_object)\n\n            images['id'] = preview_object['uid']\n            images['variants'] = {}\n            if item.nsfw:\n                images['variants']['nsfw'] = generate_image_links(\n                    preview_object, censor_nsfw=True, file_type=\"png\")\n            if preview_is_gif:\n                images['variants']['gif'] = generate_image_links(\n                    preview_object)\n                images['variants']['mp4'] = generate_image_links(\n                    preview_object, file_type=\"mp4\")\n            data['preview']['images'] = [images]\n        return data\n\n    @classmethod\n    def get_rendered(cls, item, render_style):\n        data = ThingTemplate.get_rendered(item, render_style)\n        data.update({\n            \"sr\": item.subreddit._fullname,\n        })\n        return data\n\n\nclass PromotedLinkJsonTemplate(LinkJsonTemplate):\n    @classmethod\n    def get_json(cls, item):\n        data = LinkJsonTemplate.get_json(item)\n        data.update({\n            \"promoted\": item.promoted,\n            \"imp_pixel\": getattr(item, \"imp_pixel\", None),\n            \"href_url\": item.href_url,\n            \"adserver_imp_pixel\": getattr(item, \"adserver_imp_pixel\", None),\n            \"adserver_click_url\": getattr(item, \"adserver_click_url\", None),\n            \"mobile_ad_url\": item.mobile_ad_url,\n            \"disable_comments\": item.disable_comments,\n            \"third_party_tracking\": item.third_party_tracking,\n            \"third_party_tracking_2\": item.third_party_tracking_2,\n        })\n\n        del data[\"subreddit\"]\n        del data[\"subreddit_id\"]\n        return data\n\n\nclass CommentJsonTemplate(ThingTemplate):\n    @classmethod\n    def get_parent_id(cls, item):\n        from r2.models import Comment, Link\n\n        if getattr(item, \"parent_id\", None):\n            return make_fullname(Comment, item.parent_id)\n        else:\n            return make_fullname(Link, item.link_id)\n\n    @classmethod\n    def get_link_name(cls, item):\n        from r2.models import Link\n        return make_fullname(Link, item.link_id)\n\n    @classmethod\n    def render_child(cls, item):\n        child = getattr(item, \"child\", None)\n        if child:\n            return child.render()\n        else:\n            return \"\"\n\n    @classmethod\n    def get_json(cls, item):\n        from r2.models import Link\n\n        data = ThingTemplate.get_json(item)\n\n        data.update(get_mod_attributes(item))\n        data.update(get_author_attributes(item))\n        data.update(get_distinguished_attributes(item))\n        data.update(get_edited_attributes(item))\n        data.update(get_report_reason_attributes(item))\n        data.update(get_removal_reason_attributes(item))\n\n        data.update({\n            \"archived\": not item.votable,\n            \"body\": item.body,\n            \"body_html\": spaceCompress(safemarkdown(item.body)),\n            \"controversiality\": 1 if item.is_controversial else 0,\n            \"downs\": 0,\n            \"gilded\": item.gildings,\n            \"likes\": item.likes,\n            \"link_id\": cls.get_link_name(item),\n            \"saved\": item.saved,\n            \"score\": item.score,\n            \"score_hidden\": item.score_hidden,\n            \"subreddit\": item.subreddit.name,\n            \"subreddit_id\": item.subreddit._fullname,\n            \"ups\": item.score,\n            \"replies\": cls.render_child(item),\n            \"parent_id\": cls.get_parent_id(item),\n        })\n\n        if feature.is_enabled('sticky_comments'):\n            data[\"stickied\"] = item.link.sticky_comment_id == item._id\n\n        if hasattr(item, \"action_type\"):\n            data[\"action_type\"] = item.action_type\n\n        if c.profilepage:\n            data[\"quarantine\"] = item.subreddit.quarantine\n            data[\"over_18\"] = item.link.is_nsfw\n\n            data[\"link_title\"] = item.link.title\n            data[\"link_author\"] = item.link_author.name\n\n            if item.link.is_self:\n                link_url = item.link.make_permalink(\n                    item.subreddit, force_domain=True)\n            else:\n                link_url = item.link.url\n            data[\"link_url\"] = link_url\n\n        return data\n\n    @classmethod\n    def get_rendered(cls, item, render_style):\n        data = ThingTemplate.get_rendered(item, render_style)\n        data.update({\n            \"replies\": cls.render_child(item),\n            \"contentText\": item.body,\n            \"contentHTML\": spaceCompress(safemarkdown(item.body)),\n            \"link\": cls.get_link_name(item),\n            \"parent\": cls.get_parent_id(item),\n        })\n        return data\n\n\nclass MoreCommentJsonTemplate(ThingTemplate):\n    @classmethod\n    def get_kind(cls, item):\n        return \"more\"\n\n    @classmethod\n    def get_json(cls, item):\n        data = {\n            \"children\": [to36(comment_id) for comment_id in item.children],\n            \"count\": item.count,\n            \"id\": item._id36,\n            \"name\": item._fullname,\n            \"parent_id\": CommentJsonTemplate.get_parent_id(item),\n        }\n        return data\n\n    @classmethod\n    def get_rendered(cls, item, render_style):\n        data = ThingTemplate.get_rendered(item, render_style)\n        data.update({\n            \"replies\": \"\",\n            \"contentText\": \"\",\n            \"contentHTML\": \"\",\n            \"link\": CommentJsonTemplate.get_link_name(item),\n            \"parent\": CommentJsonTemplate.get_parent_id(item),\n        })\n        return data\n\n\nclass MessageJsonTemplate(ThingJsonTemplate):\n    _data_attrs_ = ThingJsonTemplate.data_attrs(\n        author=\"author\",\n        body=\"body\",\n        body_html=\"body_html\",\n        context=\"context\",\n        created=\"created\",\n        dest=\"dest\",\n        distinguished=\"distinguished\",\n        first_message=\"first_message\",\n        first_message_name=\"first_message_name\",\n        new=\"new\",\n        parent_id=\"parent_id\",\n        replies=\"child\",\n        subject=\"subject\",\n        subreddit=\"subreddit\",\n        was_comment=\"was_comment\",\n    )\n\n    def thing_attr(self, thing, attr):\n        from r2.models import Comment, Link, Message\n        if attr == \"was_comment\":\n            return thing.was_comment\n        elif attr == \"context\":\n            return (\"\" if not thing.was_comment\n                    else thing.permalink + \"?context=3\")\n        elif attr == \"dest\":\n            if thing.to_id:\n                return thing.to.name\n            else:\n                return \"#\" + thing.subreddit.name\n        elif attr == \"subreddit\":\n            if thing.sr_id:\n                return thing.subreddit.name\n            return None\n        elif attr == \"body_html\":\n            return safemarkdown(thing.body)\n        elif attr == \"author\" and getattr(thing, \"hide_author\", False):\n            return None\n        elif attr == \"parent_id\":\n            if thing.was_comment:\n                if getattr(thing, \"parent_id\", None):\n                    return make_fullname(Comment, thing.parent_id)\n                else:\n                    return make_fullname(Link, thing.link_id)\n            elif getattr(thing, \"parent_id\", None):\n                return make_fullname(Message, thing.parent_id)\n        elif attr == \"first_message_name\":\n            if getattr(thing, \"first_message\", None):\n                return make_fullname(Message, thing.first_message)\n        return ThingJsonTemplate.thing_attr(self, thing, attr)\n\n    def raw_data(self, thing):\n        d = ThingJsonTemplate.raw_data(self, thing)\n        if thing.was_comment:\n            d['link_title'] = thing.link_title\n            d['likes'] = thing.likes\n        return d\n\n    def rendered_data(self, wrapped):\n        from r2.models import Message\n        parent_id = wrapped.parent_id\n        if parent_id:\n            parent_id = make_fullname(Message, parent_id)\n        d = ThingJsonTemplate.rendered_data(self, wrapped)\n        d['parent'] = parent_id\n        d['contentText'] = self.thing_attr(wrapped, 'body')\n        d['contentHTML'] = self.thing_attr(wrapped, 'body_html')\n        return d\n\n\nclass RedditJsonTemplate(JsonTemplate):\n    def render(self, thing = None, *a, **kw):\n        return ObjectTemplate(thing.content().render() if thing else {})\n\nclass PanestackJsonTemplate(JsonTemplate):\n    def render(self, thing = None, *a, **kw):\n        res = [t.render() for t in thing.stack if t] if thing else []\n        res = [x for x in res if x]\n        if not res:\n            return {}\n        return ObjectTemplate(res if len(res) > 1 else res[0] )\n\nclass NullJsonTemplate(JsonTemplate):\n    def render(self, thing = None, *a, **kw):\n        return \"\"\n\n    def get_def(self, name):\n        return self\n\nclass ListingJsonTemplate(ThingJsonTemplate):\n    _data_attrs_ = dict(\n        after=\"after\",\n        before=\"before\",\n        children=\"things\",\n        modhash=\"modhash\",\n    )\n    \n    def thing_attr(self, thing, attr):\n        if attr == \"modhash\":\n            return c.modhash\n        elif attr == \"things\":\n            res = []\n            for a in thing.things:\n                a.childlisting = False\n                r = a.render()\n                res.append(r)\n            return res\n        return ThingJsonTemplate.thing_attr(self, thing, attr)\n        \n\n    def rendered_data(self, thing):\n        return self.thing_attr(thing, \"things\")\n    \n    def kind(self, wrapped):\n        return \"Listing\"\n\n\nclass SearchListingJsonTemplate(ListingJsonTemplate):\n    def raw_data(self, thing):\n        data = ThingJsonTemplate.raw_data(self, thing)\n\n        def format_sr(sr, count):\n            return {'name': sr.name, 'url': sr.path, 'count': count}\n\n        facets = {}\n        if thing.subreddit_facets:\n            facets['subreddits'] = [format_sr(sr, count)\n                                    for sr, count in thing.subreddit_facets]\n        data['facets'] = facets\n\n        return data\n\n\nclass UserListingJsonTemplate(ListingJsonTemplate):\n    def raw_data(self, thing):\n        if not thing.nextprev:\n            return {\"children\": self.rendered_data(thing)}\n        return ListingJsonTemplate.raw_data(self, thing)\n\n    def kind(self, wrapped):\n        return \"Listing\" if wrapped.nextprev else \"UserList\"\n\nclass UserListJsonTemplate(ThingJsonTemplate):\n    _data_attrs_ = dict(\n        children=\"users\",\n    )\n\n    def thing_attr(self, thing, attr):\n        if attr == \"users\":\n            res = []\n            for a in thing.user_rows:\n                r = a.render()\n                res.append(r)\n            return res\n        return ThingJsonTemplate.thing_attr(self, thing, attr)\n\n    def rendered_data(self, thing):\n        return self.thing_attr(thing, \"users\")\n\n    def kind(self, wrapped):\n        return \"UserList\"\n\n\nclass UserTableItemJsonTemplate(ThingJsonTemplate):\n    _data_attrs_ = dict(\n        id=\"_fullname\",\n        name=\"name\",\n    )\n\n    def thing_attr(self, thing, attr):\n        return ThingJsonTemplate.thing_attr(self, thing.user, attr)\n\n    def render(self, thing, *a, **kw):\n        return ObjectTemplate(self.data(thing))\n\n\nclass RelTableItemJsonTemplate(UserTableItemJsonTemplate):\n    _data_attrs_ = UserTableItemJsonTemplate.data_attrs(\n        date=\"date\",\n    )\n\n    def thing_attr(self, thing, attr):\n        rel_attr, splitter, attr = attr.partition(\".\")\n        if attr == 'note':\n            # return empty string instead of None for missing note\n            return ThingJsonTemplate.thing_attr(self, thing.rel, attr) or ''\n        elif attr:\n            return ThingJsonTemplate.thing_attr(self, thing.rel, attr)\n        elif rel_attr == 'date':\n            # make date UTC\n            date = self.thing_attr(thing, 'rel._date')\n            date = time.mktime(date.astimezone(pytz.UTC).timetuple())\n            return date - time.timezone\n        else:\n            return UserTableItemJsonTemplate.thing_attr(self, thing, rel_attr)\n\n\nclass FriendTableItemJsonTemplate(RelTableItemJsonTemplate):\n    def inject_data(self, thing, d):\n        if c.user.gold and thing.type == \"friend\":\n            d[\"note\"] = self.thing_attr(thing, 'rel.note')\n        return d\n\n    def rendered_data(self, thing):\n        d = RelTableItemJsonTemplate.rendered_data(self, thing)\n        return self.inject_data(thing, d)\n\n    def raw_data(self, thing):\n        d = RelTableItemJsonTemplate.raw_data(self, thing)\n        return self.inject_data(thing, d)\n\n\nclass BannedTableItemJsonTemplate(RelTableItemJsonTemplate):\n    _data_attrs_ = RelTableItemJsonTemplate.data_attrs(\n        note=\"rel.note\",\n    )\n\n\nclass MutedTableItemJsonTemplate(RelTableItemJsonTemplate):\n    pass\n\n\nclass InvitedModTableItemJsonTemplate(RelTableItemJsonTemplate):\n    _data_attrs_ = RelTableItemJsonTemplate.data_attrs(\n        mod_permissions=\"permissions\",\n    )\n\n    def thing_attr(self, thing, attr):\n        if attr == 'permissions':\n            permissions = thing.permissions.items()\n            return [perm for perm, has in permissions if has]\n        else:\n            return RelTableItemJsonTemplate.thing_attr(self, thing, attr)\n\n\nclass OrganicListingJsonTemplate(ListingJsonTemplate):\n    def kind(self, wrapped):\n        return \"OrganicListing\"\n\nclass TrafficJsonTemplate(JsonTemplate):\n    def render(self, thing, *a, **kw):\n        res = {}\n\n        for interval in (\"hour\", \"day\", \"month\"):\n            # we don't actually care about the column definitions (used for\n            # charting) here, so just pass an empty list.\n            interval_data = thing.get_data_for_interval(interval, [])\n\n            # turn the python datetimes into unix timestamps and flatten data\n            res[interval] = [(calendar.timegm(date.timetuple()),) + data\n                             for date, data in interval_data]\n\n        return ObjectTemplate(res)\n\nclass WikiJsonTemplate(JsonTemplate):\n    def render(self, thing, *a, **kw):\n        try:\n            content = thing.content()\n        except AttributeError:\n            content = thing.listing\n        return ObjectTemplate(content.render() if thing else {})\n\nclass WikiPageListingJsonTemplate(ThingJsonTemplate):\n    def kind(self, thing):\n        return \"wikipagelisting\"\n    \n    def data(self, thing):\n        pages = [p.name for p in thing.linear_pages]\n        return pages\n\nclass WikiViewJsonTemplate(ThingJsonTemplate):\n    def kind(self, thing):\n        return \"wikipage\"\n    \n    def data(self, thing):\n        edit_date = time.mktime(thing.edit_date.timetuple()) if thing.edit_date else None\n        edit_by = None\n        if thing.edit_by and not thing.edit_by._deleted:\n             edit_by = Wrapped(thing.edit_by).render()\n        return dict(content_md=thing.page_content_md,\n                    content_html=thing.page_content,\n                    revision_by=edit_by,\n                    revision_date=edit_date,\n                    may_revise=thing.may_revise)\n\nclass WikiSettingsJsonTemplate(ThingJsonTemplate):\n     def kind(self, thing):\n         return \"wikipagesettings\"\n    \n     def data(self, thing):\n         editors = [Wrapped(e).render() for e in thing.mayedit]\n         return dict(permlevel=thing.permlevel,\n                     listed=thing.listed,\n                     editors=editors)\n\nclass WikiRevisionJsonTemplate(ThingJsonTemplate):\n    def render(self, thing, *a, **kw):\n        timestamp = time.mktime(thing.date.timetuple()) if thing.date else None\n        author = thing.get_author()\n        if author and not author._deleted:\n            author = Wrapped(author).render()\n        else:\n            author = None\n        return ObjectTemplate(dict(author=author,\n                                   id=str(thing._id),\n                                   timestamp=timestamp,\n                                   reason=thing._get('reason'),\n                                   page=thing.page))\n\nclass FlairListJsonTemplate(JsonTemplate):\n    def render(self, thing, *a, **kw):\n        def row_to_json(row):\n            if hasattr(row, 'user'):\n              return dict(user=row.user.name, flair_text=row.flair_text,\n                          flair_css_class=row.flair_css_class)\n            else:\n              # prev/next link\n              return dict(after=row.after, reverse=row.previous)\n\n        json_rows = [row_to_json(row) for row in thing.flair]\n        result = dict(users=[row for row in json_rows if 'user' in row])\n        for row in json_rows:\n            if 'after' in row:\n                if row['reverse']:\n                    result['prev'] = row['after']\n                else:\n                    result['next'] = row['after']\n        return ObjectTemplate(result)\n\nclass FlairCsvJsonTemplate(JsonTemplate):\n    def render(self, thing, *a, **kw):\n        return ObjectTemplate([l.__dict__ for l in thing.results_by_line])\n\n\nclass FlairSelectorJsonTemplate(JsonTemplate):\n    def _template_dict(self, flair):\n        return {\"flair_template_id\": flair.flair_template_id,\n                \"flair_position\": flair.flair_position,\n                \"flair_text\": flair.flair_text,\n                \"flair_css_class\": flair.flair_css_class,\n                \"flair_text_editable\": flair.flair_text_editable}\n\n    def render(self, thing, *a, **kw):\n        \"\"\"Render a list of flair choices into JSON\n\n        Sample output:\n        {\n            \"choices\": [\n                {\n                    \"flair_css_class\": \"flair-444\",\n                    \"flair_position\": \"right\",\n                    \"flair_template_id\": \"5668d204-9388-11e3-8109-080027a38559\",\n                    \"flair_text\": \"444\",\n                    \"flair_text_editable\": true\n                },\n                {\n                    \"flair_css_class\": \"flair-nouser\",\n                    \"flair_position\": \"right\",\n                    \"flair_template_id\": \"58e34d7a-9388-11e3-ab01-080027a38559\",\n                    \"flair_text\": \"nouser\",\n                    \"flair_text_editable\": true\n                },\n                {\n                    \"flair_css_class\": \"flair-bar\",\n                    \"flair_position\": \"right\",\n                    \"flair_template_id\": \"fb01cc04-9391-11e3-b1d6-080027a38559\",\n                    \"flair_text\": \"foooooo\",\n                    \"flair_text_editable\": true\n                }\n            ],\n            \"current\": {\n                \"flair_css_class\": \"444\",\n                \"flair_position\": \"right\",\n                \"flair_template_id\": \"5668d204-9388-11e3-8109-080027a38559\",\n                \"flair_text\": \"444\"\n            }\n        }\n\n        \"\"\"\n        choices = [self._template_dict(choice) for choice in thing.choices]\n\n        current_flair = {\n            \"flair_text\": thing.text,\n            \"flair_css_class\": thing.css_class,\n            \"flair_position\": thing.position,\n            \"flair_template_id\": thing.matching_template,\n        }\n        return ObjectTemplate({\"current\": current_flair, \"choices\": choices})\n\n\nclass StylesheetTemplate(ThingJsonTemplate):\n    _data_attrs_ = dict(\n        images='_images',\n        stylesheet='stylesheet_contents',\n        subreddit_id='_fullname',\n    )\n\n    def kind(self, wrapped):\n        return 'stylesheet'\n\n    def images(self):\n        sr_images = ImagesByWikiPage.get_images(c.site, \"config/stylesheet\")\n        images = []\n        for name, url in sr_images.iteritems():\n            images.append({'name': name,\n                           'link': 'url(%%%%%s%%%%)' % name,\n                           'url': url})\n        return images\n\n    def thing_attr(self, thing, attr):\n        if attr == '_images':\n            return self.images()\n        elif attr == '_fullname':\n            return c.site._fullname\n        return ThingJsonTemplate.thing_attr(self, thing, attr)\n\nclass SubredditSettingsTemplate(ThingJsonTemplate):\n    _data_attrs_ = dict(\n        allow_images='site.allow_images',\n        collapse_deleted_comments='site.collapse_deleted_comments',\n        comment_score_hide_mins='site.comment_score_hide_mins',\n        content_options='site.link_type',\n        default_set='site.allow_top',\n        description='site.description',\n        domain='site.domain',\n        exclude_banned_modqueue='site.exclude_banned_modqueue',\n        header_hover_text='site.header_title',\n        # key_color='site.key_color',\n        language='site.lang',\n        over_18='site.over_18',\n        public_description='site.public_description',\n        public_traffic='site.public_traffic',\n        # related_subreddits='site.related_subreddits',\n        hide_ads=\"site.hide_ads\",\n        show_media='site.show_media',\n        show_media_preview='site.show_media_preview',\n        submit_link_label='site.submit_link_label',\n        submit_text_label='site.submit_text_label',\n        submit_text='site.submit_text',\n        subreddit_id='site._fullname',\n        subreddit_type='site.type',\n        suggested_comment_sort=\"site.suggested_comment_sort\",\n        title='site.title',\n        wiki_edit_age='site.wiki_edit_age',\n        wiki_edit_karma='site.wiki_edit_karma',\n        wikimode='site.wikimode',\n        spam_links='site.spam_links',\n        spam_selfposts='site.spam_selfposts',\n        spam_comments='site.spam_comments',\n    )\n\n    def kind(self, wrapped):\n        return 'subreddit_settings'\n\n    def thing_attr(self, thing, attr):\n        if attr.startswith('site.') and thing.site:\n            return getattr(thing.site, attr[5:])\n        if attr == 'related_subreddits' and thing.site:\n            # string used for form input\n            return '\\n'.join(thing.site.related_subreddits)\n        return ThingJsonTemplate.thing_attr(self, thing, attr)\n\n    def raw_data(self, thing):\n        data = ThingJsonTemplate.raw_data(self, thing)\n\n        # remove this when feature is enabled and use _data_attrs instead\n        if feature.is_enabled('mobile_settings'):\n            data['key_color'] = self.thing_attr(thing, 'key_color')\n        if feature.is_enabled('related_subreddits'):\n            data['related_subreddits'] = self.thing_attr(thing, 'related_subreddits')\n\n        return data\n\n\nclass UploadedImageJsonTemplate(JsonTemplate):\n    def render(self, thing, *a, **kw):\n        return ObjectTemplate({\n            \"errors\": list(k for (k, v) in thing.errors if v),\n            \"img_src\": thing.img_src,\n        })\n\n\nclass ModActionTemplate(ThingJsonTemplate):\n    _data_attrs_ = dict(\n        action='action',\n        created_utc='date',\n        description='description',\n        details='details',\n        id='_fullname',\n        mod='moderator',\n        mod_id36='mod_id36',\n        sr_id36='sr_id36',\n        subreddit='subreddit',\n        target_author='target_author',\n        target_fullname='target_fullname',\n        target_permalink='target_permalink',\n        target_title='target_title',\n        target_body='target_body',\n    )\n\n    def thing_attr(self, thing, attr):\n        if attr == 'date':\n            return (time.mktime(thing.date.astimezone(pytz.UTC).timetuple())\n                    - time.timezone)\n        elif attr == 'target_author':\n            if thing.target_author and thing.target_author._deleted:\n                return \"[deleted]\"\n            elif thing.target_author:\n                return thing.target_author.name\n            return \"\"\n        elif attr == 'target_permalink':\n            try:\n                return thing.target.make_permalink_slow()\n            except AttributeError:\n                return None\n        elif attr == \"moderator\":\n            return thing.moderator.name\n        elif attr == \"subreddit\":\n            return thing.subreddit.name\n        elif attr == 'target_title' and isinstance(thing.target, Link):\n            return thing.target.title\n        elif attr == 'target_body' and isinstance(thing.target, Comment):\n            return thing.target.body\n        elif (attr == 'target_body' and isinstance(thing.target, Link)\n              and getattr(thing.target, 'selftext', None)):\n            return thing.target.selftext\n\n        return ThingJsonTemplate.thing_attr(self, thing, attr)\n\n    def kind(self, wrapped):\n        return 'modaction'\n\n\nclass PolicyViewJsonTemplate(ThingJsonTemplate):\n    _data_attrs_ = dict(\n        body_html=\"body_html\",\n        display_rev=\"display_rev\",\n        revs=\"revs\",\n        toc_html=\"toc_html\",\n    )\n\n    def kind(self, wrapped):\n        return \"Policy\"\n\nclass KarmaListJsonTemplate(ThingJsonTemplate):\n    def data(self, karmas):\n        from r2.lib.template_helpers import (\n            display_comment_karma, display_link_karma)\n        karmas = [{\n            'sr': sr,\n            'link_karma': display_link_karma(link_karma),\n            'comment_karma': display_comment_karma(comment_karma),\n        } for sr, (link_karma, comment_karma) in karmas.iteritems()]\n        return karmas\n\n    def kind(self, wrapped):\n        return \"KarmaList\"\n\n\ndef get_usertrophies(user):\n    trophies = Trophy.by_account(user)\n    def visible_trophy(trophy):\n        return trophy._thing2.awardtype != 'invisible'\n    trophies = filter(visible_trophy, trophies)\n    resp = TrophyListJsonTemplate().render(trophies)\n    return resp.finalize()\n\n\nclass TrophyJsonTemplate(ThingJsonTemplate):\n    _data_attrs_ = dict(\n        award_id=\"award._id36\",\n        description=\"description\",\n        name=\"award.title\",\n        id=\"_id36\",\n        icon_40=\"icon_40\",\n        icon_70=\"icon_70\",\n        url=\"trophy_url\",\n    )\n\n    def thing_attr(self, thing, attr):\n        if attr == \"icon_40\":\n            return \"https:\" + thing._thing2.imgurl % 40\n        elif attr == \"icon_70\":\n            return \"https:\" + thing._thing2.imgurl % 70\n        rel_attr, splitter, attr = attr.partition(\".\")\n        if attr:\n            return ThingJsonTemplate.thing_attr(self, thing._thing2, attr)\n        else:\n            return ThingJsonTemplate.thing_attr(self, thing, rel_attr)\n\n    def kind(self, thing):\n        return ThingJsonTemplate.kind(self, thing._thing2)\n\nclass TrophyListJsonTemplate(ThingJsonTemplate):\n    def data(self, trophies):\n        trophies = [Wrapped(t).render() for t in trophies]\n        return dict(trophies=trophies)\n\n    def kind(self, wrapped):\n        return \"TrophyList\"\n\n\nclass RulesJsonTemplate(JsonTemplate):\n    def render(self, thing, *a, **kw):\n        rules = {}\n        rules['site_rules'] = thing.site_rules\n        rules['rules'] = thing.rules\n\n        for rule in rules[\"rules\"]:\n            if rule.get(\"description\"):\n                rule[\"description_html\"] = safemarkdown(rule[\"description\"])\n            if not rule.get(\"kind\"):\n                rule[\"kind\"] = \"all\"\n\n        return ObjectTemplate(rules)\n"
  },
  {
    "path": "r2/r2/lib/language.py",
    "content": "import re\nfrom collections import namedtuple, Counter\n\nCharRange = namedtuple(\"CharRange\", \"name start end\")\n\nNONCHAR = re.compile(r\"\\W+\")\n\n\ndef charset_name(name, start, end):\n    if name.lower() == \"undefined\":\n        return \"_\".join([name.title(), start, end])\n    else:\n        return NONCHAR.sub(\"_\", name.title())\n\nCHARSET_RANGES = tuple(\n    CharRange(charset_name(name, start, end), int(start, 16), int(end, 16))\n    for start, end, name in (\n        (\"0000\", \"007F\", \"Basic Latin\"),\n        (\"0080\", \"00FF\", \"C1 Controls and Latin-1 Supplement\"),\n        (\"0100\", \"017F\", \"Latin Extended-A\"),\n        (\"0180\", \"024F\", \"Latin Extended-B\"),\n        (\"0250\", \"02AF\", \"IPA Extensions\"),\n        (\"02B0\", \"02FF\", \"Spacing Modifier Letters\"),\n        (\"0300\", \"036F\", \"Combining Diacritical Marks\"),\n        (\"0370\", \"03FF\", \"Greek/Coptic\"),\n        (\"0400\", \"04FF\", \"Cyrillic\"),\n        (\"0500\", \"052F\", \"Cyrillic Supplement\"),\n        (\"0530\", \"058F\", \"Armenian\"),\n        (\"0590\", \"05FF\", \"Hebrew\"),\n        (\"0600\", \"06FF\", \"Arabic\"),\n        (\"0700\", \"074F\", \"Syriac\"),\n        (\"0750\", \"077F\", \"Undefined\"),\n        (\"0780\", \"07BF\", \"Thaana\"),\n        (\"07C0\", \"08FF\", \"Undefined\"),\n        (\"0900\", \"097F\", \"Devanagari\"),\n        (\"0980\", \"09FF\", \"Bengali/Assamese\"),\n        (\"0A00\", \"0A7F\", \"Gurmukhi\"),\n        (\"0A80\", \"0AFF\", \"Gujarati\"),\n        (\"0B00\", \"0B7F\", \"Oriya\"),\n        (\"0B80\", \"0BFF\", \"Tamil\"),\n        (\"0C00\", \"0C7F\", \"Telugu\"),\n        (\"0C80\", \"0CFF\", \"Kannada\"),\n        (\"0D00\", \"0DFF\", \"Malayalam\"),\n        (\"0D80\", \"0DFF\", \"Sinhala\"),\n        (\"0E00\", \"0E7F\", \"Thai\"),\n        (\"0E80\", \"0EFF\", \"Lao\"),\n        (\"0F00\", \"0FFF\", \"Tibetan\"),\n        (\"1000\", \"109F\", \"Myanmar\"),\n        (\"10A0\", \"10FF\", \"Georgian\"),\n        (\"1100\", \"11FF\", \"Hangul Jamo\"),\n        (\"1200\", \"137F\", \"Ethiopic\"),\n        (\"1380\", \"139F\", \"Undefined\"),\n        (\"13A0\", \"13FF\", \"Cherokee\"),\n        (\"1400\", \"167F\", \"Unified Canadian Aboriginal Syllabics\"),\n        (\"1680\", \"169F\", \"Ogham\"),\n        (\"16A0\", \"16FF\", \"Runic\"),\n        (\"1700\", \"171F\", \"Tagalog\"),\n        (\"1720\", \"173F\", \"Hanunoo\"),\n        (\"1740\", \"175F\", \"Buhid\"),\n        (\"1760\", \"177F\", \"Tagbanwa\"),\n        (\"1780\", \"17FF\", \"Khmer\"),\n        (\"1800\", \"18AF\", \"Mongolian\"),\n        (\"18B0\", \"18FF\", \"Undefined\"),\n        (\"1900\", \"194F\", \"Limbu\"),\n        (\"1950\", \"197F\", \"Tai Le\"),\n        (\"1980\", \"19DF\", \"Undefined\"),\n        (\"19E0\", \"19FF\", \"Khmer Symbols\"),\n        (\"1A00\", \"1CFF\", \"Undefined\"),\n        (\"1D00\", \"1D7F\", \"Phonetic Extensions\"),\n        (\"1D80\", \"1DFF\", \"Undefined\"),\n        (\"1E00\", \"1EFF\", \"Latin Extended Additional\"),\n        (\"1F00\", \"1FFF\", \"Greek Extended\"),\n        (\"2000\", \"206F\", \"General Punctuation\"),\n        (\"2070\", \"209F\", \"Superscripts and Subscripts\"),\n        (\"20A0\", \"20CF\", \"Currency Symbols\"),\n        (\"20D0\", \"20FF\", \"Combining Diacritical Marks for Symbols\"),\n        (\"2100\", \"214F\", \"Letterlike Symbols\"),\n        (\"2150\", \"218F\", \"Number Forms\"),\n        (\"2190\", \"21FF\", \"Arrows\"),\n        (\"2200\", \"22FF\", \"Mathematical Operators\"),\n        (\"2300\", \"23FF\", \"Miscellaneous Technical\"),\n        (\"2400\", \"243F\", \"Control Pictures\"),\n        (\"2440\", \"245F\", \"Optical Character Recognition\"),\n        (\"2460\", \"24FF\", \"Enclosed Alphanumerics\"),\n        (\"2500\", \"257F\", \"Box Drawing\"),\n        (\"2580\", \"259F\", \"Block Elements\"),\n        (\"25A0\", \"25FF\", \"Geometric Shapes\"),\n        (\"2600\", \"26FF\", \"Miscellaneous Symbols\"),\n        (\"2700\", \"27BF\", \"Dingbats\"),\n        (\"27C0\", \"27EF\", \"Miscellaneous Mathematical Symbols-A\"),\n        (\"27F0\", \"27FF\", \"Supplemental Arrows-A\"),\n        (\"2800\", \"28FF\", \"Braille Patterns\"),\n        (\"2900\", \"297F\", \"Supplemental Arrows-B\"),\n        (\"2980\", \"29FF\", \"Miscellaneous Mathematical Symbols-B\"),\n        (\"2A00\", \"2AFF\", \"Supplemental Mathematical Operators\"),\n        (\"2B00\", \"2BFF\", \"Miscellaneous Symbols and Arrows\"),\n        (\"2C00\", \"2E7F\", \"Undefined\"),\n        (\"2E80\", \"2EFF\", \"CJK Radicals Supplement\"),\n        (\"2F00\", \"2FDF\", \"Kangxi Radicals\"),\n        (\"2FE0\", \"2FEF\", \"Undefined\"),\n        (\"2FF0\", \"2FFF\", \"Ideographic Description Characters\"),\n        (\"3000\", \"303F\", \"CJK Symbols and Punctuation\"),\n        (\"3040\", \"309F\", \"Hiragana\"),\n        (\"30A0\", \"30FF\", \"Katakana\"),\n        (\"3100\", \"312F\", \"Bopomofo\"),\n        (\"3130\", \"318F\", \"Hangul Compatibility Jamo\"),\n        (\"3190\", \"319F\", \"Kanbun (Kunten)\"),\n        (\"31A0\", \"31BF\", \"Bopomofo Extended\"),\n        (\"31C0\", \"31EF\", \"Undefined\"),\n        (\"31F0\", \"31FF\", \"Katakana Phonetic Extensions\"),\n        (\"3200\", \"32FF\", \"Enclosed CJK Letters and Months\"),\n        (\"3300\", \"33FF\", \"CJK Compatibility\"),\n        (\"3400\", \"4DBF\", \"CJK Unified Ideographs Extension A\"),\n        (\"4DC0\", \"4DFF\", \"Yijing Hexagram Symbols\"),\n        (\"4E00\", \"9FAF\", \"CJK Unified Ideographs\"),\n        (\"9FB0\", \"9FFF\", \"Undefined\"),\n        (\"A000\", \"A48F\", \"Yi Syllables\"),\n        (\"A490\", \"A4CF\", \"Yi Radicals\"),\n        (\"A4D0\", \"ABFF\", \"Undefined\"),\n        (\"AC00\", \"D7AF\", \"Hangul Syllables\"),\n        (\"D7B0\", \"D7FF\", \"Undefined\"),\n        (\"D800\", \"DBFF\", \"High Surrogate Area\"),\n        (\"DC00\", \"DFFF\", \"Low Surrogate Area\"),\n        (\"E000\", \"F8FF\", \"Private Use Area\"),\n        (\"F900\", \"FAFF\", \"CJK Compatibility Ideographs\"),\n        (\"FB00\", \"FB4F\", \"Alphabetic Presentation Forms\"),\n        (\"FB50\", \"FDFF\", \"Arabic Presentation Forms-A\"),\n        (\"FE00\", \"FE0F\", \"Variation Selectors\"),\n        (\"FE10\", \"FE1F\", \"Undefined\"),\n        (\"FE20\", \"FE2F\", \"Combining Half Marks\"),\n        (\"FE30\", \"FE4F\", \"CJK Compatibility Forms\"),\n        (\"FE50\", \"FE6F\", \"Small Form Variants\"),\n        (\"FE70\", \"FEFF\", \"Arabic Presentation Forms-B\"),\n        (\"FF00\", \"FFEF\", \"Halfwidth and Fullwidth Forms\"),\n        (\"FFF0\", \"FFFF\", \"Specials\"),\n        (\"10000\", \"1007F\", \"Linear B Syllabary\"),\n        (\"10080\", \"100FF\", \"Linear B Ideograms\"),\n        (\"10100\", \"1013F\", \"Aegean Numbers\"),\n        (\"10140\", \"102FF\", \"Undefined\"),\n        (\"10300\", \"1032F\", \"Old Italic\"),\n        (\"10330\", \"1034F\", \"Gothic\"),\n        (\"10380\", \"1039F\", \"Ugaritic\"),\n        (\"10400\", \"1044F\", \"Deseret\"),\n        (\"10450\", \"1047F\", \"Shavian\"),\n        (\"10480\", \"104AF\", \"Osmanya\"),\n        (\"104B0\", \"107FF\", \"Undefined\"),\n        (\"10800\", \"1083F\", \"Cypriot Syllabary\"),\n        (\"10840\", \"1CFFF\", \"Undefined\"),\n        (\"1D000\", \"1D0FF\", \"Byzantine Musical Symbols\"),\n        (\"1D100\", \"1D1FF\", \"Musical Symbols\"),\n        (\"1D200\", \"1D2FF\", \"Undefined\"),\n        (\"1D300\", \"1D35F\", \"Tai Xuan Jing Symbols\"),\n        (\"1D360\", \"1D3FF\", \"Undefined\"),\n        (\"1D400\", \"1D7FF\", \"Mathematical Alphanumeric Symbols\"),\n        (\"1D800\", \"1FFFF\", \"Undefined\"),\n        (\"20000\", \"2A6DF\", \"CJK Unified Ideographs Extension B\"),\n        (\"2A6E0\", \"2F7FF\", \"Undefined\"),\n        (\"2F800\", \"2FA1F\", \"CJK Compatibility Ideographs Supplement\"),\n        (\"2FAB0\", \"DFFFF\", \"Unused\"),\n        (\"E0000\", \"E007F\", \"Tags\"),\n        (\"E0080\", \"E00FF\", \"Unused\"),\n        (\"E0100\", \"E01EF\", \"Variation Selectors Supplement\"),\n        (\"E01F0\", \"EFFFF\", \"Unused\"),\n        (\"F0000\", \"FFFFD\", \"Supplementary Private Use Area-A\"),\n        (\"FFFFE\", \"FFFFF\", \"Unused\"),\n        (\"100000\", \"10FFFD\", \"Supplementary Private Use Area-B\"),\n    )\n)\n\n\ndef symbology(s):\n    \"\"\"Return a count of what unicode charsets the string contains.\"\"\"\n    symbols = sorted(ord(c) for c in s)\n    current_charset = 0\n    char_tally = Counter()\n    for symbol in symbols:\n        while CHARSET_RANGES[current_charset].end < symbol:\n            current_charset += 1\n        if CHARSET_RANGES[current_charset].start <= symbol:\n            name = CHARSET_RANGES[current_charset].name\n        else:\n            name = \"Unknown\"\n        char_tally[name] += 1\n\n    return char_tally\n\n\ndef charset_summary(s, prefix=\"\"):\n    res = {}\n    charsets = symbology(s)\n    if charsets:\n        res[\"charset\"] = charsets.most_common(1)[0][0]\n        res[\"all_charsets\"] = dict(charsets.most_common())\n    return res\n"
  },
  {
    "path": "r2/r2/lib/lock.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom __future__ import with_statement\nfrom time import sleep\nfrom datetime import datetime\nfrom threading import local\nfrom pylons import app_globals as g\nimport os\nimport socket\nimport random\n\nfrom _pylibmc import MemcachedError\n\nfrom r2.lib.utils import simple_traceback\n\n# thread-local storage for detection of recursive locks\nlocks = local()\n\nreddit_host = socket.gethostname()\nreddit_pid  = os.getpid()\n\nclass TimeoutExpired(Exception): pass\n\nclass MemcacheLock(object):\n    \"\"\"A simple global lock based on the memcache 'add' command. We\n    attempt to grab a lock by 'adding' the lock name. If the response\n    is True, we have the lock. If it's False, someone else has it.\"\"\"\n\n    def __init__(self, stats, group, key, cache,\n                 time=30, timeout=30, verbose=True):\n        # get a thread-local set of locks that we own\n        self.locks = locks.locks = getattr(locks, 'locks', set())\n\n        self.stats = stats\n        self.group = group\n        self.key = key\n        self.cache = cache\n        self.time = time\n        self.timeout = timeout\n        self.have_lock = False\n        self.owns_lock = False\n        self.verbose = verbose\n\n    def __enter__(self):\n        self.acquire()\n        return self\n\n    def __exit__(self, type, value, tb):\n        self.release()\n\n    def acquire(self):\n        start = datetime.now()\n\n        self.nonce = (reddit_host, reddit_pid, simple_traceback(limit=7))\n\n        # if this thread already has this lock, move on\n        if self.key in self.locks:\n            self.have_lock = True\n            return\n\n        timer = self.stats.get_timer(\"lock_wait\")\n        timer.start()\n\n        # try and fetch the lock, looping until it's available\n        lock = None\n        while not lock:\n            # catch all exceptions here because we can't trust the memcached\n            # protocol. The add for the lock may have actually succeeded.\n            try:\n                lock = self.cache.add(self.key, self.nonce, time = self.time)\n            except MemcachedError as e:\n                if self.cache.get(self.key) == self.nonce:\n                    g.log.error(\n                        'Memcached add succeeded, but threw an exception for key %r %s',\n                        self.key, e)\n                    break\n\n            if not lock:\n                if (datetime.now() - start).seconds > self.timeout:\n                    if self.verbose:\n                        info = self.cache.get(self.key)\n                        if info:\n                            info = \"%s %s\\n%s\" % info\n                        else:\n                            info = \"(nonexistent)\"\n                        msg = (\"\\nSome jerk is hogging %s:\\n%s\" %\n                                         (self.key, info))\n                        msg += \"^^^ that was the stack trace of the lock hog, not me.\"\n                    else:\n                        msg = \"Timed out waiting for %s\" % self.key\n                    raise TimeoutExpired(msg)\n                else:\n                    # this should prevent unnecessary spam on highly contended locks.\n                    sleep(random.uniform(0.1, 1))\n\n        timer.stop(subname=self.group)\n\n        self.owns_lock = True\n        self.have_lock = True\n\n        # tell this thread we have this lock so we can avoid deadlocks\n        # of requests for the same lock in the same thread\n        self.locks.add(self.key)\n\n    def release(self):\n        # only release the lock if we acquired it in the first place (are owner)\n        if self.owns_lock:\n            # verify that our lock did not expire before we could release it\n            if self.cache.get(self.key) == self.nonce:\n                self.cache.delete(self.key)\n            else:\n                g.log.error(\"Lock expired before completion at key %r: %s\",\n                            self.key, self.nonce)\n            self.locks.remove(self.key)\n            self.have_lock = False\n            self.owns_lock = False\n            self.nonce = None\n\n\ndef make_lock_factory(cache, stats):\n    def factory(group, key, **kw):\n        return MemcacheLock(stats, group, key, cache, **kw)\n    return factory\n"
  },
  {
    "path": "r2/r2/lib/log.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom hashlib import md5\nimport sys\n\nfrom pylons import request\nfrom pylons import tmpl_context as c\nfrom pylons import app_globals as g\nfrom pylons.util import PylonsContext, AttribSafeContextObj, ContextObj\nimport raven\nfrom raven.processors import Processor\nfrom weberror.reporter import Reporter\n\nfrom r2.lib.app_globals import Globals\n\n\ndef get_operational_exceptions():\n    import _pylibmc\n    import sqlalchemy.exc\n    import pycassa.pool\n    import r2.lib.db.thing\n    import r2.lib.lock\n    import r2.lib.cache\n\n    return (\n        SystemExit,  # gunicorn is shutting us down\n        _pylibmc.MemcachedError,\n        r2.lib.db.thing.NotFound,\n        r2.lib.lock.TimeoutExpired,\n        sqlalchemy.exc.OperationalError,\n        sqlalchemy.exc.IntegrityError,\n        pycassa.pool.AllServersUnavailable,\n        pycassa.pool.NoConnectionAvailable,\n        pycassa.pool.MaximumRetryException,\n    )\n\n\nclass SanitizeStackLocalsProcessor(Processor):\n    keys_to_remove = (\n        \"self\",\n        \"__traceback_supplement__\",\n    )\n\n    classes_to_remove = (\n        Globals,\n        PylonsContext,\n        AttribSafeContextObj,\n        ContextObj,\n    )\n\n    def filter_stacktrace(self, data, **kwargs):\n        def remove_keys(obj):\n            if isinstance(obj, dict):\n                for k in obj.keys():\n                    if k in self.keys_to_remove:\n                        obj.pop(k)\n                    elif isinstance(obj[k], self.classes_to_remove):\n                        obj.pop(k)\n                    elif isinstance(obj[k], basestring):\n                        contains_forbidden_repr = any(\n                            _cls.__name__ in obj[k]\n                            for _cls in self.classes_to_remove\n                        )\n                        if contains_forbidden_repr:\n                            obj.pop(k)\n                    elif isinstance(obj[k], (list, dict)):\n                        remove_keys(obj[k])\n            elif isinstance(obj, list):\n                for v in obj:\n                    if isinstance(v, (list, dict)):\n                        remove_keys(v)\n\n        for frame in data.get('frames', []):\n            if 'vars' in frame:\n                remove_keys(frame['vars'])\n\n\nclass RavenErrorReporter(Reporter):\n    @classmethod\n    def get_module_versions(cls):\n        return {\n            repo: commit_hash[:6]\n            for repo, commit_hash in g.versions.iteritems()\n        }\n\n    @classmethod\n    def add_http_context(cls, client):\n        \"\"\"Add request details to the 'request' context\n\n        These fields will be filtered by SanitizePasswordsProcessor\n        as long as they are one of 'data', 'cookies', 'headers', 'env', and\n        'query_string'.\n\n        \"\"\"\n\n        HEADER_WHITELIST = (\n            \"user-agent\",\n            \"host\",\n            \"accept\",\n            \"accept-encoding\",\n            \"accept-language\",\n            \"referer\",\n        )\n        headers = {\n            k: v for k, v in request.headers.iteritems()\n            if k.lower() in HEADER_WHITELIST\n        }\n\n        client.http_context({\n            \"url\": request.path,\n            \"method\": request.method,\n            \"query_string\": request.query_string,\n            \"data\": request.body,\n            \"headers\": headers,\n        })\n\n        if \"app\" in request.GET:\n            client.tags_context({\"app\": request.GET[\"app\"]})\n\n    @classmethod\n    def add_reddit_context(cls, client):\n        reddit_context = {\n            \"language\": c.lang,\n            \"render_style\": c.render_style,\n        }\n\n        if c.site:\n            reddit_context[\"subreddit\"] = c.site.name\n\n        client.extra_context(reddit_context)\n\n    @classmethod\n    def add_user_context(cls, client):\n        user_context = {}\n\n        if c.user_is_loggedin:\n            user_context[\"user\"] = c.user._id\n\n        if c.oauth2_client:\n            user_context[\"oauth_client_id\"] = c.oauth2_client._id\n            user_context[\"oauth_client_name\"] = c.oauth2_client.name\n\n        client.user_context(user_context)\n\n    @classmethod\n    def get_raven_client(cls):\n        app_path_prefixes = [\n            \"r2\",\n            \"reddit_\",  # plugins such as 'reddit_liveupdate'\n            \"/opt/\",    # scripts may be run from /opt/REPO/scripts\n        ]\n        release_str = '|'.join(\n           \"%s:%s\" % (repo, commit_hash)\n           for repo, commit_hash in sorted(g.versions.items())\n        )\n        release_hash = md5(release_str).hexdigest()\n\n        RAVEN_CLIENT = raven.Client(\n            dsn=g.sentry_dsn,\n            # use the default transport to send errors from another thread:\n            transport=raven.transport.threaded.ThreadedHTTPTransport,\n            include_paths=app_path_prefixes,\n            processors=[\n                'raven.processors.SanitizePasswordsProcessor',\n                'r2.lib.log.SanitizeStackLocalsProcessor',\n            ],\n            release=release_hash,\n            environment=g.pool_name,\n            include_versions=False,     # handled by get_module_versions\n            install_sys_hook=False,\n        )\n        return RAVEN_CLIENT\n\n    @classmethod\n    def capture_exception(cls, exc_info=None):\n        if exc_info is None:\n            # if possible exc_info should be captured as close to the exception\n            # as possible and passed in because sys.exc_info() can give\n            # unexpected behavior\n            exc_info = sys.exc_info()\n\n        if issubclass(exc_info[0], get_operational_exceptions()):\n            return\n\n        client = cls.get_raven_client()\n\n        if g.running_as_script:\n            # scripts are run like:\n            # paster run INIFILE -c \"python code to execute\"\n            # OR\n            # paster run INIFILE script.py\n            # either way sys.argv[-1] will tell us the entry point to the error\n            culprit = 'script: \"%s\"' % sys.argv[-1]\n        else:\n            cls.add_http_context(client)\n            cls.add_reddit_context(client)\n            cls.add_user_context(client)\n\n            routes_dict = request.environ[\"pylons.routes_dict\"]\n            controller = routes_dict.get(\"controller\", \"unknown\")\n            action = routes_dict.get(\"action\", \"unknown\")\n            culprit = \"%s.%s\" % (controller, action)\n\n        try:\n            client.captureException(\n                exc_info=exc_info,\n                data={\n                    \"modules\": cls.get_module_versions(),\n                    \"culprit\": culprit,\n                },\n            )\n        finally:\n            client.context.clear()\n\n    def report(self, exc_data):\n        self.capture_exception()\n\n\ndef write_error_summary(error):\n    \"\"\"Log a single-line summary of the error for easy log grepping.\"\"\"\n    fullpath = request.environ.get('FULLPATH', request.path)\n    uid = c.user._id if c.user_is_loggedin else '-'\n    g.log.error(\"E: %s U: %s FP: %s\", error, uid, fullpath)\n\n\nclass LoggingErrorReporter(Reporter):\n    \"\"\"ErrorMiddleware-compatible reporter that writes exceptions to g.log.\"\"\"\n\n    def report(self, exc_data):\n        # exception_formatted is the output of traceback.format_exception_only\n        exception = exc_data.exception_formatted[-1].strip()\n\n        # First emit a single-line summary.  This is great for grepping the\n        # streaming log for errors.\n        write_error_summary(exception)\n\n        text, extra = self.format_text(exc_data)\n        # TODO: send this all in one burst so that error reports aren't\n        # interleaved / individual lines aren't dropped. doing so will take\n        # configuration on the syslog side and potentially in apptail as well\n        for line in text.splitlines():\n            g.log.warning(line)\n"
  },
  {
    "path": "r2/r2/lib/loid.py",
    "content": "from datetime import datetime, timedelta\nfrom dateutil.parser import parse as date_parse\nimport pytz\nimport string\nfrom urllib import quote, unquote\n\nfrom pylons import app_globals as g\n\nfrom . import hooks\nfrom .utils import randstr, to_epoch_milliseconds\n\nLOID_COOKIE = \"loid\"\nLOID_CREATED_COOKIE = \"loidcreated\"\n# how long the cookie should last, by default.\nEXPIRES_RELATIVE = timedelta(days=2 * 365)\n\nGLOBAL_VERSION = 0\nLOID_LENGTH = 18\nLOID_CHARSPACE = string.uppercase + string.lowercase + string.digits\n\n\ndef isodate(d):\n    # Python's `isoformat` isn't actually perfectly ISO.  This more\n    # closely matches the format we were getting in JS\n    d = d.astimezone(pytz.UTC)\n    milliseconds = (\"%06d\" % d.microsecond)[0:3]\n    return d.strftime(\"%Y-%m-%dT%H:%M:%S.\") + milliseconds + \"Z\"\n\n\ndef ensure_unquoted(cookie_str):\n    \"\"\"Keep unquoting.  Never surrender.\n\n    Some of the cookies issued in the first version of this patch ended up\n    doubly quote()d.  As a preventative measure, unquote several times.\n    [This could be a while loop, because every iteration will cause the str\n    to at worst get shorter and at best stay the same and break the loop.  I\n    just don't want to replace an escaping error with a possible infinite\n    loop.]\n\n    :param str cookie_str: Cookie string.\n    \"\"\"\n    for _ in range(3):\n        new_str = unquote(cookie_str)\n        if new_str == cookie_str:\n            return new_str\n        cookie_str = new_str\n\n\nclass LoId(object):\n    \"\"\"Container for holding and validating logged out ids.\n\n    The primary accessor functions for this class are:\n\n     * :py:meth:`load` to pull the ``LoId`` out of the request cookies\n     * :py:meth:`save` to save an ``LoId`` to cookies\n     * :py:meth:`to_dict` to serialize this object's data to the event pipe\n    \"\"\"\n\n    def __init__(\n        self,\n        request,\n        context,\n        loid=None,\n        new=None,\n        version=GLOBAL_VERSION,\n        created=None,\n        serializable=True\n    ):\n        self.context = context\n        self.request = request\n\n        # is this a newly generated ID?\n        self.new = new\n        # the unique ID\n        self.loid = loid and str(loid)\n        # When was this loid created\n        self.created = created or datetime.now(pytz.UTC)\n\n        self.version = version\n\n        # should this be persisted as cookie?\n        self.serializable = serializable\n        # should this be re-written-out even if it's not new.\n        self.dirty = new\n\n    def _trigger_event(self, action):\n        g.events.loid_event(\n            loid=self,\n            action_name=action,\n            request=self.request,\n            context=self.context,\n        )\n\n    @classmethod\n    def _create(cls, request, context):\n        \"\"\"Create and return a new logged out id and timestamp.\n\n        This also triggers an loid_event in the event pipeline.\n\n        :param request: current :py:module:`pylons` request object\n        :param context: current :py:module:`pylons` context object\n        :rtype: :py:class:`LoId`\n        :returns: new ``LoId``\n        \"\"\"\n        loid = cls(\n            request=request,\n            context=context,\n            new=True,\n            loid=randstr(LOID_LENGTH, LOID_CHARSPACE),\n        )\n        loid._trigger_event(\"create_loid\")\n        return loid\n\n    @classmethod\n    def load(cls, request, context, create=True):\n        \"\"\"Load loid (and timestamp) from cookie or optionally create one.\n\n        :param request: current :py:module:`pylons` request object\n        :param context: current :py:module:`pylons` context object\n        :param bool create: On failure to load from a cookie,\n        :rtype: :py:class:`LoId`\n        \"\"\"\n        stub = cls(request, context, serializable=False)\n\n        loid = request.cookies.get(LOID_COOKIE)\n        if loid:\n            # future-proof to v1 id tracking\n            loid, _, _ = unquote(loid).partition(\".\")\n            try:\n                created = ensure_unquoted(\n                    request.cookies.get(LOID_CREATED_COOKIE, \"\"))\n                created = date_parse(created)\n            except ValueError:\n                created = None\n            return cls(\n                request,\n                context,\n                new=False,\n                loid=loid,\n                version=0,\n                created=created,\n            )\n        elif create:\n            return cls._create(request, context)\n        else:\n            return stub\n\n    def save(self, **cookie_attrs):\n        \"\"\"Write to cookie if serializable and dirty (generally new).\n\n        :param dict cookie_attrs: additional cookie attrs.\n        \"\"\"\n        if self.serializable and self.dirty:\n            expires = datetime.utcnow() + EXPIRES_RELATIVE\n            for (name, value) in (\n                (LOID_COOKIE, self.loid),\n                (LOID_CREATED_COOKIE, isodate(self.created)),\n            ):\n                d = cookie_attrs.copy()\n                d.setdefault(\"expires\", expires)\n                self.context.cookies.add(name, value, **d)\n\n    def to_dict(self, prefix=None):\n        \"\"\"Serialize LoId, generally for use in the event pipeline.\"\"\"\n        if not self.serializable:\n            return {}\n\n        d = {\n            \"loid\": self.loid,\n            \"loid_created\": to_epoch_milliseconds(self.created),\n            \"loid_new\": self.new,\n            \"loid_version\": self.version,\n        }\n        hook = hooks.get_hook(\"loid.to_dict\")\n        hook.call(loid=self, data=d)\n        if prefix:\n            d = {\"{}{}\".format(prefix, k): v for k, v in d.iteritems()}\n\n        return d\n"
  },
  {
    "path": "r2/r2/lib/manager/__init__.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\n"
  },
  {
    "path": "r2/r2/lib/manager/db_manager.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport logging\nimport os\nimport random\nimport socket\nimport sqlalchemy\nimport time\nimport traceback\n\n\nlogger = logging.getLogger('dm_manager')\nlogger.addHandler(logging.StreamHandler())\nAPPLICATION_NAME = \"reddit@%s:%d\" % (socket.gethostname(), os.getpid())\n\n\ndef get_engine(name, db_host='', db_user='', db_pass='', db_port='5432',\n               pool_size=5, max_overflow=5, g_override=None):\n    db_port = int(db_port)\n\n    arguments = {\n        \"dbname\": name,\n        \"host\": db_host,\n        \"port\": db_port,\n        \"application_name\": APPLICATION_NAME,\n    }\n    if db_user:\n        arguments[\"user\"] = db_user\n    if db_pass:\n        arguments[\"password\"] = db_pass\n    dsn = \"%20\".join(\"%s=%s\" % x for x in arguments.iteritems())\n\n    engine = sqlalchemy.create_engine(\n        'postgresql:///?dsn=' + dsn,\n        strategy='threadlocal',\n        pool_size=int(pool_size),\n        max_overflow=int(max_overflow),\n        # our code isn't ready for unicode to appear\n        # in place of strings yet\n        use_native_unicode=False,\n    )\n\n    if g_override:\n        sqlalchemy.event.listens_for(engine, 'before_cursor_execute')(\n            g_override.stats.pg_before_cursor_execute)\n        sqlalchemy.event.listens_for(engine, 'after_cursor_execute')(\n            g_override.stats.pg_after_cursor_execute)\n\n    return engine\n\n\nclass db_manager:\n    def __init__(self):\n        self.type_db = None\n        self.relation_type_db = None\n        self._things = {}\n        self._relations = {}\n        self._engines = {}\n        self.avoid_master_reads = {}\n        self.dead = {}\n\n    def add_thing(self, name, thing_dbs, avoid_master=False, **kw):\n        \"\"\"thing_dbs is a list of database engines. the first in the\n        list is assumed to be the master, the rest are slaves.\"\"\"\n        self._things[name] = thing_dbs\n        self.avoid_master_reads[name] = avoid_master\n\n    def add_relation(self, name, type1, type2, relation_dbs,\n                     avoid_master=False, **kw):\n        self._relations[name] = (type1, type2, relation_dbs)\n        self.avoid_master_reads[name] = avoid_master\n\n    def setup_db(self, db_name, g_override=None, **params):\n        engine = get_engine(g_override=g_override, **params)\n        self._engines[db_name] = engine\n\n        if db_name not in (\"email\", \"authorize\", \"hc\", \"traffic\"):\n            # test_engine creates a connection to the database, for some less\n            # important and less used databases we will skip this and only\n            # create the connection if it's needed\n            self.test_engine(engine, g_override)\n\n    def things_iter(self):\n        for name, engines in self._things.iteritems():\n            # ensure we ALWAYS return the actual master as the first,\n            # regardless of if we think it's dead or not.\n            yield name, [engines[0]] + [e for e in engines[1:]\n                                        if e not in self.dead]\n\n    def rels_iter(self):\n        for name, (t1_name, t2_name, engines) in self._relations.iteritems():\n            engines = [engines[0]] + [e for e in engines[1:]\n                                      if e not in self.dead]\n            yield name, (t1_name, t2_name, engines)\n\n    def mark_dead(self, engine, g_override=None):\n        logger.error(\"db_manager: marking connection dead: %r\", engine)\n        self.dead[engine] = time.time()\n\n    def test_engine(self, engine, g_override=None):\n        try:\n            list(engine.execute(\"select 1\"))\n            if engine in self.dead:\n                logger.error(\"db_manager: marking connection alive: %r\",\n                             engine)\n                del self.dead[engine]\n            return True\n        except Exception:\n            logger.error(traceback.format_exc())\n            logger.error(\"connection failure: %r\" % engine)\n            self.mark_dead(engine, g_override)\n            return False\n\n    def get_engine(self, name):\n        return self._engines[name]\n\n    def get_engines(self, names):\n        return [self._engines[name] for name in names if name in self._engines]\n\n    def get_read_table(self, tables):\n        if len(tables) == 1:\n            return tables[0]\n        return  random.choice(list(tables))\n"
  },
  {
    "path": "r2/r2/lib/manager/tp_manager.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\n\nimport hashlib\nimport inspect\n\nfrom mako.exceptions import TemplateLookupException\nfrom mako.template import Template as mTemplate\nfrom pylons import app_globals as g\n\n\nNULL_TEMPLATE = mTemplate(\"\")\nNULL_TEMPLATE.is_null = True\n\nclass tp_manager:\n    def __init__(self, template_cls=mTemplate):\n        self.templates = {}\n        self.Template = template_cls\n        self.cache_override_styles = set()\n\n    def add_handler(self, name, style, handler):\n        key = (name.lower(), style.lower())\n        self.templates[key] = handler\n\n        # a template has been manually specified for this style so record that\n        # we should override g.reload_templates when retrieving templates\n        self.cache_override_styles.add(style.lower())\n\n    def cache_template(self, cls, style, template):\n        use_cache = not g.reload_templates\n        if use_cache:\n            if (not hasattr(template, \"hash\") and\n                    getattr(template, \"filename\", None)):\n                with open(template.filename, 'r') as handle:\n                    template.hash = hashlib.sha1(handle.read()).hexdigest()\n            key = (cls.__name__.lower(), style)\n            self.templates[key] = template\n\n    def get_template(self, cls, style):\n        name = cls.__name__.lower()\n        use_cache = not g.reload_templates\n\n        if use_cache or style.lower() in self.cache_override_styles:\n            key = (name, style)\n            template = self.templates.get(key)\n            if template:\n                return template\n\n        filename = \"/%s.%s\" % (name, style)\n        try:\n            template = g.mako_lookup.get_template(filename)\n        except TemplateLookupException:\n            return\n\n        self.cache_template(cls, style, template)\n\n        return template\n\n    def get(self, thing, style):\n        if not isinstance(thing, type(object)):\n            thing = thing.__class__\n\n        style = style.lower()\n        template = self.get_template(thing, style)\n        if template:\n            return template\n\n        # walk back through base classes to find a template\n        for cls in inspect.getmro(thing)[1:]:\n            template = self.get_template(cls, style)\n            if template:\n                break\n        else:\n            # didn't find a template, use the null template\n            template = NULL_TEMPLATE\n\n        # cache template for thing so we don't need to introspect on subsequent\n        # calls\n        self.cache_template(thing, style, template)\n\n        return template\n\n"
  },
  {
    "path": "r2/r2/lib/media.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport sys\n\nimport base64\nimport cStringIO\nimport hashlib\nimport json\nimport math\nimport os\nimport re\nimport subprocess\nimport tempfile\nimport traceback\nimport urllib\nimport urllib2\nimport urlparse\nimport gzip\n\nimport BeautifulSoup\nfrom PIL import Image, ImageFile\nimport lxml.html\nimport requests\n\nfrom pylons import app_globals as g\n\nfrom r2 import models\nfrom r2.config import feature\nfrom r2.lib import amqp, hooks\nfrom r2.lib.db.tdb_cassandra import NotFound\nfrom r2.lib.memoize import memoize\nfrom r2.lib.nymph import optimize_png\nfrom r2.lib.template_helpers import format_html\nfrom r2.lib.utils import (\n    TimeoutFunction,\n    TimeoutFunctionException,\n    UrlParser,\n    coerce_url_to_protocol,\n    domain,\n    extract_urls_from_markdown,\n    get_requests_resp_json,\n    is_subdomain,\n)\nfrom r2.models.link import Link\nfrom r2.models.media_cache import (\n    ERROR_MEDIA,\n    Media,\n    MediaByURL,\n)\nfrom urllib2 import (\n    HTTPError,\n    URLError,\n)\n\n_IMAGE_PREVIEW_TEMPLATE = \"\"\"\n<img class=\"%(css_class)s\" src=\"%(url)s\" width=\"%(width)s\" height=\"%(height)s\">\n\"\"\"\n\n\ndef _image_to_str(image):\n    s = cStringIO.StringIO()\n    image.save(s, image.format)\n    return s.getvalue()\n\n\ndef str_to_image(s):\n    s = cStringIO.StringIO(s)\n    image = Image.open(s)\n    return image\n\n\ndef _image_entropy(img):\n    \"\"\"calculate the entropy of an image\"\"\"\n    hist = img.histogram()\n    hist_size = sum(hist)\n    hist = [float(h) / hist_size for h in hist]\n\n    return -sum(p * math.log(p, 2) for p in hist if p != 0)\n\n\ndef _crop_image_vertically(img, target_height):\n    \"\"\"crop image vertically the the specified height. determine\n    which pieces to cut off based on the entropy pieces.\"\"\"\n    x,y = img.size\n\n    while y > target_height:\n        #slice 10px at a time until square\n        slice_height = min(y - target_height, 10)\n\n        bottom = img.crop((0, y - slice_height, x, y))\n        top = img.crop((0, 0, x, slice_height))\n\n        #remove the slice with the least entropy\n        if _image_entropy(bottom) < _image_entropy(top):\n            img = img.crop((0, 0, x, y - slice_height))\n        else:\n            img = img.crop((0, slice_height, x, y))\n\n        x,y = img.size\n\n    return img\n\n\ndef _square_image(img):\n    \"\"\"if the image is taller than it is wide, square it off.\"\"\"\n    width = img.size[0]\n    return _crop_image_vertically(img, width)\n\n\ndef _apply_exif_orientation(image):\n    \"\"\"Update the image's orientation if it has the relevant EXIF tag.\"\"\"\n    try:\n        exif_tags = image._getexif() or {}\n    except AttributeError:\n        # image format with no EXIF tags\n        return image\n\n    # constant from EXIF spec\n    ORIENTATION_TAG_ID = 0x0112\n    orientation = exif_tags.get(ORIENTATION_TAG_ID)\n\n    if orientation == 1:\n        # 1 = Horizontal (normal)\n        pass\n    elif orientation == 2:\n        # 2 = Mirror horizontal\n        image = image.transpose(Image.FLIP_LEFT_RIGHT)\n    elif orientation == 3:\n        # 3 = Rotate 180\n        image = image.transpose(Image.ROTATE_180)\n    elif orientation == 4:\n        # 4 = Mirror vertical\n        image = image.transpose(Image.FLIP_TOP_BOTTOM)\n    elif orientation == 5:\n        # 5 = Mirror horizontal and rotate 90 CCW\n        image = image.transpose(Image.FLIP_LEFT_RIGHT)\n        image = image.transpose(Image.ROTATE_90)\n    elif orientation == 6:\n        # 6 = Rotate 270 CCW\n        image = image.transpose(Image.ROTATE_270)\n    elif orientation == 7:\n        # 7 = Mirror horizontal and rotate 270 CCW\n        image = image.transpose(Image.FLIP_LEFT_RIGHT)\n        image = image.transpose(Image.ROTATE_270)\n    elif orientation == 8:\n        # 8 = Rotate 90 CCW\n        image = image.transpose(Image.ROTATE_90)\n\n    return image\n\n\ndef _prepare_image(image):\n    image = _apply_exif_orientation(image)\n\n    image = _square_image(image)\n\n    if feature.is_enabled('hidpi_thumbnails'):\n        hidpi_dims = [int(d * g.thumbnail_hidpi_scaling) for d in g.thumbnail_size]\n\n        # If the image width is smaller than hidpi requires, set to non-hidpi\n        if image.size[0] < hidpi_dims[0]:\n            thumbnail_size = g.thumbnail_size\n        else:\n            thumbnail_size = hidpi_dims\n    else:\n        thumbnail_size = g.thumbnail_size\n\n    image.thumbnail(thumbnail_size, Image.ANTIALIAS)\n    return image\n\n\ndef _clean_url(url):\n    \"\"\"url quotes unicode data out of urls\"\"\"\n    url = url.encode('utf8')\n    url = ''.join(urllib.quote(c) if ord(c) >= 127 else c for c in url)\n    return url\n\n\ndef _initialize_request(url, referer, gzip=False):\n    url = _clean_url(url)\n\n    if not url.startswith((\"http://\", \"https://\")):\n        return\n\n    req = urllib2.Request(url)\n    if gzip:\n        req.add_header('Accept-Encoding', 'gzip')\n    if g.useragent:\n        req.add_header('User-Agent', g.useragent)\n    if referer:\n        req.add_header('Referer', referer)\n    return req\n\n\ndef _fetch_url(url, referer=None):\n    request = _initialize_request(url, referer=referer, gzip=True)\n    if not request:\n        return None, None\n    response = urllib2.urlopen(request)\n    response_data = response.read()\n    content_encoding = response.info().get(\"Content-Encoding\")\n    if content_encoding and content_encoding.lower() in [\"gzip\", \"x-gzip\"]:\n        buf = cStringIO.StringIO(response_data)\n        f = gzip.GzipFile(fileobj=buf)\n        response_data = f.read()\n    return response.headers.get(\"Content-Type\"), response_data\n\n\n@memoize('media.fetch_size', time=3600)\ndef _fetch_image_size(url, referer):\n    \"\"\"Return the size of an image by URL downloading as little as possible.\"\"\"\n\n    request = _initialize_request(url, referer)\n    if not request:\n        return None\n\n    parser = ImageFile.Parser()\n    response = None\n    try:\n        response = urllib2.urlopen(request)\n\n        while True:\n            chunk = response.read(1024)\n            if not chunk:\n                break\n\n            parser.feed(chunk)\n            if parser.image:\n                return parser.image.size\n    except urllib2.URLError:\n        return None\n    finally:\n        if response:\n            response.close()\n\n\ndef optimize_jpeg(filename):\n    with open(os.path.devnull, 'w') as devnull:\n        subprocess.check_call((\"/usr/bin/jpegoptim\", filename), stdout=devnull)\n\n\ndef thumbnail_url(link):\n    \"\"\"Given a link, returns the url for its thumbnail based on its fullname\"\"\"\n    if link.has_thumbnail:\n        if hasattr(link, \"thumbnail_url\"):\n            return link.thumbnail_url\n        else:\n            return ''\n    else:\n        return ''\n\n\ndef _filename_from_content(contents):\n    hash_bytes = hashlib.sha256(contents).digest()\n    return base64.urlsafe_b64encode(hash_bytes).rstrip(\"=\")\n\n\ndef upload_media(image, file_type='.jpg', category='thumbs'):\n    \"\"\"Upload an image to the media provider.\"\"\"\n    f = tempfile.NamedTemporaryFile(suffix=file_type, delete=False)\n    try:\n        img = image\n        do_convert = True\n        if isinstance(img, basestring):\n            img = str_to_image(img)\n            if img.format == \"PNG\" and file_type == \".png\":\n                img.verify()\n                f.write(image)\n                f.close()\n                do_convert = False\n\n        if do_convert:\n            img = img.convert('RGBA')\n            if file_type == \".jpg\":\n                # PIL does not play nice when converting alpha channels to jpg\n                background = Image.new('RGBA', img.size, (255, 255, 255))\n                background.paste(img, img)\n                img = background.convert('RGB')\n                img.save(f, quality=85) # Bug in the JPG encoder with the optimize flag, even if set to false\n            else:\n                img.save(f, optimize=True)\n\n        if file_type == \".png\":\n            optimize_png(f.name)\n        elif file_type == \".jpg\":\n            optimize_jpeg(f.name)\n        contents = open(f.name).read()\n        file_name = _filename_from_content(contents) + file_type\n        return g.media_provider.put(category, file_name, contents)\n    finally:\n        os.unlink(f.name)\n    return \"\"\n\n\ndef upload_stylesheet(content):\n    file_name = _filename_from_content(content) + \".css\"\n    return g.media_provider.put('stylesheets', file_name, content)\n\n\ndef _scrape_media(url, autoplay=False, maxwidth=600, force=False,\n                  save_thumbnail=True, use_cache=False, max_cache_age=None,\n                  use_youtube_scraper=False):\n    media = None\n    autoplay = bool(autoplay)\n    maxwidth = int(maxwidth)\n\n    # Use media from the cache (if available)\n    if not force and use_cache:\n        mediaByURL = MediaByURL.get(url,\n                                    autoplay=autoplay,\n                                    maxwidth=maxwidth,\n                                    max_cache_age=max_cache_age)\n        if mediaByURL:\n            media = mediaByURL.media\n\n    # Otherwise, scrape it if thumbnail is not present\n    if not media or not media.thumbnail_url:\n        media_object = secure_media_object = None\n        thumbnail_image = thumbnail_url = thumbnail_size = None\n\n        scraper = Scraper.for_url(url, autoplay=autoplay,\n                                  use_youtube_scraper=use_youtube_scraper)\n        try:\n            thumbnail_image, preview_object, media_object, secure_media_object = (\n                scraper.scrape())\n        except (HTTPError, URLError) as e:\n            if use_cache:\n                MediaByURL.add_error(url, str(e),\n                                     autoplay=autoplay,\n                                     maxwidth=maxwidth)\n            return None\n\n        # the scraper should be able to make a media embed out of the\n        # media object it just gave us. if not, null out the media object\n        # to protect downstream code\n        if media_object and not scraper.media_embed(media_object):\n            print \"%s made a bad media obj for url %s\" % (scraper, url)\n            media_object = None\n\n        if (secure_media_object and\n            not scraper.media_embed(secure_media_object)):\n            print \"%s made a bad secure media obj for url %s\" % (scraper, url)\n            secure_media_object = None\n\n        # If thumbnail can't be found, attempt again using _ThumbnailOnlyScraper\n        # This should fix bugs that occur when embed.ly caches links before the \n        # thumbnail is available\n        if (not thumbnail_image and \n                not isinstance(scraper, _ThumbnailOnlyScraper)):\n            scraper = _ThumbnailOnlyScraper(url)\n            try:\n                thumbnail_image, preview_object, _, _ = scraper.scrape()\n            except (HTTPError, URLError) as e:\n                use_cache = False\n\n        if thumbnail_image and save_thumbnail:\n            thumbnail_size = thumbnail_image.size\n            thumbnail_url = upload_media(thumbnail_image)\n        else:\n            # don't cache if thumbnail is absent\n            use_cache = False\n\n        media = Media(media_object, secure_media_object, preview_object,\n                      thumbnail_url, thumbnail_size)\n\n    if use_cache and save_thumbnail and media is not ERROR_MEDIA:\n        # Store the media in the cache, possibly extending the ttl\n        MediaByURL.add(url,\n                       media,\n                       autoplay=autoplay,\n                       maxwidth=maxwidth)\n\n    return media\n\n\ndef _get_scrape_url(link):\n    if not link.is_self:\n        sr_name = link.subreddit_slow.name\n        if not feature.is_enabled(\"imgur_gif_conversion\", subreddit=sr_name):\n            return link.url\n        p = UrlParser(link.url)\n        # If it's a gif link on imgur, replacing it with gifv should\n        # give us the embedly friendly video url\n        if is_subdomain(p.hostname, \"imgur.com\"):\n            if p.path_extension().lower() == \"gif\":\n                p.set_extension(\"gifv\")\n                return p.unparse()\n        return link.url\n\n    urls = extract_urls_from_markdown(link.selftext)\n    second_choice = None\n    for url in urls:\n        p = UrlParser(url)\n        if p.is_reddit_url():\n            continue\n        # If we don't find anything we like better, use the first image.\n        if not second_choice:\n            second_choice = url\n        # This is an optimization for \"proof images\" in AMAs.\n        if is_subdomain(p.netloc, 'imgur.com') or p.has_image_extension():\n            return url\n\n    return second_choice\n\n\ndef _set_media(link, force=False, **kwargs):\n    sr = link.subreddit_slow\n    \n    # Do not process thumbnails for quarantined subreddits\n    if sr.quarantine:\n        return\n\n    if not link.is_self:\n        if not force and (link.has_thumbnail or link.media_object):\n            return\n\n    if not force and link.promoted:\n        return\n\n    scrape_url = _get_scrape_url(link)\n\n    if not scrape_url:\n        if link.preview_object:\n            # If the user edited out an image from a self post, we need to make\n            # sure to remove its metadata.\n            link.set_preview_object(None)\n            link._commit()\n        return\n\n    youtube_scraper = feature.is_enabled(\"youtube_scraper\", subreddit=sr.name)\n    media = _scrape_media(scrape_url, force=force,\n                          use_youtube_scraper=youtube_scraper, **kwargs)\n\n    if media and not link.promoted:\n        # While we want to add preview images to self posts for the new apps,\n        # let's not muck about with the old-style thumbnails in case that\n        # breaks assumptions.\n        if not link.is_self:\n            link.thumbnail_url = media.thumbnail_url\n            link.thumbnail_size = media.thumbnail_size\n\n            link.set_media_object(media.media_object)\n            link.set_secure_media_object(media.secure_media_object)\n        link.set_preview_object(media.preview_object)\n\n        link._commit()\n\n        hooks.get_hook(\"scraper.set_media\").call(link=link)\n\n        if media.media_object or media.secure_media_object:\n            amqp.add_item(\"new_media_embed\", link._fullname)\n\n\ndef force_thumbnail(link, image_data, file_type=\".jpg\"):\n    image = str_to_image(image_data)\n    image = _prepare_image(image)\n    thumb_url = upload_media(image, file_type=file_type)\n\n    link.thumbnail_url = thumb_url\n    link.thumbnail_size = image.size\n    link._commit()\n\n\ndef force_mobile_ad_image(link, image_data, file_type=\".jpg\"):\n    image = str_to_image(image_data)\n    image_width = image.size[0]\n    x,y = g.mobile_ad_image_size\n    max_height = image_width * y / x\n    image = _crop_image_vertically(image, max_height)\n    image.thumbnail(g.mobile_ad_image_size, Image.ANTIALIAS)\n    image_url = upload_media(image, file_type=file_type)\n\n    link.mobile_ad_url = image_url\n    link.mobile_ad_size = image.size\n    link._commit()\n\n\ndef upload_icon(image_data, size):\n    image = str_to_image(image_data)\n    image.format = 'PNG'\n    image.thumbnail(size, Image.ANTIALIAS)\n    icon_data = _image_to_str(image)\n    file_name = _filename_from_content(icon_data)\n    return g.media_provider.put('icons', file_name + \".png\", icon_data)\n\n\ndef allowed_media_preview_url(url):\n    p = UrlParser(url)\n    if p.has_static_image_extension():\n        return True\n    for allowed_domain in g.media_preview_domain_whitelist:\n        if is_subdomain(p.hostname, allowed_domain):\n            return True\n    return False\n\n\ndef get_preview_image(preview_object, include_censored=False):\n    \"\"\"Returns a media_object for rendering a media preview image\"\"\"\n    min_width, min_height = g.preview_image_min_size\n    max_width, max_height = g.preview_image_max_size\n    source_width = preview_object['width']\n    source_height = preview_object['height']\n\n    if source_width <= max_width and source_height <= max_height:\n        width = source_width\n        height = source_height\n    else:\n        max_ratio = float(max_height) / max_width\n        source_ratio = float(source_height) / source_width\n        if source_ratio >= max_ratio:\n            height = max_height\n            width = int((height * source_width) / source_height)\n        else:\n            width = max_width\n            height = int((width * source_height) / source_width)\n\n    if width < min_width and height < min_height:\n        return None\n\n    url = g.image_resizing_provider.resize_image(preview_object, width)\n    img_html = format_html(\n        _IMAGE_PREVIEW_TEMPLATE,\n        css_class=\"preview\",\n        url=url,\n        width=width,\n        height=height,\n    )\n\n    if include_censored:\n        censored_url = g.image_resizing_provider.resize_image(\n            preview_object,\n            width,\n            censor_nsfw=True,\n        )\n        censored_img_html = format_html(\n            _IMAGE_PREVIEW_TEMPLATE,\n            css_class=\"censored-preview\",\n            url=censored_url,\n            width=width,\n            height=height,\n        )\n        img_html += censored_img_html\n\n    media_object = {\n        \"type\": \"media-preview\",\n        \"width\": width,\n        \"height\": height,\n        \"content\": img_html,\n    }\n\n    return media_object\n\n\ndef _make_custom_media_embed(media_object):\n    # this is for promoted links with custom media embeds.\n    return MediaEmbed(\n        height=media_object.get(\"height\"),\n        width=media_object.get(\"width\"),\n        content=media_object.get(\"content\"),\n    )\n\n\ndef get_media_embed(media_object):\n    if not isinstance(media_object, dict):\n        return\n\n    embed_hook = hooks.get_hook(\"scraper.media_embed\")\n    media_embed = embed_hook.call_until_return(media_object=media_object)\n    if media_embed:\n        return media_embed\n\n    if media_object.get(\"type\") == \"custom\":\n        return _make_custom_media_embed(media_object)\n\n    if \"oembed\" in media_object:\n        if media_object.get(\"type\") == \"youtube.com\":\n            return _YouTubeScraper.media_embed(media_object)\n\n        return _EmbedlyScraper.media_embed(media_object)\n\n\nclass MediaEmbed(object):\n    \"\"\"A MediaEmbed holds data relevant for serving media for an object.\"\"\"\n\n    width = None\n    height = None\n    content = None\n    scrolling = False\n\n    def __init__(self, height, width, content, scrolling=False,\n                 public_thumbnail_url=None, sandbox=True):\n        \"\"\"Build a MediaEmbed.\n\n        :param height int - The height of the media embed, in pixels\n        :param width int - The width of the media embed, in pixels\n        :param content string - The content of the media embed - HTML.\n        :param scrolling bool - Whether the media embed should scroll or not.\n        :param public_thumbnail_url string - The URL of the most representative\n            thumbnail for this media. This may be on an uncontrolled domain,\n            and is not necessarily our own thumbs domain (and should not be\n            served to browsers).\n        :param sandbox bool - True if the content should be sandboxed\n            in an iframe on the media domain.\n        \"\"\"\n\n        self.height = int(height)\n        self.width = int(width)\n        self.content = content\n        self.scrolling = scrolling\n        self.public_thumbnail_url = public_thumbnail_url\n        self.sandbox = sandbox\n\n\nclass Scraper(object):\n    @classmethod\n    def for_url(cls, url, autoplay=False, maxwidth=600, use_youtube_scraper=False):\n        scraper = hooks.get_hook(\"scraper.factory\").call_until_return(url=url)\n        if scraper:\n            return scraper\n\n        if use_youtube_scraper and _YouTubeScraper.matches(url):\n            return _YouTubeScraper(url, maxwidth=maxwidth)\n\n        embedly_services = _fetch_embedly_services()\n        for service_re in embedly_services:\n            if service_re.match(url):\n                return _EmbedlyScraper(url,\n                                       autoplay=autoplay,\n                                       maxwidth=maxwidth)\n\n        return _ThumbnailOnlyScraper(url)\n\n    def scrape(self):\n        # should return a 4-tuple of:\n        #     thumbnail, preview_object, media_object, secure_media_obj\n        raise NotImplementedError\n\n    @classmethod\n    def media_embed(cls, media_object):\n        # should take a media object and return an appropriate MediaEmbed\n        raise NotImplementedError\n\n\nclass _ThumbnailOnlyScraper(Scraper):\n    def __init__(self, url):\n        self.url = url\n        # Having the source document's protocol on hand makes it easier to deal\n        # with protocol-relative urls we extract from it.\n        self.protocol = UrlParser(url).scheme\n\n    def scrape(self):\n        thumbnail_url, image_data = self._find_thumbnail_image()\n        if not thumbnail_url:\n            return None, None, None, None\n\n        # When isolated from the context of a webpage, protocol-relative URLs\n        # are ambiguous, so let's absolutify them now.\n        if thumbnail_url.startswith('//'):\n            thumbnail_url = coerce_url_to_protocol(thumbnail_url, self.protocol)\n\n        if not image_data:\n            _, image_data = _fetch_url(thumbnail_url, referer=self.url)\n\n        if not image_data:\n            return None, None, None, None\n\n        uid = _filename_from_content(image_data)\n        image = str_to_image(image_data)\n        storage_url = upload_media(image, category='previews')\n        width, height = image.size\n        preview_object = {\n            'uid': uid,\n            'url': storage_url,\n            'width': width,\n            'height': height,\n        }\n\n        thumbnail = _prepare_image(image)\n\n        return thumbnail, preview_object, None, None\n\n    def _extract_image_urls(self, soup):\n        for img in soup.findAll(\"img\", src=True):\n            yield urlparse.urljoin(self.url, img[\"src\"])\n\n    def _find_thumbnail_image(self):\n        \"\"\"Find what we think is the best thumbnail image for a link.\n\n        Returns a 2-tuple of image url and, as an optimization, the raw image\n        data.  A value of None for the former means we couldn't find an image;\n        None for the latter just means we haven't already fetched the image.\n        \"\"\"\n        content_type, content = _fetch_url(self.url)\n\n        # if it's an image, it's pretty easy to guess what we should thumbnail.\n        if content_type and \"image\" in content_type and content:\n            return self.url, content\n\n        if content_type and \"html\" in content_type and content:\n            soup = BeautifulSoup.BeautifulSoup(content)\n        else:\n            return None, None\n\n        # Allow the content author to specify the thumbnail using the Open\n        # Graph protocol: http://ogp.me/\n        og_image = (soup.find('meta', property='og:image') or\n                    soup.find('meta', attrs={'name': 'og:image'}))\n        if og_image and og_image.get('content'):\n            return og_image['content'], None\n        og_image = (soup.find('meta', property='og:image:url') or\n                    soup.find('meta', attrs={'name': 'og:image:url'}))\n        if og_image and og_image.get('content'):\n            return og_image['content'], None\n\n        # <link rel=\"image_src\" href=\"http://...\">\n        thumbnail_spec = soup.find('link', rel='image_src')\n        if thumbnail_spec and thumbnail_spec['href']:\n            return thumbnail_spec['href'], None\n\n        # ok, we have no guidance from the author. look for the largest\n        # image on the page with a few caveats. (see below)\n        max_area = 0\n        max_url = None\n        for image_url in self._extract_image_urls(soup):\n            # When isolated from the context of a webpage, protocol-relative\n            # URLs are ambiguous, so let's absolutify them now.\n            if image_url.startswith('//'):\n                image_url = coerce_url_to_protocol(image_url, self.protocol)\n            size = _fetch_image_size(image_url, referer=self.url)\n            if not size:\n                continue\n\n            area = size[0] * size[1]\n\n            # ignore little images\n            if area < 5000:\n                g.log.debug('ignore little %s' % image_url)\n                continue\n\n            # ignore excessively long/wide images\n            if max(size) / min(size) > 1.5:\n                g.log.debug('ignore dimensions %s' % image_url)\n                continue\n\n            # penalize images with \"sprite\" in their name\n            if 'sprite' in image_url.lower():\n                g.log.debug('penalizing sprite %s' % image_url)\n                area /= 10\n\n            if area > max_area:\n                max_area = area\n                max_url = image_url\n\n        return max_url, None\n\n\nclass _EmbedlyScraper(Scraper):\n    \"\"\"Use Embedly to get information about embed info for a url.\n\n    http://embed.ly/docs/api/embed/endpoints/1/oembed\n    \"\"\"\n    EMBEDLY_API_URL = \"https://api.embed.ly/1/oembed\"\n\n    def __init__(self, url, autoplay=False, maxwidth=600):\n        self.url = url\n        self.maxwidth = int(maxwidth)\n        self.embedly_params = {}\n\n        if autoplay:\n            self.embedly_params[\"autoplay\"] = \"true\"\n\n    def _fetch_from_embedly(self, secure):\n        param_dict = {\n            \"url\": self.url,\n            \"format\": \"json\",\n            \"maxwidth\": self.maxwidth,\n            \"key\": g.embedly_api_key,\n            \"secure\": \"true\" if secure else \"false\",\n        }\n\n        param_dict.update(self.embedly_params)\n        params = urllib.urlencode(param_dict)\n\n        timer = g.stats.get_timer(\"providers.embedly.oembed\")\n        timer.start()\n        content = requests.get(self.EMBEDLY_API_URL + \"?\" + params).content\n        timer.stop()\n\n        return json.loads(content)\n\n    def _make_media_object(self, oembed):\n        if oembed.get(\"type\") in (\"video\", \"rich\"):\n            return {\n                \"type\": domain(self.url),\n                \"oembed\": oembed,\n            }\n        return None\n\n    def scrape(self):\n        oembed = self._fetch_from_embedly(secure=False)\n        if not oembed:\n            return None, None, None, None\n\n        if oembed.get(\"type\") == \"photo\":\n            thumbnail_url = oembed.get(\"url\")\n        else:\n            thumbnail_url = oembed.get(\"thumbnail_url\")\n        if not thumbnail_url:\n            return None, None, None, None\n\n        content_type, content = _fetch_url(thumbnail_url, referer=self.url)\n        uid = _filename_from_content(content)\n        image = str_to_image(content)\n        storage_url = upload_media(image, category='previews')\n        width, height = image.size\n        preview_object = {\n            'uid': uid,\n            'url': storage_url,\n            'width': width,\n            'height': height,\n        }\n\n        thumbnail = _prepare_image(image)\n\n        secure_oembed = self._fetch_from_embedly(secure=True)\n        if not self.validate_secure_oembed(secure_oembed):\n            secure_oembed = {}\n\n        return (\n            thumbnail,\n            preview_object,\n            self._make_media_object(oembed),\n            self._make_media_object(secure_oembed),\n        )\n\n    def validate_secure_oembed(self, oembed):\n        \"\"\"Check the \"secure\" embed is safe to embed, and not a placeholder\"\"\"\n        if not oembed.get(\"html\"):\n            return False\n\n        # Get the embed.ly iframe's src\n        iframe_src = lxml.html.fromstring(oembed['html']).get('src')\n        if not iframe_src:\n            return False\n        iframe_src_url = UrlParser(iframe_src)\n\n        # Per embed.ly support: If the URL for the provider is HTTP, we're\n        # gonna get a placeholder image instead\n        provider_src_url = UrlParser(iframe_src_url.query_dict.get('src'))\n        return not provider_src_url.scheme or provider_src_url.scheme == \"https\"\n\n    @classmethod\n    def media_embed(cls, media_object):\n        oembed = media_object[\"oembed\"]\n\n        html = oembed.get(\"html\")\n        width = oembed.get(\"width\")\n        height = oembed.get(\"height\")\n        public_thumbnail_url = oembed.get('thumbnail_url')\n        if not (html and width and height):\n            return\n\n        return MediaEmbed(\n            width=width,\n            height=height,\n            content=html,\n            public_thumbnail_url=public_thumbnail_url,\n        )\n\n\nclass _YouTubeScraper(Scraper):\n    OEMBED_ENDPOINT = \"https://www.youtube.com/oembed\"\n    URL_MATCH = re.compile(r\"https?://((www\\.)?youtube\\.com/watch|youtu\\.be/)\")\n\n    def __init__(self, url, maxwidth):\n        self.url = url\n        self.maxwidth = maxwidth\n\n    @classmethod\n    def matches(cls, url):\n        return cls.URL_MATCH.match(url)\n\n    def _fetch_from_youtube(self):\n        params = {\n            \"url\": self.url,\n            \"format\": \"json\",\n            \"maxwidth\": self.maxwidth,\n        }\n\n        with g.stats.get_timer(\"providers.youtube.oembed\"):\n            content = requests.get(self.OEMBED_ENDPOINT, params=params).content\n\n        return json.loads(content)\n\n    def _make_media_object(self, oembed):\n        if oembed.get(\"type\") == \"video\":\n            return {\n                \"type\": \"youtube.com\",\n                \"oembed\": oembed,\n            }\n        return None\n\n    def scrape(self):\n        oembed = self._fetch_from_youtube()\n        if not oembed:\n            return None, None, None, None\n        thumbnail_url = oembed.get(\"thumbnail_url\")\n\n        if not thumbnail_url:\n            return None, None, None, None\n\n        _, content = _fetch_url(thumbnail_url, referer=self.url)\n        uid = _filename_from_content(content)\n        image = str_to_image(content)\n        storage_url = upload_media(image, category='previews')\n        width, height = image.size\n        preview_object = {\n            'uid': uid,\n            'url': storage_url,\n            'width': width,\n            'height': height,\n        }\n\n        thumbnail = _prepare_image(image)\n        media_object = self._make_media_object(oembed)\n\n        return (\n            thumbnail,\n            preview_object,\n            media_object,\n            media_object,\n        )\n\n    @classmethod\n    def media_embed(cls, media_object):\n        oembed = media_object[\"oembed\"]\n\n        html = oembed.get(\"html\")\n        width = oembed.get(\"width\")\n        height = oembed.get(\"height\")\n        public_thumbnail_url = oembed.get('thumbnail_url')\n\n        if not (html and width and height):\n            return\n\n        return MediaEmbed(\n            width=width,\n            height=height,\n            content=html,\n            public_thumbnail_url=public_thumbnail_url,\n        )\n\n\n@memoize(\"media.embedly_services2\", time=3600)\ndef _fetch_embedly_service_data():\n    resp = requests.get(\"https://api.embed.ly/1/services/python\")\n    return get_requests_resp_json(resp)\n\n\ndef _fetch_embedly_services():\n    if not g.embedly_api_key:\n        if g.debug:\n            g.log.info(\"No embedly_api_key, using no key while in debug mode.\")\n        else:\n            g.log.warning(\"No embedly_api_key configured. Will not use \"\n                          \"embed.ly.\")\n            return []\n\n    service_data = _fetch_embedly_service_data()\n\n    return [\n        re.compile(\"(?:%s)\" % \"|\".join(service[\"regex\"]))\n        for service in service_data\n    ]\n\n\ndef run():\n    @g.stats.amqp_processor('scraper_q')\n    def process_link(msg):\n        fname = msg.body\n        link = Link._by_fullname(fname, data=True)\n\n        try:\n            TimeoutFunction(_set_media, 30)(link, use_cache=True)\n        except TimeoutFunctionException:\n            print \"Timed out on %s\" % fname\n        except KeyboardInterrupt:\n            raise\n        except:\n            print \"Error fetching %s\" % fname\n            print traceback.format_exc()\n\n    amqp.consume_items('scraper_q', process_link)\n"
  },
  {
    "path": "r2/r2/lib/memoize.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom hashlib import md5\n\nfrom r2.lib.filters import _force_utf8\nfrom r2.lib.cache import NoneResult, make_key_id\nfrom r2.lib.lock import make_lock_factory\nfrom pylons import app_globals as g\n\n\ndef memoize(iden, time = 0, stale=False, timeout=30):\n    def memoize_fn(fn):\n        from r2.lib.memoize import NoneResult\n        def new_fn(*a, **kw):\n\n            #if the keyword param _update == True, the cache will be\n            #overwritten no matter what\n            update = kw.pop('_update', False)\n\n            key = \"memo:%s:%s\" % (iden, make_key_id(*a, **kw))\n\n            res = None if update else g.memoizecache.get(key, stale=stale)\n\n            if res is None:\n                # not cached, we should calculate it.\n                with g.make_lock(\"memoize\", 'memoize_lock(%s)' % key,\n                                 time=timeout, timeout=timeout):\n\n                    # see if it was completed while we were waiting\n                    # for the lock\n                    stored = None if update else g.memoizecache.get(key)\n                    if stored is not None:\n                        # it was calculated while we were waiting\n                        res = stored\n                    else:\n                        # okay now go and actually calculate it\n                        res = fn(*a, **kw)\n                        if res is None:\n                            res = NoneResult\n                        g.memoizecache.set(key, res, time=time)\n\n            if res == NoneResult:\n                res = None\n\n            return res\n\n        new_fn.memoized_fn = fn\n        return new_fn\n    return memoize_fn\n\n@memoize('test')\ndef test(x, y):\n    import time\n    time.sleep(1)\n    print 'calculating %d + %d' % (x, y)\n    if x + y == 10:\n        return None\n    else:\n        return x + y\n"
  },
  {
    "path": "r2/r2/lib/menus.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom pylons import request\nfrom pylons import tmpl_context as c\nfrom pylons import app_globals as g\nfrom pylons.i18n import _, N_\n\nfrom r2.config import feature\nfrom r2.lib.db import operators\nfrom r2.lib.filters import _force_unicode\nfrom r2.lib.strings import StringHandler, plurals\nfrom r2.lib.utils import  class_property, query_string, timeago\nfrom r2.lib.wrapped import Styled\n\n\nclass MenuHandler(StringHandler):\n    \"\"\"Bastard child of StringHandler and plurals.  Menus are\n    typically a single word (and in some cases, a single plural word\n    like 'moderators' or 'contributors' so this class first checks its\n    own dictionary of string translations before falling back on the\n    plurals list.\"\"\"\n    def __getattr__(self, attr):\n        try:\n            return StringHandler.__getattr__(self, attr)\n        except KeyError:\n            return getattr(plurals, attr)\n\n# translation strings for every menu on the site\nmenu =   MenuHandler(hot          = _('hot'),\n                     new          = _('new'),\n                     old          = _('old'),\n                     ups          = _('ups'),\n                     downs        = _('downs'),\n                     top          = _('top'),\n                     more         = _('more'),\n                     relevance    = _('relevance'),\n                     controversial  = _('controversial'),\n                     gilded       = _('gilded'),\n                     confidence   = _('best'),\n                     random       = _('random'),\n                     qa           = _('q&a'),\n                     saved        = _('saved {toolbar}'),\n                     recommended  = _('recommended'),\n                     rising       = _('rising'),\n                     admin        = _('admin'),\n\n                     # time sort words\n                     hour         = _('past hour'),\n                     day          = _('past 24 hours'),\n                     week         = _('past week'),\n                     month        = _('past month'),\n                     year         = _('past year'),\n                     all          = _('all time'),\n\n                     # \"kind\" words\n                     spam         = _(\"spam\"),\n                     autobanned   = _(\"autobanned\"),\n\n                     # reddit header strings\n                     prefs        = _(\"preferences\"),\n                     submit       = _(\"submit\"),\n                     wiki         = _(\"wiki\"),\n                     blog         = _(\"blog\"),\n                     logout       = _(\"logout\"),\n\n                     #reddit footer strings\n                     reddiquette  = _(\"reddiquette\"),\n                     contact      = _(\"contact us\"),\n                     buttons      = _(\"buttons\"),\n                     widget       = _(\"widget\"),\n                     mobile       = _(\"mobile\"),\n                     advertising  = _(\"advertise\"),\n                     gold         = _('reddit gold'),\n                     reddits      = _('subreddits'),\n                     rules        = _('site rules'),\n                     jobs         = _('jobs'),\n                     transparency = _(\"transparency\"),\n                     source_code  = _(\"source code\"),\n                     values       = _(\"values\"),\n\n                     #preferences\n                     options      = _('options'),\n                     apps         = _(\"apps\"),\n                     feeds        = _(\"RSS feeds\"),\n                     friends      = _(\"friends\"),\n                     blocked      = _(\"blocked\"),\n                     update       = _(\"password/email\"),\n                     deactivate   = _(\"deactivate\"),\n                     security     = _(\"security\"),\n\n                     # messages\n                     compose      = _(\"send a private message\"),\n                     inbox        = _(\"inbox\"),\n                     sent         = _(\"sent\"),\n\n                     # comments\n                     comments     = _(\"comments {toolbar}\"),\n                     details      = _(\"details\"),\n                     duplicates   = _(\"other discussions (%(num)s)\"),\n                     traffic      = _(\"traffic stats\"),\n                     stylesheet   = _(\"stylesheet\"),\n\n                     # reddits\n                     home         = _(\"home\"),\n                     about        = _(\"about\"),\n                     edit_subscriptions = _(\"edit subscriptions\"),\n                     community_settings = _(\"subreddit settings\"),\n                     edit_stylesheet    = _(\"edit stylesheet\"),\n                     community_rules    = _(\"rules\"),\n                     moderators   = _(\"moderators\"),\n                     modmail      = _(\"moderator mail\"),\n                     contributors = _(\"approved submitters\"),\n                     banned       = _(\"ban users\"),\n                     banusers     = _(\"ban users\"),\n                     muted        = _(\"mute users\"),\n                     flair        = _(\"edit flair\"),\n                     log          = _(\"moderation log\"),\n                     modqueue     = _(\"moderation queue\"),\n                     unmoderated  = _(\"unmoderated posts\"),\n                     edited       = _(\"edited\"),\n                     employee     = _(\"employee\"),\n                     automod      = _(\"automoderator config\"),\n                     new_automod  = _(\"get started with automoderator\"),\n\n                     wikibanned        = _(\"ban wiki contributors\"),\n                     wikicontributors  = _(\"add wiki contributors\"),\n\n                     wikirecentrevisions = _(\"recent wiki revisions\"),\n                     wikipageslist = _(\"wiki page list\"),\n\n                     popular      = _(\"popular\"),\n                     create       = _(\"create\"),\n                     mine         = _(\"my subreddits\"),\n                     quarantine   = _(\"quarantine\"),\n                     featured     = _(\"featured\"),\n\n                     i18n         = _(\"help translate\"),\n                     errors       = _(\"errors\"),\n                     awards       = _(\"awards\"),\n                     ads          = _(\"ads\"),\n                     promoted     = _(\"promoted\"),\n                     sponsor      = _(\"sponsor\"),\n                     reporters    = _(\"reporters\"),\n                     reports      = _(\"reports\"),\n                     reportedauth = _(\"reported authors\"),\n                     info         = _(\"info\"),\n                     share        = _(\"share\"),\n\n                     overview     = _(\"overview\"),\n                     submitted    = _(\"submitted\"),\n                     upvoted      = _(\"upvoted\"),\n                     downvoted    = _(\"downvoted\"),\n                     hidden       = _(\"hidden {toolbar}\"),\n                     deleted      = _(\"deleted\"),\n                     reported     = _(\"reported\"),\n                     voting       = _(\"voting\"),\n\n                     promote        = _('advertising'),\n                     new_promo      = _('create promotion'),\n                     my_current_promos = _('my promoted links'),\n                     current_promos = _('all promoted links'),\n                     all_promos     = _('all'),\n                     future_promos  = _('unseen'),\n                     unapproved_campaigns = _('unapproved campaigns'),\n                     inventory      = _('inventory'),\n                     live_promos    = _('live'),\n                     unpaid_promos  = _('unpaid'),\n                     pending_promos = _('pending'),\n                     rejected_promos = _('rejected'),\n                     edited_live_promos = _('edited live'),\n\n                     sitewide = _('sitewide'),\n                     languages = _('languages'),\n                     adverts = _('adverts'),\n\n                     whitelist = _(\"whitelist\")\n                     )\n\ndef menu_style(type):\n    \"\"\"Simple manager function for the styled menus.  Returns a\n    (style, css_class) pair given a 'type', defaulting to style =\n    'dropdown' with no css_class.\"\"\"\n    default = ('dropdown', '')\n    d = dict(lightdrop = ('dropdown', 'lightdrop'),\n             tabdrop = ('dropdown', 'tabdrop'),\n             srdrop = ('dropdown', 'srdrop'),\n             flatlist =  ('flatlist', 'flat-list'),\n             tabmenu = ('tabmenu', ''),\n             formtab = ('tabmenu', 'formtab'),\n             flat_vert = ('flatlist', 'flat-vert'),\n             )\n    return d.get(type, default)\n\n\nclass NavMenu(Styled):\n    \"\"\"generates a navigation menu.  The intention here is that the\n    'style' parameter sets what template/layout to use to differentiate, say,\n    a dropdown from a flatlist, while the optional _class, and _id attributes\n    can be used to set individualized CSS.\"\"\"\n\n    def __init__(self, options, default=None, title='', type=\"dropdown\",\n                 base_path='', separator='|', _id='', css_class=''):\n        self.options = options\n        self.default = default\n        self.title = title\n        self.base_path = base_path\n        self.separator = separator\n\n        # add the menu style, but preserve existing css_class parameter\n        style, base_css_class = menu_style(type)\n        css_class = base_css_class + ((' ' + css_class) if css_class else '')\n\n        # since the menu contains the path info, it's buttons need a\n        # configuration pass to get them pointing to the proper urls\n        for opt in self.options:\n            opt.build(self.base_path)\n\n            # add \"choice\" css class to each button\n            if opt.css_class:\n                opt.css_class += \" choice\"\n            else:\n                opt.css_class = \"choice\"\n\n        self.selected = self.find_selected()\n\n        Styled.__init__(self, style, _id=_id, css_class=css_class)\n\n    def find_selected(self):\n        maybe_selected = [o for o in self.options if o.is_selected()]\n        if maybe_selected:\n            # pick the button with the most restrictive pathing\n            maybe_selected.sort(lambda x, y:\n                                len(y.bare_path) - len(x.bare_path))\n            return maybe_selected[0]\n        elif self.default:\n            #lookup the menu with the 'dest' that matches 'default'\n            for opt in self.options:\n                if opt.dest == self.default:\n                    return opt\n\n    def __iter__(self):\n        for opt in self.options:\n            yield opt\n\n    def cachable_attrs(self):\n        return [\n            ('options', self.options),\n            ('title', self.title),\n            ('selected', self.selected),\n            ('separator', self.separator),\n        ]\n\n\nclass NavButton(Styled):\n    \"\"\"Smallest unit of site navigation.  A button once constructed\n    must also have its build() method called with the current path to\n    set self.path.  This step is done automatically if the button is\n    passed to a NavMenu instance upon its construction.\"\"\"\n\n    _style = \"plain\"\n\n    def __init__(self, title, dest, sr_path=True, aliases=None,\n                 target=\"\", use_params=False, css_class='', data=None):\n        aliases = aliases or []\n        aliases = set(_force_unicode(a.rstrip('/')) for a in aliases)\n        if dest:\n            aliases.add(_force_unicode(dest.rstrip('/')))\n\n        self.title = title\n        self.dest = dest\n        self.selected = False\n\n        self.sr_path = sr_path\n        self.aliases = aliases\n        self.target = target\n        self.use_params = use_params\n        self.data = data\n\n        Styled.__init__(self, self._style, css_class=css_class)\n\n    def build(self, base_path=''):\n        base_path = (\"%s/%s/\" % (base_path, self.dest)).replace('//', '/')\n        self.bare_path = _force_unicode(base_path.replace('//', '/')).lower()\n        self.bare_path = self.bare_path.rstrip('/')\n        self.base_path = base_path\n\n        if self.use_params:\n            base_path += query_string(dict(request.GET))\n\n        # since we've been sloppy of keeping track of \"//\", get rid\n        # of any that may be present\n        self.path = base_path.replace('//', '/')\n\n    def is_selected(self):\n        stripped_path = _force_unicode(request.path.rstrip('/').lower())\n\n        if not (self.sr_path or c.default_sr):\n            return False\n        if stripped_path == self.bare_path:\n            return True\n        site_path = c.site.user_path.lower() + self.bare_path\n        if self.sr_path and stripped_path == site_path:\n            return True\n        if self.bare_path and stripped_path.startswith(self.bare_path):\n            return True\n        if stripped_path in self.aliases:\n            return True\n\n    def selected_title(self):\n        \"\"\"returns the title of the button when selected (for cases\n        when it is different from self.title)\"\"\"\n        return self.title\n\n    def cachable_attrs(self):\n        return [\n            ('selected', self.selected),\n            ('title', self.title),\n            ('path', self.path),\n            ('sr_path', self.sr_path),\n            ('target', self.target),\n            ('css_class', self.css_class),\n            ('_id', self._id),\n            ('data', self.data),\n        ]\n\n\nclass QueryButton(NavButton):\n    def __init__(self, title, dest, query_param, sr_path=True, aliases=None,\n                 target=\"\", css_class='', data=None):\n        self.query_param = query_param\n        NavButton.__init__(self, title, dest, sr_path=sr_path,\n                           aliases=aliases, target=target, use_params=False,\n                           css_class=css_class, data=data)\n\n    def build(self, base_path=''):\n        params = dict(request.GET)\n        if self.dest:\n            params[self.query_param] = self.dest\n        elif self.query_param in params:\n            del params[self.query_param]\n\n        self.base_path = base_path\n        base_path += query_string(params)\n        self.path = base_path.replace('//', '/')\n\n    def is_selected(self):\n        if not self.dest and self.query_param not in dict(request.GET):\n            return True\n        return dict(request.GET).get(self.query_param, '') in self.aliases\n\n\nclass PostButton(NavButton):\n    _style = \"post\"\n\n    def __init__(self, title, dest, input_name, sr_path=True, aliases=None,\n                 target=\"\", css_class='', data=None):\n        self.input_name = input_name\n        NavButton.__init__(self, title, dest, sr_path=sr_path,\n                           aliases=aliases, target=target, use_params=False,\n                           css_class=css_class, data=data)\n\n    def build(self, base_path=''):\n        self.base_path = base_path\n        self.action_params = {self.input_name: self.dest}\n\n    def cachable_attrs(self):\n        return [\n            ('selected', self.selected),\n            ('title', self.title),\n            ('base_path', self.base_path),\n            ('action_params', self.action_params),\n            ('sr_path', self.sr_path),\n            ('target', self.target),\n            ('css_class', self.css_class),\n            ('_id', self._id),\n            ('data', self.data),\n        ]\n\n    def is_selected(self):\n        return False\n\n\nclass ModeratorMailButton(NavButton):\n    def is_selected(self):\n        if c.default_sr and not self.sr_path:\n            return NavButton.is_selected(self)\n        elif not c.default_sr and self.sr_path:\n            return NavButton.is_selected(self)\n\n\nclass OffsiteButton(NavButton):\n    def build(self, base_path=''):\n        self.sr_path = False\n        self.path = self.bare_path = self.dest\n\n    def cachable_attrs(self):\n        return [\n            ('path', self.path),\n            ('title', self.title),\n            ('css_class', self.css_class),\n        ]\n\n\nclass SubredditButton(NavButton):\n    from r2.models.subreddit import Frontpage, Mod, All, Random, RandomSubscription\n    # TRANSLATORS: This refers to /r/mod\n    name_overrides = {Mod: N_(\"mod\"),\n    # TRANSLATORS: This refers to the user's front page\n                      Frontpage: N_(\"front\"),\n                      All: N_(\"all\"),\n                      Random: N_(\"random\"),\n    # TRANSLATORS: Gold feature, \"myrandom\", a random subreddit from your subscriptions\n                      RandomSubscription: N_(\"myrandom\")}\n\n    def __init__(self, sr, css_class='', data=None):\n        self.path = sr.path\n        name = self.name_overrides.get(sr)\n        name = _(name) if name else sr.name\n        self.isselected = (c.site == sr)\n        NavButton.__init__(self, name, sr.path, sr_path=False,\n                           css_class=css_class, data=data)\n\n    def build(self, base_path=''):\n        self.bare_path = \"\"\n\n    def is_selected(self):\n        return self.isselected\n\n    def cachable_attrs(self):\n        return [\n            ('path', self.path),\n            ('title', self.title),\n            ('isselected', self.isselected),\n            ('css_class', self.css_class),\n            ('data', self.data),\n        ]\n\n\nclass NamedButton(NavButton):\n    \"\"\"Convenience class for handling the majority of NavButtons\n    whereby the 'title' is just the translation of 'name' and the\n    'dest' defaults to the 'name' as well (unless specified\n    separately).\"\"\"\n\n    def __init__(self, name, sr_path=True, aliases=None,\n                 dest=None, fmt_args={}, use_params=False, css_class='',\n                 data=None):\n        self.name = name.strip('/')\n        menutext = menu[self.name] % fmt_args\n        dest = dest if dest is not None else name\n        NavButton.__init__(self, menutext, dest, sr_path=sr_path,\n                           aliases=aliases,\n                           use_params=use_params, css_class=css_class,\n                           data=data)\n\n\nclass JsButton(NavButton):\n    \"\"\"A button which fires a JS event and thus has no path and cannot\n    be in the 'selected' state\"\"\"\n\n    _style = \"js\"\n\n    def __init__(self, title, tab_name=None, onclick='', css_class='',\n                 data=None):\n        self.tab_name = tab_name\n        self.onclick = onclick\n        dest = '#'\n        NavButton.__init__(self, title, dest, sr_path=False,\n                           css_class=css_class, data=data)\n\n    def build(self, base_path=''):\n        if self.tab_name:\n            self.path = '#' + self.tab_name\n        else:\n            self.path = 'javascript:void(0)'\n\n    def is_selected(self):\n        return False\n\n    def cachable_attrs(self):\n        return [\n            ('title', self.title),\n            ('path', self.path),\n            ('target', self.target),\n            ('css_class', self.css_class),\n            ('_id', self._id),\n            ('tab_name', self.tab_name),\n            ('onclick', self.onclick),\n            ('data', self.data),\n        ]\n\n\nclass PageNameNav(Styled):\n    \"\"\"generates the links and/or labels which live in the header\n    between the header image and the first nav menu (e.g., the\n    subreddit name, the page name, etc.)\"\"\"\n    pass\n\n\nclass SortMenu(NavMenu):\n    name = 'sort'\n    hidden_options = []\n    button_cls = QueryButton\n\n    # these are _ prefixed to avoid colliding with NavMenu attributes\n    _default = 'hot'\n    _options = ('hot', 'new', 'top', 'old', 'controversial')\n    _type = 'lightdrop'\n    _title = N_(\"sorted by\")\n\n    def __init__(self, default=None, title='', base_path='', separator='|',\n                 _id='', css_class=''):\n        options = self.make_buttons()\n        default = default or self._default\n        base_path = base_path or request.path\n        title = title or _(self._title)\n        NavMenu.__init__(self, options, default=default, title=title,\n                         type=self._type, base_path=base_path,\n                         separator=separator, _id=_id, css_class=css_class)\n\n    def make_buttons(self):\n        buttons = []\n        for name in self._options:\n            css_class = 'hidden' if name in self.hidden_options else ''\n            button = self.button_cls(self.make_title(name), name, self.name,\n                                     css_class=css_class)\n            buttons.append(button)\n        return buttons\n\n    def make_title(self, attr):\n        return menu[attr]\n\n    _mapping = {\n        \"hot\": operators.desc('_hot'),\n        \"new\": operators.desc('_date'),\n        \"old\": operators.asc('_date'),\n        \"top\": operators.desc('_score'),\n        \"controversial\": operators.desc('_controversy'),\n        \"confidence\": operators.desc('_confidence'),\n        \"random\": operators.shuffled('_confidence'),\n        \"qa\": operators.desc('_qa'),\n    }\n    _reverse_mapping = {v: k for k, v in _mapping.iteritems()}\n\n    @classmethod\n    def operator(cls, sort):\n        return cls._mapping.get(sort)\n\n    @classmethod\n    def sort(cls, operator):\n        return cls._reverse_mapping.get(operator)\n\n\nclass ProfileSortMenu(SortMenu):\n    _default = 'new'\n    _options = ('hot', 'new', 'top', 'controversial')\n\n\nclass CommentSortMenu(SortMenu):\n    \"\"\"Sort menu for comments pages\"\"\"\n    _default = 'confidence'\n    _options = ('confidence', 'top', 'new', 'controversial', 'old', 'random',\n                'qa',)\n    hidden_options = ['random']\n\n    # Links may have a suggested sort of 'blank', which is an explicit None -\n    # that is, do not check the subreddit for a suggested sort, either.\n    suggested_sort_options = _options + ('blank',)\n\n    def __init__(self, *args, **kwargs):\n        self.suggested_sort = kwargs.pop('suggested_sort', None)\n        super(CommentSortMenu, self).__init__(*args, **kwargs)\n\n    @classmethod\n    def visible_options(cls):\n        return set(cls._options) - set(cls.hidden_options)\n\n    def make_title(self, attr):\n        title = super(CommentSortMenu, self).make_title(attr)\n        if attr == self.suggested_sort:\n            return title + ' ' + _('(suggested)')\n        else:\n            return title\n\n\nclass SearchSortMenu(SortMenu):\n    \"\"\"Sort menu for search pages.\"\"\"\n    _default = 'relevance'\n    _options = ('relevance', 'hot', 'top', 'new', 'comments')\n\n    @class_property\n    def hidden_options(cls):\n        return ['hot']\n\n    def make_buttons(self):\n        buttons = super(SearchSortMenu, self).make_buttons()\n        if feature.is_enabled('link_relevancy'):\n            button = self.button_cls('relevance2', 'relevance2', self.name)\n            buttons.append(button)\n        return buttons\n\n\nclass SubredditSearchSortMenu(SortMenu):\n    \"\"\"Sort menu for subreddit search pages.\"\"\"\n    _default = 'relevance'\n    _options = ('relevance', 'activity')\n\n\nclass RecSortMenu(SortMenu):\n    \"\"\"Sort menu for recommendation page\"\"\"\n    _default = 'new'\n    _options = ('hot', 'new', 'top', 'controversial', 'relevance')\n\n\nclass KindMenu(SortMenu):\n    name = 'kind'\n    _default = 'all'\n    _options = ('links', 'comments', 'messages', 'all')\n    _title = N_(\"kind\")\n\n    def make_title(self, attr):\n        if attr == \"all\":\n            return _(\"all\")\n        return menu[attr]\n\n\nclass TimeMenu(SortMenu):\n    \"\"\"Menu for setting the time interval of the listing (from 'hour' to 'all')\"\"\"\n    name = 't'\n    _default = 'all'\n    _options = ('hour', 'day', 'week', 'month', 'year', 'all')\n    _title = N_(\"links from\")\n\n    @classmethod\n    def operator(self, time):\n        from r2.models import Link\n        if time != 'all':\n            return Link.c._date >= timeago(time)\n\nclass CommentsTimeMenu(TimeMenu):\n    \"\"\"Time Menu with the title changed for comments\"\"\"\n    _title = N_(\"comments from\")\n\n\nclass ProfileOverviewTimeMenu(TimeMenu):\n    \"\"\"Time Menu with the title changed for a user overview\"\"\"\n    _title = N_(\"links and comments from\")\n\n\nclass ControversyTimeMenu(TimeMenu):\n    \"\"\"time interval for controversial sort.  Make default time 'day' rather than 'all'\"\"\"\n    _default = 'day'\n    button_cls = PostButton\n\n\nclass SubredditMenu(NavMenu):\n    def find_selected(self):\n        \"\"\"Always return False so the title is always displayed\"\"\"\n        return None\n\n\nclass JsNavMenu(NavMenu):\n    def find_selected(self):\n        \"\"\"Always return the first element.\"\"\"\n        return self.options[0]\n\n# --------------------\n# TODO: move to admin area\nclass AdminReporterMenu(SortMenu):\n    default = 'top'\n    options = ('hot', 'new', 'top')\n\nclass AdminKindMenu(KindMenu):\n    options = ('all', 'links', 'comments', 'spam', 'autobanned')\n\n\nclass AdminTimeMenu(TimeMenu):\n    get_param = 't'\n    _default = 'day'\n    _options = ('hour', 'day', 'week')\n"
  },
  {
    "path": "r2/r2/lib/merge.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport subprocess\nimport tempfile\nimport difflib\nfrom pylons.i18n import _\nfrom pylons import app_globals as g\n\n\nMAX_DIFF_LINE_LENGTH = 4000\n\n\nclass ConflictException(Exception):\n    def __init__(self, new, your, original):\n        self.your = your\n        self.new = new\n        self.original = original\n        self.htmldiff = make_htmldiff(new, your, _(\"current edit\"), _(\"your edit\"))\n        Exception.__init__(self)\n\n\ndef make_htmldiff(a, b, adesc, bdesc):\n    diffcontent = difflib.HtmlDiff(wrapcolumn=60)\n\n    def truncate(line):\n        if len(line) > MAX_DIFF_LINE_LENGTH:\n            line = line[:MAX_DIFF_LINE_LENGTH] + \"...\"\n        return line\n    return diffcontent.make_table([truncate(i) for i in a.splitlines()],\n                                  [truncate(i) for i in b.splitlines()],\n                                  fromdesc=adesc,\n                                  todesc=bdesc,\n                                  context=3)\n\ndef threewaymerge(original, a, b):\n    temp_dir = g.diff3_temp_location if g.diff3_temp_location else None\n    data = [a, original, b]\n    files = []\n    try:\n        for d in data:\n            f = tempfile.NamedTemporaryFile(dir=temp_dir)\n            f.write(d.encode('utf-8'))\n            f.flush()\n            files.append(f)\n        try:\n            final = subprocess.check_output([\"diff3\", \"-a\", \"--merge\"] + [f.name for f in files])\n        except subprocess.CalledProcessError:\n            raise ConflictException(b, a, original)\n    finally:\n        for f in files:\n            f.close()\n    return final.decode('utf-8')\n\nif __name__ == \"__main__\":\n    class test_globals:\n        diff3_temp_location = None\n    \n    g = test_globals()\n    \n    original = \"Hello people of the human rance\\n\\nHow are you tday\"\n    a = \"Hello people of the human rance\\n\\nHow are you today\"\n    b = \"Hello people of the human race\\n\\nHow are you tday\"\n    \n    print threewaymerge(original, a, b)\n    \n    g.diff3_temp_location = '/dev/shm'\n    \n    print threewaymerge(original, a, b)\n"
  },
  {
    "path": "r2/r2/lib/message_to_email.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport hashlib\nimport hmac\nimport json\n\nfrom pylons import app_globals as g\nimport requests\n\nfrom r2.lib import amqp\nfrom r2.lib.filters import _force_unicode\nfrom r2.lib.template_helpers import add_sr\nfrom r2.lib.utils import constant_time_compare\nfrom r2.models import (\n    Account,\n    Message,\n    Subreddit,\n)\n\n\ndef get_reply_to_address(message):\n    \"\"\"Construct a reply-to address that encodes the message id.\n\n    The address is of the form:\n        zendeskreply+{message_id36}-{email_mac}\n\n    where the mac is generated from {message_id36} using the\n    `modmail_email_secret`\n\n    The reply address should be configured with the inbound email service so\n    that replies to our messages are routed back to the app somehow. For mailgun\n    this involves adding a Routes filter for messages sent to\n    \"zendeskreply\\+*@\". to be forwarded to POST /api/zendeskreply.\n\n    \"\"\"\n\n    # all email replies are treated as replies to the first message in the\n    # conversation. this is to get around some peculiarities of zendesk\n    if message.first_message:\n        first_message = Message._byID(message.first_message, data=True)\n    else:\n        first_message = message\n    email_id = first_message._id36\n\n    email_mac = hmac.new(\n        g.secrets['modmail_email_secret'], email_id, hashlib.sha256).hexdigest()\n    reply_id = \"zendeskreply+{email_id}-{email_mac}\".format(\n        email_id=email_id, email_mac=email_mac)\n\n    sr = Subreddit._byID(message.sr_id, data=True)\n    return \"r/{subreddit} mail <{reply_id}@{domain}>\".format(\n        subreddit=sr.name, reply_id=reply_id, domain=g.modmail_email_domain)\n\n\ndef parse_and_validate_reply_to_address(address):\n    \"\"\"Validate the address and parse out and return the message id.\n\n    This is the reverse operation of `get_reply_to_address`.\n\n    \"\"\"\n\n    recipient, sep, domain = address.partition(\"@\")\n    if not sep or not recipient or domain != g.modmail_email_domain:\n        return\n\n    main, sep, remainder = recipient.partition(\"+\")\n    if not sep or not main or main != \"zendeskreply\":\n        return\n\n    try:\n        email_id, email_mac = remainder.split(\"-\")\n    except ValueError:\n        return\n\n    expected_mac = hmac.new(\n        g.secrets['modmail_email_secret'], email_id, hashlib.sha256).hexdigest()\n\n    if not constant_time_compare(expected_mac, email_mac):\n        return\n\n    message_id36 = email_id\n    return message_id36\n\n\ndef get_message_subject(message):\n    sr = Subreddit._byID(message.sr_id, data=True)\n\n    if message.first_message:\n        first_message = Message._byID(message.first_message, data=True)\n        conversation_subject = first_message.subject\n    else:\n        conversation_subject = message.subject\n\n    return u\"[r/{subreddit} mail]: {subject}\".format(\n        subreddit=sr.name, subject=_force_unicode(conversation_subject))\n\n\ndef get_email_ids(message):\n    parent_email_id = None\n    other_email_ids = []\n    if message.parent_id:\n        parent = Message._byID(message.parent_id, data=True)\n        if parent.email_id:\n            other_email_ids.append(parent.email_id)\n            parent_email_id = parent.email_id\n\n    if message.first_message:\n        first_message = Message._byID(message.first_message, data=True)\n        if first_message.email_id:\n            other_email_ids.append(first_message.email_id)\n\n    return parent_email_id, other_email_ids\n\n\ndef get_system_from_address(sr):\n    return \"r/{subreddit} mail <{sender_email}>\".format(\n        subreddit=sr.name, sender_email=g.modmail_system_email)\n\n\ndef send_modmail_email(message):\n    if not message.sr_id:\n        return\n\n    sr = Subreddit._byID(message.sr_id, data=True)\n\n    forwarding_email = g.live_config['modmail_forwarding_email'].get(sr.name)\n    if not forwarding_email:\n        return\n\n    sender = Account._byID(message.author_id, data=True)\n\n    if sender.name in g.admins:\n        distinguish = \"[A]\"\n    elif sr.is_moderator(sender):\n        distinguish = \"[M]\"\n    else:\n        distinguish = None\n\n    if distinguish:\n        from_address = \"u/{username} {distinguish} <{sender_email}>\".format(\n            username=sender.name, distinguish=distinguish,\n            sender_email=g.modmail_sender_email)\n    else:\n        from_address = \"u/{username} <{sender_email}>\".format(\n            username=sender.name, sender_email=g.modmail_sender_email)\n\n    reply_to = get_reply_to_address(message)\n    parent_email_id, other_email_ids = get_email_ids(message)\n    subject = get_message_subject(message)\n\n    if message.from_sr and not message.first_message:\n        # this is a message from the subreddit to a user. add some text that\n        # shows the recipient\n        recipient = Account._byID(message.to_id, data=True)\n        sender_text = (\"This message was sent from r/{subreddit} to \"\n            \"u/{user}\").format(subreddit=sr.name, user=recipient.name)\n    else:\n        userlink = add_sr(\"/u/{name}\".format(name=sender.name), sr_path=False)\n        sender_text = \"This message was sent by {userlink}\".format(\n            userlink=userlink,\n        )\n\n    reply_footer = (\"\\n\\n-\\n{sender_text}\\n\\n\"\n        \"Reply to this email directly or view it on reddit: {link}\")\n    reply_footer = reply_footer.format(\n        sender_text=sender_text,\n        link=message.make_permalink(force_domain=True),\n    )\n    message_text = message.body + reply_footer\n\n    email_id = g.email_provider.send_email(\n        to_address=forwarding_email,\n        from_address=from_address,\n        subject=subject,\n        text=message_text,\n        reply_to=reply_to,\n        parent_email_id=parent_email_id,\n        other_email_ids=other_email_ids,\n    )\n    if email_id:\n        g.log.info(\"sent %s as %s\", message._id36, email_id)\n        message.email_id = email_id\n        message._commit()\n        g.stats.simple_event(\"modmail_email.outgoing_email\")\n\n\ndef send_blocked_muted_email(sr, parent, sender_email, incoming_email_id):\n    subject = get_message_subject(parent)\n    from_address = get_system_from_address(sr)\n    text = \"Message was not delivered because recipient is muted.\"\n\n    email_id = g.email_provider.send_email(\n        to_address=sender_email,\n        from_address=from_address,\n        subject=subject,\n        text=text,\n        reply_to=from_address,\n        parent_email_id=incoming_email_id,\n        other_email_ids=[parent.email_id],\n    )\n    if email_id:\n        g.log.info(\"sent as %s\", email_id)\n\n\ndef queue_modmail_email(message):\n    amqp.add_item(\n        \"modmail_email_q\",\n        json.dumps({\n            \"event\": \"new_message\",\n            \"message_id36\": message._id36,\n        }),\n    )\n\n\ndef queue_blocked_muted_email(sr, parent, sender_email, incoming_email_id):\n    amqp.add_item(\n        \"modmail_email_q\",\n        json.dumps({\n            \"event\": \"blocked_muted\",\n            \"subreddit_id36\": sr._id36,\n            \"parent_id36\": parent._id36,\n            \"sender_email\": sender_email,\n            \"incoming_email_id\": incoming_email_id,\n        }),\n    )\n\n\ndef process_modmail_email():\n    @g.stats.amqp_processor(\"modmail_email_q\")\n    def process_message(msg):\n        msg_dict = json.loads(msg.body)\n        if msg_dict[\"event\"] == \"new_message\":\n            message_id36 = msg_dict[\"message_id36\"]\n            message = Message._byID36(message_id36, data=True)\n            send_modmail_email(message)\n        elif msg_dict[\"event\"] == \"blocked_muted\":\n            subreddit_id36 = msg_dict[\"subreddit_id36\"]\n            sr = Subreddit._byID36(subreddit_id36, data=True)\n            parent_id36 = msg_dict[\"parent_id36\"]\n            parent = Message._byID36(parent_id36, data=True)\n            sender_email = msg_dict[\"sender_email\"]\n            incoming_email_id = msg_dict[\"incoming_email_id\"]\n            send_blocked_muted_email(sr, parent, sender_email, incoming_email_id)\n\n    amqp.consume_items(\"modmail_email_q\", process_message)\n"
  },
  {
    "path": "r2/r2/lib/migrate/__init__.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\n"
  },
  {
    "path": "r2/r2/lib/migrate/campaigns_to_things.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport sys\nfrom collections import defaultdict\nfrom r2.models import *\n\ndef fix_trans_id():\n    bad_campaigns = list(PromoCampaign._query(PromoCampaign.c.trans_id == 1, data=True))\n    num_bad_campaigns = len(bad_campaigns)\n\n    if not num_bad_campaigns:\n        print \"No campaigns with trans_id == 1\"\n        return\n\n    # print some info and prompt user to continue\n    print (\"Found %d campaigns with trans_id == 1. \\n\"\n           \"Campaigns ids: %s \\n\" \n           \"Press 'c' to fix them or any other key to abort.\" %\n           (num_bad_campaigns, [pc._id for pc in bad_campaigns]))\n    input_char = sys.stdin.read(1)\n    if input_char != 'c' and input_char != 'C':\n        print \"aborting...\"\n        return\n\n    # log the ids for reference\n    print (\"Fixing %d campaigns with bad freebie trans_id: %s\" % \n           (num_bad_campaigns, [pc._id for pc in bad_campaigns]))\n\n    # get corresponding links and copy trans_id from link data to campaign thing\n    link_ids = set([campaign.link_id for campaign in bad_campaigns])\n    print \"Fetching associated links: %s\" % link_ids\n    try:\n        links = Link._byID(link_ids, data=True, return_dict=False)\n    except NotFound, e:\n        print(\"Invalid data: Some promocampaigns have invalid link_ids. \"\n              \"Please delete these campaigns or fix the data before \"\n              \"continuing. Exception: %s\" % e)\n\n    # organize bad campaigns by link_id\n    bad_campaigns_by_link = defaultdict(list)\n    for c in bad_campaigns:\n        bad_campaigns_by_link[c.link_id].append(c)\n\n    # iterate through links and copy trans_id from pickled list on the link to \n    # the campaign thing\n    failed = []\n    for link in links:\n        link_campaigns = getattr(link, \"campaigns\")\n        thing_campaigns = bad_campaigns_by_link[link._id]\n        for campaign in thing_campaigns:\n            try:\n                sd, ed, bid, sr_name, trans_id = link_campaigns[campaign._id]\n                if trans_id != campaign.trans_id:\n                    campaign.trans_id = trans_id\n                    campaign._commit()\n            except:\n                failed.append({\n                    'link_id': link._id,\n                    'campaign_id': campaign._id,\n                    'exc type': sys.exc_info()[0],\n                    'exc msg': sys.exc_info()[1]\n                })\n\n    # log the actions for future reference\n    msg = (\"%d of %d campaigns updated successfully. %d updates failed: %s\" %\n           (num_bad_campaigns, num_bad_campaigns - len(failed), len(failed), failed))\n    print msg\n\n        \n"
  },
  {
    "path": "r2/r2/lib/migrate/migrate.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\n\"\"\"\nOne-time use functions to migrate from one reddit-version to another\n\"\"\"\nfrom r2.lib.promote import *\n\ndef add_allow_top_to_srs():\n    \"Add the allow_top property to all stored subreddits\"\n    from r2.models import Subreddit\n    from r2.lib.db.operators import desc\n    from r2.lib.utils import fetch_things2\n\n    q = Subreddit._query(Subreddit.c._spam == (True,False),\n                         sort = desc('_date'))\n    for sr in fetch_things2(q):\n        sr.allow_top = True; sr._commit()\n\ndef subscribe_to_blog_and_annoucements(filename):\n    import re\n    from time import sleep\n    from r2.models import Account, Subreddit\n\n    r_blog = Subreddit._by_name(\"blog\")\n    r_announcements = Subreddit._by_name(\"announcements\")\n\n    contents = file(filename).read()\n    numbers = [ int(s) for s in re.findall(\"\\d+\", contents) ]\n\n#    d = Account._byID(numbers, data=True)\n\n#   for i, account in enumerate(d.values()):\n    for i, account_id in enumerate(numbers):\n        account = Account._byID(account_id, data=True)\n\n        for sr in r_blog, r_announcements:\n            if sr.add_subscriber(account):\n                sr._incr(\"_ups\", 1)\n                print (\"%d: subscribed %s to %s\" % (i, account.name, sr.name))\n            else:\n                print (\"%d: didn't subscribe %s to %s\" % (i, account.name, sr.name))\n\n\ndef recompute_unread(min_date = None):\n    from r2.models import Inbox, Account, Comment, Message\n    from r2.lib.db import queries\n\n    def load_accounts(inbox_rel):\n        accounts = set()\n        q = inbox_rel._query(eager_load = False, data = False,\n                             sort = desc(\"_date\"))\n        if min_date:\n            q._filter(inbox_rel.c._date > min_date)\n\n        for i in fetch_things2(q):\n            accounts.add(i._thing1_id)\n\n        return accounts\n\n    accounts_m = load_accounts(Inbox.rel(Account, Message))\n    for i, a in enumerate(accounts_m):\n        a = Account._byID(a)\n        print \"%s / %s : %s\" % (i, len(accounts_m), a)\n        queries.get_unread_messages(a).update()\n        queries.get_unread_comments(a).update()\n        queries.get_unread_selfreply(a).update()\n\n    accounts = load_accounts(Inbox.rel(Account, Comment)) - accounts_m\n    for i, a in enumerate(accounts):\n        a = Account._byID(a)\n        print \"%s / %s : %s\" % (i, len(accounts), a)\n        queries.get_unread_comments(a).update()\n        queries.get_unread_selfreply(a).update()\n\n\n\ndef pushup_permacache(verbosity=1000):\n    \"\"\"When putting cassandra into the permacache chain, we need to\n       push everything up into the rest of the chain, so this is\n       everything that uses the permacache, as of that check-in.\"\"\"\n    from pylons import app_globals as g\n    from r2.models import Link, Subreddit, Account\n    from r2.lib.db.operators import desc\n    from r2.lib.comment_tree import comments_key, messages_key\n    from r2.lib.utils import fetch_things2, in_chunks\n    from r2.lib.utils import last_modified_key\n    from r2.lib.promote import promoted_memo_key\n    from r2.lib.subreddit_search import load_all_reddits\n    from r2.lib.db import queries\n    from r2.lib.cache import CassandraCacheChain\n\n    authority = g.permacache.caches[-1]\n    nonauthority = CassandraCacheChain(g.permacache.caches[1:-1])\n\n    def populate(keys):\n        vals = authority.simple_get_multi(keys)\n        if vals:\n            nonauthority.set_multi(vals)\n\n    def gen_keys():\n        yield promoted_memo_key\n\n        # just let this one do its own writing\n        load_all_reddits()\n\n        yield queries.get_all_comments().iden\n\n        l_q = Link._query(Link.c._spam == (True, False),\n                          Link.c._deleted == (True, False),\n                          sort=desc('_date'),\n                          data=True,\n                          )\n        for link in fetch_things2(l_q, verbosity):\n            yield comments_key(link._id)\n            yield last_modified_key(link, 'comments')\n\n        a_q = Account._query(Account.c._spam == (True, False),\n                             sort=desc('_date'),\n                             )\n        for account in fetch_things2(a_q, verbosity):\n            yield messages_key(account._id)\n            yield last_modified_key(account, 'overview')\n            yield last_modified_key(account, 'commented')\n            yield last_modified_key(account, 'submitted')\n            yield last_modified_key(account, 'liked')\n            yield last_modified_key(account, 'disliked')\n            yield queries.get_comments(account, 'new', 'all').iden\n            yield queries.get_submitted(account, 'new', 'all').iden\n            yield queries.get_liked(account).iden\n            yield queries.get_disliked(account).iden\n            yield queries.get_hidden(account).iden\n            yield queries.get_saved(account).iden\n            yield queries.get_inbox_messages(account).iden\n            yield queries.get_unread_messages(account).iden\n            yield queries.get_inbox_comments(account).iden\n            yield queries.get_unread_comments(account).iden\n            yield queries.get_inbox_selfreply(account).iden\n            yield queries.get_unread_selfreply(account).iden\n            yield queries.get_sent(account).iden\n\n        sr_q = Subreddit._query(Subreddit.c._spam == (True, False),\n                                sort=desc('_date'),\n                                )\n        for sr in fetch_things2(sr_q, verbosity):\n            yield last_modified_key(sr, 'stylesheet_contents')\n            yield queries.get_links(sr, 'hot', 'all').iden\n            yield queries.get_links(sr, 'new', 'all').iden\n\n            for sort in 'top', 'controversial':\n                for time in 'hour', 'day', 'week', 'month', 'year', 'all':\n                    yield queries.get_links(sr, sort, time,\n                                            merge_batched=False).iden\n            yield queries.get_spam_links(sr).iden\n            yield queries.get_spam_comments(sr).iden\n            yield queries.get_reported_links(sr).iden\n            yield queries.get_reported_comments(sr).iden\n            yield queries.get_subreddit_messages(sr).iden\n            yield queries.get_unread_subreddit_messages(sr).iden\n\n    done = 0\n    for keys in in_chunks(gen_keys(), verbosity):\n        g.reset_caches()\n        done += len(keys)\n        print 'Done %d: %r' % (done, keys[-1])\n        populate(keys)\n\n\ndef port_cassaurls(after_id=None, estimate=15231317):\n    from r2.models import Link, LinksByUrlAndSubreddit\n    from r2.lib.db import tdb_cassandra\n    from r2.lib.db.operators import desc\n    from r2.lib.db.tdb_cassandra import CL\n    from r2.lib.utils import fetch_things2, in_chunks, progress\n\n    q = Link._query(Link.c._spam == (True, False),\n                    sort=desc('_date'), data=True)\n    if after_id:\n        q._after(Link._byID(after_id,data=True))\n    q = fetch_things2(q, chunk_size=500)\n    q = progress(q, estimate=estimate)\n    q = (l for l in q\n         if getattr(l, 'url', 'self') != 'self'\n         and not getattr(l, 'is_self', False))\n    chunks = in_chunks(q, 500)\n\n    for chunk in chunks:\n        for l in chunk:\n            LinksByUrlAndSubreddit.add_link(l)\n\ndef port_deleted_links(after_id=None):\n    from r2.models import Link\n    from r2.lib.db.operators import desc\n    from r2.models.query_cache import CachedQueryMutator\n    from r2.lib.db.queries import get_deleted_links\n    from r2.lib.utils import fetch_things2, in_chunks, progress\n\n    q = Link._query(Link.c._deleted == True,\n                    Link.c._spam == (True, False),\n                    sort=desc('_date'), data=True)\n    q = fetch_things2(q, chunk_size=500)\n    q = progress(q, verbosity=1000)\n\n    for chunk in in_chunks(q):\n        with CachedQueryMutator() as m:\n            for link in chunk:\n                query = get_deleted_links(link.author_id)\n                m.insert(query, [link])\n\ndef convert_query_cache_to_json():\n    import cPickle\n    from r2.models.query_cache import json, UserQueryCache\n\n    with UserQueryCache._cf.batch() as m:\n        for key, columns in UserQueryCache._cf.get_range():\n            out = {}\n            for ckey, cvalue in columns.iteritems():\n                try:\n                    raw = cPickle.loads(cvalue)\n                except cPickle.UnpicklingError:\n                    continue\n                out[ckey] = json.dumps(raw)\n            m.insert(key, out)\n\ndef populate_spam_filtered():\n    from r2.lib.db.queries import get_spam_links, get_spam_comments\n    from r2.lib.db.queries import get_spam_filtered_links, get_spam_filtered_comments\n    from r2.models.query_cache import CachedQueryMutator\n\n    def was_filtered(thing):\n        if thing._spam and not thing._deleted and \\\n           getattr(thing, 'verdict', None) != 'mod-removed':\n            return True\n        else:\n            return False\n\n    q = Subreddit._query(sort = asc('_date'))\n    for sr in fetch_things2(q):\n        print 'Processing %s' % sr.name\n        links = Thing._by_fullname(get_spam_links(sr), data=True,\n                                   return_dict=False)\n        comments = Thing._by_fullname(get_spam_comments(sr), data=True,\n                                      return_dict=False)\n        insert_links = [l for l in links if was_filtered(l)]\n        insert_comments = [c for c in comments if was_filtered(c)]\n        with CachedQueryMutator() as m:\n            m.insert(get_spam_filtered_links(sr), insert_links)\n            m.insert(get_spam_filtered_comments(sr), insert_comments)\n"
  },
  {
    "path": "r2/r2/lib/migrate/mr_domains.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\n\"\"\"\nGenerate the data for the listings for the time-based Subreddit\nqueries. The format is eventually that of the CachedResults objects\nused by r2.lib.db.queries (with some intermediate steps), so changes\nthere may warrant changes here\n\"\"\"\n\n# to run:\n\"\"\"\nexport LINKDBHOST=prec01\nexport USER=ri\nexport INI=production.ini\ncd ~/reddit/r2\ntime psql -F\"\\t\" -A -t -d newreddit -U $USER -h $LINKDBHOST \\\n     -c \"\\\\copy (select t.thing_id, 'thing', 'link',\n                        t.ups, t.downs, t.deleted, t.spam, extract(epoch from t.date)\n                   from reddit_thing_link t\n                  where not t.spam and not t.deleted\n                  )\n                  to 'reddit_thing_link.dump'\"\ntime psql -F\"\\t\" -A -t -d newreddit -U $USER -h $LINKDBHOST \\\n     -c \"\\\\copy (select d.thing_id, 'data', 'link',\n                        d.key, d.value\n                   from reddit_data_link d\n                  where d.key = 'url' ) to 'reddit_data_link.dump'\"\ncat reddit_data_link.dump reddit_thing_link.dump | sort -T. -S200m | paster --plugin=r2 run $INI r2/lib/migrate/mr_domains.py -c \"join_links()\" > links.joined\ncat links.joined | paster --plugin=r2 run $INI r2/lib/migrate/mr_domains.py -c \"time_listings()\" | sort -T. -S200m | paster --plugin=r2 run $INI r2/lib/migrate/mr_domains.py -c \"write_permacache()\"\n\"\"\"\n\nimport sys\n\nfrom r2.models import Account, Subreddit, Link\nfrom r2.lib.db.sorts import epoch_seconds, score, controversy, _hot\nfrom r2.lib.db import queries\nfrom r2.lib import mr_tools\nfrom r2.lib.utils import timeago, UrlParser\nfrom r2.lib.jsontemplates import make_fullname # what a strange place\n                                               # for this function\ndef join_links():\n    mr_tools.join_things(('url',))\n\n\ndef time_listings(times = ('all',)):\n    oldests = dict((t, epoch_seconds(timeago('1 %s' % t)))\n                   for t in times if t != \"all\")\n    oldests['all'] = epoch_seconds(timeago('10 years'))\n\n    @mr_tools.dataspec_m_thing((\"url\", str),)\n    def process(link):\n        assert link.thing_type == 'link'\n\n        timestamp = link.timestamp\n        fname = make_fullname(Link, link.thing_id)\n\n        if not link.spam and not link.deleted:\n            if link.url:\n                domains = UrlParser(link.url).domain_permutations()\n            else:\n                domains = []\n            ups, downs = link.ups, link.downs\n\n            for tkey, oldest in oldests.iteritems():\n                if timestamp > oldest:\n                    sc = score(ups, downs)\n                    contr = controversy(ups, downs)\n                    h = _hot(ups, downs, timestamp)\n                    for domain in domains:\n                        yield ('domain/top/%s/%s' % (tkey, domain),\n                               sc, timestamp, fname)\n                        yield ('domain/controversial/%s/%s' % (tkey, domain),\n                               contr, timestamp, fname)\n                        if tkey == \"all\":\n                            yield ('domain/hot/%s/%s' % (tkey, domain),\n                                   h, timestamp, fname)\n                            yield ('domain/new/%s/%s' % (tkey, domain),\n                                   timestamp, timestamp, fname)\n\n    mr_tools.mr_map(process)\n\ndef store_keys(key, maxes):\n    # we're building queries using queries.py, but we could make the\n    # queries ourselves if we wanted to avoid the individual lookups\n    # for accounts and subreddits.\n\n    # Note that we're only generating the 'sr-' type queries here, but\n    # we're also able to process the other listings generated by the\n    # old migrate.mr_permacache for convenience\n\n    userrel_fns = dict(liked = queries.get_liked,\n                       disliked = queries.get_disliked,\n                       saved = queries.get_saved,\n                       hidden = queries.get_hidden)\n\n    if key.startswith('user-'):\n        acc_str, keytype, account_id = key.split('-')\n        account_id = int(account_id)\n        fn = queries.get_submitted if keytype == 'submitted' else queries.get_comments\n        q = fn(Account._byID(account_id), 'new', 'all')\n        q._insert_tuples([(fname, float(timestamp))\n                    for (timestamp, fname)\n                    in maxes])\n\n    elif key.startswith('sr-'):\n        sr_str, sort, time, sr_id = key.split('-')\n        sr_id = int(sr_id)\n\n        if sort == 'controversy':\n            # I screwed this up in the mapper and it's too late to fix\n            # it\n            sort = 'controversial'\n\n        q = queries.get_links(Subreddit._byID(sr_id), sort, time)\n        q._insert_tuples([tuple([item[-1]] + map(float, item[:-1]))\n                    for item in maxes])\n    elif key.startswith('domain/'):\n        d_str, sort, time, domain = key.split('/')\n        q = queries.get_domain_links(domain, sort, time)\n        q._insert_tuples([tuple([item[-1]] + map(float, item[:-1]))\n                    for item in maxes])\n\n\n    elif key.split('-')[0] in userrel_fns:\n        key_type, account_id = key.split('-')\n        account_id = int(account_id)\n        fn = userrel_fns[key_type]\n        q = fn(Account._byID(account_id))\n        q._insert_tuples([tuple([item[-1]] + map(float, item[:-1]))\n                    for item in maxes])\n\n\ndef write_permacache(fd = sys.stdin):\n    mr_tools.mr_reduce_max_per_key(lambda x: map(float, x[:-1]), num=1000,\n                                   post=store_keys,\n                                   fd = fd)\n"
  },
  {
    "path": "r2/r2/lib/migrate/mr_permacache.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\n\"\"\"\nTry to regenerate the permacache items devoted to listings after a\nstorage failure in Cassandra\n\"\"\"\n\n\"\"\"\ncat > mr_permacache <<HERE\n#!/bin/sh\ncd ~/reddit/r2\npaster run staging.ini ./mr_permacache.py -c \"\\$1\"\nHERE\nchmod u+x mr_permacache\n\nLINKDBHOST=prec01\nCOMMENTDBHOST=db02s1\nVOTEDBHOST=db03s1\nSAVEHIDEDBHOST=db01s1\n\n## links\ntime psql -F\"\\t\" -A -t -d newreddit -U ri -h $LINKDBHOST \\\n     -c \"\\\\copy (select t.thing_id, 'thing', 'link',\n                        t.ups, t.downs, t.deleted, t.spam, extract(epoch from t.date)\n                   from reddit_thing_link t) to 'reddit_thing_link.dump'\"\ntime psql -F\"\\t\" -A -t -d newreddit -U ri -h $LINKDBHOST \\\n     -c \"\\\\copy (select d.thing_id, 'data', 'link',\n                        d.key, d.value\n                   from reddit_data_link d\n                  where d.key = 'author_id' or d.key = 'sr_id') to 'reddit_data_link.dump'\"\npv reddit_data_link.dump reddit_thing_link.dump | sort -T. -S200m | ./mr_permacache \"join_links()\" > links.joined\npv links.joined | ./mr_permacache \"link_listings()\" | sort -T. -S200m > links.listings\n\n## comments\npsql -F\"\\t\" -A -t -d newreddit -U ri -h $COMMENTDBHOST \\\n     -c \"\\\\copy (select t.thing_id, 'thing', 'comment',\n                        t.ups, t.downs, t.deleted, t.spam, extract(epoch from t.date)\n                   from reddit_thing_comment t) to 'reddit_thing_comment.dump'\"\npsql -F\"\\t\" -A -t -d newreddit -U ri -h $COMMENTDBHOST \\\n     -c \"\\\\copy (select d.thing_id, 'data', 'comment',\n                        d.key, d.value\n                   from reddit_data_comment d\n                  where d.key = 'author_id') to 'reddit_data_comment.dump'\"\ncat reddit_data_comment.dump reddit_thing_comment.dump | sort -T. -S200m | ./mr_permacache \"join_comments()\" > comments.joined\ncat links.joined | ./mr_permacache \"comment_listings()\" | sort -T. -S200m > comments.listings\n\n## linkvotes\npsql -F\"\\t\" -A -t -d newreddit -U ri -h $VOTEDBHOST \\\n     -c \"\\\\copy (select r.rel_id, 'vote_account_link',\n                        r.thing1_id, r.thing2_id, r.name, extract(epoch from r.date)\n                   from reddit_rel_vote_account_link r) to 'reddit_linkvote.dump'\"\npv reddit_linkvote.dump | ./mr_permacache \"linkvote_listings()\" | sort -T. -S200m > linkvotes.listings\n\n#savehide\npsql -F\"\\t\" -A -t -d newreddit -U ri -h $SAVEHIDEDBHOST \\\n     -c \"\\\\copy (select r.rel_id, 'savehide',\n                        r.thing1_id, r.thing2_id, r.name, extract(epoch from r.date)\n                   from reddit_rel_savehide r) to 'reddit_savehide.dump'\"\npv reddit_savehide.dump | ./mr_permacache \"savehide_listings()\" | sort -T. -S200m > savehide.listings\n\n## load them up\n# the individual .listings files are sorted so even if it's not sorted\n# overall we don't need to re-sort them\nmkdir listings\npv *.listings | ./mr_permacache \"top1k_writefiles('listings')\"\n./mr_permacache \"write_permacache_from_dir('$PWD/listings')\"\n\n\"\"\"\n\nimport os, os.path, errno\nimport sys\nimport itertools\nfrom hashlib import md5\n\nfrom r2.lib import mr_tools\nfrom r2.lib.mr_tools import dataspec_m_thing, dataspec_m_rel, join_things\n\n\nfrom dateutil.parser import parse as parse_timestamp\n\nfrom r2.models import *\nfrom r2.lib.db.sorts import epoch_seconds, score, controversy, _hot\nfrom r2.lib.utils import fetch_things2, in_chunks, progress, UniqueIterator, tup\nfrom r2.lib import comment_tree\nfrom r2.lib.db import queries\n\nfrom r2.lib.jsontemplates import make_fullname # what a strange place\n                                               # for this function\n\ndef join_links():\n    join_things(('author_id', 'sr_id'))\n\ndef link_listings():\n    @dataspec_m_thing(('author_id', int),\n                      ('sr_id', int))\n    def process(link):\n        assert link.thing_type == 'link'\n\n        author_id = link.author_id\n        timestamp = link.timestamp\n        fname = make_fullname(Link, link.thing_id)\n\n        yield 'user-submitted-%d' % author_id, timestamp, fname\n        if not link.spam:\n            sr_id = link.sr_id\n            ups, downs = link.ups, link.downs\n\n            yield ('sr-hot-all-%d' % sr_id, _hot(ups, downs, timestamp),\n                   timestamp, fname)\n            yield 'sr-new-all-%d' % sr_id, timestamp, fname\n            yield 'sr-top-all-%d' % sr_id, score(ups, downs), timestamp, fname\n            yield ('sr-controversial-all-%d' % sr_id,\n                   controversy(ups, downs), timestamp, fname)\n            for time in '1 year', '1 month', '1 week', '1 day', '1 hour':\n                if timestamp > epoch_seconds(timeago(time)):\n                    tkey = time.split(' ')[1]\n                    yield ('sr-top-%s-%d' % (tkey, sr_id),\n                           score(ups, downs), timestamp, fname)\n                    yield ('sr-controversial-%s-%d' % (tkey, sr_id),\n                           controversy(ups, downs),\n                           timestamp, fname)\n\n    mr_tools.mr_map(process)\n\ndef join_comments():\n    join_things(('author_id',))\n\ndef comment_listings():\n    @dataspec_m_thing(('author_id', int),)\n    def process(comment):\n        assert comment.thing_type == 'comment'\n\n        yield ('user-commented-%d' % comment.author_id,\n               comment.timestamp, make_fullname(Comment, comment.thing_id))\n\n    mr_tools.mr_map(process)\n\ndef rel_listings(names, thing2_cls = Link):\n    # names examples: {'1': 'liked',\n    #                  '-1': 'disliked'}\n    @dataspec_m_rel()\n    def process(rel):\n        if rel.name in names:\n            yield ('%s-%s' % (names[rel.name], rel.thing1_id), rel.timestamp,\n                   make_fullname(thing2_cls, rel.thing2_id))\n    mr_tools.mr_map(process)\n\ndef linkvote_listings():\n    rel_listings({'1': 'liked',\n                  '-1': 'disliked'})\n\ndef savehide_listings():\n    rel_listings({'save': 'saved',\n                  'hide': 'hidden'})\n\ndef insert_to_query(q, items):\n    q._insert_tuples(items)\n\ndef store_keys(key, maxes):\n    # we're building queries from queries.py, but we could avoid this\n    # by making the queries ourselves if we wanted to avoid the\n    # individual lookups for accounts and subreddits\n    userrel_fns = dict(liked = queries.get_liked,\n                       disliked = queries.get_disliked,\n                       saved = queries.get_saved,\n                       hidden = queries.get_hidden)\n    if key.startswith('user-'):\n        acc_str, keytype, account_id = key.split('-')\n        account_id = int(account_id)\n        fn = queries.get_submitted if keytype == 'submitted' else queries.get_comments\n        q = fn(Account._byID(account_id), 'new', 'all')\n        insert_to_query(q, [(fname, float(timestamp))\n                            for (timestamp, fname)\n                            in maxes ])\n    elif key.startswith('sr-'):\n        sr_str, sort, time, sr_id = key.split('-')\n        sr_id = int(sr_id)\n\n        if sort == 'controversy':\n            # I screwed this up in the mapper and it's too late to fix\n            # it\n            sort = 'controversial'\n\n        q = queries.get_links(Subreddit._byID(sr_id), sort, time)\n        insert_to_query(q, [tuple([item[-1]] + map(float, item[:-1]))\n                            for item in maxes])\n\n    elif key.split('-')[0] in userrel_fns:\n        key_type, account_id = key.split('-')\n        account_id = int(account_id)\n        fn = userrel_fns[key_type]\n        q = fn(Account._byID(account_id))\n        insert_to_query(q, [tuple([item[-1]] + map(float, item[:-1]))\n                            for item in maxes])\n\ndef top1k_writefiles(dirname):\n    \"\"\"Divide up the top 1k of each key into its own file to make\n       restarting after a failure much easier. Pairs with\n       write_permacache_from_dir\"\"\"\n    def hashdir(name, levels = [3]):\n        # levels is a list of how long each stage if the hashdirname\n        # should be. So [2,2] would make dirs like\n        # 'ab/cd/thelisting.txt' (and this function would just return\n        # the string 'ab/cd', so that you have the dirname that you\n        # can create before os.path.joining to the filename)\n        h = md5(name).hexdigest()\n\n        last = 0\n        dirs = []\n        for l in levels:\n            dirs.append(h[last:last+l])\n            last += l\n\n        return os.path.join(*dirs)\n\n    def post(key, maxes):\n        # we're taking a hash like 12345678901234567890123456789012\n        # and making a directory name two deep out of the first half\n        # of the characters. We may want to tweak this as the number\n        # of listings\n\n        hd = os.path.join(dirname, hashdir(key))\n        try:\n            os.makedirs(hd)\n        except OSError as e:\n            if e.errno != errno.EEXIST:\n                raise\n        filename = os.path.join(hd, key)\n\n        with open(filename, 'w') as f:\n            for item in maxes:\n                f.write('%s\\t' % key)\n                f.write('\\t'.join(item))\n                f.write('\\n')\n        \n    mr_tools.mr_reduce_max_per_key(lambda x: map(float, x[:-1]), num=1000,\n                                   post=post)\n\ndef top1k_writepermacache(fd = sys.stdin):\n    mr_tools.mr_reduce_max_per_key(lambda x: map(float, x[:-1]), num=1000,\n                                   post=store_keys,\n                                   fd = fd)\n\ndef write_permacache_from_dir(dirname):\n    # we want the whole list so that we can display accurate progress\n    # information. If we're operating on more than tens of millions of\n    # files, we should either bail out or tweak this to not need the\n    # whole list at once\n    allfiles = []\n    for root, dirs, files in os.walk(dirname):\n        for f in files:\n            allfiles.append(os.path.join(root, f))\n\n    for fname in progress(allfiles, persec=True):\n        try:\n            write_permacache_from_file(fname)\n            os.unlink(fname)\n        except:\n            mr_tools.status('failed on %r' % fname)\n            raise\n\n    mr_tools.status('Removing empty directories')\n    for root, dirs, files in os.walk(dirname, topdown=False):\n        for d in dirs:\n            dname = os.path.join(root, d)\n            try:\n                os.rmdir(dname)\n            except OSError as e:\n                if e.errno == errno.ENOTEMPTY:\n                    mr_tools.status('%s not empty' % (dname,))\n                else:\n                    raise\n\ndef write_permacache_from_file(fname):\n    with open(fname) as fd:\n        top1k_writepermacache(fd = fd)\n"
  },
  {
    "path": "r2/r2/lib/migrate/vote_details_ip_backfill.py",
    "content": "import json\nfrom collections import defaultdict\nfrom datetime import datetime, timedelta\n\nfrom pylons import app_globals as g\n\nfrom r2.lib.db.sorts import epoch_seconds\nfrom r2.lib.db.tdb_cassandra import write_consistency_level\nfrom r2.lib.utils import in_chunks\nfrom r2.models.vote import VoteDetailsByComment, VoteDetailsByLink, VoterIPByThing\n\n\ndef backfill_vote_details(cls):\n    ninety_days = timedelta(days=90).total_seconds()\n    for chunk in in_chunks(cls._all(), size=100):\n        detail_chunk = defaultdict(dict)\n        try:\n            with VoterIPByThing._cf.batch(write_consistency_level=cls._write_consistency_level) as b:\n                for vote_list in chunk:\n                    thing_id36 = vote_list._id\n                    thing_fullname = vote_list.votee_fullname\n                    details = vote_list.decode_details()\n                    for detail in details:\n                        voter_id36 = detail[\"voter_id\"]\n                        if \"ip\" in detail and detail[\"ip\"]:\n                            ip = detail[\"ip\"]\n                            redacted = dict(detail)\n                            del redacted[\"ip\"]\n                            cast = detail[\"date\"]\n                            now = epoch_seconds(datetime.utcnow().replace(tzinfo=g.tz))\n                            ttl = ninety_days - (now - cast)\n                            oneweek = \"\"\n                            if ttl < 3600 * 24 * 7:\n                                oneweek = \"(<= one week left)\"\n                            print \"Inserting %s with IP ttl %d %s\" % (redacted, ttl, oneweek)\n                            detail_chunk[thing_id36][voter_id36] = json.dumps(redacted)\n                            if ttl <= 0:\n                                print \"Skipping bogus ttl for %s: %d\" % (redacted, ttl)\n                                continue\n                            b.insert(thing_fullname, {voter_id36: ip}, ttl=ttl)\n        except Exception:\n            # Getting some really weird spurious errors here; complaints about negative\n            # TTLs even though they can't possibly be negative, errors from cass\n            # that have an explanation of \"(why=')\"\n            # Just going to brute-force this through.  We might lose 100 here and there\n            # but mostly it'll be intact.\n            pass\n        for votee_id36, valuedict in detail_chunk.iteritems():\n            cls._set_values(votee_id36, valuedict)\n\n\ndef main():\n    cfs = [VoteDetailsByComment, VoteDetailsByLink]\n    for cf in cfs:\n        backfill_vote_details(cf)\n\nif __name__ == '__builtin__':\n    main()\n"
  },
  {
    "path": "r2/r2/lib/mr_tools/__init__.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom r2.lib.mr_tools._mr_tools import *\nfrom r2.lib.mr_tools.mr_tools import *\n"
  },
  {
    "path": "r2/r2/lib/mr_tools/_mr_tools.pyx",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport sys\nfrom itertools import imap, groupby\nfrom heapq import nlargest\n\nstdin = sys.stdin\nstderr = sys.stderr\n\nclass _Chunker(object):\n    __slots__ = ('_size', '_done', '_it')\n\n    def __init__(self, it, long size=25):\n        self._it = iter(it)\n        self._size = range(size)\n        self._done = 0\n\n    def next(self):\n        if self._done:\n            raise StopIteration\n\n        cdef list chunk = []\n\n        for x in self._size:\n            try:\n                chunk.append(next(self._it))\n            except StopIteration:\n                if chunk:\n                    self._done = 1\n                    return chunk\n                else:\n                    raise\n        return chunk\n\ncdef class in_chunks(object):\n    cdef it\n    cdef int size\n\n    def __init__(self, it, int size=25):\n        self.it = it\n        self.size = size\n\n    def __iter__(self):\n        return _Chunker(self.it, self.size)\n\ncdef class Storage(dict):\n    def __getattr__(self, attr):\n        return self[attr]\n\ndef valiter(grouper):\n    key, group = grouper\n    return key, imap(lambda x: x[1:], group)\n\ncpdef list _keyiter_splitter(str x):\n    x = x.strip('\\n')\n    return x.split('\\t')\n\ndef keyiter(stream=stdin):\n    lines = imap(_keyiter_splitter, stream)\n    groups = groupby(lines, lambda x: x[0])\n    return imap(valiter, groups)\n\ndef emit(vals):\n    print '\\t'.join(map(str, vals))\n\ndef emit_all(vals):\n    for val in vals:\n        emit(val)\n\ndef status(msg, **opts):\n    if opts:\n        msg = msg % opts\n    stderr.write(\"%s\\n\" % msg)\n\ncpdef Storage format_dataspec(msg, specs):\n    # spec() =:= name | (name, fn)\n    # specs  =:= [ spec() ]\n    cdef Storage ret = Storage()\n    for val, spec in zip(msg, specs):\n        if isinstance(spec, basestring):\n            # the spec is just a name\n            name = spec\n            ret[name] = val\n        else:\n            # the spec is a tuple of the name and the function to pass\n            # the string through to make the real value\n            name, fn = spec\n            ret[name] = fn(val)\n    return Storage(**ret)\n\ncdef class dataspec_m(object):\n    cdef specs\n\n    def __init__(self, *specs):\n        self.specs = specs\n\n    def __call__(self, fn):\n        specs = self.specs\n        def wrapped_fn_m(args):\n            return fn(format_dataspec(args, specs))\n        return wrapped_fn_m\n\ncdef class dataspec_r(object):\n    cdef specs\n\n    def __init__(self, *specs):\n        self.specs = specs\n\n    def __call__(self, fn):\n        specs = self.specs\n        def wrapped_fn_r(key, msgs):\n            return fn(key, imap(lambda msg: format_dataspec(msg, specs),\n                                msgs))\n        return wrapped_fn_r\n\ncpdef mr_map(process, fd = stdin):\n    for line in fd:\n        vals = line.strip('\\n').split('\\t')\n        for res in process(vals):\n            emit(res)\n\ncpdef mr_reduce(process, fd = stdin):\n    for key, vals in keyiter(fd):\n        for res in process(key, vals):\n            emit(res)\n\ncpdef mr_foldl(process, init, emit = False, fd = stdin):\n    acc = init\n    for key, vals in keyiter(fd):\n        acc = process(key, vals, acc)\n\n    if emit:\n        emit(acc)\n\n    return acc\n\ncpdef mr_max(process, int idx = 0, int num = 10, emit = False, fd = stdin):\n    \"\"\"a reducer that, in the process of reduction, only returns the\n       top N results\"\"\"\n    cdef list maxes = []\n    for key, vals in keyiter(fd):\n        for newvals in in_chunks(process(key, vals)):\n            for val in newvals:\n                if len(maxes) < num or val[idx] > maxes[-1][idx]:\n                    maxes.append(val)\n            maxes.sort(reverse=True)\n            maxes = maxes[:num]\n\n    if emit:\n        emit_all(maxes)\n\n    return maxes\n\ncpdef _sbool(str x):\n    return x == 't'\n\ndef dataspec_m_rel(*fields):\n    return dataspec_m(*((('rel_id', int),\n                         'rel_type',\n                         ('thing1_id', int),\n                         ('thing2_id', int),\n                         'name',\n                         ('timestamp', float))\n                        + fields))\n\ndef dataspec_m_thing(*fields):\n    return dataspec_m(*((('thing_id', int),\n                         'thing_type',\n                         ('ups', int),\n                         ('downs', int),\n                         ('deleted', _sbool),\n                         ('spam', _sbool),\n                         ('timestamp', float))\n                        + fields))\n\ndef mr_reduce_max_per_key(sort_key, post = None, num = 10, fd = sys.stdin):\n    def process(key, vals):\n        cdef list maxes = nlargest(num, vals, key=sort_key)\n\n        if post:\n            # if we were passed a \"post\" function, he takes\n            # responsibility for emitting\n            post(key, maxes)\n            return []\n\n        return [ ([key] + item)\n                 for item in maxes ]\n\n    return mr_reduce(process, fd = fd)\n"
  },
  {
    "path": "r2/r2/lib/mr_tools/mr_tools.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport sys\nimport multiprocessing\n\nfrom r2.lib.mr_tools._mr_tools import mr_map, mr_reduce, format_dataspec\nfrom r2.lib.mr_tools._mr_tools import stdin, emit\n\ndef join_things(fields, deleted=False, spam=True):\n    \"\"\"A reducer that joins thing table dumps and data table dumps\"\"\"\n    # Because of how Python handles scope, if we want to modify these outside\n    # the closure function below, they need to be inside a mutable object.\n    # http://stackoverflow.com/a/23558809/120999\n    counters = {\n        'processed': 0,\n        'skipped': 0,\n    }\n    def process(thing_id, vals):\n        data = {}\n        thing = None\n\n        for val in vals:\n            if val[0] == 'thing':\n                thing = format_dataspec(val,\n                                        ['data_type', # e.g. 'thing'\n                                         'thing_type', # e.g. 'link'\n                                         'ups',\n                                         'downs',\n                                         'deleted',\n                                         'spam',\n                                         'timestamp'])\n            elif val[0] == 'data':\n                val = format_dataspec(val,\n                                      ['data_type', # e.g. 'data'\n                                       'thing_type', # e.g. 'link'\n                                       'key', # e.g. 'sr_id'\n                                       'value'])\n                if val.key in fields:\n                    data[val.key] = val.value\n\n        if (\n            # silently ignore if we didn't see the 'thing' row\n            thing is not None\n\n            # remove spam and deleted as appriopriate\n            and (deleted or thing.deleted == 'f')\n            and (spam or thing.spam == 'f')\n\n            # and silently ignore items that don't have all of the\n            # data that we need\n            and all(field in data for field in fields)):\n\n            counters['processed'] += 1\n            yield ((thing_id, thing.thing_type, thing.ups, thing.downs,\n                    thing.deleted, thing.spam, thing.timestamp)\n                   + tuple(data[field] for field in fields))\n        else:\n            counters['skipped'] += 1\n\n    mr_reduce(process)\n    # Print to stderr to avoid getting this caught up in the pipe of\n    # compute_time_listings.\n    print >> sys.stderr, '%s items processed, %s skipped' % (\n                         counters['processed'], counters['skipped'])\n\nclass Mapper(object):\n    def __init__(self):\n        pass\n\n    def process(self, values):\n        raise NotImplemented\n\n    def __call__(self, line):\n        line = line.strip('\\n')\n        vals = line.split('\\t')\n        return list(self.process(vals)) # a list of tuples\n\ndef mr_map_parallel(processor, fd = stdin,\n                    workers = multiprocessing.cpu_count(),\n                    chunk_size = 1000):\n    # `process` must be an instance of Mapper and promise that it is\n    # safe to execute in a fork()d process.  Also note that we fuck\n    # up the result ordering, but relying on result ordering breaks\n    # the mapreduce contract anyway. Note also that like many of the\n    # mr_tools functions, we break on newlines in the emitted output\n\n    if workers == 1:\n        return mr_map(process, fd=fd)\n\n    pool = multiprocessing.Pool(workers)\n\n    for res in pool.imap_unordered(processor, fd, chunk_size):\n        for subres in res:\n            emit(subres)\n\ndef test():\n    from r2.lib.mr_tools._mr_tools import keyiter\n\n    for key, vals in keyiter():\n        print key, vals\n        for val in vals:\n            print '\\t', val\n\nclass UpperMapper(Mapper):\n    def process(self, values):\n        yield map(str.upper, values)\n\ndef test_parallel():\n    return mr_map_parallel(UpperMapper())\n"
  },
  {
    "path": "r2/r2/lib/mr_top.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\n# Known bug: if a given listing hasn't had a submission in the\n# allotted time (e.g. the year listing in a subreddit that hasn't had\n# a submission in the last year), we won't write out an empty\n# list. I'll call it a feature.\n\nimport sys\n\nfrom r2.models import Link, Comment\nfrom r2.lib.db.sorts import epoch_seconds, score, controversy\nfrom r2.lib.db import queries\nfrom r2.lib import mr_tools\nfrom r2.lib.utils import timeago, UrlParser\nfrom r2.lib.jsontemplates import make_fullname # what a strange place\n                                               # for this function\n\nthingcls_by_name = {\n    \"link\": Link,\n    \"comment\": Comment,\n}\ndata_fields_by_name = {\n    \"link\": {\n        \"url\": str,\n        \"sr_id\": int,\n        \"author_id\": int,\n    },\n    \"comment\": {\n        \"sr_id\": int,\n        \"author_id\": int,\n    },\n}\n\n\ndef join_things(thing_type):\n    mr_tools.join_things(data_fields_by_name[thing_type].keys())\n\n\ndef _get_cutoffs(intervals):\n    cutoffs = {}\n    for interval in intervals:\n        if interval == \"all\":\n            cutoffs[\"all\"] = 0.0\n        else:\n            cutoffs[interval] = epoch_seconds(timeago(\"1 %s\" % interval))\n\n    return cutoffs\n\n\ndef time_listings(intervals, thing_type):\n    cutoff_by_interval = _get_cutoffs(intervals)\n\n    @mr_tools.dataspec_m_thing(*data_fields_by_name[thing_type].items())\n    def process(thing):\n        if thing.deleted:\n            return\n\n        thing_cls = thingcls_by_name[thing.thing_type]\n        fname = make_fullname(thing_cls, thing.thing_id)\n        thing_score = score(thing.ups, thing.downs)\n        thing_controversy = controversy(thing.ups, thing.downs)\n\n        for interval, cutoff in cutoff_by_interval.iteritems():\n            if thing.timestamp < cutoff:\n                continue\n\n            yield (\"user/%s/top/%s/%d\" % (thing.thing_type, interval, thing.author_id),\n                   thing_score, thing.timestamp, fname)\n            yield (\"user/%s/controversial/%s/%d\" % (thing.thing_type, interval, thing.author_id),\n                   thing_controversy, thing.timestamp, fname)\n\n            if thing.spam:\n                continue\n\n            if thing.thing_type == \"link\":\n                yield (\"sr/link/top/%s/%d\" % (interval, thing.sr_id),\n                       thing_score, thing.timestamp, fname)\n                yield (\"sr/link/controversial/%s/%d\" % (interval, thing.sr_id),\n                       thing_controversy, thing.timestamp, fname)\n\n                if thing.url:\n                    try:\n                        parsed = UrlParser(thing.url)\n                    except ValueError:\n                        continue\n\n                    for domain in parsed.domain_permutations():\n                        yield (\"domain/link/top/%s/%s\" % (interval, domain),\n                               thing_score, thing.timestamp, fname)\n                        yield (\"domain/link/controversial/%s/%s\" % (interval, domain),\n                               thing_controversy, thing.timestamp, fname)\n\n    mr_tools.mr_map(process)\n\n\ndef store_keys(key, maxes):\n    category, thing_cls, sort, time, id = key.split(\"/\")\n\n    query = None\n    if category == \"user\":\n        if thing_cls == \"link\":\n            query = queries._get_submitted(int(id), sort, time)\n        elif thing_cls == \"comment\":\n            query = queries._get_comments(int(id), sort, time)\n    elif category == \"sr\":\n        if thing_cls == \"link\":\n            query = queries._get_links(int(id), sort, time)\n    elif category == \"domain\":\n        if thing_cls == \"link\":\n            query = queries.get_domain_links(id, sort, time)\n\n    assert query, 'unknown query type for %s' % (key,)\n\n    item_tuples = [tuple([item[-1]] + [float(x) for x in item[:-1]])\n                   for item in maxes]\n\n    # we only need locking updates for non-time-based listings, since for time-\n    # based ones we're the only ones that ever update it\n    lock = time == 'all'\n\n    query._replace(item_tuples, lock=lock)\n\ndef write_permacache(fd = sys.stdin):\n    mr_tools.mr_reduce_max_per_key(lambda x: map(float, x[:-1]), num=1000,\n                                   post=store_keys,\n                                   fd = fd)\n\ndef reduce_listings(fd=sys.stdin):\n    # like write_permacache, but just sends the reduced version of the listing\n    # to stdout instead of to the permacache. It's handy for debugging to see\n    # the final result before it's written out\n    mr_tools.mr_reduce_max_per_key(lambda x: map(float, x[:-1]), num=1000,\n                                   fd = fd)\n"
  },
  {
    "path": "r2/r2/lib/newsletter.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom pylons import app_globals as g\n\nimport json\nimport requests\n\nBASE_URL = \"https://api.createsend.com/api/v3.1/\"\nAPI_KEY = g.secrets['newsletter_api_key']\nLIST_ID = g.newsletter_list_id\n\n# under providers even though this is not yet a provider because this will be\n# moved to become one, so we want the right stats namespace.\nSTATS_NAMESPACE = \"providers.campaignmonitor\"\n\n\nclass NewsletterError(Exception):\n    pass\n\n\nclass EmailUnacceptableError(NewsletterError):\n    pass\n\n\ndef add_subscriber(email, source=\"\"):\n    \"\"\"Given an email, add this user to our upvoted newsletter.\n\n    Optionally, also provide a source parameter describing where the subscribe\n    came from - like \"register\".\n\n    If the email was unable to be added, throws `NewsletterError`. Returns\n    `True` on success.\n\n    This should be used sparingly and outside of high traffic areas, as it\n    requires a network call.\n    \"\"\"\n    if not API_KEY or not LIST_ID:\n        raise NewsletterError(\"Unable to subscribe user %s to newsletter. \"\n                              \"API key or list ID not set.\" % email)\n\n    params = {\n        \"EmailAddress\": email\n    }\n    if source:\n        params[\"CustomFields\"] = [{\"Key\": \"source\", \"Value\": source}]\n\n    timer = g.stats.get_timer('%s.add_subscriber' % STATS_NAMESPACE)\n    timer.start()\n    try:\n        r = requests.post(\n            \"%s/subscribers/%s.json\" % (BASE_URL, LIST_ID),\n            json.dumps(params),\n            timeout=5,\n            auth=(API_KEY, 'x'),\n        )\n    except requests.exceptions.Timeout:\n        g.stats.simple_event('%s.request.timeout' % STATS_NAMESPACE)\n        raise NewsletterError(\"Unable to subscribe user %s to newsletter. \"\n                              \"Request timed out.\" % email)\n    except requests.exceptions.SSLError:\n        g.stats.simple_event('%s.request.ssl_error' % STATS_NAMESPACE)\n        raise NewsletterError(\"Unable to subscribe user %s to newsletter. \"\n                              \"SSL Error.\" % email)\n    else:\n        if r.status_code == 201:\n            return True\n        elif r.status_code == 400:\n            g.stats.simple_event('%s.request.email_unacceptable' %\n                                 STATS_NAMESPACE)\n            raise EmailUnacceptableError(\"Could not subscribe user %s to\"\n                                         \"newsletter. Email was unacceptable, \"\n                                         \"likely due to subscription status.\" %\n                                         email)\n        else:\n            raise NewsletterError(\"Could not subscribe user %s to \"\n                                  \"newsletter. Status code: %s\" %\n                                  (email, r.status_code))\n    finally:\n        timer.stop()\n"
  },
  {
    "path": "r2/r2/lib/normalized_hot.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport heapq\nimport itertools\nfrom datetime import datetime, timedelta\n\nfrom pylons import app_globals as g\n\nfrom r2.config import feature\nfrom r2.lib.db.queries import _get_links, CachedResults\nfrom r2.lib.db.sorts import epoch_seconds\n\n\nMAX_PER_SUBREDDIT = 150\nMAX_LINKS = 1000\n\n\ndef get_hot_tuples(sr_ids, ageweight=None):\n    queries_by_sr_id = {sr_id: _get_links(sr_id, sort='hot', time='all')\n                        for sr_id in sr_ids}\n    CachedResults.fetch_multi(queries_by_sr_id.values(), stale=True)\n    tuples_by_srid = {sr_id: [] for sr_id in sr_ids}\n\n    now_seconds = epoch_seconds(datetime.now(g.tz))\n\n    for sr_id, q in queries_by_sr_id.iteritems():\n        if not q.data:\n            continue\n\n        hot_factor = get_hot_factor(q.data[0], now_seconds, ageweight)\n\n        for link_name, hot, timestamp in q.data[:MAX_PER_SUBREDDIT]:\n            effective_hot = hot / hot_factor\n            # heapq.merge sorts from smallest to largest so we need to flip\n            # ehot and hot to get the hottest links first\n            tuples_by_srid[sr_id].append(\n                (-effective_hot, -hot, link_name, timestamp)\n            )\n\n    return tuples_by_srid\n\n\ndef get_hot_factor(qdata, now, ageweight):\n    \"\"\"Return a \"hot factor\" score for a link's hot tuple.\n\n    Recalculate the item's hot score as if it had been submitted\n    more recently than it was. This will cause the `effective_hot` value in\n    get_hot_tuples to move older first items back\n\n    ageweight should be a float from 0.0 - 1.0, which \"scales\" how far\n    between the original submission time and \"now\" to use as the base\n    for the new hot score. Smaller values will favor older #1 posts in\n    multireddits; larger values will drop older posts further in the ranking\n    (or possibly off the ranking entirely).\n\n    \"\"\"\n    ageweight = float(ageweight or 0.0)\n    link_name, hot, timestamp = qdata\n    return max(hot + ((now - timestamp) * ageweight) / 45000.0, 1.0)\n\n\ndef normalized_hot(sr_ids, obey_age_limit=True, ageweight=None):\n    timer = g.stats.get_timer(\"normalized_hot\")\n    timer.start()\n\n    if not sr_ids:\n        return []\n\n    if not feature.is_enabled(\"scaled_normalized_hot\"):\n        ageweight = None\n\n    tuples_by_srid = get_hot_tuples(sr_ids, ageweight=ageweight)\n\n    if obey_age_limit:\n        cutoff = datetime.now(g.tz) - timedelta(days=g.HOT_PAGE_AGE)\n        oldest = epoch_seconds(cutoff)\n    else:\n        oldest = 0.\n\n    merged = heapq.merge(*tuples_by_srid.values())\n    generator = (link_name for ehot, hot, link_name, timestamp in merged\n                           if timestamp > oldest)\n    ret = list(itertools.islice(generator, MAX_LINKS))\n    timer.stop()\n    return ret\n"
  },
  {
    "path": "r2/r2/lib/nymph.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport os\nimport re\nimport hashlib\nfrom PIL import Image\nimport subprocess\n\nfrom r2.lib.static import generate_static_name\n\nSPRITE_PADDING = 6\nsprite_line = re.compile(\n    r\"\"\"background-image:\\ *\n        url\\((?P<filename>.*)\\)\\ *\n        .*\n        /\\*\\ *SPRITE\\ *\n        (?P<stretch>stretch-x)?\\ *\n        (?:pixel-ratio=(?P<pixel_ratio>\\d))?\\ *\n        \\*/\n     \"\"\", re.X)\n\n\ndef optimize_png(filename):\n    with open(os.path.devnull, 'w') as devnull:\n        subprocess.check_call((\"/usr/bin/optipng\", filename), stdout=devnull)\n\n\ndef _extract_css_info(match):\n    image_filename = match.group(\"filename\")\n    should_stretch_property = match.group(\"stretch\")\n    pixel_ratio_property = match.group(\"pixel_ratio\")\n    image_filename = image_filename.strip('\"\\'')\n    should_stretch = (should_stretch_property == 'stretch-x')\n    if pixel_ratio_property:\n        pixel_ratio = int(pixel_ratio_property)\n    else:\n        pixel_ratio = 1\n    return image_filename, should_stretch, pixel_ratio\n\n\nclass SpritableImage(object):\n    def __init__(self, image, should_stretch=False):\n        self.image = image\n        self.stretch = should_stretch\n        self.filenames = []\n\n    @property\n    def width(self):\n        return self.image.size[0]\n\n    @property\n    def height(self):\n        return self.image.size[1]\n\n    def stretch_to_width(self, width):\n        self.image = self.image.resize((width, self.height))\n\n\nclass SpriteBin(object):\n    def __init__(self, bounding_box):\n        # the bounding box is a tuple of\n        # top-left-x, top-left-y, bottom-right-x, bottom-right-y\n        self.bounding_box = bounding_box\n        self.offset = 0\n        self.height = bounding_box[3] - bounding_box[1]\n\n    def has_space_for(self, image):\n        return (self.offset + image.width <= self.bounding_box[2] and\n                self.height >= image.height)\n\n    def add_image(self, image):\n        image.sprite_location = (self.offset, self.bounding_box[1])\n        self.offset += image.width + SPRITE_PADDING\n\n\ndef _load_spritable_images(css_filename):\n    css_location = os.path.dirname(os.path.abspath(css_filename))\n\n    images = {}\n    with open(css_filename, 'r') as f:\n        for line in f:\n            m = sprite_line.search(line)\n            if not m:\n                continue\n\n            image_filename, should_stretch, pixel_ratio = _extract_css_info(m)\n            image = Image.open(os.path.join(css_location, image_filename))\n            image_hash = hashlib.md5(image.convert(\"RGBA\").tostring()).hexdigest()\n\n            if image_hash not in images:\n                images[image_hash] = SpritableImage(image, should_stretch)\n            else:\n                assert images[image_hash].stretch == should_stretch\n            images[image_hash].filenames.append(image_filename)\n\n    # Sort images by filename to group the layout by names when possible.\n    return sorted(images.values(), key=lambda i: i.filenames[0])\n\n\ndef _generate_sprite(images, sprite_path):\n    sprite_width = max(i.width for i in images)\n    sprite_height = 0\n\n    # put all the max-width and stretch-x images together at the top\n    small_images = []\n    for image in images:\n        if image.width == sprite_width or image.stretch:\n            if image.stretch:\n                image.stretch_to_width(sprite_width)\n            image.sprite_location = (0, sprite_height)\n            sprite_height += image.height + SPRITE_PADDING\n        else:\n            small_images.append(image)\n\n    # lay out the remaining images -- done with a greedy algorithm\n    small_images.sort(key=lambda i: i.height, reverse=True)\n    bins = []\n\n    for image in small_images:\n        # find a bin to fit in\n        for bin in bins:\n            if bin.has_space_for(image):\n                break\n        else:\n            # or give up and create a new bin\n            bin = SpriteBin((0, sprite_height, sprite_width, sprite_height + image.height))\n            sprite_height += image.height + SPRITE_PADDING\n            bins.append(bin)\n\n        bin.add_image(image)\n\n    # generate the image\n    sprite_dimensions = (sprite_width, sprite_height)\n    background_color = (255, 69, 0, 0)  # transparent orangered\n    sprite = Image.new('RGBA', sprite_dimensions, background_color)\n\n    for image in images:\n        sprite.paste(image.image, image.sprite_location)\n\n    sprite.save(sprite_path, optimize=True)\n    optimize_png(sprite_path)\n\n    # give back the sprite and mangled name\n    sprite_base, sprite_name = os.path.split(sprite_path)\n    return (sprite, generate_static_name(sprite_name, base=sprite_base))\n\n\ndef _rewrite_css(css_filename, sprite_path, images, sprite_size):\n    # map filenames to coordinates\n    locations = {}\n    for image in images:\n        for filename in image.filenames:\n            locations[filename] = image.sprite_location\n\n    def rewrite_sprite_reference(match):\n        image_filename, should_stretch, pixel_ratio = _extract_css_info(match)\n        position = locations[image_filename]\n\n        if pixel_ratio > 1:\n            position = (position[0] / pixel_ratio, position[1] / pixel_ratio)\n\n        css_properties = [\n            'background-image: url(%s);' % sprite_path,\n            'background-position: -%dpx -%dpx;' % position,\n            'background-repeat: %s;' % ('repeat' if should_stretch else 'no-repeat'),\n        ]\n\n        if pixel_ratio > 1:\n            size = (sprite_size[0] / pixel_ratio, sprite_size[1] / pixel_ratio)\n            css_properties.append(\n                'background-size: %dpx %dpx;' % size,\n            )\n\n        return ''.join(css_properties)\n\n    # read in the css and replace sprite references\n    with open(css_filename, 'r') as f:\n        css = f.read()\n    return sprite_line.sub(rewrite_sprite_reference, css)\n\n\ndef spritify(css_filename, sprite_path):\n    images = _load_spritable_images(css_filename)\n    sprite, sprite_path = _generate_sprite(images, sprite_path)\n    return _rewrite_css(css_filename, sprite_path, images, sprite.size)\n\n\nif __name__ == '__main__':\n    import sys\n    print spritify(sys.argv[1], sys.argv[2])\n"
  },
  {
    "path": "r2/r2/lib/organic.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom r2.models import *\nfrom r2.lib.normalized_hot import normalized_hot\nfrom r2.lib import count\nfrom r2.lib.utils import UniqueIterator, timeago\n\nimport random\nfrom time import time\n\norganic_max_length= 50\n\n\ndef cached_organic_links(*sr_ids):\n    sr_count = count.get_link_counts()\n    #only use links from reddits that you're subscribed to\n    link_names = filter(lambda n: sr_count[n][1] in sr_ids, sr_count.keys())\n    link_names.sort(key = lambda n: sr_count[n][0])\n\n    if not link_names and g.debug:\n        q = All.get_links('new', 'all')\n        q._limit = 100 # this decomposes to a _query\n        link_names = [x._fullname for x in q if x.promoted is None]\n        g.log.debug('Used inorganic links')\n\n    #potentially add an up and coming link\n    if random.choice((True, False)) and sr_ids:\n        sr_id = random.choice(sr_ids)\n        fnames = normalized_hot([sr_id])\n        if fnames:\n            if len(fnames) == 1:\n                new_item = fnames[0]\n            else:\n                new_item = random.choice(fnames[1:4])\n            link_names.insert(0, new_item)\n\n    return link_names\n\ndef organic_links(user):\n    sr_ids = Subreddit.user_subreddits(user)\n    # make sure that these are sorted so the cache keys are constant\n    sr_ids.sort()\n\n    # get the default subreddits if the user is not logged in\n    user_id = None if isinstance(user, FakeAccount) else user\n    sr_ids = Subreddit.user_subreddits(user, True)\n\n    # pass the cached function a sorted list so that we can guarantee\n    # cachability\n    sr_ids.sort()\n    return cached_organic_links(*sr_ids)[:organic_max_length]\n\n"
  },
  {
    "path": "r2/r2/lib/pages/__init__.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom pages import *\nfrom admin_pages import *\n"
  },
  {
    "path": "r2/r2/lib/pages/admin_pages.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom pylons import config\nfrom pylons import tmpl_context as c\nfrom pylons import app_globals as g\nfrom pylons.i18n import N_\n\nfrom r2.lib.wrapped import Templated\nfrom r2.lib.pages import LinkInfoBar, Reddit\nfrom r2.lib.menus import (\n    NamedButton,\n    NavButton,\n    menu,\n    NavMenu,\n    OffsiteButton,\n)\nfrom r2.lib.utils import timesince\n\ndef admin_menu(**kwargs):\n    buttons = [\n        OffsiteButton(\"traffic\", \"/traffic\"),\n        NavButton(menu.awards, \"awards\"),\n        NavButton(menu.errors, \"error log\"),\n    ]\n\n    admin_menu = NavMenu(buttons, title='admin tools', base_path='/admin',\n                         type=\"lightdrop\", **kwargs)\n    return admin_menu\n\nclass AdminSidebar(Templated):\n    def __init__(self, user):\n        Templated.__init__(self)\n        self.user = user\n\n\nclass SponsorSidebar(Templated):\n    def __init__(self, user):\n        Templated.__init__(self)\n        self.user = user\n\n\nclass Details(Templated):\n    def __init__(self, link, *a, **kw):\n        Templated.__init__(self, *a, **kw)\n        self.link = link\n\n\nclass AdminPage(Reddit):\n    create_reddit_box  = False\n    submit_box         = False\n    extension_handling = False\n    show_sidebar = False\n\n    def __init__(self, nav_menus = None, *a, **kw):\n        Reddit.__init__(self, nav_menus = nav_menus, *a, **kw)\n\nclass AdminProfileMenu(NavMenu):\n    def __init__(self, path):\n        NavMenu.__init__(self, [], base_path = path,\n                         title = 'admin', type=\"tabdrop\")\n\n\nclass AdminLinkMenu(NavMenu):\n    def __init__(self, link):\n        NavMenu.__init__(self, [], title='admin', type=\"tabdrop\")\n\n\nclass AdminNotesSidebar(Templated):\n    EMPTY_MESSAGE = {\n        \"domain\": N_(\"No notes for this domain\"),\n        \"ip\": N_(\"No notes for this IP address\"),\n        \"subreddit\": N_(\"No notes for this subreddit\"),\n        \"user\": N_(\"No notes for this user\"),\n    }\n\n    SYSTEMS = {\n        \"domain\": N_(\"domain\"),\n        \"ip\": N_(\"IP address\"),\n        \"subreddit\": N_(\"subreddit\"),\n        \"user\": N_(\"user\"),\n    }\n\n    def __init__(self, system, subject):\n        from r2.models.admin_notes import AdminNotesBySystem\n\n        self.system = system\n        self.subject = subject\n        self.author = c.user.name\n        self.notes = AdminNotesBySystem.in_display_order(system, subject)\n        # Convert timestamps for easier reading/translation\n        for note in self.notes:\n            note[\"timesince\"] = timesince(note[\"when\"])\n        Templated.__init__(self)\n\n\nclass AdminLinkInfoBar(LinkInfoBar):\n    pass\n\n\nclass AdminDetailsBar(Templated):\n    pass\n\n\nif config['r2.import_private']:\n    from r2admin.lib.pages import *\n"
  },
  {
    "path": "r2/r2/lib/pages/pages.py",
    "content": "# -*- coding: utf-8 -*-\n# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom collections import Counter, OrderedDict\n\nfrom r2.config import feature\nfrom r2.lib.contrib.ipaddress import ip_address\nfrom r2.lib.db.operators import asc\nfrom r2.lib.wrapped import Wrapped, Templated, CachedTemplate\nfrom r2.models import (\n    Account,\n    All,\n    AllMinus,\n    AllSR,\n    Comment,\n    DefaultSR,\n    DomainSR,\n    FakeSubreddit,\n    Filtered,\n    Flair,\n    FlairListBuilder,\n    FlairTemplate,\n    FlairTemplateBySubredditIndex,\n    Friends,\n    Frontpage,\n    LINK_FLAIR,\n    LabeledMulti,\n    Link,\n    ReadNextLink,\n    ReadNextListing,\n    Mod,\n    ModSR,\n    MultiReddit,\n    NotFound,\n    OLD_SITEWIDE_RULES,\n    Printable,\n    PromoCampaign,\n    PromotionPrices,\n    IDBuilder,\n    Random,\n    RandomNSFW,\n    RandomSubscription,\n    SITEWIDE_RULES,\n    StylesheetsEverywhere,\n    Subreddit,\n    SubredditRules,\n    Target,\n    Trophy,\n    USER_FLAIR,\n    make_feedurl,\n)\nfrom r2.models.bidding import Bid\nfrom r2.models.gold import (\n    calculate_server_seconds,\n    days_to_pennies,\n    paypal_subscription_url,\n    gold_payments_by_user,\n    gold_received_by_user,\n    get_current_value_of_month,\n    gold_goal_on,\n    gold_revenue_steady,\n    gold_revenue_volatile,\n    get_subscription_details,\n    TIMEZONE as GOLD_TIMEZONE,\n)\nfrom r2.models.promo import (\n    NO_TRANSACTION,\n    PROMOTE_COST_BASIS,\n    PROMOTE_PRIORITIES,\n    PromotionLog,\n    Collection,\n)\nfrom r2.models.token import OAuth2Client, OAuth2AccessToken\nfrom r2.models import traffic\nfrom r2.models import ModAction\nfrom r2.models import Thing\nfrom r2.models.wiki import WikiPage, ImagesByWikiPage\nfrom r2.lib.db import tdb_cassandra, queries\nfrom r2.config.extensions import is_api\nfrom r2.lib.menus import CommentSortMenu\n\nfrom pylons.i18n import _, ungettext\nfrom pylons import request, config\nfrom pylons import tmpl_context as c\nfrom pylons import app_globals as g\nfrom pylons.controllers.util import abort\n\nfrom r2.lib import hooks, inventory, media\nfrom r2.lib import promote, tracking\nfrom r2.lib.captcha import get_iden\nfrom r2.lib.filters import (\n    scriptsafe_dumps,\n    spaceCompress,\n    _force_unicode,\n    _force_utf8,\n    unsafe,\n    websafe,\n    SC_ON,\n    SC_OFF,\n    websafe_json,\n    wikimarkdown,\n)\nfrom r2.lib.menus import NavButton, NamedButton, NavMenu, PageNameNav, JsButton\nfrom r2.lib.menus import SubredditButton, SubredditMenu, ModeratorMailButton\nfrom r2.lib.menus import OffsiteButton, menu, JsNavMenu\nfrom r2.lib.normalized_hot import normalized_hot\nfrom r2.lib.providers import image_resizing\nfrom r2.lib.strings import (\n    get_funny_translated_string,\n    plurals,\n    Score,\n    strings,\n)\nfrom r2.lib.utils import is_subdomain, title_to_url, UrlParser\nfrom r2.lib.utils import url_links_builder, median, to36\nfrom r2.lib.utils import trunc_time, timesince, timeuntil, weighted_lottery\nfrom r2.lib.template_helpers import (\n    add_sr,\n    comment_label,\n    format_number,\n    get_domain,\n    make_url_https,\n    make_url_protocol_relative,\n    static,\n)\nfrom r2.lib.subreddit_search import popular_searches\nfrom r2.lib.memoize import memoize\nfrom r2.lib.utils import trunc_string as _truncate, to_date\nfrom r2.lib.filters import safemarkdown\nfrom r2.lib.utils import (\n    Storage,\n    feature_utils,\n    precise_format_timedelta,\n    tup,\n    url_is_embeddable_image,\n)\nfrom r2.lib.cache import make_key_id, MemcachedError\n\nfrom babel.numbers import format_currency\nfrom babel.dates import format_date\nfrom collections import defaultdict, namedtuple\nimport csv\nimport hmac\nimport hashlib\nimport cStringIO\nimport sys, random, datetime, calendar, simplejson, re, time\nimport time\nfrom itertools import chain, product\nfrom urllib import quote, urlencode\nfrom urlparse import urlparse\n\nfrom r2.lib.ip_events import ips_by_account_id\n\nfrom things import wrap_links, wrap_things, default_thing_wrapper\n\ndatefmt = _force_utf8(_('%d %b %Y'))\n\nMAX_DESCRIPTION_LENGTH = 150\n\ndef get_captcha():\n    if not c.user_is_loggedin or c.user.needs_captcha():\n        return get_iden()\n\ndef responsive(res, space_compress=None):\n    \"\"\"\n    Use in places where the template is returned as the result of the\n    controller so that it becomes compatible with the page cache.\n    \"\"\"\n    if space_compress is None:\n        space_compress = not g.template_debug\n\n    if is_api():\n        res = res or u''\n        if not c.allowed_callback and request.environ.get(\"WANT_RAW_JSON\"):\n            res = scriptsafe_dumps(res)\n        else:\n            res = websafe_json(simplejson.dumps(res))\n\n        if c.allowed_callback:\n            # Add a comment to the beginning to prevent the \"Rosetta Flash\"\n            # XSS when an attacker controls the beginning of a resource\n            res = \"/**/%s(%s)\" % (websafe_json(c.allowed_callback), res)\n    elif space_compress:\n        res = spaceCompress(res)\n    return res\n\n\nclass Robots(Templated):\n\n    def __init__(self, **context):\n        Templated.__init__(self, **context)\n        self.subreddit_sitemap = g.sitemap_subreddit_static_url\n\nclass CrossDomain(Templated):\n    pass\n\n\nclass Reddit(Templated):\n    '''Base class for rendering a page on reddit.  Handles toolbar creation,\n    content of the footers, and content of the corner buttons.\n\n    Constructor arguments:\n\n        space_compress -- run r2.lib.filters.spaceCompress on render\n        loginbox -- enable/disable rendering of the small login box in the right margin\n          (only if no user is logged in; login box will be disabled for a logged in user)\n        show_sidebar -- enable/disable content in the right margin\n\n        infotext -- text to display in a <p class=\"infotext\"> above the content\n        nav_menus -- list of Menu objects to be shown in the area below the header\n        content -- renderable object to fill the main content well in the page.\n\n    settings determined at class-declaration time\n\n      create_reddit_box -- enable/disable display of the \"Create a reddit\" box\n      submit_box        -- enable/disable display of the \"Submit\" box\n      searchbox         -- enable/disable the \"search\" box in the header\n      extension_handling -- enable/disable rendering using non-html templates\n                            (e.g. js, xml for rss, etc.)\n    '''\n\n    create_reddit_box  = True\n    submit_box         = True\n    header             = True\n    searchbox          = True\n    extension_handling = True\n    enable_login_cover = True\n    site_tracking      = True\n    show_infobar       = True\n    content_id         = None\n    css_class          = None\n    extra_page_classes = None\n    extra_stylesheets  = []\n\n    def __init__(self, space_compress=None, nav_menus=None, loginbox=True,\n                 infotext='', infotext_class=None, infotext_show_icon=False,\n                 content=None, short_description='', title='',\n                 robots=None, show_sidebar=True, show_chooser=False,\n                 header=True, srbar=True, page_classes=None, short_title=None,\n                 show_wiki_actions=False, extra_js_config=None,\n                 show_locationbar=False, auction_announcement=False,\n                 show_newsletterbar=False, canonical_link=None,\n                 **context):\n        Templated.__init__(self, **context)\n        self.title = title\n        self.short_title = short_title\n        self.short_description = short_description\n        self.robots = robots\n        self.infotext = infotext\n        self.extra_js_config = extra_js_config\n        self.show_wiki_actions = show_wiki_actions\n        self.loginbox = loginbox\n        self.show_sidebar = show_sidebar\n        self.space_compress = space_compress\n        self.dnt_enabled = feature.is_enabled(\"do_not_track\")\n        self.header = header\n        self.footer = RedditFooter()\n        self.debug_footer = DebugFooter()\n        self.supplied_page_classes = page_classes or []\n        self.show_newsletterbar = show_newsletterbar\n\n        self.auction_announcement = auction_announcement\n\n        #put the sort menus at the top\n        self.nav_menu = MenuArea(menus = nav_menus) if nav_menus else None\n\n        #add the infobar\n        self.welcomebar = None\n        self.newsletterbar = None\n        self.locationbar = None\n        self.infobar = None\n        self.mobilewebredirectbar = None\n        self.show_timeout_modal = False\n\n        if feature.is_enabled(\"new_expando_icons\"):\n            self.feature_new_expando_icons = True\n        if feature.is_enabled(\"expando_nsfw_flow\"):\n            self.feature_expando_nsfw_flow = True\n\n        # generate a canonical link for google\n        canonical_url = UrlParser(canonical_link or request.url)\n        canonical_url.canonicalize()\n        self.canonical_link = canonical_url.unparse()\n        if c.render_style != \"html\":\n            u = UrlParser(request.fullpath)\n            u.set_extension(\"\")\n            u.hostname = g.domain\n            u.scheme = g.default_scheme\n            if g.domain_prefix:\n                u.hostname = \"%s.%s\" % (g.domain_prefix, u.hostname)\n            self.canonical_link = u.unparse()\n\n        if self.show_infobar:\n            if not infotext:\n                if g.heavy_load_mode:\n                    # heavy load mode message overrides read only\n                    infotext = strings.heavy_load_msg\n                elif g.read_only_mode:\n                    infotext = strings.read_only_msg\n                elif g.live_config.get(\"announcement_message\"):\n                    infotext = g.live_config[\"announcement_message\"]\n            if c.user_is_loggedin and c.user.in_timeout:\n                timeout_days_remaining = c.user.days_remaining_in_timeout\n\n                if timeout_days_remaining:\n                    days = ungettext('day', 'days', timeout_days_remaining)\n                    days_str = '%(num)s %(days)s' % {\n                        'num': timeout_days_remaining,\n                        'days': days,\n                    }\n                    message = strings.in_temp_timeout_msg % {'days': days_str}\n                else:\n                    message = strings.in_perma_timeout_msg\n\n                self.infobar = RedditInfoBar(\n                    message=message,\n                    extra_class='timeout-infobar',\n                    show_icon=True,\n                )\n            elif infotext:\n                self.infobar = RedditInfoBar(\n                    message=infotext,\n                    extra_class=infotext_class,\n                    show_icon=infotext_show_icon,\n                )\n            elif isinstance(c.site, AllMinus) and not c.user.gold:\n                self.infobar = RedditInfoBar(message=strings.all_minus_gold_only,\n                                       extra_class=\"gold\")\n\n            if not c.user_is_loggedin:\n                if getattr(self, \"show_welcomebar\", True):\n                    self.welcomebar = WelcomeBar()\n                if self.show_newsletterbar:\n                    self.newsletterbar = NewsletterBar()\n\n            if (c.render_style == \"compact\" and\n                    getattr(self, \"show_mobilewebredirectbar\", True)):\n                self.mobilewebredirectbar = MobileWebRedirectBar()\n\n            show_locationbar &= not c.user.pref_hide_locationbar\n            if (show_locationbar and c.used_localized_defaults and\n                    (not c.user_is_loggedin or\n                     not c.user.has_subscribed)):\n                self.locationbar = LocationBar()\n\n        self.srtopbar = None\n        if srbar and not is_api():\n            self.srtopbar = SubredditTopBar()\n\n        panes = [content]\n\n        if c.user_is_loggedin and not is_api() and not self.show_wiki_actions:\n            # insert some form templates for js to use\n            # TODO: move these to client side templates\n            gold_link = GoldPayment(\"gift\",\n                                    \"monthly\",\n                                    months=1,\n                                    signed=False,\n                                    recipient=\"\",\n                                    giftmessage=None,\n                                    passthrough=None,\n                                    thing=None,\n                                    clone_template=True,\n                                    thing_type=\"link\",\n                                   )\n            gold_comment = GoldPayment(\"gift\",\n                                       \"monthly\",\n                                       months=1,\n                                       signed=False,\n                                       recipient=\"\",\n                                       giftmessage=None,\n                                       passthrough=None,\n                                       thing=None,\n                                       clone_template=True,\n                                       thing_type=\"comment\",\n                                      )\n\n            report_form_templates = ReportFormTemplates()\n\n            panes.append(report_form_templates)\n\n            if self.show_sidebar:\n                panes.extend([gold_comment, gold_link])\n\n            if c.user_is_sponsor:\n                panes.append(FraudForm())\n\n        if c.user_is_loggedin and c.user.in_timeout:\n            self.show_timeout_modal = True\n            self.timeout_days_remaining = c.user.days_remaining_in_timeout\n\n        self.popup_panes = self.build_popup_panes()\n        panes.append(self.popup_panes)\n\n        self._content = PaneStack(panes)\n\n        self.show_chooser = (\n            show_chooser and\n            c.render_style == \"html\" and\n            c.user_is_loggedin and\n            (\n                isinstance(c.site, (DefaultSR, AllSR, ModSR, LabeledMulti)) or\n                c.site.name == g.live_config[\"listing_chooser_explore_sr\"]\n            )\n        )\n\n        self.toolbars = self.build_toolbars()\n\n        has_style_override = (c.user_is_loggedin and\n                c.user.pref_default_theme_sr and\n                feature.is_enabled('stylesheets_everywhere') and\n                c.user.pref_enable_default_themes)\n        # if there is no style or the style is disabled for this subreddit\n        self.no_sr_styles = (isinstance(c.site, DefaultSR) or\n            (not self.get_subreddit_stylesheet_url(c.site) and not c.site.header) or\n            (c.user and not c.user.use_subreddit_style(c.site)))\n\n        self.default_theme_sr = DefaultSR()\n        # use override stylesheet if they have custom styles disabled or\n        # this subreddit has no custom stylesheet (or is the front page)\n        if self.no_sr_styles:\n            self.subreddit_stylesheet_url = self.get_subreddit_stylesheet_url(\n                self.default_theme_sr)\n        else:\n            self.subreddit_stylesheet_url = self.get_subreddit_stylesheet_url(c.site)\n\n        if has_style_override and self.no_sr_styles:\n            sr = Subreddit._by_name(c.user.pref_default_theme_sr)\n            # make sure they can still view their override subreddit\n            if sr.can_view(c.user) and sr.stylesheet_url:\n                self.subreddit_stylesheet_url = self.get_subreddit_stylesheet_url(sr)\n                if c.can_apply_styles and c.allow_styles and sr.header:\n                    self.default_theme_sr = sr\n\n\n    @staticmethod\n    def get_subreddit_stylesheet_url(sr):\n        if not g.css_killswitch and c.can_apply_styles and c.allow_styles:\n            if c.secure:\n                if sr.stylesheet_url:\n                    return make_url_https(sr.stylesheet_url)\n                elif sr.stylesheet_url_https:\n                    return sr.stylesheet_url_https\n            else:\n                if sr.stylesheet_url:\n                    return sr.stylesheet_url\n                elif sr.stylesheet_url_http:\n                    return sr.stylesheet_url_http\n\n    def wiki_actions_menu(self, moderator=False):\n        data_attrs = lambda event: (\n            {'type': 'subreddit', 'event-action': 'pageview', 'event-detail': event})\n\n        buttons = []\n\n        buttons.append(NamedButton(\n            \"wikirecentrevisions\",\n            css_class=\"wikiaction-revisions\",\n            dest=\"/wiki/revisions\",\n            data=data_attrs('wikirevisions')))\n        buttons.append(NamedButton(\n            \"wikipageslist\",\n            css_class=\"wikiaction-pages\",\n            dest=\"/wiki/pages\",\n            data=data_attrs('wikipages')))\n\n        if moderator:\n            buttons.append(NamedButton(\n                'wikibanned',\n                css_class='reddit-ban access-required',\n                dest='/about/wikibanned',\n                data=data_attrs('wikibanned')))\n            buttons.append(NamedButton(\n                'wikicontributors',\n                css_class='reddit-contributors access-required',\n                dest='/about/wikicontributors',\n                data=data_attrs('wikicontributors')))\n\n        return SideContentBox(_('wiki tools'),\n                      [NavMenu(buttons,\n                               type=\"flat_vert\",\n                               css_class=\"icon-menu\",\n                               separator=\"\")],\n                      _id=\"wikiactions\",\n                      collapsible=True)\n\n    def sr_admin_menu(self):\n        buttons = []\n        is_single_subreddit = not isinstance(c.site, (ModSR, MultiReddit))\n        is_admin = c.user_is_loggedin and c.user_is_admin\n        is_moderator_with_perms = lambda *perms: (\n            is_admin or c.site.is_moderator_with_perms(c.user, *perms))\n        data_attrs = lambda event: (\n            {'type': 'subreddit', 'event-action': 'pageview', 'event-detail': event})\n\n        if is_single_subreddit and is_moderator_with_perms('config'):\n            buttons.append(NavButton(\n                menu.community_settings,\n                css_class=\"reddit-edit access-required\",\n                dest=\"edit\",\n                data=data_attrs('editsubreddit')))\n            buttons.append(NavButton(\n                menu.edit_stylesheet,\n                css_class=\"edit-stylesheet access-required\",\n                dest=\"stylesheet\",\n                data=data_attrs('stylesheet')))\n            if feature.is_enabled(\"subreddit_rules\", subreddit=c.site.name):\n                buttons.append(NavButton(\n                    menu.community_rules,\n                    css_class=\"community-rules access-required\",\n                    dest=\"rules\",\n                    data=data_attrs('rules')))\n\n        if is_moderator_with_perms('mail'):\n            buttons.append(NamedButton(\n                \"modmail\",\n                dest=\"message/inbox\",\n                css_class=\"moderator-mail access-required\",\n                data=data_attrs('modmail')))\n\n        if is_single_subreddit:\n            if is_moderator_with_perms('access'):\n                buttons.append(NamedButton(\n                    \"moderators\",\n                    css_class=\"reddit-moderators\",\n                    data=data_attrs('moderators')))\n\n                if not c.site.hide_contributors:\n                    buttons.append(NavButton(\n                        menu.contributors,\n                        \"contributors\",\n                        css_class=\"reddit-contributors access-required\",\n                        data=data_attrs('contributors')))\n\n            buttons.append(NamedButton(\n                \"traffic\",\n                css_class=\"reddit-traffic access-required\",\n                data=data_attrs('traffic')))\n\n        if is_moderator_with_perms('posts'):\n            buttons.append(NamedButton(\n                \"modqueue\",\n                css_class=\"reddit-modqueue access-required\",\n                data=data_attrs('modqueue')))\n            buttons.append(NamedButton(\n                \"reports\",\n                css_class=\"reddit-reported access-required\",\n                data=data_attrs('reports')))\n            buttons.append(NamedButton(\n                \"spam\",\n                css_class=\"reddit-spam access-required\",\n                data=data_attrs('spam')))\n            buttons.append(NamedButton(\n                \"edited\",\n                css_class=\"reddit-edited access-required\",\n                data=data_attrs('edited')))\n\n        if is_single_subreddit:\n            if is_moderator_with_perms('access'):\n                buttons.append(NamedButton(\n                    \"banned\",\n                    css_class=\"reddit-ban access-required\",\n                    data=data_attrs('banned')))\n            if is_moderator_with_perms('access', 'mail'):\n                buttons.append(NamedButton(\n                    \"muted\",\n                    css_class=\"reddit-mute access-required\",\n                    data=data_attrs('muted')))\n            if is_moderator_with_perms('flair'):\n                buttons.append(NamedButton(\n                    \"flair\",\n                    css_class=\"reddit-flair access-required\",\n                    data=data_attrs('flair')))\n\n        # append AutoMod button if it's enabled and they have perms to change it\n        if (g.automoderator_account and\n                is_single_subreddit and\n                is_moderator_with_perms('config')):\n            # link to their config if they have one, or the docs if not\n            try:\n                WikiPage.get(c.site, \"config/automoderator\")\n                buttons.append(NamedButton(\n                    \"automod\",\n                    dest=\"../wiki/edit/config/automoderator\",\n                    css_class=\"reddit-automod access-required\",\n                    data=data_attrs('automoderator')))\n            except tdb_cassandra.NotFound:\n                buttons.append(NamedButton(\n                    \"new_automod\",\n                    sr_path=False,\n                    dest=\"../wiki/automoderator\",\n                    css_class=\"reddit-automod access-required\",\n                ))\n\n        buttons.append(NamedButton(\n            \"log\",\n            css_class=\"reddit-moderationlog access-required\",\n            data=data_attrs('moderationlog')))\n        if is_moderator_with_perms('posts'):\n            buttons.append(NamedButton(\n                    \"unmoderated\",\n                    css_class=\"reddit-unmoderated access-required\",\n                    data=data_attrs('unmoderated')))\n\n        return SideContentBox(_('moderation tools'),\n                              [NavMenu(buttons,\n                                       type=\"flat_vert\",\n                                       base_path=\"/about/\",\n                                       css_class=\"icon-menu\",\n                                       separator=\"\")],\n                              _id=\"moderation_tools\",\n                              collapsible=True)\n\n    def rightbox(self):\n        \"\"\"generates content in <div class=\"rightbox\">\"\"\"\n\n        ps = PaneStack(css_class='spacer')\n\n        if self.searchbox:\n            ps.append(SearchForm())\n\n        sidebar_message = g.live_config.get(\"sidebar_message\")\n        if sidebar_message and isinstance(c.site, DefaultSR):\n            ps.append(SidebarMessage(sidebar_message[0]))\n\n        gold_sidebar_message = g.live_config.get(\"gold_sidebar_message\")\n        if (c.user_is_loggedin and c.user.gold and\n                gold_sidebar_message and isinstance(c.site, DefaultSR)):\n            ps.append(SidebarMessage(gold_sidebar_message[0],\n                                     extra_class=\"gold\"))\n\n        if not c.user_is_loggedin and self.loginbox and not g.read_only_mode:\n            ps.append(LoginFormWide())\n\n        if isinstance(c.site, DomainSR) and c.user_is_admin:\n            from r2.lib.pages.admin_pages import AdminNotesSidebar\n            notebar = AdminNotesSidebar('domain', c.site.domain)\n            ps.append(notebar)\n\n        if isinstance(c.site, Subreddit) and c.user_is_admin:\n            from r2.lib.pages.admin_pages import AdminNotesSidebar\n            notebar = AdminNotesSidebar('subreddit', c.site.name)\n            ps.append(notebar)\n\n        if not c.user.pref_hide_ads or not c.user.gold:\n            ps.append(SponsorshipBox())\n\n        if (isinstance(c.site, Filtered) and not\n            (isinstance(c.site, AllSR) and not c.user.gold)):\n            ps.append(FilteredInfoBar())\n        elif isinstance(c.site, AllSR):\n            ps.append(AllInfoBar(c.site, c.user))\n        elif isinstance(c.site, ModSR):\n            ps.append(ModSRInfoBar())\n\n        if isinstance(c.site, (MultiReddit, ModSR)):\n            srs = Subreddit._byID(c.site.sr_ids, data=True,\n                                  return_dict=False, stale=True)\n\n            if (srs and c.user_is_loggedin and\n                    (c.user_is_admin or c.site.is_moderator(c.user))):\n                ps.append(self.sr_admin_menu())\n\n            if isinstance(c.site, LabeledMulti):\n                ps.append(MultiInfoBar(c.site, srs, c.user))\n                c.js_preload.set_wrapped(\n                    '/api/multi/%s' % c.site.path.lstrip('/'), c.site)\n            elif srs:\n                if isinstance(c.site, ModSR):\n                    box = SubscriptionBox(srs, multi_text=strings.mod_multi)\n                else:\n                    box = SubscriptionBox(srs)\n                ps.append(SideContentBox(_('these subreddits'), [box]))\n\n        user_banned = c.user_is_loggedin and c.site.is_banned(c.user)\n\n        if (self.submit_box\n                and (c.user_is_loggedin or not g.read_only_mode)\n                and not user_banned):\n            if (not isinstance(c.site, FakeSubreddit)\n                    and c.site.type in (\"archived\",\n                                        \"restricted\",\n                                        \"gold_restricted\")\n                    and not (c.user_is_loggedin\n                             and c.site.can_submit(c.user))):\n                if c.site.type == \"archived\":\n                    subtitle = _('this subreddit is archived '\n                                 'and no longer accepting submissions.')\n                    ps.append(SideBox(title=_('Submissions disabled'),\n                                      css_class=\"submit\",\n                                      disabled=True,\n                                      subtitles=[subtitle],\n                                      show_icon=False))\n                else:\n                    if c.site.type == 'restricted':\n                        subtitle = _('Only approved users may post in this '\n                                     'community.')\n                    elif c.site.type == 'gold_restricted':\n                        subtitle = _('Anyone can view or comment, but only '\n                                     'Reddit Gold members can post in this '\n                                     'community.')\n                    ps.append(SideBox(title=_('Submissions restricted'),\n                                      css_class=\"submit\",\n                                      disabled=True,\n                                      subtitles=[subtitle],\n                                      show_icon=False))\n            else:\n                fake_sub = isinstance(c.site, FakeSubreddit)\n                is_multi = isinstance(c.site, MultiReddit)\n                mod_link_override = mod_self_override = False\n\n                if isinstance(c.site, FakeSubreddit):\n                    submit_buttons = set((\"link\", \"self\"))\n                else:\n                    # we want to show submit buttons for logged-out users too\n                    # so we can't just use can_submit_link/text\n                    submit_buttons = c.site.allowed_types\n\n                    if c.user_is_loggedin:\n                        if (\"link\" not in submit_buttons and\n                                c.site.can_submit_link(c.user)):\n                            submit_buttons.add(\"link\")\n                            mod_link_override = True\n                        if (\"self\" not in submit_buttons and\n                                c.site.can_submit_text(c.user)):\n                            submit_buttons.add(\"self\")\n                            mod_self_override = True\n\n                if \"link\" in submit_buttons:\n                    css_class = \"submit submit-link\"\n                    if mod_link_override:\n                        css_class += \" mod-override\"\n                    data_attrs = {\n                        'type': 'subreddit',\n                        'event-action': 'submit',\n                        'event-detail': 'link',\n                    }\n                    ps.append(SideBox(title=c.site.submit_link_label or\n                                            strings.submit_link_label,\n                                      css_class=css_class,\n                                      link=\"/submit\",\n                                      sr_path=not fake_sub or is_multi,\n                                      data_attrs=data_attrs,\n                                      show_cover=True))\n                if \"self\" in submit_buttons:\n                    css_class = \"submit submit-text\"\n                    if mod_self_override:\n                        css_class += \" mod-override\"\n                    data_attrs = {\n                        'type': 'subreddit',\n                        'event-action': 'submit',\n                        'event-detail': 'self',\n                    }\n                    ps.append(SideBox(title=c.site.submit_text_label or\n                                            strings.submit_text_label,\n                                      css_class=css_class,\n                                      link=\"/submit?selftext=true\",\n                                      sr_path=not fake_sub or is_multi,\n                                      data_attrs=data_attrs,\n                                      show_cover=True))\n\n        no_ads_yet = True\n        user_disabled_ads = c.user.gold and c.user.pref_hide_ads\n        show_adbox = c.site.allow_ads and not (user_disabled_ads or g.disable_ads)\n\n        # don't show the subreddit info bar on cnames unless the option is set\n        if not isinstance(c.site, FakeSubreddit):\n            ps.append(SubredditInfoBar())\n            moderator = c.user_is_loggedin and (c.user_is_admin or\n                                          c.site.is_moderator(c.user))\n            wiki_moderator = c.user_is_loggedin and (\n                c.user_is_admin\n                or c.site.is_moderator_with_perms(c.user, 'wiki'))\n            if self.show_wiki_actions:\n                menu = self.wiki_actions_menu(moderator=wiki_moderator)\n                ps.append(menu)\n            if moderator:\n                ps.append(self.sr_admin_menu())\n            if show_adbox:\n                ps.append(Ads())\n            no_ads_yet = False\n        elif self.show_wiki_actions:\n            ps.append(self.wiki_actions_menu())\n\n        if self.create_reddit_box and c.user_is_loggedin:\n            if (c.user._age.days >= g.min_membership_create_community and\n                    c.user.can_create_subreddit):\n                subtitles = get_funny_translated_string(\"create_subreddit\", 2)\n                data_attrs = {'event-action': 'createsubreddit'}\n                ps.append(SideBox(_('Create your own subreddit'),\n                           '/subreddits/create', 'create',\n                           subtitles=subtitles,\n                           data_attrs=data_attrs,\n                           show_cover = True))\n\n        if c.default_sr:\n            hook = hooks.get_hook('home.add_sidebox')\n            extra_sidebox = hook.call_until_return()\n            if extra_sidebox:\n                ps.append(extra_sidebox)\n\n        if not isinstance(c.site, FakeSubreddit):\n            moderator_ids = c.site.moderator_ids()\n            if moderator_ids:\n                sidebar_list_length = 10\n                allow_stale = (not c.user_is_loggedin or\n                    c.user._id not in moderator_ids)\n                moderators = Account._byID(\n                    moderator_ids[:sidebar_list_length], data=True,\n                    return_dict=False, stale=allow_stale)\n                num_not_shown = len(moderator_ids) - sidebar_list_length\n\n                if num_not_shown > 0:\n                    more_text = _(\"...and %d more\") % (num_not_shown)\n                else:\n                    more_text = _(\"about moderation team\")\n\n                is_admin_sr = '/r/%s' % c.site.name == g.admin_message_acct\n\n                if is_admin_sr:\n                    label = _('message the admins')\n                else:\n                    label = _('message the moderators')\n\n                wrapped_moderators = [WrappedUser(mod) for mod in moderators\n                    if not mod._deleted]\n                helplink = HelpLink(\n                    \"/message/compose?to=%%2Fr%%2F%s\" % c.site.name,\n                    label,\n                    access_required=not is_admin_sr,\n                    data_attrs={\n                        'type': 'subreddit',\n                        'fullname': c.site._fullname,\n                        'event-action': 'compose',\n                    })\n\n                mod_href = c.site.path + 'about/moderators'\n                ps.append(SideContentBox(_('moderators'),\n                                         wrapped_moderators,\n                                         helplink = helplink,\n                                         more_href = mod_href,\n                                         more_text = more_text))\n\n        if no_ads_yet and show_adbox:\n            ps.append(Ads())\n            if g.live_config[\"gold_revenue_goal\"]:\n                ps.append(Goldvertisement())\n\n        if c.user.pref_clickgadget and c.recent_clicks:\n            ps.append(SideContentBox(_(\"Recently viewed links\"),\n                                     [ClickGadget(c.recent_clicks)]))\n\n        if c.user_is_loggedin:\n            activity_link = AccountActivityBox()\n            ps.append(activity_link)\n\n        return ps\n\n    def render(self, *a, **kw):\n        \"\"\"Overrides default Templated.render with two additions\n           * support for rendering API requests with proper wrapping\n           * support for space compression of the result\n        In adition, unlike Templated.render, the result is in the form of a pylons\n        Response object with it's content set.\n        \"\"\"\n        if c.bare_content:\n            res = self.content().render()\n        else:\n            res = Templated.render(self, *a, **kw)\n\n        return responsive(res, self.space_compress)\n\n    def corner_buttons(self):\n        \"\"\"set up for buttons in upper right corner of main page.\"\"\"\n        buttons = []\n        if c.user_is_loggedin:\n            if c.user.name in g.admins:\n                if c.user_is_admin:\n                    buttons += [OffsiteButton(\n                        _(\"turn admin off\"),\n                        dest=\"%s/adminoff?dest=%s\" %\n                            (g.https_endpoint, quote(request.fullpath)),\n                        target = \"_self\",\n                    )]\n                else:\n                    buttons += [OffsiteButton(\n                        _(\"turn admin on\"),\n                        dest=\"%s/adminon?dest=%s\" %\n                            (g.https_endpoint, quote(request.fullpath)),\n                        target = \"_self\",\n                    )]\n            buttons += [NamedButton(\"prefs\", False,\n                                  css_class = \"pref-lang\")]\n        else:\n            lang = c.lang.split('-')[0] if c.lang else ''\n            lang_name = g.lang_name.get(lang) or [lang, '']\n            lang_name = \"\".join(lang_name)\n            buttons += [JsButton(lang_name,\n                                 onclick = \"return showlang();\",\n                                 css_class = \"pref-lang\")]\n        return NavMenu(buttons, base_path = \"/\", type = \"flatlist\")\n\n    def build_toolbars(self):\n        \"\"\"Sets the layout of the navigation topbar on a Reddit.  The result\n        is a list of menus which will be rendered in order and\n        displayed at the top of the Reddit.\"\"\"\n        if c.site == Friends:\n            main_buttons = [NamedButton('new', dest='', aliases=['/hot']),\n                            NamedButton('comments'),\n                            NamedButton('gilded'),\n                            ]\n        else:\n            main_buttons = [NamedButton('hot', dest='', aliases=['/hot']),\n                            NamedButton('new'),\n                            NamedButton('rising'),\n                            NamedButton('controversial'),\n                            NamedButton('top'),\n                            ]\n\n            if c.site.allow_gilding:\n                main_buttons.append(NamedButton('gilded',\n                                                aliases=['/comments/gilded']))\n\n            mod = False\n            if c.user_is_loggedin:\n                mod = bool(c.user_is_admin\n                           or c.site.is_moderator_with_perms(c.user, 'wiki'))\n            if c.site._should_wiki and (c.site.wikimode != 'disabled' or mod):\n                if not g.disable_wiki:\n                    main_buttons.append(NavButton('wiki', 'wiki'))\n\n            if (isinstance(c.site, (Subreddit, DefaultSR, MultiReddit)) and\n                    c.site.allow_ads):\n                main_buttons.append(NavButton(menu.promoted, 'ads'))\n\n        more_buttons = []\n\n        if c.user_is_loggedin and c.site.allow_ads:\n            if c.user_is_sponsor:\n                sponsor_button = NavButton(\n                    menu.sponsor, dest='/sponsor', sr_path=False)\n                more_buttons.append(sponsor_button)\n            elif c.user.pref_show_promote:\n                more_buttons.append(NavButton(menu.promote, 'promoted', False))\n\n        #if there's only one button in the dropdown, get rid of the dropdown\n        if len(more_buttons) == 1:\n            main_buttons.append(more_buttons[0])\n            more_buttons = []\n\n        toolbar = [NavMenu(main_buttons, type='tabmenu')]\n        if more_buttons:\n            toolbar.append(NavMenu(more_buttons, title=menu.more, type='tabdrop'))\n\n        if not isinstance(c.site, DefaultSR):\n            func = 'subreddit'\n            if isinstance(c.site, DomainSR):\n                func = 'domain'\n            toolbar.insert(0, PageNameNav(func))\n\n        return toolbar\n\n    def __repr__(self):\n        return \"<Reddit>\"\n\n    @staticmethod\n    def content_stack(panes, css_class = None):\n        \"\"\"Helper method for reordering the content stack.\"\"\"\n        return PaneStack(filter(None, panes), css_class = css_class)\n\n    def content(self):\n        \"\"\"returns a Wrapped (or renderable) item for the main content div.\"\"\"\n        if self.newsletterbar:\n            self.welcomebar = None\n\n        return self.content_stack((\n            self.welcomebar,\n            self.newsletterbar,\n            self.infobar,\n            self.locationbar,\n            self.mobilewebredirectbar,\n            self.nav_menu,\n            self._content,\n        ))\n\n    def build_popup_panes(self):\n        panes = []\n\n        panes.append(Popup('archived-popup', ArchivedInterstitial()))\n\n        if self.show_timeout_modal:\n            popup_content = InTimeoutInterstitial(\n                timeout_days_remaining=self.timeout_days_remaining,\n                hide_message=True,\n            )\n            panes.append(Popup('access-popup', popup_content))\n\n        return HtmlPaneStack(panes)\n\n    def is_gold_page(self):\n        return \"gold-page-ga-tracking\" in self.supplied_page_classes\n\n    def page_classes(self):\n        classes = set()\n\n        if c.user_is_loggedin:\n            classes.add('loggedin')\n            if not isinstance(c.site, FakeSubreddit):\n                if c.site.is_subscriber(c.user):\n                    classes.add('subscriber')\n                if c.site.is_contributor(c.user):\n                    classes.add('contributor')\n            if c.site.is_moderator(c.user):\n                classes.add('moderator')\n            if c.user.gold:\n                classes.add('gold')\n            if c.user.pref_highlight_controversial:\n                classes.add('show-controversial')\n\n        if c.user_is_admin:\n            if not isinstance(c.site, FakeSubreddit) and c.site._spam:\n                classes.add(\"banned\")\n\n        if isinstance(c.site, MultiReddit):\n            classes.add('multi-page')\n\n        if self.show_chooser:\n            classes.add('with-listing-chooser')\n            if c.user.pref_collapse_left_bar:\n                classes.add('listing-chooser-collapsed')\n\n        if c.user_is_loggedin and c.user.pref_compress:\n            classes.add('compressed-display')\n\n        if getattr(c.site, 'type', None) == 'gold_only':\n            classes.add('gold-only')\n\n        if getattr(c.site, 'quarantine', False):\n            classes.add('quarantine')\n\n        if self.extra_page_classes:\n            classes.update(self.extra_page_classes)\n        if self.supplied_page_classes:\n            classes.update(self.supplied_page_classes)\n\n        return classes\n\n\nclass DebugFooter(Templated):\n    def __init__(self):\n        if request.via_cdn:\n            cdn_geoinfo = g.cdn_provider.get_client_location(request.environ)\n            if cdn_geoinfo:\n                c.location_info = \"country code: %s\" % cdn_geoinfo\n        Templated.__init__(self)\n\n\nclass AccountActivityBox(Templated):\n    def __init__(self):\n        super(AccountActivityBox, self).__init__()\n\n\nclass RedditFooter(CachedTemplate):\n    def cachable_attrs(self):\n        return [('path', request.path),\n                ('buttons', [[(x.title, x.path) for x in y] for y in self.nav])]\n\n    def __init__(self):\n        self.nav = [\n            NavMenu([\n                    NamedButton(\"blog\", False, dest=\"/blog\"),\n                    OffsiteButton(\"about\", \"https://about.reddit.com/\"),\n                    NamedButton(\"source_code\", False, dest=\"/code\"),\n                    NamedButton(\"advertising\", False),\n                    NamedButton(\"jobs\", False),\n                ],\n                title = _(\"about\"),\n                type = \"flat_vert\",\n                separator = \"\"),\n\n            NavMenu([\n                    NamedButton(\"rules\", False),\n                    OffsiteButton(_(\"FAQ\"), \"https://reddit.zendesk.com\"),\n                    NamedButton(\"wiki\", False),\n                    NamedButton(\"reddiquette\", False, dest=\"/wiki/reddiquette\"),\n                    NamedButton(\"transparency\", False, dest=\"/wiki/transparency\"),\n                    NamedButton(\"contact\", False),\n                ],\n                title = _(\"help\"),\n                type = \"flat_vert\",\n                separator = \"\"),\n\n            NavMenu([\n                    OffsiteButton(_(\"Reddit for iPhone\"),\n                        \"https://itunes.apple.com/us/app/reddit-the-official-app/id1064216828?mt=8\"),\n                    OffsiteButton(_(\"Reddit for Android\"),\n                        \"https://play.google.com/store/apps/details?id=com.reddit.frontpage\"),\n                    OffsiteButton(_(\"mobile website\"), \"https://m.reddit.com\"),\n                    NamedButton(\"buttons\", False),\n                ],\n                title = _(\"apps & tools\"),\n                type = \"flat_vert\",\n                separator = \"\"),\n\n            NavMenu([\n                    NamedButton(\"gold\", False, dest=\"/gold/about\", css_class=\"buygold\"),\n                    OffsiteButton(_(\"redditgifts\"), \"//redditgifts.com\"),\n                ],\n                title = _(\"<3\"),\n                type = \"flat_vert\",\n                separator = \"\")\n        ]\n        CachedTemplate.__init__(self)\n\nclass ClickGadget(Templated):\n    def __init__(self, links, *a, **kw):\n        self.links = links\n        self.content = ''\n        if c.user_is_loggedin and self.links:\n            self.content = self.make_content()\n        Templated.__init__(self, *a, **kw)\n\n    def make_content(self):\n        #this will disable the hardcoded widget styles\n        request.GET[\"style\"] = \"off\"\n        wrapper = default_thing_wrapper(embed_voting_style = 'votable',\n                                        style = \"htmllite\")\n        content = wrap_links(self.links, wrapper = wrapper)\n\n        return content.render(style = \"htmllite\")\n\n\nclass LoginFormWide(CachedTemplate):\n    \"\"\"generates a login form suitable for the 300px rightbox.\"\"\"\n    pass\n\n\n\nclass SubredditInfoBar(CachedTemplate):\n    \"\"\"When not on Default, renders a sidebox which gives info about\n    the current reddit, including links to the moderator and\n    contributor pages, as well as links to the banning page if the\n    current user is a moderator.\"\"\"\n\n    def __init__(self, site = None):\n        site = site or c.site\n\n        # hackity hack. do i need to add all the others props?\n        self.sr = list(wrap_links(site))[0]\n        self.description_usertext = UserText(self.sr, self.sr.description)\n\n        # we want to cache on the number of subscribers\n        self.subscribers = self.sr._ups\n\n        # so the menus cache properly\n        self.path = request.path\n\n        self.active_visitors = self.sr.count_activity()\n\n        if c.user_is_loggedin and c.user.pref_show_flair:\n            self.flair_prefs = FlairPrefs()\n        else:\n            self.flair_prefs = None\n\n        self.sr_style_toggle = False\n        self.use_subreddit_style = True\n\n        self.quarantine = self.sr.quarantine\n\n        has_custom_stylesheet = (self.sr.stylesheet_url or\n            self.sr.stylesheet_url_https or self.sr.stylesheet_url_http)\n        if (c.user_is_loggedin and\n                (has_custom_stylesheet or self.sr.header) and\n                feature.is_enabled('stylesheets_everywhere')):\n            # defaults to c.user.pref_show_stylesheets if a match doesn't exist\n            self.sr_style_toggle = True\n            self.use_subreddit_style = c.user.use_subreddit_style(c.site)\n\n        if c.user_is_admin and hasattr(self.sr, 'ban_info'):\n            self.sr_ban_info = self.sr.ban_info\n\n        CachedTemplate.__init__(self)\n\n    @property\n    def creator_text(self):\n        if self.sr.author:\n            if self.sr.is_moderator(self.sr.author) or self.sr.author._deleted:\n                return WrappedUser(self.sr.author).render()\n            else:\n                return self.sr.author.name\n        return None\n\n\nclass SponsorshipBox(Templated):\n    pass\n\n\nclass HelpLink(Templated):\n    def __init__(self, url, label, access_required=False, data_attrs={}):\n        Templated.__init__(self, url=url, label=label,\n                           access_required=access_required,\n                           data_attrs=data_attrs)\n\n\nclass SideContentBox(Templated):\n    def __init__(self, title, content, helplink=None, _id=None, extra_class=None,\n                 more_href=None, more_text=\"more\", collapsible=False):\n        Templated.__init__(self, title=title, helplink = helplink,\n                           content=content, _id=_id, extra_class=extra_class,\n                           more_href = more_href, more_text = more_text,\n                           collapsible=collapsible)\n\nclass SideBox(CachedTemplate):\n    \"\"\"\n    Generic sidebox used to generate the 'submit' and 'create a reddit' boxes.\n    \"\"\"\n    def __init__(self, title, link=None, css_class='', subtitles = [],\n                 show_cover = False, sr_path = False,\n                 disabled=False, show_icon=True, target='_top', data_attrs={}):\n        CachedTemplate.__init__(self, link = link, target = target,\n                                title = title, css_class = css_class,\n                                sr_path = sr_path, subtitles = subtitles,\n                                show_cover = show_cover,\n                                disabled=disabled, show_icon=show_icon,\n                                data_attrs=data_attrs)\n\n\nclass PrefsPage(Reddit):\n    \"\"\"container for pages accessible via /prefs.  No extension handling.\"\"\"\n\n    extension_handling = False\n\n    def __init__(self, show_sidebar = False, title=None, *a, **kw):\n        title = title or \"%s (%s)\" % (_(\"preferences\"), c.site.name.strip(' '))\n        Reddit.__init__(self, show_sidebar = show_sidebar,\n                        title=title,\n                        *a, **kw)\n\n    def build_toolbars(self):\n        buttons = [NavButton(menu.options, ''),\n                   NamedButton('apps')]\n\n        if c.user.pref_private_feeds:\n            buttons.append(NamedButton('feeds'))\n\n        buttons.extend([\n            NamedButton('friends'),\n            NamedButton('blocked'),\n            NamedButton('update'),\n        ])\n\n        if c.user.name in g.admins:\n            buttons += [NamedButton('security')]\n\n        buttons += [NamedButton('deactivate')]\n\n        return [PageNameNav('nomenu', title = _(\"preferences\")),\n                NavMenu(buttons, base_path = \"/prefs\", type=\"tabmenu\")]\n\n\nclass PrefOptions(Templated):\n    \"\"\"Preference form for updating language and display options\"\"\"\n    def __init__(self, done=False, error_style_override=None, generic_error=None):\n        themes = []\n        use_other_theme = True\n        if feature.is_enabled('stylesheets_everywhere'):\n            for theme in StylesheetsEverywhere.get_all():\n                if theme.is_enabled:\n                    themes.append(theme)\n                if theme.id == c.user.pref_default_theme_sr:\n                    use_other_theme = False\n                    theme.checked = True\n\n        feature_autoexpand_media_previews = feature.is_enabled(\"autoexpand_media_previews\")\n        Templated.__init__(self, done=done,\n                error_style_override=error_style_override,\n                feature_autoexpand_media_previews=feature_autoexpand_media_previews,\n                generic_error=generic_error, themes=themes, use_other_theme=use_other_theme)\n\n\nclass PrefFeeds(Templated):\n    pass\n\nclass PrefSecurity(Templated):\n    pass\n\n\nre_promoted = re.compile(r\"/promoted.*\", re.I)\n\nclass PrefUpdate(Templated):\n    \"\"\"Preference form for updating email address and passwords\"\"\"\n    def __init__(self, email=True, password=True, verify=False, dest=None, subscribe=False):\n        is_promoted = dest and re.match(re_promoted, urlparse(dest).path) != None\n        self.email = email\n        self.password = password\n        self.verify = verify\n        self.dest = dest\n        self.subscribe = subscribe or is_promoted\n        Templated.__init__(self)\n\nclass PrefApps(Templated):\n    \"\"\"Preference form for managing authorized third-party applications.\"\"\"\n\n    def __init__(self, my_apps, developed_apps):\n        self.my_apps = my_apps\n        self.developed_apps = developed_apps\n        super(PrefApps, self).__init__()\n\n    def render_developed_app(self, app, collapsed):\n        base_template = self.template()\n        developed_app_fn = base_template.get_def('developed_app')\n        res = developed_app_fn.render(app, collapsed=collapsed)\n        return spaceCompress(res)\n\n    def render_editable_developer(self, app, dev):\n        base_template = self.template()\n        editable_developer_fn = base_template.get_def('editable_developer')\n        res = editable_developer_fn.render(app, dev)\n        return spaceCompress(res)\n\n\nclass PrefDeactivate(Templated):\n    \"\"\"Preference form for deactivating a user's own account.\"\"\"\n    def __init__(self):\n        self.has_paypal_subscription = c.user.has_paypal_subscription\n        if self.has_paypal_subscription:\n            self.paypal_subscr_id = c.user.gold_subscr_id\n            self.paypal_url = paypal_subscription_url()\n        Templated.__init__(self)\n\n\nclass MessagePage(Reddit):\n    \"\"\"Defines the content for /message/*\"\"\"\n    def __init__(self, *a, **kw):\n        if not kw.has_key('show_sidebar'):\n            kw['show_sidebar'] = False\n\n        source = kw.pop(\"source\", None)\n\n        Reddit.__init__(self, *a, **kw)\n\n        if is_api():\n            self.replybox = None\n        else:\n            self.replybox = UserText(\n                item=None,\n                creating=True,\n                post_form='comment',\n                display=False,\n                cloneable=True,\n                source=source,\n            )\n\n    def content(self):\n        return self.content_stack((self.replybox,\n                                   self.infobar,\n                                   self.nav_menu,\n                                   self._content))\n\n    def build_toolbars(self):\n        if isinstance(c.site, MultiReddit):\n            mod_srs = c.site.srs_with_perms(c.user, \"mail\")\n            sr_path = bool(mod_srs)\n        elif (not isinstance(c.site, FakeSubreddit) and\n                c.site.is_moderator_with_perms(c.user, \"mail\")):\n            sr_path = True\n        else:\n            sr_path = False\n\n        buttons =  [NamedButton('compose', sr_path=sr_path),\n                    NamedButton('inbox', aliases = [\"/message/comments\",\n                                                    \"/message/unread\",\n                                                    \"/message/messages\",\n                                                    \"/message/mentions\",\n                                                    \"/message/selfreply\"],\n                                sr_path = False),\n                    NamedButton('sent', sr_path = False)]\n        if c.user_is_loggedin and c.user.is_moderator_somewhere:\n            buttons.append(ModeratorMailButton(menu.modmail, \"moderator\",\n                                               sr_path = False))\n        if not c.default_sr:\n            buttons.append(ModeratorMailButton(\n                _(\"%(site)s mail\") % {'site': c.site.name}, \"moderator\",\n                aliases = [\"/about/message/inbox\",\n                           \"/about/message/unread\"]))\n        return [PageNameNav('nomenu', title = _(\"message\")),\n                NavMenu(buttons, base_path = \"/message\", type=\"tabmenu\")]\n\nclass MessageCompose(Templated):\n    \"\"\"Compose message form.\"\"\"\n    def __init__(self, to='', subject='', message='', captcha=None,\n                 admin_check=True, restrict_recipient=False):\n        from r2.models.admintools import admintools\n\n        if admin_check:\n            self.admins = admintools.admin_list()\n\n        Templated.__init__(self, to=to, subject=subject, message=message,\n                           captcha=captcha, admin_check=admin_check,\n                           restrict_recipient=restrict_recipient)\n\n\nclass ModeratorMessageCompose(MessageCompose):\n    def __init__(self, mod_srs, only_as_subreddit=False, **kw):\n        self.mod_srs = sorted(mod_srs, key=lambda sr: sr.name.lower())\n        self.only_as_subreddit = only_as_subreddit\n        MessageCompose.__init__(self, admin_check=False, **kw)\n\n\nclass BoringPage(Reddit):\n    \"\"\"parent class For rendering all sorts of uninteresting,\n    sortless, navless form-centric pages.  The top navmenu is\n    populated only with the text provided with pagename and the page\n    title is 'reddit.com: pagename'\"\"\"\n\n    extension_handling= False\n\n    def __init__(self, pagename, css_class=None, **context):\n        self.pagename = pagename\n        name = c.site.name or g.default_sr\n        if css_class:\n            self.css_class = css_class\n        if \"title\" not in context:\n            context['title'] = \"%s: %s\" % (name, pagename)\n\n        Reddit.__init__(self, **context)\n\n    def build_toolbars(self):\n        if not isinstance(c.site, DefaultSR):\n            return [PageNameNav('subreddit', title = self.pagename)]\n        else:\n            return [PageNameNav('nomenu', title = self.pagename)]\n\nclass HelpPage(BoringPage):\n    def build_toolbars(self):\n        return [PageNameNav('help', title = self.pagename)]\n\nclass FormPage(BoringPage):\n    create_reddit_box  = False\n    submit_box         = False\n    \"\"\"intended for rendering forms with no rightbox needed or wanted\"\"\"\n    def __init__(self, pagename, show_sidebar = False, *a, **kw):\n        BoringPage.__init__(self, pagename,  show_sidebar = show_sidebar,\n                            *a, **kw)\n\nclass LoginPage(BoringPage):\n    enable_login_cover = False\n    short_title = \"log in\"\n\n    \"\"\"a boring page which provides the Login/register form\"\"\"\n    def __init__(self, **context):\n        self.dest = context.get('dest', '')\n        context['loginbox'] = False\n        context['show_sidebar'] = False\n        context['page_classes'] = ['login-page']\n\n        if c.render_style == \"compact\":\n            title = self.short_title\n        else:\n            title = _(\"sign up or log in\")\n\n        BoringPage.__init__(self, title, **context)\n\n        if self.dest:\n            u = UrlParser(self.dest)\n            # Display a preview message for OAuth2 client authorizations\n            if u.path in ['/api/v1/authorize', '/api/v1/authorize.compact']:\n                client_id = u.query_dict.get(\"client_id\")\n                self.client = client_id and OAuth2Client.get_token(client_id)\n                if self.client:\n                    self.infobar = ClientInfoBar(self.client,\n                                                 strings.oauth_login_msg)\n                else:\n                    self.infobar = None\n\n    def content(self):\n        kw = {}\n        for x in ('user_login', 'user_reg'):\n            kw[x] = getattr(self, x) if hasattr(self, x) else ''\n        login_content = self.login_template(dest = self.dest, **kw)\n        return self.content_stack((self.infobar, login_content))\n\n    @classmethod\n    def login_template(cls, **kw):\n        return Login(**kw)\n\nclass RegisterPage(LoginPage):\n    short_title = \"sign up\"\n    @classmethod\n    def login_template(cls, **kw):\n        return Register(**kw)\n\n\nclass Login(Templated):\n    \"\"\"The two-unit login and register form.\"\"\"\n    def __init__(self, user_reg = '', user_login = '', dest='', is_popup=False):\n        Templated.__init__(self, user_reg = user_reg, user_login = user_login,\n                           dest = dest, captcha = Captcha(),\n                           is_popup=is_popup,\n                           registration_info=RegistrationInfo())\n\nclass Register(Login):\n    pass\n\n\nclass RegistrationInfo(Templated):\n    def __init__(self):\n        html = unsafe(self.get_registration_info_html())\n        Templated.__init__(self, content_html=html)\n\n    @classmethod\n    @memoize('registration_info_html', stale=True, time=10*60)\n    def get_registration_info_html(cls):\n        try:\n            wp = WikiPage.get(Frontpage, g.wiki_page_registration_info)\n        except tdb_cassandra.NotFound:\n            return ''\n        else:\n            return wikimarkdown(wp.content, include_toc=False, target='_blank')\n\n\nclass OAuth2AuthorizationPage(BoringPage):\n    show_mobilewebredirectbar = False\n\n    def __init__(self, client, redirect_uri, scope, state, duration,\n                 response_type):\n        if duration == \"permanent\":\n            expiration = None\n        else:\n            expiration = (\n                datetime.datetime.now(g.tz)\n                + datetime.timedelta(seconds=OAuth2AccessToken._ttl + 1))\n        content = OAuth2Authorization(client=client,\n                                      redirect_uri=redirect_uri,\n                                      scope=scope,\n                                      state=state,\n                                      duration=duration,\n                                      expiration=expiration,\n                                      response_type=response_type,\n                                      )\n        BoringPage.__init__(self, _(\"request for permission\"),\n                            show_sidebar=False, content=content,\n                            short_title=_(\"permission\"))\n\nclass OAuth2Authorization(Templated):\n    pass\n\nclass SearchPage(BoringPage):\n    \"\"\"Search results page\"\"\"\n    searchbox = False\n    extra_page_classes = ['search-page']\n\n    def __init__(self, pagename, prev_search,\n                 search_params={},\n                 simple=False, restrict_sr=False, site=None,\n                 syntax=None, converted_data=None, facets={}, sort=None,\n                 recent=None, subreddits=None,\n                 *a, **kw):\n        if not (feature.is_enabled('legacy_search') or c.user.pref_legacy_search):\n            self.extra_page_classes = self.extra_page_classes + ['combined-search-page']\n        self.searchbar = SearchBar(prev_search=prev_search,\n                                   search_params=search_params,\n                                   site=site,\n                                   simple=simple, restrict_sr=restrict_sr,\n                                   syntax=syntax, converted_data=converted_data)\n        self.subreddits = subreddits\n\n        # generate the over18 redirect url for the current search if needed\n        if kw['nav_menus'] and not c.over18 and feature.is_enabled('safe_search'):\n            u = UrlParser(add_sr('/search'))\n            if prev_search:\n                u.update_query(q=prev_search)\n            if restrict_sr:\n                u.update_query(restrict_sr='on')\n            u.update_query(**search_params)\n            u.update_query(over18='yes')\n            over18_url = u.unparse()\n            kw['nav_menus'].append(MenuLink(title=_('enable NSFW results'),\n                                            url=over18_url))\n\n        self.sr_facets = SubredditFacets(prev_search=prev_search, facets=facets,\n                                         sort=sort, recent=recent)\n        BoringPage.__init__(self, pagename, robots='noindex', *a, **kw)\n\n    def content(self):\n        if feature.is_enabled('legacy_search') or c.user.pref_legacy_search:\n            return self.content_stack((self.searchbar, self.sr_facets, self.infobar,\n                                   self.nav_menu, self.subreddits, self._content))\n\n        return self.content_stack((self.searchbar, self.infobar,\n                                   self.subreddits, self._content,\n                                   self.sr_facets))\n\n\nclass MenuLink(Templated):\n    pass\n\n\nclass TakedownPage(BoringPage):\n    def __init__(self, link):\n        BoringPage.__init__(self, getattr(link, \"takedown_title\", _(\"bummer\")),\n                            content = TakedownPane(link))\n\n    def render(self, *a, **kw):\n        response = BoringPage.render(self, *a, **kw)\n        return response\n\n\nclass TakedownPane(Templated):\n    def __init__(self, link, *a, **kw):\n        self.link = link\n        self.explanation = getattr(self.link, \"explanation\",\n                                   _(\"this page is no longer available due to a copyright claim.\"))\n        Templated.__init__(self, *a, **kw)\n\n\nclass CommentVisitsBox(Templated):\n    def __init__(self, visits, *a, **kw):\n        self.visits = list(reversed(visits))\n        Templated.__init__(self, *a, **kw)\n\nclass LinkInfoPage(Reddit):\n    \"\"\"Renders the varied /info pages for a link.  The Link object is\n    passed via the link argument and the content passed to this class\n    will be rendered after a one-element listing consisting of that\n    link object.\n\n    In addition, the rendering is reordered so that any nav_menus\n    passed to this class will also be rendered underneath the rendered\n    Link.\n    \"\"\"\n\n    create_reddit_box = False\n    extra_page_classes = ['single-page']\n    metadata_image_widths = (320, 216)\n\n    def __init__(self, link = None, comment = None, disable_comments=False,\n                 link_title = '', subtitle = None, num_duplicates = None,\n                 show_promote_button=False, sr_detail=False,\n                 campaign_fullname=promote.NO_CAMPAIGN, click_url=None,\n                 *a, **kw):\n\n        c.permalink_page = True\n        expand_children = kw.get(\"expand_children\", not bool(comment))\n\n        wrapper = default_thing_wrapper(expand_children=expand_children)\n\n\n        # link_listing will be the one-element listing at the top\n        self.link_listing = wrap_links(link, wrapper=wrapper, sr_detail=sr_detail)\n        things = self.link_listing.things\n        self.link = things[0]\n\n        # add click tracker\n        self.link.campaign = campaign_fullname\n\n        # don't need to track clicks on self posts since they've\n        # already been clicked at this point\n        if not self.link.is_self:\n            promote.add_trackers(\n                things,\n                c.site,\n                adserver_click_urls={campaign_fullname: click_url},\n            )\n\n        self.disable_comments = disable_comments\n\n        if promote.is_promo(self.link) and not promote.is_promoted(self.link):\n            self.link.votable = False\n\n        link_title = ((self.link.title) if hasattr(self.link, 'title') else '')\n\n        # defaults whether or not there is a comment\n        params = {'title':_force_unicode(link_title), 'site' : c.site.name}\n        title = strings.link_info_title % params\n        short_description = None\n        if link and link.selftext and not (link._spam or link._deleted):\n            short_description = _truncate(link.selftext.strip(), MAX_DESCRIPTION_LENGTH)\n        # only modify the title if the comment/author are neither deleted nor spam\n        if comment and not comment._deleted and not comment._spam:\n            author = Account._byID(comment.author_id, data=True)\n\n            if not author._deleted and not author._spam:\n                params = {'author' : author.name, 'title' : _force_unicode(link_title)}\n                title = strings.permalink_title % params\n                short_description = _truncate(comment.body.strip(), MAX_DESCRIPTION_LENGTH) if comment.body else None\n\n        self.subtitle = subtitle\n\n        if hasattr(self.link, \"shortlink\"):\n            self.shortlink = self.link.shortlink\n\n        self.og_data = self._build_og_data(\n            _force_unicode(link_title),\n            short_description,\n        )\n\n        self.twitter_card = self._build_twitter_card_data(\n            _force_unicode(link_title),\n            short_description,\n        )\n        hook = hooks.get_hook('comments_page.twitter_card')\n        hook.call(tags=self.twitter_card, sr_name=c.site.name,\n                  id36=self.link._id36)\n\n        if hasattr(self.link, \"dart_keyword\"):\n            c.custom_dart_keyword = self.link.dart_keyword\n\n        # if we're already looking at the 'duplicates' page, we can\n        # avoid doing this lookup twice\n        if num_duplicates is None:\n            builder = url_links_builder(self.link.url,\n                                        exclude=self.link._fullname,\n                                        public_srs_only=True)\n            self.num_duplicates = len(builder.get_items()[0])\n        else:\n            self.num_duplicates = num_duplicates\n\n        self.show_promote_button = show_promote_button\n        if link._deleted or link._spam:\n            robots = \"noindex,nofollow\"\n        elif comment:\n            # We don't want crawlers to index the comment permalink pages.\n            robots = \"noindex\"\n        else:\n            robots = None\n\n        if 'extra_js_config' not in kw:\n            kw['extra_js_config'] = {}\n\n        kw['extra_js_config'].update({\n            \"cur_link\": link._fullname,\n        });\n\n        if c.can_embed:\n            from r2.lib import embeds\n            kw['extra_js_config'].update({\n                \"embed_inject_template\": websafe(embeds.get_inject_template()),\n            })\n\n        Reddit.__init__(self, title = title, short_description=short_description, robots=robots, *a, **kw)\n\n    def _build_og_data(self, link_title, meta_description):\n        sr_fragment = \"/r/\" + c.site.name if not c.default_sr else get_domain()\n        data = {\n            \"site_name\": \"reddit\",\n            \"title\": u\"%s • %s\" % (link_title, sr_fragment),\n            \"description\": self._build_og_description(meta_description),\n            \"ttl\": \"600\",  # re-fetch frequently to update vote/comment count\n        }\n        if not self.link.nsfw:\n            image_data = self._build_og_image()\n            for key, value in image_data.iteritems():\n                # Although the spec[0] and their docs[1] say 'og:image' and\n                # 'og:image:url' are equivalent, Facebook doesn't actually take\n                # the thumbnail from the latter form.  Even if that gets fixed,\n                # it's likely the authors of other scrapers haven't read the\n                # spec in-depth, either, so we'll just keep on doing the more\n                # well-supported thing.\n                #\n                # [0]: http://ogp.me/#structured\n                # [1]: https://developers.facebook.com/docs/sharing/webmasters#images\n                if key == 'url':\n                    data['image'] = value\n                else:\n                    data[\"image:%s\" % key] = value\n\n        return data\n\n    def _build_og_image(self):\n        if self.link.media_object:\n            media_embed = media.get_media_embed(self.link.media_object)\n            if media_embed and media_embed.public_thumbnail_url:\n                return {\n                    'url': media_embed.public_thumbnail_url,\n                    'width': media_embed.width,\n                    'height': media_embed.height,\n                }\n\n        if self.link.url and url_is_embeddable_image(self.link.url):\n            return {'url': self.link.url}\n\n        preview_object = self.link.preview_image\n        if preview_object:\n            for width in self.metadata_image_widths:\n                try:\n                    return {\n                        'url': g.image_resizing_provider.resize_image(\n                                    preview_object, width),\n                        'width': width,\n                    }\n                except image_resizing.NotLargeEnough:\n                    pass\n\n        if self.link.has_thumbnail and self.link.thumbnail:\n            # This is really not a great thumbnail for facebook right now\n            # because it's so small, but it's better than nothing.\n            data = {'url': self.link.thumbnail}\n\n            # Some old posts don't have a recorded size for whatever reason, so\n            # let's just ignore dimensions for them.\n            if hasattr(self.link, 'thumbnail_size'):\n                width, height = self.link.thumbnail_size\n                data['width'] = width\n                data['height'] = height\n            return data\n\n        # Default to the reddit icon if we've got nothing else.  Force it to be\n        # absolute because not all scrapers handle relative protocols or paths\n        # well.\n        return {'url': static('icon.png', absolute=True)}\n\n    def _build_og_description(self, meta_description):\n        if self.link.selftext:\n            return meta_description\n\n        return strings.link_info_og_description % {\n            \"score\": self.link.score,\n            \"num_comments\": self.link.num_comments,\n        }\n\n    def _build_twitter_card_data(self, link_title, meta_description):\n        \"\"\"Build a set of data for Twitter's Summary Cards:\n        https://dev.twitter.com/cards/types/summary\n        https://dev.twitter.com/cards/markup\n        \"\"\"\n\n        # Twitter limits us to 70 characters for the title.  Even though it's\n        # at the end, we'd like to always show the whole subreddit name, so\n        # let's truncate the title while still ensuring the entire thing is\n        # under the limit.\n        sr_fragment = u\" • /r/\" + c.site.name if not c.default_sr else get_domain()\n        max_link_title_length = 70 - len(sr_fragment)\n\n        return {\n            \"site\": \"reddit\", # The twitter account of the site.\n            \"card\": \"summary\",\n            \"title\": _truncate(link_title, max_link_title_length) + sr_fragment\n            # Twitter will fall back to any defined OpenGraph attributes, so we\n            # don't need to define 'twitter:image' or 'twitter:description'.\n        }\n\n    def build_toolbars(self):\n        base_path = \"/%s/%s/\" % (self.link._id36, title_to_url(self.link.title))\n        base_path = _force_utf8(base_path)\n\n\n        def info_button(name, **fmt_args):\n            return NamedButton(name, dest = '/%s%s' % (name, base_path),\n                               aliases = ['/%s/%s' % (name, self.link._id36)],\n                               fmt_args = fmt_args)\n        buttons = []\n        if not self.disable_comments:\n            buttons.append(info_button('comments'))\n\n            if self.num_duplicates > 0:\n                buttons.append(info_button('duplicates', num=self.num_duplicates))\n\n        if self.show_promote_button:\n            buttons.append(NavButton(menu.promote, 'promoted', sr_path=False))\n\n        toolbar = [NavMenu(buttons, base_path = \"\", type=\"tabmenu\")]\n\n        if not isinstance(c.site, DefaultSR):\n            toolbar.insert(0, PageNameNav('subreddit'))\n\n        if c.user_is_admin:\n            from admin_pages import AdminLinkMenu\n            toolbar.append(AdminLinkMenu(self.link))\n\n        return toolbar\n\n    def content(self):\n        if self.disable_comments:\n            comment_area = InfoBar(message=_(\"comments disabled\"))\n        else:\n            panes = [self.nav_menu, self._content]\n\n            comment_area = PaneStack([\n                PaneStack(\n                    panes\n                )],\n                title=self.subtitle,\n                title_buttons=getattr(self, \"subtitle_buttons\", []),\n                css_class=\"commentarea\",\n            )\n\n        return self.content_stack((\n            self.infobar,\n            self.link_listing,\n            comment_area,\n            self.popup_panes,\n        ))\n\n    def build_popup_panes(self):\n        panes = super(LinkInfoPage, self).build_popup_panes()\n\n        if self.link.locked:\n            panes.append(Popup('locked-popup', LockedInterstitial()))\n\n        return panes\n\n\n    def rightbox(self):\n        rb = Reddit.rightbox(self)\n\n        if (c.site and not c.default_sr and c.render_style == 'html' and\n                feature.is_enabled('read_next')):\n            link = self.link\n\n            def wrapper_fn(thing):\n                w = Wrapped(thing)\n                w.render_class = ReadNextLink\n                return w\n\n            query_obj = c.site.get_links('hot', 'all')\n            builder = IDBuilder(query_obj,\n                                wrap=wrapper_fn,\n                                skip=True, num=10)\n            listing = ReadNextListing(builder).listing()\n            if len(listing.things):\n                rb.append(ReadNext(c.site, listing.render()))\n\n        if not (self.link.promoted and not c.user_is_sponsor):\n            if c.user_is_admin:\n                from admin_pages import AdminLinkInfoBar\n                rb.insert(1, AdminLinkInfoBar(a=self.link))\n            else:\n                rb.insert(1, LinkInfoBar(a=self.link))\n        return rb\n\n    def page_classes(self):\n        classes = Reddit.page_classes(self)\n\n        if self.link.flair_css_class:\n            for css_class in self.link.flair_css_class.split():\n                classes.add('post-linkflair-' + css_class)\n\n        if c.user_is_loggedin and self.link.author == c.user:\n            classes.add(\"post-submitter\")\n\n        time_ago = datetime.datetime.now(g.tz) - self.link._date\n        delta = datetime.timedelta\n        steps = [\n            delta(minutes=10),\n            delta(hours=6),\n            delta(hours=24),\n        ]\n        for step in steps:\n            if time_ago < step:\n                if step < delta(hours=1):\n                    step_str = \"%dm\" % (step.total_seconds() / 60)\n                else:\n                    step_str = \"%dh\" % (step.total_seconds() / (60 * 60))\n                classes.add(\"post-under-%s-old\" % step_str)\n\n        return classes\n\nclass LinkCommentSep(Templated):\n    pass\n\nclass CommentPane(Templated):\n    def cache_key(self):\n        num = self.article.num_comments\n        # bit of triage: we don't care about 10% changes in comment\n        # trees once they get to a certain length.  The cache is only a few\n        # min long anyway.\n        if num > 1000:\n            num = (num / 100) * 100\n        elif num > 100:\n            num = (num / 10) * 10\n\n        cache_key_args = [\n            self.article._fullname,\n            self.article.contest_mode,\n            self.article.locked,\n            num,\n            self.sort,\n            self.num,\n            c.lang,\n            self.can_reply,\n            c.render_style,\n            c.domain_prefix,\n            c.secure,\n            c.user.pref_show_flair,\n            c.can_embed,\n            self.max_depth,\n            self.edits_visible,\n        ]\n\n        if feature_utils.is_tracking_link_enabled(self.article):\n            cache_key_args.append(\"utm_comment_links\")\n\n        _id = make_key_id(*cache_key_args)\n        key = \"pane:%s\" % _id\n        return key\n\n    def __init__(self, article, sort, comment, context, num, **kw):\n        from r2.models import Builder, CommentBuilder, NestedListing\n        from r2.controllers.reddit_base import UnloggedUser\n\n        self.sort = sort\n        self.num = num\n        self.article = article\n\n        self.max_depth = kw.get('max_depth')\n        self.edits_visible = kw.get(\"edits_visible\")\n\n        is_html = c.render_style == \"html\"\n\n        if is_html:\n            timer = g.stats.get_timer(\"service_time.CommentPaneCache\")\n        else:\n            timer = g.stats.get_timer(\n                \"service_time.CommentPaneCache.%s\" % c.render_style)\n        timer.start()\n\n        try_cache = (\n            not comment and\n            not context and\n            is_html and\n            not c.user_is_admin and\n            not (c.user_is_loggedin and c.user._id == article.author_id)\n        )\n\n        if c.user_is_loggedin:\n            sr = article.subreddit_slow\n            try_cache &= not bool(sr.can_ban(c.user))\n\n            user_threshold = c.user.pref_min_comment_score\n            default_threshold = Account._defaults[\"pref_min_comment_score\"]\n            try_cache &= user_threshold == default_threshold\n\n        if c.user_is_loggedin:\n            self.can_reply = article.can_comment_slow(c.user)\n        else:\n            # assume that the common case is for loggedin users to see reply\n            # buttons and do the same for loggedout users so they can use the\n            # same cached page. reply buttons will be hidden client side for\n            # loggedout users\n            self.can_reply = not article.archived_slow and not article.locked\n\n        builder = CommentBuilder(\n            article, sort, comment=comment, context=context, num=num, **kw)\n\n        if try_cache and c.user_is_loggedin:\n            builder._get_comments()\n            timer.intermediate(\"build_comments\")\n            for comment in builder.comments:\n                if comment.author_id == c.user._id:\n                    try_cache = False\n                    break\n\n        if not try_cache:\n            listing = NestedListing(builder, parent_name=article._fullname)\n            listing_for_user = listing.listing()\n            timer.intermediate(\"build_listing\")\n            self.rendered = listing_for_user.render()\n            timer.intermediate(\"render_listing\")\n        else:\n            g.log.debug(\"using comment page cache\")\n            key = self.cache_key()\n            self.rendered = g.commentpanecache.get(key)\n\n            if self.rendered:\n                cache_hit = True\n\n                if c.user_is_loggedin:\n                    # don't need the builder to make a listing so stop its timer\n                    builder.timer.stop(\"waiting\")\n\n            else:\n                cache_hit = False\n\n                # spoof an unlogged in user\n                user = c.user\n                logged_in = c.user_is_loggedin\n                try:\n                    c.user = UnloggedUser([c.lang])\n                    # Preserve the viewing user's flair preferences.\n                    c.user.pref_show_flair = user.pref_show_flair\n\n                    c.user_is_loggedin = False\n\n                    # make the comment listing. if the user is loggedin we\n                    # already made the builder retrieve/build the comment tree\n                    # and lookup the comments.\n                    listing = NestedListing(\n                        builder, parent_name=article._fullname)\n                    generic_listing = listing.listing()\n\n                    if logged_in:\n                        timer.intermediate(\"build_listing\")\n                    else:\n                        timer.intermediate(\"build_comments_and_listing\")\n\n                    self.rendered = generic_listing.render()\n                    timer.intermediate(\"render_listing\")\n                finally:\n                    # undo the spoofing\n                    c.user = user\n                    c.user_is_loggedin = logged_in\n\n                try:\n                    g.commentpanecache.set(\n                        key,\n                        self.rendered,\n                        time=g.commentpane_cache_time\n                    )\n                except MemcachedError as e:\n                    g.log.warning(\"Ignored exception (%r) on commentpane \"\n                                  \"write for %r\", e, request.path)\n\n            # figure out what needs to be updated on the listing\n            if c.user_is_loggedin:\n                updates = []\n\n                # wrap the comments so the builder will customize them for\n                # the loggedin user\n                wrapped_for_user = Builder.wrap_items(builder, builder.comments)\n                timer.intermediate(\"wrap_comments_for_user\")\n\n                for t in wrapped_for_user:\n                    if not hasattr(t, \"likes\"):\n                        # this is for MoreComments and MoreRecursion\n                        continue\n\n                    is_friend = (getattr(t, \"friend\", False) and\n                                 not t.author._deleted)\n                    is_enemy = getattr(t, \"enemy\", False)\n\n                    update = {}\n                    if is_friend:\n                        update['friend'] = True\n                    if is_enemy:\n                        update['enemy'] = True\n                    if t.likes:\n                        update['voted'] = 1\n                    if t.likes is False:\n                        update['voted'] = -1\n                    if t.saved:\n                        update['saved'] = True\n                    if t.user_gilded:\n                        update['gilded'] = (t.gilded_message, t.gildings)\n\n                    if update:\n                        update['id'] = t._fullname\n                        updates.append(update)\n\n                self.rendered += ThingUpdater(updates=updates).render()\n                timer.intermediate(\"thingupdater\")\n\n        if try_cache:\n            if cache_hit:\n                timer.stop(\"hit\")\n            else:\n                timer.stop(\"miss\")\n        else:\n            timer.stop(\"uncached\")\n\n    def listing_iter(self, l):\n        for t in l:\n            yield t\n            for x in self.listing_iter(getattr(t, \"child\", [])):\n                yield x\n\n    def render(self, *a, **kw):\n        return self.rendered\n\nclass ThingUpdater(Templated):\n    pass\n\n\nclass LinkInfoBar(Templated):\n    \"\"\"Right box for providing info about a link.\"\"\"\n    def __init__(self, a = None):\n        if a:\n            a = Wrapped(a)\n        Templated.__init__(self, a = a, datefmt = datefmt)\n\nclass EditReddit(Reddit):\n    \"\"\"Container for the about page for a reddit\"\"\"\n    extension_handling= False\n\n    def __init__(self, *a, **kw):\n        from r2.lib.menus import menu\n\n        try:\n            key = kw.pop(\"location\")\n            title = menu[key]\n        except KeyError:\n            is_moderator = c.user_is_loggedin and \\\n                c.site.is_moderator(c.user) or c.user_is_admin\n\n            title = (_('subreddit settings') if is_moderator else\n                     _('about %(site)s') % dict(site=c.site.name))\n\n        Reddit.__init__(self, title=title, *a, **kw)\n\n    def build_toolbars(self):\n        return [PageNameNav('subreddit', title=self.title)]\n\n\nclass SubredditsPage(Reddit):\n    \"\"\"container for rendering a list of reddits.  The corner\n    searchbox is hidden and its functionality subsumed by an in page\n    SearchBar for searching over reddits.  As a result this class\n    takes the same arguments as SearchBar, which it uses to construct\n    self.searchbar\"\"\"\n    searchbox    = False\n    submit_box   = False\n    def __init__(self, prev_search = '',\n                 title = '', loginbox = True, infotext = None, show_interestbar=False,\n                 search_params = {}, *a, **kw):\n        Reddit.__init__(self, title = title, loginbox = loginbox, infotext = infotext,\n                        *a, **kw)\n        self.searchbar = SearchBar(\n            prev_search = prev_search,\n            header=_('search subreddits by name'),\n            search_params={},\n            simple=True,\n            subreddit_search=True,\n            search_path=\"/subreddits/search\",\n        )\n        self.sr_infobar = InfoBar(message = strings.sr_subscribe)\n        self.interestbar = InterestBar(True) if show_interestbar else None\n\n    def build_toolbars(self):\n        buttons =  [NavButton(menu.popular, \"\"),\n                    NamedButton(\"new\")]\n        if c.user_is_admin:\n            buttons.append(NamedButton(\"banned\"))\n        if c.user.employee:\n            buttons.append(NamedButton(\"employee\"))\n        if c.user.gold or c.user.gold_charter:\n            buttons.append(NamedButton(\"gold\"))\n        if c.user_is_admin:\n            buttons.append(NamedButton(\"quarantine\"))\n        if c.user_is_admin:\n            buttons.append(NamedButton(\"featured\"))\n        if c.user_is_loggedin:\n            #add the aliases to \"my reddits\" stays highlighted\n            buttons.append(NamedButton(\"mine\",\n                                       aliases=['/subreddits/mine/subscriber',\n                                                '/subreddits/mine/contributor',\n                                                '/subreddits/mine/moderator']))\n\n        return [PageNameNav('subreddits'),\n                NavMenu(buttons, base_path = '/subreddits', type=\"tabmenu\")]\n\n    def content(self):\n        return self.content_stack((self.interestbar, self.searchbar,\n                                   self.nav_menu, self.sr_infobar,\n                                   self._content))\n\n    def rightbox(self):\n        ps = Reddit.rightbox(self)\n        srs = Subreddit.user_subreddits(c.user, ids=False, limit=None)\n        srs.sort(key=lambda sr: sr.name.lower())\n        subscribe_box = SubscriptionBox(srs,\n                                        multi_text=strings.subscribed_multi)\n        num_reddits = len(subscribe_box.srs)\n        ps.append(SideContentBox(_(\"your front page subreddits (%s)\") %\n                                 num_reddits, [subscribe_box]))\n        return ps\n\nclass MySubredditsPage(SubredditsPage):\n    \"\"\"Same functionality as SubredditsPage, without the search box.\"\"\"\n\n    def content(self):\n        return self.content_stack((self.nav_menu, self.infobar, self._content))\n\n\ndef votes_visible(user):\n    \"\"\"Determines whether to show/hide a user's votes.  They are visible:\n     * if the current user is the user in question\n     * if the user has a preference showing votes\n     * if the current user is an administrator\n    \"\"\"\n    return ((c.user_is_loggedin and c.user.name == user.name) or\n            user.pref_public_votes or\n            c.user_is_admin)\n\n\nclass ProfilePage(Reddit):\n    \"\"\"Container for a user's profile page.  As such, the Account\n    object of the user must be passed in as the first argument, along\n    with the current sub-page (to determine the title to be rendered\n    on the page)\"\"\"\n\n    searchbox         = False\n    create_reddit_box = False\n    submit_box        = False\n    extra_page_classes = ['profile-page']\n\n    def __init__(self, user, *a, **kw):\n        self.user     = user\n        Reddit.__init__(self, *a, **kw)\n\n    def build_toolbars(self):\n        path = \"/user/%s/\" % self.user.name\n        main_buttons = [NavButton(menu.overview, '/', aliases = ['/overview']),\n                   NamedButton('comments'),\n                   NamedButton('submitted'),\n                   NamedButton('gilded')]\n\n        if votes_visible(self.user):\n            main_buttons += [\n                NamedButton('upvoted'),\n                NamedButton('downvoted'),\n            ]\n\n        if c.user_is_loggedin and (c.user._id == self.user._id or\n                                   c.user_is_admin):\n            main_buttons += [NamedButton('hidden'), NamedButton('saved')]\n\n        if c.user_is_sponsor:\n            main_buttons += [NamedButton('promoted')]\n\n        toolbar = [PageNameNav('nomenu', title = self.user.name),\n                   NavMenu(main_buttons, base_path = path, type=\"tabmenu\")]\n\n        if c.user_is_admin:\n            from admin_pages import AdminProfileMenu\n            toolbar.append(AdminProfileMenu(path))\n\n        return toolbar\n\n    def page_classes(self):\n        classes = Reddit.page_classes(self)\n\n        if c.user_is_admin:\n            if self.user.in_timeout:\n                if self.user.timeout_expiration:\n                    classes.add(\"user-in-timeout-temp\")\n                else:\n                    classes.add(\"user-in-timeout-perma\")\n            if self.user._spam:\n                classes.add(\"user-spam\")\n            if self.user._banned:\n                classes.add(\"user-banned\")\n            if self.user._deleted:\n                classes.add(\"user-deleted\")\n\n        return classes\n\n    def rightbox(self):\n        rb = Reddit.rightbox(self)\n\n        tc = TrophyCase(self.user)\n        helplink = HelpLink(\"/wiki/awards\", _(\"what's this?\"))\n        scb = SideContentBox(title=_(\"trophy case\"),\n                 helplink=helplink, content=[tc],\n                 extra_class=\"trophy-area\")\n\n        rb.push(scb)\n\n        multis = LabeledMulti.by_owner(self.user, load_subreddits=False)\n\n        public_multis = [m for m in multis if m.is_public()]\n        if public_multis:\n            scb = SideContentBox(title=_(\"public multireddits\"), content=[\n                SidebarMultiList(public_multis)\n            ])\n            rb.push(scb)\n\n        hidden_multis = [m for m in multis if m.is_hidden()]\n        if c.user == self.user and hidden_multis:\n            scb = SideContentBox(title=_(\"hidden multireddits\"), content=[\n                SidebarMultiList(hidden_multis)\n            ])\n            rb.push(scb)\n\n        if c.user_is_admin:\n            from r2.lib.pages.admin_pages import AdminNotesSidebar\n            from admin_pages import AdminSidebar\n\n            rb.push(AdminSidebar(self.user))\n            rb.push(AdminNotesSidebar('user', self.user.name))\n        elif c.user_is_sponsor:\n            from admin_pages import SponsorSidebar\n            rb.push(SponsorSidebar(self.user))\n\n        mod_sr_ids = Subreddit.reverse_moderator_ids(self.user)\n        all_mod_srs = Subreddit._byID(mod_sr_ids, data=True,\n                                      return_dict=False, stale=True)\n        mod_srs = [sr for sr in all_mod_srs if sr.can_view_in_modlist(c.user)]\n        if mod_srs:\n            rb.push(SideContentBox(title=_(\"moderator of\"),\n                                   content=[SidebarModList(mod_srs)]))\n\n        if (c.user == self.user or c.user.employee or\n            self.user.pref_public_server_seconds):\n            seconds_bar = ServerSecondsBar(self.user)\n            if seconds_bar.message or seconds_bar.gift_message:\n                rb.push(seconds_bar)\n\n        rb.push(ProfileBar(self.user))\n\n        return rb\n\nclass TrophyCase(Templated):\n    def __init__(self, user):\n        self.user = user\n        self.trophies = []\n        self.invisible_trophies = []\n\n        for trophy in Trophy.by_account(user):\n            if trophy._thing2.awardtype == 'invisible':\n                self.invisible_trophies.append(trophy)\n            else:\n                self.trophies.append(trophy)\n\n        Templated.__init__(self)\n\n\nclass SidebarMultiList(Templated):\n    def __init__(self, multis):\n        Templated.__init__(self)\n        multis.sort(key=lambda multi: multi.name.lower())\n        self.multis = multis\n\n\nclass SidebarModList(Templated):\n    def __init__(self, subreddits):\n        Templated.__init__(self)\n        # primary sort is desc. subscribers, secondary is name\n        self.subreddits = sorted(subreddits,\n                                 key=lambda sr: (-sr._ups, sr.name.lower()))\n\n\nclass ProfileBar(Templated):\n    \"\"\"Draws a right box for info about the user (karma, etc)\"\"\"\n    def __init__(self, user):\n        Templated.__init__(self, user=user)\n        if c.user_is_loggedin:\n            self.viewing_self = user._id == c.user._id\n            self.show_private_info = self.viewing_self or c.user_is_admin\n        else:\n            self.viewing_self = False\n            self.show_private_info = False\n\n        self.show_users_gold_expiration = (self.show_private_info or\n            user.pref_show_gold_expiration) and user.gold\n        self.show_private_gold_info = (self.show_private_info and\n            (user.gold or user.gold_creddits > 0 or user.num_gildings > 0))\n\n        if self.show_users_gold_expiration:\n            gold_days_left = (user.gold_expiration -\n                              datetime.datetime.now(g.tz)).days\n\n            if gold_days_left < 1:\n                self.gold_remaining = _(\"less than a day\")\n            else:\n                # Round remaining gold to number of days\n                precision = 60 * 60 * 24\n                self.gold_remaining = timeuntil(user.gold_expiration,\n                                                precision)\n\n        if c.user_is_loggedin:\n            if user.gold and self.show_private_info:\n                if user.has_paypal_subscription:\n                    self.paypal_subscr_id = user.gold_subscr_id\n                    self.paypal_url = paypal_subscription_url()\n                if user.has_stripe_subscription:\n                    self.stripe_customer_id = user.gold_subscr_id\n\n            if user.gold_creddits > 0 and self.show_private_info:\n                msg = ungettext(\"%(creddits)s gold creddit to give\",\n                                \"%(creddits)s gold creddits to give\",\n                                user.gold_creddits)\n                msg = msg % dict(creddits=user.gold_creddits)\n                self.gold_creddit_message = msg\n\n            if user.num_gildings > 0 and self.show_private_info:\n                gildings_msg = ungettext(\n                    \"%(gildings)s gilding given out\",\n                    \"%(gildings)s gildings given out\",\n                    user.num_gildings)\n                gildings_msg = gildings_msg % dict(gildings=user.num_gildings)\n                self.num_gildings_message = gildings_msg\n\n            if not self.viewing_self:\n                self.goldlink = \"/gold?goldtype=gift&recipient=\" + user.name\n                self.giftmsg = _(\"give reddit gold to %(user)s to show \"\n                                 \"your appreciation\") % {'user': user.name}\n            elif not user.gold:\n                self.goldlink = \"/gold/about\"\n                self.giftmsg = _(\"get extra features and help support reddit \"\n                                 \"with a reddit gold subscription\")\n            elif gold_days_left < 7 and not user.gold_will_autorenew:\n                self.goldlink = \"/gold/about\"\n                self.giftmsg = _(\"renew your reddit gold\")\n\n            if not self.viewing_self:\n                self.is_friend = user._id in c.user.friends\n\n            if self.show_private_info:\n                self.all_karmas = user.all_karmas()\n\n\nclass ServerSecondsBar(Templated):\n    my_message = _(\"you have helped pay for *%(time)s* of reddit server time.\")\n    their_message = _(\"/u/%(user)s has helped pay for *%%(time)s* of reddit server \"\n                      \"time.\")\n\n    my_gift_message = _(\"gifts on your behalf have helped pay for *%(time)s* of \"\n                        \"reddit server time.\")\n    their_gift_message = _(\"gifts on behalf of /u/%(user)s have helped pay for \"\n                           \"*%%(time)s* of reddit server time.\")\n\n    def make_message(self, seconds, my_message, their_message):\n        if not seconds:\n            return ''\n\n        delta = datetime.timedelta(seconds=seconds)\n        server_time = precise_format_timedelta(delta, threshold=5,\n                                                locale=c.locale)\n        if c.user == self.user:\n            message = my_message\n        else:\n            message = their_message % {'user': self.user.name}\n        return message % {'time': server_time}\n\n    def __init__(self, user):\n        Templated.__init__(self)\n\n        self.is_public = user.pref_public_server_seconds\n        self.is_user = c.user == user\n        self.user = user\n\n        seconds = 0.\n        gold_payments = gold_payments_by_user(user)\n\n        for payment in gold_payments:\n            seconds += calculate_server_seconds(payment.pennies, payment.date)\n\n        try:\n            q = (Bid.query().filter(Bid.account_id == user._id)\n                    .filter(Bid.status == Bid.STATUS.CHARGE)\n                    .filter(Bid.transaction > 0))\n            selfserve_payments = list(q)\n        except NotFound:\n            selfserve_payments = []\n\n        for payment in selfserve_payments:\n            pennies = payment.charge_amount * 100\n            seconds += calculate_server_seconds(pennies, payment.date)\n        self.message = self.make_message(seconds, self.my_message,\n                                         self.their_message)\n\n        seconds = 0.\n        gold_gifts = gold_received_by_user(user)\n\n        for payment in gold_gifts:\n            pennies = days_to_pennies(payment.days)\n            seconds += calculate_server_seconds(pennies, payment.date)\n        self.gift_message = self.make_message(seconds, self.my_gift_message,\n                                              self.their_gift_message)\n\n\nclass MenuArea(Templated):\n    \"\"\"Draws the gray box at the top of a page for sort menus\"\"\"\n    def __init__(self, menus = []):\n        Templated.__init__(self, menus = menus)\n\n\nclass InfoBar(Templated):\n    \"\"\"Draws the yellow box at the top of a page for info\"\"\"\n    def __init__(self, message='', extra_class=''):\n        Templated.__init__(self, message=message, extra_class=extra_class)\n\n\nclass RedditInfoBar(InfoBar):\n    def __init__(self, message='', extra_class='', show_icon=False):\n        self.show_icon = show_icon\n        super(RedditInfoBar, self).__init__(\n            message=message,\n            extra_class=extra_class,\n        )\n\n\nclass WelcomeBar(InfoBar):\n    def __init__(self):\n        messages = g.live_config.get(\"welcomebar_messages\")\n        if messages:\n            message = random.choice(messages).split(\" / \")\n        else:\n            message = (_(\"reddit is a platform for internet communities\"),\n                       _(\"where your votes shape what the world is talking about.\"))\n        InfoBar.__init__(self, message=message)\n\nclass NewsletterBar(InfoBar):\n    pass\n\nclass ClientInfoBar(InfoBar):\n    \"\"\"Draws the message the top of a login page before OAuth2 authorization\"\"\"\n    def __init__(self, client, *args, **kwargs):\n        kwargs.setdefault(\"extra_class\", \"client-info\")\n        InfoBar.__init__(self, *args, **kwargs)\n        self.client = client\n\n\nclass LocationBar(Templated): pass\n\nclass MobileWebRedirectBar(Templated):\n    pass\n\nclass SidebarMessage(Templated):\n    \"\"\"An info message box on the sidebar.\"\"\"\n    def __init__(self, message, extra_class=None):\n        Templated.__init__(self, message=message, extra_class=extra_class)\n\nclass RedditError(BoringPage):\n    show_infobar = False\n    site_tracking = False\n\n    def __init__(self, title, message, image=None, sr_description=None,\n            include_message_mods_link=False, explanation=None):\n        content = ErrorPage(\n            title=title,\n            message=message,\n            image=image,\n            sr_description=sr_description,\n            include_message_mods_link=include_message_mods_link,\n            explanation=explanation,\n        )\n        BoringPage.__init__(self, title, loginbox=False,\n                            show_sidebar = False,\n                            content=content)\n\nclass ErrorPage(Templated):\n    \"\"\"Wrapper for an error message\"\"\"\n    def __init__(self, title, message, image=None, explanation=None, **kwargs):\n        if not image:\n            letter = random.choice(['a', 'b', 'c', 'd', 'e'])\n            image = 'reddit404' + letter + '.png'\n        # Normalize explanation strings.\n        if explanation:\n            explanation = explanation.lower().rstrip('.') + '.'\n        Templated.__init__(self,\n                           title=title,\n                           message=message,\n                           image_url=image,\n                           explanation=explanation,\n                           **kwargs)\n\n\nclass InterstitialPage(BoringPage):\n    show_infobar = False\n\n    def __init__(self, title, content=None):\n        BoringPage.__init__(\n            self,\n            title,\n            loginbox=False,\n            show_sidebar=False,\n            show_welcomebar=False,\n            robots='noindex,nofollow',\n            content=content,\n        )\n\n    def page_classes(self):\n        classes = super(BoringPage, self).page_classes()\n        if 'quarantine' in classes:\n            classes.remove('quarantine')\n        return classes\n\n\nclass Interstitial(Templated):\n    \"\"\"Generic template for rendering an interstitial page's content.\"\"\"\n\n    def __init__(self, image=None, title=None, message=None, sr_name=None,\n                 sr_description=None, **kwargs):\n        Templated.__init__(\n            self,\n            image=image,\n            title=title,\n            message=message,\n            sr_name=sr_name,\n            sr_description=sr_description,\n            **kwargs\n        )\n\n\nclass AdminInterstitial(Interstitial):\n    \"\"\"The admin password verification form.\"\"\"\n    pass\n\n\nclass BannedInterstitial(Interstitial):\n    \"\"\"The banned subreddit message.\"\"\"\n    pass\n\n\nclass BannedUserInterstitial(BannedInterstitial):\n    \"\"\"The message shown when viewing a banned user profile.\"\"\"\n    pass\n\n\nclass UserBlockedInterstitial(BannedInterstitial):\n    \"\"\"The message shown when viewing a blocked user profile.\"\"\"\n    pass\n\n\nclass InTimeoutInterstitial(BannedInterstitial):\n    \"\"\"The message shown to a user in timeout.\"\"\"\n    def __init__(self, timeout_days_remaining=0, hide_message=False):\n        self.timeout_days_remaining = timeout_days_remaining\n        self.hide_message = hide_message\n        super(InTimeoutInterstitial, self).__init__()\n\n\nclass PrivateInterstitial(Interstitial):\n    \"\"\"The interstitial shown on private subreddits.\"\"\"\n    pass\n\n\nclass GoldOnlyInterstitial(Interstitial):\n    \"\"\"Interstitial for gold-only subreddits.\"\"\"\n    pass\n\n\nclass QuarantineInterstitial(Interstitial):\n    \"\"\"The opt in page for viewing quarantined content.\"\"\"\n\n    def __init__(self, sr_name, logged_in, email_verified):\n        can_opt_in = logged_in and email_verified\n        Interstitial.__init__(\n            self,\n            sr_name=sr_name,\n            logged_in=logged_in,\n            can_opt_in=can_opt_in,\n        )\n\n\nclass Over18Interstitial(Interstitial):\n    \"\"\"The no-longer-creepy 'over 18' check page for nsfw content.\"\"\"\n    pass\n\n\nclass LockedInterstitial(Interstitial):\n    \"\"\"The error message shown when attempting to comment on a locked post.\"\"\"\n    pass\n\n\nclass ArchivedInterstitial(Interstitial):\n    \"\"\"The error message shown when attempting to comment on an archived post.\"\"\"\n    def __init__(self):\n        days = g.ARCHIVE_AGE.days\n        months = days // 30\n        super(ArchivedInterstitial, self).__init__(\n            archive_age_months=months,\n        )\n\n\nclass DeletedUserInterstitial(Interstitial):\n    \"\"\"The deleted user message.\"\"\"\n    pass\n\n\nclass Popup(Templated):\n    \"\"\"Generic template for rendering a modal.\"\"\"\n    def __init__(self, popup_id=None, content=None, **kwargs):\n        Templated.__init__(\n            self,\n            popup_id=popup_id,\n            content=content,\n            **kwargs\n        )\n\n\nclass SubredditTopBar(CachedTemplate):\n\n    \"\"\"The horizontal strip at the top of most pages for navigating\n    user-created reddits.\"\"\"\n    def __init__(self):\n        self._my_reddits = None\n        self._pop_reddits = None\n        name = '' if not c.user_is_loggedin else c.user.name\n        # poor man's expiration, with random initial time\n        t = int(time.time()) / 3600\n        if c.user_is_loggedin:\n            t += c.user._id\n\n        # HACK: depends on something in the page's content calling\n        # Subreddit.default_subreddits so that c.location is set prior to this\n        # template being added to the header. set c.location as an attribute so\n        # it is added to the render cache key.\n        self.location = c.location or \"no_location\"\n        self.my_subreddits_dropdown = self.my_reddits_dropdown()\n        CachedTemplate.__init__(self, name=name, t=t, over18=c.over18)\n\n    @property\n    def my_reddits(self):\n        if self._my_reddits is None:\n            self._my_reddits = Subreddit.user_subreddits(c.user, ids=False)\n        return self._my_reddits\n\n    @property\n    def pop_reddits(self):\n        if self._pop_reddits is None:\n            defaults = Subreddit.default_subreddits(ids=False)\n            # sort the default subreddits by \"popularity\" descending\n            defaults = sorted(defaults, key=lambda sr: sr._downs, reverse=True)\n            self._pop_reddits = defaults\n        return self._pop_reddits\n\n    def my_reddits_dropdown(self):\n        drop_down_buttons = []\n        for sr in sorted(self.my_reddits, key = lambda sr: sr.name.lower()):\n            drop_down_buttons.append(SubredditButton(sr))\n        drop_down_buttons.append(NavButton(menu.edit_subscriptions,\n                                           sr_path = False,\n                                           css_class = 'bottom-option',\n                                           dest = '/subreddits/'))\n        return SubredditMenu(drop_down_buttons,\n                             title = _('my subreddits'),\n                             type = 'srdrop')\n\n    def subscribed_reddits(self):\n        srs = [SubredditButton(sr) for sr in\n                        sorted(self.my_reddits,\n                               key = lambda sr: sr._downs,\n                               reverse=True)\n                        ]\n        return NavMenu(srs,\n                       type='flatlist', separator = '-',\n                       css_class = 'sr-bar')\n\n    def popular_reddits(self, exclude_mine=False):\n        exclude = self.my_reddits if exclude_mine else []\n        buttons = [SubredditButton(sr) for sr in self.pop_reddits\n                                       if sr not in exclude]\n\n        return NavMenu(buttons,\n                       type='flatlist', separator = '-',\n                       css_class = 'sr-bar', _id = 'sr-bar')\n\n    def special_reddits(self):\n        css_classes = {Random: \"random\",\n                       RandomSubscription: \"gold\"}\n        reddits = [Frontpage, All, Random]\n        if getattr(c.site, \"over_18\", False):\n            reddits.append(RandomNSFW)\n        if c.user_is_loggedin:\n            if c.user.gold:\n                reddits.append(RandomSubscription)\n            if c.user.friends:\n                reddits.append(Friends)\n            if c.user.is_moderator_somewhere:\n                reddits.append(Mod)\n        return NavMenu([SubredditButton(sr, css_class=css_classes.get(sr))\n                        for sr in reddits],\n                       type = 'flatlist', separator = '-',\n                       css_class = 'sr-bar')\n\n    def sr_bar (self):\n        sep = '<span class=\"separator\">&nbsp;|&nbsp;</span>'\n        menus = []\n        menus.append(self.special_reddits())\n        menus.append(RawString(sep))\n\n        if not c.user_is_loggedin:\n            menus.append(self.popular_reddits())\n        else:\n            menus.append(self.subscribed_reddits())\n\n            # if the user has more than ~10 subscriptions the top bar will be\n            # completely full and anything we add to it won't be seen\n            if len(self.my_reddits) < 10:\n                menus.append(RawString(sep))\n                menus.append(self.popular_reddits(exclude_mine=True))\n\n        return menus\n\n\nclass MultiInfoBar(Templated):\n    def __init__(self, multi, srs, user):\n        Templated.__init__(self)\n        self.multi = wrap_things(multi)[0]\n        self.can_edit = multi.can_edit(user)\n        self.can_copy = c.user_is_loggedin\n        self.can_rename = c.user_is_loggedin and multi.owner == c.user\n        srs.sort(key=lambda sr: sr.name.lower())\n        self.description_md = multi.description_md\n        self.srs = srs\n        self.subreddit_selector = SubredditSelector(\n                placeholder=_(\"add subreddit\"),\n                class_name=\"sr-name\",\n                include_user_subscriptions=False,\n                show_add=True,\n            )\n\n        self.color_options = Subreddit.KEY_COLORS\n\n        self.icon_options = g.multi_icons\n\n        explore_sr = g.live_config[\"listing_chooser_explore_sr\"]\n        if explore_sr:\n            self.share_url = \"/r/%(sr)s/submit?url=%(url)s\" % {\n                \"sr\": explore_sr,\n                \"url\": g.origin + self.multi.path,\n            }\n        else:\n            self.share_url = None\n\n\nclass SubscriptionBox(Templated):\n    \"\"\"The list of reddits a user is currently subscribed to to go in\n    the right pane.\"\"\"\n    def __init__(self, srs, multi_text=None):\n        self.srs = srs\n        self.goldlink = None\n        self.goldmsg = None\n        self.prelink = None\n        self.multi_path = None\n        self.multi_text = multi_text\n\n        # Construct MultiReddit path\n        if multi_text:\n            self.multi_path = '/r/' + '+'.join([sr.name for sr in srs])\n\n        if len(srs) > Subreddit.sr_limit and c.user_is_loggedin:\n            if not c.user.gold:\n                self.goldlink = \"/gold\"\n                self.goldmsg = _(\"raise it to %s\") % Subreddit.gold_limit\n                self.prelink = [\"/wiki/faq#wiki_how_many_subreddits_can_i_subscribe_to.3F\",\n                                _(\"%s visible\") % Subreddit.sr_limit]\n            else:\n                self.goldlink = \"/gold/about\"\n                extra = min(len(srs) - Subreddit.sr_limit,\n                            Subreddit.gold_limit - Subreddit.sr_limit)\n                visible = min(len(srs), Subreddit.gold_limit)\n                bonus = {\"bonus\": extra}\n                self.goldmsg = _(\"%(bonus)s bonus subreddits\") % bonus\n                self.prelink = [\"/wiki/faq#wiki_how_many_subreddits_can_i_subscribe_to.3F\",\n                                _(\"%s visible\") % visible]\n\n        Templated.__init__(self, srs=srs, goldlink=self.goldlink,\n                           goldmsg=self.goldmsg)\n\n    @property\n    def reddits(self):\n        return wrap_links(self.srs)\n\n\nclass ModSRInfoBar(Templated):\n    pass\n\n\nclass FilteredInfoBar(Templated):\n    def __init__(self):\n        self.css_class = None\n        if c.site.filtername == \"all\":\n            self.css_class = \"gold-accent\"\n        Templated.__init__(self)\n\n\nclass AllInfoBar(Templated):\n    def __init__(self, site, user):\n        self.sr = site\n        self.allminus_url = None\n        self.css_class = None\n        if isinstance(site, AllMinus) and c.user.gold:\n            self.description = (strings.r_all_minus_description + \"\\n\\n\" +\n                \" \".join(\"/r/\" + sr.name for sr in site.exclude_srs))\n            self.css_class = \"gold-accent\"\n        else:\n            self.description = strings.r_all_description\n            sr_ids = Subreddit.user_subreddits(user)\n            srs = Subreddit._byID(\n                sr_ids, data=True, return_dict=False, stale=True)\n            if srs:\n                self.allminus_url = '/r/all-' + '-'.join([sr.name for sr in srs])\n\n        self.gilding_listing = False\n        if request.path.startswith(\"/comments/gilded\"):\n            self.gilding_listing = True\n\n        Templated.__init__(self)\n\n\nclass CreateSubreddit(Templated):\n    \"\"\"reddit creation form.\"\"\"\n    def __init__(self, site = None, name = '', captcha=None):\n        allow_image_upload = site and not site.quarantine\n        feature_autoexpand_media_previews = feature.is_enabled(\"autoexpand_media_previews\")\n        Templated.__init__(self,\n                           site=site,\n                           name=name,\n                           captcha=captcha,\n                           comment_sorts=CommentSortMenu.visible_options(),\n                           allow_image_upload=allow_image_upload,\n                           feature_autoexpand_media_previews=feature_autoexpand_media_previews,\n                           )\n        self.color_options = Subreddit.KEY_COLORS\n        self.subreddit_selector = SubredditSelector(\n                placeholder=_(\"add subreddit\"),\n                class_name=\"sr-name\",\n                include_user_subscriptions=False,\n                show_add=True,\n            )\n\n\nclass SubredditStylesheetBase(Templated):\n    \"\"\"Base subreddit stylesheet page.\"\"\"\n    def __init__(self, stylesheet_contents, **kwargs):\n        raw_images = ImagesByWikiPage.get_images(c.site, \"config/stylesheet\")\n        images = {name: make_url_protocol_relative(url)\n                  for name, url in raw_images.iteritems()}\n        super(SubredditStylesheetBase, self).__init__(\n            images=images,\n            stylesheet_contents=stylesheet_contents,\n            **kwargs\n        )\n\n\nclass SubredditStylesheet(SubredditStylesheetBase):\n    \"\"\"form for editing or creating subreddit stylesheets\"\"\"\n    def __init__(self, site=None, stylesheet_contents=''):\n        allow_image_upload = site and not site.quarantine\n        super(SubredditStylesheet, self).__init__(\n            stylesheet_contents=stylesheet_contents,\n            site=site,\n            allow_image_upload=allow_image_upload,\n        )\n\n    @staticmethod\n    def find_preview_comments(sr):\n        comments = queries.get_sr_comments(sr)\n        comments = list(comments)\n        if not comments:\n            comments = queries.get_all_comments()\n            comments = list(comments)\n\n        return Thing._by_fullname(comments[:25], data=True, return_dict=False)\n\n    @staticmethod\n    def find_preview_links(sr):\n        # try to find a link to use, otherwise give up and return\n        links = normalized_hot([sr._id])\n        if not links:\n            links = normalized_hot(Subreddit.default_subreddits())\n\n        if links:\n            links = links[:25]\n            links = Link._by_fullname(links, data=True, return_dict=False)\n\n        return links\n\n    @staticmethod\n    def rendered_link(links, media, compress, stickied=False):\n        with c.user.safe_set_attr:\n            c.user.pref_compress = compress\n            c.user.pref_media = media\n        links = wrap_links(links, show_nums=True, num=1)\n        for wrapped in links:\n            wrapped.stickied = stickied\n        delattr(c.user, \"pref_compress\")\n        delattr(c.user, \"pref_media\")\n        return links.render(style=\"html\")\n\n    @staticmethod\n    def rendered_comment(comments, gilded=False):\n        wrapped = wrap_links(comments, num=1)\n        if gilded:\n            for w in wrapped:\n                w.gilded_message = \"this comment was fake-gilded\"\n        return wrapped.render(style=\"html\")\n\n\nclass SubredditStylesheetSource(SubredditStylesheetBase):\n    \"\"\"A view of the unminified source of a subreddit's stylesheet.\"\"\"\n    pass\n\n\nclass AutoModeratorConfig(Templated):\n    \"\"\"A view of a subreddit's AutoModerator configuration.\"\"\"\n    def __init__(self, automoderator_config):\n        Templated.__init__(self, automoderator_config=automoderator_config)\n\n\nclass RawCode(Templated):\n    \"\"\"A \"raw code\" view of a wiki page - not rendered as markdown.\"\"\"\n    def __init__(self, code):\n        Templated.__init__(self, code=code)\n\n\nclass CssError(Templated):\n    \"\"\"Rendered error returned to the stylesheet editing page via ajax\"\"\"\n    def __init__(self, error):\n        # error is an instance of cssfilter.py:ValidationError\n        Templated.__init__(self, error = error)\n\n    @property\n    def message(self):\n        return _(self.error.message_key) % self.error.message_params\n\nclass UploadedImage(Templated):\n    \"The page rendered in the iframe during an upload of a header image\"\n    def __init__(self,status,img_src, name=\"\", errors = {}, form_id = \"\"):\n        self.errors = list(errors.iteritems())\n        Templated.__init__(self, status=status, img_src=img_src, name = name,\n                           form_id = form_id)\n\n    def render(self, *a, **kw):\n        return responsive(Templated.render(self, *a, **kw))\n\nclass Thanks(Templated):\n    \"\"\"The page to claim reddit gold trophies\"\"\"\n    def __init__(self, secret=None):\n        if secret and secret.startswith(\"cr_\"):\n            status = \"creddits\"\n        elif c.user.gold:\n            status = \"gold\"\n        else:\n            status = \"mundane\"\n\n        Templated.__init__(self, status=status, secret=secret)\n\nclass GoldThanks(Templated):\n    \"\"\"An actual 'Thanks for buying gold!' landing page\"\"\"\n    pass\n\nclass Gold(Templated):\n    def __init__(self, goldtype, period, months, signed,\n                 email, recipient, giftmessage, can_subscribe=True,\n                 edit=False):\n\n        if c.user.employee:\n            user_creddits = 50\n        else:\n            user_creddits = c.user.gold_creddits\n\n        Templated.__init__(self, goldtype = goldtype, period = period,\n                           months = months, signed = signed,\n                           email=email,\n                           recipient=recipient,\n                           giftmessage=giftmessage,\n                           user_creddits = user_creddits,\n                           can_subscribe=can_subscribe,\n                           edit=edit)\n\n\nclass Creddits(Templated):\n    pass\n\n\nclass GoldPayment(Templated):\n    def __init__(self, goldtype, period, months, signed,\n                 recipient, giftmessage, passthrough, thing,\n                 clone_template=False, thing_type=None):\n        desc = None\n\n        if period == \"monthly\" or 1 <= months < 12:\n            unit_price = g.gold_month_price\n            if period == 'monthly':\n                price = unit_price\n            else:\n                price = unit_price * months\n        else:\n            unit_price = g.gold_year_price\n            if period == 'yearly':\n                price = unit_price\n            else:\n                years = months / 12\n                price = unit_price * years\n\n        if c.user.employee:\n            user_creddits = 50\n        else:\n            user_creddits = c.user.gold_creddits\n\n        if (goldtype in (\"gift\", \"code\", \"onetime\") and\n                months <= user_creddits):\n            can_use_creddits = True\n        else:\n            can_use_creddits = False\n\n        if goldtype == \"autorenew\":\n            if period == \"monthly\":\n                paypal_buttonid = g.PAYPAL_BUTTONID_AUTORENEW_BYMONTH\n                summary = strings.gold_summary_autorenew_monthly % dict(\n                    user=c.user.name,\n                    price=price,\n                )\n            elif period == \"yearly\":\n                paypal_buttonid = g.PAYPAL_BUTTONID_AUTORENEW_BYYEAR\n                summary = strings.gold_summary_autorenew_yearly % dict(\n                    user=c.user.name,\n                    price=price,\n                )\n\n            quantity = None\n            stripe_key = g.secrets['stripe_public_key']\n            coinbase_button_id = None\n\n        elif goldtype == \"onetime\":\n            if months < 12:\n                paypal_buttonid = g.PAYPAL_BUTTONID_ONETIME_BYMONTH\n                quantity = months\n                coinbase_name = 'COINBASE_BUTTONID_ONETIME_%sMO' % quantity\n                coinbase_button_id = getattr(g, coinbase_name, None)\n            else:\n                paypal_buttonid = g.PAYPAL_BUTTONID_ONETIME_BYYEAR\n                quantity = months / 12\n                months = quantity * 12\n                coinbase_name = 'COINBASE_BUTTONID_ONETIME_%sYR' % quantity\n                coinbase_button_id = getattr(g, coinbase_name, None)\n\n            summary = strings.gold_summary_onetime % dict(\n                amount=Score.somethings(months, \"month\"),\n                user=c.user.name,\n                price=price,\n            )\n\n            stripe_key = g.secrets['stripe_public_key']\n\n        else:\n            if months < 12:\n                if goldtype == \"code\":\n                    paypal_buttonid = g.PAYPAL_BUTTONID_GIFTCODE_BYMONTH\n                else:\n                    paypal_buttonid = g.PAYPAL_BUTTONID_CREDDITS_BYMONTH\n                quantity = months\n                coinbase_name = 'COINBASE_BUTTONID_ONETIME_%sMO' % quantity\n                coinbase_button_id = getattr(g, coinbase_name, None)\n            else:\n                if goldtype == \"code\":\n                    paypal_buttonid = g.PAYPAL_BUTTONID_GIFTCODE_BYYEAR\n                else:\n                    paypal_buttonid = g.PAYPAL_BUTTONID_CREDDITS_BYYEAR\n                quantity = months / 12\n                months = quantity * 12\n                coinbase_name = 'COINBASE_BUTTONID_ONETIME_%sYR' % quantity\n                coinbase_button_id = getattr(g, coinbase_name, None)\n\n            if goldtype == \"creddits\":\n                summary = strings.gold_summary_creddits % dict(\n                    amount=Score.somethings(months, \"creddit\"),\n                    price=price,\n                )\n            elif goldtype == \"gift\":\n                if clone_template:\n                    if thing_type == \"comment\":\n                        format = strings.gold_summary_gilding_comment\n                    elif thing_type == \"link\":\n                        format = strings.gold_summary_gilding_link\n                elif thing:\n                    if isinstance(thing, Comment):\n                        format = strings.gold_summary_gilding_page_comment\n                        desc = thing.body\n                    else:\n                        format = strings.gold_summary_gilding_page_link\n                        desc = thing.markdown_link_slow()\n                elif signed:\n                    format = strings.gold_summary_signed_gift\n                else:\n                    format = strings.gold_summary_anonymous_gift\n\n                if not clone_template:\n                    summary = format % dict(\n                        amount=Score.somethings(months, \"month\"),\n                        recipient=recipient and\n                                  recipient.name.replace('_', '&#95;'),\n                        price=price,\n                    )\n                else:\n                    # leave the replacements to javascript\n                    summary = format\n            elif goldtype == \"code\":\n                summary = strings.gold_summary_gift_code % dict(\n                    amount=Score.somethings(months, \"month\"),\n                    price=price,\n                )\n            else:\n                raise ValueError(\"wtf is %r\" % goldtype)\n\n            stripe_key = g.secrets['stripe_public_key']\n\n        Templated.__init__(self, goldtype=goldtype, period=period,\n                           months=months, quantity=quantity,\n                           unit_price=unit_price, price=price,\n                           summary=summary, giftmessage=giftmessage,\n                           can_use_creddits=can_use_creddits,\n                           passthrough=passthrough,\n                           thing=thing, clone_template=clone_template,\n                           description=desc, thing_type=thing_type,\n                           paypal_buttonid=paypal_buttonid,\n                           stripe_key=stripe_key,\n                           coinbase_button_id=coinbase_button_id,\n                           user_creddits=user_creddits,\n                           )\n\n\nclass GoldSubscription(Templated):\n    def __init__(self, user):\n        if user.has_stripe_subscription:\n            details = get_subscription_details(user)\n        else:\n            details = None\n\n        if details:\n            self.has_stripe_subscription = True\n            date = details['next_charge_date']\n            next_charge_date = format_date(date, format=\"short\",\n                                           locale=c.locale)\n            credit_card_last4 = details['credit_card_last4']\n            amount = format_currency(float(details['pennies']) / 100, 'USD',\n                                     locale=c.locale)\n            text = _(\"you have a credit card gold subscription. your card \"\n                     \"(ending in %(last4)s) will be charged %(amount)s on \"\n                     \"%(date)s.\")\n            self.text = text % dict(last4=credit_card_last4,\n                                    amount=amount,\n                                    date=next_charge_date)\n            self.user_fullname = user._fullname\n        else:\n            self.has_stripe_subscription = False\n\n        if user.has_paypal_subscription:\n            self.has_paypal_subscription = True\n            self.paypal_subscr_id = user.gold_subscr_id\n            self.paypal_url = paypal_subscription_url()\n        else:\n            self.has_paypal_subscription = False\n\n        self.stripe_key = g.secrets['stripe_public_key']\n        Templated.__init__(self)\n\nclass CreditGild(Templated):\n    \"\"\"Page for credit card payments for gilding.\"\"\"\n    pass\n\nclass GoldGiftCodeEmail(Templated):\n    \"\"\"Email sent to a logged-out person that purchases a reddit\n    gold gift code.\"\"\"\n    pass\n\n\nclass Gilding(Templated):\n    pass\n\n\nclass ReportForm(CachedTemplate):\n    def __init__(self, thing=None, **kw):\n        self.rules = []\n        self.system_rules = []\n        self.thing_fullname = thing._fullname\n        self.kind = None\n        subreddit = None\n\n        if isinstance(thing, (Comment, Link)):\n            subreddit = thing.subreddit_slow\n            self.kind = thing.__class__.__name__.lower()\n\n        if (subreddit and\n                feature.is_enabled(\"subreddit_rules\", subreddit=subreddit.name)):\n            for rule in SubredditRules.get_rules(subreddit, self.kind):\n                self.rules.append(rule[\"short_name\"])\n            if self.rules:\n                self.system_rules = SITEWIDE_RULES\n                self.rules_page_link = \"/r/%s/about/rules\" % subreddit.name\n        if not self.rules:\n            self.rules = OLD_SITEWIDE_RULES\n            self.rules_page_link = \"/help/contentpolicy\"\n\n        Templated.__init__(self)\n\n\nclass SubredditReportForm(CachedTemplate):\n    def __init__(self, thing=None, filter_by_kind=True, **kw):\n        self.rules = []\n        self.system_rules = SITEWIDE_RULES\n        self.thing_fullname = thing._fullname\n        self.kind = None\n        subreddit = None\n\n        if isinstance(thing, Comment, Link):\n            subreddit = thing.subreddit_slow\n            self.sr_name = subreddit.name\n            if filter_by_kind:\n                self.kind = thing.__class__.__name__.lower()\n        else:\n            self.sr_name = None\n\n        if (subreddit and\n                feature.is_enabled(\"subreddit_rules\", subreddit=subreddit.name)):\n            self.rules = SubredditRules.get_rules(subreddit, self.kind)\n\n        Templated.__init__(self)\n\n\nclass ReportFormTemplates(Templated):\n    def __init__(self):\n        super(ReportFormTemplates, self).__init__(\n            system_rules=SITEWIDE_RULES,\n            rules_page_link=\"/help/contentpolicy\",\n        )\n\n\nclass FraudForm(Templated):\n    pass\n\n\nclass Password(Templated):\n    \"\"\"Form encountered when 'recover password' is clicked in the LoginFormWide.\"\"\"\n    def __init__(self, success=False):\n        Templated.__init__(self, success = success)\n\nclass PasswordReset(Templated):\n    \"\"\"Template for generating an email to the user who wishes to\n    reset their password (step 2 of password recovery, after they have\n    entered their user name in Password.)\"\"\"\n    pass\n\nclass MessageNotificationEmail(Templated):\n    \"\"\"Notification e-mail that a user has received a new message.\"\"\"\n    pass\n\nclass MessageNotificationEmailsUnsubscribe(Templated):\n    \"\"\"The page we show users when they unsubscribe from notification\n    emails.\"\"\"\n    pass\n\nclass PasswordChangeEmail(Templated):\n    \"\"\"Notification e-mail that a user's password has changed.\"\"\"\n    pass\n\nclass EmailChangeEmail(Templated):\n    \"\"\"Notification e-mail that a user's e-mail has changed.\"\"\"\n    pass\n\nclass VerifyEmail(Templated):\n    pass\n\nclass Promo_Email(Templated):\n    def __init__(self, *args, **kwargs):\n        # if total_budget_dollars is passed,\n        # format into printable_total_budget\n        if 'total_budget_dollars' in kwargs:\n            locale = c.locale or g.locale\n            self.printable_total_budget = format_currency(\n                kwargs['total_budget_dollars'], 'USD', locale=locale)\n        super(Promo_Email, self).__init__(*args, **kwargs)\n\nclass SuspiciousPaymentEmail(Templated):\n    def __init__(self, user, link):\n        Templated.__init__(self, user=user, link=link)\n\n\nclass ResetPassword(Templated):\n    \"\"\"Form for actually resetting a lost password, after the user has\n    clicked on the link provided to them in the Password_Reset email\n    (step 3 of password recovery.)\"\"\"\n    pass\n\n\nclass Captcha(Templated):\n    \"\"\"Container for rendering robot detection device.\"\"\"\n    def __init__(self, error=None):\n        self.error = _('try entering those letters again') if error else \"\"\n        self.iden = get_captcha()\n        Templated.__init__(self)\n\nclass PermalinkMessage(Templated):\n    \"\"\"renders the box on comment pages that state 'you are viewing a\n    single comment's thread'\"\"\"\n    def __init__(self, comments_url):\n        Templated.__init__(self, comments_url = comments_url)\n\nclass PaneStack(Templated):\n    \"\"\"Utility class for storing and rendering a list of block elements.\"\"\"\n\n    def __init__(self, panes=[], div_id = None, css_class=None, div=False,\n                 title=\"\", title_buttons = []):\n        div = div or div_id or css_class or False\n        self.div_id    = div_id\n        self.css_class = css_class\n        self.div       = div\n        self.stack     = list(panes)\n        self.title = title\n        self.title_buttons = title_buttons\n        Templated.__init__(self)\n\n    def append(self, item):\n        \"\"\"Appends an element to the end of the current stack\"\"\"\n        self.stack.append(item)\n\n    def push(self, item):\n        \"\"\"Prepends an element to the top of the current stack\"\"\"\n        self.stack.insert(0, item)\n\n    def insert(self, *a):\n        \"\"\"inerface to list.insert on the current stack\"\"\"\n        return self.stack.insert(*a)\n\n\nclass HtmlPaneStack(PaneStack):\n    \"\"\"Same as panestack, but won't show up in json responses.\"\"\"\n    pass\n\n\nclass SearchForm(Templated):\n    \"\"\"The simple search form in the header of the page.  prev_search\n    is the previous search.\"\"\"\n    def __init__(self, prev_search='', search_params={}, site=None,\n                 simple=True, restrict_sr=False, subreddit_search=False,\n                 syntax=None, search_path=\"/search\"):\n        Templated.__init__(self, prev_search=prev_search,\n                           search_params=search_params, site=site,\n                           simple=simple, restrict_sr=restrict_sr,\n                           subreddit_search=subreddit_search, syntax=syntax,\n                           search_path=search_path)\n\n        # generate the over18 redirect url for the current search if needed\n        if not c.over18 and feature.is_enabled('safe_search'):\n            u = UrlParser(add_sr(search_path))\n            if prev_search:\n                u.update_query(q=prev_search)\n            if restrict_sr:\n                u.update_query(restrict_sr='on')\n            u.update_query(**search_params)\n            u.update_query(over18='yes')\n            self.over18_url = u.unparse()\n        else:\n            self.over18_url = None\n\n\nclass SearchBar(Templated):\n    \"\"\"More detailed search box for /search and /subreddits pages.\n\n    Displays the previous search as well\n\n    \"\"\"\n    def __init__(self, header=None, prev_search='', search_params={},\n                 simple=False, restrict_sr=False, site=None, syntax=None,\n                 subreddit_search=False, converted_data=None,\n                 search_path=\"/search\"):\n        if header is None:\n            header = _(\"search\")\n        self.header = header\n        self.prev_search  = prev_search\n        self.converted_data = converted_data\n\n        self.search_form = SearchForm(\n            prev_search=prev_search,\n            search_params=search_params,\n            site=site,\n            subreddit_search=subreddit_search,\n            simple=simple,\n            restrict_sr=restrict_sr,\n            syntax=syntax,\n            search_path=search_path,\n        )\n        Templated.__init__(self)\n\n\nclass SubredditFacets(Templated):\n    def __init__(self, prev_search='', facets={}, sort=None, recent=None):\n        self.prev_search = prev_search\n\n        Templated.__init__(self, facets=facets, sort=sort, recent=recent)\n\n\nclass NewLink(Templated):\n    \"\"\"Render the link submission form\"\"\"\n    def __init__(self, captcha=None, url='', title='', text='', selftext='',\n                 resubmit=False, default_sr=None,\n                 extra_subreddits=None, show_link=True, show_self=True):\n\n        self.show_link = show_link\n        self.show_self = show_self\n\n        tabs = []\n        if show_link:\n            tabs.append(('link', ('link-desc', 'url-field')))\n        if show_self:\n            tabs.append(('text', ('text-desc', 'text-field')))\n\n        if self.show_self and self.show_link:\n            all_fields = set(chain(*(parts for (tab, parts) in tabs)))\n            buttons = []\n\n            if selftext == 'true' or text != '':\n                self.default_tab = tabs[1][0]\n            else:\n                self.default_tab = tabs[0][0]\n\n            for tab_name, parts in tabs:\n                to_show = ','.join('#' + p for p in parts)\n                to_hide = ','.join('#' + p for p in all_fields if p not in parts)\n                onclick = \"return select_form_tab(this, '%s', '%s');\"\n                onclick = onclick % (to_show, to_hide)\n                if tab_name == self.default_tab:\n                    self.default_show = to_show\n                    self.default_hide = to_hide\n\n                buttons.append(JsButton(tab_name, onclick=onclick, css_class=tab_name + \"-button\"))\n\n            self.formtabs_menu = JsNavMenu(buttons, type = 'formtab')\n\n        self.resubmit = resubmit\n        self.default_sr = default_sr\n        self.extra_subreddits = extra_subreddits\n\n        Templated.__init__(self, captcha = captcha, url = url,\n                         title = title, text = text)\n\n\nclass Share(Templated):\n    pass\n\nclass Mail_Opt(Templated):\n    pass\n\nclass OptOut(Templated):\n    pass\n\nclass OptIn(Templated):\n    pass\n\n\nclass Button(Wrapped):\n    cachable = True\n    extension_handling = False\n    def __init__(self, link, **kw):\n        Wrapped.__init__(self, link, **kw)\n        if link is None:\n            self.title = \"\"\n            self.add_props(c.user, [self])\n\n\n    @classmethod\n    def add_props(cls, user, wrapped):\n        # unlike most wrappers we can guarantee that there is a link\n        # that this wrapper is wrapping.\n        Link.add_props(user, [w for w in wrapped if hasattr(w, \"_fullname\")])\n        for w in wrapped:\n            # caching: store the user name since each button has a modhash\n            w.user_name = c.user.name if c.user_is_loggedin else \"\"\n            if not hasattr(w, '_fullname'):\n                w._fullname = None\n\n    def render(self, *a, **kw):\n        res = Wrapped.render(self, *a, **kw)\n        return responsive(res, True)\n\nclass ButtonLite(Button):\n    def render(self, *a, **kw):\n        return Wrapped.render(self, *a, **kw)\n\nclass ButtonDemoPanel(Templated):\n    \"\"\"The page for showing the different styles of embedable voting buttons\"\"\"\n    pass\n\nclass ContactUs(Templated):\n    pass\n\n\nclass WidgetDemoPanel(Templated):\n    \"\"\"Demo page for the .embed widget.\"\"\"\n    pass\n\n\nclass UserAwards(Templated):\n    \"\"\"For drawing the regular-user awards page.\"\"\"\n    def __init__(self):\n        from r2.models import Award, Trophy\n        Templated.__init__(self)\n\n        self.regular_winners = []\n        self.manuals = []\n        self.invisibles = []\n\n        for award in Award._all_awards():\n            if award.awardtype == 'regular':\n                trophies = Trophy.by_award(award)\n                # Don't show awards that nobody's ever won\n                # (e.g., \"9-Year Club\")\n                if trophies:\n                    winner = trophies[0]._thing1.name\n                    self.regular_winners.append( (award, winner, trophies[0]) )\n            elif award.awardtype == 'manual':\n                self.manuals.append(award)\n            elif award.awardtype == 'invisible':\n                self.invisibles.append(award)\n            else:\n                raise NotImplementedError\n\n\nclass AdminAwards(Templated):\n    \"\"\"The admin page for editing awards\"\"\"\n    def __init__(self):\n        from r2.models import Award\n        Templated.__init__(self)\n        self.awards = Award._all_awards()\n\nclass AdminAwardGive(Templated):\n    \"\"\"The interface for giving an award\"\"\"\n    def __init__(self, award, recipient='', desc='', url='', hours=''):\n        now = datetime.datetime.now(g.display_tz)\n        if desc:\n            self.description = desc\n        elif award.awardtype == 'regular':\n            self.description = \"??? -- \" + now.strftime(\"%Y-%m-%d\")\n        else:\n            self.description = \"\"\n        self.url = url\n        self.recipient = recipient\n        self.hours = hours\n\n        Templated.__init__(self, award = award)\n\nclass AdminAwardWinners(Templated):\n    \"\"\"The list of winners of an award\"\"\"\n    def __init__(self, award):\n        trophies = Trophy.by_award(award)\n        Templated.__init__(self, award = award, trophies = trophies)\n\n\nclass AdminCreddits(Templated):\n    \"\"\"The admin interface for giving creddits to a user.\"\"\"\n    def __init__(self, recipient):\n        self.recipient = recipient\n        Templated.__init__(self)\n\n\nclass AdminGold(Templated):\n    \"\"\"The admin interface for giving or taking days of gold for a user.\"\"\"\n    def __init__(self, recipient):\n        self.recipient = recipient\n        Templated.__init__(self)\n\n\nclass Ads(Templated):\n    def __init__(self):\n        Templated.__init__(self)\n        self.ad_url = g.ad_domain + \"/ads/\"\n        self.frame_id = \"ad-frame\"\n\n\nclass ReadNext(Templated):\n    def __init__(self, sr, links):\n        Templated.__init__(self)\n        self.sr = sr\n        self.links = links\n\n\nclass Embed(Templated):\n    \"\"\"wrapper for embedding /help into reddit as if it were not on a separate wiki.\"\"\"\n    def __init__(self,content = ''):\n        Templated.__init__(self, content = content)\n\n\ndef wrapped_flair(user, subreddit, force_show_flair):\n    if isinstance(subreddit, FakeSubreddit):\n        # FakeSubreddits don't show user flair\n        return False, 'right', '', ''\n    elif not (force_show_flair or subreddit.flair_enabled):\n        return False, 'right', '', ''\n\n    enabled = user.flair_enabled_in_sr(subreddit._id)\n    position = subreddit.flair_position\n    text = user.flair_text(subreddit._id)\n    css_class = user.flair_css_class(subreddit._id)\n\n    return enabled, position, text, css_class\n\nclass WrappedUser(CachedTemplate):\n    cachable = False\n    FLAIR_CSS_PREFIX = 'flair-'\n\n    def __init__(self, user, attribs = [], context_thing = None, gray = False,\n                 subreddit = None, force_show_flair = None,\n                 flair_template = None, flair_text_editable = False,\n                 include_flair_selector = False):\n        if not subreddit:\n            subreddit = c.site\n\n        attribs.sort()\n        author_cls = 'author'\n\n        author_title = ''\n        if gray:\n            author_cls += ' gray'\n        for tup in attribs:\n            author_cls += \" \" + tup[2]\n            # Hack: '(' should be in tup[3] iff this friend has a note\n            if tup[1] == 'F' and '(' in tup[3]:\n                author_title = tup[3]\n\n        flair = wrapped_flair(user, subreddit or c.site, force_show_flair)\n        flair_enabled, flair_position, flair_text, flair_css_class = flair\n        has_flair = bool(\n            c.user.pref_show_flair and (flair_text or flair_css_class))\n\n        if flair_template:\n            flair_template_id = flair_template._id\n            flair_text = flair_template.text\n            flair_css_class = flair_template.css_class\n            has_flair = True\n        else:\n            flair_template_id = None\n\n        if flair_css_class:\n            # This is actually a list of CSS class *suffixes*. E.g., \"a b c\"\n            # should expand to \"flair-a flair-b flair-c\".\n            flair_css_class = ' '.join(self.FLAIR_CSS_PREFIX + c\n                                       for c in flair_css_class.split())\n\n        if include_flair_selector:\n            if (not getattr(c.site, 'flair_self_assign_enabled', True)\n                and not (c.user_is_admin\n                         or c.site.is_moderator_with_perms(c.user, 'flair'))):\n                include_flair_selector = False\n\n        target = None\n        ip_span = None\n        context_deleted = None\n        if context_thing:\n            target = getattr(context_thing, 'target', None)\n            ip_span = getattr(context_thing, 'ip_span', None)\n            context_deleted = context_thing.deleted\n\n        karma = ''\n        context_thing_fullname = ''\n        show_details_link = False\n\n        if c.user_is_admin:\n            karma = ' (%d)' % user.link_karma\n\n            if user.in_timeout:\n                if user.timeout_expiration:\n                    author_cls += \" user-in-timeout-temp\"\n                else:\n                    author_cls += \" user-in-timeout-perma\"\n\n            if context_thing:\n                context_thing_fullname = context_thing._fullname\n\n                if isinstance(context_thing, Wrapped):\n                    unwrapped_thing = context_thing.lookups[0]\n                else:\n                    unwrapped_thing = context_thing\n                if isinstance(unwrapped_thing, (Link, Comment)):\n                    show_details_link = True\n\n            if user._spam:\n                author_cls += \" user-spam\"\n            if user._banned:\n                author_cls += \" user-banned\"\n\n        CachedTemplate.__init__(self,\n                                name = user.name,\n                                force_show_flair = force_show_flair,\n                                has_flair = has_flair,\n                                flair_enabled = flair_enabled,\n                                flair_position = flair_position,\n                                flair_text = flair_text,\n                                flair_text_editable = flair_text_editable,\n                                flair_css_class = flair_css_class,\n                                flair_template_id = flair_template_id,\n                                include_flair_selector = include_flair_selector,\n                                author_cls = author_cls,\n                                author_title = author_title,\n                                attribs = attribs,\n                                context_thing_fullname = context_thing_fullname,\n                                show_details_link = show_details_link,\n                                karma = karma,\n                                ip_span = ip_span,\n                                context_deleted = context_deleted,\n                                fullname = user._fullname,\n                                user_deleted = user._deleted)\n\nclass UserTableItem(Templated):\n    type = ''\n    remove_action = 'unfriend'\n    cells = ('user', 'age', 'sendmessage', 'remove')\n\n    @property\n    def executed_message(self):\n        return _(\"added\")\n\n    def __init__(self, user, editable=True, **kw):\n        self.user = user\n        self.editable = editable\n        self.author_cls = ''\n\n        if c.user_is_admin and user._spam:\n            self.author_cls = 'banned-user'\n\n        Templated.__init__(self, **kw)\n\n    def __repr__(self):\n        return '<UserTableItem \"%s\">' % self.user.name\n\nclass RelTableItem(UserTableItem):\n    def __init__(self, rel, **kw):\n        self._id = rel._id\n        self.rel = rel\n        UserTableItem.__init__(self, rel._thing2, **kw)\n\n    @property\n    def _fullname(self):\n        # needed for paging (see Listing.listing())\n        return self.rel._fullname\n\n    @property\n    def container_name(self):\n        return c.site._fullname\n\nclass FriendTableItem(RelTableItem):\n    remove_access_required = False\n    type = 'friend'\n\n    @property\n    def cells(self):\n        if c.user.gold:\n            return ('user', 'sendmessage', 'note', 'age', 'remove')\n        return ('user', 'sendmessage', 'remove')\n\n    @property\n    def container_name(self):\n        return c.user._fullname\n\nclass EnemyTableItem(RelTableItem):\n    remove_access_required = False\n    type = 'enemy'\n    cells = ('user', 'age', 'remove')\n\n    @property\n    def container_name(self):\n        return c.user._fullname\n\nclass BannedTableItem(RelTableItem):\n    type = 'banned'\n    cells = ('user', 'age', 'sendmessage', 'remove', 'note', 'temp')\n\n    @property\n    def executed_message(self):\n        return _(\"banned\")\n\n\nclass MutedTableItem(RelTableItem):\n    type = 'muted'\n    cells = ('user', 'age', 'remove', 'note')\n\n    @property\n    def executed_message(self):\n        return _(\"muted\")\n\n\nclass WikiBannedTableItem(BannedTableItem):\n    type = 'wikibanned'\n\nclass ContributorTableItem(RelTableItem):\n    type = 'contributor'\n\nclass WikiMayContributeTableItem(RelTableItem):\n    type = 'wikicontributor'\n\nclass InvitedModTableItem(RelTableItem):\n    type = 'moderator_invite'\n    cells = ('user', 'age', 'permissions', 'permissionsctl')\n\n    @property\n    def executed_message(self):\n        return _(\"invited\")\n\n    def is_editable(self, user):\n        if not c.user_is_loggedin:\n            return False\n        elif c.user_is_admin:\n            return True\n        return c.site.is_unlimited_moderator(c.user)\n\n    def __init__(self, rel, editable=True, **kw):\n        if editable:\n            self.cells += ('remove',)\n        editable = self.is_editable(rel._thing2)\n        self.permissions = ModeratorPermissions(rel._thing2, self.type,\n                                                rel.get_permissions(),\n                                                editable=editable)\n        RelTableItem.__init__(self, rel, editable=editable, **kw)\n\nclass ModTableItem(InvitedModTableItem):\n    type = 'moderator'\n\n    @property\n    def executed_message(self):\n        return _(\"added\")\n\n    def is_editable(self, user):\n        if not c.user_is_loggedin:\n            return False\n        elif c.user_is_admin:\n            return True\n        return c.user != user and c.site.can_demod(c.user, user)\n\n\nclass ModToolsPage(Reddit):\n    \"\"\"A mod tool page.\"\"\"\n\n    def __init__(self, **kwargs):\n        super(ModToolsPage, self).__init__(\n            page_classes=['modtools-page'],\n            **kwargs\n        )\n\n\nclass Rules(Templated):\n    \"\"\"Show subreddit rules for everyone and add edit controls for mods.\"\"\"\n    def __init__(self, title, kind_labels):\n        self.title = title\n        self.can_edit = c.user_is_loggedin and (c.user_is_admin or\n            c.site.is_moderator_with_perms(c.user, 'config'))\n        self.rules = SubredditRules.get_rules(c.site)\n        self.site_rules = SITEWIDE_RULES\n        self.kind_labels = kind_labels\n        Templated.__init__(self)\n\n\nclass FlairPane(Templated):\n    def __init__(self, num, after, reverse, name, user):\n        # Make sure c.site isn't stale before rendering.\n        c.site = Subreddit._byID(c.site._id, data=True, stale=False)\n\n        tabs = [\n            ('grant', _('grant flair'), FlairList(num, after, reverse, name,\n                                                  user)),\n            ('templates', _('user flair templates'),\n             FlairTemplateList(USER_FLAIR)),\n            ('link_templates', _('link flair templates'),\n             FlairTemplateList(LINK_FLAIR)),\n        ]\n\n        Templated.__init__(\n            self,\n            tabs=TabbedPane(tabs, linkable=True),\n            flair_enabled=c.site.flair_enabled,\n            flair_position=c.site.flair_position,\n            link_flair_position=c.site.link_flair_position,\n            flair_self_assign_enabled=c.site.flair_self_assign_enabled,\n            link_flair_self_assign_enabled=\n                c.site.link_flair_self_assign_enabled)\n\nclass FlairList(Templated):\n    \"\"\"List of users who are tagged with flair within a subreddit.\"\"\"\n\n    def __init__(self, num, after, reverse, name, user):\n        Templated.__init__(self, num=num, after=after, reverse=reverse,\n                           name=name, user=user)\n\n    @property\n    def flair(self):\n        if self.user:\n            return [FlairListRow(self.user)]\n\n        if self.name:\n            # user lookup was requested, but no user was found, so abort\n            return []\n\n        query = Flair._query(\n            Flair.c._thing1_id == c.site._id,\n            Flair.c._name == 'flair',\n            sort=asc('_thing2_id'),\n            eager_load=True,\n            thing_data=True,\n        )\n\n        # To maintain API compatibility we can't use the `before` or `after`s\n        # returned by Builder.get_items(), since we use different logic to\n        # determine them. We also need to fetch an extra item to be *sure*\n        # there's a next page.\n        builder = FlairListBuilder(query, wrap=FlairListRow.from_rel,\n                                   after=self.after, reverse=self.reverse,\n                                   num=self.num + 1)\n\n        items = builder.get_items()[0]\n\n        if not items:\n            return []\n\n        have_more = False\n        if self.num and len(items) > self.num:\n            if self.reverse:\n                have_more = items.pop(0)\n            else:\n                have_more = items.pop()\n\n        # FlairLists are unusual in that afters that aren't in the queryset\n        # work correctly due to the filter just doing a gt (or lt) on\n        # the after's `_id`. They also use _thing2's fullname instead\n        # of the fullname of the rel for pagination.\n        before = items[0].user._fullname\n        after = items[-1].user._fullname\n\n        links = []\n        show_next = have_more or self.reverse\n        if (not self.reverse and self.after) or (self.reverse and have_more):\n            links.append(FlairNextLink(before, previous=True,\n                                       needs_border=show_next))\n        if show_next:\n            links.append(FlairNextLink(after, previous=False))\n\n        return items + links\n\n\nclass FlairListRow(Templated):\n    def __init__(self, user):\n        self.user = user\n        Templated.__init__(self,\n                           flair_text=user.flair_text(c.site._id),\n                           flair_css_class=user.flair_css_class(c.site._id))\n\n    @classmethod\n    def from_rel(cls, rel):\n        instance = cls(rel._thing2)\n        # Needed by the builder to do wrapped -> unwrapped lookups\n        instance._id = rel._id\n        return instance\n\n\nclass FlairNextLink(Templated):\n    def __init__(self, after, previous=False, needs_border=False):\n        Templated.__init__(self, after=after, previous=previous,\n                           needs_border=needs_border)\n\nclass FlairCsv(Templated):\n    class LineResult:\n        def __init__(self):\n            self.errors = {}\n            self.warnings = {}\n            self.status = 'skipped'\n            self.ok = False\n\n        def error(self, field, desc):\n            self.errors[field] = desc\n\n        def warn(self, field, desc):\n            self.warnings[field] = desc\n\n    def __init__(self):\n        Templated.__init__(self, results_by_line=[])\n\n    def add_line(self):\n        self.results_by_line.append(self.LineResult())\n        return self.results_by_line[-1]\n\nclass FlairTemplateList(Templated):\n    def __init__(self, flair_type):\n        Templated.__init__(self, flair_type=flair_type)\n\n    @property\n    def templates(self):\n        ids = FlairTemplateBySubredditIndex.get_template_ids(\n                c.site._id, flair_type=self.flair_type)\n        fts = FlairTemplate._byID(ids)\n        return [FlairTemplateEditor(fts[i], self.flair_type) for i in ids]\n\nclass FlairTemplateEditor(Templated):\n    def __init__(self, flair_template, flair_type):\n        Templated.__init__(self,\n                           id=flair_template._id,\n                           text=flair_template.text,\n                           css_class=flair_template.css_class,\n                           text_editable=flair_template.text_editable,\n                           sample=FlairTemplateSample(flair_template,\n                                                      flair_type),\n                           position=getattr(c.site, 'flair_position', 'right'),\n                           flair_type=flair_type)\n\n    def render(self, *a, **kw):\n        res = Templated.render(self, *a, **kw)\n        if not g.template_debug:\n            res = spaceCompress(res)\n        return res\n\nclass FlairTemplateSample(Templated):\n    \"\"\"Like a read-only version of FlairTemplateEditor.\"\"\"\n    def __init__(self, flair_template, flair_type):\n        if flair_type == USER_FLAIR:\n            wrapped_user = WrappedUser(c.user, subreddit=c.site,\n                                       force_show_flair=True,\n                                       flair_template=flair_template)\n        else:\n            wrapped_user = None\n        Templated.__init__(self,\n                           flair_template=flair_template,\n                           wrapped_user=wrapped_user, flair_type=flair_type)\n\nclass FlairPrefs(CachedTemplate):\n    def __init__(self):\n        sr_flair_enabled = getattr(c.site, 'flair_enabled', False)\n        user_flair_enabled = getattr(c.user, 'flair_%s_enabled' % c.site._id,\n                                     True)\n        sr_flair_self_assign_enabled = getattr(\n            c.site, 'flair_self_assign_enabled', True)\n        wrapped_user = WrappedUser(c.user, subreddit=c.site,\n                                   force_show_flair=True,\n                                   include_flair_selector=True)\n        CachedTemplate.__init__(\n            self,\n            sr_flair_enabled=sr_flair_enabled,\n            sr_flair_self_assign_enabled=sr_flair_self_assign_enabled,\n            user_flair_enabled=user_flair_enabled,\n            wrapped_user=wrapped_user)\n\nclass FlairSelectorLinkSample(CachedTemplate):\n    def __init__(self, link, site, flair_template):\n        flair_position = getattr(site, 'link_flair_position', 'right')\n        admin = bool(c.user_is_admin\n                     or site.is_moderator_with_perms(c.user, 'flair'))\n        CachedTemplate.__init__(\n            self,\n            title=link.title,\n            flair_position=flair_position,\n            flair_template_id=flair_template._id,\n            flair_text=flair_template.text,\n            flair_css_class=flair_template.css_class,\n            flair_text_editable=admin or flair_template.text_editable,\n            )\n\nclass FlairSelector(CachedTemplate):\n    \"\"\"Provide user with flair options according to subreddit settings.\"\"\"\n    def __init__(self, user, site, link=None):\n        admin = bool(\n            c.user_is_admin or site.is_moderator_with_perms(c.user, 'flair'))\n\n        if link:\n            flair_type = LINK_FLAIR\n            target = link\n            target_name = link._fullname\n            attr_pattern = 'flair_%s'\n            position = getattr(site, 'link_flair_position', 'right')\n            target_wrapper = (\n                lambda flair_template: FlairSelectorLinkSample(\n                    link, site, flair_template))\n            self_assign_enabled = (\n                c.user._id == link.author_id\n                and site.link_flair_self_assign_enabled)\n        else:\n            flair_type = USER_FLAIR\n            target = user\n            target_name = user.name\n            position = getattr(site, 'flair_position', 'right')\n            attr_pattern = 'flair_%s_%%s' % c.site._id\n            target_wrapper = (\n                lambda flair_template: WrappedUser(\n                    user, subreddit=site, force_show_flair=True,\n                    flair_template=flair_template,\n                    flair_text_editable=admin or template.text_editable))\n            self_assign_enabled = site.flair_self_assign_enabled\n\n        text = getattr(target, attr_pattern % 'text', '')\n        css_class = getattr(target, attr_pattern % 'css_class', '')\n        templates, matching_template = self._get_templates(\n                site, flair_type, text, css_class)\n\n        if self_assign_enabled or admin:\n            choices = [target_wrapper(template) for template in templates]\n        else:\n            choices = []\n\n        # If one of the templates is already selected, modify its text to match\n        # the user's current flair.\n        if matching_template:\n            for choice in choices:\n                if choice.flair_template_id == matching_template:\n                    if choice.flair_text_editable:\n                        choice.flair_text = text\n                    break\n\n        Templated.__init__(self, text=text, css_class=css_class,\n                           position=position, choices=choices,\n                           matching_template=matching_template,\n                           target_name=target_name)\n\n    def render(self, *a, **kw):\n        return responsive(CachedTemplate.render(self, *a, **kw), True)\n\n    def _get_templates(self, site, flair_type, text, css_class):\n        ids = FlairTemplateBySubredditIndex.get_template_ids(\n            site._id, flair_type)\n        template_dict = FlairTemplate._byID(ids)\n        templates = [template_dict[i] for i in ids]\n        for template in templates:\n            if template.covers((text, css_class)):\n                matching_template = template._id\n                break\n        else:\n             matching_template = None\n        return templates, matching_template\n\n\nclass DetailsPage(LinkInfoPage):\n    extension_handling= False\n\n    def __init__(self, thing, *args, **kwargs):\n        from admin_pages import Details\n        after = kwargs.pop('after', None)\n        reverse = kwargs.pop('reverse', False)\n        count = kwargs.pop('count', None)\n        self.details = None\n\n        if isinstance(thing, (Link, Comment)):\n            self.details = Details(thing, after=after, reverse=reverse,\n                                   count=count)\n\n        if isinstance(thing, Link):\n            link = thing\n            comment = None\n            content = self.details\n        elif isinstance(thing, Comment):\n            comment = thing\n            link = Link._byID(comment.link_id, data=True)\n            content = PaneStack()\n            content.append(PermalinkMessage(link.make_permalink_slow()))\n            content.append(LinkCommentSep())\n            content.append(CommentPane(link, CommentSortMenu.operator('new'),\n                                   comment, None, 1))\n            content.append(self.details)\n\n        kwargs['content'] = content\n        LinkInfoPage.__init__(self, link, comment, *args, **kwargs)\n\n    def rightbox(self):\n        rb = LinkInfoPage.rightbox(self)\n\n        if c.user_is_admin:\n            from admin_pages import AdminDetailsBar\n            rb.append(AdminDetailsBar(from_page='details'))\n\n        return rb\n\n\nclass PromotePage(Reddit):\n    create_reddit_box  = False\n    submit_box         = False\n    extension_handling = False\n    searchbox          = False\n\n    @classmethod\n    def get_menu(cls):\n        if c.user_is_sponsor:\n            buttons = [\n                NavButton(menu['new_promo'], dest='/promoted/new_promo'),\n                NavButton(menu['current_promos'], dest='/sponsor/promoted',\n                          aliases=['/sponsor']),\n                NavButton('inventory', '/sponsor/inventory'),\n                NavButton('report', '/sponsor/report'),\n                NavButton('underdelivered', '/sponsor/promoted/underdelivered'),\n                NavButton('house ads', '/sponsor/promoted/house'),\n                NavButton('reported links', '/sponsor/promoted/reported'),\n                NavButton('fraud', '/sponsor/promoted/fraud'),\n                NavButton('lookup user', '/sponsor/lookup_user'),\n            ]\n            return NavMenu(buttons, type='flatlist')\n        else:\n            buttons = [\n                NamedButton('new_promo'),\n                NamedButton('my_current_promos', dest=''),\n            ]\n            return NavMenu(buttons, base_path='/promoted', type='flatlist')\n\n    def __init__(self, nav_menus=None, *a, **kw):\n        menu = self.get_menu()\n\n        if nav_menus:\n            nav_menus.insert(0, menu)\n        else:\n            nav_menus = [menu]\n\n        kw['show_sidebar'] = False\n        auction_announcement = not feature.is_enabled('ads_auction')\n        Reddit.__init__(self, nav_menus=nav_menus,\n            auction_announcement=auction_announcement, *a, **kw)\n\n\nclass PromoteLinkBase(Templated):\n    min_start = None\n    max_start = None\n    max_end = None\n\n    def __init__(self, **kw):\n        self.mobile_targeting_enabled = feature.is_enabled(\"mobile_targeting\")\n        Templated.__init__(self, **kw)\n\n    def get_locations(self):\n        # geotargeting\n        def location_sort(location_tuple):\n            code, name, default = location_tuple\n            if code == '':\n                return -2\n            elif code == 'US':\n                return -1\n            else:\n                return name\n\n        countries = [(code, country['name'], False) for code, country\n                                                    in g.locations.iteritems()]\n        countries.append(('', _('none'), True))\n\n        countries = sorted(countries, key=location_sort)\n        regions = {}\n        metros = {}\n        for code, country in g.locations.iteritems():\n            if 'regions' in country and country['regions']:\n                regions[code] = [('', _('all'), True)]\n\n                for region_code, region in country['regions'].iteritems():\n                    if region['metros']:\n                        region_tuple = (region_code, region['name'], False)\n                        regions[code].append(region_tuple)\n                        if c.user_is_sponsor:\n                            metros[region_code] = []\n                        else:\n                            metros[region_code] = [('', _('all'), True)]\n\n                        for metro_code, metro in region['metros'].iteritems():\n                            metro_tuple = (metro_code, metro['name'], False)\n                            metros[region_code].append(metro_tuple)\n                        metros[region_code].sort(key=location_sort)\n                regions[code].sort(key=location_sort)\n\n        self.countries = countries\n        self.regions = regions\n        self.metros = metros\n\n        ads_auction_enabled = feature.is_enabled('ads_auction')\n        self.force_auction = (ads_auction_enabled and not c.user_is_sponsor)\n        self.auction_optional = (ads_auction_enabled and c.user_is_sponsor)\n\n        self.cpc_pricing = feature.is_enabled('cpc_pricing')\n\n    def get_collections(self):\n        self.collections = [cl.__dict__ for cl in Collection.get_all()]\n\n    def get_mobile_versions(self):\n        self.ios_versions = g.ios_versions\n        self.android_versions = g.android_versions\n\n\nclass PromoteLinkNew(PromoteLinkBase):\n    def __init__(self, images=None, *a, **kw):\n        images = images or {}\n        self.images = images\n        PromoteLinkBase.__init__(self, **kw)\n\n\nclass PromoteLinkEdit(PromoteLinkBase):\n    def __init__(self, link, listing, *a, **kw):\n        self.setup(link, listing)\n        PromoteLinkBase.__init__(self, **kw)\n\n    def setup(self, link, listing):\n        self.bids = []\n        self.author = Account._byID(link.author_id, data=True)\n\n        if c.user_is_sponsor:\n            try:\n                bids = Bid.lookup(thing_id=link._id)\n            except NotFound:\n                pass\n            else:\n                bids.sort(key=lambda x: x.date, reverse=True)\n                bidders = Account._byID(set(bid.account_id for bid in bids),\n                                        data=True, return_dict=True)\n                for bid in bids:\n                    status = Bid.STATUS.name[bid.status].lower()\n                    bidder = bidders[bid.account_id]\n                    row = Storage(\n                        status=status,\n                        bidder=bidder.name,\n                        date=bid.date,\n                        transaction=bid.transaction,\n                        campaign=bid.campaign,\n                        pay_id=bid.pay_id,\n                        amount_str=format_currency(bid.bid, 'USD',\n                                                   locale=c.locale),\n                        charge_str=format_currency(bid.charge or bid.bid, 'USD',\n                                                   locale=c.locale),\n                    )\n                    self.bids.append(row)\n\n        min_start, max_start, max_end = promote.get_date_limits(\n            link, c.user_is_sponsor)\n\n        default_end = min_start + datetime.timedelta(days=7)\n        default_start = min_start\n\n        self.min_start = min_start.strftime(\"%m/%d/%Y\")\n        self.max_start = max_start.strftime(\"%m/%d/%Y\")\n        self.max_end = max_end.strftime(\"%m/%d/%Y\")\n        self.default_start = default_start.strftime(\"%m/%d/%Y\")\n        self.default_end = default_end.strftime(\"%m/%d/%Y\")\n\n        self.link = link\n        self.listing = listing\n        campaigns = list(PromoCampaign._by_link(link._id))\n        self.campaigns = RenderableCampaign.from_campaigns(link, campaigns)\n        self.promotion_log = PromotionLog.get(link)\n\n        if c.user_is_sponsor:\n            self.min_budget_dollars = 0\n            self.max_budget_dollars = 0\n        else:\n            self.min_budget_dollars = g.min_total_budget_pennies / 100.\n            self.max_budget_dollars = g.max_total_budget_pennies / 100.\n\n        self.default_budget_dollars = g.default_total_budget_pennies / 100.\n\n        if c.user_is_sponsor:\n            self.min_bid_dollars = 0.\n            self.max_bid_dollars = 0.\n        else:\n            self.min_bid_dollars = g.min_bid_pennies / 100.\n            self.max_bid_dollars = g.max_bid_pennies / 100.\n\n        self.priorities = [\n            (p.name, p.text, p.description, p.default,\n            p.inventory_override, p == PROMOTE_PRIORITIES['auction'])\n            for p in PROMOTE_PRIORITIES.values()\n        ]\n\n        self.get_locations()\n        self.get_collections()\n        self.get_mobile_versions()\n\n        user_srs = [sr for sr in Subreddit.user_subreddits(c.user, ids=False)\n                    if sr.can_submit(c.user, promotion=True) and sr.allow_ads]\n        top_srs = sorted(user_srs, key=lambda sr: sr._ups, reverse=True)[:20]\n        extra_subreddits = [(_(\"suggestions:\"), top_srs)]\n        self.subreddit_selector = SubredditSelector(\n            extra_subreddits=extra_subreddits, include_user_subscriptions=False)\n        self.inventory = {}\n        message = _(\"Create your ad on this page. Have questions? \"\n                    \"Check out the [Help Center](%(help_center)s) \"\n                    \"or [/r/selfserve](%(selfserve)s).\")\n        message %= {\n            'help_center': 'https://reddit.zendesk.com/hc/en-us/categories/200352595-Advertising',\n            'selfserve': 'https://www.reddit.com/r/selfserve'\n        }\n        self.infobar = RedditInfoBar(message=message)\n        self.price_dict = PromotionPrices.get_price_dict(self.author)\n\n        self.frequency_cap_min = g.frequency_cap_min\n\n        self.ads_auction_enabled = feature.is_enabled('ads_auction')\n\n\nclass RenderableCampaign(Templated):\n    def __init__(self, link, campaign, transaction, is_pending, is_live,\n                 is_complete, is_edited_live, full_details=True,\n                 hide_after_seen=False):\n        self.link = link\n        self.campaign = campaign\n\n        self.ads_auction_enabled = feature.is_enabled('ads_auction')\n        if self.ads_auction_enabled:\n            self.is_auction = campaign.is_auction\n        else:\n            self.is_auction = False\n\n        # Permission to edit is always granted when:\n        # 1) Advertiser is sponsor\n        # 2) Campaign is auction\n        # 3) Auction is not enabled and campaign is not live\n        if (c.user_is_sponsor or campaign.is_auction or\n                (not self.ads_auction_enabled and not is_live)):\n            self.editable = True\n        else:\n            self.editable = False\n\n        # Convert total_budget_pennies to dollars for UI\n        self.total_budget_dollars = campaign.total_budget_pennies / 100.\n\n        if full_details:\n            if not self.campaign.is_house and not self.campaign.is_auction:\n                self.spent = promote.get_spent_amount(campaign)\n            else:\n                self.spent = campaign.adserver_spent_pennies / 100.\n        else:\n            self.spent = 0.\n\n        self.paid = bool(transaction and not transaction.is_void())\n        self.free = campaign.is_freebie()\n        self.is_pending = is_pending\n        self.is_live = is_live\n        self.is_complete = is_complete\n        self.is_edited_live = is_edited_live\n        self.needs_refund = (is_complete and c.user_is_sponsor and\n                             (transaction and not transaction.is_refund()) and\n                             self.spent < campaign.total_budget_dollars)\n        self.pay_url = promote.pay_url(link, campaign)\n        sr_name = random.choice(campaign.target.subreddit_names)\n        self.view_live_url = promote.view_live_url(link, campaign, sr_name)\n        self.refund_url = promote.refund_url(link, campaign)\n\n        if campaign.location:\n            self.country = campaign.location.country or ''\n            self.region = campaign.location.region or ''\n            self.metro = campaign.location.metro or ''\n        else:\n            self.country, self.region, self.metro = '', '', ''\n        self.location_str = campaign.location_str\n        if campaign.target.is_collection:\n            self.targeting_data = campaign.target.collection.name\n        else:\n            sr_name = campaign.target.subreddit_name\n            # LEGACY: sponsored.js uses blank to indicate no targeting, meaning\n            # targeted to the frontpage\n            self.targeting_data = '' if sr_name == Frontpage.name else sr_name\n\n        self.platform = campaign.platform\n        self.mobile_os = campaign.mobile_os\n        self.ios_devices = campaign.ios_devices\n        self.ios_versions = campaign.ios_version_range\n        self.android_devices = campaign.android_devices\n        self.android_versions = campaign.android_version_range\n\n        self.pause_ads_enabled = feature.is_enabled('pause_ads')\n\n        # If ads_auction not enabled, default cost_basis to fixed_cpm\n        if not feature.is_enabled('ads_auction'):\n            self.cost_basis = PROMOTE_COST_BASIS.name[PROMOTE_COST_BASIS.fixed_cpm]\n        elif campaign.cost_basis != PROMOTE_COST_BASIS.fixed_cpm:\n            self.cost_basis = PROMOTE_COST_BASIS.name[campaign.cost_basis]\n        else:\n            self.cost_basis = PROMOTE_COST_BASIS.name[PROMOTE_COST_BASIS.cpm]\n        self.bid_pennies = campaign.bid_pennies\n\n        self.printable_bid = format_currency(campaign.bid_dollars, 'USD',\n            locale=c.locale)\n\n        Templated.__init__(self)\n\n    @classmethod\n    def from_campaigns(cls, link, campaigns,\n                       full_details=True, hide_after_seen=False):\n        campaigns, is_single = tup(campaigns, ret_is_single=True)\n\n        if full_details:\n            transactions = promote.get_transactions(link, campaigns)\n            live_campaigns = promote.live_campaigns_by_link(link)\n        else:\n            transactions = {}\n            live_campaigns = []\n\n        ret = []\n        now = promote.promo_datetime_now()\n        for camp in campaigns:\n            transaction = transactions.get(camp._id)\n            is_pending = promote.is_pending(camp)\n            is_live = camp in live_campaigns\n            is_charged_or_refunded = (transaction and\n                (transaction.is_charged() or transaction.is_refund()))\n            is_expired_house = camp.is_house and camp.end_date < now\n            is_live_or_pending = is_live or is_pending\n            is_edited_live = promote.is_edited_live(link)\n            is_complete = (not is_edited_live and\n                (is_charged_or_refunded and\n                not is_live_or_pending) or\n                is_expired_house)\n            rc = cls(link, camp, transaction, is_pending, is_live, is_complete,\n                     is_edited_live, full_details, hide_after_seen)\n            ret.append(rc)\n        if is_single:\n            return ret[0]\n        else:\n            return ret\n\n    def render_html(self):\n        return spaceCompress(self.render(style='html'))\n\n\nclass RefundPage(Reddit):\n    def __init__(self, link, campaign):\n        self.link = link\n        self.campaign = campaign\n        self.listing = wrap_links(link, skip=False)\n        billable_impressions = promote.get_billable_impressions(campaign)\n        billable_amount = promote.get_billable_amount(campaign,\n                                                      billable_impressions)\n        refund_amount = promote.get_refund_amount(campaign, billable_amount)\n        self.billable_impressions = billable_impressions\n        self.billable_amount = billable_amount\n        self.refund_amount = refund_amount\n        self.printable_total_budget = format_currency(\n            campaign.total_budget_dollars, 'USD', locale=c.locale)\n        self.printable_bid = format_currency(campaign.bid_dollars, 'USD',\n            locale=c.locale)\n        self.traffic_url = '/traffic/%s/%s' % (link._id36, campaign._id36)\n        Reddit.__init__(self, title=\"refund\", show_sidebar=False)\n\nclass PromotePost(PromoteLinkBase):\n    def __init__(self):\n        PromoteLinkBase.__init__(self)\n\n\nclass SponsorLookupUser(PromoteLinkBase):\n    def __init__(self, id_user=None, email=None, email_users=None):\n        PromoteLinkBase.__init__(\n            self, id_user=id_user, email=email, email_users=email_users or [])\n\n\n\n\nclass SponsorLookupUser(PromoteLinkBase):\n    def __init__(self, id_user=None, email=None, email_users=None):\n        PromoteLinkBase.__init__(\n            self, id_user=id_user, email=email, email_users=email_users or [])\n\n\nclass TabbedPane(Templated):\n    def __init__(self, tabs, linkable=False):\n        \"\"\"Renders as tabbed area where you can choose which tab to\n        render. Tabs is a list of tuples (tab_name, tab_pane).\"\"\"\n        buttons = []\n        for tab_name, title, pane in tabs:\n            onclick = \"return select_tab_menu(this, '%s')\" % tab_name\n            buttons.append(JsButton(title, tab_name=tab_name, onclick=onclick))\n\n        self.tabmenu = JsNavMenu(buttons, type = 'tabmenu')\n        self.tabs = tabs\n\n        Templated.__init__(self, linkable=linkable)\n\nclass LinkChild(object):\n    def __init__(self, link, load=False, expand=False, nofollow=False,\n                 position_inline=False):\n        self.link = link\n        self.expand = expand\n        self.load = load or expand\n        self.nofollow = nofollow\n        self.position_inline = position_inline\n\n    def content(self):\n        return ''\n\ndef make_link_child(item, show_media_preview=False):\n    link_child = None\n    editable = False\n    expandable = getattr(item, 'expand_children', False)\n\n    # if the item has a media_object, try to make a MediaEmbed for rendering\n    if not c.secure:\n        media_object = item.media_object\n    else:\n        media_object = item.secure_media_object\n\n    if media_object:\n        media_embed = None\n        expand = False\n        position_inline = False\n\n        if isinstance(media_object, basestring):\n            media_embed = media_object\n        else:\n            is_autoexpand_type = media_object.get('type') in g.autoexpand_media_types\n            expand = expandable and (show_media_preview or is_autoexpand_type)\n            position_inline = expandable and is_autoexpand_type\n\n            try:\n                media_embed = media.get_media_embed(media_object)\n            except TypeError:\n                g.log.warning(\"link %s has a bad media object\" % item)\n                media_embed = None\n\n            if media_embed:\n                if media_embed.sandbox:\n                    should_authenticate = (item.subreddit.type in Subreddit.private_types or\n                        item.subreddit.quarantine)\n                    media_embed = MediaEmbed(\n                        media_domain=g.media_domain,\n                        height=media_embed.height + 10,\n                        width=media_embed.width + 10,\n                        scrolling=media_embed.scrolling,\n                        id36=item._id36,\n                        authenticated=should_authenticate,\n                    )\n                else:\n                    media_embed = media_embed.content\n            else:\n                g.log.debug(\"media_object without media_embed %s\" % item)\n\n        if media_embed:\n            link_child = MediaChild(item,\n                                    media_embed,\n                                    load=True,\n                                    expand=expand,\n                                    position_inline=position_inline)\n\n    # if the item is_self, add a selftext child\n    elif item.is_self:\n        if not item.selftext: item.selftext = u''\n\n        expand = expandable\n        position_inline = expandable\n        editable = (expand and\n                    item.author == c.user and\n                    not item._deleted)\n        link_child = SelfTextChild(item,\n                                   expand=expand,\n                                   nofollow=item.nofollow,\n                                   position_inline=position_inline)\n    # if the item has a preview image and is on the whitelist, show it\n    elif (feature.is_enabled('media_previews') and\n            item.preview_object and\n            media.allowed_media_preview_url(item.url)):\n        media_object = media.get_preview_image(\n            item.preview_object,\n            include_censored=item.nsfw,\n        )\n        expand = show_media_preview and expandable\n\n        if media_object:\n            media_preview = MediaPreview(\n                media_object=media_object,\n                id36=item._id36,\n                url=item.url,\n            )\n            link_child = MediaChild(\n                item,\n                media_preview,\n                load=True,\n                expand=expand,\n                position_inline=False,\n            )\n\n    return link_child, editable\n\n\nclass MediaChild(LinkChild):\n    \"\"\"renders when the user hits the expando button to expand media\n       objects, like embedded videos\"\"\"\n    css_style = \"video\"\n    def __init__(self, link, content, **kw):\n        self._content = content\n        LinkChild.__init__(self, link, **kw)\n\n    def content(self):\n        if isinstance(self._content, basestring):\n            return self._content\n        return self._content.render()\n\nclass MediaEmbed(Templated):\n    \"\"\"The actual rendered iframe for a media child\"\"\"\n\n    def __init__(self, *args, **kwargs):\n        authenticated = kwargs.pop(\"authenticated\", False)\n        if authenticated:\n            mac = hmac.new(g.secrets[\"media_embed\"], kwargs[\"id36\"],\n                           hashlib.sha1)\n            self.credentials = \"/\" + mac.hexdigest()\n        else:\n            self.credentials = \"\"\n        Templated.__init__(self, *args, **kwargs)\n\n\nclass MediaPreview(Templated):\n    \"\"\"Rendered html container for a media child\"\"\"\n\n    def __init__(self, media_object, id36, url, **kwargs):\n        self.media_content = media_object[\"content\"]\n        self.width = media_object[\"width\"]\n        self.id36 = id36\n        self.url = url\n        super(MediaPreview, self).__init__(**kwargs)\n\n\nclass SelfTextChild(LinkChild):\n    css_style = \"selftext\"\n\n    def content(self):\n        u = UserText(self.link, self.link.selftext,\n                     editable = c.user == self.link.author,\n                     nofollow = self.nofollow,\n                     expunged=self.link.expunged)\n        return u.render()\n\nclass UserText(CachedTemplate):\n    cachable = False\n\n    def __init__(self,\n                 item,\n                 text = '',\n                 have_form = True,\n                 editable = False,\n                 creating = False,\n                 nofollow = False,\n                 target = None,\n                 display = True,\n                 post_form = 'editusertext',\n                 cloneable = False,\n                 extra_css = '',\n                 textarea_class = '',\n                 name = \"text\",\n                 expunged=False,\n                 include_errors=True,\n                 show_embed_help=False,\n                 admin_takedown=False,\n                 data_attrs={},\n                 source=None,\n                ):\n\n        css_class = \"usertext\"\n        if cloneable:\n            css_class += \" cloneable\"\n        if extra_css:\n            css_class += \" \" + extra_css\n\n        if text is None:\n            text = ''\n\n        # set the attribute for admin takedowns\n        if getattr(item, 'admin_takedown', False):\n            admin_takedown = True\n\n        fullname = ''\n        # Do not pass fullname on deleted things, unless we're admin\n        if hasattr(item, '_fullname'):\n            if not getattr(item, 'deleted', False) or c.user_is_admin:\n                fullname = item._fullname\n\n        CachedTemplate.__init__(self,\n                                fullname = fullname,\n                                text = text,\n                                have_form = have_form,\n                                editable = editable,\n                                creating = creating,\n                                nofollow = nofollow,\n                                target = target,\n                                display = display,\n                                post_form = post_form,\n                                cloneable = cloneable,\n                                css_class = css_class,\n                                textarea_class = textarea_class,\n                                name = name,\n                                expunged=expunged,\n                                include_errors=include_errors,\n                                show_embed_help=show_embed_help,\n                                admin_takedown=admin_takedown,\n                                data_attrs=data_attrs,\n                                source=source,\n                               )\n\nclass MediaEmbedBody(CachedTemplate):\n    \"\"\"What's rendered inside the iframe that contains media objects\"\"\"\n    def render(self, *a, **kw):\n        res = CachedTemplate.render(self, *a, **kw)\n        return responsive(res, True)\n\n\nclass PaymentForm(Templated):\n    countries = sorted({c['name'] for c in g.locations.values()})\n\n    default_country = g.locations.get(\"US\").get(\"name\")\n\n    def __init__(self, link, campaign, **kw):\n        self.link = link\n        self.duration = strings.time_label\n        self.duration %= {'num': campaign.ndays,\n                          'time': ungettext(\"day\", \"days\", campaign.ndays)}\n        self.start_date = campaign.start_date.strftime(\"%m/%d/%Y\")\n        self.end_date = campaign.end_date.strftime(\"%m/%d/%Y\")\n        self.campaign_id36 = campaign._id36\n        self.budget = format_currency(campaign.total_budget_dollars, 'USD',\n            locale=c.locale)\n        Templated.__init__(self, **kw)\n\n\nclass Bookings(object):\n    def __init__(self):\n        self.subreddit = 0\n        self.collection = 0\n\n    def __add__(self, other):\n        if isinstance(other, int) and other == 0:\n            return self\n\n        added = Bookings()\n        added.subreddit = self.subreddit + other.subreddit\n        added.collection = self.collection + other.collection\n\n        return added\n\n    def __radd__(self, other):\n        return self.__add__(other)\n\n    def __repr__(self):\n        if self.subreddit and not self.collection:\n            return format_number(self.subreddit)\n        elif self.collection and not self.subreddit:\n            return \"%s*\" % format_number(self.collection)\n        elif not self.subreddit and not self.collection:\n            return format_number(0)\n        else:\n            nums = tuple(map(format_number, (self.subreddit, self.collection)))\n            return \"%s (%s*)\" % nums\n\n\nclass PromoteInventory(PromoteLinkBase):\n    def __init__(self, start, end, target):\n        Templated.__init__(self)\n        self.start = start\n        self.end = end\n        self.default_start = start.strftime('%m/%d/%Y')\n        self.default_end = end.strftime('%m/%d/%Y')\n        self.target = target\n        self.display_name = target.pretty_name\n        p = request.GET.copy()\n        self.csv_url = '%s.csv?%s' % (request.path, urlencode(p))\n        if target.is_collection:\n            self.sr_input = None\n            self.collection_input = target.collection.name\n            self.targeting_type = \"collection\"\n        else:\n            self.sr_input = target.subreddit_name\n            self.collection_input = None\n            self.targeting_type = \"collection\" if target.subreddit_name == Frontpage.name else \"one\"\n        self.setup()\n\n    def as_csv(self):\n        out = cStringIO.StringIO()\n        writer = csv.writer(out)\n\n        writer.writerow(tuple(self.header))\n\n        for row in self.rows:\n            if not row.is_total:\n                outrow = [row.info['author']]\n            else:\n                outrow = [row.info['title']]\n            outrow.extend(row.columns)\n            writer.writerow(outrow)\n\n        return out.getvalue()\n\n    def setup(self):\n        srs = self.target.subreddits_slow\n        campaigns_by_date = inventory.get_campaigns_by_date(\n            srs, self.start, self.end)\n        link_ids = {camp.link_id for camp\n                    in chain.from_iterable(campaigns_by_date.itervalues())}\n        links_by_id = Link._byID(link_ids, data=True)\n        dates = inventory.get_date_range(self.start, self.end)\n        total_by_date = {date: Bookings() for date in dates}\n        imps_by_link = defaultdict(lambda: {date: Bookings() for date in dates})\n        for date, campaigns in campaigns_by_date.iteritems():\n            for camp in campaigns:\n                link = links_by_id[camp.link_id]\n                daily_impressions = camp.impressions / camp.ndays\n                if camp.target.is_collection:\n                    total_by_date[date].collection += daily_impressions\n                    imps_by_link[link._id][date].collection += daily_impressions\n                else:\n                    total_by_date[date].subreddit += daily_impressions\n                    imps_by_link[link._id][date].subreddit += daily_impressions\n\n        account_ids = {link.author_id for link in links_by_id.itervalues()}\n        accounts_by_id = Account._byID(account_ids, data=True)\n\n        self.header = ['link'] + [date.strftime(\"%m/%d/%Y\") for date in dates] + ['total']\n        rows = []\n        for link_id, imps_by_date in imps_by_link.iteritems():\n            link = links_by_id[link_id]\n            author = accounts_by_id[link.author_id]\n            info = {\n                'author': author.name,\n                'edit_url': promote.promo_edit_url(link),\n            }\n            row = Storage(info=info, is_total=False)\n            row.columns = ([str(imps_by_date[date]) for date in dates] +\n                [str(sum(imps_by_date.values()))])\n            rows.append(row)\n        rows.sort(key=lambda row: row.info['author'].lower())\n\n        total_row = Storage(\n            info={'title': 'total'},\n            is_total=True,\n            columns=([str(total_by_date[date]) for date in dates] +\n                [str(sum(total_by_date.values()))]),\n        )\n        rows.append(total_row)\n\n        predicted_pageviews_by_sr = inventory.get_predicted_pageviews(srs)\n        predicted_pageviews = sum(pageviews for pageviews\n                                  in predicted_pageviews_by_sr.itervalues())\n        predicted_row = Storage(\n            info={'title': 'predicted'},\n            is_total=True,\n            columns=([format_number(predicted_pageviews) for date in dates] +\n                [format_number(sum([predicted_pageviews for date in dates]))]),\n        )\n        rows.append(predicted_row)\n\n        available_pageviews = inventory.get_available_pageviews(\n            self.target, self.start, self.end)\n        remaining_row = Storage(\n            info={'title': 'remaining'},\n            is_total=True,\n            columns=([format_number(available_pageviews[date]) for date in dates] +\n                [format_number(sum(available_pageviews.values()))]),\n        )\n        rows.append(remaining_row)\n\n        self.rows = rows\n\n        default_sr = None\n        if not self.target.is_collection and self.sr_input:\n            default_sr = Subreddit._by_name(self.sr_input)\n        self.subreddit_selector = SubredditSelector(\n                default_sr=default_sr,\n                include_user_subscriptions=False)\n\n        self.get_locations()\n        self.get_collections()\n\n\nReportKey = namedtuple(\"ReportKey\", [\"date\", \"link\", \"campaign\"])\nReportItem = namedtuple(\"ReportItem\",\n    [\"bid\", \"fp_imps\", \"sr_imps\", \"fp_clicks\", \"sr_clicks\"])\n\n\nclass PromoteReport(PromoteLinkBase):\n    def __init__(self, links, link_text, owner_name, bad_links, start, end,\n                 group_by_date=False):\n        self.links = links\n        self.start = start\n        self.end = end\n        self.default_start = start.strftime('%m/%d/%Y')\n        self.default_end = end.strftime('%m/%d/%Y')\n        self.group_by_date = group_by_date\n\n        if links:\n            self.make_report()\n            p = request.GET.copy()\n            self.csv_url = '%s.csv?%s' % (request.path, urlencode(p))\n        else:\n            self.link_report = []\n            self.campaign_report = []\n            self.csv_url = None\n\n        Templated.__init__(self, link_text=link_text, owner_name=owner_name,\n                           bad_links=bad_links)\n\n    def as_csv(self):\n        out = cStringIO.StringIO()\n        writer = csv.writer(out)\n\n        writer.writerow((_(\"start date\"), self.start.strftime('%m/%d/%Y')))\n        writer.writerow((_(\"end date\"), self.end.strftime('%m/%d/%Y')))\n        writer.writerow([])\n        writer.writerow((_(\"links\"),))\n        if self.group_by_date:\n            outrow = [_(\"date\")]\n        else:\n            outrow = []\n        outrow.extend([_(\"id\"), _(\"owner\"), _(\"url\"), _(\"comments\"),\n            _(\"upvotes\"), _(\"downvotes\"), _(\"clicks\"), _(\"impressions\")])\n        writer.writerow(outrow)\n        for row in self.link_report:\n            if self.group_by_date:\n                outrow = [row['date']]\n            else:\n                outrow = []\n            outrow.extend([row['id36'], row['owner'], row['url'],\n                row['comments'], row['upvotes'], row['downvotes'],\n                row['clicks'], row['impressions']])\n            writer.writerow(outrow)\n\n        writer.writerow([])\n        writer.writerow((_(\"campaigns\"),))\n        if self.group_by_date:\n            outrow = [_(\"date\")]\n        else:\n            outrow = []\n        outrow.extend([_(\"link id\"), _(\"owner\"), _(\"campaign id\"), _(\"target\"),\n            _(\"bid\"), _(\"frontpage clicks\"), _(\"frontpage impressions\"),\n            _(\"subreddit clicks\"), _(\"subreddit impressions\"),\n            _(\"total clicks\"), _(\"total impressions\")])\n        writer.writerow(outrow)\n        for row in self.campaign_report:\n            if self.group_by_date:\n                outrow = [row['date']]\n            else:\n                outrow = []\n            outrow.extend([row['link'], row['owner'], row['campaign'],\n                row['target'], row['bid'], row['fp_clicks'],\n                row['fp_impressions'], row['sr_clicks'], row['sr_impressions'],\n                row['total_clicks'], row['total_impressions']])\n            writer.writerow(outrow)\n        return out.getvalue()\n\n    @classmethod\n    def get_traffic(self, campaigns, start, end):\n        campaigns_by_name = {camp._fullname: camp for camp in campaigns}\n        codenames = campaigns_by_name.keys()\n\n        start_date = start.date()\n        ndays = (end - start).days\n        dates = {start_date + datetime.timedelta(days=i) for i in xrange(ndays)}\n\n        # traffic database uses datetimes with no timezone, also need to shift\n        # start, end to account for campaigns launching at 12:00 EST\n        start = (start - promote.timezone_offset).replace(tzinfo=None)\n        end = (end - promote.timezone_offset).replace(tzinfo=None)\n\n        # start and end are dates so we need to subtract an hour from end to\n        # only include 24 hours per day\n        end -= datetime.timedelta(hours=1)\n\n        fp_imps_by_date = {d: defaultdict(int) for d in dates}\n        sr_imps_by_date = {d: defaultdict(int) for d in dates}\n        fp_clicks_by_date = {d: defaultdict(int) for d in dates}\n        sr_clicks_by_date = {d: defaultdict(int) for d in dates}\n\n        imps = traffic.TargetedImpressionsByCodename.campaign_history(\n            codenames, start, end)\n        clicks = traffic.TargetedClickthroughsByCodename.campaign_history(\n            codenames, start, end)\n\n        for date, codename, sr, (uniques, pageviews) in imps:\n            # convert from utc hour to campaign date\n            traffic_date = (date + promote.timezone_offset).date()\n\n            if sr == '':\n                # LEGACY: traffic uses '' to indicate Frontpage\n                fp_imps_by_date[traffic_date][codename] += pageviews\n            else:\n                sr_imps_by_date[traffic_date][codename] += pageviews\n\n        for date, codename, sr, (uniques, pageviews) in clicks:\n            traffic_date = (date + promote.timezone_offset).date()\n\n            if sr == '':\n                # NOTE: clicks use hourly uniques\n                fp_clicks_by_date[traffic_date][codename] += uniques\n            else:\n                sr_clicks_by_date[traffic_date][codename] += uniques\n\n        traffic_by_key = {}\n        for camp in campaigns:\n            fullname = camp._fullname\n            bid = camp.total_budget_pennies / max(camp.ndays, 1)\n            camp_ndays = max(1, (camp.end_date - camp.start_date).days)\n            camp_start = camp.start_date.date()\n            days = xrange(camp_ndays)\n            camp_dates = {camp_start + datetime.timedelta(days=i) for i in days}\n\n            for date in camp_dates.intersection(dates):\n                fp_imps = fp_imps_by_date[date][fullname]\n                sr_imps = sr_imps_by_date[date][fullname]\n                fp_clicks = fp_clicks_by_date[date][fullname]\n                sr_clicks = sr_clicks_by_date[date][fullname]\n                key = ReportKey(date, camp.link_id, camp._fullname)\n                item = ReportItem(bid, fp_imps, sr_imps, fp_clicks, sr_clicks)\n                traffic_by_key[key] = item\n        return traffic_by_key\n\n    def make_report(self):\n        campaigns = PromoCampaign._by_link([link._id for link in self.links])\n        campaigns = filter(promote.charged_or_not_needed, campaigns)\n        traffic_by_key = self.get_traffic(campaigns, self.start, self.end)\n\n        def group_and_combine(items_by_key, group_on=None):\n            # combine all items whose keys have the same value for the\n            # attributes in group_on, and create new keys with None values for\n            # the attributes we aren't grouping on.\n            by_group = defaultdict(list)\n            for item_key, item in items_by_key.iteritems():\n                attrs = [getattr(item_key, a) if a in group_on else None\n                    for a in ReportKey._fields]\n                group_key = ReportKey(*attrs)\n                by_group[group_key].append(item)\n\n            new_items_by_key = {}\n            for group_key, items in by_group.iteritems():\n                bid = fp_imps = sr_imps = fp_clicks = sr_clicks = 0\n                for item in items:\n                    bid += item.bid\n                    fp_imps += item.fp_imps\n                    sr_imps += item.sr_imps\n                    fp_clicks += item.fp_clicks\n                    sr_clicks += item.sr_clicks\n                item = ReportItem(bid, fp_imps, sr_imps, fp_clicks, sr_clicks)\n                new_items_by_key[group_key] = item\n            return new_items_by_key\n\n        # make the campaign report\n        if not self.group_by_date:\n            traffic_by_key = group_and_combine(\n                traffic_by_key, group_on=[\"link\", \"campaign\"])\n\n        owners = Account._byID([link.author_id for link in self.links],\n                               data=True)\n        links_by_id = {link._id: link for link in self.links}\n        camps_by_name = {camp._fullname: camp for camp in campaigns}\n\n        self.campaign_report_totals = {\n            'fp_clicks': 0,\n            'fp_imps': 0,\n            'sr_clicks': 0,\n            'sr_imps': 0,\n            'total_clicks': 0,\n            'total_imps': 0,\n            'bid': 0,\n        }\n        self.campaign_report = []\n        for rk in sorted(traffic_by_key):\n            item = traffic_by_key[rk]\n            link = links_by_id[rk.link]\n            camp = camps_by_name[rk.campaign]\n\n            self.campaign_report_totals['fp_clicks'] += item.fp_clicks\n            self.campaign_report_totals['fp_imps'] += item.fp_imps\n            self.campaign_report_totals['sr_clicks'] += item.sr_clicks\n            self.campaign_report_totals['sr_imps'] += item.sr_imps\n            self.campaign_report_totals['bid'] += item.bid\n\n            self.campaign_report.append({\n                'date': rk.date,\n                'link': link._id36,\n                'owner': owners[link.author_id].name,\n                'campaign': camp._id36,\n                'target': camp.target.pretty_name,\n                'bid': format_currency(item.bid, 'USD', locale=c.locale),\n                'fp_impressions': item.fp_imps,\n                'sr_impressions': item.sr_imps,\n                'fp_clicks': item.fp_clicks,\n                'sr_clicks': item.sr_clicks,\n                'total_impressions': item.fp_imps + item.sr_imps,\n                'total_clicks': item.fp_clicks + item.sr_clicks,\n            })\n        crt = self.campaign_report_totals\n        crt['total_clicks'] = crt['sr_clicks'] + crt['fp_clicks']\n        crt['total_imps'] = crt['sr_imps'] + crt['fp_imps']\n        crt['bid'] = format_currency(crt['bid'], 'USD', locale=c.locale)\n        # make the link report\n        traffic_by_key = group_and_combine(\n                traffic_by_key, group_on=[\"link\", \"date\"])\n\n        self.link_report = []\n        for rk in sorted(traffic_by_key):\n            item = traffic_by_key[rk]\n            link = links_by_id[rk.link]\n            self.link_report.append({\n                'date': rk.date,\n                'owner': owners[link.author_id].name,\n                'id36': link._id36,\n                'comments': link.num_comments,\n                'upvotes': link._ups,\n                'downvotes': link._downs,\n                'clicks': item.fp_clicks + item.sr_clicks,\n                'impressions': item.fp_imps + item.sr_imps,\n                'url': link.url,\n            })\n\n\nclass RawString(Templated):\n   def __init__(self, s):\n       self.s = s\n\n   def render(self, *a, **kw):\n       return unsafe(self.s)\n\n\nclass TryCompact(Reddit):\n    def __init__(self, dest, **kw):\n        dest = dest or \"/\"\n        u = UrlParser(dest)\n        u.set_extension(\"compact\")\n        self.compact = u.unparse()\n\n        u.update_query(keep_extension = True)\n        self.like = u.unparse()\n\n        u.set_extension(\"mobile\")\n        self.mobile = u.unparse()\n        Reddit.__init__(self, **kw)\n\nclass AccountActivityPage(BoringPage):\n    def __init__(self):\n        super(AccountActivityPage, self).__init__(_(\"account activity\"))\n\n    def content(self):\n        return UserIPHistory()\n\nclass UserIPHistory(Templated):\n    def __init__(self):\n        self.my_apps = OAuth2Client._by_user_grouped(c.user)\n        self.ips = ips_by_account_id(c.user._id)\n\n        if not c.user_is_admin:\n            self.ips = [\n                ip\n                for ip in self.ips\n                if not ip_address(ip[0]).is_private\n            ]\n        super(UserIPHistory, self).__init__()\n\nclass ApiHelp(Templated):\n    def __init__(self, api_docs, *a, **kw):\n        self.api_docs = api_docs\n        super(ApiHelp, self).__init__(*a, **kw)\n\n\nclass AwardReceived(Templated):\n    pass\n\nclass ConfirmAwardClaim(Templated):\n    pass\n\nclass TimeSeriesChart(Templated):\n    def __init__(self, id, title, interval, columns, rows,\n                 latest_available_data=None, classes=[],\n                 make_period_link=None):\n        self.id = id\n        self.title = title\n        self.interval = interval\n        self.columns = columns\n        self.rows = rows\n        self.latest_available_data = (latest_available_data or\n                                      datetime.datetime.utcnow())\n        self.classes = \" \".join(classes)\n        self.make_period_link = make_period_link\n\n        Templated.__init__(self)\n\nclass InterestBar(Templated):\n    def __init__(self, has_subscribed):\n        self.has_subscribed = has_subscribed\n        Templated.__init__(self)\n\nclass Goldvertisement(Templated):\n    def __init__(self):\n        now = datetime.datetime.now(GOLD_TIMEZONE)\n        today = now.date()\n        tomorrow = now + datetime.timedelta(days=1)\n        end_time = tomorrow.replace(hour=0, minute=0, second=0, microsecond=0)\n        revenue_today = gold_revenue_volatile(today)\n        yesterday = today - datetime.timedelta(days=1)\n        revenue_yesterday = gold_revenue_steady(yesterday)\n        revenue_goal = gold_goal_on(today)\n        revenue_goal_yesterday = gold_goal_on(yesterday)\n\n        if revenue_goal:\n            self.percent_filled = int((revenue_today / revenue_goal) * 100)\n        else:\n            self.percent_filled = 0\n\n        if revenue_goal_yesterday:\n            self.percent_filled_yesterday = int((revenue_yesterday /\n                                                 revenue_goal_yesterday) * 100)\n        else:\n            self.percent_filled_yesterday = 0\n\n        seconds = get_current_value_of_month()\n        delta = datetime.timedelta(seconds=seconds)\n        self.hours_paid = precise_format_timedelta(\n            delta, threshold=5, locale=c.locale)\n\n        self.time_left_today = timeuntil(end_time, precision=60)\n        if c.user.employee:\n            self.goal_today = revenue_goal / 100.0\n            self.goal_yesterday = revenue_goal_yesterday / 100.0\n\n        if c.user_is_loggedin:\n            self.default_type = \"autorenew\"\n        else:\n            self.default_type = \"code\"\n\n        Templated.__init__(self)\n\nclass LinkCommentsSettings(Templated):\n    def __init__(self, link, sort, suggested_sort):\n        Templated.__init__(self)\n        self.sr = link.subreddit_slow\n        self.link = link\n        self.is_author = c.user_is_loggedin and c.user._id == link.author_id\n        self.contest_mode = link.contest_mode\n        self.stickied = link.is_stickied(self.sr)\n        self.stickies_full = self.sr.has_max_stickies\n        self.sendreplies = link.sendreplies\n        self.can_edit = (\n            c.user_is_loggedin and\n            (c.user_is_admin or\n                self.sr.is_moderator_with_perms(c.user, \"posts\"))\n        )\n        self.can_sticky = False\n        if self.can_edit:\n            if self.stickied:\n                # always allow un-stickying things\n                self.can_sticky = True\n            # non deleted/spam self-posts by mods are eligible for stickying\n            else:\n                self.can_sticky = link.is_stickyable()\n        self.sort = sort\n        self.suggested_sort = suggested_sort\n\nclass ModeratorPermissions(Templated):\n    def __init__(self, user, permissions_type, permissions,\n                 editable=False, embedded=False):\n        self.user = user\n        self.permissions = permissions\n        Templated.__init__(self, permissions_type=permissions_type,\n                           editable=editable, embedded=embedded)\n\n    def items(self):\n        return self.permissions.iteritems()\n\nclass ListingChooser(Templated):\n    def __init__(self):\n        Templated.__init__(self)\n        self.sections = defaultdict(list)\n        self.add_item(\"global\", _(\"subscribed\"), site=Frontpage,\n                      description=_(\"your front page\"))\n        self.add_item(\"global\", _(\"explore\"), path=\"/explore\")\n        if c.user_is_loggedin and c.user.gold:\n            self.add_item(\"other\", _(\"everything\"),\n                          path=\"/me/f/all\",\n                          extra_class=\"gold-perks\",\n                          description=_(\"from all subreddits\"))\n        else:\n            self.add_item(\"other\", _(\"everything\"), site=All,\n                          description=_(\"from all subreddits\"))\n        if c.user_is_loggedin and c.user.is_moderator_somewhere:\n            self.add_item(\"other\", _(\"moderating\"), site=Mod,\n                          description=_(\"subreddits you mod\"))\n\n        self.add_item(\"other\", _(\"saved\"), path='/user/%s/saved' % c.user.name)\n\n        gold_multi = g.live_config[\"listing_chooser_gold_multi\"]\n        if c.user_is_loggedin and c.user.gold and gold_multi:\n            self.add_item(\"other\", name=_(\"gold perks\"), path=gold_multi,\n                          extra_class=\"gold-perks\")\n\n        self.show_samples = False\n        if c.user_is_loggedin:\n            multis = LabeledMulti.by_owner(c.user, load_subreddits=False)\n            multis.sort(key=lambda multi: multi.name.lower())\n            for multi in multis:\n                if not multi.is_hidden():\n                    self.add_item(\"multi\", multi.name, site=multi)\n\n            explore_sr = g.live_config[\"listing_chooser_explore_sr\"]\n            if explore_sr:\n                sr = Subreddit._by_name(explore_sr, stale=True)\n                self.add_item(\"multi\", name=_(\"explore multis\"), site=sr)\n\n            self.show_samples = not multis\n\n        if self.show_samples:\n            self.add_samples()\n\n        self.selected_item = self.find_selected()\n        if self.selected_item:\n            self.selected_item[\"selected\"] = True\n\n    def add_item(self, section, name, path=None, site=None, description=None,\n                 extra_class=None):\n        self.sections[section].append({\n            \"name\": name,\n            \"description\": description,\n            \"path\": path or site.user_path,\n            \"site\": site,\n            \"selected\": False,\n            \"extra_class\": extra_class,\n        })\n\n    def add_samples(self):\n        for path in g.live_config[\"listing_chooser_sample_multis\"]:\n            self.add_item(\n                section=\"sample\",\n                name=path.rpartition('/')[2],\n                path=path,\n            )\n\n    def find_selected(self):\n        path = request.path\n        matching = []\n        for item in chain(*self.sections.values()):\n            if item[\"site\"]:\n                if item[\"site\"] == c.site:\n                    matching.append(item)\n            elif path.startswith(item[\"path\"]):\n                matching.append(item)\n\n        matching.sort(key=lambda item: len(item[\"path\"]), reverse=True)\n        return matching[0] if matching else None\n\nclass PolicyView(Templated):\n    pass\n\n\nclass PolicyPage(BoringPage):\n    css_class = 'policy-page'\n    show_infobar = False\n\n    def __init__(self, pagename=None, content=None, **kw):\n        BoringPage.__init__(self, pagename=pagename, show_sidebar=False,\n            content=content, **kw)\n        self.welcomebar = None\n\n    def build_toolbars(self):\n        toolbars = BoringPage.build_toolbars(self)\n        policies_buttons = [\n            NavButton(_('privacy policy'), '/privacypolicy'),\n            NavButton(_('user agreement'), '/useragreement'),\n            NavButton(_('content policy'), '/contentpolicy'),\n        ]\n        policies_menu = NavMenu(policies_buttons, type='tabmenu',\n                                base_path='/help')\n        toolbars.append(policies_menu)\n        return toolbars\n\n\nclass GoogleTagManagerJail(Templated):\n    pass\n\n\nclass GoogleTagManager(Templated):\n    pass\n\n\nclass Newsletter(BoringPage):\n    extra_page_classes = ['newsletter']\n\n    def __init__(self, pagename=None, content=None, **kw):\n        BoringPage.__init__(self, pagename=pagename, show_sidebar=False,\n                            content=content, **kw)\n\n\nclass SubscribeButton(Templated):\n    def __init__(self, sr, bubble_class=None):\n        Templated.__init__(self)\n        self.sr = sr\n        self.data_attrs = {\"sr_name\": sr.name}\n        if bubble_class:\n            self.data_attrs[\"bubble_class\"] = bubble_class\n\n\nclass QuarantineOptoutButton(Templated):\n    def __init__(self, sr, bubble_class=None):\n        Templated.__init__(self)\n        self.sr = sr\n        self.data_attrs = {\"sr_name\": sr.name}\n        if bubble_class:\n            self.data_attrs[\"bubble_class\"] = bubble_class\n\n\nclass SubredditSelector(Templated):\n    def __init__(self, default_sr=None, extra_subreddits=None, required=False,\n                 include_searches=True, include_user_subscriptions=True, class_name=None,\n                 placeholder=None, show_add=False):\n        Templated.__init__(self)\n\n        self.placeholder = placeholder\n        self.class_name = class_name\n        self.show_add = show_add\n\n        if extra_subreddits:\n            self.subreddits = extra_subreddits\n        else:\n            self.subreddits = []\n\n        if include_user_subscriptions:\n            self.subreddits.append((\n                _('your subscribed subreddits'),\n                Subreddit.user_subreddits(c.user, ids=False)\n            ))\n\n        self.default_sr = default_sr\n        self.required = required\n        if include_searches:\n            self.sr_searches = simplejson.dumps(\n                popular_searches(include_over_18=c.over18)\n            )\n        else:\n            self.sr_searches = simplejson.dumps({})\n        self.include_searches = include_searches\n\n    @property\n    def subreddit_names(self):\n        groups = []\n        for title, subreddits in self.subreddits:\n            names = [sr.name for sr in subreddits if sr.can_submit(c.user)]\n            names.sort(key=str.lower)\n            groups.append((title, names))\n        return groups\n\n\nclass ListingSuggestions(Templated):\n    def __init__(self):\n        Templated.__init__(self)\n\n        self.suggestion_type = None\n        if c.default_sr:\n            if c.user_is_loggedin and random.randint(0, 1) == 1:\n                self.suggestion_type = \"explore\"\n                return\n\n            if c.user_is_loggedin:\n                multis = LabeledMulti.by_owner(c.user, load_subreddits=False)\n            else:\n                multis = []\n\n            if multis and c.site in multis:\n                multis.remove(c.site)\n\n            if multis:\n                self.suggestion_type = \"multis\"\n                if len(multis) <= 3:\n                    self.suggestions = multis\n                else:\n                    self.suggestions = random.sample(multis, 3)\n            else:\n                self.suggestion_type = \"random\"\n\n\nclass UnreadMessagesSuggestions(Templated):\n    \"\"\"Let a user mark all as read if they have > 1 page of unread messages.\"\"\"\n    pass\n\n\nclass ExploreItem(Templated):\n    \"\"\"For managing recommended content.\"\"\"\n\n    def __init__(self, item_type, rec_src, sr, link, comment=None):\n        \"\"\"Constructor.\n\n        item_type - string that helps templates know how to render this item.\n        rec_src - code that lets us track where the rec originally came from,\n            useful for comparing performance of data sources or algorithms\n        sr and link are required\n        comment is optional\n\n        See r2.lib.recommender for valid values of item_type and rec_src.\n\n        \"\"\"\n        self.sr = sr\n        self.link = link\n        self.comment = comment\n        self.type = item_type\n        self.src = rec_src\n        Templated.__init__(self)\n\n    def is_over18(self):\n        return self.sr.over_18 or self.link.is_nsfw\n\n\nclass ExploreItemListing(Templated):\n    def __init__(self, recs, settings):\n        self.things = []\n        self.settings = settings\n        if recs:\n            links, srs = zip(*[(rec.link, rec.sr) for rec in recs])\n            wrapped_links = {l._id: l for l in wrap_links(links).things}\n            wrapped_srs = {sr._id: sr for sr in wrap_things(*srs)}\n            for rec in recs:\n                if rec.link._id in wrapped_links:\n                    rec.link = wrapped_links[rec.link._id]\n                    rec.sr = wrapped_srs[rec.sr._id]\n                    self.things.append(rec)\n        Templated.__init__(self)\n\n\nclass TrendingSubredditsBar(Templated):\n    def __init__(self, subreddit_names, comment_url, comment_count):\n        Templated.__init__(self)\n        self.subreddit_names = subreddit_names\n        self.comment_url = comment_url\n        self.comment_count = comment_count\n        self.comment_label, self.comment_label_cls = \\\n            comment_label(comment_count)\n\n\nclass GeotargetNotice(Templated):\n    def __init__(self, city_target=False):\n        self.targeting_level = \"city\" if city_target else \"country\"\n        if city_target:\n            text = _(\"this promoted link uses city level targeting and may \"\n                     \"have been shown to you because of your location. \"\n                     \"([learn more](%(link)s))\")\n        else:\n            text = _(\"this promoted link uses country level targeting and may \"\n                     \"have been shown to you because of your location. \"\n                     \"([learn more](%(link)s))\")\n        more_link = \"/wiki/targetingbycountrycity\"\n        self.text = text % {\"link\": more_link}\n        Templated.__init__(self)\n\n\nclass ShareClose(Templated):\n    pass\n"
  },
  {
    "path": "r2/r2/lib/pages/things.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom r2.config import feature\nfrom r2.lib.db.thing import NotFound\nfrom r2.lib.menus import (\n  JsButton,\n  NavButton,\n  NavMenu,\n  Styled,\n)\nfrom r2.lib.wrapped import Wrapped\nfrom r2.models import Comment, LinkListing, Link, Message, PromotedLink, Report\nfrom r2.models import IDBuilder, Thing\nfrom r2.lib.utils import tup\nfrom r2.lib.strings import Score\nfrom r2.lib.promote import *\nfrom datetime import datetime\nfrom pylons import tmpl_context as c\nfrom pylons import app_globals as g\nfrom pylons.i18n import _, ungettext\n\nclass PrintableButtons(Styled):\n    cachable = False\n\n    def __init__(self, style, thing,\n                 show_delete = False, show_report = True,\n                 show_distinguish = False, show_lock = False,\n                 show_unlock = False, show_marknsfw = False,\n                 show_unmarknsfw = False, is_link=False,\n                 show_flair=False, show_rescrape=False,\n                 show_givegold=False, show_sticky_comment=False,\n                 **kw):\n        show_ignore = thing.show_reports\n        approval_checkmark = getattr(thing, \"approval_checkmark\", None)\n        show_approve = (thing.show_spam or show_ignore or\n                        (is_link and approval_checkmark is None)) and not thing._deleted\n\n        Styled.__init__(self, style = style,\n                        thing = thing,\n                        fullname = thing._fullname,\n                        can_ban = thing.can_ban and not thing._deleted,\n                        show_spam = thing.show_spam,\n                        show_reports = thing.show_reports,\n                        show_ignore = show_ignore,\n                        approval_checkmark = approval_checkmark,\n                        show_delete = show_delete,\n                        show_approve = show_approve,\n                        show_report = show_report,\n                        show_distinguish = show_distinguish,\n                        show_sticky_comment=show_sticky_comment,\n                        show_lock = show_lock,\n                        show_unlock = show_unlock,\n                        show_marknsfw = show_marknsfw,\n                        show_unmarknsfw = show_unmarknsfw,\n                        show_flair = show_flair,\n                        show_rescrape=show_rescrape,\n                        show_givegold=show_givegold,\n                        **kw)\n        \nclass BanButtons(PrintableButtons):\n    def __init__(self, thing,\n                 show_delete = False, show_report = True):\n        PrintableButtons.__init__(self, \"banbuttons\", thing)\n\nclass LinkButtons(PrintableButtons):\n    def __init__(self, thing, comments = True, delete = True, report = True):\n        # is the current user the author?\n        is_author = (c.user_is_loggedin and thing.author and\n                     c.user.name == thing.author.name)\n        # do we show the report button?\n        show_report = not is_author and not thing._deleted and report\n\n        show_share = ((c.user_is_loggedin or not g.read_only_mode) and\n                      not thing.subreddit.quarantine and\n                      not thing.disable_comments and\n                      not thing._deleted)\n\n        # if they are the author, can they edit it?\n        thing_editable = getattr(thing, 'editable', True)\n        thing_takendown = getattr(thing, 'admin_takedown', False)\n        editable = is_author and thing_editable and not thing_takendown\n\n        show_lock = show_unlock = False\n        lockable = thing.can_ban and not thing.archived\n        if lockable:\n            show_lock = not thing.locked\n            show_unlock = not show_lock\n\n        show_marknsfw = show_unmarknsfw = False\n        show_rescrape = False\n        if thing.can_ban or is_author or (thing.promoted and c.user_is_sponsor):\n            if not thing.nsfw:\n                show_marknsfw = True\n            else:\n                show_unmarknsfw = True\n\n            if (not thing.is_self and\n                    not (thing.has_thumbnail or thing.media_object)):\n                show_rescrape = True\n\n        show_givegold = thing.can_gild and (c.permalink_page or c.profilepage)\n\n        # do we show the delete button?\n        show_delete = is_author and delete and not thing._deleted\n        # disable the delete button for live sponsored links\n        if (is_promoted(thing) and not c.user_is_sponsor):\n            show_delete = False\n\n        # do we show the distinguish button? among other things,\n        # we never want it to appear on link listings -- only\n        # comments pages\n        show_distinguish = (is_author and\n                            (thing.can_ban or  # Moderator distinguish\n                             c.user.employee or  # Admin distinguish\n                             c.user_special_distinguish)\n                            and getattr(thing, \"expand_children\", False))\n\n        permalink = thing.permalink\n\n        kw = {}\n        if thing.promoted is not None:\n            if getattr(thing, \"campaign\", False):\n                permalink = update_query(permalink, {\n                    \"campaign\": thing.campaign,\n                })\n\n            now = datetime.now(g.tz)\n            kw = dict(promo_url = promo_edit_url(thing),\n                      promote_status = getattr(thing, \"promote_status\", 0),\n                      user_is_sponsor = c.user_is_sponsor,\n                      traffic_url = promo_traffic_url(thing),\n                      is_author = thing.is_author,\n                      )\n\n            if c.user_is_sponsor:\n                kw[\"is_awaiting_fraud_review\"] = is_awaiting_fraud_review(thing)\n                kw[\"payment_flagged_reason\"] = thing.payment_flagged_reason\n                kw[\"hide_after_seen\"] = getattr(thing, \"hide_after_seen\", False)\n                kw[\"show_approval\"] = thing.promoted and not thing._deleted\n\n        PrintableButtons.__init__(self, 'linkbuttons', thing, \n                                  # user existence and preferences\n                                  is_loggedin = c.user_is_loggedin,\n                                  # comment link params\n                                  comment_label = thing.comment_label,\n                                  commentcls = thing.commentcls,\n                                  permalink  = permalink,\n                                  # button visibility\n                                  saved = thing.saved,\n                                  editable = editable, \n                                  deleted = thing._deleted,\n                                  hidden = thing.hidden, \n                                  ignore_reports = thing.ignore_reports,\n                                  show_delete = show_delete,\n                                  show_report = show_report and c.user_is_loggedin,\n                                  mod_reports=thing.mod_reports,\n                                  user_reports=thing.user_reports,\n                                  show_distinguish = show_distinguish,\n                                  distinguished=thing.distinguished,\n                                  show_lock = show_lock,\n                                  show_unlock = show_unlock,\n                                  show_marknsfw = show_marknsfw,\n                                  show_unmarknsfw = show_unmarknsfw,\n                                  show_flair = thing.can_flair,\n                                  show_rescrape=show_rescrape,\n                                  show_givegold=show_givegold,\n                                  show_comments = comments,\n                                  show_share=show_share,\n                                  # promotion\n                                  promoted = thing.promoted,\n                                  is_link = True,\n                                  **kw)\n\nclass CommentButtons(PrintableButtons):\n    def __init__(self, thing, delete = True, report = True):\n        # is the current user the author?\n        is_author = thing.is_author\n\n        # if they are the author, can they edit it?\n        thing_editable = getattr(thing, 'editable', True)\n        thing_takendown = getattr(thing, 'admin_takedown', False)\n        editable = is_author and thing_editable and not thing_takendown\n\n        # do we show the report button?\n        show_report = not is_author and report and thing.can_reply\n        # do we show the delete button?\n        show_delete = is_author and delete and not thing._deleted\n        suppress_reply_buttons = getattr(thing, 'suppress_reply_buttons', False)\n\n        if thing.link.is_archived(thing.subreddit):\n            suppress_reply_buttons = True\n\n        show_distinguish = (is_author and\n                            (thing.can_ban or  # Moderator distinguish\n                             c.user.employee or  # Admin distinguish\n                             c.user_special_distinguish))\n\n        show_sticky_comment = (feature.is_enabled('sticky_comments') and\n                               thing.is_stickyable and\n                               is_author and\n                               thing.can_ban)\n\n        show_givegold = thing.can_gild\n\n        embed_button = False\n        \n        show_admin_context = c.user_is_admin\n\n        if thing.can_embed:\n            embed_button = JsButton(\"embed\",\n                css_class=\"embed-comment\",\n                data={\n                    \"media\": g.media_domain or g.domain,\n                    \"comment\": thing.permalink,\n                    \"link\": thing.link.make_permalink(thing.subreddit),\n                    \"title\": thing.link.title,\n                    \"root\": (\"true\" if thing.parent_id is None else \"false\"),\n                })\n\n            embed_button.build()\n\n        PrintableButtons.__init__(self, \"commentbuttons\", thing,\n                                  is_author = is_author, \n                                  profilepage = c.profilepage,\n                                  permalink = thing.permalink,\n                                  saved = thing.saved,\n                                  editable = editable,\n                                  ignore_reports = thing.ignore_reports,\n                                  full_comment_path = thing.full_comment_path,\n                                  full_comment_count = thing.full_comment_count,\n                                  deleted = thing.deleted,\n                                  parent_permalink = thing.parent_permalink, \n                                  can_reply = thing.can_reply,\n                                  locked = thing.link.locked,\n                                  suppress_reply_buttons = suppress_reply_buttons,\n                                  show_report=show_report,\n                                  mod_reports=thing.mod_reports,\n                                  user_reports=thing.user_reports,\n                                  show_distinguish = show_distinguish,\n                                  distinguished=thing.distinguished,\n                                  show_sticky_comment=show_sticky_comment,\n                                  show_delete = show_delete,\n                                  show_givegold=show_givegold,\n                                  embed_button=embed_button,\n                                  show_admin_context=show_admin_context,\n        )\n\nclass MessageButtons(PrintableButtons):\n    def __init__(self, thing, delete = False, report = True):\n        was_comment = getattr(thing, 'was_comment', False)\n        permalink = thing.permalink\n        # don't allow replying to self unless it's modmail\n        valid_recipient = (thing.author_id != c.user._id or\n                           thing.sr_id)\n\n        can_reply = (c.user_is_loggedin and\n                     getattr(thing, \"repliable\", True) and\n                     valid_recipient)\n        can_block = True\n        can_mute = False\n        is_admin_message = False\n        show_distinguish = c.user.employee and c.user._id == thing.author_id\n        del_on_recipient = (isinstance(thing, Message) and\n                            thing.del_on_recipient)\n\n        if not was_comment:\n            first_message = thing\n            if getattr(thing, 'first_message', False):\n                first_message = Message._byID(thing.first_message, data=True)\n\n            if thing.sr_id:\n                sr = thing.subreddit_slow\n                is_admin_message = '/r/%s' % sr.name == g.admin_message_acct\n\n                if (sr.is_muted(first_message.author_slow) or\n                        (first_message.to_id and\n                            sr.is_muted(first_message.recipient_slow))):\n                    can_reply = False\n\n                can_mute = sr.can_mute(c.user, thing.author_slow)\n\n        if not was_comment and thing.display_author:\n            can_block = False\n\n        if was_comment:\n            link = thing.link_slow\n            if link.is_archived(thing.subreddit) or link.locked:\n                can_reply = False\n\n        # Allow comment-reply messages to have links to the full thread.\n        if was_comment:\n            self.full_comment_path = thing.link_permalink\n            self.full_comment_count = thing.full_comment_count\n\n        PrintableButtons.__init__(self, \"messagebuttons\", thing,\n                                  profilepage = c.profilepage,\n                                  permalink = permalink,\n                                  was_comment = was_comment,\n                                  unread = thing.new,\n                                  user_is_recipient = thing.user_is_recipient,\n                                  can_reply = can_reply,\n                                  parent_id = getattr(thing, \"parent_id\", None),\n                                  show_report = True,\n                                  show_delete = False,\n                                  can_block = can_block,\n                                  can_mute = can_mute,\n                                  is_admin_message = is_admin_message,\n                                  del_on_recipient=del_on_recipient,\n                                  show_distinguish=show_distinguish,\n                                  distinguished=thing.distinguished,\n                                 )\n\n\ndef make_wrapper(parent_wrapper = Wrapped, **params):\n    def wrapper_fn(thing):\n        w = parent_wrapper(thing)\n        for k, v in params.iteritems():\n            setattr(w, k, v)\n        return w\n    return wrapper_fn\n\n\n# formerly ListingController.builder_wrapper\ndef default_thing_wrapper(**params):\n    def _default_thing_wrapper(thing):\n        w = Wrapped(thing)\n        style = params.get('style', c.render_style)\n        if isinstance(thing, Link):\n            if thing.promoted is not None:\n                w.render_class = PromotedLink\n            elif style == 'htmllite':\n                w.score_fmt = Score.safepoints\n            w.should_incr_counts = style != 'htmllite'\n        return w\n    params['parent_wrapper'] = _default_thing_wrapper\n    return make_wrapper(**params)\n\n# TODO: move this into lib somewhere?\ndef wrap_links(links, wrapper = default_thing_wrapper(),\n               listing_cls = LinkListing, \n               num = None, show_nums = False, nextprev = False, **kw):\n    links = tup(links)\n    if not all(isinstance(x, basestring) for x in links):\n        links = [x._fullname for x in links]\n    b = IDBuilder(links, num = num, wrap = wrapper, **kw)\n    l = listing_cls(b, nextprev = nextprev, show_nums = show_nums)\n    return l.listing()\n\n\ndef hot_links_by_url_listing(url, sr=None, num=None, **kw):\n    try:\n        links_for_url = Link._by_url(url, sr)\n    except NotFound:\n        links_for_url = []\n\n    links_for_url.sort(key=lambda link: link._hot, reverse=True)\n    listing = wrap_links(links_for_url, num=num, **kw)\n    return listing\n\n\ndef wrap_things(*things):\n    \"\"\"Instantiate Wrapped for each thing, calling add_props if available.\"\"\"\n    if not things:\n        return []\n\n    wrapped = [Wrapped(thing) for thing in things]\n    if hasattr(things[0], 'add_props'):\n        # assume all things are of the same type and use the first thing's\n        # add_props to process the list.\n        things[0].add_props(c.user, wrapped)\n    return wrapped\n"
  },
  {
    "path": "r2/r2/lib/pages/trafficpages.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\"\"\"View models for the traffic statistic pages on reddit.\"\"\"\n\nimport collections\nimport datetime\nimport pytz\nimport urllib\n\nfrom pylons.i18n import _\nfrom pylons import request\nfrom pylons import tmpl_context as c\nfrom pylons import app_globals as g\n\nimport babel.core\nfrom babel.dates import format_datetime\nfrom babel.numbers import format_currency\n\nfrom r2.lib import promote\nfrom r2.lib.db.sorts import epoch_seconds\nfrom r2.lib.menus import menu\nfrom r2.lib.menus import NavButton, NamedButton, PageNameNav, NavMenu\nfrom r2.lib.pages.pages import Reddit, TimeSeriesChart, TabbedPane\nfrom r2.lib.promote import cost_per_mille, cost_per_click\nfrom r2.lib.template_helpers import format_number\nfrom r2.lib.utils import Storage, to_date, timedelta_by_name\nfrom r2.lib.wrapped import Templated\nfrom r2.models import Thing, Link, PromoCampaign, traffic\nfrom r2.models.subreddit import Subreddit, _DefaultSR\n\n\nCOLORS = Storage(UPVOTE_ORANGE=\"#ff5700\",\n                 DOWNVOTE_BLUE=\"#9494ff\",\n                 MISCELLANEOUS=\"#006600\")\n\n\nclass TrafficPage(Reddit):\n    \"\"\"Base page template for pages rendering traffic graphs.\"\"\"\n\n    extension_handling = False\n    extra_page_classes = [\"traffic\"]\n\n    def __init__(self, content):\n        Reddit.__init__(self, title=_(\"traffic stats\"), content=content)\n\n    def build_toolbars(self):\n        main_buttons = [NavButton(menu.sitewide, \"/\"),\n                        NamedButton(\"languages\"),\n                        NamedButton(\"adverts\")]\n\n        toolbar = [PageNameNav(\"nomenu\", title=self.title),\n                   NavMenu(main_buttons, base_path=\"/traffic\", type=\"tabmenu\")]\n\n        return toolbar\n\n\nclass SitewideTrafficPage(TrafficPage):\n    \"\"\"Base page for sitewide traffic overview.\"\"\"\n\n    extra_page_classes = TrafficPage.extra_page_classes + [\"traffic-sitewide\"]\n\n    def __init__(self):\n        TrafficPage.__init__(self, SitewideTraffic())\n\n\nclass LanguageTrafficPage(TrafficPage):\n    \"\"\"Base page for interface language traffic summaries or details.\"\"\"\n\n    def __init__(self, langcode):\n        if langcode:\n            content = LanguageTraffic(langcode)\n        else:\n            content = LanguageTrafficSummary()\n\n        TrafficPage.__init__(self, content)\n\n\nclass AdvertTrafficPage(TrafficPage):\n    \"\"\"Base page for advert traffic summaries or details.\"\"\"\n\n    def __init__(self, code):\n        if code:\n            content = AdvertTraffic(code)\n        else:\n            content = AdvertTrafficSummary()\n        TrafficPage.__init__(self, content)\n\n\nclass RedditTraffic(Templated):\n    \"\"\"A generalized content pane for traffic reporting.\"\"\"\n\n    make_period_link = None\n\n    def __init__(self, place):\n        self.place = place\n\n        self.traffic_last_modified = traffic.get_traffic_last_modified()\n        self.traffic_lag = (datetime.datetime.utcnow() -\n                            self.traffic_last_modified)\n\n        self.make_tables()\n\n        Templated.__init__(self)\n\n    def make_tables(self):\n        \"\"\"Create tables to put in the main table area of the page.\n\n        See the stub implementations below for ways to hook into this process\n        without completely overriding this method.\n\n        \"\"\"\n\n        self.tables = []\n\n        for interval in (\"month\", \"day\", \"hour\"):\n            columns = [\n                dict(color=COLORS.UPVOTE_ORANGE,\n                     title=_(\"uniques by %s\" % interval),\n                     shortname=_(\"uniques\")),\n                dict(color=COLORS.DOWNVOTE_BLUE,\n                     title=_(\"pageviews by %s\" % interval),\n                     shortname=_(\"pageviews\")),\n            ]\n\n            data = self.get_data_for_interval(interval, columns)\n\n            title = _(\"traffic by %s\" % interval)\n            graph = TimeSeriesChart(\"traffic-\" + interval,\n                                    title,\n                                    interval,\n                                    columns,\n                                    data,\n                                    self.traffic_last_modified,\n                                    classes=[\"traffic-table\"],\n                                    make_period_link=self.make_period_link,\n                                   )\n            self.tables.append(graph)\n\n        try:\n            self.dow_summary = self.get_dow_summary()\n        except NotImplementedError:\n            self.dow_summary = None\n        else:\n            uniques_total = collections.Counter()\n            pageviews_total = collections.Counter()\n            days_total = collections.Counter()\n\n            # don't include the latest (likely incomplete) day\n            for date, (uniques, pageviews) in self.dow_summary[1:]:\n                dow = date.weekday()\n                uniques_total[dow] += uniques\n                pageviews_total[dow] += pageviews\n                days_total[dow] += 1\n\n            # make a summary of the averages for each day of the week\n            self.dow_summary = []\n            for dow in xrange(7):\n                day_count = days_total[dow]\n                if day_count:\n                    avg_uniques = uniques_total[dow] / day_count\n                    avg_pageviews = pageviews_total[dow] / day_count\n                    self.dow_summary.append((dow,\n                                             (avg_uniques, avg_pageviews)))\n                else:\n                    self.dow_summary.append((dow, (0, 0)))\n\n            # calculate the averages for *any* day of the week\n            mean_uniques = sum(r[1][0] for r in self.dow_summary) / 7.0\n            mean_pageviews = sum(r[1][1] for r in self.dow_summary) / 7.0\n            self.dow_means = (round(mean_uniques), round(mean_pageviews))\n\n    def get_dow_summary(self):\n        \"\"\"Return day-interval data to be aggregated by day of week.\n\n        If implemented, a summary table will be shown on the traffic page\n        with the average per day of week over the data interval given.\n\n        \"\"\"\n        raise NotImplementedError()\n\n    def get_data_for_interval(self, interval, columns):\n        \"\"\"Return data for the main overview at the interval given.\n\n        This data will be shown as a set of graphs at the top of the page and a\n        table for monthly and daily data (hourly is present but hidden by\n        default.)\n\n        \"\"\"\n        raise NotImplementedError()\n\n\ndef make_subreddit_traffic_report(subreddits=None, num=None):\n    \"\"\"Return a report of subreddit traffic in the last full month.\n\n    If given a list of subreddits, those subreddits will be put in the report\n    otherwise the top subreddits by pageviews will be automatically chosen.\n\n    \"\"\"\n\n    if subreddits:\n        subreddit_summary = traffic.PageviewsBySubreddit.last_month(subreddits)\n    else:\n        subreddit_summary = traffic.PageviewsBySubreddit.top_last_month(num)\n\n    report = []\n    for srname, data in subreddit_summary:\n        if srname == _DefaultSR.name:\n            name = _(\"[frontpage]\")\n            url = None\n        elif srname in Subreddit._specials:\n            name = \"[%s]\" % srname\n            url = None\n        else:\n            name = \"/r/%s\" % srname\n            url = name + \"/about/traffic\"\n\n        report.append(((name, url), data))\n    return report\n\n\nclass SitewideTraffic(RedditTraffic):\n    \"\"\"An overview of all traffic to the site.\"\"\"\n    def __init__(self):\n        self.subreddit_summary = make_subreddit_traffic_report(num=250)\n        RedditTraffic.__init__(self, g.domain)\n\n    def get_dow_summary(self):\n        return traffic.SitewidePageviews.history(\"day\")\n\n    def get_data_for_interval(self, interval, columns):\n        return traffic.SitewidePageviews.history(interval)\n\n\nclass LanguageTrafficSummary(Templated):\n    \"\"\"An overview of traffic by interface language on the site.\"\"\"\n\n    def __init__(self):\n        # convert language codes to real names\n        language_summary = traffic.PageviewsByLanguage.top_last_month()\n        locale = c.locale\n        self.language_summary = []\n        for language_code, data in language_summary:\n            name = LanguageTraffic.get_language_name(language_code, locale)\n            self.language_summary.append(((language_code, name), data))\n        Templated.__init__(self)\n\n\nclass AdvertTrafficSummary(RedditTraffic):\n    \"\"\"An overview of traffic for all adverts on the site.\"\"\"\n\n    def __init__(self):\n        RedditTraffic.__init__(self, _(\"adverts\"))\n\n    def make_tables(self):\n        # overall promoted link traffic\n        impressions = traffic.AdImpressionsByCodename.historical_totals(\"day\")\n        clicks = traffic.ClickthroughsByCodename.historical_totals(\"day\")\n        data = traffic.zip_timeseries(impressions, clicks)\n\n        columns = [\n            dict(color=COLORS.UPVOTE_ORANGE,\n                 title=_(\"total impressions by day\"),\n                 shortname=_(\"impressions\")),\n            dict(color=COLORS.DOWNVOTE_BLUE,\n                 title=_(\"total clicks by day\"),\n                 shortname=_(\"clicks\")),\n        ]\n\n        self.totals = TimeSeriesChart(\"traffic-ad-totals\",\n                                      _(\"ad totals\"),\n                                      \"day\",\n                                      columns,\n                                      data,\n                                      self.traffic_last_modified,\n                                      classes=[\"traffic-table\"])\n\n        # get summary of top ads\n        advert_summary = traffic.AdImpressionsByCodename.top_last_month()\n        things = AdvertTrafficSummary.get_things(ad for ad, data\n                                                 in advert_summary)\n        self.advert_summary = []\n        for id, data in advert_summary:\n            name = AdvertTrafficSummary.get_ad_name(id, things=things)\n            url = AdvertTrafficSummary.get_ad_url(id, things=things)\n            self.advert_summary.append(((name, url), data))\n\n    @staticmethod\n    def split_codename(codename):\n        \"\"\"Codenames can be \"fullname_campaign\". Rend the parts asunder.\"\"\"\n        split_code = codename.split(\"_\")\n        fullname = \"_\".join(split_code[:2])\n        campaign = \"_\".join(split_code[2:])\n        return fullname, campaign\n\n    @staticmethod\n    def get_things(codes):\n        \"\"\"Fetch relevant things for a list of ad codenames in batch.\"\"\"\n        fullnames = [AdvertTrafficSummary.split_codename(code)[0]\n                     for code in codes\n                     if code.startswith(Thing._type_prefix)]\n        return Thing._by_fullname(fullnames, data=True, return_dict=True)\n\n    @staticmethod\n    def get_sr_name(name):\n        \"\"\"Return the display name for a subreddit.\"\"\"\n        if name == g.default_sr:\n            return _(\"frontpage\")\n        else:\n            return \"/r/\" + name\n\n    @staticmethod\n    def get_ad_name(code, things=None):\n        \"\"\"Return a human-readable name for an ad given its codename.\n\n        Optionally, a dictionary of things can be passed in so lookups can\n        be done in batch upstream.\n\n        \"\"\"\n\n        if not things:\n            things = AdvertTrafficSummary.get_things([code])\n\n        thing = things.get(code)\n        campaign = None\n\n        # if it's not at first a thing, see if it's a thing with campaign\n        # appended to it.\n        if not thing:\n            fullname, campaign = AdvertTrafficSummary.split_codename(code)\n            thing = things.get(fullname)\n\n        if not thing:\n            if code.startswith(\"dart_\"):\n                srname = code.split(\"_\", 1)[1]\n                srname = AdvertTrafficSummary.get_sr_name(srname)\n                return \"DART: \" + srname\n            else:\n                return code\n        elif isinstance(thing, Link):\n            return \"Link: \" + thing.title\n        elif isinstance(thing, Subreddit):\n            srname = AdvertTrafficSummary.get_sr_name(thing.name)\n            name = \"300x100: \" + srname\n            if campaign:\n                name += \" (%s)\" % campaign\n            return name\n\n    @staticmethod\n    def get_ad_url(code, things):\n        \"\"\"Given a codename, return the canonical URL for its traffic page.\"\"\"\n        thing = things.get(code)\n        if isinstance(thing, Link):\n            return \"/traffic/%s\" % thing._id36\n        return \"/traffic/adverts/%s\" % code\n\n\nclass LanguageTraffic(RedditTraffic):\n    def __init__(self, langcode):\n        self.langcode = langcode\n        name = LanguageTraffic.get_language_name(langcode)\n        RedditTraffic.__init__(self, name)\n\n    def get_data_for_interval(self, interval, columns):\n        return traffic.PageviewsByLanguage.history(interval, self.langcode)\n\n    @staticmethod\n    def get_language_name(language_code, locale=None):\n        if not locale:\n            locale = c.locale\n\n        try:\n            lang_locale = babel.core.Locale.parse(language_code, sep=\"-\")\n        except (babel.core.UnknownLocaleError, ValueError):\n            return language_code\n        else:\n            return lang_locale.get_display_name(locale)\n\n\nclass AdvertTraffic(RedditTraffic):\n    def __init__(self, code):\n        self.code = code\n        name = AdvertTrafficSummary.get_ad_name(code)\n        RedditTraffic.__init__(self, name)\n\n    def get_data_for_interval(self, interval, columns):\n        columns[1][\"title\"] = _(\"impressions by %s\" % interval)\n        columns[1][\"shortname\"] = _(\"impressions\")\n\n        columns += [\n            dict(shortname=_(\"unique clicks\")),\n            dict(color=COLORS.MISCELLANEOUS,\n                 title=_(\"clicks by %s\" % interval),\n                 shortname=_(\"total clicks\")),\n        ]\n\n        imps = traffic.AdImpressionsByCodename.history(interval, self.code)\n        clicks = traffic.ClickthroughsByCodename.history(interval, self.code)\n        return traffic.zip_timeseries(imps, clicks)\n\n\nclass SubredditTraffic(RedditTraffic):\n    def __init__(self):\n        RedditTraffic.__init__(self, \"/r/\" + c.site.name)\n\n        if c.user_is_sponsor:\n            fullname = c.site._fullname\n            codes = traffic.AdImpressionsByCodename.recent_codenames(fullname)\n            self.codenames = [(code,\n                               AdvertTrafficSummary.split_codename(code)[1])\n                               for code in codes]\n\n    @staticmethod\n    def make_period_link(interval, date):\n        date = date.replace(tzinfo=g.tz)  # won't be necessary after tz fixup\n        if interval == \"month\":\n            if date.month != 12:\n                end = date.replace(month=date.month + 1)\n            else:\n                end = date.replace(month=1, year=date.year + 1)\n        else:\n            end = date + timedelta_by_name(interval)\n\n        query = urllib.urlencode({\n            \"syntax\": \"cloudsearch\",\n            \"restrict_sr\": \"on\",\n            \"sort\": \"top\",\n            \"q\": \"timestamp:{:d}..{:d}\".format(int(epoch_seconds(date)),\n                                               int(epoch_seconds(end))),\n        })\n        return \"/r/%s/search?%s\" % (c.site.name, query)\n\n    def get_dow_summary(self):\n        return traffic.PageviewsBySubreddit.history(\"day\", c.site.name)\n\n    def get_data_for_interval(self, interval, columns):\n        pageviews = traffic.PageviewsBySubreddit.history(interval, c.site.name)\n\n        if interval == \"day\":\n            columns.append(dict(color=COLORS.MISCELLANEOUS,\n                                title=_(\"subscriptions by day\"),\n                                shortname=_(\"subscriptions\")))\n\n            sr_name = c.site.name\n            subscriptions = traffic.SubscriptionsBySubreddit.history(interval,\n                                                                     sr_name)\n\n            return traffic.zip_timeseries(pageviews, subscriptions)\n        else:\n            return pageviews\n\n\ndef _clickthrough_rate(impressions, clicks):\n    \"\"\"Return the click-through rate percentage.\"\"\"\n    if impressions:\n        return (float(clicks) / impressions) * 100.\n    else:\n        return 0\n\n\ndef _is_promo_preliminary(end_date):\n    \"\"\"Return if results are preliminary for this promotion.\n\n    Results are preliminary until 1 day after the promotion ends.\n\n    \"\"\"\n\n    now = datetime.datetime.now(g.tz)\n    return end_date + datetime.timedelta(days=1) > now\n\n\ndef get_promo_traffic(thing, start, end):\n    \"\"\"Get traffic for a Promoted Link or PromoCampaign\"\"\"\n    if isinstance(thing, Link):\n        imp_fn = traffic.AdImpressionsByCodename.promotion_history\n        click_fn = traffic.ClickthroughsByCodename.promotion_history\n    elif isinstance(thing, PromoCampaign):\n        imp_fn = traffic.TargetedImpressionsByCodename.promotion_history\n        click_fn = traffic.TargetedClickthroughsByCodename.promotion_history\n\n    imps = imp_fn(thing._fullname, start.replace(tzinfo=None),\n                  end.replace(tzinfo=None))\n    clicks = click_fn(thing._fullname, start.replace(tzinfo=None),\n                      end.replace(tzinfo=None))\n\n    if imps and not clicks:\n        clicks = [(imps[0][0], (0,))]\n    elif clicks and not imps:\n        imps = [(clicks[0][0], (0,))]\n\n    history = traffic.zip_timeseries(imps, clicks, order=\"ascending\")\n    return history\n\n\ndef get_billable_traffic(campaign):\n    \"\"\"Get traffic for dates when PromoCampaign is active.\"\"\"\n    start, end = promote.get_traffic_dates(campaign)\n    return get_promo_traffic(campaign, start, end)\n\n\ndef is_early_campaign(campaign):\n    # traffic by campaign was only recorded starting 2012/9/12\n    return campaign.end_date < datetime.datetime(2012, 9, 12, 0, 0, tzinfo=g.tz)\n\n\ndef is_launched_campaign(campaign):\n    now = datetime.datetime.now(g.tz).date()\n    return (promote.charged_or_not_needed(campaign) and\n            campaign.start_date.date() <= now)\n\n\nclass PromotedLinkTraffic(Templated):\n    def __init__(self, thing, campaign, before, after):\n        self.thing = thing\n        self.campaign = campaign\n        self.before = before\n        self.after = after\n        self.period = datetime.timedelta(days=7)\n        self.prev = None\n        self.next = None\n        self.has_live_campaign = False\n        self.has_early_campaign = False\n        self.detail_name = ('campaign %s' % campaign._id36 if campaign\n                                                           else 'all campaigns')\n\n        editable = c.user_is_sponsor or c.user._id == thing.author_id\n        self.traffic_last_modified = traffic.get_traffic_last_modified()\n        self.traffic_lag = (datetime.datetime.utcnow() -\n                            self.traffic_last_modified)\n        self.make_hourly_table(campaign or thing)\n        self.make_campaign_table()\n        Templated.__init__(self)\n\n    @classmethod\n    def make_campaign_table_row(cls, id, start, end, target, location,\n            budget_dollars, spent, paid_impressions, impressions, clicks,\n            is_live, is_active, url, is_total):\n\n        if impressions:\n            cpm = format_currency(promote.cost_per_mille(spent, impressions),\n                                  'USD', locale=c.locale)\n        else:\n            cpm = '---'\n\n        if clicks:\n            cpc = format_currency(promote.cost_per_click(spent, clicks), 'USD',\n                                  locale=c.locale)\n            ctr = format_number(_clickthrough_rate(impressions, clicks))\n        else:\n            cpc = '---'\n            ctr = '---'\n\n        return {\n            'id': id,\n            'start': start,\n            'end': end,\n            'target': target,\n            'location': location,\n            'budget': format_currency(budget_dollars, 'USD', locale=c.locale),\n            'spent': format_currency(spent, 'USD', locale=c.locale),\n            'impressions_purchased': format_number(paid_impressions),\n            'impressions_delivered': format_number(impressions),\n            'cpm': cpm,\n            'clicks': format_number(clicks),\n            'cpc': cpc,\n            'ctr': ctr,\n            'live': is_live,\n            'active': is_active,\n            'url': url,\n            'csv': url + '.csv',\n            'total': is_total,\n        }\n\n    def make_campaign_table(self):\n        campaigns = PromoCampaign._by_link(self.thing._id)\n\n        total_budget_dollars = 0.\n        total_spent = 0\n        total_paid_impressions = 0\n        total_impressions = 0\n        total_clicks = 0\n\n        self.campaign_table = []\n        for camp in campaigns:\n            if not is_launched_campaign(camp):\n                continue\n\n            is_live = camp.is_live_now()\n            self.has_early_campaign |= is_early_campaign(camp)\n            self.has_live_campaign |= is_live\n\n            history = get_billable_traffic(camp)\n            impressions, clicks = 0, 0\n            for date, (imp, click) in history:\n                impressions += imp\n                clicks += click\n\n            start = to_date(camp.start_date).strftime('%Y-%m-%d')\n            end = to_date(camp.end_date).strftime('%Y-%m-%d')\n            target = camp.target.pretty_name\n            location = camp.location_str\n            spent = promote.get_spent_amount(camp)\n            is_active = self.campaign and self.campaign._id36 == camp._id36\n            url = '/traffic/%s/%s' % (self.thing._id36, camp._id36)\n            is_total = False\n            campaign_budget_dollars = camp.total_budget_dollars\n            row = self.make_campaign_table_row(camp._id36,\n                                               start=start,\n                                               end=end,\n                                               target=target,\n                                               location=location,\n                                               budget_dollars=campaign_budget_dollars,\n                                               spent=spent,\n                                               paid_impressions=camp.impressions,\n                                               impressions=impressions,\n                                               clicks=clicks,\n                                               is_live=is_live,\n                                               is_active=is_active,\n                                               url=url,\n                                               is_total=is_total)\n            self.campaign_table.append(row)\n\n            total_budget_dollars += campaign_budget_dollars\n            total_spent += spent\n            total_paid_impressions += camp.impressions\n            total_impressions += impressions\n            total_clicks += clicks\n\n        # total row\n        start = '---'\n        end = '---'\n        target = '---'\n        location = '---'\n        is_live = False\n        is_active = not self.campaign\n        url = '/traffic/%s' % self.thing._id36\n        is_total = True\n        row = self.make_campaign_table_row(_('total'),\n                                           start=start,\n                                           end=end,\n                                           target=target,\n                                           location=location,\n                                           budget_dollars=total_budget_dollars,\n                                           spent=total_spent,\n                                           paid_impressions=total_paid_impressions,\n                                           impressions=total_impressions,\n                                           clicks=total_clicks,\n                                           is_live=is_live,\n                                           is_active=is_active,\n                                           url=url,\n                                           is_total=is_total)\n        self.campaign_table.append(row)\n\n    def check_dates(self, thing):\n        \"\"\"Shorten range for display and add next/prev buttons.\"\"\"\n        start, end = promote.get_traffic_dates(thing)\n\n        # Check date of latest traffic (campaigns can end early).\n        history = list(get_promo_traffic(thing, start, end))\n        if history:\n            end = max(date for date, data in history)\n            end = end.replace(tzinfo=g.tz)  # get_promo_traffic returns tz naive\n                                            # datetimes but is actually g.tz\n\n        if self.period:\n            display_start = self.after\n            display_end = self.before\n\n            if not display_start and not display_end:\n                display_end = end\n                display_start = end - self.period\n            elif not display_end:\n                display_end = display_start + self.period\n            elif not display_start:\n                display_start = display_end - self.period\n\n            if display_start > start:\n                p = request.GET.copy()\n                p.update({\n                    'after': None,\n                    'before': display_start.strftime('%Y%m%d%H'),\n                })\n                self.prev = '%s?%s' % (request.path, urllib.urlencode(p))\n            else:\n                display_start = start\n\n            if display_end < end:\n                p = request.GET.copy()\n                p.update({\n                    'after': display_end.strftime('%Y%m%d%H'),\n                    'before': None,\n                })\n                self.next = '%s?%s' % (request.path, urllib.urlencode(p))\n            else:\n                display_end = end\n        else:\n            display_start, display_end = start, end\n\n        return display_start, display_end\n\n    @classmethod\n    def get_hourly_traffic(cls, thing, start, end):\n        \"\"\"Retrieve hourly traffic for a Promoted Link or PromoCampaign.\"\"\"\n        history = get_promo_traffic(thing, start, end)\n        computed_history = []\n        for date, data in history:\n            imps, clicks = data\n            ctr = _clickthrough_rate(imps, clicks)\n\n            date = date.replace(tzinfo=pytz.utc)\n            date = date.astimezone(pytz.timezone(\"EST\"))\n            datestr = format_datetime(\n                date,\n                locale=c.locale,\n                format=\"yyyy-MM-dd HH:mm zzz\",\n            )\n            computed_history.append((date, datestr, data + (ctr,)))\n        return computed_history\n\n    def make_hourly_table(self, thing):\n        start, end = self.check_dates(thing)\n        self.history = self.get_hourly_traffic(thing, start, end)\n\n        self.total_impressions, self.total_clicks = 0, 0\n        for date, datestr, data in self.history:\n            imps, clicks, ctr = data\n            self.total_impressions += imps\n            self.total_clicks += clicks\n        if self.total_impressions > 0:\n            self.total_ctr = _clickthrough_rate(self.total_impressions,\n                                                self.total_clicks)\n        # XXX: _is_promo_preliminary correctly expects tz-aware datetimes\n        # because it's also used with datetimes from promo code. this hack\n        # relies on the fact that we're storing UTC w/o timezone info.\n        # TODO: remove this when traffic is correctly using timezones.\n        end_aware = end.replace(tzinfo=g.tz)\n        self.is_preliminary = _is_promo_preliminary(end_aware)\n\n    @classmethod\n    def as_csv(cls, thing):\n        \"\"\"Return the traffic data in CSV format for reports.\"\"\"\n\n        import csv\n        import cStringIO\n\n        start, end = promote.get_traffic_dates(thing)\n        history = cls.get_hourly_traffic(thing, start, end)\n\n        out = cStringIO.StringIO()\n        writer = csv.writer(out)\n\n        writer.writerow((_(\"date and time (UTC)\"),\n                         _(\"impressions\"),\n                         _(\"clicks\"),\n                         _(\"click-through rate (%)\")))\n        for date, datestr, values in history:\n            # flatten (date, datestr, value-tuple) to (date, value1, value2...)\n            writer.writerow((date,) + values)\n\n        return out.getvalue()\n\n\nclass SubredditTrafficReport(Templated):\n    def __init__(self):\n        self.srs, self.invalid_srs, self.report = [], [], []\n\n        self.textarea = request.params.get(\"subreddits\")\n        if self.textarea:\n            requested_srs = [srname.strip()\n                             for srname in self.textarea.splitlines()]\n            subreddits = Subreddit._by_name(requested_srs)\n\n            for srname in requested_srs:\n                if srname in subreddits:\n                    self.srs.append(srname)\n                else:\n                    self.invalid_srs.append(srname)\n\n            if subreddits:\n                self.report = make_subreddit_traffic_report(subreddits.values())\n\n            param = urllib.quote(self.textarea)\n            self.csv_url = \"/traffic/subreddits/report.csv?subreddits=\" + param\n\n        Templated.__init__(self)\n\n    def as_csv(self):\n        \"\"\"Return the traffic data in CSV format for reports.\"\"\"\n\n        import csv\n        import cStringIO\n\n        out = cStringIO.StringIO()\n        writer = csv.writer(out)\n\n        writer.writerow((_(\"subreddit\"),\n                         _(\"uniques\"),\n                         _(\"pageviews\")))\n        for (name, url), (uniques, pageviews) in self.report:\n            writer.writerow((name, uniques, pageviews))\n\n        return out.getvalue()\n"
  },
  {
    "path": "r2/r2/lib/pages/wiki.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom r2.lib.pages.pages import (\n    AutoModeratorConfig,\n    RawCode,\n    Reddit,\n    SubredditStylesheetSource,\n)\nfrom pylons import tmpl_context as c\nfrom r2.lib.wrapped import Templated\nfrom r2.lib.menus import PageNameNav\nfrom r2.lib.validator.wiki import this_may_revise\nfrom r2.lib.filters import wikimarkdown, safemarkdown\nfrom pylons.i18n import _\n\nclass WikiView(Templated):\n    def __init__(self, content, edit_by, edit_date, may_revise=False,\n                 page=None, diff=None, renderer='wiki'):\n        self.page_content_md = content\n        if renderer == 'wiki':\n            self.page_content = wikimarkdown(content)\n        elif renderer == 'reddit':\n            self.page_content = safemarkdown(content)\n        elif renderer == 'stylesheet':\n            self.page_content = SubredditStylesheetSource(content).render()\n        elif renderer == \"automoderator\":\n            self.page_content = AutoModeratorConfig(content).render()\n        elif renderer == \"rawcode\":\n            self.page_content = RawCode(content).render()\n\n        self.renderer = renderer\n        self.page = page\n        self.diff = diff\n        self.edit_by = edit_by\n        self.may_revise = may_revise\n        self.edit_date = edit_date\n        self.base_url = c.wiki_base_url\n        Templated.__init__(self)\n\nclass WikiPageNotFound(Templated):\n    def __init__(self, page):\n        self.page = page\n        self.base_url = c.wiki_base_url\n        Templated.__init__(self)\n\nclass WikiPageListing(Templated):\n    def __init__(self, pages, linear_pages, page=None):\n        self.pages = pages\n        self.page = page\n        self.linear_pages = linear_pages\n        self.base_url = c.wiki_base_url\n        Templated.__init__(self)\n\nclass WikiEditPage(Templated):\n    def __init__(self, page_content='', previous='', page=None):\n        self.page_content = page_content\n        self.page = page\n        self.previous = previous\n        self.base_url = c.wiki_base_url\n        Templated.__init__(self)\n\nclass WikiPageSettings(Templated):\n    def __init__(self, settings, mayedit, show_editors=True,\n                 show_settings=True, page=None, **context):\n        self.permlevel = settings['permlevel']\n        self.listed = settings['listed']\n        self.show_settings = show_settings\n        self.show_editors = show_editors\n        self.page = page\n        self.base_url = c.wiki_base_url\n        self.mayedit = mayedit\n        Templated.__init__(self)\n\nclass WikiPageRevisions(Templated):\n    def __init__(self, revisions, page=None):\n        self.listing = revisions\n        self.page = page\n        Templated.__init__(self)\n\nclass WikiPageDiscussions(Templated):\n    def __init__(self, listing, page=None):\n        self.listing = listing\n        self.page = page\n        Templated.__init__(self)\n\nclass WikiBasePage(Reddit):\n    extra_page_classes = ['wiki-page']\n    \n    def __init__(self, content, page=None, may_revise=False,\n                 actionless=False, alert=None, description=None, \n                 showtitle=False, **context):\n        pageactions = []\n        if not actionless and page:\n            pageactions += [(page, _(\"view\"), False, 'wikiview')]\n            if may_revise:\n                pageactions += [('edit', _(\"edit\"), True, 'wikiedit')]\n            pageactions += [('revisions/%s' % page, _(\"history\"), False, 'wikirevisions')]\n            pageactions += [('discussions', _(\"talk\"), True, 'wikidiscussions')]\n            if c.is_wiki_mod and may_revise:\n                pageactions += [('settings', _(\"settings\"), True, 'wikisettings')]\n\n        action = context.get('wikiaction', (page, 'wiki'))\n        \n        if alert:\n            context['infotext'] = alert\n        elif c.wikidisabled:\n            context['infotext'] = _(\"this wiki is currently disabled, only mods may interact with this wiki\")\n        \n        self.pageactions = pageactions\n        self.page = page\n        self.base_url = c.wiki_base_url\n        self.action = action\n        self.description = description\n        \n        if showtitle:\n            self.pagetitle = action[1]\n        else:\n            self.pagetitle = None\n\n        page_classes = None\n\n        if page and \"title\" not in context:\n            context[\"title\"] = _(\"%(page)s - %(site)s\") % {\n                \"site\": c.site.name,\n                \"page\": page}\n            page_classes = ['wiki-page-%s' % page.replace('/', '-')]\n\n        Reddit.__init__(self, extra_js_config={'wiki_page': page}, \n                        show_wiki_actions=True, page_classes=page_classes,\n                        content=content, short_title=page, **context)\n\n    def content(self):\n        return self._content\n\nclass WikiPageView(WikiBasePage):\n    def __init__(self, content, page, diff=None, renderer='wiki', **context):\n        may_revise = context.get('may_revise')\n        if not content and not context.get('alert'):\n            if may_revise:\n                context['alert'] = _(\"this page is empty, edit it to add some content.\")\n        content = WikiView(content, context.get('edit_by'), context.get('edit_date'), \n                           may_revise=may_revise, page=page, diff=diff, renderer=renderer)\n        WikiBasePage.__init__(self, content, page=page, **context)\n\nclass WikiNotFound(WikiBasePage):\n    def __init__(self, page, **context):\n        content = WikiPageNotFound(page)\n        context['alert'] = _(\"page %s does not exist in this subreddit\") % page\n        context['actionless'] = True\n        WikiBasePage.__init__(self, content, page=page, **context)\n\nclass WikiCreate(WikiBasePage):\n    def __init__(self, page, **context):\n        context['alert'] = _(\"page %s does not exist in this subreddit\") % page\n        context['actionless'] = True\n        content = WikiEditPage(page=page)\n        WikiBasePage.__init__(self, content, page, **context)\n\nclass WikiEdit(WikiBasePage):\n    def __init__(self, content, previous, page, **context):\n        content = WikiEditPage(content, previous, page)\n        context['wikiaction'] = ('edit', _(\"editing\"))\n        WikiBasePage.__init__(self, content, page=page, **context)\n\nclass WikiSettings(WikiBasePage):\n    def __init__(self, settings, mayedit, page, restricted, **context):\n        content = WikiPageSettings(settings, mayedit, page=page, **context)\n        if restricted:\n            context['alert'] = _(\"This page is restricted, only moderators may edit it.\")\n        context['wikiaction'] = ('settings', _(\"settings\"))\n        WikiBasePage.__init__(self, content, page=page, **context)\n\nclass WikiRevisions(WikiBasePage):\n    def __init__(self, revisions, page, **context):\n        content = WikiPageRevisions(revisions, page)\n        context['wikiaction'] = ('revisions/%s' % page, _(\"revisions\"))\n        WikiBasePage.__init__(self, content, page=page, **context)\n\nclass WikiRecent(WikiBasePage):\n    def __init__(self, revisions, **context):\n        content = WikiPageRevisions(revisions)\n        context['wikiaction'] = ('revisions', _(\"Viewing recent revisions for /r/%s\") % c.wiki_id)\n        WikiBasePage.__init__(self, content, showtitle=True, **context)\n\nclass WikiListing(WikiBasePage):\n    def __init__(self, pages, linear_pages, **context):\n        content = WikiPageListing(pages, linear_pages)\n        context['wikiaction'] = ('pages', _(\"Viewing pages for /r/%s\") % c.wiki_id)\n        description = [_(\"Below is a list of pages in this wiki visible to you in this subreddit.\")]\n        WikiBasePage.__init__(self, content, description=description, showtitle=True, **context)\n\nclass WikiDiscussions(WikiBasePage):\n    def __init__(self, listing, page, **context):\n        content = WikiPageDiscussions(listing, page)\n        context['wikiaction'] = ('discussions', _(\"discussions\"))\n        description = [_(\"Discussions are site-wide links to this wiki page.\"),\n                       _(\"Submit a link to this wiki page or see other discussions about this wiki page.\")]\n        WikiBasePage.__init__(self, content, page=page, description=description, **context)\n\n"
  },
  {
    "path": "r2/r2/lib/permissions.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom pylons.i18n import N_\n\n\nclass PermissionSet(dict):\n    ALL = 'all'\n\n    info = None\n\n    def __init__(self, *args, **kwargs):\n        super(PermissionSet, self).__init__(*args, **kwargs)\n\n    @classmethod\n    def loads(cls, encoded, validate=False):\n        if not encoded:\n            return cls()\n        result = cls(((term[1:], term[0] == '+')\n                     for term in encoded.split(',')))\n        if result.get(cls.ALL) == False:\n            del result[cls.ALL]\n        if validate and not result.is_valid():\n            raise ValueError\n        return result\n\n    def dumps(self):\n        if self.is_superuser():\n            return '+all'\n        return ','.join('-+'[bool(v)] + k for k, v in sorted(self.iteritems()))\n\n    def is_superuser(self):\n        return bool(super(PermissionSet, self).get(self.ALL))\n\n    def is_valid(self):\n        if not self.info:\n            return False\n        for k in self:\n            if k != self.ALL and k not in self.info:\n                return False\n        return True\n\n    def get(self, key, default=None):\n        if self.info and self.is_superuser():\n            return True if key in self.info else default\n        return super(PermissionSet, self).get(key, default)\n\n    def __getitem__(self, key):\n        if self.info and self.is_superuser():\n            return key == self.ALL or key in self.info\n        return super(PermissionSet, self).get(key, False)\n\n\nclass ModeratorPermissionSet(PermissionSet):\n    info = dict(\n        access=dict(\n            title=N_('access'),\n            description=N_('manage the lists of contributors and banned/muted users'),\n        ),\n        config=dict(\n            title=N_('config'),\n            description=N_('edit settings, sidebar, css, images, and AutoModerator config'),\n        ),\n        flair=dict(\n            title=N_('flair'),\n            description=N_('manage user flair, link flair, and flair templates'),\n        ),\n        mail=dict(\n            title=N_('mail'),\n            description=N_('read and reply to moderator mail'),\n        ),\n        posts=dict(\n            title=N_('posts'),\n            description=N_(\n                'use the approve, remove, spam, distinguish, and nsfw buttons'),\n        ),\n        wiki=dict(\n            title=N_('wiki'),\n            description=N_('manage the wiki and access to the wiki'),\n        ),\n    )\n\n    @classmethod\n    def loads(cls, encoded, **kwargs):\n        if encoded is None:\n            return cls(all=True)\n        return super(ModeratorPermissionSet, cls).loads(encoded, **kwargs)\n"
  },
  {
    "path": "r2/r2/lib/plugin.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport sys\nimport os.path\nimport pkg_resources\nfrom collections import OrderedDict\n\n\nclass Plugin(object):\n    js = {}\n    config = {}\n    live_config = {}\n    needs_static_build = False\n    needs_translation = True\n    errors = {}\n    source_root_url = None\n\n    def __init__(self, entry_point):\n        self.entry_point = entry_point\n\n    @property\n    def name(self):\n        return self.entry_point.name\n\n    @property\n    def path(self):\n        module = sys.modules[type(self).__module__]\n        return os.path.dirname(module.__file__)\n\n    @property\n    def template_dir(self):\n        \"\"\"Add module/templates/ as a template directory.\"\"\"\n        return os.path.join(self.path, 'templates')\n\n    @property\n    def static_dir(self):\n        return os.path.join(self.path, 'public')\n\n    def on_load(self, g):\n        pass\n\n    def add_js(self, module_registry=None):\n        if not module_registry:\n            from r2.lib import js\n            module_registry = js.module\n\n        for name, module in self.js.iteritems():\n            if name not in module_registry:\n                module_registry[name] = module\n            else:\n                module_registry[name].extend(module)\n\n    def declare_queues(self, queues):\n        pass\n\n    def add_routes(self, mc):\n        pass\n\n    def load_controllers(self):\n        pass\n\n    def get_documented_controllers(self):\n        return []\n\n\nclass PluginLoader(object):\n    def __init__(self, working_set=None, plugin_names=None):\n        self.working_set = working_set or pkg_resources.WorkingSet()\n\n        if plugin_names is None:\n            entry_points = self.available_plugins()\n        else:\n            entry_points = []\n            for name in plugin_names:\n                try:\n                    entry_point = self.available_plugins(name).next()\n                except StopIteration:\n                    print >> sys.stderr, (\"Unable to locate plugin \"\n                                          \"%s. Skipping.\" % name)\n                    continue\n                else:\n                    entry_points.append(entry_point)\n\n        self.plugins = OrderedDict()\n        for entry_point in entry_points:\n            try:\n                plugin_cls = entry_point.load()\n            except Exception as e:\n                if plugin_names:\n                    # if this plugin was specifically requested, fail.\n                    raise e\n                else:\n                    print >> sys.stderr, (\"Error loading plugin %s (%s).\"\n                                          \" Skipping.\" % (entry_point.name, e))\n                    continue\n            self.plugins[entry_point.name] = plugin_cls(entry_point)\n\n    def __len__(self):\n        return len(self.plugins)\n\n    def __iter__(self):\n        return self.plugins.itervalues()\n\n    def __reversed__(self):\n        return reversed(self.plugins.values())\n\n    def __getitem__(self, key):\n        return self.plugins[key]\n\n    def available_plugins(self, name=None):\n        return self.working_set.iter_entry_points('r2.plugin', name)\n\n    def declare_queues(self, queues):\n        for plugin in self:\n            plugin.declare_queues(queues)\n\n    def load_plugins(self, config):\n        g = config['pylons.app_globals']\n        for plugin in self:\n            # Record plugin version\n            entry = plugin.entry_point\n            git_dir = os.path.join(entry.dist.location, '.git')\n            g.record_repo_version(entry.name, git_dir)\n\n            # Load plugin\n            g.config.add_spec(plugin.config)\n            config['pylons.paths']['templates'].insert(0, plugin.template_dir)\n            plugin.add_js()\n            plugin.on_load(g)\n\n    def load_controllers(self):\n        # this module relies on pylons.i18n._ at import time (for translating\n        # messages) which isn't available 'til we're in request context.\n        from r2.lib import errors\n\n        for plugin in self:\n            errors.add_error_codes(plugin.errors)\n            plugin.load_controllers()\n\n    def get_documented_controllers(self):\n        for plugin in self:\n            for controller, url_prefix in plugin.get_documented_controllers():\n                yield controller, url_prefix\n"
  },
  {
    "path": "r2/r2/lib/profiler.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport cProfile\nimport pstats\nfrom functools import wraps, partial\n\n\ndef profile(fn):\n    @wraps(fn)\n    def _fn(*a, **kw):\n        currently_profiling = cProfile.Profile()\n        currently_profiling.enable()\n\n        ret = fn(*a, **kw)\n\n        currently_profiling.disable()\n        stats = pstats.Stats(currently_profiling)\n        stats.sort_stats('cumtime')\n        stats.print_stats(.1)\n\n        return ret\n    return _fn\n"
  },
  {
    "path": "r2/r2/lib/promote.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport calendar\nfrom collections import namedtuple\nimport datetime\nfrom decimal import Decimal, ROUND_DOWN, ROUND_UP\nimport hashlib\nimport hmac\nimport itertools\nimport json\nimport random\nimport time\nimport urllib\nimport urlparse\n\nfrom pylons import tmpl_context as c\nfrom pylons import app_globals as g\nfrom pylons.i18n import ungettext\nfrom pytz import timezone\n\nfrom r2.lib import (\n    authorize,\n    emailer,\n    hooks,\n)\nfrom r2.lib.db.operators import not_\nfrom r2.lib.db import queries\nfrom r2.lib.filters import _force_utf8\nfrom r2.lib.geoip import location_by_ips\nfrom r2.lib.memoize import memoize\nfrom r2.lib.sgm import sgm\nfrom r2.lib.strings import strings\nfrom r2.lib.utils import (\n    constant_time_compare,\n    to_date,\n    weighted_lottery,\n)\nfrom r2.models import (\n    Account,\n    Bid,\n    Collection,\n    DefaultSR,\n    FakeAccount,\n    FakeSubreddit,\n    Frontpage,\n    Link,\n    MultiReddit,\n    NotFound,\n    NO_TRANSACTION,\n    PromoCampaign,\n    PROMOTE_STATUS,\n    PromotedLink,\n    PromotionLog,\n    PromotionWeights,\n    Subreddit,\n    traffic,\n)\nfrom r2.models.keyvalue import NamedGlobals\n\nPROMO_HEALTH_KEY = 'promotions_last_updated'\n\ndef _mark_promos_updated():\n    NamedGlobals.set(PROMO_HEALTH_KEY, time.time())\n\n\ndef health_check():\n    \"\"\"Calculate the number of seconds since promotions were last updated\"\"\"\n    return time.time() - int(NamedGlobals.get(PROMO_HEALTH_KEY, default=0))\n\n\ndef cost_per_mille(spend, impressions):\n    \"\"\"Return the cost-per-mille given ad spend and impressions.\"\"\"\n    if impressions:\n        return 1000. * float(spend) / impressions\n    else:\n        return 0\n\n\ndef cost_per_click(spend, clicks):\n    \"\"\"Return the cost-per-click given ad spend and clicks.\"\"\"\n    if clicks:\n        return float(spend) / clicks\n    else:\n        return 0\n\n\ndef promo_keep_fn(item):\n    return (is_promoted(item) and\n            not item.hidden and\n            (c.over18 or not item.over_18))\n\n\n# attrs\n\ndef _base_host(is_mobile_web=False):\n    domain_prefix = \"m\" if is_mobile_web else g.domain_prefix\n    if domain_prefix:\n        base_domain = domain_prefix + '.' + g.domain\n    else:\n        base_domain = g.domain\n    return \"%s://%s\" % (g.default_scheme, base_domain)\n\n\ndef promo_traffic_url(l): # old traffic url\n    return \"%s/traffic/%s/\" % (_base_host(), l._id36)\n\ndef promotraffic_url(l): # new traffic url\n    return \"%s/promoted/traffic/headline/%s\" % (_base_host(), l._id36)\n\ndef promo_edit_url(l):\n    return \"%s/promoted/edit_promo/%s\" % (_base_host(), l._id36)\n\ndef view_live_url(link, campaign, srname):\n    is_mobile_web = campaign.platform == \"mobile_web\"\n    host = _base_host(is_mobile_web=is_mobile_web)\n    if srname:\n        host += '/r/%s' % srname\n    return '%s/?ad=%s' % (host, link._fullname)\n\ndef payment_url(action, link_id36, campaign_id36):\n    path = '/promoted/%s/%s/%s' % (action, link_id36, campaign_id36)\n    return urlparse.urljoin(g.payment_domain, path)\n\ndef pay_url(l, campaign):\n    return payment_url('pay', l._id36, campaign._id36)\n\ndef refund_url(l, campaign):\n    return payment_url('refund', l._id36, campaign._id36)\n\n# booleans\n\ndef is_awaiting_fraud_review(link):\n    return link.payment_flagged_reason and link.fraud == None\n\ndef is_promo(link):\n    return (link and not link._deleted and link.promoted is not None\n            and hasattr(link, \"promote_status\"))\n\ndef is_accepted(link):\n    return (is_promo(link) and\n            link.promote_status != PROMOTE_STATUS.rejected and\n            link.promote_status != PROMOTE_STATUS.edited_live and\n            link.promote_status >= PROMOTE_STATUS.accepted)\n\ndef is_unpaid(link):\n    return is_promo(link) and link.promote_status == PROMOTE_STATUS.unpaid\n\ndef is_unapproved(link):\n    return is_promo(link) and link.promote_status <= PROMOTE_STATUS.unseen\n\ndef is_rejected(link):\n    return is_promo(link) and link.promote_status == PROMOTE_STATUS.rejected\n\ndef is_promoted(link):\n    return is_promo(link) and link.promote_status == PROMOTE_STATUS.promoted\n\ndef is_edited_live(link):\n    return is_promo(link) and link.promote_status == PROMOTE_STATUS.edited_live\n\ndef is_finished(link):\n    return is_promo(link) and link.promote_status == PROMOTE_STATUS.finished\n\ndef is_live_on_sr(link, sr):\n    return bool(live_campaigns_by_link(link, sr=sr))\n\ndef is_pending(campaign):\n    today = promo_datetime_now().date()\n    return today < to_date(campaign.start_date)\n\ndef update_query(base_url, query_updates, unset=False):\n    scheme, netloc, path, params, query, fragment = urlparse.urlparse(base_url)\n    query_dict = urlparse.parse_qs(query)\n    query_dict.update(query_updates)\n\n    if unset:\n        query_dict = dict((k, v) for k, v in query_dict.iteritems() if v is not None)\n\n    query = urllib.urlencode(query_dict, doseq=True)\n    return urlparse.urlunparse((scheme, netloc, path, params, query, fragment))\n\n\ndef update_served(items):\n    for item in items:\n        if not item.promoted:\n            continue\n\n        campaign = PromoCampaign._by_fullname(item.campaign)\n\n        if not campaign.has_served:\n            campaign.has_served = True\n            campaign._commit()\n\n\nNO_CAMPAIGN = \"NO_CAMPAIGN\"\n\ndef is_valid_click_url(link, click_url, click_hash):\n    expected_mac = get_click_url_hmac(link, click_url)\n\n    return constant_time_compare(click_hash, expected_mac)\n\n\ndef get_click_url_hmac(link, click_url):\n    secret = g.secrets[\"adserver_click_url_secret\"]\n    data = \"|\".join([link._fullname, click_url])\n\n    return hmac.new(secret, data, hashlib.sha256).hexdigest()\n\n\ndef add_trackers(items, sr, adserver_click_urls=None):\n    \"\"\"Add tracking names and hashes to a list of wrapped promoted links.\"\"\"\n    adserver_click_urls = adserver_click_urls or {}\n    for item in items:\n        if not item.promoted:\n            continue\n\n        if item.campaign is None:\n            item.campaign = NO_CAMPAIGN\n\n        tracking_name_fields = [item.fullname, item.campaign]\n        if not isinstance(sr, FakeSubreddit):\n            tracking_name_fields.append(sr.name)\n\n        tracking_name = '-'.join(tracking_name_fields)\n\n        # construct the impression pixel url\n        pixel_mac = hmac.new(\n            g.tracking_secret, tracking_name, hashlib.sha1).hexdigest()\n        pixel_query = {\n            \"id\": tracking_name,\n            \"hash\": pixel_mac,\n            \"r\": random.randint(0, 2147483647), # cachebuster\n        }\n        item.imp_pixel = update_query(g.adtracker_url, pixel_query)\n        \n        if item.third_party_tracking:\n            item.third_party_tracking_url = item.third_party_tracking\n        if item.third_party_tracking_2:\n            item.third_party_tracking_url_2 = item.third_party_tracking_2\n\n        # construct the click redirect url\n        item_url = adserver_click_urls.get(item.campaign) or item.url\n        url = _force_utf8(item_url)\n        hashable = ''.join((url, tracking_name.encode(\"utf-8\")))\n        click_mac = hmac.new(\n            g.tracking_secret, hashable, hashlib.sha1).hexdigest()\n        click_query = {\n            \"id\": tracking_name,\n            \"hash\": click_mac,\n            \"url\": url,\n        }\n        click_url = update_query(g.clicktracker_url, click_query)\n\n        # overwrite the href_url with redirect click_url\n        item.href_url = click_url\n\n        # also overwrite the permalink url with redirect click_url for selfposts\n        if item.is_self:\n            item.permalink = click_url\n        else:\n            # add encrypted click url to the permalink for comments->click\n            item.permalink = update_query(item.permalink, {\n                \"click_url\": url,\n                \"click_hash\": get_click_url_hmac(item, url),\n            })\n\ndef update_promote_status(link, status):\n    queries.set_promote_status(link, status)\n    hooks.get_hook('promote.edit_promotion').call(link=link)\n\n\ndef new_promotion(is_self, title, content, author, ip):\n    \"\"\"\n    Creates a new promotion with the provided title, etc, and sets it\n    status to be 'unpaid'.\n    \"\"\"\n    sr = Subreddit._byID(Subreddit.get_promote_srid())\n    l = Link._submit(\n        is_self=is_self,\n        title=title,\n        content=content,\n        author=author,\n        sr=sr,\n        ip=ip,\n    )\n\n    l.promoted = True\n    l.disable_comments = False\n    l.sendreplies = True\n    PromotionLog.add(l, 'promotion created')\n\n    update_promote_status(l, PROMOTE_STATUS.unpaid)\n\n    # the user has posted a promotion, so enable the promote menu unless\n    # they have already opted out\n    if author.pref_show_promote is not False:\n        author.pref_show_promote = True\n        author._commit()\n\n    # notify of new promo\n    emailer.new_promo(l)\n    return l\n\n\ndef get_transactions(link, campaigns):\n    \"\"\"Return Bids for specified campaigns on the link.\n\n    A PromoCampaign can have several bids associated with it, but the most\n    recent one is recorded on the trans_id attribute. This is the one that will\n    be returned.\n\n    \"\"\"\n\n    campaigns = [c for c in campaigns if (c.trans_id != 0\n                                          and c.link_id == link._id)]\n    if not campaigns:\n        return {}\n\n    bids = Bid.lookup(thing_id=link._id)\n    bid_dict = {(b.campaign, b.transaction): b for b in bids}\n    bids_by_campaign = {c._id: bid_dict[(c._id, c.trans_id)] for c in campaigns}\n    return bids_by_campaign\n\ndef new_campaign(link, dates, target, frequency_cap,\n                 priority, location, platform,\n                 mobile_os, ios_devices, ios_version_range, android_devices,\n                 android_version_range, total_budget_pennies, cost_basis,\n                 bid_pennies):\n    campaign = PromoCampaign.create(link, target, dates[0], dates[1],\n                                    frequency_cap, priority,\n                                    location, platform, mobile_os, ios_devices,\n                                    ios_version_range, android_devices,\n                                    android_version_range, total_budget_pennies,\n                                    cost_basis, bid_pennies)\n    PromotionWeights.add(link, campaign)\n    PromotionLog.add(link, 'campaign %s created' % campaign._id)\n\n    if not campaign.is_house:\n        author = Account._byID(link.author_id, data=True)\n        if getattr(author, \"complimentary_promos\", False):\n            free_campaign(link, campaign, c.user)\n\n    hooks.get_hook('promote.new_campaign').call(link=link, campaign=campaign)\n    return campaign\n\n\ndef free_campaign(link, campaign, user):\n    auth_campaign(link, campaign, user, freebie=True)\n\n\ndef edit_campaign(link, campaign, dates, target, frequency_cap,\n                  priority, location,\n                  total_budget_pennies, cost_basis, bid_pennies,\n                  platform='desktop', mobile_os=None, ios_devices=None,\n                  ios_version_range=None, android_devices=None,\n                  android_version_range=None):\n    changed = {}\n    if dates[0] != campaign.start_date or dates[1] != campaign.end_date:\n        original = '%s to %s' % (campaign.start_date, campaign.end_date)\n        edited = '%s to %s' % (dates[0], dates[1])\n        changed['dates'] = (original, edited)\n        campaign.start_date = dates[0]\n        campaign.end_date = dates[1]\n    if target != campaign.target:\n        changed['target'] = (campaign.target, target)\n        campaign.target = target\n    if frequency_cap != campaign.frequency_cap:\n        changed['frequency_cap'] = (campaign.frequency_cap, frequency_cap)\n        campaign.frequency_cap = frequency_cap\n    if priority != campaign.priority:\n        changed['priority'] = (campaign.priority.name, priority.name)\n        campaign.priority = priority\n    if location != campaign.location:\n        changed['location'] = (campaign.location, location)\n        campaign.location = location\n    if platform != campaign.platform:\n        changed[\"platform\"] = (campaign.platform, platform)\n        campaign.platform = platform\n    if mobile_os != campaign.mobile_os:\n        changed[\"mobile_os\"] = (campaign.mobile_os, mobile_os)\n        campaign.mobile_os = mobile_os\n    if ios_devices != campaign.ios_devices:\n        changed['ios_devices'] = (campaign.ios_devices, ios_devices)\n        campaign.ios_devices = ios_devices\n    if android_devices != campaign.android_devices:\n        changed['android_devices'] = (campaign.android_devices, android_devices)\n        campaign.android_devices = android_devices\n    if ios_version_range != campaign.ios_version_range:\n        changed['ios_version_range'] = (campaign.ios_version_range,\n                                        ios_version_range)\n        campaign.ios_version_range = ios_version_range\n    if android_version_range != campaign.android_version_range:\n        changed['android_version_range'] = (campaign.android_version_range,\n                                            android_version_range)\n        campaign.android_version_range = android_version_range\n    if total_budget_pennies != campaign.total_budget_pennies:\n        void_campaign(link, campaign, reason='changed_budget')\n        campaign.total_budget_pennies = total_budget_pennies\n    if cost_basis != campaign.cost_basis:\n        changed['cost_basis'] = (campaign.cost_basis, cost_basis)\n        campaign.cost_basis = cost_basis\n    if bid_pennies != campaign.bid_pennies:\n        changed['bid_pennies'] = (campaign.bid_pennies,\n                                        bid_pennies)\n        campaign.bid_pennies = bid_pennies\n\n    change_strs = map(lambda t: '%s: %s -> %s' % (t[0], t[1][0], t[1][1]),\n                      changed.iteritems())\n    change_text = ', '.join(change_strs)\n    campaign._commit()\n\n    # update the index\n    PromotionWeights.reschedule(link, campaign)\n\n    if not campaign.is_house:\n        # make it a freebie, if applicable\n        author = Account._byID(link.author_id, True)\n        if getattr(author, \"complimentary_promos\", False):\n            free_campaign(link, campaign, c.user)\n\n    # record the changes\n    if change_text:\n        PromotionLog.add(link, 'edited %s: %s' % (campaign, change_text))\n\n    hooks.get_hook('promote.edit_campaign').call(link=link, campaign=campaign)\n\n\ndef terminate_campaign(link, campaign):\n    if not is_live_promo(link, campaign):\n        return\n\n    now = promo_datetime_now()\n    original_end = campaign.end_date\n    dates = [campaign.start_date, now]\n\n    # NOTE: this will delete PromotionWeights after and including now.date()\n    edit_campaign(\n        link=link,\n        campaign=campaign,\n        dates=dates,\n        target=campaign.target,\n        frequency_cap=campaign.frequency_cap,\n        priority=campaign.priority,\n        location=campaign.location,\n        total_budget_pennies=campaign.total_budget_pennies,\n        cost_basis=campaign.cost_basis,\n        bid_pennies=campaign.bid_pennies,\n    )\n\n    campaigns = list(PromoCampaign._by_link(link._id))\n    is_live = any(is_live_promo(link, camp) for camp in campaigns\n                                            if camp._id != campaign._id)\n    if not is_live:\n        update_promote_status(link, PROMOTE_STATUS.finished)\n        all_live_promo_srnames(_update=True)\n\n    msg = 'terminated campaign %s (original end %s)' % (campaign._id,\n                                                        original_end.date())\n    PromotionLog.add(link, msg)\n\n\ndef delete_campaign(link, campaign):\n    PromotionWeights.delete(link, campaign)\n    void_campaign(link, campaign, reason='deleted_campaign')\n    campaign.delete()\n    PromotionLog.add(link, 'deleted campaign %s' % campaign._id)\n    hooks.get_hook('promote.delete_campaign').call(link=link, campaign=campaign)\n\n\ndef toggle_pause_campaign(link, campaign, should_pause):\n    campaign.paused = should_pause\n    campaign._commit()\n\n    action = 'paused' if should_pause else 'resumed'\n    PromotionLog.add(link, '%s campaign %s' % (action, campaign._id))\n\n    hooks.get_hook('promote.edit_campaign').call(link=link,\n        campaign=campaign)\n\n\ndef void_campaign(link, campaign, reason):\n    transactions = get_transactions(link, [campaign])\n    bid_record = transactions.get(campaign._id)\n    if bid_record:\n        a = Account._byID(link.author_id)\n        authorize.void_transaction(a, bid_record.transaction, campaign._id)\n        campaign.trans_id = NO_TRANSACTION\n        campaign._commit()\n        text = ('voided transaction for %s: (trans_id: %d)'\n                % (campaign, bid_record.transaction))\n        PromotionLog.add(link, text)\n\n        if bid_record.transaction > 0:\n            # notify the user that the transaction was voided if it was not\n            # a freebie\n            emailer.void_payment(\n                link,\n                campaign,\n                reason=reason,\n                total_budget_dollars=campaign.total_budget_dollars\n            )\n\n\ndef auth_campaign(link, campaign, user, pay_id=None, freebie=False):\n    \"\"\"\n    Authorizes (but doesn't charge) a budget with authorize.net.\n    Args:\n    - link: promoted link\n    - campaign: campaign to be authorized\n    - user: Account obj of the user doing the auth (usually the currently\n        logged in user)\n    - pay_id: customer payment profile id to use for this transaction. (One\n        user can have more than one payment profile if, for instance, they have\n        more than one credit card on file.) Set pay_id to -1 for freebies.\n\n    Returns: (True, \"\") if successful or (False, error_msg) if not. \n    \"\"\"\n    void_campaign(link, campaign, reason='changed_payment')\n\n    if freebie:\n        trans_id, reason = authorize.auth_freebie_transaction(\n            campaign.total_budget_dollars, user, link, campaign._id)\n    else:\n        trans_id, reason = authorize.auth_transaction(\n            campaign.total_budget_dollars, user, pay_id, link, campaign._id)\n\n    if trans_id and not reason:\n        text = ('updated payment and/or budget for campaign %s: '\n                'SUCCESS (trans_id: %d, amt: %0.2f)' %\n                (campaign._id, trans_id, campaign.total_budget_dollars))\n        PromotionLog.add(link, text)\n        if trans_id < 0:\n            PromotionLog.add(link, 'FREEBIE (campaign: %s)' % campaign._id)\n\n        if trans_id:\n            if is_finished(link):\n                # When a finished promo gets a new paid campaign it doesn't\n                # need to go through approval again and is marked accepted\n                new_status = PROMOTE_STATUS.accepted\n            else:\n                new_status = max(PROMOTE_STATUS.unseen, link.promote_status)\n        else:\n            new_status = max(PROMOTE_STATUS.unpaid, link.promote_status)\n        update_promote_status(link, new_status)\n\n        if user and (user._id == link.author_id) and trans_id > 0:\n            emailer.promo_total_budget(link,\n                campaign.total_budget_dollars,\n                campaign.start_date)\n\n    else:\n        text = (\"updated payment and/or budget for campaign %s: FAILED ('%s')\"\n                % (campaign._id, reason))\n        PromotionLog.add(link, text)\n        trans_id = 0\n\n    campaign.trans_id = trans_id\n    campaign._commit()\n\n    return bool(trans_id), reason\n\n\n\n# dates are referenced to UTC, while we want promos to change at (roughly)\n# midnight eastern-US.\n# TODO: make this a config parameter\ntimezone_offset = -5 # hours\ntimezone_offset = datetime.timedelta(0, timezone_offset * 3600)\ndef promo_datetime_now(offset=None):\n    now = datetime.datetime.now(g.tz) + timezone_offset\n    if offset is not None:\n        now += datetime.timedelta(offset)\n    return now\n\n\n# campaigns can launch the following day if they're created before 17:00 PDT\nDAILY_CUTOFF = datetime.time(17, tzinfo=timezone(\"US/Pacific\"))\n\ndef get_date_limits(link, is_sponsor=False):\n    promo_today = promo_datetime_now().date()\n\n    if is_sponsor:\n        min_start = promo_today\n    elif is_accepted(link):\n        # link is already accepted--let user create a campaign starting\n        # tomorrow because it doesn't need to be re-reviewed\n        min_start = promo_today + datetime.timedelta(days=1)\n    else:\n        # campaign and link will need to be reviewed before they can launch.\n        # review can happen until DAILY_CUTOFF PDT Monday through Friday and\n        # Sunday. Any campaign created after DAILY_CUTOFF is treated as if it\n        # were created the following day.\n        now = datetime.datetime.now(tz=timezone(\"US/Pacific\"))\n        now_today = now.date()\n        too_late_for_review = now.time() > DAILY_CUTOFF\n\n        if too_late_for_review and now_today.weekday() == calendar.FRIDAY:\n            # no review late on Friday--earliest review is Sunday to launch\n            # on Monday\n            min_start = now_today + datetime.timedelta(days=3)\n        elif now_today.weekday() == calendar.SATURDAY:\n            # no review any time on Saturday--earliest review is Sunday to\n            # launch on Monday\n            min_start = now_today + datetime.timedelta(days=2)\n        elif too_late_for_review:\n            # no review late in the day--earliest review is tomorrow to\n            # launch the following day\n            min_start = now_today + datetime.timedelta(days=2)\n        else:\n            # review will happen today so can launch tomorrow\n            min_start = now_today + datetime.timedelta(days=1)\n\n    if is_sponsor:\n        max_end = promo_today + datetime.timedelta(days=366)\n    else:\n        max_end = promo_today + datetime.timedelta(days=93)\n\n    if is_sponsor:\n        max_start = max_end - datetime.timedelta(days=1)\n    else:\n        # authorization hold happens now but expires after 30 days. charge\n        # happens 1 day before the campaign launches. the latest a campaign\n        # can start is 30 days from now (it will get charged in 29 days).\n        max_start = promo_today + datetime.timedelta(days=30)\n\n    return min_start, max_start, max_end\n\n\ndef accept_promotion(link):\n    was_edited_live = is_edited_live(link)\n    update_promote_status(link, PROMOTE_STATUS.accepted)\n\n    if link._spam:\n        link._spam = False\n        link._commit()\n\n    if not was_edited_live:\n        emailer.accept_promo(link)\n\n    # if the link has campaigns running now charge them and promote the link\n    now = promo_datetime_now()\n    campaigns = list(PromoCampaign._by_link(link._id))\n    is_live = False\n    for camp in campaigns:\n        if is_accepted_promo(now, link, camp):\n            # if link was edited live, do not check against Authorize.net\n            if not was_edited_live:\n                charge_campaign(link, camp)\n            if charged_or_not_needed(camp):\n                promote_link(link, camp)\n                is_live = True\n\n    if is_live:\n        all_live_promo_srnames(_update=True)\n\n\ndef flag_payment(link, reason):\n    # already determined to be fraud or already flagged for that reason.\n    if link.fraud or reason in link.payment_flagged_reason:\n        return\n\n    if link.payment_flagged_reason:\n        link.payment_flagged_reason += (\", %s\" % reason)\n    else:\n        link.payment_flagged_reason = reason\n\n    link._commit()\n    PromotionLog.add(link, \"payment flagged: %s\" % reason)\n    queries.set_payment_flagged_link(link)\n\n\ndef review_fraud(link, is_fraud):\n    link.fraud = is_fraud\n    link._commit()\n    PromotionLog.add(link, \"marked as fraud\" if is_fraud else \"resolved as not fraud\")\n    queries.unset_payment_flagged_link(link)\n\n    if is_fraud:\n        reject_promotion(link, \"fraud\", notify_why=False)\n        hooks.get_hook(\"promote.fraud_identified\").call(link=link, sponsor=c.user)\n\n\ndef reject_promotion(link, reason=None, notify_why=True):\n    if is_rejected(link):\n        return\n\n    was_live = is_promoted(link)\n    update_promote_status(link, PROMOTE_STATUS.rejected)\n    if reason:\n        PromotionLog.add(link, \"rejected: %s\" % reason)\n\n    # Send a rejection email (unless the advertiser requested the reject)\n    if not c.user or c.user._id != link.author_id:\n        emailer.reject_promo(link, reason=(reason if notify_why else None))\n\n    if was_live:\n        all_live_promo_srnames(_update=True)\n\n\ndef unapprove_promotion(link):\n    if is_unpaid(link):\n        return\n    elif is_finished(link):\n        # when a finished promo is edited it is bumped down to unpaid so if it\n        # eventually gets a paid campaign it can get upgraded to unseen and\n        # reviewed\n        update_promote_status(link, PROMOTE_STATUS.unpaid)\n    else:\n        update_promote_status(link, PROMOTE_STATUS.unseen)\n\n\ndef edited_live_promotion(link):\n    update_promote_status(link, PROMOTE_STATUS.edited_live)\n    emailer.edited_live_promo(link)\n\n\ndef authed_or_not_needed(campaign):\n    authed = campaign.trans_id != NO_TRANSACTION\n    needs_auth = not campaign.is_house\n    return authed or not needs_auth\n\n\ndef charged_or_not_needed(campaign):\n    # True if a campaign has a charged transaction or doesn't need one\n    charged = authorize.is_charged_transaction(campaign.trans_id, campaign._id)\n    needs_charge = not campaign.is_house\n    return charged or not needs_charge\n\n\ndef is_served_promo(date, link, campaign):\n    return (campaign.start_date <= date < campaign.end_date and\n            campaign.has_served)\n\n\ndef is_accepted_promo(date, link, campaign):\n    return (campaign.start_date <= date < campaign.end_date and\n            is_accepted(link) and\n            authed_or_not_needed(campaign))\n\n\ndef is_scheduled_promo(date, link, campaign):\n    return (is_accepted_promo(date, link, campaign) and \n            charged_or_not_needed(campaign))\n\n\ndef is_live_promo(link, campaign):\n    now = promo_datetime_now()\n    return is_promoted(link) and is_scheduled_promo(now, link, campaign)\n\n\ndef is_complete_promo(link, campaign):\n    return (campaign.is_paid and \n        not (is_live_promo(link, campaign) or is_pending(campaign)))\n\n\ndef _is_geotargeted_promo(link):\n    campaigns = live_campaigns_by_link(link)\n    geotargeted = filter(lambda camp: camp.location, campaigns)\n    city_target = any(camp.location.metro for camp in geotargeted)\n    return bool(geotargeted), city_target\n\n\ndef is_geotargeted_promo(link):\n    key = 'geopromo:%s' % link._id\n    from_cache = g.gencache.get(key)\n    if not from_cache:\n        ret = _is_geotargeted_promo(link)\n        g.gencache.set(key, ret, time=60)\n        return ret\n    else:\n        return from_cache\n\n\ndef get_promos(date, sr_names=None, link=None):\n    campaign_ids = PromotionWeights.get_campaign_ids(\n        date, sr_names=sr_names, link=link)\n    campaigns = PromoCampaign._byID(campaign_ids, data=True, return_dict=False)\n    link_ids = {camp.link_id for camp in campaigns}\n    links = Link._byID(link_ids, data=True)\n    for camp in campaigns:\n        yield camp, links[camp.link_id]\n\n\ndef get_accepted_promos(offset=0):\n    date = promo_datetime_now(offset=offset)\n    for camp, link in get_promos(date):\n        if is_accepted_promo(date, link, camp):\n            yield camp, link\n\n\ndef get_scheduled_promos(offset=0):\n    date = promo_datetime_now(offset=offset)\n    for camp, link in get_promos(date):\n        if is_scheduled_promo(date, link, camp):\n            yield camp, link\n\n\ndef get_served_promos(offset=0):\n    date = promo_datetime_now(offset=offset)\n    for camp, link in get_promos(date):\n        if is_served_promo(date, link, camp):\n            yield camp, link\n\n\ndef charge_campaign(link, campaign):\n    if charged_or_not_needed(campaign):\n        return\n\n    user = Account._byID(link.author_id)\n    success, reason = authorize.charge_transaction(user, campaign.trans_id,\n                                                   campaign._id)\n\n    if not success:\n        if reason == authorize.TRANSACTION_NOT_FOUND:\n            # authorization hold has expired\n            original_trans_id = campaign.trans_id\n            campaign.trans_id = NO_TRANSACTION\n            campaign._commit()\n            text = ('voided expired transaction for %s: (trans_id: %d)'\n                    % (campaign, original_trans_id))\n            PromotionLog.add(link, text)\n        return\n\n    hooks.get_hook('promote.edit_campaign').call(link=link, campaign=campaign)\n\n    if not is_promoted(link):\n        update_promote_status(link, PROMOTE_STATUS.pending)\n\n    emailer.queue_promo(link,\n        campaign.total_budget_dollars,\n        campaign.trans_id)\n    text = ('auth charge for campaign %s, trans_id: %d' %\n            (campaign._id, campaign.trans_id))\n    PromotionLog.add(link, text)\n\n\ndef charge_pending(offset=1):\n    for camp, link in get_accepted_promos(offset=offset):\n        charge_campaign(link, camp)\n\n\ndef live_campaigns_by_link(link, sr=None):\n    if not is_promoted(link):\n        return []\n\n    sr_names = [sr.name] if sr else None\n    now = promo_datetime_now()\n    return [camp for camp, link in get_promos(now, sr_names=sr_names,\n                                              link=link)\n            if is_live_promo(link, camp)]\n\n\ndef promote_link(link, campaign):\n    if (not link.over_18 and\n        not link.over_18_override and\n        any(sr.over_18 for sr in campaign.target.subreddits_slow)):\n        link.over_18 = True\n        link._commit()\n\n    if not is_promoted(link):\n        update_promote_status(link, PROMOTE_STATUS.promoted)\n        emailer.live_promo(link)\n\n\ndef make_daily_promotions():\n    # charge campaigns so they can go live\n    charge_pending(offset=0)\n    charge_pending(offset=1)\n\n    # promote links and record ids of promoted links\n    link_ids = set()\n    for campaign, link in get_scheduled_promos(offset=0):\n        link_ids.add(link._id)\n        promote_link(link, campaign)\n\n    # expire finished links\n    q = Link._query(Link.c.promote_status == PROMOTE_STATUS.promoted, data=True)\n    q = q._filter(not_(Link.c._id.in_(link_ids)))\n    for link in q:\n        update_promote_status(link, PROMOTE_STATUS.finished)\n        emailer.finished_promo(link)\n\n    # update subreddits with promos\n    all_live_promo_srnames(_update=True)\n\n    _mark_promos_updated()\n    finalize_completed_campaigns(daysago=1)\n    hooks.get_hook('promote.make_daily_promotions').call(offset=0)\n\n\ndef finalize_completed_campaigns(daysago=1):\n    # PromoCampaign.end_date is utc datetime with year, month, day only\n    now = datetime.datetime.now(g.tz)\n    date = now - datetime.timedelta(days=daysago)\n    date = date.replace(hour=0, minute=0, second=0, microsecond=0)\n\n    q = PromoCampaign._query(PromoCampaign.c.end_date == date,\n                             # exclude no transaction\n                             PromoCampaign.c.trans_id != NO_TRANSACTION,\n                             data=True)\n    # filter out freebies\n    campaigns = filter(lambda camp: camp.trans_id > NO_TRANSACTION, q)\n\n    if not campaigns:\n        return\n\n    # check that traffic is up to date\n    earliest_campaign = min(campaigns, key=lambda camp: camp.start_date)\n    start, end = get_total_run(earliest_campaign)\n    missing_traffic = traffic.get_missing_traffic(start.replace(tzinfo=None),\n                                                  date.replace(tzinfo=None))\n    if missing_traffic:\n        raise ValueError(\"Can't finalize campaigns finished on %s.\"\n                         \"Missing traffic from %s\" % (date, missing_traffic))\n\n    links = Link._byID([camp.link_id for camp in campaigns], data=True)\n    underdelivered_campaigns = []\n\n    for camp in campaigns:\n        if hasattr(camp, 'refund_amount'):\n            continue\n\n        link = links[camp.link_id]\n        billable_impressions = get_billable_impressions(camp)\n        billable_amount = get_billable_amount(camp, billable_impressions)\n\n        if billable_amount >= camp.total_budget_pennies:\n            if hasattr(camp, 'cpm'):\n                text = '%s completed with $%s billable (%s impressions @ $%s).'\n                text %= (camp, billable_amount, billable_impressions,\n                    camp.bid_dollars)\n            else:\n                text = '%s completed with $%s billable (pre-CPM).'\n                text %= (camp, billable_amount) \n            PromotionLog.add(link, text)\n            camp.refund_amount = 0.\n            camp._commit()\n        elif charged_or_not_needed(camp):\n            underdelivered_campaigns.append(camp)\n\n        if underdelivered_campaigns:\n            queries.set_underdelivered_campaigns(underdelivered_campaigns)\n\n\ndef get_refund_amount(camp, billable):\n    existing_refund = getattr(camp, 'refund_amount', 0.)\n    charge = camp.total_budget_dollars - existing_refund\n    refund_amount = charge - billable\n    refund_amount = Decimal(str(refund_amount)).quantize(Decimal('.01'),\n                                                    rounding=ROUND_UP)\n    return max(float(refund_amount), 0.)\n\n\ndef refund_campaign(link, camp, refund_amount, billable_amount,\n        billable_impressions):\n    owner = Account._byID(camp.owner_id, data=True)\n    success, reason = authorize.refund_transaction(\n        owner, camp.trans_id, camp._id, refund_amount)\n    if not success:\n        text = ('%s $%s refund failed' % (camp, refund_amount))\n        PromotionLog.add(link, text)\n        g.log.debug(text + ' (reason: %s)' % reason)\n\n        return False\n\n    if billable_impressions:\n        text = ('%s completed with $%s billable (%s impressions @ $%s).'\n                ' %s refunded.' % (camp, billable_amount,\n                                   billable_impressions,\n                                   camp.bid_pennies / 100.,\n                                   refund_amount))\n    else:\n        text = ('%s completed with $%s billable. %s refunded' % (camp,\n            billable_amount, refund_amount))\n\n    PromotionLog.add(link, text)\n    camp.refund_amount = refund_amount\n    camp._commit()\n    queries.unset_underdelivered_campaigns(camp)\n    emailer.refunded_promo(link)\n\n    return True\n\n\nPromoTuple = namedtuple('PromoTuple', ['link', 'weight', 'campaign'])\n\n\n@memoize('all_live_promo_srnames', stale=True)\ndef all_live_promo_srnames():\n    now = promo_datetime_now()\n    srnames = itertools.chain.from_iterable(\n        camp.target.subreddit_names for camp, link in get_promos(now)\n                                    if is_live_promo(link, camp)\n    )\n    return set(srnames)\n\n@memoize('get_nsfw_collections_srnames', time=(60*60), stale=True)\ndef get_nsfw_collections_srnames():\n    all_collections = Collection.get_all()\n    nsfw_collections = [col for col in all_collections if col.over_18]\n    srnames = itertools.chain.from_iterable(\n        col.sr_names for col in nsfw_collections\n    )\n\n    return set(srnames)\n\n\ndef is_site_over18(site):\n    # a site should be considered nsfw if it's included in a\n    # nsfw collection because nsfw ads can target nsfw collections.\n    nsfw_collection_srnames = get_nsfw_collections_srnames()\n    return site.over_18 or site.name in nsfw_collection_srnames\n\n\ndef srnames_from_site(user, site, include_subscriptions=True):\n    is_logged_in = user and not isinstance(user, FakeAccount)\n    over_18 = is_site_over18(site)\n    srnames = set()\n\n    if not isinstance(site, FakeSubreddit):\n        srnames.add(site.name)\n    elif isinstance(site, MultiReddit):\n        srnames = srnames | {sr.name for sr in site.srs}\n    else:\n        srnames.add(Frontpage.name)\n\n        if is_logged_in and include_subscriptions:\n            subscriptions = Subreddit.user_subreddits(\n                user,\n                ids=False,\n            )\n\n            # only use subreddits that aren't quarantined and have the same\n            # age gate as the subreddit being viewed.\n            subscriptions = filter(\n                lambda sr: not sr.quarantine and sr.over_18 == over_18,\n                subscriptions,\n            )\n\n            subscription_srnames = {sr.name for sr in subscriptions}\n\n            # remove any subscriptions that may have nsfw ads targeting\n            # them because they're apart of a nsfw collection.\n            nsfw_collection_srnames = get_nsfw_collections_srnames()\n\n            if not over_18:\n                subscription_srnames = (subscription_srnames -\n                    nsfw_collection_srnames)\n\n            srnames = srnames | subscription_srnames\n\n    return srnames\n\n\ndef keywords_from_context(\n        user, site,\n        include_subscriptions=True,\n        live_promos_only=True,\n    ):\n\n    keywords = srnames_from_site(\n        user, site,\n        include_subscriptions,\n    )\n\n    # if the ad was created by selfserve then we know\n    # whether or not there exists an ad for that keyword\n    # and can remove un-targeted keywords accordingly.\n    if live_promos_only:\n        live_srnames = all_live_promo_srnames()\n        keywords = live_srnames.intersection(keywords)\n\n    if (not isinstance(site,FakeSubreddit) and\n            site._downs > g.live_config[\"ads_popularity_threshold\"]):\n        keywords.add(\"s.popular\")\n\n    if is_site_over18(site):\n        keywords.add(\"s.nsfw\")\n    else:\n        keywords.add(\"s.sfw\")\n\n    if c.user_is_loggedin:\n        keywords.add(\"loggedin\")\n    else:\n        keywords.add(\"loggedout\")\n\n    return keywords\n\n\n# special handling for memcache ascii protocol\nSPECIAL_NAMES = {\" reddit.com\": \"_reddit.com\"}\nREVERSED_NAMES = {v: k for k, v in SPECIAL_NAMES.iteritems()}\n\n\ndef _get_live_promotions(sanitized_names):\n    now = promo_datetime_now()\n    sr_names = [REVERSED_NAMES.get(name, name) for name in sanitized_names]\n    ret = {sr_name: [] for sr_name in sanitized_names}\n    for camp, link in get_promos(now, sr_names=sr_names):\n        if is_live_promo(link, camp):\n            weight = (camp.total_budget_dollars / camp.ndays)\n            pt = PromoTuple(link=link._fullname, weight=weight,\n                            campaign=camp._fullname)\n            for sr_name in camp.target.subreddit_names:\n                if sr_name in sr_names:\n                    sanitized_name = SPECIAL_NAMES.get(sr_name, sr_name)\n                    ret[sanitized_name].append(pt)\n    return ret\n\n\ndef get_live_promotions(sr_names):\n    sanitized_names = [SPECIAL_NAMES.get(name, name) for name in sr_names]\n    promos_by_sanitized_name = sgm(\n        cache=g.gencache,\n        keys=sanitized_names,\n        miss_fn=_get_live_promotions,\n        prefix='srpromos:',\n        time=60,\n        stale=True,\n    )\n    promos_by_srname = {\n        REVERSED_NAMES.get(name, name): val\n        for name, val in promos_by_sanitized_name.iteritems()\n    }\n    return itertools.chain.from_iterable(promos_by_srname.itervalues())\n\n\ndef lottery_promoted_links(sr_names, n=10):\n    \"\"\"Run weighted_lottery to order and choose a subset of promoted links.\"\"\"\n    promo_tuples = get_live_promotions(sr_names)\n\n    # house priority campaigns have weight of 0, use some small value\n    # so they'll show if there are no other campaigns\n    weights = {p: p.weight or 0.001 for p in promo_tuples}\n    selected = []\n    while weights and len(selected) < n:\n        s = weighted_lottery(weights)\n        del weights[s]\n        selected.append(s)\n    return selected\n\n\ndef get_total_run(thing):\n    \"\"\"Return the total time span this link or campaign will run.\n\n    Starts at the start date of the earliest campaign and goes to the end date\n    of the latest campaign.\n\n    \"\"\"\n    campaigns = []\n    if isinstance(thing, Link):\n        campaigns = PromoCampaign._by_link(thing._id)\n    elif isinstance(thing, PromoCampaign):\n        campaigns = [thing]\n    else:\n        campaigns = []\n\n    earliest = None\n    latest = None\n    for campaign in campaigns:\n        if not charged_or_not_needed(campaign):\n            continue\n\n        if not earliest or campaign.start_date < earliest:\n            earliest = campaign.start_date\n\n        if not latest or campaign.end_date > latest:\n            latest = campaign.end_date\n\n    # a manually launched promo (e.g., sr discovery) might not have campaigns.\n    if not earliest or not latest:\n        latest = datetime.datetime.utcnow()\n        earliest = latest - datetime.timedelta(days=30)  # last month\n\n    # ugh this stuff is a mess. they're stored as \"UTC\" but actually mean UTC-5.\n    earliest = earliest.replace(tzinfo=g.tz) - timezone_offset\n    latest = latest.replace(tzinfo=g.tz) - timezone_offset\n\n    return earliest, latest\n\n\ndef get_traffic_dates(thing):\n    \"\"\"Retrieve the start and end of a Promoted Link or PromoCampaign.\"\"\"\n    now = datetime.datetime.now(g.tz).replace(minute=0, second=0, microsecond=0)\n    start, end = get_total_run(thing)\n    end = min(now, end)\n    return start, end\n\n\ndef get_billable_impressions(campaign):\n    start, end = get_traffic_dates(campaign)\n    if start > datetime.datetime.now(g.tz):\n        return 0\n\n    traffic_lookup = traffic.TargetedImpressionsByCodename.promotion_history\n    imps = traffic_lookup(campaign._fullname, start.replace(tzinfo=None),\n                          end.replace(tzinfo=None))\n    billable_impressions = sum(imp for date, (imp,) in imps)\n    return billable_impressions\n\n\ndef get_billable_amount(camp, impressions):\n    if not camp.is_auction:\n        value_delivered = impressions / 1000. * camp.bid_dollars\n        billable_amount = min(camp.total_budget_dollars, value_delivered)\n    else:\n        # pre-CPM campaigns are charged in full regardless of impressions\n        billable_amount = camp.total_budget_dollars\n\n    billable_amount = Decimal(str(billable_amount)).quantize(Decimal('.01'),\n                                                        rounding=ROUND_DOWN)\n    return float(billable_amount)\n\n\ndef get_spent_amount(campaign):\n    if campaign.is_house:\n        spent = 0.\n    elif hasattr(campaign, 'refund_amount'):\n        # no need to calculate spend if we've already refunded\n        spent = campaign.total_budget_dollars - campaign.refund_amount\n    elif campaign.is_auction:\n        spent = campaign.adserver_spent_pennies / 100.\n    else:\n        billable_impressions = get_billable_impressions(campaign)\n        spent = get_billable_amount(campaign, billable_impressions)\n    return spent\n\n\ndef successful_payment(link, campaign, ip, address):\n    if not address:\n        return\n\n    campaign.trans_ip = ip\n    campaign.trans_billing_country = address.country\n\n    location = location_by_ips(ip)\n\n    if location:\n        campaign.trans_ip_country = location.get(\"country_name\")\n\n        countries_match = (campaign.trans_billing_country.lower() ==\n            campaign.trans_ip_country.lower())\n        campaign.trans_country_match = countries_match\n\n    campaign._commit()\n\n\ndef new_payment_method(user, ip, address, link):\n    user._incr('num_payment_methods')\n    hooks.get_hook('promote.new_payment_method').call(user=user, ip=ip, address=address, link=link)\n\n\ndef failed_payment_method(user, link):\n    user._incr('num_failed_payments')\n    hooks.get_hook('promote.failed_payment').call(user=user, link=link)\n\n\ndef Run(verbose=True):\n    \"\"\"reddit-job-update_promos: Intended to be run hourly to pull in\n    scheduled changes to ads\n\n    \"\"\"\n\n    if verbose:\n        print \"%s promote.py:Run() - make_daily_promotions()\" % datetime.datetime.now(g.tz)\n\n    make_daily_promotions()\n\n    if verbose:\n        print \"%s promote.py:Run() - finished\" % datetime.datetime.now(g.tz)\n"
  },
  {
    "path": "r2/r2/lib/providers/__init__.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\ndef select_provider(config_parser, working_set, type, name):\n    \"\"\"Given a type and name return an instantiated provider.\n\n    Providers are objects that abstract away an external service. They are\n    looked up via the pkg_resources system which means that they may not even\n    be implemented in the main reddit code.\n\n    A provider must implement the expected interface, be registered via\n    setuptools with the right entry point, and have a no-argument constructor.\n\n    If a provider class has an attribute `config` which is a ConfigParse-style\n    spec dictionary, the spec will be added to the main configuration parser\n    similarly to the plugin system. This allows providers to declare extra\n    config options for parsing.\n\n    \"\"\"\n\n    try:\n        entry_point = working_set.iter_entry_points(type, name).next()\n    except StopIteration:\n        raise Exception(\"unknown %s provider: %r\" % (type, name))\n    else:\n        provider_cls = entry_point.load()\n\n    if hasattr(provider_cls, \"config\"):\n        config_parser.add_spec(provider_cls.config)\n\n    return provider_cls()\n"
  },
  {
    "path": "r2/r2/lib/providers/auth/__init__.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\n\nclass AuthenticationProvider(object):\n    \"\"\"Provider for authenticating web requests.\n\n    Authentication providers should look at the request environment and\n    determine if a particular user is logged in to the site.  This may take the\n    form of cookies like on reddit.com or perhaps a web server provided HTTP\n    header in intranet environments.\n\n    Authentication systems may allow users to select their login, or may force\n    them to a particular one and disallow logout.\n\n    Note: this is NOT intended for API authentication, see instead: OAuth.\n\n    \"\"\"\n\n    def is_logout_allowed(self):\n        \"\"\"Return if the user allowed to log out.\n\n        Some authentication systems, such as single sign-on on an intranet,\n        pick up the authenticated user from an external system and logging out\n        of reddit would be meaningless.  If disallowed, some UI elements can be\n        disabled to reduce confusion.\n\n        \"\"\"\n        raise NotImplementedError\n\n    def get_authenticated_account(self):\n        \"\"\"Return the authenticated user, or None if logged out.\n\n        \"\"\"\n        raise NotImplementedError\n"
  },
  {
    "path": "r2/r2/lib/providers/auth/cookie.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport urllib\n\nfrom pylons import request\nfrom pylons import app_globals as g\n\nfrom r2.lib.configparse import ConfigValue\nfrom r2.lib.providers.auth import AuthenticationProvider\nfrom r2.lib.utils import constant_time_compare\n\n\nclass CookieAuthenticationProvider(AuthenticationProvider):\n    \"\"\"An authentication provider that uses standard HTTP cookies.\n\n    \"\"\"\n\n    config = {\n        ConfigValue.str: [\n            \"login_cookie\",\n        ],\n    }\n\n    def is_logout_allowed(self):\n        return True\n\n    def get_authenticated_account(self):\n        from r2.models import Account, NotFound\n\n        quoted_session_cookie = request.cookies.get(g.login_cookie)\n        if not quoted_session_cookie:\n            return None\n        session_cookie = urllib.unquote(quoted_session_cookie)\n\n        try:\n            uid, timestr, hash = session_cookie.split(\",\")\n            uid = int(uid)\n        except:\n            return None\n\n        try:\n            account = Account._byID(uid, data=True)\n        except NotFound:\n            return None\n\n        expected_cookie = account.make_cookie(timestr)\n        if not constant_time_compare(session_cookie, expected_cookie):\n            return None\n        return account\n"
  },
  {
    "path": "r2/r2/lib/providers/auth/http.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport crypt\n\nimport bcrypt\n\nfrom pylons import request\nfrom pylons import app_globals as g\n\nfrom r2.lib.configparse import ConfigValue\nfrom r2.lib.providers.auth import AuthenticationProvider\nfrom r2.lib.require import RequirementException\nfrom r2.lib.utils import constant_time_compare, parse_http_basic\n\n\nclass HttpAuthenticationProvider(AuthenticationProvider):\n    \"\"\"An authentication provider based on HTTP basic authentication.\n\n    This provider uses HTTP basic authentication (via the Authorization header)\n    to authenticate the user. Logout is disallowed.\n\n    If auth_trust_http_authorization is set to true, the Authorization header\n    is fully trusted. The password will not be checked and accounts will be\n    automatically registered when not already present.\n\n    \"\"\"\n\n    config = {\n        ConfigValue.bool: [\n            \"auth_trust_http_authorization\",\n        ],\n    }\n\n    def is_logout_allowed(self):\n        return False\n\n    def get_authenticated_account(self):\n        from r2.models import Account, NotFound, register\n\n        try:\n            authorization = request.environ.get(\"HTTP_AUTHORIZATION\")\n            username, password = parse_http_basic(authorization)\n        except RequirementException:\n            return None\n\n        try:\n            account = Account._by_name(username)\n        except NotFound:\n            if g.auth_trust_http_authorization:\n                # note: we're explicitly allowing automatic re-registration of\n                # _deleted accounts and login of _banned accounts here because\n                # we're trusting you know what you're doing in an SSO situation\n                account = register(username, password, request.ip)\n            else:\n                return None\n\n        # if we're to trust the authorization headers, don't check passwords\n        if g.auth_trust_http_authorization:\n            return account\n\n        # not all systems support bcrypt in the standard crypt\n        if account.password.startswith(\"$2a$\"):\n            expected_hash = bcrypt.hashpw(password, account.password)\n        else:\n            expected_hash = crypt.crypt(password, account.password)\n\n        if not constant_time_compare(expected_hash, account.password):\n            return None\n        return account\n"
  },
  {
    "path": "r2/r2/lib/providers/cdn/__init__.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nclass CdnProvider(object):\n    \"\"\"Provider for handling Content Delivery Network (CDN) interactions.\n\n    \"\"\"\n\n    def get_client_ip(self, environ):\n        \"\"\"Verify and return the CDN-provided remote IP address.\n\n        For requests coming through the CDN, the value of REMOTE_ADDR will be\n        the IP address of one of the CDN's edge nodes rather than that of the\n        client itself.  The CDN can provide the remote client's true IP as well\n        as verification that the provided IP is indeed accurate via extra\n        headers, the details of which vary with each CDN.\n\n        This function should analyze the request environment and return the\n        remote client's true IP address if it's present and validates. If it is\n        not present or does not validate, it should return None.\n\n        \"\"\"\n        raise NotImplementedError\n\n    def get_client_location(self, environ):\n        \"\"\"Return CDN-provided geo location data for the requester.\n\n        The return value is an ISO 3166-1 Alpha 2 format country code or None.\n\n        This function is only defined when get_client_ip returns a non-None\n        value, i.e. when the request has been validated as being from the CDN.\n\n        \"\"\"\n        raise NotImplementedError\n\n    def purge_content(self, url):\n        \"\"\"Purge content from the CDN by URL\"\"\"\n        raise NotImplementedError\n"
  },
  {
    "path": "r2/r2/lib/providers/cdn/cloudflare.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport hashlib\nimport json\nimport requests\n\nfrom pylons import app_globals as g\n\nfrom r2.lib.providers.cdn import CdnProvider\nfrom r2.lib.utils import constant_time_compare\n\nclass CloudFlareCdnProvider(CdnProvider):\n    \"\"\"A provider for reddit's configuration of CloudFlare.\n\n    \"\"\"\n \n    def _do_content_purge(self, url):  \n        \"\"\"Does the purge of the content from CloudFlare.\"\"\"      \n        data = {\n            'files': [\n                url,\n            ]\n        }\n\n        timer = g.stats.get_timer(\"providers.cloudflare.content_purge\")\n        timer.start()\n        response = requests.delete(\n            g.secrets['cloudflare_purge_key_url'],\n            headers={\n                'X-Auth-Email': g.secrets['cloudflare_email_address'],\n                'X-Auth-Key': g.secrets['cloudflare_api_key'],\n                'content-type': 'application/json',\n            },\n            data=json.dumps(data),\n        )\n        timer.stop()\n\n    def get_client_ip(self, environ):\n        try:\n            client_ip = environ[\"HTTP_CF_CONNECTING_IP\"]\n            provided_hash = environ[\"HTTP_CF_CIP_TAG\"].lower()\n        except KeyError:\n            return None\n\n        secret = g.secrets[\"cdn_ip_verification\"]\n        expected_hash = hashlib.sha1(client_ip + secret).hexdigest()\n\n        if not constant_time_compare(expected_hash, provided_hash):\n            return None\n\n        return client_ip\n\n    def get_client_location(self, environ):\n        return environ.get(\"HTTP_CF_IPCOUNTRY\", None)\n\n    def purge_content(self, url):\n        \"\"\"Purges the content specified by url from the cache.\"\"\"\n\n        # per the CloudFlare docs:\n        #    https://www.cloudflare.com/docs/client-api.html#s4.5\n        #    The full URL of the file that needs to be purged from \n        #    CloudFlare's  cache. Keep in mind, that if an HTTP and \n        #    an HTTPS version of the file exists, then both versions \n        #    will need to be purged independently\n        # create the \"alternate\" URL for http or https\n        if 'https://' in url:\n            url_altered = url.replace('https://', 'http://')\n        else:\n            url_altered = url.replace('http://', 'https://')\n\n        self._do_content_purge(url)\n        self._do_content_purge(url_altered)\n\n        return True\n"
  },
  {
    "path": "r2/r2/lib/providers/cdn/fastly.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport hashlib\nimport requests\nfrom pylons import app_globals as g\n\nfrom r2.lib.providers.cdn import CdnProvider\nfrom r2.lib.utils import constant_time_compare\n\n\nclass FastlyCdnProvider(CdnProvider):\n    \"\"\"A provider for reddit's configuration of Fastly.\"\"\"\n\n    def get_client_ip(self, environ):\n        try:\n            client_ip = environ[\"HTTP_CF_CONNECTING_IP\"]\n            provided_hash = environ[\"HTTP_CF_CIP_TAG\"].lower()\n        except KeyError:\n            return None\n\n        secret = g.secrets[\"cdn_ip_verification\"]\n        expected_hash = hashlib.sha1(client_ip + secret).hexdigest()\n\n        if not constant_time_compare(expected_hash, provided_hash):\n            return None\n\n        return client_ip\n\n    def get_client_location(self, environ):\n        return environ.get(\"HTTP_CF_IPCOUNTRY\", None)\n\n    def purge_content(self, url):\n        \"\"\"Purge the content specified by url from the cache.\n\n        https://docs.fastly.com/api/purge#purge\n\n        \"\"\"\n\n        with g.stats.get_timer('providers.fastly.content_purge'):\n            response = requests.request('PURGE', url)\n"
  },
  {
    "path": "r2/r2/lib/providers/cdn/null.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom r2.lib.providers.cdn import CdnProvider\n\n\nclass NullCdnProvider(CdnProvider):\n    \"\"\"A no-op provider for when no CDN is present.\n\n    \"\"\"\n\n    def get_client_ip(self, environ):\n        return None\n\n    def get_client_location(self, environ):\n        return None\n"
  },
  {
    "path": "r2/r2/lib/providers/email/__init__.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nclass EmailProvider(object):\n    \"\"\"Provider for sending emails.\n\n    \"\"\"\n    def send_email(self, to_address, from_address, subject, text, reply_to,\n                       parent_email_id=None, other_email_ids=None):\n        \"\"\"Send an email.\n\n        `to_address` is a string or list of string email addresses.\n\n        `from_address` is a email address.\n\n        `subject` is the email's subject.\n\n        `text` is the text of the email.\n\n        `reply_to` is the reply-to address, which will be sent as the \"Reply-To\"\n        email header.\n\n        `parent_email_id` is the Message-Id of the email this is a reply to (if\n        any). This is sent as the \"In-Reply-To\" email header.\n\n        `other_email_ids` is a list of Message-Ids of emails in the conversation\n        and including `parent_email_id`. This will be converted to a space\n        delimited string and sent as the \"References\" email header.\n\n        The return value is the Message-Id of the sent email.\n\n        \"\"\"\n        raise NotImplementedError\n\n\nclass EmailSendError(Exception): pass\n"
  },
  {
    "path": "r2/r2/lib/providers/email/mailgun.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\n\nimport json\nimport re\n\nimport requests\n\nfrom r2.lib.configparse import ConfigValue\nfrom r2.lib.providers.email import (\n    EmailProvider,\n    EmailSendError,\n)\nfrom r2.lib.utils import tup\n\n\nclass MailgunEmailProvider(EmailProvider):\n    \"\"\"A provider that uses mailgun to send emails.\"\"\"\n\n    config = {\n        ConfigValue.str: [\n            \"mailgun_api_base_url\",\n        ],\n        # This also requires the secret \"mailgun_api_key\"\n    }\n\n    def send_email(self, to_address, from_address, subject, text, reply_to,\n                   parent_email_id=None, other_email_ids=None):\n        from pylons import app_globals as g\n\n        if not text and not html:\n            msg = \"must provide either text or html in email body\"\n            raise TypeError(msg)\n\n        # pick out the mailgun domain from the from_address field domain\n        from_domain_match = re.search(\"@([\\w.]+)\", from_address)\n\n        if from_domain_match is None:\n            raise ValueError(\"from address is malformed\")\n\n        mailgun_domain = from_domain_match.group(1)\n\n        if mailgun_domain not in g.mailgun_domains:\n            raise ValueError(\"from address must be from an approved domain\")\n\n        message_post_url = \"/\".join((\n            g.mailgun_api_base_url, mailgun_domain, \"messages\"))\n\n        to_address = tup(to_address)\n        parent_email_id = parent_email_id or ''\n        other_email_ids = other_email_ids or []\n\n        response = requests.post(\n            message_post_url,\n            auth=(\"api\", g.secrets['mailgun_api_key']),\n            data={\n                \"from\": from_address,\n                \"to\": to_address,\n                \"subject\": subject,\n                \"text\": text,\n                \"o:tracking\": False,    # disable link rewriting\n                \"h:Reply-To\": reply_to,\n                \"h:In-Reply-To\": parent_email_id,\n                \"h:References\": \" \".join(other_email_ids),\n            },\n        )\n\n        if response.status_code != 200:\n            msg = \"mailgun sending email failed {status}: {text}\".format(\n                status=response.status_code, text=response.text)\n            raise EmailSendError(msg)\n\n        try:\n            body = json.loads(response.text)\n        except ValueError:\n            msg = \"mailgun sending email bad response {status}: {text}\".format(\n                status=response.status_code, text=response.text)\n            g.stats.simple_event(\"mailgun.outgoing.failure\")\n            raise EmailSendError(msg)\n\n        g.stats.simple_event(\"mailgun.outgoing.success\")\n        email_id = body[\"id\"]\n        return email_id\n"
  },
  {
    "path": "r2/r2/lib/providers/email/null.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom r2.lib.providers.email import EmailProvider\n\n\nclass NullEmailProvider(EmailProvider):\n    \"\"\"A no-op email provider.\n\n    \"\"\"\n\n    def send_email(self, to_address, from_address, subject, text, reply_to,\n                       parent_email_id=None, other_email_ids=None):\n        return None\n"
  },
  {
    "path": "r2/r2/lib/providers/image_resizing/__init__.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nclass ImageResizingProvider(object):\n    \"\"\"Provider for generating resizable image urls.\n\n    \"\"\"\n    def resize_image(self, image, width=None, censor_nsfw=False, max_ratio=None):\n        \"\"\"Turn a url of an image in storage into one that will produce a\n        resized image.\n\n        `image` must be a dictionary with keys:\n            url: the storage url given by the media provider after uploading\n            width: in pixels\n            height: in pixels\n\n        `width` is optionally a number of pixels wide for the resultant image;\n        if not specified, the dimensions will be the same as the source image.\n\n        `censor_nsfw` is a boolean indicating whether the resizer should\n        attempt to censor the image (e.g. by blurring it) due to it being NSFW.\n\n        `max_ratio` is the maximum value of the height of the resultant image\n        divided by the width; if not specified, the aspect ratio will be the\n        same as the source image.\n\n        The return value should be an absolute URL with the `https` scheme if\n        supported, but should also work if accessed with `http`.\n\n        Throws NotLargeEnough if the source image is smaller than the requested\n        width.\n        \"\"\"\n        raise NotImplementedError\n\n    def purge_url(self, url):\n        \"\"\"Purge an image (by url) from the provider.\n\n        Providers should override and implement this method if they do\n        something like keep a cache of resized versions that are\n        requested. This will allow the cached version to be deleted, in\n        cases where it needs to be re-generated or removed entirely.\n        \"\"\"\n        pass\n\n\nclass NotLargeEnough(Exception): pass\n"
  },
  {
    "path": "r2/r2/lib/providers/image_resizing/imgix.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport hashlib\n\nfrom pylons import app_globals as g\nimport requests\n\nfrom r2.lib.configparse import ConfigValue\nfrom r2.lib.providers.image_resizing import (\n    ImageResizingProvider,\n    NotLargeEnough,\n)\nfrom r2.lib.utils import UrlParser, query_string\n\nclass ImgixImageResizingProvider(ImageResizingProvider):\n    \"\"\"A provider that uses imgix to create on-the-fly resizings.\"\"\"\n    config = {\n        ConfigValue.str: [\n            'imgix_domain',\n        ],\n    }\n\n    def resize_image(self, image, width=None, censor_nsfw=False, max_ratio=None):\n        url = UrlParser(image['url'])\n        url.hostname = g.imgix_domain\n        # Let's encourage HTTPS; it's cool, works just fine on HTTP pages, and\n        # will prevent insecure content warnings on HTTPS pages.\n        url.scheme = 'https'\n\n        if max_ratio:\n            url.update_query(fit='crop')\n            # http://www.imgix.com/docs/reference/size#param-crop\n            url.update_query(crop='faces,entropy')\n            url.update_query(arh=max_ratio)\n\n        if width:\n            if width > image['width']:\n                raise NotLargeEnough()\n            # http://www.imgix.com/docs/reference/size#param-w\n            url.update_query(w=width)\n        if censor_nsfw:\n            # Do an initial blur to make sure we're getting rid of icky\n            # details.\n            #\n            # http://www.imgix.com/docs/reference/stylize#param-blur\n            url.update_query(blur=600)\n            # And then add pixellation to help the image compress well.\n            #\n            # http://www.imgix.com/docs/reference/stylize#param-px\n            url.update_query(px=32)\n        if g.imgix_signing:\n            url = self._sign_url(url, g.secrets['imgix_signing_token'])\n        return url.unparse()\n\n    def _sign_url(self, url, token):\n        \"\"\"Sign a url for imgix's secured sources.\n\n        Based very heavily on the example code in the docs:\n            http://www.imgix.com/docs/tutorials/securing-images\n\n        Arguments:\n\n        * url -- a UrlParser instance of the url to sign.  This object may be\n                 modified by the function, so make a copy beforehand if that is\n                 a concern.\n        * token -- a string token provided by imgix for request signing\n\n        Returns a UrlParser instance with signing parameters.\n        \"\"\"\n        # Build the signing value\n        signvalue = token + url.path\n        if url.query_dict:\n          signvalue += query_string(url.query_dict)\n\n        # Calculate MD5 of the signing value.\n        signature = hashlib.md5(signvalue).hexdigest()\n\n        url.update_query(s=signature)\n        return url\n\n    def purge_url(self, url):\n        \"\"\"Purge an image (by url) from imgix.\n\n        Reference: http://www.imgix.com/docs/tutorials/purging-images\n\n        Note that as mentioned in the imgix docs, in order to remove\n        an image, this function should be used *after* already\n        removing the image from our source, or imgix will just re-fetch\n        and replace the image with a new copy even after purging.\n        \"\"\"\n        requests.post(\n            \"https://api.imgix.com/v2/image/purger\",\n            auth=(g.secrets[\"imgix_api_key\"], \"\"),\n            data={\"url\": url},\n        )\n"
  },
  {
    "path": "r2/r2/lib/providers/image_resizing/no_op.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom r2.lib.providers.image_resizing import ImageResizingProvider\n\nclass NoOpImageResizingProvider(ImageResizingProvider):\n    \"\"\"A passthrough solution that won't actually resize any images.\n    \n    Combines well with the filesystem media provider for an entirely local\n    setup.\n    \"\"\"\n    def resize_image(self, image, width=None, censor_nsfw=False, max_ratio=None):\n        # The simplest solution: just pass it on through.\n        return image['url']\n"
  },
  {
    "path": "r2/r2/lib/providers/image_resizing/unsplashit.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom r2.lib.providers.image_resizing import ImageResizingProvider\n\nclass UnsplashitImageResizingProvider(ImageResizingProvider):\n    \"\"\"A simple resizer that provides correctly-sized kitten images.\n\n    Useful if you don't want the external dependencies of imgix, but need\n    correctly-sized images for testing a UI.\n    \"\"\"\n    def resize_image(self, image, width=None, censor_nsfw=False, max_ratio=None):\n        if width is None:\n            width = image['width']\n        height = width * 2\n\n        return 'https://unsplash.it/%d/%d' % (width, height)\n"
  },
  {
    "path": "r2/r2/lib/providers/media/__init__.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nclass MediaProvider(object):\n    \"\"\"Provider for storing media objects.\n\n    Media objects are thumbnails, subreddit images/stylesheets, and app icons.\n    A media provider must allow new objects to be added to the system and for\n    users to be able to view those objects over HTTP.\n\n    \"\"\"\n    def make_inaccessible(self, url):\n        \"\"\"Make the content unavaiable, but do not remove. Content could\n        be recovered at a later time.\n\n        `url` must be a url linking to the content\n\n        The return value should be a \n        \"\"\"\n        raise NotImplementedError\n\n    def put(self, category, name, contents, headers=None):\n        \"\"\"Put a media object on the media server and return its HTTP URL.\n\n        `name` must be a local filename including an extension.\n\n        `contents` is a byte string of the contents of the file or a file-like\n                   object the contents of which will be read.\n\n        `headers` an optional dict of additional headers to attach to the media\n                  object. the provider MAY ignore these.\n\n        The return value should be an absolute URL with the `http` scheme but\n        should also work if accessed with `https`.\n\n        \"\"\"\n        raise NotImplementedError\n\n    def purge(self, url):\n        \"\"\"Remove the content. Content can not be recovered.\"\"\"\n        raise NotImplementedError\n"
  },
  {
    "path": "r2/r2/lib/providers/media/filesystem.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport os\nimport shutil\nimport urlparse\n\nfrom pylons import app_globals as g\n\nfrom r2.lib.configparse import ConfigValue\nfrom r2.lib.providers.media import MediaProvider\n\n\nclass FileSystemMediaProvider(MediaProvider):\n    \"\"\"A simple media provider that writes to the filesystem.\n\n    It is assumed that an external HTTP server will take care of serving the\n    media objects once written.\n\n    `media_fs_root` is the root directory on the filesystem to write the objects\n    into.\n\n    `media_fs_base_url_http` is the base URL on which to find the media\n    objects. It should be an absolute URL to the root directory of the media\n    object server that is accessible via both HTTP and HTTPS.\n\n    \"\"\"\n    config = {\n        ConfigValue.str: [\n            \"media_fs_root\",\n            \"media_fs_base_url_http\",\n        ],\n    }\n\n    def make_inaccessible(self, url):\n        # When it comes to file system, there isn't really the concept of\n        # \"making a file inaccessible\" separate from deletion without\n        # losing track of it. For the sake of not creating orphaned files, \n        # not implementing this method\n        g.log.warning(\n            'FileSystemMediaProvider.make_inaccessible is consciously '\n            'not implemented and does not raise an error.'\n        )\n        return True\n\n    def put(self, category, name, contents, headers=None):\n        assert os.path.dirname(name) == \"\"\n        path = os.path.join(g.media_fs_root, name)\n        with open(path, \"w\") as f:\n            if isinstance(contents, basestring):\n                f.write(contents)\n            else:\n                shutil.copyfileobj(contents, f)\n        return urlparse.urljoin(g.media_fs_base_url_http, name)\n        \n    def purge(self, url):\n        \"\"\"Remove the content from disk. Content can not be recovered.\"\"\"\n\n        name = url.split('/')[-1]\n        path = os.path.join(g.media_fs_root, name)\n        os.remove(path)\n        return True\n"
  },
  {
    "path": "r2/r2/lib/providers/media/s3.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport mimetypes\nimport os\nimport re\n\nimport boto\n\nfrom pylons import app_globals as g\n\nfrom r2.lib.configparse import ConfigValue\nfrom r2.lib.providers.media import MediaProvider\n\n\n_NEVER = \"Thu, 31 Dec 2037 23:59:59 GMT\"\n\n\nclass S3MediaProvider(MediaProvider):\n    \"\"\"A media provider using Amazon S3.\n\n    Credentials for uploading objects can be provided via `S3KEY_ID` and\n    `S3SECRET_KEY`. If not provided, boto will search for credentials in\n    alternate venues including environment variables and EC2 instance roles if\n    on Amazon EC2.\n\n    The `s3_media_direct` option configures how URLs are generated. When true,\n    URLs will use Amazon's domain name meaning a zero-DNS configuration. If\n    false, the bucket name will be assumed to be a valid domain name that is\n    appropriately CNAME'd to S3 and URLs will be generated accordingly.\n\n    If more than one bucket is provided in `s3_media_buckets`, items will be\n    sharded out to the various buckets based on their filename. This allows for\n    hostname parallelization in the non-direct HTTP case.\n\n    \"\"\"\n    config = {\n        ConfigValue.str: [\n            \"S3KEY_ID\",\n            \"S3SECRET_KEY\",\n            \"s3_media_domain\",\n        ],\n        ConfigValue.bool: [\n            \"s3_media_direct\",\n        ],\n        ConfigValue.tuple: [\n            \"s3_media_buckets\",\n            \"s3_image_buckets\",\n        ],\n    }\n\n    buckets = {\n        'thumbs': 's3_media_buckets',\n        'stylesheets': 's3_media_buckets',\n        'icons': 's3_media_buckets',\n        'previews': 's3_image_buckets',\n    }\n \n    def _get_bucket(self, bucket_name, validate=False):\n     \n        s3 = boto.connect_s3(g.S3KEY_ID or None, g.S3SECRET_KEY or None)\n        bucket = s3.get_bucket(bucket_name, validate=validate)\n\n        return bucket\n\n    def _get_bucket_key_from_url(self, url):\n        if g.s3_media_domain in url:\n            r_bucket = re.compile('.*\\://(?:%s.)?([^\\/]+)' % g.s3_media_domain)\n        else:\n            r_bucket = re.compile('.*\\://?([^\\/]+)')\n\n        bucket_name = r_bucket.findall(url)[0]\n        key_name = url.split('/')[-1]\n\n        return bucket_name, key_name\n     \n    def make_inaccessible(self, url):\n        \"\"\"Make the content unavailable, but do not remove.\"\"\"\n        bucket_name, key_name = self._get_bucket_key_from_url(url)\n\n        timer = g.stats.get_timer(\"providers.s3.key_set_private\")\n        timer.start()\n\n        bucket = self._get_bucket(bucket_name, validate=False)\n\n        key = bucket.get_key(key_name)\n        if key:\n            # set the file as private, but don't delete it, if it exists\n            key.set_acl('private')\n\n        timer.stop()\n\n        return True\n\n    def put(self, category, name, contents, headers=None):\n        buckets = getattr(g, self.buckets[category])\n        # choose a bucket based on the filename\n        name_without_extension = os.path.splitext(name)[0]\n        index = ord(name_without_extension[-1]) % len(buckets)\n        bucket_name = buckets[index]\n\n        # guess the mime type\n        mime_type, encoding = mimetypes.guess_type(name)\n\n        # build up the headers\n        s3_headers = {\n            \"Content-Type\": mime_type,\n            \"Expires\": _NEVER,\n        }\n        if headers:\n            s3_headers.update(headers)\n\n        # send the key\n        bucket = self._get_bucket(bucket_name, validate=False)\n        key = bucket.new_key(name)\n\n        if isinstance(contents, basestring):\n            set_fn = key.set_contents_from_string\n        else:\n            set_fn = key.set_contents_from_file\n\n        set_fn(\n            contents,\n            headers=s3_headers,\n            policy=\"public-read\",\n            reduced_redundancy=True,\n            replace=True,\n        )\n\n        if g.s3_media_direct:\n            return \"http://%s/%s/%s\" % (g.s3_media_domain, bucket_name, name)\n        else:\n            return \"http://%s/%s\" % (bucket_name, name)\n\n    def purge(self, url):\n        \"\"\"Deletes the key as specified by the url\"\"\"\n        bucket_name, key_name = self._get_bucket_key_from_url(url)\n\n        timer = g.stats.get_timer(\"providers.s3.key_set_private\")\n        timer.start()\n\n        bucket = self._get_bucket(bucket_name, validate=False)\n\n        key_name = url.split('/')[-1]\n        key = bucket.get_key(key_name)\n        if key:\n            # delete the key if it exists\n            key.delete()\n\n        timer.stop()\n\n        return True\n"
  },
  {
    "path": "r2/r2/lib/providers/search/__init__.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\n\nclass SearchProvider(object):\n    \"\"\"Provider for search.\n    \"\"\"\n\n    def InvalidQuery(self):\n        raise NotImplementedError\n\n    def SearchException(self):\n        raise NotImplementedError\n\n    def Query(self):\n        raise NotImplementedError\n\n    def SubredditSearchQuery(self):\n        raise NotImplementedError\n\n    def sorts(self):\n        raise NotImplementedError\n\n    def run_changed(self):\n        raise NotImplementedError\n"
  },
  {
    "path": "r2/r2/lib/providers/search/cloudsearch.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\nimport cPickle as pickle\nfrom datetime import datetime, timedelta\nimport functools\nimport httplib\nimport json\nfrom lxml import etree\nfrom pylons import tmpl_context as c\nfrom pylons import app_globals as g\nimport socket\nimport time\nimport urllib\n\nimport l2cs\n\nfrom r2.lib import amqp, filters\nfrom r2.lib.db.operators import desc\nfrom r2.lib.db.sorts import epoch_seconds\nfrom r2.lib.filters import _force_unicode\nfrom r2.lib.providers.search import SearchProvider\nfrom r2.lib.providers.search.common import (\n    InvalidQuery,\n    LinkFields,\n    Results,\n    safe_get,\n    safe_xml_str,\n    SearchError,\n    SearchHTTPError,\n    SubredditFields,\n)\nimport r2.lib.utils as r2utils\nfrom r2.models import (\n    Account,\n    AllMinus,\n    DomainSR,\n    FakeSubreddit,\n    FriendsSR,\n    Link,\n    MultiReddit,\n    NotFound,\n    Subreddit,\n    Thing,\n)\n\n_TIMEOUT = 5 # seconds for http requests to cloudsearch\n_CHUNK_SIZE = 4000000 # Approx. 4 MB, to stay under the 5MB limit\n_VERSION_OFFSET = 13257906857\n\n\nclass CloudSearchUploader(object):\n    use_safe_get = False\n    types = ()\n\n    def __init__(self, doc_api, fullnames=None, version_offset=_VERSION_OFFSET):\n        self.doc_api = doc_api\n        self._version_offset = version_offset\n        self.fullnames = fullnames\n\n    @classmethod\n    def desired_fullnames(cls, items):\n        '''Pull fullnames that represent instances of 'types' out of items'''\n        fullnames = set()\n        type_ids = [type_._type_id for type_ in cls.types]\n        for item in items:\n            item_type = r2utils.decompose_fullname(item['fullname'])[1]\n            if item_type in type_ids:\n                fullnames.add(item['fullname'])\n        return fullnames\n\n    def _version_tenths(self):\n        '''Cloudsearch documents don't update unless the sent \"version\" field\n        is higher than the one currently indexed. As our documents don't have\n        \"versions\" and could in theory be updated multiple times in one second,\n        for now, use \"tenths of a second since 12:00:00.00 1/1/2012\" as the\n        \"version\" - this will last approximately 13 years until bumping up against\n        the version max of 2^32 for cloudsearch docs'''\n        return int(time.time() * 10) - self._version_offset\n\n    def _version_seconds(self):\n        return int(time.time()) - int(self._version_offset / 10)\n\n    _version = _version_tenths\n\n    def add_xml(self, thing, version):\n        add = etree.Element(\"add\", id=thing._fullname, version=str(version),\n                            lang=\"en\")\n\n        for field_name, value in self.fields(thing).iteritems():\n            field = etree.SubElement(add, \"field\", name=field_name)\n            field.text = safe_xml_str(value)\n\n        return add\n\n    def delete_xml(self, thing, version=None):\n        '''Return the cloudsearch XML representation of\n        \"delete this from the index\"\n        \n        '''\n        version = str(version or self._version())\n        delete = etree.Element(\"delete\", id=thing._fullname, version=version)\n        return delete\n\n    def delete_ids(self, ids):\n        '''Delete documents from the index.\n        'ids' should be a list of fullnames\n        \n        '''\n        version = self._version()\n        deletes = [etree.Element(\"delete\", id=id_, version=str(version))\n                   for id_ in ids]\n        batch = etree.Element(\"batch\")\n        batch.extend(deletes)\n        return self.send_documents(batch)\n\n    def xml_from_things(self):\n        '''Generate a <batch> XML tree to send to cloudsearch for\n        adding/updating/deleting the given things\n        \n        '''\n        batch = etree.Element(\"batch\")\n        self.batch_lookups()\n        version = self._version()\n        for thing in self.things:\n            try:\n                if thing._spam or thing._deleted:\n                    delete_node = self.delete_xml(thing, version)\n                    batch.append(delete_node)\n                elif self.should_index(thing):\n                    add_node = self.add_xml(thing, version)\n                    batch.append(add_node)\n            except (AttributeError, KeyError) as e:\n                # Problem! Bail out, which means these items won't get\n                # \"consumed\" from the queue. If the problem is from DB\n                # lag or a transient issue, then the queue consumer\n                # will succeed eventually. If it's something else,\n                # then manually run a consumer with 'use_safe_get'\n                # on to get past the bad Thing in the queue\n                if not self.use_safe_get:\n                    raise\n                else:\n                    g.log.warning(\"Ignoring problem on thing %r.\\n\\n%r\",\n                                  thing, e)\n        return batch\n\n    def should_index(self, thing):\n        raise NotImplementedError\n\n    def batch_lookups(self):\n        try:\n            self.things = Thing._by_fullname(self.fullnames, data=True,\n                                             return_dict=False)\n        except NotFound:\n            if self.use_safe_get:\n                self.things = safe_get(Thing._by_fullname, self.fullnames,\n                                       data=True, return_dict=False)\n            else:\n                raise\n\n    def fields(self, thing):\n        raise NotImplementedError\n\n    def inject(self, quiet=False):\n        '''Send things to cloudsearch. Return value is time elapsed, in seconds,\n        of the communication with the cloudsearch endpoint\n        \n        '''\n        xml_things = self.xml_from_things()\n\n        if not len(xml_things):\n            return 0\n\n        cs_start = datetime.now(g.tz)\n        sent = self.send_documents(xml_things)\n        cs_time = (datetime.now(g.tz) - cs_start).total_seconds()\n\n        adds, deletes, warnings = 0, 0, []\n        for record in sent:\n            response = etree.fromstring(record)\n            adds += int(response.get(\"adds\", 0))\n            deletes += int(response.get(\"deletes\", 0))\n            if response.get(\"warnings\"):\n                warnings.append(response.get(\"warnings\"))\n\n        g.stats.simple_event(\"cloudsearch.uploads.adds\", delta=adds)\n        g.stats.simple_event(\"cloudsearch.uploads.deletes\", delta=deletes)\n        g.stats.simple_event(\"cloudsearch.uploads.warnings\",\n                delta=len(warnings))\n\n        if not quiet:\n            print \"%s Changes: +%i -%i\" % (self.__class__.__name__,\n                                           adds, deletes)\n            if len(warnings):\n                print \"%s Warnings: %s\" % (self.__class__.__name__,\n                                           \"; \".join(warnings))\n\n        return cs_time\n\n    def send_documents(self, docs):\n        '''Open a connection to the cloudsearch endpoint, and send the documents\n        for indexing. Multiple requests are sent if a large number of documents\n        are being sent (see chunk_xml())\n        \n        Raises SearchHTTPError if the endpoint indicates a failure\n        '''\n        responses = []\n        connection = httplib.HTTPConnection(\n            self.doc_api, port=80, timeout=_TIMEOUT)\n        chunker = chunk_xml(docs)\n        try:\n            for data in chunker:\n                headers = {}\n                headers['Content-Type'] = 'application/xml'\n                # HTTPLib calculates Content-Length header automatically\n                connection.request('POST', \"/2011-02-01/documents/batch\",\n                                   data, headers)\n                response = connection.getresponse()\n                if 200 <= response.status < 300:\n                    responses.append(response.read())\n                else:\n                    raise SearchHTTPError(response.status,\n                                               response.reason,\n                                               response.read())\n        finally:\n            connection.close()\n        return responses\n\n\nclass LinkUploader(CloudSearchUploader):\n    types = (Link,)\n\n    def __init__(self, doc_api, fullnames=None, version_offset=_VERSION_OFFSET):\n        super(LinkUploader, self).__init__(doc_api, fullnames, version_offset)\n        self.accounts = {}\n        self.srs = {}\n\n    def fields(self, thing):\n        '''Return fields relevant to a Link search index'''\n        account = self.accounts[thing.author_id]\n        sr = self.srs[thing.sr_id]\n        return LinkFields(thing, account, sr).fields()\n\n    def batch_lookups(self):\n        super(LinkUploader, self).batch_lookups()\n        author_ids = [thing.author_id for thing in self.things\n                      if hasattr(thing, 'author_id')]\n        try:\n            self.accounts = Account._byID(author_ids, data=True,\n                                          return_dict=True)\n        except NotFound:\n            if self.use_safe_get:\n                self.accounts = safe_get(Account._byID, author_ids, data=True,\n                                         return_dict=True)\n            else:\n                raise\n\n        sr_ids = [thing.sr_id for thing in self.things\n                  if hasattr(thing, 'sr_id')]\n        try:\n            self.srs = Subreddit._byID(sr_ids, data=True, return_dict=True)\n        except NotFound:\n            if self.use_safe_get:\n                self.srs = safe_get(Subreddit._byID, sr_ids, data=True,\n                                    return_dict=True)\n            else:\n                raise\n\n    def should_index(self, thing):\n        return (thing.promoted is None and getattr(thing, \"sr_id\", None) != -1)\n\n\nclass SubredditUploader(CloudSearchUploader):\n    types = (Subreddit,)\n    _version = CloudSearchUploader._version_seconds\n\n    def fields(self, thing):\n        return SubredditFields(thing).fields()\n\n    def should_index(self, thing):\n        return thing._id != Subreddit.get_promote_srid()\n\n\ndef chunk_xml(xml, depth=0):\n    '''Chunk POST data into pieces that are smaller than the 20 MB limit.\n    \n    Ideally, this never happens (if chunking is necessary, would be better\n    to avoid xml'ifying before testing content_length)'''\n    data = etree.tostring(xml)\n    content_length = len(data)\n    if content_length < _CHUNK_SIZE:\n        yield data\n    else:\n        depth += 1\n        print \"WARNING: Chunking (depth=%s)\" % depth\n        half = len(xml) / 2\n        left_half = xml # for ease of reading\n        right_half = etree.Element(\"batch\")\n        # etree magic simultaneously removes the elements from one tree\n        # when they are appended to a different tree\n        right_half.extend(xml[half:])\n        for chunk in chunk_xml(left_half, depth=depth):\n            yield chunk\n        for chunk in chunk_xml(right_half, depth=depth):\n            yield chunk\n\n\n@g.stats.amqp_processor('cloudsearch_changes')\ndef _run_changed(msgs, chan):\n    '''Consume the cloudsearch_changes queue, and print reporting information\n    on how long it took and how many remain\n    \n    '''\n    start = datetime.now(g.tz)\n\n    changed = [pickle.loads(msg.body) for msg in msgs]\n\n    link_fns = LinkUploader.desired_fullnames(changed)\n    sr_fns = SubredditUploader.desired_fullnames(changed)\n\n    link_uploader = LinkUploader(g.CLOUDSEARCH_DOC_API, fullnames=link_fns)\n    subreddit_uploader = SubredditUploader(g.CLOUDSEARCH_SUBREDDIT_DOC_API,\n                                           fullnames=sr_fns)\n\n    link_time = link_uploader.inject()\n    subreddit_time = subreddit_uploader.inject()\n    cloudsearch_time = link_time + subreddit_time\n\n    totaltime = (datetime.now(g.tz) - start).total_seconds()\n\n    print (\"%s: %d messages in %.2fs seconds (%.2fs secs waiting on \"\n           \"cloudsearch); %d duplicates, %s remaining)\" %\n           (start, len(changed), totaltime, cloudsearch_time,\n            len(changed) - len(link_fns | sr_fns),\n            msgs[-1].delivery_info.get('message_count', 'unknown')))\n\n\ndef run_changed(drain=False, min_size=500, limit=1000, sleep_time=10,\n                use_safe_get=False, verbose=False):\n    '''Run by `cron` (through `paster run`) on a schedule to send Things to\n        Amazon CloudSearch\n    \n    '''\n    if use_safe_get:\n        CloudSearchUploader.use_safe_get = True\n    amqp.handle_items('cloudsearch_changes', _run_changed, min_size=min_size,\n                      limit=limit, drain=drain, sleep_time=sleep_time,\n                      verbose=verbose)\n\n\ndef _progress_key(item):\n    return \"%s/%s\" % (item._id, item._date)\n\n\ndef rebuild_link_index(start_at=None, sleeptime=1, cls=Link,\n                       uploader=LinkUploader, doc_api='CLOUDSEARCH_DOC_API',\n                       estimate=50000000, chunk_size=1000):\n    doc_api = getattr(g, doc_api)\n    uploader = uploader(doc_api)\n\n    q = cls._query(cls.c._deleted == (True, False), sort=desc('_date'))\n\n    if start_at:\n        after = cls._by_fullname(start_at)\n        assert isinstance(after, cls)\n        q._after(after)\n\n    q = r2utils.fetch_things2(q, chunk_size=chunk_size)\n    q = r2utils.progress(q, verbosity=1000, estimate=estimate, persec=True,\n                         key=_progress_key)\n    for chunk in r2utils.in_chunks(q, size=chunk_size):\n        uploader.things = chunk\n        for x in range(5):\n            try:\n                uploader.inject()\n            except httplib.HTTPException as err:\n                print \"Got %s, sleeping %s secs\" % (err, x)\n                time.sleep(x)\n                continue\n            else:\n                break\n        else:\n            raise err\n        last_update = chunk[-1]\n        print \"last updated %s\" % last_update._fullname\n        time.sleep(sleeptime)\n\n\nrebuild_subreddit_index = functools.partial(rebuild_link_index,\n                                            cls=Subreddit,\n                                            uploader=SubredditUploader,\n                                            doc_api='CLOUDSEARCH_SUBREDDIT_DOC_API',\n                                            estimate=200000,\n                                            chunk_size=1000)\n\n\ndef test_run_link(start_link, count=1000):\n    '''Inject `count` number of links, starting with `start_link`'''\n    if isinstance(start_link, basestring):\n        start_link = int(start_link, 36)\n    links = Link._byID(range(start_link - count, start_link), data=True,\n                       return_dict=False)\n    uploader = LinkUploader(g.CLOUDSEARCH_DOC_API, things=links)\n    return uploader.inject()\n\n\ndef test_run_srs(*sr_names):\n    '''Inject Subreddits by name into the index'''\n    srs = Subreddit._by_name(sr_names).values()\n    uploader = SubredditUploader(g.CLOUDSEARCH_SUBREDDIT_DOC_API, things=srs)\n    return uploader.inject()\n\n\n### Query Code ###\n_SEARCH = \"/2011-02-01/search?\"\nINVALID_QUERY_CODES = ('CS-UnknownFieldInMatchExpression',\n                       'CS-IncorrectFieldTypeInMatchExpression',\n                       'CS-InvalidMatchSetExpression',)\nDEFAULT_FACETS = {\"reddit\": {\"count\":20}}\ndef basic_query(query=None, bq=None, faceting=None, size=1000,\n                start=0, rank=None, rank_expressions=None,\n                return_fields=None, record_stats=False, search_api=None):\n    if search_api is None:\n        search_api = g.CLOUDSEARCH_SEARCH_API\n    if faceting is None:\n        faceting = DEFAULT_FACETS\n    path = _encode_query(query, bq, faceting, size, start, rank,\n                         rank_expressions, return_fields)\n    timer = None\n    if record_stats:\n        timer = g.stats.get_timer(\"providers.cloudsearch\")\n        timer.start()\n    connection = httplib.HTTPConnection(search_api, port=80, timeout=_TIMEOUT)\n    try:\n        connection.request('GET', path)\n        resp = connection.getresponse()\n        response = resp.read()\n        if record_stats:\n            g.stats.action_count(\"event.search_query\", resp.status)\n        if resp.status >= 300:\n            try:\n                reasons = json.loads(response)\n            except ValueError:\n                pass\n            else:\n                messages = reasons.get(\"messages\", [])\n                for message in messages:\n                    if message['code'] in INVALID_QUERY_CODES:\n                        raise InvalidQuery(resp.status, resp.reason, message,\n                                           search_api, path, reasons)\n            raise SearchHTTPError(resp.status, resp.reason,\n                                  search_api, path, response)\n    except socket.timeout as e:\n        g.stats.simple_event('cloudsearch.error.timeout')\n        raise SearchError(e, search_api, path)\n    except socket.error as e:\n        g.stats.simple_event('cloudsearch.error.socket')\n        raise SearchError(e, search_api, path)\n    finally:\n        connection.close()\n        if timer is not None:\n            timer.stop()\n\n    return json.loads(response)\n\n\nbasic_link = functools.partial(basic_query, size=10, start=0,\n                               rank=\"-relevance\",\n                               return_fields=['title', 'reddit',\n                                              'author_fullname'],\n                               record_stats=False,\n                               search_api=g.CLOUDSEARCH_SEARCH_API)\n\n\nbasic_subreddit = functools.partial(basic_query,\n                                    faceting=None,\n                                    size=10, start=0,\n                                    rank=\"-activity\",\n                                    return_fields=['title', 'reddit',\n                                                   'author_fullname'],\n                                    record_stats=False,\n                                    search_api=g.CLOUDSEARCH_SUBREDDIT_SEARCH_API)\n\n\ndef _encode_query(query, bq, faceting, size, start, rank, rank_expressions,\n                  return_fields):\n    if not (query or bq):\n        raise ValueError(\"Need query or bq\")\n    params = {}\n    if bq:\n        params[\"bq\"] = bq\n    if query:\n        params[\"q\"] = query\n    params[\"results-type\"] = \"json\"\n    params[\"size\"] = size\n    params[\"start\"] = start\n    if rank:\n        params[\"rank\"] = rank\n    if rank_expressions:\n        for rank, expression in rank_expressions.iteritems():\n            params['rank-%s' % rank] = expression\n    if faceting:\n        params[\"facet\"] = \",\".join(faceting.iterkeys())\n        for facet, options in faceting.iteritems():\n            params[\"facet-%s-top-n\" % facet] = options.get(\"count\", 20)\n            if \"sort\" in options:\n                params[\"facet-%s-sort\" % facet] = options[\"sort\"]\n    if return_fields:\n        params[\"return-fields\"] = \",\".join(return_fields)\n    encoded_query = urllib.urlencode(params)\n    path = _SEARCH + encoded_query\n    return path\n\n\nclass CloudSearchQuery(object):\n    '''Represents a search query sent to cloudsearch'''\n    search_api = None\n    sorts = {}\n    recents = {None: None}\n    default_syntax = \"plain\"\n    lucene_parser = None\n\n    def __init__(self, query, sr=None, sort=None, syntax=None, raw_sort=None,\n                 faceting=None, recent=None, include_over18=True,\n                 rank_expressions=None, start=0, num=1000):\n        if syntax is None:\n            syntax = self.default_syntax\n        elif syntax not in self.known_syntaxes:\n            raise ValueError(\"Unknown search syntax: %s\" % syntax)\n        self.syntax = syntax\n\n        self.query = filters._force_unicode(query or u'')\n\n        # parsed query\n        self.converted_data = None\n        self.q = u''\n        self.bq = u''\n\n        # filters\n        self.sr = sr\n        self._recent = recent\n        self.recent = self.recents[recent]\n        self.include_over18 = include_over18\n\n        # rank / rank expressions\n        self._sort = sort\n        if raw_sort:\n            self.sort = raw_sort\n        else:\n            self.sort = self.sorts.get(sort)\n        self.rank_expressions = rank_expressions\n\n        # pagination\n        self.start = start\n        self.num = num\n\n        # facets\n        self.faceting = faceting\n\n        self.results = None\n\n    def run(self, _update=False):\n        results = self._run(_update=_update)\n        self.results = Results(results.docs, results.hits, results._facets)\n        return self.results\n\n    def _parse(self):\n        query = self.preprocess_query(self.query)\n\n        if self.syntax == \"cloudsearch\":\n            self.bq = self.customize_query(query)\n        elif self.syntax == \"lucene\":\n            bq = l2cs.convert(query, self.lucene_parser)\n            self.converted_data = {\"syntax\": \"cloudsearch\", \"converted\": bq}\n            self.bq = self.customize_query(bq)\n        elif self.syntax == \"plain\":\n            self.q = query.encode('utf-8')\n            self.bq = self.customize_query()\n\n        if not self.q and not self.bq:\n            raise InvalidQuery\n\n    def _run(self, _update=False):\n        '''Run the search against self.query'''\n        try:\n            self._parse()\n        except InvalidQuery:\n            return Results([], 0, {})\n\n        if g.sqlprinting:\n            g.log.info(\"%s\", self)\n\n        return self._run_cached(self.q, self.bq.encode('utf-8'), self.sort,\n                                self.rank_expressions, self.faceting,\n                                start=self.start, num=self.num, _update=_update)\n\n    def preprocess_query(self, query):\n        return query\n\n    def customize_query(self, bq=u''):\n        return bq\n\n    @classmethod\n    def create_boolean_query(cls, queries):\n        '''Return an AND clause combining all queries'''\n        if len(queries) > 1:\n            return '(and ' + ' '.join(queries) + ')'\n        elif queries:\n            return queries[0]\n        return u''\n\n    def __repr__(self):\n        '''Return a string representation of this query'''\n        result = [\"<\", self.__class__.__name__, \"> query:\",\n                  repr(self.query), \" \"]\n        if self.bq:\n            result.append(\" bq:\")\n            result.append(repr(self.bq))\n            result.append(\" \")\n        if self.sort:\n            result.append(\"sort:\")\n            result.append(self.sort)\n        return ''.join(result)\n\n    @classmethod\n    def _run_cached(cls, query, bq, sort=\"relevance\", rank_expressions=None,\n                    faceting=None, start=0, num=1000, _update=False):\n        '''Query the cloudsearch API. _update parameter allows for supposed\n        easy memoization at later date.\n        \n        Example result set:\n        \n        {u'facets': {u'reddit': {u'constraints':\n                                    [{u'count': 114, u'value': u'politics'},\n                                    {u'count': 42, u'value': u'atheism'},\n                                    {u'count': 27, u'value': u'wtf'},\n                                    {u'count': 19, u'value': u'gaming'},\n                                    {u'count': 12, u'value': u'bestof'},\n                                    {u'count': 12, u'value': u'tf2'},\n                                    {u'count': 11, u'value': u'AdviceAnimals'},\n                                    {u'count': 9, u'value': u'todayilearned'},\n                                    {u'count': 9, u'value': u'pics'},\n                                    {u'count': 9, u'value': u'funny'}]}},\n         u'hits': {u'found': 399,\n                   u'hit': [{u'id': u't3_11111'},\n                            {u'id': u't3_22222'},\n                            {u'id': u't3_33333'},\n                            {u'id': u't3_44444'},\n                            ...\n                            ],\n                   u'start': 0},\n         u'info': {u'cpu-time-ms': 10,\n                   u'messages': [{u'code': u'CS-InvalidFieldOrRankAliasInRankParameter',\n                                  u'message': u\"Unable to create score object for rank '-hot'\",\n                                  u'severity': u'warning'}],\n                   u'rid': u'<hash>',\n                   u'time-ms': 9},\n                   u'match-expr': u\"(label 'my query')\",\n                   u'rank': u'-text_relevance'}\n        \n        '''\n        try:\n            response = basic_query(query=query, bq=bq, size=num, start=start,\n                                   rank=sort, rank_expressions=rank_expressions,\n                                   search_api=cls.search_api,\n                                   faceting=faceting, record_stats=True)\n        except (SearchHTTPError, SearchError) as e:\n            g.log.error(\"Search Error: %r\", e)\n            raise\n\n        warnings = response['info'].get('messages', [])\n        for warning in warnings:\n            g.log.warning(\"%(code)s (%(severity)s): %(message)s\" % warning)\n\n        hits = response['hits']['found']\n        docs = [doc['id'] for doc in response['hits']['hit']]\n        facets = response.get('facets', {})\n        for facet in facets.keys():\n            values = facets[facet]['constraints']\n            facets[facet] = values\n\n        results = Results(docs, hits, facets)\n        return results\n\n\nclass LinkSearchQuery(CloudSearchQuery):\n    search_api = g.CLOUDSEARCH_SEARCH_API\n    sorts = {\n        'relevance': '-relevance',\n        'relevance2': '-relevance2',\n        'hot': '-hot2',\n        'top': '-top',\n        'new': '-timestamp',\n        'comments': '-num_comments',\n    }\n    recents = {\n        'hour': timedelta(hours=1),\n        'day': timedelta(days=1),\n        'week': timedelta(days=7),\n        'month': timedelta(days=31),\n        'year': timedelta(days=366),\n        'all': None,\n        None: None,\n    }\n    schema = l2cs.make_schema(LinkFields.lucene_fieldnames())\n    lucene_parser = l2cs.make_parser(\n             int_fields=LinkFields.lucene_fieldnames(type_=int),\n             yesno_fields=LinkFields.lucene_fieldnames(type_=\"yesno\"),\n             schema=schema)\n    known_syntaxes = g.search_syntaxes\n    default_syntax = \"lucene\"\n\n    def customize_query(self, bq=u''):\n        queries = []\n        if bq:\n            queries = [bq]\n        if self.sr:\n            subreddit_query = self._restrict_sr(self.sr)\n            if subreddit_query:\n                queries.append(subreddit_query)\n        if self.recent:\n            recent_query = self._restrict_recent(self.recent)\n            queries.append(recent_query)\n        if not self.include_over18:\n            queries.append('over18:0')\n        return self.create_boolean_query(queries)\n\n    @staticmethod\n    def _restrict_recent(recent):\n        now = datetime.now(g.tz)\n        since = epoch_seconds(now - recent)\n        return 'timestamp:%i..' % since\n\n    @staticmethod\n    def _restrict_sr(sr):\n        '''Return a cloudsearch appropriate query string that restricts\n        results to only contain results from sr\n        \n        '''\n        if isinstance(sr, MultiReddit):\n            if not sr.sr_ids:\n                raise InvalidQuery\n            srs = [\"sr_id:%s\" % sr_id for sr_id in sr.sr_ids]\n            return \"(or %s)\" % ' '.join(srs)\n        elif isinstance(sr, DomainSR):\n            return \"site:'\\\"%s\\\"'\" % sr.domain\n        elif isinstance(sr, FriendsSR):\n            if not c.user_is_loggedin or not c.user.friends:\n                raise InvalidQuery\n            # The query limit is roughly 8k bytes. Limit to 200 friends to\n            # avoid getting too close to that limit\n            friend_ids = c.user.friends[:200]\n            friends = [\"author_fullname:'%s'\" %\n                       Account._fullname_from_id36(r2utils.to36(id_))\n                       for id_ in friend_ids]\n            return \"(or %s)\" % ' '.join(friends)\n        elif isinstance(sr, AllMinus):\n            if not sr.exclude_sr_ids:\n                raise InvalidQuery\n            exclude_srs = [\"sr_id:%s\" % sr_id for sr_id in sr.exclude_sr_ids]\n            return \"(not (or %s))\" % ' '.join(exclude_srs)\n        elif not isinstance(sr, FakeSubreddit):\n            return \"sr_id:%s\" % sr._id\n\n        return None\n\n\nclass CloudSearchSubredditSearchQuery(CloudSearchQuery):\n    search_api = g.CLOUDSEARCH_SUBREDDIT_SEARCH_API\n    sorts = {\n        'relevance': '-relevance',\n        'activity': '-activity',\n    }\n    known_syntaxes = (\"plain\",)\n    default_syntax = \"plain\"\n\n    def preprocess_query(self, query):\n        # Expand search for /r/subreddit to include subreddit name.\n        sr = query.strip('/').split('/')\n        if len(sr) == 2 and sr[0] == 'r' and Subreddit.is_valid_name(sr[1]):\n            query = '\"%s\" | %s' % (query, sr[1])\n        return query\n\n    def customize_query(self, bq=u''):\n        queries = []\n        if bq:\n            queries = [bq]\n        if not self.include_over18:\n            queries.append('over18:0')\n        return self.create_boolean_query(queries)\n\n\nclass CloudSearchProvider(SearchProvider):\n    '''Provider implementation: wrap it all up as a SearchProvider'''\n    InvalidQuery = (InvalidQuery,)\n    SearchException = (SearchHTTPError, SearchError)\n\n    SearchQuery = LinkSearchQuery\n\n    SubredditSearchQuery = CloudSearchSubredditSearchQuery\n\n    def run_changed(self, drain=False, min_size=int(getattr(g, 'SOLR_MIN_BATCH', 500)), limit=1000, sleep_time=10, \n            use_safe_get=False, verbose=False):\n        '''Run by `cron` (through `paster run`) on a schedule to send Things to Cloud\n        '''\n        if use_safe_get:\n            CloudSearchUploader.use_safe_get = True\n        amqp.handle_items('cloudsearch_changes', _run_changed, min_size=min_size,\n                          limit=limit, drain=drain, sleep_time=sleep_time,\n                          verbose=verbose)\n"
  },
  {
    "path": "r2/r2/lib/providers/search/common.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom datetime import datetime\nfrom pylons import tmpl_context as c\nfrom pylons import app_globals as g\nimport collections\nimport httplib\nimport time\nimport re\n\nimport r2.lib.utils as r2utils\nfrom r2.models import (Link, NotFound, Subreddit)\n\n\nclass InvalidQuery(Exception):\n    pass\n\n\nclass SearchError(Exception):\n    pass\n\n\nclass SearchHTTPError(httplib.HTTPException):\n    pass\n\n\ndef safe_xml_str(s, use_encoding=\"utf-8\"):\n    '''Replace invalid-in-XML unicode control characters with '\\uFFFD'.\n    Also, coerces result to unicode\n    \n    '''\n    illegal_xml = re.compile(u'[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1F\\uD800-\\uDFFF\\uFFFE\\uFFFF]')\n\n    if not isinstance(s, unicode):\n        if isinstance(s, str):\n            s = unicode(s, use_encoding, errors=\"replace\")\n        else:\n            # ints will raise TypeError if the \"errors\" kwarg\n            # is passed, but since it's not a str no problem\n            s = unicode(s)\n    s = illegal_xml.sub(u\"\\uFFFD\", s)\n    return s\n\n\ndef safe_get(get_fn, ids, return_dict=True, **kw):\n    items = {}\n    for i in ids:\n        try:\n            item = get_fn(i, **kw)\n        except NotFound:\n            g.log.info(\"%s failed for %r\", get_fn.__name__, i)\n        else:\n            items[i] = item\n    if return_dict:\n        return items\n    else:\n        return items.values()\n\n\n\nSAME_AS_CLOUDSEARCH = object()\nFIELD_TYPES = (int, str, datetime, SAME_AS_CLOUDSEARCH, \"yesno\")\n\ndef field(name=None, cloudsearch_type=str, lucene_type=SAME_AS_CLOUDSEARCH):\n    Field = collections.namedtuple(\"Field\", \"name cloudsearch_type \"\n                                   \"lucene_type function\")\n    if lucene_type is SAME_AS_CLOUDSEARCH:\n        lucene_type = cloudsearch_type\n    if cloudsearch_type not in FIELD_TYPES + (None,):\n        raise ValueError(\"cloudsearch_type %r not in %r\" %\n                         (cloudsearch_type, FIELD_TYPES))\n    if lucene_type not in FIELD_TYPES + (None,):\n        raise ValueError(\"lucene_type %r not in %r\" %\n                         (lucene_type, FIELD_TYPES))\n    if callable(name):\n        # Simple case; decorated as '@field'; act as a decorator instead\n        # of a decorator factory\n        function = name\n        name = None\n    else:\n        function = None\n\n    def field_inner(fn):\n        fn.field = Field(name or fn.func_name, cloudsearch_type,\n                         lucene_type, fn)\n        return fn\n\n    if function:\n        return field_inner(function)\n    else:\n        return field_inner\n\n\nclass FieldsMeta(type):\n    def __init__(cls, name, bases, attrs):\n        type.__init__(cls, name, bases, attrs)\n        fields = []\n        for attr in attrs.itervalues():\n            if hasattr(attr, \"field\"):\n                fields.append(attr.field)\n        cls._fields = tuple(fields)\n\n\nclass FieldsBase(object):\n    __metaclass__ = FieldsMeta\n\n    def fields(self):\n        data = {}\n        for field in self._fields:\n            if field.cloudsearch_type is None:\n                continue\n            val = field.function(self)\n            if val is not None:\n                data[field.name] = val\n        return data\n\n    @classmethod\n    def all_fields(cls):\n        return cls._fields\n\n    @classmethod\n    def cloudsearch_fields(cls, type_=None, types=FIELD_TYPES):\n        types = (type_,) if type_ else types\n        return [f for f in cls._fields if f.cloudsearch_type in types]\n\n    @classmethod\n    def lucene_fields(cls, type_=None, types=FIELD_TYPES):\n        types = (type_,) if type_ else types\n        return [f for f in cls._fields if f.lucene_type in types]\n\n    @classmethod\n    def cloudsearch_fieldnames(cls, type_=None, types=FIELD_TYPES):\n        return [f.name for f in cls.cloudsearch_fields(type_=type_,\n                                                       types=types)]\n\n    @classmethod\n    def lucene_fieldnames(cls, type_=None, types=FIELD_TYPES):\n        return [f.name for f in cls.lucene_fields(type_=type_, types=types)]\n\n\nclass LinkFields(FieldsBase):\n    def __init__(self, link, author, sr):\n        self.link = link\n        self.author = author\n        self.sr = sr\n\n    @field(cloudsearch_type=int, lucene_type=None)\n    def ups(self):\n        return max(0, self.link._ups)\n\n    @field(cloudsearch_type=int, lucene_type=None)\n    def downs(self):\n        return max(0, self.link._downs)\n\n    @field(cloudsearch_type=int, lucene_type=None)\n    def num_comments(self):\n        return max(0, getattr(self.link, 'num_comments', 0))\n\n    @field\n    def fullname(self):\n        return self.link._fullname\n\n    @field\n    def subreddit(self):\n        return self.sr.name\n\n    @field\n    def reddit(self):\n        return self.sr.name\n\n    @field\n    def title(self):\n        return self.link.title\n\n    @field(cloudsearch_type=int)\n    def sr_id(self):\n        return self.link.sr_id\n\n    @field(cloudsearch_type=int, lucene_type=datetime)\n    def timestamp(self):\n        return int(time.mktime(self.link._date.utctimetuple()))\n\n    @field(cloudsearch_type=int, lucene_type=\"yesno\")\n    def over18(self):\n        nsfw = self.sr.over_18 or self.link.is_nsfw\n        return (1 if nsfw else 0)\n\n    @field(cloudsearch_type=None, lucene_type=\"yesno\")\n    def nsfw(self):\n        return NotImplemented\n\n    @field(cloudsearch_type=int, lucene_type=\"yesno\")\n    def is_self(self):\n        return (1 if self.link.is_self else 0)\n\n    @field(name=\"self\", cloudsearch_type=None, lucene_type=\"yesno\")\n    def self_(self):\n        return NotImplemented\n\n    @field\n    def author_fullname(self):\n        return None if self.author._deleted else self.author._fullname\n\n    @field(name=\"author\")\n    def author_field(self):\n        return None if self.author._deleted else self.author.name\n\n    @field(cloudsearch_type=int)\n    def type_id(self):\n        return self.link._type_id\n\n    @field\n    def site(self):\n        if self.link.is_self:\n            return g.domain\n        else:\n            try:\n                url = r2utils.UrlParser(self.link.url)\n                return list(url.domain_permutations())\n            except ValueError:\n                return None\n\n    @field\n    def selftext(self):\n        if self.link.is_self and self.link.selftext:\n            return self.link.selftext\n        else:\n            return None\n\n    @field\n    def url(self):\n        if not self.link.is_self:\n            return self.link.url\n        else:\n            return None\n\n    @field\n    def flair_css_class(self):\n        return self.link.flair_css_class\n\n    @field\n    def flair_text(self):\n        return self.link.flair_text\n\n    @field(cloudsearch_type=None, lucene_type=str)\n    def flair(self):\n        return NotImplemented\n\n\nclass SubredditFields(FieldsBase):\n    def __init__(self, sr):\n        self.sr = sr\n\n    @field\n    def name(self):\n        return self.sr.name\n\n    @field\n    def title(self):\n        return self.sr.title\n\n    @field(name=\"type\")\n    def type_(self):\n        return self.sr.type\n\n    @field\n    def language(self):\n        return self.sr.lang\n\n    @field\n    def header_title(self):\n        return None if self.sr.type == 'private' else self.sr.header_title\n\n    @field\n    def description(self):\n        return self.sr.public_description\n\n    @field\n    def sidebar(self):\n        return None if self.sr.type == 'private' else self.sr.description\n\n    @field(cloudsearch_type=int)\n    def over18(self):\n        return 1 if self.sr.over_18 else 0\n\n    @field\n    def link_type(self):\n        return self.sr.link_type\n\n    @field\n    def activity(self):\n        return self.sr._downs\n\n    @field\n    def subscribers(self):\n        return self.sr._ups\n\n    @field\n    def type_id(self):\n        return self.sr._type_id\n\n\nclass Results(object):\n    def __init__(self, docs, hits, facets):\n        self.docs = docs\n        self.hits = hits\n        self._facets = facets\n        self._subreddits = []\n\n    def __repr__(self):\n        return '%s(%r, %r, %r)' % (self.__class__.__name__,\n                                   self.docs,\n                                   self.hits,\n                                   self._facets)\n\n    @property\n    def subreddit_facets(self):\n        '''Filter out subreddits that the user isn't allowed to see'''\n        if not self._subreddits and 'reddit' in self._facets:\n            sr_facets = [(sr['value'], sr['count']) for sr in\n                         self._facets['reddit']]\n\n            # look up subreddits\n            srs_by_name = Subreddit._by_name([name for name, count\n                                              in sr_facets])\n\n            sr_facets = [(srs_by_name[name], count) for name, count\n                         in sr_facets if name in srs_by_name]\n\n            # filter by can_view\n            self._subreddits = [(sr, count) for sr, count in sr_facets\n                                if sr.can_view(c.user)]\n\n        return self._subreddits\n"
  },
  {
    "path": "r2/r2/lib/providers/search/solr.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport cPickle as pickle\nfrom datetime import datetime, timedelta\nimport functools\nimport httplib\nimport json\nimport socket\nimport time\nimport urllib\n\nfrom lxml import etree\nfrom pylons import tmpl_context as c\nfrom pylons import app_globals as g\n\nfrom r2.lib import amqp, filters\nfrom r2.lib.configparse import ConfigValue\nfrom r2.lib.db.operators import desc\nfrom r2.lib.db.sorts import epoch_seconds\nfrom r2.lib.providers.search import SearchProvider\nfrom r2.lib.providers.search.common import (\n        InvalidQuery, \n        LinkFields, \n        Results,\n        safe_get, \n        safe_xml_str,\n        SearchError,\n        SearchHTTPError, \n        SubredditFields, \n    )\nimport r2.lib.utils as r2utils\nfrom r2.models import (\n        Account, \n        All, \n        AllMinus,\n        DefaultSR,\n        DomainSR, \n        FakeSubreddit, \n        Friends, \n        Link, \n        MultiReddit, \n        NotFound,\n        Subreddit, \n        Thing,\n    )\n\n\n_CHUNK_SIZE = 4000000 # Approx. 4 MB, to stay under the 5MB limit\nDEFAULT_FACETS = {\"reddit\": {\"count\":20}}\n\nWARNING_XPATH = \".//lst[@name='error']/str[@name='warning']\"\nSTATUS_XPATH = \".//lst/int[@name='status']\"\n\nSORTS_DICT = {'text_relevance': 'score',\n              'relevance': 'score'}\n\ndef basic_query(query=None, bq=None, faceting=None, size=1000,\n                start=0, rank=\"\", return_fields=None, record_stats=False,\n                search_api=None):\n    if search_api is None:\n        search_api = g.solr_search_host\n    if faceting is None:\n        faceting = DEFAULT_FACETS\n    path = _encode_query(query, faceting, size, start, rank, return_fields)\n    timer = None\n    if record_stats:\n        timer = g.stats.get_timer(\"solrsearch_timer\")\n        timer.start()\n    connection = httplib.HTTPConnection(search_api, g.solr_port)\n    try:\n        connection.request('GET', path)\n        resp = connection.getresponse()\n        response = resp.read()\n        if record_stats:\n            g.stats.action_count(\"event.search_query\", resp.status)\n        if resp.status >= 300:\n            try:\n                response_json = json.loads(response)\n            except ValueError:\n                pass\n            else:\n                if 'error' in response_json:\n                    message = response_json['error'].get('msg', 'Unknown error')\n                    raise InvalidQuery(resp.status, resp.reason, message,\n                                       search_api, path, reasons)\n            raise SearchHTTPError(resp.status, resp.reason,\n                                  search_api, path, response)\n    except socket.error as e:\n        raise SearchError(e, search_api, path)\n    finally:\n        connection.close()\n        if timer is not None:\n            timer.stop()\n\n    return json.loads(response)\n\n\nbasic_link = functools.partial(basic_query, size=10, start=0,\n                               rank=\"\",\n                               return_fields=['title', 'reddit',\n                                              'author_fullname'],\n                               record_stats=False,\n                               search_api=g.solr_search_host)\n\n\nbasic_subreddit = functools.partial(basic_query,\n                                    faceting=None,\n                                    size=10, start=0,\n                                    rank=\"activity\",\n                                    return_fields=['title', 'reddit',\n                                                   'author_fullname'],\n                                    record_stats=False,\n                                    search_api=g.solr_subreddit_search_host)\n\n\nclass SolrSearchQuery(object):\n    '''Represents a search query sent to solr'''\n    search_api = None\n    recents = {None: None}\n    default_syntax = \"solr\"\n\n    def __init__(self, query, sr=None, sort=None, syntax=None, raw_sort=None,\n                 faceting=None, recent=None, include_over18=True,\n                 rank_expressions=None, start=0, num=1000):\n        if syntax is None:\n            syntax = self.default_syntax\n        elif syntax not in self.known_syntaxes:\n            raise ValueError(\"Unknown search syntax: %s\" % syntax)\n        self.bq = None\n        self.query = filters._force_unicode(query or u'')\n        self.converted_data = None\n        self.syntax = syntax\n\n        # filters\n        self.sr = sr\n        self._recent = recent\n        self.recent = self.recents[recent]\n        self.include_over18 = include_over18\n        \n        # rank / rank expressions\n        self._sort = sort\n        if raw_sort:\n            self.sort = _translate_raw_sort(raw_sort)\n        elif sort:\n            self.sort = self.sorts.get(sort)\n        else:\n            self.sort = 'score'\n        self.rank_expressions = rank_expressions\n\n        # pagination\n        self.start = start\n        self.num = num\n\n        # facets\n        self.faceting = faceting\n        \n        self.results = None\n\n    def run(self, after=None, reverse=False, num=1000, _update=False):\n        self.bq = u''\n        results = self._run(_update=_update)\n\n        docs, hits, facets = results.docs, results.hits, results._facets\n\n        after_docs = r2utils.get_after(docs, after, num, reverse=reverse)\n\n        self.results = Results(after_docs, hits, facets)\n        return self.results\n\n    def _run(self, start=0, num=1000, _update=False):\n        '''Run the search against self.query'''\n        \n        q = self.query.encode('utf-8')\n        if g.sqlprinting:\n            g.log.info(\"%s\", self)\n        q = self.customize_query(self.query)\n        return self._run_cached(q, self.bq.encode('utf-8'), self.sort,\n                                self.faceting, start=start, num=num,\n                                _update=_update)\n\n    def customize_query(self, q):\n        return q\n\n    def __repr__(self):\n        '''Return a string representation of this query'''\n        result = [\"<\", self.__class__.__name__, \"> query:\",\n                  repr(self.query), \" \"]\n        if self.bq:\n            result.append(\" bq:\")\n            result.append(repr(self.bq))\n            result.append(\" \")\n        result.append(\"sort:\")\n        result.append(self.sort)\n        return ''.join(result)\n\n    @classmethod\n    def _run_cached(cls, query, bq, sort=\"score\", faceting=None, start=0,\n                    num=1000, _update=False):\n        '''Query the solr HOST. _update parameter allows for supposed\n        easy memoization at later date.\n        \n        Example result set:\n        {\n            u'responseHeader':{\n                u'status':0,\n                u'QTime':2,\n                u'params':{\n                    u'sort':u'activity desc',\n                    u'defType':u'edismax',\n                    u'q':u'coffee',\n                    u'start':u'0',\n                    u'wt':u'json',\n                    u'size':u'1000'\n                }\n            },\n            u'response':{\n                u'start':0,\n                u'numFound':1,\n                u'docs':[\n                    {\n                        u'_version_':1496564637825499136,\n                        u'type_id':5,\n                        u'reddit':u'coffee',\n                        u'fullname':u't5_3',\n                        u'author':u'grandpa',\n                        u'url':u'http://hamsandwich.com/sideoffries/?attachment_id=44',\n                        u'num_comments':0,\n                        u'downs':1,\n                        u'title':u'013',\n                        u'site':u\"[u'reddit.com',u'hamsandwich.reddit.com']\", \n                        u'author_s': u'grandpa', \n                        u'over18': False, \n                        u'timestamp': 1427180669, \n                        u'sr_id': 2, \n                        u'author_fullname': u't2_1', \n                        u'is_self': False, \n                        u'subreddit': u'coffee', \n                        u'ups': 0, u'id': u't5_3'}, \n                    {\n                ]\n            }\n        }\n        '''\n        if not query:\n            return Results([], 0, {})\n        try:\n            response = basic_query(query=query, bq=bq, size=num, start=start,\n                               rank=sort, search_api=cls.search_api,\n                               faceting=faceting, record_stats=True)\n        except (SearchHTTPError, SearchError) as e:\n            g.log.error(\"Search Error: %r\", e)\n            raise\n\n        hits = response['response']['numFound']\n        docs = [doc['id'] for doc in response['response']['docs']]\n        facets = {}\n        if hits and faceting:\n            facet_fields = response['facet_counts'].get('facet_fields', {})\n            for field in facet_fields:\n                facets[field] = []\n                while facet_fields[field]:\n                    value = facet_fields[field].pop(0)\n                    count = facet_fields[field].pop(0)\n                    facets[field].append(dict(value=value, count=count)) \n            \n\n        results = Results(docs, hits, facets)\n        return results\n\n\nclass LinkSearchQuery(SolrSearchQuery):\n    search_api = g.solr_search_host\n    sorts = {\n        'relevance': 'score desc',\n        'hot': 'max(hot/45000.0, 1.0) desc',\n        'top': 'top desc',\n        'new': 'timestamp desc',\n        'comments': 'num_comments desc',\n    }\n    recents = {\n        'hour': timedelta(hours=1),\n        'day': timedelta(days=1),\n        'week': timedelta(days=7),\n        'month': timedelta(days=31),\n        'year': timedelta(days=366),\n        'all': None,\n        None: None,\n    }\n    known_syntaxes = g.search_syntaxes\n    default_syntax = 'solr'\n\n    def customize_query(self, bq):\n        queries = [bq]\n        subreddit_query = self._get_sr_restriction(self.sr)\n        if subreddit_query:\n            queries.append(subreddit_query)\n        if self.recent:\n            recent_query = self._restrict_recent(self.recent)\n            queries.append(recent_query)\n        return self.create_boolean_query(queries)\n\n    @classmethod\n    def create_boolean_query(cls, queries):\n        '''Return an AND clause combining all queries'''\n        if len(queries) > 1:\n            bq = ' AND  '.join(['(%s)' %q for q in queries]) \n        else:\n            bq = queries[0]\n        return bq\n\n    @staticmethod\n    def _restrict_recent(recent):\n        now = datetime.now(g.tz)\n        since = epoch_seconds(now - recent)\n        return 'timestamp:%i..' % since\n\n    @staticmethod\n    def _get_sr_restriction(sr):\n        '''Return a solr-appropriate query string that restricts\n        results to only contain results from sr\n        \n        '''\n        bq = []\n        if (not sr) or sr == All or isinstance(sr, DefaultSR):\n            return None\n        elif isinstance(sr, MultiReddit):\n            for sr_id in sr.sr_ids:\n                bq.append(\"sr_id:%s\" % sr_id)\n        elif isinstance(sr, DomainSR):\n            bq = [\"site:'%s'\" % sr.domain]\n        elif sr == Friends:\n            if not c.user_is_loggedin or not c.user.friends:\n                return None\n            friend_ids = c.user.friends\n            friends = [\"author_fullname:'%s'\" %\n                       Account._fullname_from_id36(r2utils.to36(id_))\n                       for id_ in friend_ids]\n            bq.extend(friends)\n        elif isinstance(sr, AllMinus):\n            for sr_id in sr.exclude_sr_ids:\n                bq.append(\"-sr_id:%s\" % sr_id)\n        elif not isinstance(sr, FakeSubreddit):\n            bq = [\"sr_id:%s\" % sr._id]\n        return ' OR '.join(bq)\n\n\nclass SolrSubredditSearchQuery(SolrSearchQuery):\n    search_api = g.solr_subreddit_search_host\n    sorts = {\n        'relevance': 'activity desc',\n    }\n    known_syntaxes = (\"plain\", \"solr\")\n    default_syntax = \"plain\"\n\n\ndef _encode_query(query, faceting, size, start, rank, return_fields):\n    if not query:\n        raise ValueError(\"Need query\")\n    params = {}\n    params[\"q\"] = query\n    params[\"wt\"] = \"json\"\n    #params[\"defType\"] = \"edismax\"\n    params[\"size\"] = size\n    params[\"start\"] = start\n    if rank: \n        params['sort'] = rank.strip().lower()\n        if not params['sort'].split()[-1] in ['asc', 'desc']:\n            params['sort'] = '%s desc' % params['sort']\n    facet_limit = []\n    facet_sort = []\n    if faceting:\n        params[\"facet\"] = \"true\"\n        params[\"facet.field\"] = \",\".join(faceting.iterkeys())\n        for facet, options in faceting.iteritems():\n            facet_limit.append(options.get(\"count\", 20))\n            if \"sort\" in options:\n                if not options['sort'].split()[-1] in ['asc', 'desc']:\n                    options['sort'] = '%s desc' % options['sort']\n                facet_sort.append(options[\"sort\"])\n        params[\"facet.limit\"] = \",\".join([str(l) for l in facet_limit])\n        params[\"facet.sort\"] = \",\".join(facet_sort)\n        params[\"facet.sort\"] = params[\"facet.sort\"] or 'score desc' \n    if return_fields:\n        params[\"qf\"] = \",\".join(return_fields)\n    encoded_query = urllib.urlencode(params)\n    if getattr(g, 'solr_version', '1').startswith('4'):\n        path = '/solr/%s/select?%s' % \\\n            (getattr(g, 'solr_core', 'collection1'), encoded_query)\n    else:\n        path = '/solr/select?%s' %  encoded_query\n    return path    \n\n\nclass SolrSearchUploader(object):\n    \n    def __init__(self, solr_host=None, solr_port=None, fullnames=None):\n        self.solr_host = solr_host or g.solr_doc_host\n        self.solr_port = solr_port or g.solr_port\n        self.fullnames = fullnames    \n\n    @classmethod\n    def desired_fullnames(cls, items):\n        '''Pull fullnames that represent instances of 'types' out of items'''\n\n        fullnames = set()\n        type_ids = [type_._type_id for type_ in cls.types]\n        for item in items:\n            item_type = r2utils.decompose_fullname(item['fullname'])[1]\n            if item_type in type_ids:\n                fullnames.add(item['fullname'])\n        return fullnames\n\n    def add_xml(self, thing):\n\n        doc = etree.Element(\"doc\")\n        field = etree.SubElement(doc, \"field\", name='id')\n        field.text = thing._fullname\n\n        for field_name, value in self.fields(thing).iteritems():\n            field = etree.SubElement(doc, \"field\", name=field_name)\n            field.text = safe_xml_str(value)\n\n        return doc\n\n    def delete_xml(self, thing):\n        '''Return the solr XML representation of\n        \"delete this from the index\"\n        \n        '''\n        delete = etree.fromstring('<id>%s</id>' % thing._id)\n        return delete\n\n    def delete_ids(self, ids):\n        '''Delete documents from the index.\n        'ids' should be a list of fullnames\n        \n        '''\n        deletes = [etree.fromstring('<id>%s</id>' % id_) \\\n                for id_ in ids]\n\n\n        batch = etree.Element(\"delete\")\n        batch.extend(deletes)\n        return self.send_documents(batch)\n\n    def batch_lookups(self):                                                                                            \n        try:                                                                                                            \n            self.things = Thing._by_fullname(self.fullnames, data=True,                                                 \n                                             return_dict=False)                                                         \n        except NotFound:                                                                                                \n            if self.use_safe_get:                                                                                       \n                self.things = safe_get(Thing._by_fullname, self.fullnames,                                              \n                                       data=True, return_dict=False)                                                    \n            else:                                                                                                       \n                raise \n\n    def xml_from_things(self):\n        '''Generate a <batch> XML tree to send to solr for\n        adding/updating/deleting the given things\n        \n        '''\n        add = etree.Element(\"add\")\n        delete = etree.Element(\"delete\")\n        commit = etree.Element(\"commit\")\n        commit.attrib[\"waitSearcher\"] = \"false\"\n\n        self.batch_lookups()\n        for thing in self.things:\n            try:\n                if thing._spam or thing._deleted:\n                    delete_node = self.delete_xml(thing)\n                    delete.append(delete_node)\n                elif self.should_index(thing):\n                    add_node = self.add_xml(thing)\n                    add.append(add_node)\n            except (AttributeError, KeyError) as e:\n                # Problem! Bail out, which means these items won't get\n                # \"consumed\" from the queue. If the problem is from DB\n                # lag or a transient issue, then the queue consumer\n                # will succeed eventually. If it's something else,\n                # then manually run a consumer with 'use_safe_get'\n                # on to get past the bad Thing in the queue\n                if not self.use_safe_get:\n                    raise\n                else:\n                    g.log.warning(\"Ignoring problem on thing %r.\\n\\n%r\",\n                                  thing, e)\n\n        elems = []\n        if len(add):\n            elems.append(add)\n        if len(delete):\n            elems.append(delete)\n        if elems:\n            # Only need to commit if something is sent\n            elems.append(commit)\n        return elems\n\n\n    def inject(self, quiet=False):\n        '''Send things to solr. Return value is time elapsed, in seconds,\n        of the communication with the solr endpoint\n        \n        '''\n\n        xml_things = self.xml_from_things()\n\n        cs_time = 0\n        for batch in xml_things:\n\n            cs_start = datetime.now(g.tz)\n            sent = self.send_documents(batch)\n            cs_time = cs_time + (datetime.now(g.tz) - cs_start).total_seconds()\n\n            adds, deletes, warnings = 0, 0, []\n            for record in sent:\n                response = etree.fromstring(record)\n                status = response.find(STATUS_XPATH).text\n                if status == '0':\n                    # success! \n                    adds += len(batch.findall('doc'))\n                    deletes += len(batch.findall('delete'))\n                    for w in response.find(WARNING_XPATH) or []:\n                        warnings.append(w.text)\n\n            g.stats.simple_event(\"solrsearch.uploads.adds\", delta=adds)\n            g.stats.simple_event(\"solrsearch.uploads.deletes\", delta=deletes)\n            g.stats.simple_event(\"solrsearch.uploads.warnings\",\n                    delta=len(warnings))\n\n            if not quiet:\n                print \"%s Changes: +%i -%i\" % (self.__class__.__name__,\n                                               adds, deletes)\n                if len(warnings):\n                    print \"%s Warnings: %s\" % (self.__class__.__name__,\n                                               \"; \".join(warnings))\n\n        return cs_time    \n\n    def send_documents(self, docs):\n        '''Open a connection to the Solr endpoint, and send the documents\n        for indexing. Multiple requests are sent if a large number of documents\n        are being sent (see chunk_xml())\n        \n        Raises SearchHTTPError if the endpoint indicates a failure\n        '''\n        core = getattr(g, 'solr_core', 'collection1') \n        responses = []\n        connection = httplib.HTTPConnection(self.solr_host, self.solr_port)\n        chunker = chunk_xml(docs)\n        headers = {}\n        headers['Content-Type'] = 'application/xml'\n        try:\n            for data in chunker:\n                # HTTPLib calculates Content-Length header automatically\n                if getattr(g, 'solr_version', '1').startswith('4'):\n                    connection.request('POST', \"/solr/%s/update/\" % core,\n                                       data, headers)\n                else:     \n                    connection.request('POST', \"/solr/update/\",\n                                       data, headers)\n                response = connection.getresponse()\n                if 200 <= response.status < 300:\n                    responses.append(response.read())\n                else:\n                    raise SearchHTTPError(response.status,\n                                               response.reason,\n                                               response.read())\n        finally:\n            connection.close()\n        return responses\n\n\ndef chunk_xml(xml, depth=0):\n    '''Chunk POST data into pieces that are smaller than the 20 MB limit.\n    \n    Ideally, this never happens (if chunking is necessary, would be better\n    to avoid xml'ifying before testing content_length)'''\n    data = etree.tostring(xml)\n    root = xml.findall('add') and 'add' or 'delete'\n    content_length = len(data)\n    if content_length < _CHUNK_SIZE:\n        yield data\n    else:\n        depth += 1\n        print \"WARNING: Chunking (depth=%s)\" % depth\n        half = len(xml) / 2\n        left_half = xml # for ease of reading\n        right_half = etree.Element(root)\n        # etree magic simultaneously removes the elements from one tree\n        # when they are appended to a different tree\n        right_half.append(xml[half:])\n        for chunk in chunk_xml(left_half, depth=depth):\n            yield chunk\n        for chunk in chunk_xml(right_half, depth=depth):\n            yield chunk\n\n\n@g.stats.amqp_processor('solrsearch_q')\ndef _run_changed(msgs, chan):\n    '''Consume the cloudsearch_changes queue, and print reporting information\n    on how long it took and how many remain\n    \n    '''\n    start = datetime.now(g.tz)\n\n    changed = [pickle.loads(msg.body) for msg in msgs]\n\n    link_fns = SolrLinkUploader.desired_fullnames(changed)\n    sr_fns = SolrSubredditUploader.desired_fullnames(changed)\n\n    link_uploader = SolrLinkUploader(g.solr_doc_host, fullnames=link_fns)\n    subreddit_uploader = SolrSubredditUploader(g.solr_subreddit_doc_host,\n                                           fullnames=sr_fns)\n\n    link_time = link_uploader.inject()\n    subreddit_time = subreddit_uploader.inject()\n    solrsearch_time = link_time + subreddit_time\n\n    totaltime = (datetime.now(g.tz) - start).total_seconds()\n\n    print (\"%s: %d messages in %.2fs seconds (%.2fs secs waiting on \"\n           \"solr); %d duplicates, %s remaining)\" %\n           (start, len(changed), totaltime, solrsearch_time,\n            len(changed) - len(link_fns | sr_fns),\n            msgs[-1].delivery_info.get('message_count', 'unknown')))\n\n\nclass SolrLinkUploader(SolrSearchUploader):\n    types = (Link,)\n\n    def __init__(self, solr_host=None, solr_port=None, fullnames=None):\n        super(SolrLinkUploader, self).__init__(fullnames=fullnames)\n        self.accounts = {}\n        self.srs = {}\n\n    def fields(self, thing):\n        '''Return fields relevant to a Link search index'''\n        account = self.accounts[thing.author_id]\n        sr = self.srs[thing.sr_id]\n        return LinkFields(thing, account, sr).fields()\n\n    def batch_lookups(self):\n        super(SolrLinkUploader, self).batch_lookups()\n        author_ids = [thing.author_id for thing in self.things\n                      if hasattr(thing, 'author_id')]\n        try:\n            self.accounts = Account._byID(author_ids, data=True,\n                                          return_dict=True)\n        except NotFound:\n            if self.use_safe_get:\n                self.accounts = safe_get(Account._byID, author_ids, data=True,\n                                         return_dict=True)\n            else:\n                raise\n\n        sr_ids = [thing.sr_id for thing in self.things\n                  if hasattr(thing, 'sr_id')]\n        try:\n            self.srs = Subreddit._byID(sr_ids, data=True, return_dict=True)\n        except NotFound:\n            if self.use_safe_get:\n                self.srs = safe_get(Subreddit._byID, sr_ids, data=True,\n                                    return_dict=True)\n            else:\n                raise\n\n    def should_index(self, thing):\n        return (thing.promoted is None and getattr(thing, \"sr_id\", None) != -1)\n    \n\nclass SolrSubredditUploader(SolrSearchUploader):\n    types = (Subreddit,)\n\n    def fields(self, thing):\n        return SubredditFields(thing).fields()\n\n    def should_index(self, thing):\n        return thing._id != Subreddit.get_promote_srid()\n\n \ndef _progress_key(item):\n    return \"%s/%s\" % (item._id, item._date)\n\n\ndef _rebuild_link_index(start_at=None, sleeptime=1, cls=Link,\n                       uploader=SolrLinkUploader, estimate=50000000, \n                       chunk_size=1000):\n    uploader = uploader()\n\n    q = cls._query(cls.c._deleted == (True, False), sort=desc('_date'))\n\n    if start_at:\n        after = cls._by_fullname(start_at)\n        assert isinstance(after, cls)\n        q._after(after)\n\n    q = r2utils.fetch_things2(q, chunk_size=chunk_size)\n    q = r2utils.progress(q, verbosity=1000, estimate=estimate, persec=True,\n                         key=_progress_key)\n    for chunk in r2utils.in_chunks(q, size=chunk_size):\n        uploader.things = chunk\n        uploader.fullnames = [c._fullname for c in chunk] \n        for x in range(5):\n            try:\n                uploader.inject()\n            except httplib.HTTPException as err:\n                print \"Got %s, sleeping %s secs\" % (err, x)\n                time.sleep(x)\n                continue\n            else:\n                break\n        else:\n            raise err\n        last_update = chunk[-1]\n        print \"last updated %s\" % last_update._fullname\n        time.sleep(sleeptime)\n\n\nrebuild_subreddit_index = functools.partial(_rebuild_link_index,\n                                            cls=Subreddit,\n                                            uploader=SolrSubredditUploader,\n                                            estimate=200000,\n                                            chunk_size=1000)\n\n\ndef test_run_link(start_link, count=1000):\n    '''Inject `count` number of links, starting with `start_link`'''\n    if isinstance(start_link, basestring):\n        start_link = int(start_link, 36)\n    links = Link._byID(range(start_link - count, start_link), data=True,\n                       return_dict=False)\n    uploader = SolrLinkUploader(things=links)\n    return uploader.inject()\n\n\ndef test_run_srs(*sr_names):\n    '''Inject Subreddits by name into the index'''\n    srs = Subreddit._by_name(sr_names).values()\n    uploader = SolrSubredditUploader(things=srs)\n    return uploader.inject()\n\n\ndef _translate_raw_sort(sort):\n    '''translate from cloudsearch syntax'''\n    sort_dir = ''\n    if sort.startswith('-'):\n        sort = sort[1:]\n        sort_dir = ' desc'\n    sort = SORTS_DICT.get(sort, sort) \n    return '%s%s' % (sort, sort_dir)\n\nclass SolrSearchProvider(SearchProvider):\n    '''Provider implementation: wrap it all up as a SearchProvider\n    \n    example config:\n    # version of solr service--versions 1.x and 4.x have been tested. \n    # only the major version number matters here\n    solr_version = 1\n    # solr search service hostname or IP\n    solr_search_host = 127.0.0.1\n    # hostname or IP for link upload\n    solr_doc_host = 127.0.0.1\n    # hostname or IP for subreddit search\n    solr_subreddit_search_host = 127.0.0.1\n    # hostname or IP subreddit upload\n    solr_subreddit_doc_host = 127.0.0.1\n    # solr port (assumed same on all hosts)\n    solr_port = 8080\n    # solr4 core name (not used with Solr 1.x)\n    solr_core = collection1\n    # default batch size \n    # limit is hard-coded to 1000\n    # set to 1 for testing\n    solr_min_batch = 500\n    # optionally, you may select your solr query parser here\n    # see documentation for your version of Solr\n    solr_query_parser = \n    '''\n\n    SOLR_VERSION = 1\n  \n    config = {\n        ConfigValue.int: [\n            \"solr_port\",\n            \"solr_min_batch\",\n        ],\n        ConfigValue.str: [\n            \"solr_search_host\",\n            \"solr_doc_host\",\n            \"solr_subreddit_search_host\",\n            \"solr_subreddit_doc_host\",\n            \"solr_core\",\n            \"solr_version\",\n        ],\n    }    \n\n    InvalidQuery = (InvalidQuery,)\n    SearchException = (SearchHTTPError, SearchError)\n\n    SearchQuery = LinkSearchQuery\n\n    SubredditSearchQuery = SolrSubredditSearchQuery\n    \n    def run_changed(self, drain=False, min_size=int(getattr(g, 'solr_min_batch', 500)), limit=1000, sleep_time=10, \n            use_safe_get=False, verbose=False):\n        '''Run by `cron` (through `paster run`) on a schedule to send Things to Solr\n        '''\n        if use_safe_get:\n            SolrSearchUploader.use_safe_get = True\n        amqp.handle_items('cloudsearch_changes', _run_changed, min_size=min_size,\n                          limit=limit, drain=drain, sleep_time=sleep_time,\n                          verbose=verbose)\n\n    def rebuild_link_index(self, start_at=None, sleeptime=1, cls=Link,\n                           uploader=SolrLinkUploader, estimate=50000000, \n                           chunk_size=1000):\n         _rebuild_link_index(start_at, sleeptime, cls, uploader, estimate,  \n                            chunk_size)\n"
  },
  {
    "path": "r2/r2/lib/providers/support/__init__.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nclass TicketProvider(object):\n    \"\"\"Provider for handling support tickets interactions.\n\n    \"\"\"\n    \n    def build_ticket_url_from_id(self, ticket_id):\n        \"\"\"\n        Creates a URL to access a ticket based on URL\n\n        `ticket_id` the unique identifier for the ticket\n\n        Returns a URL to the ticket\n        \"\"\"\n        raise NotImplementedError\n        \n    def create(self):\n        \"\"\"Creates a new support ticket with the provider. \n\n        Parameters may vary depending on the provider you use.\n\n        The return value should be a JSON object of the created ticket.\n        \"\"\"\n        raise NotImplementedError\n\n    def get(self, ticket_id):\n        \"\"\"Gets an existing ticket from the provider.\n\n        `ticket_id` is a unique identifier dor the ticket.\n\n        The return value should be a JSON object of the created ticket.\n        \"\"\"\n        raise NotImplementedError\n\n    def update(self):\n        \"\"\"Updates the ticket with the provider\n\n        Parameters may vary depending on the provider you use.\n\n        The return value should be a JSON object of the updated ticket.\n        \"\"\"\n        raise NotImplementedError\n"
  },
  {
    "path": "r2/r2/lib/providers/support/zendesk.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport json\nimport re\nimport requests\n\nfrom pylons import app_globals as g\n\nfrom r2.lib.configparse import ConfigValue\nfrom r2.lib.providers.support import TicketProvider\n\nclass ZenDeskProvider(TicketProvider):\n    \"\"\"A provider that interfaces with ZenDesk for managing tickets.\"\"\"\n\n    def build_ticket_url_from_id(self, ticket_id):\n        return '%sagent/tickets/%s' % (\n            g.live_config['ticket_base_url'], \n            ticket_id,\n        )\n\n    def create(self, \n               subject, \n               group_id,\n               comment_body, \n               comment_is_public=False,\n               requester_id=None, \n               custom_fields=[],\n               ):\n        \"\"\"Creates a new support ticket on ZenDesk. \n\n        `subject` (self explanatory)\n        `group_id` The group ID to assign to\n        `comment_body` (self explanatory)\n        `comment_is_public` Whether the comment is public or internal\n        `requester_id` The external user ID for the user submitting\n        `custom_fields` If custom feilds are defined and need to be set\n\n        Returns a JSON object of the newly created ticket.\n        \"\"\"\n        zd_ticket_create_params = {\n            'ticket': {\n                \"requester_id\": requester_id,\n                \"subject\": subject,\n                \"comment\": { \n                    \"body\": comment_body,\n                    \"public\": comment_is_public,\n                },\n                \"group_id\": group_id,\n                \"custom_fields\": custom_fields,\n            }\n        }\n        \n        timer = g.stats.get_timer(\"providers.zendesk.ticket_create\")\n        timer.start()\n        response = requests.post(\n            '%s/api/v2/tickets.json' % g.live_config['ticket_base_url'], \n            auth=(\n                '%s/token' % g.secrets['zendesk_user'], \n                g.secrets[\"zendesk_api_key\"]\n            ),\n            headers={'content-type': 'application/json'},\n            data=json.dumps(zd_ticket_create_params))\n        timer.stop\n        \n        if response.status_code != 201:\n            g.log.error(\n                'ZENDESK_CREATE_ERROR: code: %s msg: %s' % \n                (response.status_code, response.text)\n            )\n            return None\n\n        return json.loads(response.content)['ticket']\n    \n    def get_ticket_id_from_url(self, ticket_url):\n        \"\"\"Extracts the ticket ID from a URL.\"\"\"\n        r = re.compile('(\\d).*')\n        numbers_in_url = r.findall(ticket_url)\n        if not numbers_in_url:\n            raise Exception('No digits in request_url')\n        else:\n            return numbers_in_url[0]\n\n    def get(self, ticket_id):        \n        \"\"\"Gets a ticket from ZenDesk as a JSON object\"\"\"\n        timer = g.stats.get_timer(\"providers.zendesk.ticket_get\")\n        timer.start()\n        response = requests.get(\n            '%s/api/v2/tickets/%s.json' % (\n                g.live_config['ticket_base_url'], \n                ticket_id\n            ),\n            auth=(\n                '%s/token' % g.secrets['zendesk_user'], \n                g.secrets[\"zendesk_api_key\"]\n            ),\n            headers={'content-type': 'application/json'},\n        )\n        timer.stop()\n        \n        if response.status_code != 200:\n            g.log.error(\n                'ZENDESK_GET_ERROR: code: %s msg: %s' % \n                (response.status_code, response.text)\n            )\n            return None\n            \n        return json.loads(response.content)['ticket']\n\n    def update(self, ticket, status=None,\n               comment_body='', comment_is_public=False, \n               tag_list=None):\n        \"\"\"Updates the ticket on ZenDesk.\"\"\"\n        \n        if comment_body:\n            comment_json = {\n                    \"public\": comment_is_public, \n                    \"body\": comment_body,\n                }\n        else:\n            comment_json = {}\n        \n        ticket_updated_json = {\n            'ticket': {\n                \"comment\": comment_json,\n                \"status\": status or ticket['status'],\n                \"tags\": tag_list or ticket['tags'],\n            }\n        }\n        \n        timer = g.stats.get_timer(\"providers.zendesk.ticket_update\")\n        timer.start()\n        response = requests.put(\n            '%s/api/v2/tickets/%s.json' % (\n                g.live_config['ticket_base_url'], \n                ticket['id']\n            ),\n            auth=(\n                '%s/token' % g.secrets['zendesk_user'], \n                g.secrets[\"zendesk_api_key\"]\n            ),\n            headers={'content-type': 'application/json'},\n            data=json.dumps(ticket_updated_json)\n        )\n        timer.stop()\n        \n        if response.status_code != 200:\n            g.log.error(\n                'ZENDESK_UPDATE_ERROR: code: %s msg: %s' % \n                (response.status_code, response.text)\n            )\n            return None\n            \n        return json.loads(response.content)['ticket']\n"
  },
  {
    "path": "r2/r2/lib/ratelimit.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2016 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\"\"\"A rate limit implementation with state in a key-value cache with item expiry.\n\nThe standalone functions (get_usage, get_timeslice, record_usage and the _multi\nvariants) adapt from the building blocks needed to compute a rate limit to the\nkey-value operations.\n\nThis protocol is best-effort, *not* atomic and transactional. Multiple\nsimultaneous accesses may push the actual rate limit above the intended limit.\n\nThe RateLimit class implements the \"check a rate limit\" and the \"record a usage\"\noperations and a policy of how the rate limit configuration is loaded from the\napplication configuration.\n\"\"\"\n\nimport collections\nimport time\n\nimport pylibmc\n\nfrom pylons import app_globals as g\n\n# AKA, a half open interval.\nclass TimeSlice(collections.namedtuple(\"TimeSlice\", [\"beginning\", \"end\"])):\n\n  @property\n  def remaining(self):\n      return self.end - int(time.time())\n\n\nclass RatelimitError(Exception):\n    def __init__(self, e):\n        self.wrapped = e\n\n    def __str__(self):\n        return str(self.wrapped)\n\n\ndef _make_ratelimit_cache_key(key_prefix, time_slice):\n    # Short term rate limits: use a timestamp that's only valid for a day.\n    fmt = '-%H%M%S'\n    # Long term rate limits: use an unambigious timestamp.\n    if time_slice.end - time_slice.beginning >= 86400:\n        fmt = '-@%s'\n\n    # enforce the \"rl:\" prefix for mcrouter\n    prefix = \"rl:\" + key_prefix\n    return prefix + time.strftime(fmt, time.gmtime(time_slice.beginning))\n\n\ndef get_timeslice(slice_seconds):\n    \"\"\"Return tuple describing the current slice given slice width.\n\n    The elements of the tuple are:\n\n    - `beginning`: seconds since epoch to beginning of time period\n    - `end`: seconds since epoch to end of time period\n    \"\"\"\n\n    now = int(time.time())\n    slice_count = now // slice_seconds\n    slice_start = int(slice_count * slice_seconds)\n    slice_end = slice_start + slice_seconds\n    return TimeSlice(slice_start, slice_end)\n\n\ndef record_usage(key_prefix, time_slice):\n    \"\"\"Record usage of a ratelimit for the specified time slice.\n\n    The total usage (including this one) of the ratelimit is returned or\n    RatelimitError is raised if something went wrong during the process.\n\n    \"\"\"\n\n    key = _make_ratelimit_cache_key(key_prefix, time_slice)\n\n    try:\n        g.ratelimitcache.add(key, 0, time=time_slice.remaining)\n\n        try:\n            return g.ratelimitcache.incr(key)\n        except pylibmc.NotFound:\n            # Previous round of ratelimiting fell out in the\n            # time between calling `add` and calling `incr`.\n            now = int(time.time())\n            if now < time_slice.end:\n                g.ratelimitcache.add(key, 1, time=time_slice.end - now + 1)\n                g.stats.simple_event(\"ratelimit.eviction\")\n            return 1\n    except pylibmc.Error as e:\n        raise RatelimitError(e)\n\n\ndef record_usage_multi(prefix_slices):\n    \"\"\"Record usage of multiple rate limits.\n\n    If any of the of the rate limits expire during the processing of the\n    function, the usage counts may be inaccurate and it is not defined\n    which, if any, of the keys have been updated in the underlying cache.\n\n    Arguments:\n        prefix_slices: A list of (prefix, timeslice)\n\n    Returns:\n        A list of the usage counts in the same order as prefix_slices.\n\n    Raises:\n        RateLimitError if anything goes wrong.\n        It is not defined which, if any, of the keys have been updated if this\n        happens.\n\n    \"\"\"\n\n    keys = [_make_ratelimit_cache_key(k, t) for k, t in prefix_slices]\n\n    try:\n        # Can't use add_multi because the various timeslices may be different.\n        now = int(time.time())\n        for key, (_, time_slice) in zip(keys, prefix_slices):\n            g.ratelimitcache.add(key, 0, time=time_slice.end - now + 1)\n\n        try:\n            recent_usage = g.ratelimitcache.incr_multi(keys)\n        except pylibmc.NotFound:\n            # Some part of the previous round of ratelimiting fell out in the\n            # time between calling `add` and calling `incr`.\n            now = int(time.time())\n            if now < time_slice.end:\n                recent_usage = []\n                for key, (_, time_slice) in zip(keys, prefix_slices):\n                    if g.ratelimitcache.add(key, 1,\n                                            time=time_slice.end - now + 1):\n                        recent_usage.append(1)\n                        g.stats.simple_event(\"ratelimit.eviction\")\n                    else:\n                        recent_usage.append(g.ratelimitcache.get(key))\n        return recent_usage\n    except pylibmc.Error as e:\n        raise RatelimitError(e)\n\n\ndef get_usage(key_prefix, time_slice):\n    \"\"\"Return the current usage of a ratelimit for the specified time slice.\"\"\"\n\n    key = _make_ratelimit_cache_key(key_prefix, time_slice)\n\n    try:\n        return g.ratelimitcache.get(key)\n    except pylibmc.NotFound:\n        return 0\n    except pylibmc.Error as e:\n        raise RatelimitError(e)\n\n\ndef get_usage_multi(prefix_slices):\n    \"\"\"Return the current usage of several rate limits.\n\n    Arguments:\n        prefix_slices: A list of (prefix, timeslice)\n\n    Returns:\n        A list of usages in the same order as prefix_slices\n    \"\"\"\n    keys = [_make_ratelimit_cache_key(k, t) for k, t in prefix_slices]\n\n    try:\n        values = g.ratelimitcache.get_multi(keys)\n        return [values.get(k, 0) for k in keys]\n    except pylibmc.Error as e:\n        raise RatelimitError(e)\n\n\nclass RateLimit(object):\n    \"\"\"A general purpose whole system rate limit.\n\n    This class takes several basically-static properties for configuring the\n    rate limit and logging and provides the basic operations of checking the\n    limit and using the limit.\n\n    The limit data is stored as a cluster of keys with a common prefix in the\n    g.ratelimitcache memcached cluster.\n\n    Subclasses should set the the parameter properties somehow. @property and in\n    __init__ work fine if necessary.  This class is designed for ease of static\n    declaration in the subclass definition while still allowing subclasses to\n    use dynamic values when necessary.\n\n    Properties:\n        sample_rate: How frequently to log to g.stats, probabalistically.\n            Defaults to 0.1.\n        event_name: The g.stats event prefix.\n        event_type: The g.stats event suffix.\n        key: The ratelimitcache key prefix.\n        limit: The count of events per self.seconds to allow.\n        seconds: The length of the interval over which to limit rates.\n\n    Example:\n        class MyRateLimit(RateLimit):\n            event_name = 'my_system'\n            event_type = 'thing'\n            key = 'my-rate-limit'\n            # Limit to 10 times every 5 minutes\n            limit = 10\n            seconds = 300\n\n        LIMIT = MyRateLimit()\n\n        if LIMIT.check():\n            do_something()\n            LIMIT.record_usage()\n\n        This code logs (at self.sample_rate)\n            my_system.check_my_thing\n            (if limited) my_system.thing_limit_hit\n            (if not limited) my_system.set_thing_limit\n    \"\"\"\n    sample_rate = 0.1\n\n    def _record_event(self, event_type_template):\n        g.stats.event_count(\n            self.event_name,\n            event_type_template.format(event_type=self.event_type),\n            sample_rate=self.sample_rate)\n\n    @property\n    def timeslice(self):\n        return get_timeslice(self.seconds)\n\n    def _check(self, usage):\n        self._record_event('check_{event_type}')\n        below_limit = usage is None or usage < self.limit\n        if not below_limit:\n            self._record_event('{event_type}_limit_hit')\n        return below_limit\n\n    def check(self):\n        \"\"\"Check that the usage in the current timeslice is below the limit.\"\"\"\n        return self._check(get_usage(self.key, self.timeslice))\n\n    @staticmethod\n    def check_multi(ratelimits):\n        \"\"\"Check that all of the ratelimits can allow more usage.\"\"\"\n        usage = get_usage_multi([(r.key, r.timeslice) for r in ratelimits])\n        return all(r._check(u) for u, r in zip(usage, ratelimits))\n\n    def get_usage(self):\n        \"\"\"Get the usage of this limit. You should probably be using check().\"\"\"\n        return get_usage(self.key, self.timeslice)\n\n    def record_usage(self):\n        \"\"\"Record a new usage within the current timeslice.\"\"\"\n        self._record_event('set_{event_type}_limit')\n        return record_usage(self.key, self.timeslice)\n\n    @staticmethod\n    def record_multi(ratelimits):\n        \"\"\"Record a new usage of all of the ratelimits.\n        See record_usage_multi for everything that could go wrong.\n        \"\"\"\n        for r in ratelimits:\n            r._record_event('set_{event_type}_limit')\n        return record_usage_multi([(r.key, r.timeslice) for r in ratelimits])\n\n\nclass LiveConfigRateLimit(RateLimit):\n    \"\"\"A RateLimit that derives its parameters from values in g.live_config.\n\n    Properties:\n        limit_live_key: The g.live_config key for the number per timespan\n        seconds_live_key: The g.live_config key for the timespan over which the\n            rate is limited, in seconds.\n    \"\"\"\n\n    @property\n    def seconds(self):\n        return g.live_config[self.seconds_live_key]\n\n    @property\n    def limit(self):\n        return g.live_config[self.limit_live_key]\n\n\nclass SimpleRateLimit(RateLimit):\n    \"\"\"Simple ratelimiting class.\n\n    Useful for cases where we just want to be able to call record_usage() and\n    check(). Does not record events to g.stats.\n\n    \"\"\"\n\n    def __init__(self, name, seconds, limit):\n        self.key = name\n        self.seconds = seconds\n        self.limit = limit\n\n    def _record_event(self, event_type_template):\n        # make this a no-op\n        pass\n\n    def record_and_check(self):\n        self.record_usage()\n        return self.check()\n"
  },
  {
    "path": "r2/r2/lib/recommender.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom itertools import chain\nimport math\nimport random\nfrom collections import defaultdict\nfrom datetime import timedelta\nfrom operator import itemgetter\nfrom pycassa.types import LongType\n\nfrom r2.lib import rising\nfrom r2.lib.db import operators, tdb_cassandra\nfrom r2.lib.pages import ExploreItem\nfrom r2.lib.normalized_hot import normalized_hot\nfrom r2.lib.utils import roundrobin, tup, to36\nfrom r2.models import Link, Subreddit\nfrom r2.models.builder import CommentBuilder\nfrom r2.models.listing import NestedListing\nfrom r2.models.recommend import (\n    AccountSRPrefs,\n    AccountSRFeedback,\n)\n\nfrom pylons import app_globals as g\nfrom pylons.i18n import _\n\n# recommendation sources\nSRC_MULTIREDDITS = 'mr'\nSRC_EXPLORE = 'e'  # favors lesser known srs\n\n# explore item types\nTYPE_RISING = _(\"rising\")\nTYPE_DISCOVERY = _(\"discovery\")\nTYPE_HOT = _(\"hot\")\nTYPE_COMMENT = _(\"comment\")\n\n\ndef get_recommendations(srs,\n                        count=10,\n                        source=SRC_MULTIREDDITS,\n                        to_omit=None,\n                        match_set=True,\n                        over18=False):\n    \"\"\"Return subreddits recommended if you like the given subreddits.\n\n    Args:\n    - srs is one Subreddit object or a list of Subreddits\n    - count is total number of results to return\n    - source is a prefix telling which set of recommendations to use\n    - to_omit is a single or list of subreddit id36s that should not be\n        be included. (Useful for omitting recs that were already rejected.)\n    - match_set=True will return recs that are similar to each other, useful\n        for matching the \"theme\" of the original set\n    - over18 content is filtered unless over18=True or one of the original srs\n        is over18\n\n    \"\"\"\n    srs = tup(srs)\n    to_omit = tup(to_omit) if to_omit else []\n\n    # fetch more recs than requested because some might get filtered out\n    rec_id36s = SRRecommendation.for_srs([sr._id36 for sr in srs],\n                                          to_omit,\n                                          count * 2,\n                                          source,\n                                          match_set=match_set)\n\n    # always check for private subreddits at runtime since type might change\n    rec_srs = Subreddit._byID36(rec_id36s, return_dict=False)\n    filtered = [sr for sr in rec_srs if is_visible(sr)]\n\n    # don't recommend adult srs unless one of the originals was over_18\n    if not over18 and not any(sr.over_18 for sr in srs):\n        filtered = [sr for sr in filtered if not sr.over_18]\n\n    return filtered[:count]\n\n\ndef get_recommended_content_for_user(account,\n                                     settings,\n                                     record_views=False,\n                                     src=SRC_EXPLORE):\n    \"\"\"Wrapper around get_recommended_content() that fills in user info.\n\n    If record_views == True, the srs will be noted in the user's preferences\n    to keep from showing them again too soon.\n\n    settings is an ExploreSettings object that controls what types of content\n    will be included.\n\n    Returns a list of ExploreItems.\n\n    \"\"\"\n    prefs = AccountSRPrefs.for_user(account)\n    recs = get_recommended_content(prefs, src, settings)\n    if record_views:\n        # mark as seen so they won't be shown again too soon\n        sr_data = {r.sr: r.src for r in recs}\n        AccountSRFeedback.record_views(account, sr_data)\n    return recs\n\n\ndef get_recommended_content(prefs, src, settings):\n    \"\"\"Get a mix of content from subreddits recommended for someone with\n    the given preferences (likes and dislikes.)\n\n    Returns a list of ExploreItems.\n\n    \"\"\"\n    # numbers chosen empirically to give enough results for explore page\n    num_liked = 10  # how many liked srs to use when generating the recs\n    num_recs = 20  # how many recommended srs to ask for\n    num_discovery = 2  # how many discovery-related subreddits to mix in\n    num_rising = 4  # how many rising links to mix in\n    num_items = 20  # total items to return\n    rising_items = discovery_items = comment_items = hot_items = []\n\n    # make a list of srs that shouldn't be recommended\n    default_srid36s = [to36(srid) for srid in Subreddit.default_subreddits()]\n    omit_srid36s = list(prefs.likes.union(prefs.dislikes,\n                                          prefs.recent_views,\n                                          default_srid36s))\n    # pick random subset of the user's liked srs\n    liked_srid36s = random_sample(prefs.likes, num_liked) if settings.personalized else []\n    # pick random subset of discovery srs\n    candidates = set(get_discovery_srid36s()).difference(prefs.dislikes)\n    discovery_srid36s = random_sample(candidates, num_discovery)\n    # multiget subreddits\n    to_fetch = liked_srid36s + discovery_srid36s\n    srs = Subreddit._byID36(to_fetch)\n    liked_srs = [srs[sr_id36] for sr_id36 in liked_srid36s]\n    discovery_srs = [srs[sr_id36] for sr_id36 in discovery_srid36s]\n    if settings.personalized:\n        # generate recs from srs we know the user likes\n        recommended_srs = get_recommendations(liked_srs,\n                                              count=num_recs,\n                                              to_omit=omit_srid36s,\n                                              source=src,\n                                              match_set=False,\n                                              over18=settings.nsfw)\n        random.shuffle(recommended_srs)\n        # split list of recommended srs in half\n        midpoint = len(recommended_srs) / 2\n        srs_slice1 = recommended_srs[:midpoint]\n        srs_slice2 = recommended_srs[midpoint:]\n        # get hot links plus top comments from one half\n        comment_items = get_comment_items(srs_slice1, src)\n        # just get hot links from the other half\n        hot_items = get_hot_items(srs_slice2, TYPE_HOT, src)\n    if settings.discovery:\n        # get links from subreddits dedicated to discovery\n        discovery_items = get_hot_items(discovery_srs, TYPE_DISCOVERY, 'disc')\n    if settings.rising:\n        # grab some (non-personalized) rising items\n        omit_sr_ids = set(int(id36, 36) for id36 in omit_srid36s)\n        rising_items = get_rising_items(omit_sr_ids, count=num_rising)\n    # combine all items and randomize order to get a mix of types\n    all_recs = list(chain(rising_items,\n                          comment_items,\n                          discovery_items,\n                          hot_items))\n    random.shuffle(all_recs)\n    # make sure subreddits aren't repeated\n    seen_srs = set()\n    recs = []\n    for r in all_recs:\n        if not settings.nsfw and r.is_over18():\n            continue\n        if not is_visible(r.sr):  # could happen in rising items\n            continue\n        if r.sr._id not in seen_srs:\n            recs.append(r)\n            seen_srs.add(r.sr._id)\n        if len(recs) >= num_items:\n            break\n    return recs\n\n\ndef get_hot_items(srs, item_type, src):\n    \"\"\"Get hot links from specified srs.\"\"\"\n    hot_srs = {sr._id: sr for sr in srs}  # for looking up sr by id\n    hot_link_fullnames = normalized_hot([sr._id for sr in srs])\n    hot_links = Link._by_fullname(hot_link_fullnames, return_dict=False)\n    hot_items = []\n    for l in hot_links:\n        hot_items.append(ExploreItem(item_type, src, hot_srs[l.sr_id], l))\n    return hot_items\n\n\ndef get_rising_items(omit_sr_ids, count=4):\n    \"\"\"Get links that are rising right now.\"\"\"\n    all_rising = rising.get_all_rising()\n    candidate_sr_ids = {sr_id for link, score, sr_id in all_rising}.difference(omit_sr_ids)\n    link_fullnames = [link for link, score, sr_id in all_rising if sr_id in candidate_sr_ids]\n    link_fullnames_to_show = random_sample(link_fullnames, count)\n    rising_links = Link._by_fullname(link_fullnames_to_show,\n                                     return_dict=False,\n                                     data=True)\n    rising_items = [ExploreItem(TYPE_RISING, 'ris', Subreddit._byID(l.sr_id), l)\n                   for l in rising_links]\n    return rising_items\n\n\ndef get_comment_items(srs, src, count=4):\n    \"\"\"Get hot links from srs, plus top comment from each link.\"\"\"\n    link_fullnames = normalized_hot([sr._id for sr in srs])\n    hot_links = Link._by_fullname(link_fullnames[:count], return_dict=False)\n    top_comments = []\n    for link in hot_links:\n        builder = CommentBuilder(link,\n                                 operators.desc('_confidence'),\n                                 comment=None,\n                                 context=None,\n                                 num=1,\n                                 load_more=False)\n        listing = NestedListing(builder, parent_name=link._fullname).listing()\n        top_comments.extend(listing.things)\n    srs = Subreddit._byID([com.sr_id for com in top_comments])\n    links = Link._byID([com.link_id for com in top_comments])\n    comment_items = [ExploreItem(TYPE_COMMENT,\n                                 src,\n                                 srs[com.sr_id],\n                                 links[com.link_id],\n                                 com) for com in top_comments]\n    return comment_items\n\n\ndef get_discovery_srid36s():\n    \"\"\"Get list of srs that help people discover other srs.\"\"\"\n    srs = Subreddit._by_name(g.live_config['discovery_srs'])\n    return [sr._id36 for sr in srs.itervalues()]\n\n\ndef random_sample(items, count):\n    \"\"\"Safe random sample that won't choke if len(items) < count.\"\"\"\n    sample_size = min(count, len(items))\n    return random.sample(items, sample_size)\n\n\ndef is_visible(sr):\n    \"\"\"True if sr is visible to regular users, false if private or banned.\"\"\"\n    return (\n        sr.type not in Subreddit.private_types and\n        not sr._spam and\n        sr.discoverable\n    )\n\n\nclass SRRecommendation(tdb_cassandra.View):\n    _use_db = True\n\n    _compare_with = LongType()\n\n    # don't keep these around if a run hasn't happened lately, or if the last\n    # N runs didn't generate recommendations for a given subreddit\n    _ttl = timedelta(days=7, hours=12)\n\n    # we know that we mess with these but it's okay\n    _warn_on_partial_ttl = False\n\n    @classmethod\n    def for_srs(cls, srid36, to_omit, count, source, match_set=True):\n        # It's usually better to use get_recommendations() than to call this\n        # function directly because it does privacy filtering.\n\n        srid36s = tup(srid36)\n        to_omit = set(to_omit)\n        to_omit.update(srid36s)  # don't show the originals\n        rowkeys = ['%s.%s' % (source, srid36) for srid36 in srid36s]\n\n        # fetch multiple sets of recommendations, one for each input srid36\n        rows = cls._byID(rowkeys, return_dict=False)\n\n        if match_set:\n            sorted_recs = cls._merge_and_sort_by_count(rows)\n            # heuristic: if input set is large, rec should match more than one\n            min_count = math.floor(.1 * len(srid36s))\n            sorted_recs = (rec[0] for rec in sorted_recs if rec[1] > min_count)\n        else:\n            sorted_recs = cls._merge_roundrobin(rows)\n        # remove duplicates and ids listed in to_omit\n        filtered = []\n        for r in sorted_recs:\n            if r not in to_omit:\n                filtered.append(r)\n                to_omit.add(r)\n        return filtered[:count]\n\n    @classmethod\n    def _merge_roundrobin(cls, rows):\n        \"\"\"Combine multiple sets of recs, preserving order.\n\n        Picks items equally from each input sr, which can be useful for\n        getting a diverse set of recommendations instead of one that matches\n        a theme. Preserves ordering, so all rank 1 recs will be listed first,\n        then all rank 2, etc.\n\n        Returns a list of id36s.\n\n        \"\"\"\n        return roundrobin(*[row._values().itervalues() for row in rows])\n\n    @classmethod\n    def _merge_and_sort_by_count(cls, rows):\n        \"\"\"Combine and sort multiple sets of recs.\n\n        Combines multiple sets of recs and sorts by number of times each rec\n        appears, the reasoning being that an item recommended for several of\n        the original srs is more likely to match the \"theme\" of the set.\n\n        \"\"\"\n        # combine recs from all input srs\n        rank_id36_pairs = chain.from_iterable(row._values().iteritems()\n                                              for row in rows)\n        ranks = defaultdict(list)\n        for rank, id36 in rank_id36_pairs:\n            ranks[id36].append(rank)\n        recs = [(id36, len(ranks), max(ranks))\n                for id36, ranks in ranks.iteritems()]\n        # first, sort ascending by rank\n        recs = sorted(recs, key=itemgetter(2))\n        # next, sort descending by number of times the rec appeared. since\n        # python sort is stable, tied items will still be ordered by rank\n        return sorted(recs, key=itemgetter(1), reverse=True)\n"
  },
  {
    "path": "r2/r2/lib/require.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nclass RequirementException(Exception):\n    pass\n\ndef require(val):\n    \"\"\"A safe version of assert\n\n    Assert can be stripped out if python is run in an optimized\n    mode. This function implements assertions in a way that is\n    guaranteed to execute.\n    \"\"\"\n    if not val:\n        raise RequirementException\n    return val\n\ndef require_split(s, length, sep=None):\n    require(s)\n    res = s.split(sep)\n    require(len(res) == length)\n    return res\n"
  },
  {
    "path": "r2/r2/lib/rising.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom datetime import datetime\nimport heapq\n\nfrom pylons import app_globals as g\n\nfrom r2.lib import count\nfrom r2.lib.sgm import sgm\nfrom r2.models.link import Link\n\n\ndef calc_rising():\n    link_counts = count.get_link_counts()\n\n    links = Link._by_fullname(link_counts.keys(), data=True)\n\n    def score(link):\n        count = link_counts[link._fullname][0]\n        return float(link._ups) / max(count, 1)\n\n    # build the rising list, excluding items having 1 or less upvotes\n    rising = []\n    for link in links.values():\n        if link._ups > 1:\n            rising.append((link._fullname, score(link), link.sr_id))\n\n    # return rising sorted by score\n    return sorted(rising, key=lambda x: x[1], reverse=True)\n\n\ndef set_rising():\n    g.gencache.set(\"all:rising\", calc_rising())\n\n\ndef get_all_rising():\n    return g.gencache.get(\"all:rising\", [], stale=True)\n\n\ndef get_rising(sr):\n    rising = get_all_rising()\n    return [link for link, score, sr_id in rising if sr.keep_for_rising(sr_id)]\n\n\ndef get_rising_tuples(sr_ids):\n    rising = get_all_rising()\n\n    tuples_by_srid = {sr_id: [] for sr_id in sr_ids}\n    top_rising = {}\n\n    for link, score, sr_id in rising:\n        if sr_id not in sr_ids:\n            continue\n\n        if sr_id not in top_rising:\n            top_rising[sr_id] = score\n\n        norm_score = score / top_rising[sr_id]\n        tuples_by_srid[sr_id].append((-norm_score, -score, link))\n\n    return tuples_by_srid\n\n\ndef normalized_rising(sr_ids):\n    if not sr_ids:\n        return []\n\n    tuples_by_srid = sgm(\n        cache=g.gencache,\n        keys=sr_ids,\n        miss_fn=get_rising_tuples,\n        prefix='rising:',\n        time=90,\n    )\n\n    merged = heapq.merge(*tuples_by_srid.values())\n\n    return [link_name for norm_score, score, link_name in merged]\n"
  },
  {
    "path": "r2/r2/lib/s3_helpers.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport base64\nimport boto\nimport hashlib\nimport hmac\nimport json\nimport os\nimport sys\nimport time\nimport datetime\nimport pytz\nfrom collections import namedtuple\n\nfrom pylons import app_globals as g\n\n\nHADOOP_FOLDER_SUFFIX = '_$folder$'\n\nSIGNATURE_V4_ALGORITHM = \"AWS4-HMAC-SHA256\"\n\n\ndef _to_path(bucket, key):\n    if not bucket:\n        raise ValueError\n    return 's3://%s/%s' % (bucket, key)\n\n\ndef _from_path(path):\n    \"\"\"Return bucket and key names from an s3 path.\n\n    Path of 's3://BUCKET/KEY/NAME' would return 'BUCKET', 'KEY/NAME'.\n\n    \"\"\"\n\n    if not path.startswith('s3://'):\n        raise ValueError('Bad S3 path %s' % path)\n\n    r = path[len('s3://'):].split('/', 1)\n    bucket = key = None\n\n    if len(r) == 2:\n        bucket, key = r[0], r[1]\n    else:\n        bucket = r[0]\n\n    if not bucket:\n        raise ValueError('Bad S3 path %s' % path)\n\n    return bucket, key\n\n\nS3Path = namedtuple('S3Path', ['bucket', 'key'])\n\n\ndef parse_s3_path(path):\n    return S3Path(*_from_path(path))\n\n\ndef format_expires(expires):\n    return expires.strftime(EXPIRES_DATE_FORMAT)\n\n\ndef get_text_from_s3(s3_connection, path):\n    \"\"\"Read a file from S3 and return it as text.\"\"\"\n    bucket_name, key_name = _from_path(path)\n    bucket = s3_connection.get_bucket(bucket_name)\n    k = boto.s3.Key(bucket)\n    k.key = key_name\n    txt = k.get_contents_as_string()\n    return txt\n\n\ndef mv_file_s3(s3_connection, src_path, dst_path):\n    \"\"\"Move a file within S3.\"\"\"\n    src_bucket_name, src_key_name = _from_path(src_path)\n    dst_bucket_name, dst_key_name = _from_path(dst_path)\n\n    src_bucket = s3_connection.get_bucket(src_bucket_name)\n    k = boto.s3.Key(src_bucket)\n    k.key = src_key_name\n    k.copy(dst_bucket_name, dst_key_name)\n    k.delete()\n\n\ndef s3_key_exists(s3_connection, path):\n    bucket_name, key_name = _from_path(path)\n    bucket = s3_connection.get_bucket(bucket_name)\n    key = bucket.get_key(key_name)\n    return bool(key)\n\n\ndef copy_to_s3(s3_connection, local_path, dst_path, verbose=False):\n    def callback(trans, total):\n        sys.stdout.write('%s/%s' % trans, total)\n        sys.stdout.flush()\n\n    dst_bucket_name, dst_key_name = _from_path(dst_path)\n    bucket = s3_connection.get_bucket(dst_bucket_name)\n\n    filename = os.path.basename(local_path)\n    if not filename:\n        return\n\n    key_name = os.path.join(dst_key_name, filename)\n    k = boto.s3.Key(bucket)\n    k.key = key_name\n\n    kw = {}\n    if verbose:\n        print 'Uploading %s to %s' % (local_path, dst_path)\n        kw['cb'] = callback\n\n    k.set_contents_from_filename(logfile, **kw)\n\n\ndef get_connection():\n    return boto.connect_s3(g.S3KEY_ID or None, g.S3SECRET_KEY or None)\n\n\ndef get_key(bucket_name, key, connection=None):\n    connection = connection or get_connection()\n    bucket = connection.get_bucket(bucket_name)\n\n    return bucket.get_key(key)\n\ndef get_keys(bucket_name, meta=False, connection=None, **kwargs):\n    connection = connection or get_connection()\n    bucket = connection.get_bucket(bucket_name)\n    keys = bucket.get_all_keys(**kwargs)\n\n    if not meta:\n        return keys\n\n    return [bucket.get_key(key.name)\n            for key in keys]\n\n\ndef delete_keys(bucket_name, prefix, connection=None):\n    connection = connection or get_connection()\n\n    keys = get_keys(bucket_name, prefix=prefix, connection=connection)\n    return connection.get_bucket(bucket_name).delete_keys(keys)\n\n\ndef _get_v4_credential(aws_access_key_id, date, service_name, region_name):\n    return (\"%(aws_access_key_id)s/%(datestamp)s/%(region_name)s/%(service_name)s/aws4_request\" % {\n        \"aws_access_key_id\": aws_access_key_id,\n        \"datestamp\": date.strftime(\"%Y%m%d\"),\n        \"region_name\": region_name,\n        \"service_name\": service_name,\n    })\n\ndef _get_upload_policy(\n        bucket, key, credential, date, acl,\n        ttl=60,\n        success_action_redirect=None,\n        success_action_status=\"201\",\n        content_type=None,\n        max_content_length=((1024**2) * 3),\n        storage_class=\"STANDARD\",\n        region_name=None,\n        meta=None,\n        connection=None,\n    ):\n\n    connection = connection or get_connection()\n    meta = meta or {}\n\n    expiration = time.gmtime(int(time.time() + ttl))\n    conditions = []\n\n    conditions.append({\"bucket\": bucket})\n\n    if key.endswith(\"${filename}\"):\n        conditions.append([\"starts-with\", \"$key\", key[:-len(\"${filename}\")]])\n    else:\n        conditions.append({\"key\": key})\n\n    conditions.append({\"acl\": acl})\n    conditions.append({\"x-amz-storage-class\": storage_class})\n\n    conditions.append({\"x-amz-credential\": credential})\n    conditions.append({\"x-amz-algorithm\": SIGNATURE_V4_ALGORITHM})\n    conditions.append({\"x-amz-date\": date.strftime(\"%Y%m%dT%H%M%SZ\")})\n    conditions.append({\"x-amz-security-token\": connection.provider.security_token})\n\n    if success_action_redirect:\n        conditions.append([\n            \"starts-with\",\n            \"$success_action_redirect\",\n            success_action_redirect,\n        ])\n    else:\n        conditions.append({\n            \"success_action_status\": success_action_status,\n        })\n\n    conditions.append([\n        \"content-length-range\", 0, max_content_length])\n\n    for key, value in meta.iteritems():\n        conditions.append({key: value})\n\n    if content_type:\n        conditions.append({\"content-type\": content_type})\n\n    return base64.b64encode(json.dumps({\n        \"expiration\": time.strftime(boto.utils.ISO8601, expiration),\n        \"conditions\": conditions,\n    }))\n\n\ndef _sign(secret, msg):\n    return hmac.new(secret, msg.encode(\"utf-8\"), hashlib.sha256).digest()\n\n\ndef _derive_v4_signature_key(secret, date, region_name, service_name):\n    key_date = _sign((\"AWS4\" + secret).encode(\"utf-8\"), date.strftime(\"%Y%m%d\"))\n    key_region = _sign(key_date, region_name)\n    key_service = _sign(key_region, service_name)\n    return _sign(key_service, \"aws4_request\")\n\n\ndef _get_upload_signature(\n        policy,\n        date,\n        region_name,\n        connection=None,\n    ):\n\n    connection = connection or get_connection()\n\n    key = connection.provider.secret_key.encode(\"utf-8\")\n    v4_key = _derive_v4_signature_key(\n        secret=key, date=date, region_name=region_name, service_name=\"s3\")\n\n    return hmac.new(v4_key, policy, hashlib.sha256).hexdigest()\n\n\ndef get_post_args(\n        bucket, key,\n        acl=\"public-read\",\n        success_action_redirect=None,\n        success_action_status=\"201\",\n        content_type=None,\n        storage_class=\"STANDARD\",\n        region_name=\"us-east-1\",\n        meta=None,\n        connection=None,\n        **kwargs\n    ):\n\n    meta = meta or []\n    connection = connection or get_connection()\n    algorithm = \"AWS4-HMAC-SHA256\"\n    date = datetime.datetime.now(pytz.utc)\n    credential = _get_v4_credential(\n        aws_access_key_id=connection.provider.access_key,\n        date=date,\n        service_name=\"s3\",\n        region_name=region_name,\n    )\n    policy = _get_upload_policy(\n        bucket=bucket,\n        key=key,\n        credential=credential,\n        date=date,\n        acl=acl,\n        success_action_redirect=success_action_redirect,\n        success_action_status=success_action_status,\n        content_type=content_type,\n        storage_class=storage_class,\n        region_name=region_name,\n        meta=meta,\n        connection=connection,\n    )\n    signature = _get_upload_signature(\n        policy=policy,\n        date=date,\n        region_name=region_name,\n        connection=connection,\n    )\n\n    fields = []\n\n    fields.append({\n        \"name\": \"acl\",\n        \"value\": acl,\n    })\n\n    fields.append({\n        \"name\": \"key\",\n        \"value\": key,\n    })\n\n    fields.append({\n        \"name\": \"X-Amz-Credential\",\n        \"value\": credential,\n    })\n\n    fields.append({\n        \"name\": \"X-Amz-Algorithm\",\n        \"value\": SIGNATURE_V4_ALGORITHM,\n    })\n\n    fields.append({\n        \"name\": \"X-Amz-Date\",\n        \"value\": date.strftime(\"%Y%m%dT%H%M%SZ\"),\n    })\n\n    if success_action_redirect:\n        fields.append({\n            \"name\": \"success_action_redirect\",\n            \"value\": success_action_redirect,\n        })\n    else:\n        fields.append({\n            \"name\": \"success_action_status\",\n            \"value\": success_action_status,\n        })\n\n    fields.append({\n        \"name\": \"content-type\",\n        \"value\": content_type,\n    })\n\n    fields.append({\n        \"name\": \"x-amz-storage-class\",\n        \"value\": storage_class,\n    })\n\n    for key, value in meta.iteritems():\n        fields.append({\n            \"name\": key,\n            \"value\": value,\n        })\n\n    fields.append({\n        \"name\": \"policy\",\n        \"value\": policy,\n    })\n\n    fields.append({\n        \"name\": \"X-Amz-Signature\",\n        \"value\": signature,\n    })\n\n    fields.append({\n        \"name\": \"x-amz-security-token\",\n        \"value\": connection.provider.security_token,\n    })\n\n    return {\n        \"action\": \"//%s.%s\" % (bucket, g.s3_media_domain),\n        \"fields\": fields,\n    }\n"
  },
  {
    "path": "r2/r2/lib/sgm.pyx",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom r2.lib.cache import MemcachedError\n\n\n# smart get multi:\n# For any keys not found in the cache, miss_fn() is run and the result is\n# stored in the cache. Then it returns everything, both the hits and misses.\ndef sgm(cache, keys, miss_fn, str prefix='', int time=0, stale=False,\n        found_fn=None, _update=False, stat_subname=None,\n        ignore_set_errors=False):\n    cdef dict ret\n    cdef dict s_keys\n    cdef dict cached\n    cdef dict calculated\n    cdef dict calculated_to_cache\n    cdef set  still_need\n\n    ret = {}\n\n    # map the string versions of the keys to the real version. we only\n    # need this to interprate the cache's response and turn it back\n    # into the version they asked for\n    s_keys = {}\n    for key in keys:\n        s_keys[str(key)] = key\n\n    if _update:\n        cached = {}\n    else:\n        kw = {}\n        if stale:\n            kw['stale'] = stale\n        if stat_subname:\n            kw['stat_subname'] = stat_subname\n\n        cached = cache.get_multi(s_keys.keys(), prefix=prefix, **kw)\n\n        for k, v in cached.iteritems():\n            ret[s_keys[k]] = v\n\n    still_need = set(s_keys.values()) - set(ret.keys())\n\n    if found_fn is not None:\n        # give the caller an opportunity to reject some of the cache\n        # hits if they aren't good enough. it's expected to use the\n        # mutability of the cached dict and still_need set to modify\n        # them as appropriate\n        found_fn(ret, still_need)\n\n    if miss_fn and still_need:\n        # if we didn't get all of the keys from the cache, go to the\n        # miss_fn with the keys they asked for minus the ones that we\n        # found\n        calculated = miss_fn(still_need)\n        ret.update(calculated)\n\n        calculated_to_cache = {}\n        for k, v in calculated.iteritems():\n            calculated_to_cache[str(k)] = v\n\n        try:\n            cache.set_multi(calculated_to_cache, prefix=prefix, time=time)\n        except MemcachedError:\n            if not ignore_set_errors:\n                raise\n\n    return ret\n"
  },
  {
    "path": "r2/r2/lib/signing.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\"\"\"Module for request signing.\n\n\"\"\"\nimport hmac\nimport hashlib\nimport re\nimport pytz\n\nfrom datetime import datetime\nfrom collections import namedtuple\nfrom pylons import app_globals as g\n\nfrom r2.lib.utils import Storage, epoch_timestamp, constant_time_compare, tup\n\nGLOBAL_TOKEN_VERSION = 1\nSIGNATURE_UA_HEADER = \"X-hmac-signed-result\"\nSIGNATURE_BODY_HEADER = \"X-hmac-signed-body\"\n\nSIG_HEADER_RE = re.compile(r\"^(?P<global_version>\\d+?):(?P<payload>.*)$\")\nSIG_CONTENT_V1_RE = re.compile(\n    r\"^(?P<platform>.+?):(?P<version>\\d+?):(?P<epoch>\\d+?):(?P<mac>.*)$\"\n)\n\n\nERRORS = Storage()\nSignatureError = namedtuple(\"SignatureError\", \"code msg\")\nfor code, msg in (\n    (\"UNKNOWN\", \"default signature failure mode (shouldn't happen!)\"),\n    (\"INVALID_FORMAT\", \"no signature header or completely unparsable\"),\n    (\"UNKOWN_GLOBAL_VERSION\", \"token global version is from the future\"),\n    (\"UNPARSEABLE\", \"couldn't parse signature for this global version\"),\n    (\"INVALIDATED_TOKEN\", \"platform/version combination is invalid.\"),\n    (\"EXPIRED_TOKEN\", \"epoch provided is too old.\"),\n    (\"SIGNATURE_MISMATCH\", \"the payload's signature doesn't match the header\"),\n    (\"MULTISIG_MISMATCH\", \"more than one version on multiple signatures!\")\n):\n    code = code.upper()\n    ERRORS[code] = SignatureError(code, msg)\n\n\nclass SigningResult(object):\n    \"\"\"\n    \"\"\"\n    __slots__ = [\"global_version\", \"platform\", \"version\",\n                 \"mac\", \"valid_hmac\", \"epoch\", \"ignored_errors\", \"errors\"]\n\n    def __init__(\n        self,\n        global_version=-1,\n        platform=None,\n        version=-1,\n        mac=None,\n        valid_hmac=False,\n        epoch=None,\n    ):\n        self.global_version = global_version\n        self.platform = platform\n        self.version = version\n        self.mac = mac\n        self.valid_hmac = valid_hmac\n        self.epoch = epoch\n        self.ignored_errors = []\n        self.errors = {}\n\n    def __repr__(self):\n        return \"<%s (%s)>\" % (\n            self.__class__.__name__,\n            \", \".join(\"%s=%r\" % (k, getattr(self, k)) for k in self.__slots__)\n        )\n\n    def add_error(self, error, field=None, details=None):\n        \"\"\"Add an error.\n\n        Duplicate errors (those with the same code and field) will have the\n        last `details` stored.\n\n        :param error: The error to be set.\n        :param field: where the error came from (generally \"body\" or \"ua\")\n        :param details: additional error info (for the event)\n\n        :type error: :py:class:`SignatureError`\n        :type field: str or None\n        :type details: object\n        \"\"\"\n        self.errors[(error.code, field)] = details\n\n    def add_ignore(self, ignored_error):\n        \"\"\"Add error to list of ignored errors.\n\n        :param ignored_error: error to be ignored.\n        :type ignored_error: :py:class:`SignatureError`\n        \"\"\"\n        self.ignored_errors.append(ignored_error)\n\n    def has_errors(self):\n        \"\"\"Determines if the signature has any errors.\n\n        :returns: whether or not there are non-ignored errors\n        :rtype: bool\n        \"\"\"\n        if self.ignored_errors:\n            igcodes = {err.code for err in tup(self.ignored_errors)}\n            error_codes = {code for code, _ in self.errors}\n            return not error_codes.issubset(igcodes)\n        else:\n            return bool(self.errors)\n\n    def is_valid(self):\n        \"\"\"Returns if the hmac is valid and the signature has no errors.\n\n        :returns: whether or not this is valid\n        :rtype: bool\n        \"\"\"\n        return self.valid_hmac and not self.has_errors()\n\n    def update(self, other):\n        \"\"\"Destructively merge this result with another.\n\n        the signatures are combined as needed to generate a final signature\n        that is generally the combination of the two as follows:\n\n         - `errors` are combined\n         - `global_version`, `platform`, and `version` are compared. In the\n            case of a mismatch, \"MULTISIG_MISMATCH\" error is set.\n         - signature validity is independently checked with :py:meth:`is_valid`\n\n        :param other: other result to be merged from\n        :type other: :py:class:`SigningResult`\n\n        \"\"\"\n        assert isinstance(other, SigningResult)\n\n        # copy errors onto self\n        self.errors.update(other.errors)\n\n        # verify both signatures share versioning info (if they don't,\n        # something is _very weird_)\n        for attr in (\"global_version\", \"platform\", \"version\"):\n            if getattr(self, attr) != getattr(other, attr):\n                self.add_error(ERRORS.MULTISIG_MISMATCH, details=attr)\n                break\n\n        # also the signature is valid only if both hmacs are valid\n        self.valid_hmac = self.valid_hmac and other.valid_hmac\n\n\ndef current_epoch():\n    return int(epoch_timestamp(datetime.now(pytz.UTC)))\n\n\ndef valid_epoch(platform, epoch, max_age=5 * 60):\n    now = current_epoch()\n    dt = abs(now - epoch)\n    g.stats.simple_timing(\"signing.%s.skew\" % platform, dt * 1000)\n    return dt < max_age\n\n\ndef epoch_wrap(epoch, payload):\n    return \"Epoch:{}|{}\".format(epoch, payload)\n\n\ndef versioned_hmac(secret, body, global_version=GLOBAL_TOKEN_VERSION):\n    \"\"\"Provide an hex hmac for the provided global_version.\n\n    This provides future compatibility if we want to bump the token version\n    and change our hashing algorithm.\n    \"\"\"\n    # If we want to change the hashing algo or anything else about this hmac,\n    # this is the place to make that change based on global_version.\n    assert global_version <= GLOBAL_TOKEN_VERSION, (\n        \"Invalid version signing version '%s'!\" % global_version\n    )\n    return hmac.new(secret, body, hashlib.sha256).hexdigest()\n\n\ndef get_secret_token(platform, version, global_version=GLOBAL_TOKEN_VERSION):\n    \"\"\"For a given platform and version, provide the signing token.\n\n    The signing token for a given platform (\"ios\", \"android\", etc.) and version\n    is derived by hashing the platform and versions with a server secret. This\n    ensures that we can issue new tokens to new clients as need be without\n    needing to keep them in a database.  It also means we can invalidate\n    old versions of tokens in `is_invalid_token` and trust that the client\n    isn't lying to us.\n    \"\"\"\n    token_identifier = \"{global_version}:{platform}:{version}\".format(\n        global_version=global_version,\n        platform=platform,\n        version=version,\n    )\n    # NOTE: this is the only place in this file where we reference g.secrets.\n    # If we wanted to rotate the global secret, this is the place to do it.\n    global_secret = g.secrets[\"request_signature_secret\"]\n    return versioned_hmac(global_secret, token_identifier, global_version)\n\n\ndef is_invalid_token(platform, version):\n    \"\"\"Conditionally reject a token based on platform and version.\"\"\"\n    return False\n\n\ndef valid_post_signature(request, signature_header=SIGNATURE_BODY_HEADER):\n    \"Validate that the request has a properly signed body.\"\n    return valid_signature(\n        \"Body:{}\".format(request.body),\n        request.headers.get(signature_header),\n        field=\"body\",\n    )\n\n\ndef valid_ua_signature(\n    request,\n    signed_headers=(\"User-Agent\", \"Client-Vendor-ID\"),\n    signature_header=SIGNATURE_UA_HEADER,\n):\n    \"Validate that the request has a properly signed user-agent.\"\n    payload = \"|\".join(\n        \"{}:{}\".format(h, request.headers.get(h) or \"\")\n        for h in signed_headers\n    )\n    return valid_signature(\n        payload,\n        request.headers.get(signature_header),\n        field=\"ua\",\n    )\n\n\ndef valid_signature(payload, signature, field=None):\n    \"\"\"Checks if `signature` matches `payload`.\n\n    `Signature` (at least as of version 1) be of the form:\n\n       {global_version}:{platform}:{version}:{signature}\n\n    where:\n\n      * global_version (currently hard-coded to be \"1\") can be used to change\n            this header's underlying schema later if needs be.  As such, can\n            be treated as a protocol version.\n      * platform is the client platform type (generally \"ios\" or \"android\")\n      * version is the client's token version (can be updated and incremented\n            per app build as needs be.\n      * signature is the hmac of the request's POST body with the token derived\n            from the above three parameters via `get_secret_token`\n\n    :param str payload: the signed data\n    :param str signature: the signature of the payload\n    :param str field: error field to set (one of \"ua\", \"body\")\n    :returns: object with signature validity and any errors\n    :rtype: :py:class:`SigningResult`\n    \"\"\"\n    result = SigningResult()\n\n    # if the signature is unparseable, there's not much to do\n    sig_match = SIG_HEADER_RE.match(signature or \"\")\n    if not sig_match:\n        result.add_error(ERRORS.INVALID_FORMAT, field=field)\n        return result\n\n    sig_header_dict = sig_match.groupdict()\n    # we're matching \\d so this shouldn't throw a TypeError\n    result.global_version = int(sig_header_dict['global_version'])\n\n    # incrementing this value is drastic.  We can't validate a token protocol\n    # we don't understand.\n    if result.global_version > GLOBAL_TOKEN_VERSION:\n        result.add_error(ERRORS.UNKOWN_GLOBAL_VERSION, field=field)\n        return result\n\n    # currently there's only one version, but here's where we'll eventually\n    # patch in more.\n    sig_match = SIG_CONTENT_V1_RE.match(sig_header_dict['payload'])\n    if not sig_match:\n        result.add_error(ERRORS.UNPARSEABLE, field=field)\n        return result\n\n    # slop the matched data over to the SigningResult\n    sig_match_dict = sig_match.groupdict()\n    result.platform = sig_match_dict['platform']\n    result.version = int(sig_match_dict['version'])\n    result.epoch = int(sig_match_dict['epoch'])\n    result.mac = sig_match_dict['mac']\n\n    # verify that the token provided hasn't been invalidated\n    if is_invalid_token(result.platform, result.version):\n        result.add_error(ERRORS.INVALIDATED_TOKEN, field=field)\n        return result\n\n    # check the epoch validity, but don't fail -- leave that up to the\n    # validator!\n    if not valid_epoch(result.platform, result.epoch):\n        result.add_error(\n            ERRORS.EXPIRED_TOKEN,\n            field=field,\n            details=result.epoch,\n        )\n\n    # get the expected secret used to verify this request.\n    secret_token = get_secret_token(\n        result.platform,\n        result.version,\n        global_version=result.global_version,\n    )\n\n    result.valid_hmac = constant_time_compare(\n        result.mac,\n        versioned_hmac(\n            secret_token,\n            epoch_wrap(result.epoch, payload),\n            result.global_version\n        ),\n    )\n\n    if not result.valid_hmac:\n        result.add_error(ERRORS.SIGNATURE_MISMATCH, field=field)\n\n    return result\n\n\ndef sign_v1_message(body, platform, version, epoch=None):\n    \"\"\"Reference implementation of the v1 mobile body signing.\"\"\"\n    token = get_secret_token(platform, version, global_version=1)\n    epoch = int(epoch or current_epoch())\n    payload = epoch_wrap(epoch, body)\n    signature = versioned_hmac(token, payload, global_version=1)\n    return \"{global_version}:{platform}:{version}:{epoch}:{signature}\".format(\n        global_version=1,\n        platform=platform,\n        version=version,\n        epoch=epoch,\n        signature=signature,\n    )\n"
  },
  {
    "path": "r2/r2/lib/sitemaps/__init__.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\n\"\"\"This module is used for the generation of sitemaps.\n\nSitemaps (http://www.sitemaps.org/protocol.html) are an xml\nprotocol designed to exhaustively describe websites. In lieu of\nan external link or a link from a reddit, in general most reddit\nlinks do not get indexed by search engines. This means that the vast\nmajority of Reddit content does not get indexed by Google, Bing, or Yahoo.\nSitemaps solve the problem by showing search engines links that they\nlikely don't have access to any other way.\n\nReddit contains tons and tons of links. Generating them on the fly is simply\nimpractical. The solution this module implements is a very slow batch that\ngoes through every subreddit and every link and creates crawlable permalinks\nfrom them. These links are then put into sitemaps and stored in\ns3. We then upload those sitemaps as static files to s3 where we host them.\n\nThe Sitemap protocol specifies a hard limit of 50000 links. Since we have\nsignificantly more links than that, we have to define a Sitemap Index\n(http://www.sitemaps.org/protocol.html#index.) The sitemap similarly has\nup to 50000 links to other sitemaps. For now it suits our purposes to have\nexactly one sitemap, but it may change in the future.\n\nThere are only two types of links we currently support. Subreddit links\nin the form of\n\nhttps://www.reddit.com/r/hiphopheads\n\nand comment links in the form of\n\nhttps://www.reddit.com/r/hiphopheads/comments/4gxk5i/fresh_album_drake_views/.\n\n\nThis module is split into 3 parts.\n\n  r2.lib.sitemaps.data - Loads up the raw Subreddit and Link Things.\n  r2.lib.sitemaps.generate - Transforms the Things into sitemap xml strings.\n  r2.lib.sitemaps.store - Stores the sitemaps on s3.\n  r2.lib.sitemaps.watcher - Reads from the SQS queue and starts a new upload\n\n\nThe only function that's supposed to be used outside of this module is\nr2.lib.sitemaps.watcher.watcher. This is designed to be used as a constantly\nrunning daemon.\n\"\"\"\n"
  },
  {
    "path": "r2/r2/lib/sitemaps/data.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\n\"\"\"Generates all the data used in making sitemaps and sitemap links.\n\nCurrently only supports subreddit links but will soon support comment links.\n\"\"\"\n\nimport tempfile\n\nfrom boto.s3.connection import S3Connection\nfrom pylons import app_globals as g\n\nfrom r2.lib.hadoop_decompress import hadoop_decompress\n\n\ndef _read_subreddit_etl_from_s3(s3path):\n    s3conn = S3Connection()\n    bucket = s3conn.get_bucket(s3path.bucket, validate=False)\n    s3keys = bucket.list(s3path.key)\n\n    key_count = 0\n    for s3key in s3keys:\n        g.log.info(\"Importing key %r\", s3key)\n\n        with tempfile.TemporaryFile(mode='rw+b') as ntf_download:\n            with tempfile.TemporaryFile(mode='rw+b') as ntf_decompress:\n\n                # download it\n                g.log.debug(\"Downloading %r\", s3key)\n                s3key.get_contents_to_file(ntf_download)\n\n                # decompress it\n                ntf_download.flush()\n                ntf_download.seek(0)\n                g.log.debug(\"Decompressing %r\", s3key)\n                hadoop_decompress(ntf_download, ntf_decompress)\n                ntf_decompress.flush()\n                ntf_decompress.seek(0)\n\n                # import it\n                g.log.debug(\"Starting import of %r\", s3key)\n                for line in ntf_decompress:\n                    yield line\n        key_count += 1\n\n    if key_count == 0:\n        raise ValueError('{0} contains no readable keys.'.format(s3path))\n\n\ndef find_all_subreddits(s3path):\n    for line in _read_subreddit_etl_from_s3(s3path):\n        _, subreddit, __ = line.split('\\x01')\n        yield subreddit\n"
  },
  {
    "path": "r2/r2/lib/sitemaps/generate.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\n\n\"\"\"Create exhaustive sitemaps for Reddit.\n\nThis module exists to make fairly exhaustive sitemaps as defined by the\nsitemap protocol (http://www.sitemaps.org/protocol.html)\n\nWe currently support two types of sitemaps:\n\nThe sitemap index which takes the form of:\n\n------------------------------------------------------------------------\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<sitemapindex xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n  <sitemap>\n    <loc>http://reddit.com/r/subreddit_sitemap?index=0</loc>\n  </sitemap>\n  <sitemap>\n    <loc>http://reddit.com/r/subreddit_sitemap?index=1</loc>\n  </sitemap>\n  <sitemap>\n    <loc>http://reddit.com/r/permalink_sitemap?index=0</loc>\n  </sitemap>\n  <sitemap>\n    <loc>http://reddit.com/r/permalink_sitemap?index=1</loc>\n  </sitemap>\n</sitemapindex>\n------------------------------------------------------------------------\n\nNext are subreddit sitemaps which take the form of:\n\n------------------------------------------------------------------------\n <?xml version=\"1.0\" encoding=\"UTF-8\"?>\n <urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n   <url>\n     <loc>http://reddit.com/r/{some_postfix}</loc>\n   </url>\n </urlset>\n------------------------------------------------------------------------\n\n\nEach sitemap and sitemap index will have 50000 links or fewer.\n\"\"\"\n\nfrom lxml import etree\nfrom pylons import app_globals as g\n\nfrom r2.lib.template_helpers import add_sr\nfrom r2.lib.utils import in_chunks\n\nSITEMAP_NAMESPACE = \"http://www.sitemaps.org/schemas/sitemap/0.9\"\nLINKS_PER_SITEMAP = 50000\n\n\ndef _absolute_url(path):\n    return add_sr(path, force_https=True, sr_path=False)\n\n\ndef _stringify_xml(root_element):\n    return etree.tostring(\n        root_element,\n        pretty_print=g.debug,\n        xml_declaration=True,\n        encoding='UTF-8'\n    )\n\n\ndef _subreddit_links(subreddits):\n    for subreddit in subreddits:\n        path = '/r/{0}/'.format(subreddit)\n        yield _absolute_url(path)\n\n\ndef _subreddit_sitemap(subreddits):\n    urlset = etree.Element('urlset', xmlns=SITEMAP_NAMESPACE)\n    for link in _subreddit_links(subreddits):\n        url_elem = etree.SubElement(urlset, 'url')\n        loc_elem = etree.SubElement(url_elem, 'loc')\n        loc_elem.text = link\n    return _stringify_xml(urlset)\n\n\ndef subreddit_sitemaps(subreddits):\n    \"\"\"Create an array of sitemaps.\n\n    Each sitemap has up to 50000 links, being the maximum allowable number of\n    links according to the sitemap standard.\n    \"\"\"\n    for subreddit_chunks in in_chunks(subreddits, LINKS_PER_SITEMAP):\n        yield _subreddit_sitemap(subreddit_chunks)\n\n\ndef sitemap_index(count):\n    sm_elem = etree.Element('sitemapindex', xmlns=SITEMAP_NAMESPACE)\n    for i in xrange(count):\n        sitemap_elem = etree.SubElement(sm_elem, 'sitemap')\n        loc_elem = etree.SubElement(sitemap_elem, 'loc')\n        url = '{0}/subreddit_sitemap/{1}.xml'.format(\n            g.sitemap_s3_static_host, i)\n        loc_elem.text = url\n    return _stringify_xml(sm_elem)\n"
  },
  {
    "path": "r2/r2/lib/sitemaps/store.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\n\"\"\"Store sitemaps in s3.\n\nThis module is uploads all subreddit sitemaps as well as the sitemap index\nto s3. The basic idea is that amazon will be serving the static sitemaps for\nus.\n\nThe binary data we send to s3 is a gzipped xml file. In addition we also\nsend the appropriate type and encoding headers so this is understood\ncorrectly by the browser.\n\nThe only file expected to be used outside this module is:\n\nstore_sitemaps_in_s3(subreddits)\n\nEven though the subreddits are expected to be generated and passed into this\nfunction, the sitemap index is created here. The reasoning is that in order\nto create the sitemap index we need to know how many sitemaps we have.\nIf we simply queried the subreddit iterator for it's length then we would\nhave to load all of the subreddits into memory, which would be ... bad.\n\"\"\"\n\n\nimport gzip\nfrom StringIO import StringIO\n\nfrom boto.s3.connection import S3Connection\nfrom boto.s3.key import Key\nfrom pylons import app_globals as g\n\nfrom r2.lib.sitemaps.generate import subreddit_sitemaps, sitemap_index\n\n\nHEADERS = {\n    'Content-Type': 'text/xml',\n    'Content-Encoding': 'gzip',\n}\n\n\ndef zip_string(string):\n    zipbuffer = StringIO()\n    with gzip.GzipFile(mode='w', fileobj=zipbuffer) as f:\n        f.write(string)\n    return zipbuffer.getvalue()\n\n\ndef upload_sitemap(key, sitemap):\n    key.set_contents_from_string(zip_string(sitemap), headers=HEADERS)\n\n\ndef store_subreddit_sitemap(bucket, index, sitemap):\n    key = Key(bucket)\n    key.key = 'subreddit_sitemap/{0}.xml'.format(index)\n    g.log.debug(\"Uploading %r\", key)\n\n    upload_sitemap(key, sitemap)\n\n\ndef store_sitemap_index(bucket, count):\n    key = Key(bucket)\n    key.key = g.sitemap_subreddit_keyname\n    g.log.debug(\"Uploading %r\", key)\n\n    upload_sitemap(key, sitemap_index(count))\n\n\ndef store_sitemaps_in_s3(subreddits):\n    s3conn = S3Connection()\n    bucket = s3conn.get_bucket(g.sitemap_upload_s3_bucket, validate=False)\n\n    sitemap_count = 0\n    for i, sitemap in enumerate(subreddit_sitemaps(subreddits)):\n        store_subreddit_sitemap(bucket, i, sitemap)\n        sitemap_count += 1\n\n    store_sitemap_index(bucket, sitemap_count)\n"
  },
  {
    "path": "r2/r2/lib/sitemaps/watcher.py",
    "content": "import datetime\nimport dateutil\nimport json\nimport pytz\nimport time\n\nfrom boto.s3.connection import S3Connection\nfrom boto.sqs.connection import SQSConnection\nfrom pylons import app_globals as g\n\nfrom r2.lib.s3_helpers import parse_s3_path\nfrom r2.lib.sitemaps.store import store_sitemaps_in_s3\nfrom r2.lib.sitemaps.data import find_all_subreddits\n\n\"\"\"Watch for SQS messages informing us to read, generate, and store sitemaps.\n\nThere is only function that should be used outside this module\n\nwatcher()\n\nIt is designed to be used in a daemon process.\n\"\"\"\n\n\ndef watcher():\n    \"\"\"Poll for new sitemap data and process it as necessary.\"\"\"\n    while True:\n        _process_message()\n\n\ndef _subreddit_sitemap_key():\n    conn = S3Connection()\n    bucket = conn.get_bucket(g.sitemap_upload_s3_bucket, validate=False)\n    return bucket.get_key(g.sitemap_subreddit_keyname)\n\n\ndef _datetime_from_timestamp(timestamp):\n    return datetime.datetime.fromtimestamp(timestamp / 1000, pytz.utc)\n\n\ndef _before_last_sitemap(timestamp):\n    sitemap_key = _subreddit_sitemap_key()\n    if sitemap_key is None:\n        return False\n\n    sitemap_datetime = dateutil.parser.parse(sitemap_key.last_modified)\n    compare_datetime = _datetime_from_timestamp(timestamp)\n    return compare_datetime < sitemap_datetime\n\n\ndef _process_message():\n    if not g.sitemap_sqs_queue:\n        return\n\n    sqs = SQSConnection()\n    sqs_q = sqs.get_queue(g.sitemap_sqs_queue)\n\n    messages = sqs.receive_message(sqs_q, number_messages=1)\n\n    if not messages:\n        return\n\n    message, = messages\n\n    js = json.loads(message.get_body())\n    s3path = parse_s3_path(js['location'])\n\n    # There are some error cases that allow us to get messages\n    # for sitemap creation that are now out of date.\n    timestamp = js.get('timestamp')\n    if timestamp is not None and _before_last_sitemap(timestamp):\n        sqs_q.delete_message(message)\n        return\n\n    g.log.info(\"Got import job %r\", js)\n\n    subreddits = find_all_subreddits(s3path)\n    store_sitemaps_in_s3(subreddits)\n\n    sqs_q.delete_message(message)\n\n\ndef _current_timestamp():\n    return time.time() * 1000\n\n\ndef _create_test_message():\n    \"\"\"A dev only function that drops a new message on the sqs queue.\"\"\"\n    sqs = SQSConnection()\n    sqs_q = sqs.get_queue(g.sitemap_sqs_queue)\n\n    # it returns None on failure\n    assert sqs_q, \"failed to connect to queue\"\n\n    message = sqs_q.new_message(body=json.dumps({\n        'job_name': 'daily-sr-sitemap-reporting',\n        'location': ('s3://reddit-data-analysis/big-data/r2/prod/' +\n                     'daily_sr_sitemap_reporting/dt=2016-06-14'),\n        'timestamp': _current_timestamp(),\n    }))\n    sqs_q.write(message)\n"
  },
  {
    "path": "r2/r2/lib/souptest.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\"\"\"\nTools to check if arbitrary HTML fragments would be safe to embed inline\n\"\"\"\n\n\nimport os\nimport re\nimport sys\nimport urllib\nimport urlparse\n\nimport lxml.etree\n\nfrom cStringIO import StringIO\n\nvalid_link_schemes = (\n    '/',\n    '#',\n    'http://',\n    'https://',\n    'ftp://',\n    'mailto:',\n    'steam://',\n    'irc://',\n    'ircs://',\n    'news://',\n    'mumble://',\n    'ssh://',\n    'git://',\n    'ts3server://',\n)\n\nallowed_tags = {\n    'div': {'class'},\n    'a': {'href', 'title', 'target', 'nofollow', 'rel'},\n    'img': {'src', 'alt', 'title'},\n}\n\nmarkdown_boring_tags = {\n    'p', 'em', 'strong', 'br', 'ol', 'ul', 'hr', 'li', 'pre', 'code',\n    'blockquote', 'center', 'sup', 'del', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',\n}\n\nmarkdown_user_tags = {\n    'table', 'th', 'tr', 'td', 'tbody', 'thead', 'tfoot', 'caption',\n}\n\nfor bt in markdown_boring_tags:\n    allowed_tags[bt] = {'id', 'class'}\n\nfor bt in markdown_user_tags:\n    allowed_tags[bt] = {\n        'colspan', 'rowspan', 'cellspacing', 'cellpadding', 'align', 'scope',\n    }\n\n\ndef souptest_sniff_node(node):\n    \"\"\"Check that a node from an (X)HTML document passes the sniff test\"\"\"\n    # Because IE loves conditional comments.\n    if node.tag is lxml.etree.Comment:\n        # Benign, used to turn space compression on and off.\n        if node.text.strip() not in {\"SC_ON\", \"SC_OFF\"}:\n            raise SoupUnexpectedCommentError(node)\n    # Looks like all nodes but `Element` have functions in `.tag`\n    elif isinstance(node.tag, basestring):\n        # namespaces are tacked onto the front of the tag / attr name if\n        # applicable so we don't need to worry about checking for those.\n        tag_name = node.tag\n        if tag_name not in allowed_tags:\n            raise SoupUnsupportedTagError(tag_name)\n\n        for attr, val in node.items():\n            if attr not in allowed_tags[tag_name]:\n                raise SoupUnsupportedAttrError(attr)\n\n            if tag_name == 'a' and attr == 'href':\n                lv = val.lower()\n                if not lv.startswith(valid_link_schemes):\n                    raise SoupUnsupportedSchemeError(val)\n                # work around CRBUG-464270\n                parsed_url = urlparse.urlparse(lv)\n                if parsed_url.hostname and len(parsed_url.hostname) > 255:\n                    raise SoupDetectedCrasherError(parsed_url.hostname)\n                # work around for Chrome crash with \"%%30%30\" - Sep 2015\n                if \"%00\" in urllib.unquote(parsed_url.path):\n                    raise SoupDetectedCrasherError(lv)\n    else:\n        # Processing instructions and friends fall down here.\n        raise SoupUnsupportedNodeError(node)\n\n\nENTITY_DTD_PATH = os.path.join(\n    os.path.dirname(os.path.abspath(__file__)),\n    'contrib/dtds/allowed_entities.dtd')\n\nSOUPTEST_DOCTYPE_FMT = '<!DOCTYPE div- [\\n %s \\n]>'\n\nUNDEFINED_ENTITY_RE = re.compile(r\"\\AEntity '([^']*)' not defined,.*\")\n\n\ndef souptest_fragment(fragment):\n    \"\"\"Check if an HTML fragment is sane and safe to embed.\n\n    For checking markup is safe, and that it will embed properly in both HTML\n    and XML documents. In practice, this means we check that all\n    tags / attributes are safe, and that the syntax conforms to a restricted\n    subset of XHTML.\n\n    No processing instructions, only specific comments, no CDATA sections,\n    and a whitelist of named character entities to deal with inconsistencies\n    between how parsers handle lookup failures for them.\n    \"\"\"\n    # We lazily load this so processes that don't souptest don't have to load\n    # it every startup.\n    souptest_doctype = getattr(souptest_fragment, 'souptest_doctype', None)\n    if souptest_doctype is None:\n        # Slurp in all of the entity definitions so we can avoid a read every\n        # time we parse\n        with open(ENTITY_DTD_PATH, 'r') as ent_file:\n            souptest_doctype = SOUPTEST_DOCTYPE_FMT % ent_file.read()\n        souptest_fragment.souptest_doctype = souptest_doctype\n\n    # lxml makes it *very* difficult to tell if there's a CDATA node in the\n    # tree, even if you tell it not to fold them into adjacent text sections.\n    # CDATA sections will be ignored and their innards parsed in most doctypes,\n    # so we need this hack to keep them out of markup.\n    if \"<![CDATA\" in fragment:\n        raise SoupUnexpectedCDataSectionError(fragment)\n\n    # We need to prepend our doctype + DTD or lxml can't resolve entities.\n    # We also need to wrap everything in a div, as lxml throws out\n    # comments outside the root tag. This also ensures that attempting an\n    # entity declaration or similar shenanigans will cause a syntax error.\n    documentized_fragment = \"%s<div>%s</div>\" % (souptest_doctype, fragment)\n    s = StringIO(documentized_fragment)\n\n    try:\n        parser = lxml.etree.XMLParser()\n        # Can't use a SAX interface because lxml's doesn't give you a handler\n        # for comments or entities unless you use Python 3. Oh well.\n        for node in lxml.etree.parse(s, parser).iter():\n            souptest_sniff_node(node)\n    except lxml.etree.XMLSyntaxError:\n        # Wrap the exception while keeping the original traceback\n        type_, value, trace = sys.exc_info()\n        # In XML some characters are illegal even as references, thankfully\n        # they're almost all control codes: (`&#x00;`, `&#x1c;`, etc.)\n        if value.msg.startswith('xmlParseCharRef: invalid xmlChar '):\n            raise SoupUnsupportedEntityError, (value,), trace\n        undef_ent = re.match(UNDEFINED_ENTITY_RE, value.msg)\n        if undef_ent:\n            raise SoupUnsupportedEntityError, (value, undef_ent.group(1)), trace\n\n        raise SoupSyntaxError, (value,), trace\n\n\nclass SoupError(Exception):\n    \"\"\"An error specific to the souptesting process\"\"\"\n    pass\n\n\nclass SoupReprError(SoupError):\n    \"\"\"Give a class-defined message as well as a repr of the passed object\"\"\"\n    HUMAN_MESSAGE = None\n\n    def __init__(self, obj):\n        self.obj = obj\n\n    def __str__(self):\n        return \"HAX: %s: %r\" % (self.HUMAN_MESSAGE, self.obj)\n\n\nclass SoupSyntaxError(SoupReprError):\n    \"\"\"Found a general syntax error\"\"\"\n    HUMAN_MESSAGE = \"XML Parsing error\"\n\n\nclass SoupUnsupportedNodeError(SoupReprError):\n    \"\"\"Found a weird node, like a processing instruction\"\"\"\n    HUMAN_MESSAGE = \"Unsupported node type\"\n\n\nclass SoupUnexpectedCommentError(SoupReprError):\n    \"\"\"Found a comment that hasn't been specifically whitelisted\"\"\"\n    HUMAN_MESSAGE = \"Unexpected comment\"\n\n\nclass SoupUnexpectedCDataSectionError(SoupReprError):\n    \"\"\"Found a CDATA section, which have no meaning in HTML5\"\"\"\n    HUMAN_MESSAGE = \"Unexpected CDATA section\"\n\n\nclass SoupUnsupportedSchemeError(SoupReprError):\n    \"\"\"Found a URL whose scheme hasn't been explicitly whitelisted\"\"\"\n    HUMAN_MESSAGE = \"Unsupported URL scheme\"\n\n\nclass SoupUnsupportedAttrError(SoupReprError):\n    \"\"\"Found an element attribute that hasn't been explicitly whitelisted\"\"\"\n    HUMAN_MESSAGE = \"Unsupported attribute\"\n\n\nclass SoupUnsupportedTagError(SoupReprError):\n    \"\"\"Found an element that hasn't been explicitly whitelisted\"\"\"\n    HUMAN_MESSAGE = \"Unsupported tag\"\n\n\nclass SoupDetectedCrasherError(SoupReprError):\n    HUMAN_MESSAGE = \"Known crasher posted\"\n\n\nclass SoupUnsupportedEntityError(SoupReprError):\n    \"\"\"Found an otherwise well-formed entity that couldn't be accepted\"\"\"\n    HUMAN_MESSAGE = \"Invalid or unrecognized entity\"\n\n    # `entity` is optional, because we can't get the passed-in entity in\n    # the case of an exception because of invalid numeric entities.\n    def __init__(self, obj, entity=None):\n        self.entity = entity\n        SoupReprError.__init__(self, obj)\n"
  },
  {
    "path": "r2/r2/lib/sr_pops.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom r2.lib import count\nfrom r2.models import Subreddit\n\n\ndef set_downs():\n    sr_counts = count.get_sr_counts()\n    names = [k for k, v in sr_counts.iteritems() if v != 0]\n    srs = Subreddit._by_fullname(names)\n    for name in names:\n        sr,c = srs[name], sr_counts[name]\n        if c != sr._downs and c > 0:\n            sr._downs = max(c, 0)\n            sr._commit()\n\n\ndef run():\n    set_downs()\n"
  },
  {
    "path": "r2/r2/lib/static.py",
    "content": "#!/usr/bin/env python\n# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport sys\nimport os\nimport hashlib\nimport json\nimport base64\nimport shutil\n\n\ndef locate_static_file(name):\n    from pylons import app_globals as g\n    static_dirs = [plugin.static_dir for plugin in g.plugins]\n    static_dirs.insert(0, g.paths['static_files'])\n\n    for static_dir in reversed(static_dirs):\n        file_path = os.path.join(static_dir, name.lstrip('/'))\n        if os.path.exists(file_path):\n            return file_path\n\n\ndef static_mtime(name):\n    path = locate_static_file(name)\n    if path:\n        return os.path.getmtime(path)\n\n\ndef generate_static_name(name, base=None):\n    \"\"\"Generate a unique filename.\n    \n    Unique filenames are generated by base 64 encoding the first 64 bits of\n    the SHA1 hash. This hash string is added to the filename before the extension.\n    \"\"\"\n    if base:\n        path = os.path.join(base, name)\n    else:\n        path = name\n\n    sha = hashlib.sha1(open(path).read()).digest()\n    shorthash = base64.urlsafe_b64encode(sha[0:8]).rstrip(\"=\")\n    name, ext = os.path.splitext(name)\n    return name + '.' + shorthash + ext\n\n\ndef update_static_names(names_file, files):\n    \"\"\"Generate a unique file name mapping for ``files`` and write it to a\n    JSON file at ``names_file``.\"\"\"\n    if os.path.exists(names_file):\n        names = json.load(open(names_file))\n    else:\n        names = {}\n\n    base = os.path.dirname(names_file)\n    for path in files:\n        name = os.path.relpath(path, base)\n        mangled_name = generate_static_name(name, base)\n        names[name] = mangled_name\n\n        if not os.path.islink(path):\n            mangled_path = os.path.join(base, mangled_name)\n            shutil.move(path, mangled_path)\n            # When on NFS, cp has a bad habit of turning our symlinks into\n            # hardlinks. shutil.move will then call rename which will noop in\n            # the case of hardlinks to the same inode.\n            if os.path.exists(path):\n                os.unlink(path)\n            os.symlink(mangled_name, path)\n\n    json_enc = json.JSONEncoder(indent=2, sort_keys=True)\n    open(names_file, \"w\").write(json_enc.encode(names))\n\n    return names\n\n\nif __name__ == \"__main__\":\n    update_static_names(sys.argv[1], sys.argv[2:])\n"
  },
  {
    "path": "r2/r2/lib/stats.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport collections\nimport functools\nimport os\nimport random\nimport socket\nimport time\nimport threading\nimport random\n\nfrom pycassa import columnfamily\nfrom pycassa import pool\n\nfrom r2.lib import baseplate_integration\nfrom r2.lib import cache\nfrom r2.lib import utils\n\nclass TimingStatBuffer:\n    \"\"\"Dictionary of keys to cumulative time+count values.\n\n    This provides thread-safe accumulation of pairs of values. Iterating over\n    instances of this class yields (key, (total_time, count)) tuples.\n    \"\"\"\n\n    Timing = collections.namedtuple('Timing', ['key', 'start', 'end'])\n\n\n    def __init__(self):\n        # Store data internally as a map of keys to complex values. The real\n        # part of the complex value is the total time (in seconds), and the\n        # imaginary part is the total count.\n        self.data = collections.defaultdict(complex)\n        self.log = threading.local()\n\n    def record(self, key, start, end, publish=True):\n        if publish:\n            # Add to the total time and total count with a single complex value,\n            # so as to avoid inconsistency from a poorly timed context switch.\n            self.data[key] += (end - start) + 1j\n\n        if getattr(self.log, 'timings', None) is not None:\n            self.log.timings.append(self.Timing(key, start, end))\n\n    def flush(self):\n        \"\"\"Yields accumulated timing and counter data and resets the buffer.\"\"\"\n        data, self.data = self.data, collections.defaultdict(complex)\n        while True:\n            try:\n                k, v = data.popitem()\n            except KeyError:\n                break\n\n            total_time, count = v.real, v.imag\n            divisor = count or 1\n            mean = total_time / divisor\n            yield k, str(mean * 1000) + '|ms'\n\n    def start_logging(self):\n        self.log.timings = []\n\n    def end_logging(self):\n        timings = getattr(self.log, 'timings', None)\n        self.log.timings = None\n        return timings\n\n\nclass CountingStatBuffer:\n    \"\"\"Dictionary of keys to cumulative counts.\"\"\"\n\n    def __init__(self):\n        self.data = collections.defaultdict(int)\n\n    def record(self, key, delta):\n        self.data[key] += delta\n\n    def flush(self):\n        \"\"\"Yields accumulated counter data and resets the buffer.\"\"\"\n        data, self.data = self.data, collections.defaultdict(int)\n        for k, v in data.iteritems():\n            yield k, str(v) + '|c'\n\n\nclass StringCountBuffer:\n    \"\"\"Dictionary of keys to counts of various values.\"\"\"\n\n    def __init__(self):\n        self.data = collections.defaultdict(\n            functools.partial(collections.defaultdict, int))\n\n    @staticmethod\n    def _encode_string(string):\n        # escape \\ -> \\\\, | -> \\&, : -> \\;, and newline -> \\n\n        return (\n            string.replace('\\\\', '\\\\\\\\')\n                .replace('\\n', '\\\\n')\n                .replace('|', '\\\\&')\n                .replace(':', '\\\\;'))\n\n    def record(self, key, value, count=1):\n        self.data[key][value] += count\n\n    def flush(self):\n        new_data = collections.defaultdict(\n            functools.partial(collections.defaultdict, int))\n        data, self.data = self.data, new_data\n        for k, counts in data.iteritems():\n            for v, count in counts.iteritems():\n                yield k, str(count) + '|s|' + self._encode_string(v)\n\n\nclass StatsdConnection:\n    def __init__(self, addr, compress=True):\n        if addr:\n            self.host, self.port = self._parse_addr(addr)\n            self.sock = self._make_socket()\n        else:\n            self.host = self.port = self.sock = None\n        self.compress = compress\n\n    @classmethod\n    def _make_socket(cls):\n        return socket.socket(socket.AF_INET, socket.SOCK_DGRAM)\n\n    @staticmethod\n    def _parse_addr(addr):\n        host, port_str = addr.rsplit(':', 1)\n        return host, int(port_str)\n\n    @staticmethod\n    def _compress(lines):\n        compressed_lines = []\n        previous = ''\n        for line in sorted(lines):\n            prefix = os.path.commonprefix([previous, line])\n            if len(prefix) > 3:\n                prefix_len = len(prefix)\n                compressed_lines.append(\n                    '^%02x%s' % (prefix_len, line[prefix_len:]))\n            else:\n                compressed_lines.append(line)\n            previous = line\n        return compressed_lines\n\n    def send(self, data):\n        if self.sock is None:\n            return\n        data = ('%s:%s' % item for item in data)\n        if self.compress:\n            data = self._compress(data)\n        payload = '\\n'.join(data)\n        self.sock.sendto(payload, (self.host, self.port))\n\n\nclass StatsdClient:\n    _data_iterator = iter\n    _make_conn = StatsdConnection\n\n    def __init__(self, addr=None, sample_rate=1.0):\n        self.sample_rate = sample_rate\n        self.timing_stats = TimingStatBuffer()\n        self.counting_stats = CountingStatBuffer()\n        self.string_counts = StringCountBuffer()\n        self.connect(addr)\n\n    def connect(self, addr):\n        self.conn = self._make_conn(addr)\n\n    def disconnect(self):\n        self.conn = self._make_conn(None)\n\n    def flush(self):\n        data = list(self.timing_stats.flush())\n        data.extend(self.counting_stats.flush())\n        data.extend(self.string_counts.flush())\n        self.conn.send(self._data_iterator(data))\n\n\ndef _get_stat_name(*name_parts):\n    def to_str(value):\n        if isinstance(value, unicode):\n            value = value.encode('utf-8', 'replace')\n        return value\n    return '.'.join(to_str(x) for x in name_parts if x)\n\n\nclass Counter:\n    def __init__(self, client, name):\n        self.client = client\n        self.name = name\n\n    def _send(self, subname, delta):\n        name = _get_stat_name(self.name, subname)\n        return self.client.counting_stats.record(name, delta)\n\n    def increment(self, subname=None, delta=1):\n        self._send(subname, delta)\n\n    def decrement(self, subname=None, delta=1):\n        self._send(subname, -delta)\n\n    def __add__(self, delta):\n        self.increment(delta=delta)\n        return self\n\n    def __sub__(self, delta):\n        self.decrement(delta=delta)\n        return self\n\n\nclass Timer:\n    _time = time.time\n\n    def __init__(self, client, name, publish=True):\n        self.client = client\n        self.name = name\n        self.publish = publish\n        self._start = None\n        self._last = None\n        self._stop = None\n        self._timings = []\n\n    def __enter__(self):\n        self.start()\n        return self\n\n    def __exit__(self, type, value, tb):\n        self.stop()\n\n    def flush(self):\n        for timing in self._timings:\n            self.send(*timing)\n        self._timings = []\n\n    def elapsed_seconds(self):\n        if self._start is None:\n            raise AssertionError(\"timer hasn't been started\")\n        if self._stop is None:\n            raise AssertionError(\"timer hasn't been stopped\")\n        return self._stop - self._start\n\n    def send(self, subname, start, end):\n        name = _get_stat_name(self.name, subname)\n        self.client.timing_stats.record(name, start, end,\n                                        publish=self.publish)\n\n    def start(self):\n        self._last = self._start = self._time()\n\n    def intermediate(self, subname):\n        if self._last is None:\n            raise AssertionError(\"timer hasn't been started\")\n        if self._stop is not None:\n            raise AssertionError(\"timer is stopped\")\n        last, self._last = self._last, self._time()\n        self._timings.append((subname, last, self._last))\n\n    def stop(self, subname='total'):\n        if self._start is None:\n            raise AssertionError(\"timer hasn't been started\")\n        if self._stop is not None:\n            raise AssertionError('timer is already stopped')\n        self._stop = self._time()\n        self.flush()\n        self.send(subname, self._start, self._stop)\n\n\nclass Stats:\n    # Sample rate for recording cache hits/misses, relative to the global\n    # sample_rate.\n    CACHE_SAMPLE_RATE = 0.01\n\n    CASSANDRA_KEY_SUFFIXES = ['error', 'ok']\n\n    def __init__(self, addr, sample_rate):\n        self.client = StatsdClient(addr, sample_rate)\n\n    def get_timer(self, name, publish=True):\n        return Timer(self.client, name, publish)\n\n    # Just a convenience method to use timers as context managers clearly\n    def quick_time(self, *args, **kwargs):\n        return self.get_timer(*args, **kwargs)\n\n    def transact(self, action, start, end):\n        timer = self.get_timer('service_time')\n        timer.send(action, start, end)\n\n    def get_counter(self, name):\n        return Counter(self.client, name)\n\n    def action_count(self, counter_name, name, delta=1):\n        counter = self.get_counter(counter_name)\n        if counter:\n            from pylons import request\n            counter.increment('%s.%s' % (request.environ[\"pylons.routes_dict\"][\"action\"], name), delta=delta)\n\n    def action_event_count(self, event_name, state=None, delta=1, true_name=\"success\", false_name=\"fail\"):\n        counter_name = 'event.%s' % event_name\n        if state == True:\n            self.action_count(counter_name, true_name, delta=delta)\n        elif state == False:\n            self.action_count(counter_name, false_name, delta=delta)\n        self.action_count(counter_name, 'total', delta=delta)\n\n    def simple_event(self, event_name, delta=1):\n        parts = event_name.split('.')\n        counter = self.get_counter('.'.join(['event'] + parts[:-1]))\n        if counter:\n            counter.increment(parts[-1], delta=delta)\n\n    def simple_timing(self, event_name, ms):\n        self.client.timing_stats.record(event_name, start=0, end=ms)\n\n    def event_count(self, event_name, name, sample_rate=None):\n        if sample_rate is None:\n            sample_rate = 1.0\n        counter = self.get_counter('event.%s' % event_name)\n        if counter and random.random() < sample_rate:\n            counter.increment(name)\n            counter.increment('total')\n\n    def cache_count_multi(self, data, sample_rate=None):\n        if sample_rate is None:\n            sample_rate = self.CACHE_SAMPLE_RATE\n        counter = self.get_counter('cache')\n        if counter and random.random() < sample_rate:\n            for name, delta in data.iteritems():\n                counter.increment(name, delta=delta)\n\n    def amqp_processor(self, queue_name):\n        \"\"\"Decorator for recording stats for amqp queue consumers/handlers.\"\"\"\n        def decorator(processor):\n            def wrap_processor(msgs, *args):\n                # Work the same for amqp.consume_items and amqp.handle_items.\n                msg_tup = utils.tup(msgs)\n\n                metrics_name = \"amqp.\" + queue_name\n                start = time.time()\n                try:\n                    with baseplate_integration.make_server_span(metrics_name):\n                        return processor(msgs, *args)\n                finally:\n                    service_time = (time.time() - start) / len(msg_tup)\n                    for n, msg in enumerate(msg_tup):\n                        fake_start = start + n * service_time\n                        fake_end = fake_start + service_time\n                        self.transact(metrics_name, fake_start, fake_end)\n                    self.flush()\n            return wrap_processor\n        return decorator\n\n    def flush(self):\n        self.client.flush()\n\n    def start_logging_timings(self):\n        self.client.timing_stats.start_logging()\n\n    def end_logging_timings(self):\n        return self.client.timing_stats.end_logging()\n\n    def cf_key_iter(self, operation, column_families, suffix):\n        if not self.client:\n            return\n        if not isinstance(column_families, list):\n            column_families = [column_families]\n        for cf in column_families:\n            yield '.'.join(['cassandra', cf, operation, suffix])\n\n    def cassandra_timing(self, operation, column_families, success,\n                         start, end):\n        suffix = self.CASSANDRA_KEY_SUFFIXES[success]\n        for key in self.cf_key_iter(operation, column_families, suffix):\n            self.client.timing_stats.record(key, start, end)\n\n    def cassandra_counter(self, operation, column_families, suffix, delta):\n        for key in self.cf_key_iter(operation, column_families, suffix):\n            self.client.counting_stats.record(key, delta)\n\n    def pg_before_cursor_execute(self, conn, cursor, statement, parameters,\n                               context, executemany):\n        from pylons import tmpl_context as c\n\n        context._query_start_time = time.time()\n\n        try:\n            c.trace\n        except TypeError:\n            # the tmpl_context global isn't available out of request\n            return\n\n        if c.trace:\n            context.pg_child_trace = c.trace.make_child(\"postgres\")\n            context.pg_child_trace.start()\n\n    def pg_after_cursor_execute(self, conn, cursor, statement, parameters,\n                              context, executemany):\n        dsn = dict(part.split('=', 1)\n                   for part in context.engine.url.query['dsn'].split())\n\n        if getattr(context, \"pg_child_trace\", None):\n            context.pg_child_trace.set_tag(\"host\", dsn[\"host\"])\n            context.pg_child_trace.set_tag(\"db\", dsn[\"dbname\"])\n            context.pg_child_trace.set_tag(\"statement\", statement)\n            context.pg_child_trace.finish()\n\n        start = context._query_start_time\n        self.pg_event(dsn['host'], dsn['dbname'], start, time.time())\n\n    def pg_event(self, db_server, db_name, start, end):\n        if not self.client:\n            return\n        key = '.'.join(['pg', db_server.replace('.', '-'), db_name])\n        self.client.timing_stats.record(key, start, end)\n\n    def count_string(self, key, value, count=1):\n        self.client.string_counts.record(key, str(value), count=count)\n\n\nclass CacheStats:\n    def __init__(self, parent, cache_name):\n        self.parent = parent\n        self.cache_name = cache_name\n        self.hit_stat_name = '%s.hit' % self.cache_name\n        self.miss_stat_name = '%s.miss' % self.cache_name\n        self.total_stat_name = '%s.total' % self.cache_name\n        self.hit_stat_template = '%s.%%s.hit' % self.cache_name\n        self.miss_stat_template = '%s.%%s.miss' % self.cache_name\n        self.total_stat_template = '%s.%%s.total' % self.cache_name\n\n    def cache_hit(self, delta=1, subname=None):\n        if delta:\n            data = {\n                self.hit_stat_name: delta,\n                self.total_stat_name: delta,\n            }\n            if subname:\n                data.update({\n                    self.hit_stat_template % subname: delta,\n                    self.total_stat_template % subname: delta,\n                })\n            self.parent.cache_count_multi(data)\n\n    def cache_miss(self, delta=1, subname=None):\n        if delta:\n            data = {\n                self.miss_stat_name: delta,\n                self.total_stat_name: delta,\n            }\n            if subname:\n                data.update({\n                    self.miss_stat_template % subname: delta,\n                    self.total_stat_template % subname: delta,\n                })\n            self.parent.cache_count_multi(data)\n\n\nclass StaleCacheStats(CacheStats):\n    def __init__(self, parent, cache_name):\n        CacheStats.__init__(self, parent, cache_name)\n        self.stale_hit_name = '%s.stale.hit' % self.cache_name\n        self.stale_miss_name = '%s.stale.miss' % self.cache_name\n        self.stale_total_name = '%s.stale.total' % self.cache_name\n        self.stale_hit_stat_template = '%s.stale.%%s.hit' % self.cache_name\n        self.stale_miss_stat_template = '%s.stale.%%s.miss' % self.cache_name\n        self.stale_total_stat_template = '%s.stale.%%s.total' % self.cache_name\n\n    def stale_hit(self, delta=1, subname=None):\n        if delta:\n            data = {\n                self.stale_hit_name: delta,\n                self.stale_total_name: delta,\n            }\n            if subname:\n                data.update({\n                    self.stale_hit_stat_template % subname: delta,\n                    self.stale_total_stat_template % subname: delta,\n                })\n            self.parent.cache_count_multi(data)\n\n    def stale_miss(self, delta=1, subname=None):\n        if delta:\n            data = {\n                self.stale_miss_name: delta,\n                self.stale_total_name: delta,\n            }\n            if subname:\n                data.update({\n                    self.stale_miss_stat_template % subname: delta,\n                    self.stale_total_stat_template % subname: delta,\n                })\n            self.parent.cache_count_multi(data)\n\n\nclass StatsCollectingConnectionPool(pool.ConnectionPool):\n    def __init__(self, keyspace, stats=None, *args, **kwargs):\n        pool.ConnectionPool.__init__(self, keyspace, *args, **kwargs)\n        self.stats = stats\n\n    def _get_new_wrapper(self, server):\n        host, sep, port = server.partition(':')\n        self.stats.event_count('cassandra.connections', host)\n\n        cf_types = (columnfamily.ColumnParent, columnfamily.ColumnPath)\n\n        def get_cf_name_from_args(args, kwargs):\n            for v in args:\n                if isinstance(v, cf_types):\n                    return v.column_family\n            for v in kwargs.itervalues():\n                if isinstance(v, cf_types):\n                    return v.column_family\n            return None\n\n        def get_cf_name_from_batch_mutation(args, kwargs):\n            cf_names = set()\n            mutation_map = args[0]\n            for key_mutations in mutation_map.itervalues():\n                cf_names.update(key_mutations)\n            return list(cf_names)\n\n        instrumented_methods = dict(\n            get=get_cf_name_from_args,\n            get_slice=get_cf_name_from_args,\n            multiget_slice=get_cf_name_from_args,\n            get_count=get_cf_name_from_args,\n            multiget_count=get_cf_name_from_args,\n            get_range_slices=get_cf_name_from_args,\n            get_indexed_slices=get_cf_name_from_args,\n            insert=get_cf_name_from_args,\n            batch_mutate=get_cf_name_from_batch_mutation,\n            add=get_cf_name_from_args,\n            remove=get_cf_name_from_args,\n            remove_counter=get_cf_name_from_args,\n            truncate=lambda args, kwargs: args[0],\n        )\n\n        def record_error(method_name, cf_name, start, end):\n            if cf_name and self.stats:\n                self.stats.cassandra_timing(method_name, cf_name, False,\n                                           start, end)\n\n        def record_success(method_name, cf_name, start, end):\n            if cf_name and self.stats:\n                self.stats.cassandra_timing(method_name, cf_name, True,\n                                           start, end)\n\n        def record_size(method_name, cf_name, result, key=\"size\"):\n            if cf_name and self.stats:\n                # if we don't have easy access to the wire-size, we can\n                # proxy because thrift objects have descriptive reprs\n                size = len(repr(result))\n                self.stats.cassandra_counter(method_name, cf_name, key, size)\n\n        # size_sample determines how often we measure and track the size\n        # of the response from cassandra.\n        def instrument(f, get_cf_name, size_sample=0.01):\n            def call_with_instrumentation(*args, **kwargs):\n                from pylons import tmpl_context as c\n\n                cf_name = get_cf_name(args, kwargs)\n                method_name = f.__name__\n\n                try:\n                    c.trace\n                except TypeError:\n                    # the tmpl_context global isn't available out of request\n                    cassandra_child_trace = utils.SimpleSillyStub()\n                else:\n                    if c.trace:\n                        cassandra_child_trace = c.trace.make_child(\"cassandra\")\n                        cassandra_child_trace.set_tag(\"column_family\", cf_name)\n                        cassandra_child_trace.set_tag(\"method\", method_name)\n                    else:\n                        cassandra_child_trace = utils.SimpleSillyStub()\n\n                start = time.time()\n                try:\n                    with cassandra_child_trace:\n                        result = f(*args, **kwargs)\n                except:\n                    record_error(method_name, cf_name, start, time.time())\n                    raise\n                else:\n                    if random.random() < size_sample:\n                        record_size(method_name, cf_name, result)\n\n                    record_success(method_name, cf_name, start, time.time())\n                    return result\n            return call_with_instrumentation\n\n        wrapper = pool.ConnectionPool._get_new_wrapper(self, server)\n        for method_name, get_cf_name in instrumented_methods.iteritems():\n            f = getattr(wrapper, method_name)\n            setattr(wrapper, method_name, instrument(f, get_cf_name))\n        return wrapper\n"
  },
  {
    "path": "r2/r2/lib/strings.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\n\"\"\"\nModule for maintaining long or commonly used translatable strings,\nremoving the need to pollute the code with lots of extra _ and\nungettext calls.\n\"\"\"\n\nfrom pylons import tmpl_context as c\nfrom pylons import app_globals as g\nfrom pylons.i18n import _, ungettext, get_lang\nimport random\nimport babel.numbers\n\nfrom r2.lib.filters import websafe\nfrom r2.lib.generate_strings import funny_translatable_strings\nfrom r2.lib.translation import set_lang\n\n\n__all__ = ['StringHandler', 'strings', 'PluralManager', 'plurals',\n           'Score', 'get_funny_translated_string']\n\n# here's where all of the really long site strings (that need to be\n# translated) live so as not to clutter up the rest of the code.  This\n# dictionary is not used directly but rather is managed by the single\n# StringHandler instance strings\nstring_dict = dict(\n\n    banned_by = \"removed by %s\",\n    banned    = \"removed\",\n    times_banned=\"removed %d times\",\n    time_banned=\"removed at %s\",\n    time_approved=\"approved at %s\",\n    reports   = \"reports: %d\",\n\n    submitting = _(\"submitting...\"),\n\n    # this accomodates asian languages which don't use spaces\n    number_label = _(\"%(num)d %(thing)s\"),\n\n    # this accomodates asian languages which don't use spaces\n    points_label = _(\"%(num)d %(point)s\"),\n\n    # this accomodates asian languages which don't use spaces\n    time_label = _(\"%(num)d %(time)s\"),\n\n    # this accomodates asian languages which don't use spaces\n    float_label = _(\"%(num)5.3f %(thing)s\"),\n\n    already_submitted = _(\"that link has already been submitted, but you can try to [submit it again](%s).\"),\n\n    multiple_submitted = _(\"that link has been submitted to multiple subreddits. you can try to [submit it again](%s).\"),\n\n    user_deactivated = _(\"your account has been deactivated, but we won't judge you for it.\"),\n\n    oauth_login_msg = _(\n        \"Log in or sign up to connect your reddit account with %(app)s.\"),\n\n    legal = _(\"I understand and agree that registration on or use of this site constitutes agreement to its %(user_agreement)s and %(privacy_policy)s.\"),\n\n    friends = _('to view reddit with only submissions from your friends, use [reddit.com/r/friends](%s)'),\n\n    sr_created = _('your subreddit has been created'),\n\n    more_info_link = _(\"visit [%(link)s](%(link)s) for more information\"),\n\n    sr_messages = dict(\n        empty =  _('you have not subscribed to any subreddits.'),\n        subscriber =  _('below are the subreddits you have subscribed to.'),\n        contributor =  _('below are the subreddits that you are an approved submitter on.'),\n        moderator = _('below are the subreddits that you have moderator access to.')\n        ),\n\n    sr_subscribe =  _('click the `subscribe` or `unsubscribe` buttons to choose which subreddits appear on your front page.'),\n\n    searching_a_reddit = _('you\\'re searching within the [%(reddit_name)s](%(reddit_link)s) subreddit. '+\n                           'you can also search within [all subreddits](%(all_reddits_link)s)'),\n\n    permalink_title = _(\"%(author)s comments on %(title)s\"),\n    link_info_title = _(\"%(title)s : %(site)s\"),\n    link_info_og_description = _(\"%(score)s points and %(num_comments)s comments so far on reddit\"),\n\n    submit_link = _(\"\"\"You are submitting a link. The key to a successful submission is interesting content and a descriptive title.\"\"\"),\n    submit_text = _(\"\"\"You are submitting a text-based post. Speak your mind. A title is required, but expanding further in the text field is not. Beginning your title with \"vote up if\" is violation of intergalactic law.\"\"\"),\n    submit_link_label = _(\"Submit a new link\"),\n    submit_text_label = _(\"Submit a new text post\"),\n    verify_email = _(\"we're going to need to verify your email address for you to proceed.\"),\n    verify_email_submit = _(\"you'll be able to submit more frequently once you verify your email address\"),\n    email_verified =  _(\"your email address has been verified\"),\n    email_verify_failed = _(\"Verification failed.  Please try that again\"),\n    email_verify_wrong_user = _(\"The email verification link you've followed is for a different user. Please log out and switch to that user or try again below.\"),\n    search_failed = _(\"Our search machines are under too much load to handle your request right now. :( Sorry for the inconvenience. Try again in a little bit -- but please don't mash reload; that only makes the problem worse.\"),\n    invalid_search_query = _(\"I couldn't understand your query, so I simplified it and searched for \\\"%(clean_query)s\\\" instead.\"),\n    completely_invalid_search_query = _(\"I couldn't understand your search query. Please try again.\"),\n    search_help = _(\"You may also want to check the [search help page](%(search_help)s) for more information.\"),\n    formatting_help_info = _('reddit uses a slightly-customized version of [Markdown](http://daringfireball.net/projects/markdown/syntax) for formatting. See below for some basics, or check [the commenting wiki page](/wiki/commenting) for more detailed help and solutions to common issues.'),\n    read_only_msg = _(\"Reddit is in \\\"emergency read-only mode\\\" right now. :( You won't be able to log in. We're sorry and are working frantically to fix the problem.\"),\n    heavy_load_msg = _(\"this page is temporarily in read-only mode due to heavy traffic.\"),\n    in_perma_timeout_msg = _(\"Your account has been permanently [suspended](https://reddit.zendesk.com/hc/en-us/articles/205687686) from Reddit.\"),\n    in_temp_timeout_msg = _(\"Your account has been [suspended](https://reddit.zendesk.com/hc/en-us/articles/205687686) from Reddit for %(days)s.\"),\n    gold_benefits_msg = \"reddit gold is our premium membership program. It grants you access to [extra features](https://www.reddit.com/gold/about) to improve your reddit experience. It also makes you really quite dapper. If you have questions about your gold, please visit /r/goldbenefits.\",\n    lounge_msg = \"Grab a drink and join us in /r/lounge, the super-secret members-only community that may or may not exist.\",\n    postcard_msg = _(\"You sent us a postcard! (Or something similar.) When we run out of room on our refrigerator, we might one day auction off the stuff that people sent in. Is it okay if we include your thing?\"),\n    over_comment_limit = _(\"Sorry, the maximum number of comments is %(max)d. (However, if you subscribe to reddit gold, it goes up to %(goldmax)d.)\"),\n    over_comment_limit_gold = _(\"Sorry, the maximum number of comments is %d.\"),\n    youve_got_gold = \"%(sender)s just gifted you %(amount)s of reddit gold!\",\n    giftgold_note = \"Here's a note that was included:\\n\\n----\\n\\n\",\n    youve_been_gilded_comment = \"%(sender)s liked [your comment](%(url)s) so much that they gilded it, giving you reddit gold.\\n\\n\",\n    youve_been_gilded_link = \"%(sender)s liked [your submission](%(url)s) so much that they gilded it, giving you reddit gold.\\n\\n\",\n    respond_to_anonymous_gilder = \"Want to say thanks to your mysterious benefactor? Reply to this message. You will find out their username if they choose to reply back.\",\n    unsupported_respond_to_gilder = \"Sorry, replying directly to your mysterious benefactor is not yet supported for this gilding.\",\n    anonymous_gilder_warning = _(\"***WARNING: Responding to this message will reveal your username to the gildee.***\\n\\n\"),\n    gold_claimed_code = _(\"Thanks for claiming a reddit gold code.\\n\\n\"),\n    gold_summary_autorenew_monthly = _(\"You're about to set up an ongoing, autorenewing subscription to reddit gold for yourself (%(user)s). \\n\\nYou'll pay **%(price)s** for this, **monthly**. \\n\\n>This subscription will renew automatically each month until you cancel. You may cancel at any time. If you cancel, you will not be billed for any additional months of service, and service will continue until the end of the billing period. If you cancel, you will not receive a refund for any service already paid for. Receipts will be delivered via private message in your account.\"),\n    gold_summary_autorenew_yearly = _(\"You're about to set up an ongoing, autorenewing subscription to reddit gold for yourself (%(user)s). \\n\\nYou'll pay **%(price)s** for this, **yearly**. \\n\\n>This subscription will renew automatically each year until you cancel. You may cancel at any time. If you cancel, you will not be billed for any additional years of service, and service will continue until the end of the billing period. If you cancel, you will not receive a refund for any service already paid for. Receipts will be delivered via private message in your account.\"),\n    gold_summary_onetime = _(\"You're about to make a one-time purchase of %(amount)s of reddit gold for yourself (%(user)s). You'll pay a total of %(price)s for this.\"),\n    gold_summary_creddits = _(\"You're about to purchase %(amount)s. They work like gift certificates: each creddit you have will allow you to give one month of reddit gold to someone else. You'll pay a total of %(price)s for this.\"),\n    gold_summary_gift_code = _(\"You're about to purchase %(amount)s of reddit gold in the form of a gift code. The recipient (or you) will be able to claim the code to redeem that gold to their account. You'll pay a total of %(price)s for this.\"),\n    gold_summary_signed_gift = _(\"You're about to give %(amount)s of reddit gold to %(recipient)s, who will be told that it came from you. You'll pay a total of %(price)s for this.\"),\n    gold_summary_anonymous_gift = _(\"You're about to give %(amount)s of reddit gold to %(recipient)s. It will be an anonymous gift. You'll pay a total of %(price)s for this.\"),\n    gold_summary_gilding_comment = _(\"Want to say thanks to *%(recipient)s* for this comment? Give them a month of [reddit gold](/gold/about).\"),\n    gold_summary_gilding_link = _(\"Want to say thanks to *%(recipient)s* for this submission? Give them a month of [reddit gold](/gold/about).\"),\n    gold_summary_gilding_page_comment = _(\"You're about to give *%(recipient)s* a month of [reddit gold](/gold/about) for this comment:\"),\n    gold_summary_gilding_page_link = _(\"You're about to give *%(recipient)s* a month of [reddit gold](/gold/about) for this submission:\"),\n    gold_summary_gilding_page_footer = _(\"You'll pay a total of %(price)s for this.\"),\n    archived_post_message = _(\"This is an archived post. You won't be able to vote or comment.\"),\n    locked_post_message = _(\"This post is locked. You won't be able to comment.\"),\n    account_activity_blurb = _(\"This page shows a history of recent activity on your account. If you notice unusual activity, you should change your password immediately. Location information is guessed from your computer's IP address and may be wildly wrong, especially for visits from mobile devices.\"),\n    your_current_ip_is = _(\"You are currently accessing reddit from this IP address: %(address)s.\"),\n    account_activity_apps_blurb = _(\"\"\"\nThese apps are authorized to access your account. Signing out of all sessions\nwill revoke access from all apps. You may also revoke access from individual\napps below.\n\"\"\"),\n\n    traffic_promoted_link_explanation = _(\"Below you will see your promotion's impression and click traffic per hour of promotion.  Please note that these traffic totals will lag behind by two to three hours, and that daily totals will be preliminary until 24 hours after the link has finished its run.\"),\n    traffic_processing_slow = _(\"Traffic processing is currently running slow. The latest data available is from %(date)s. This page will be updated as new data becomes available.\"),\n    traffic_processing_normal = _(\"Traffic processing occurs on an hourly basis. The latest data available is from %(date)s. This page will be updated as new data becomes available.\"),\n    traffic_help_email = _(\"Questions? Email self serve support: %(email)s\"),\n\n    traffic_subreddit_explanation = _(\"\"\"\nBelow are the traffic statistics for your subreddit. Each graph represents one of the following over the interval specified.\n\n* **pageviews** are all hits to %(subreddit)s, including both listing pages and comment pages.\n* **uniques** are the total number of unique visitors (determined by a combination of their IP address and User Agent string) that generate the above pageviews. This is independent of whether or not they are signed in.\n* **subscriptions** is the number of new subscriptions that have been generated in a given day. This number is less accurate than the first two metrics, as, though we can track new subscriptions, we have no way to track unsubscriptions.\n\nNote: there are a couple of places outside of your subreddit where someone can click \"subscribe\", so it is possible (though unlikely) that the subscription count can exceed the unique count on a given day.\n\"\"\"),\n\n    subscribed_multi = _(\"multireddit of your subscriptions\"),\n    mod_multi = _(\"multireddit of subreddits you moderate\"),\n\n    r_all_description = _(\"/r/all displays content from all of reddit, including subreddits you aren't subscribed to. Some subreddits have chosen to exclude themselves from /r/all.\"),\n    r_all_minus_description = _(\"Displaying content from /r/all of reddit, except the following subreddits:\"),\n    all_minus_gold_only = _('Filtering /r/all is a feature only available to [reddit gold](/gold/about) subscribers. Displaying unfiltered results from /r/all.'),\n)\n\nclass StringHandler(object):\n    \"\"\"Class for managing long translatable strings.  Allows accessing\n    of strings via both getitem and getattr.  In both cases, the\n    string is passed through the gettext _ function before being\n    returned.\"\"\"\n    def __init__(self, **sdict):\n        self.string_dict = sdict\n\n    def get(self, attr, default=None):\n        try:\n            return self[attr]\n        except KeyError:\n            return default\n\n    def __getitem__(self, attr):\n        try:\n            return self.__getattr__(attr)\n        except AttributeError:\n            raise KeyError\n\n    def __getattr__(self, attr):\n        rval = self.string_dict[attr]\n        if isinstance(rval, (str, unicode)):\n            return _(rval)\n        elif isinstance(rval, dict):\n            return StringHandler(**rval)\n        else:\n            raise AttributeError\n\n    def __iter__(self):\n        return iter(self.string_dict)\n\n    def keys(self):\n        return self.string_dict.keys()\n\nstrings = StringHandler(**string_dict)\n\n\ndef P_(x, y):\n    \"\"\"Convenience method for handling pluralizations.  This identity\n    function has been added to the list of keyword functions for babel\n    in setup.cfg so that the arguments are translated without having\n    to resort to ungettext and _ trickery.\"\"\"\n    return (x, y)\n\nclass PluralManager(object):\n    \"\"\"String handler for dealing with pluralizable forms.  plurals\n    are passed in in pairs (sing, pl) and can be accessed via\n    self.sing and self.pl.\n\n    Additionally, calling self.N_sing(n) (or self.N_pl(n)) (where\n    'sing' and 'pl' are placeholders for a (sing, pl) pairing) is\n    equivalent to ungettext(sing, pl, n)\n    \"\"\"\n    def __init__(self, plurals):\n        self.string_dict = {}\n        for s, p in plurals:\n            self.string_dict[s] = self.string_dict[p] = (s, p)\n\n    def __getattr__(self, attr):\n        to_func = False\n        if attr.startswith(\"N_\"):\n            attr = attr[2:]\n            to_func = True\n\n        attr = attr.replace(\"_\", \" \")\n        if to_func:\n            rval = self.string_dict[attr]\n            return lambda x: ungettext(rval[0], rval[1], x)\n        else:\n            rval = self.string_dict[attr]\n            n = 1 if attr == rval[0] else 5\n            return ungettext(rval[0], rval[1], n)\n\nplurals = PluralManager([P_(\"comment\",     \"comments\"),\n                         P_(\"point\",       \"points\"),\n\n                         # things\n                         P_(\"link\",        \"links\"),\n                         P_(\"comment\",     \"comments\"),\n                         P_(\"message\",     \"messages\"),\n                         P_(\"subreddit\",   \"subreddits\"),\n                         P_(\"creddit\",     \"creddits\"),\n\n                         # people\n                         P_(\"reader\",  \"readers\"),\n                         P_(\"subscriber\",  \"subscribers\"),\n                         P_(\"approved submitter\", \"approved submitters\"),\n                         P_(\"moderator\",   \"moderators\"),\n                         P_(\"user here now\",   \"users here now\"),\n\n                         # time words\n                         P_(\"milliseconds\",\"milliseconds\"),\n                         P_(\"second\",      \"seconds\"),\n                         P_(\"minute\",      \"minutes\"),\n                         P_(\"hour\",        \"hours\"),\n                         P_(\"day\",         \"days\"),\n                         P_(\"month\",       \"months\"),\n                         P_(\"year\",        \"years\"),\n])\n\n\nclass Score(object):\n    \"\"\"Convienience class for populating '10 points' in a traslatible\n    fasion, used primarily by the score() method in printable.html\"\"\"\n\n    # This used to pass through _() because allegedly Japanese needed different\n    # markup, but that doesn't appear to be the case anymore\n    PERSON_LABEL = ('<span class=\"number\">%(num)s</span>&#32;'\n                    '<span class=\"word\">%(persons)s</span>')\n\n    @staticmethod\n    def number_only(x):\n        return str(max(x, 0))\n\n    @staticmethod\n    def points(x):\n        return strings.points_label % dict(num=x,\n                                           point=plurals.N_points(x))\n\n    @staticmethod\n    def safepoints(x):\n        return Score.points(max(x, 0))\n\n    @staticmethod\n    def _people(x, label, prepend=''):\n        num = prepend + babel.numbers.format_number(x, c.locale)\n        return Score.PERSON_LABEL % \\\n            dict(num=num, persons=websafe(label(x)))\n\n    @staticmethod\n    def subscribers(x):\n        return Score._people(x, plurals.N_subscribers)\n\n    @staticmethod\n    def readers(x):\n        return Score._people(x, plurals.N_readers)\n\n    @staticmethod\n    def somethings(x, word):\n        p = plurals.string_dict[word]\n        f = lambda x: ungettext(p[0], p[1], x)\n        return strings.number_label % dict(num=x, thing=f(x))\n\n    @staticmethod\n    def users_here_now(x, prepend=''):\n        return Score._people(x, plurals.N_users_here_now, prepend=prepend)\n\n    @staticmethod\n    def none(x):\n        return \"\"\n\n\ndef fallback_trans(x):\n    \"\"\"For translating placeholder strings the user should never see\n    in raw form, such as 'funny 500 message'.  If the string does not\n    translate in the current language, falls back on the g.lang\n    translation that we've hopefully already provided\"\"\"\n    t = _(x)\n    if t == x:\n        l = get_lang()\n        set_lang(g.lang, graceful_fail = True)\n        t = _(x)\n        if l and l[0] != g.lang:\n            set_lang(l[0])\n    return t\n\n\ndef get_funny_translated_string(category, num=1):\n    strings = random.sample(funny_translatable_strings[category], num)\n    ret = [fallback_trans(string) for string in strings]\n    if len(ret) == 1:\n        return ret[0]\n    else:\n        return ret\n"
  },
  {
    "path": "r2/r2/lib/subreddit_search.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom r2.models import Subreddit\nfrom r2.lib.memoize import memoize\nfrom r2.lib.db.operators import desc\nfrom r2.lib import utils\nfrom r2.lib.db import tdb_cassandra\nfrom r2.lib.cache import CL_ONE\n\nclass SubredditsByPartialName(tdb_cassandra.View):\n    _use_db = True\n    _value_type = 'pickle'\n    _connection_pool = 'main'\n    _read_consistency_level = CL_ONE\n\ndef load_all_reddits():\n    query_cache = {}\n\n    q = Subreddit._query(Subreddit.c.type == 'public',\n                         Subreddit.c._spam == False,\n                         Subreddit.c._downs > 1,\n                         sort = (desc('_downs'), desc('_ups')),\n                         data = True)\n    for sr in utils.fetch_things2(q):\n        if sr.quarantine:\n            continue\n        name = sr.name.lower()\n        for i in xrange(len(name)):\n            prefix = name[:i + 1]\n            names = query_cache.setdefault(prefix, [])\n            if len(names) < 10:\n                names.append((sr.name, sr.over_18))\n\n    for name_prefix, subreddits in query_cache.iteritems():\n        SubredditsByPartialName._set_values(name_prefix, {'tups': subreddits})\n\ndef search_reddits(query, include_over_18=True):\n    query = str(query.lower())\n\n    try:\n        result = SubredditsByPartialName._byID(query)\n        return [name for (name, over_18) in getattr(result, 'tups', [])\n                if not over_18 or include_over_18]\n    except tdb_cassandra.NotFound:\n        return []\n\n@memoize('popular_searches', stale=True, time=3600)\ndef popular_searches(include_over_18=True):\n    top_reddits = Subreddit._query(Subreddit.c.type == 'public',\n                                   sort = desc('_downs'),\n                                   limit = 100,\n                                   data = True)\n    top_searches = {}\n    for sr in top_reddits:\n        if sr.quarantine:\n            continue\n        if sr.over_18 and not include_over_18:\n            continue\n        name = sr.name.lower()\n        for i in xrange(min(len(name), 3)):\n            query = name[:i + 1]\n            r = search_reddits(query, include_over_18)\n            top_searches[query] = r\n    return top_searches\n\n"
  },
  {
    "path": "r2/r2/lib/support_tickets.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom pylons import app_globals as g\n\nclass SupportTicketError(Exception):\n    pass\n\nclass SupportTickerNotFoundError(SupportTicketError):\n    pass\n\ndef create_support_ticket(subject,\n                          comment_body,\n                          comment_is_public=False,\n                          group=None,\n                          requester_email=None, \n                          product=None,\n                          ):\n    requester_id = None\n    if requester_email == 'contact@reddit.com':\n        requester_id = g.live_config['ticket_contact_user_id']\n        \n    custom_fields = []\n    if product:\n        custom_fields.append({\n            'id': g.live_config['ticket_user_fields']['Product'],\n            'value': product,\n        })\n        \n    return g.ticket_provider.create(\n        requester_id=requester_id,\n        subject=subject,\n        comment_body=comment_body,\n        comment_is_public=comment_is_public,\n        group_id=g.live_config['ticket_groups'][group],\n        custom_fields=custom_fields,\n    )\n\ndef get_support_ticket(ticket_id):\n    return g.ticket_provider.get(ticket_id)\n\ndef get_support_ticket_url(ticket_id):\n    return g.ticket_provider.build_ticket_url_from_id(ticket_id)\n\ndef update_support_ticket(ticket=None, ticket_id=None,\n                          status=None,\n                          comment_body=None,\n                          comment_is_public=False,\n                          tag_list=None,\n                          ):\n    if not ticket and not ticket_id:\n        raise SupportTickerNotFoundError(\n            'No ticket provided to update.'\n        )\n        \n    if not ticket:\n        ticket = get_support_ticket(ticket_id)\n    \n    return g.ticket_provider.update(\n            ticket=ticket,\n            status=status,\n            comment_body=comment_body,\n            comment_is_public=comment_is_public,\n            tag_list=tag_list,\n        )\n"
  },
  {
    "path": "r2/r2/lib/system_messages.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom pylons import request\nfrom pylons.i18n import _, N_\n\nfrom r2.models import Account, Message\nfrom r2.lib.db import queries\nfrom r2.lib.utils import blockquote_text\n\n\nuser_added_messages = {\n    \"moderator\": {\n        \"pm\": {\n            \"subject\": N_(\"you are a moderator\"),\n            \"msg\": N_(\"you have been added as a moderator to [%(title)s](%(url)s).\"),\n        },\n    },\n    \"moderator_invite\": {\n        \"pm\": {\n            \"subject\": N_(\"invitation to moderate %(url)s\"),\n            \"msg\": N_(\"**gadzooks! you are invited to become a moderator of [%(title)s](%(url)s)!**\\n\\n\"\n                      \"*to accept*, visit the [moderators page for %(url)s](%(url)s/about/moderators) and click \\\"accept\\\".\\n\\n\"\n                      \"*otherwise,* if you did not expect to receive this, you can simply ignore this invitation or report it.\"),\n        },\n        \"modmail\": {\n            \"subject\": N_(\"moderator invited\"),\n            \"msg\": N_(\"%(user)s has been invited by %(author)s to moderate %(url)s.\"),\n        },\n    },\n    \"accept_moderator_invite\": {\n        \"modmail\": {\n            \"subject\": N_(\"moderator added\"),\n            \"msg\": N_(\"%(user)s has accepted an invitation to become moderator of %(url)s.\"),\n        },\n    },\n    \"contributor\": {\n        \"pm\": {\n            \"subject\": N_(\"you are an approved submitter\"),\n            \"msg\": N_(\"you have been added as an approved submitter to [%(title)s](%(url)s).\"),\n        },\n    },\n    \"traffic\": {\n        \"pm\": {\n            \"subject\": N_(\"you can view traffic on a promoted link\"),\n            \"msg\": N_('you have been added to the list of users able to see [traffic for the sponsored link \"%(title)s\"](%(traffic_url)s).'),\n        },\n    },\n}\n\n\ndef notify_user_added(rel_type, author, user, target):\n    msgs = user_added_messages.get(rel_type)\n    if not msgs:\n        return\n\n    srname = target.path.rstrip(\"/\")\n    d = {\n        \"url\": srname,\n        \"title\": \"%s: %s\" % (srname, target.title),\n        \"author\": \"/u/\" + author.name,\n        \"user\": \"/u/\" + user.name,\n    }\n\n    if \"pm\" in msgs and author != user:\n        subject = msgs[\"pm\"][\"subject\"] % d\n        msg = msgs[\"pm\"][\"msg\"] % d\n\n        if rel_type in (\"moderator_invite\", \"contributor\"):\n            # send the message from the subreddit\n            item, inbox_rel = Message._new(\n                author, user, subject, msg, request.ip, sr=target, from_sr=True,\n                can_send_email=False)\n        else:\n            item, inbox_rel = Message._new(\n                author, user, subject, msg, request.ip, can_send_email=False)\n\n        queries.new_message(item, inbox_rel, update_modmail=False)\n\n    if \"modmail\" in msgs:\n        subject = msgs[\"modmail\"][\"subject\"] % d\n        msg = msgs[\"modmail\"][\"msg\"] % d\n\n        if rel_type == \"moderator_invite\":\n            modmail_author = Account.system_user()\n        else:\n            modmail_author = author\n\n        item, inbox_rel = Message._new(modmail_author, target, subject, msg,\n                                       request.ip, sr=target)\n        queries.new_message(item, inbox_rel)\n\n\ndef send_mod_removal_message(subreddit, mod, user):\n    sr_name = \"/r/\" + subreddit.name\n    u_name = \"/u/\" + user.name\n    subject = \"%(user)s has been removed as a moderator from %(subreddit)s\"\n    message = (\n        \"%(user)s: You have been removed as a moderator from %(subreddit)s.  \"\n        \"If you have a question regarding your removal, you can \"\n        \"contact the moderator team for %(subreddit)s by replying to this \"\n        \"message.\"\n    )\n    subject %= {\"subreddit\": sr_name, \"user\": u_name}\n    message %= {\"subreddit\": sr_name, \"user\": user.name}\n\n    item, inbox_rel = Message._new(\n        mod, user, subject, message, request.ip,\n        sr=subreddit,\n        from_sr=True,\n        can_send_email=False,\n    )\n    queries.new_message(item, inbox_rel, update_modmail=True)\n\n\ndef send_ban_message(subreddit, mod, user, note=None, days=None, new=True):\n    sr_name = \"/r/\" + subreddit.name\n    if days:\n        subject = \"You've been temporarily banned from participating in %(subreddit)s\"\n        message = (\"You have been temporarily banned from participating in \"\n            \"%(subreddit)s. This ban will last for %(duration)s days. \")\n    else:\n        subject = \"You've been banned from participating in %(subreddit)s\"\n        message = \"You have been banned from participating in %(subreddit)s. \"\n\n    message += (\"You can still view and subscribe to %(subreddit)s, but you \"\n                \"won't be able to post or comment.\")\n\n    if not new:\n        subject = \"Your ban from %(subreddit)s has changed\"\n\n    subject %= {\"subreddit\": sr_name}\n    message %= {\"subreddit\": sr_name, \"duration\": days}\n\n    if note:\n        message += \"\\n\\n\" + 'Note from the moderators:'\n        message += \"\\n\\n\" + blockquote_text(note)\n\n    message += \"\\n\\n\" + (\"If you have a question regarding your ban, you can \"\n        \"contact the moderator team for %(subreddit)s by replying to this \"\n        \"message.\") % {\"subreddit\": sr_name}\n\n    message += \"\\n\\n\" + (\"**Reminder from the Reddit staff**: If you use \"\n        \"another account to circumvent this subreddit ban, that will be \"\n        \"considered a violation of [the Content Policy](/help/contentpolicy#section_prohibited_behavior) \"\n        \"and can result in your account being [suspended](https://reddit.zendesk.com/hc/en-us/articles/205687686) \"\n        \"from the site as a whole.\")\n\n    item, inbox_rel = Message._new(\n        mod, user, subject, message, request.ip, sr=subreddit, from_sr=True,\n        can_send_email=False)\n    queries.new_message(item, inbox_rel, update_modmail=False)\n"
  },
  {
    "path": "r2/r2/lib/takedowns.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport json\nimport requests\n\nfrom pylons import app_globals as g\n\ndef post_takedown_notice_to_external_site(title, \n                          request_type, \n                          date_sent,\n                          date_received,\n                          source,\n                          action_taken,\n                          public_description,\n                          kind,\n                          original_url,\n                          infringing_urls,\n                          submitter_attributes,\n                          sender_name,\n                          sender_kind,\n                          sender_country,\n                          ):\n    \"\"\"This method publicly posts a copy of the takedown notice to \n    https://lumendatabase.org. Posting notices to Lumen is free, and needs to\n    be arranged by contacting their team. Read more about Lumen at\n    https://www.lumendatabase.org/pages/about\n    \"\"\"\n    # API documentation for lumendatabase.org found here:\n    # https://github.com/berkmancenter/lumendatabase/blob/master/doc/api_documentation.mkd\n    notice_json = {\n        'authentication_token': g.secrets['lumendatabase_org_api_key'],\n        'notice': {\n            'title': title,\n            'type': request_type,\n            'date_sent': date_sent.strftime('%Y-%m-%d'),\n            'date_received': date_received.strftime('%Y-%m-%d'),\n            'source': source,\n            'jurisdiction_list': 'US, CA',\n            'action_taken': action_taken,\n            'works_attributes': [\n                {\n                    'description': public_description,\n                    'kind': kind,\n                    'copyrighted_urls_attributes': [\n                        { 'url': original_url },\n                    ],\n                    'infringing_urls_attributes': infringing_urls\n                }\n            ],\n            'entity_notice_roles_attributes': [\n                {\n                    'name': 'recipient',\n                    'entity_attributes': submitter_attributes,\n                },\n                {\n                    'name': 'sender',\n                    'entity_attributes': {\n                        'name': sender_name,\n                        'kind': sender_kind,\n                        'address_line_1': '',\n                        'city': '',\n                        'state': '', \n                        'zip': '',\n                        'country_code': sender_country,\n                    }\n                }\n            ]\n        }\n    }\n        \n    timer = g.stats.get_timer('lumendatabase.takedown_create')\n    timer.start()\n    response = requests.post(\n        '%snotices' % g.live_config['lumendatabase_org_api_base_url'],\n        headers={\n            'Content-type': 'application/json',\n            'Accept': 'application/json',\n        },\n        data=json.dumps(notice_json)\n    )\n    timer.stop()\n    return response.headers['location']\n"
  },
  {
    "path": "r2/r2/lib/template_helpers.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport hmac\nimport hashlib\nimport urllib\n\nfrom r2.config import feature\nfrom r2.models import *\nfrom filters import (\n    _force_unicode,\n    _force_utf8,\n    conditional_websafe,\n    keep_space,\n    unsafe,\n    double_websafe,\n    websafe,\n)\nfrom r2.lib.cache_poisoning import make_poisoning_report_mac\nfrom r2.lib.utils import UrlParser, timeago, timesince, is_subdomain\n\nfrom r2.lib import hooks\nfrom r2.lib.static import static_mtime\nfrom r2.lib import js, tracking\n\nimport babel.numbers\nimport simplejson\nimport os.path\nfrom copy import copy\nimport random\nimport urlparse\nimport calendar\nimport math\nimport time\nimport pytz\n\nfrom pylons import request\nfrom pylons import tmpl_context as c\nfrom pylons import app_globals as g\nfrom pylons.i18n import _, ungettext\n\nstatic_text_extensions = {\n    '.js': 'js',\n    '.css': 'css',\n    '.less': 'css'\n}\ndef static(path, absolute=False, mangle_name=True):\n    \"\"\"\n    Simple static file maintainer which automatically paths and\n    versions files being served out of static.\n\n    In the case of JS and CSS where g.uncompressedJS is set, the\n    version of the file is set to be random to prevent caching and it\n    mangles the path to point to the uncompressed versions.\n    \"\"\"\n    dirname, filename = os.path.split(path)\n    extension = os.path.splitext(filename)[1]\n    is_text = extension in static_text_extensions\n    should_cache_bust = False\n\n    path_components = []\n    actual_filename = None if mangle_name else filename\n\n    # If building an absolute url, default to https because we like it and the\n    # static server should support it.\n    scheme = 'https' if absolute else None\n\n    if g.static_domain:\n        domain = g.static_domain\n    else:\n        path_components.append(c.site.static_path)\n\n        if g.uncompressedJS:\n            # unminified static files are in type-specific subdirectories\n            if not dirname and is_text:\n                path_components.append(static_text_extensions[extension])\n\n            should_cache_bust = True\n            actual_filename = filename\n\n        domain = g.domain if absolute else None\n\n    path_components.append(dirname)\n    if not actual_filename:\n        actual_filename = g.static_names.get(filename, filename)\n    path_components.append(actual_filename)\n\n    actual_path = os.path.join(*path_components)\n\n    query = None\n    if path and should_cache_bust:\n        file_id = static_mtime(actual_path) or random.randint(0, 1000000)\n        query = 'v=' + str(file_id)\n\n    return urlparse.urlunsplit((\n        scheme,\n        domain,\n        actual_path,\n        query,\n        None\n    ))\n\n\ndef make_url_protocol_relative(url):\n    if not url or url.startswith(\"//\"):\n        return url\n\n    scheme, netloc, path, query, fragment = urlparse.urlsplit(url)\n    return urlparse.urlunsplit((None, netloc, path, query, fragment))\n\n\ndef make_url_https(url):\n    if not url or url.startswith(\"https://\"):\n        return url\n\n    scheme, netloc, path, query, fragment = urlparse.urlsplit(url)\n    return urlparse.urlunsplit((\"https\", netloc, path, query, fragment))\n\n\ndef header_url(url, absolute=False):\n    if url == g.default_header_url:\n        return static(url, absolute=absolute)\n    elif absolute:\n        return make_url_https(url)\n    else:\n        return make_url_protocol_relative(url)\n\n\ndef js_config(extra_config=None):\n    logged = c.user_is_loggedin and c.user.name\n    user_id = c.user_is_loggedin and c.user._id\n    user_in_timeout = c.user_is_loggedin and c.user.in_timeout\n    gold = bool(logged and c.user.gold)\n    controller_name = request.environ['pylons.routes_dict']['controller']\n    action_name = request.environ['pylons.routes_dict']['action']\n    route_name = controller_name + '.' + action_name\n\n    cache_policy = \"loggedout_www\"\n    if c.user_is_loggedin:\n        cache_policy = \"loggedin_www_new\"\n\n    # Canary for detecting cache poisoning\n    poisoning_canary = None\n    poisoning_report_mac = None\n    if logged:\n        if \"pc\" in c.cookies and len(c.cookies[\"pc\"].value) == 2:\n            poisoning_canary = c.cookies[\"pc\"].value\n            poisoning_report_mac = make_poisoning_report_mac(\n                poisoner_canary=poisoning_canary,\n                poisoner_name=logged,\n                poisoner_id=user_id,\n                cache_policy=cache_policy,\n                source=\"web\",\n                route_name=route_name,\n            )\n\n    mac = hmac.new(g.secrets[\"action_name\"], route_name, hashlib.sha1)\n    verification = mac.hexdigest()\n    cur_subreddit = \"\"\n    cur_sr_fullname = \"\"\n    cur_listing = \"\"\n    listing_over_18 = False\n    pref_no_profanity = not logged or c.user.pref_no_profanity\n    pref_media_preview = c.user.pref_media_preview\n\n    if not feature.is_enabled(\"autoexpand_media_previews\"):\n        expando_preference = None\n    elif pref_media_preview == \"subreddit\":\n        expando_preference = \"subreddit_default\"\n    elif pref_media_preview == \"on\":\n        expando_preference = \"auto_expand\"\n    else:\n        expando_preference = \"do_not_expand\"\n\n    pref_beta = c.user.pref_beta\n    nsfw_media_acknowledged = logged and c.user.nsfw_media_acknowledged\n\n    if isinstance(c.site, Subreddit) and not c.default_sr:\n        cur_subreddit = c.site.name\n        cur_sr_fullname = c.site._fullname\n        cur_listing = cur_subreddit\n        listing_over_18 = c.site.over_18\n    elif isinstance(c.site, DefaultSR):\n        cur_listing = \"frontpage\"\n    elif isinstance(c.site, FakeSubreddit):\n        cur_listing = c.site.name\n\n    if g.debug:\n        events_collector_url = g.events_collector_test_url\n        events_collector_key = g.secrets['events_collector_test_js_key']\n        events_collector_secret = g.secrets['events_collector_test_js_secret']\n    else:\n        events_collector_url = g.events_collector_url\n        events_collector_key = g.secrets['events_collector_js_key']\n        events_collector_secret = g.secrets['events_collector_js_secret']\n\n    config = {\n        # is the user logged in?\n        \"logged\": logged,\n        # logged in user's id\n        \"user_id\": user_id,\n        # is user in timeout?\n        \"user_in_timeout\": user_in_timeout,\n        # the subreddit's name (for posts)\n        \"post_site\": cur_subreddit,\n        \"cur_site\": cur_sr_fullname,\n        \"cur_listing\": cur_listing,\n        # the user's voting hash\n        \"modhash\": c.modhash or False,\n        # the current rendering style\n        \"renderstyle\": c.render_style,\n\n        # they're welcome to try to override this in the DOM because we just\n        # disable the features server-side if applicable\n        'store_visits': gold and c.user.pref_store_visits,\n\n        # current domain\n        \"cur_domain\": get_domain(subreddit=False, no_www=True),\n        # where do ajax requests go?\n        \"ajax_domain\": get_domain(subreddit=False),\n        \"stats_domain\": g.stats_domain or '',\n        \"stats_sample_rate\": g.stats_sample_rate or 0,\n        \"extension\": c.extension,\n        \"https_endpoint\": is_subdomain(request.host, g.domain) and g.https_endpoint,\n        \"media_domain\": g.media_domain,\n        # does the client only want to communicate over HTTPS?\n        \"https_forced\": feature.is_enabled(\"force_https\"),\n        # debugging?\n        \"debug\": g.debug,\n        \"poisoning_canary\": poisoning_canary,\n        \"poisoning_report_mac\": poisoning_report_mac,\n        \"cache_policy\": cache_policy,\n        \"send_logs\": g.live_config[\"frontend_logging\"],\n        \"server_time\": math.floor(time.time()),\n        \"status_msg\": {\n          \"fetching\": _(\"fetching title...\"),\n          \"submitting\": _(\"submitting...\"),\n          \"loading\": _(\"loading...\")\n        },\n        \"is_fake\": isinstance(c.site, FakeSubreddit),\n        \"tracker_url\": \"\",  # overridden below if configured\n        \"adtracker_url\": g.adtracker_url,\n        \"clicktracker_url\": g.clicktracker_url,\n        \"uitracker_url\": g.uitracker_url,\n        \"eventtracker_url\": g.eventtracker_url,\n        \"anon_eventtracker_url\": g.anon_eventtracker_url,\n        \"events_collector_url\": events_collector_url,\n        \"events_collector_key\": events_collector_key,\n        \"events_collector_secret\": events_collector_secret,\n        \"feature_screenview_events\": feature.is_enabled('screenview_events'),\n        \"static_root\": static(''),\n        \"over_18\": bool(c.over18),\n        \"listing_over_18\": listing_over_18,\n        \"expando_preference\": expando_preference,\n        \"pref_no_profanity\": pref_no_profanity,\n        \"pref_beta\": pref_beta,\n        \"nsfw_media_acknowledged\": nsfw_media_acknowledged,\n        \"new_window\": logged and bool(c.user.pref_newwindow),\n        \"mweb_blacklist_expressions\": g.live_config['mweb_blacklist_expressions'],\n        \"gold\": gold,\n        \"has_subscribed\": logged and c.user.has_subscribed,\n        \"is_sponsor\": logged and c.user_is_sponsor,\n        \"pageInfo\": {\n          \"verification\": verification,\n          \"actionName\": route_name,\n        },\n        \"facebook_app_id\": g.live_config[\"facebook_app_id\"],\n        \"feature_new_report_dialog\": feature.is_enabled('new_report_dialog'),\n        \"email_verified\": logged and c.user.email and c.user.email_verified,\n    }\n\n    if g.tracker_url:\n        config[\"tracker_url\"] = tracking.get_pageview_pixel_url()\n\n    if g.uncompressedJS:\n        config[\"uncompressedJS\"] = True\n\n    if extra_config:\n        config.update(extra_config)\n\n    hooks.get_hook(\"js_config\").call(config=config)\n\n    return config\n\n\nclass JSPreload(js.DataSource):\n    def __init__(self, data=None):\n        if data is None:\n            data = {}\n        js.DataSource.__init__(self, \"r.preload.set({content})\", data)\n\n    def set(self, url, data):\n        self.data[url] = data\n\n    def set_wrapped(self, url, wrapped):\n        from r2.lib.pages.things import wrap_things\n        if not isinstance(wrapped, Wrapped):\n            wrapped = wrap_things(wrapped)[0]\n        self.data[url] = wrapped.render_nocache('api').finalize()\n\n    def use(self):\n        hooks.get_hook(\"js_preload.use\").call(js_preload=self)\n\n        if self.data:\n            return js.DataSource.use(self)\n        else:\n            return ''\n\n\ndef class_dict():\n    t_cls = [Link, Comment, Message, Subreddit]\n    l_cls = [Listing, OrganicListing]\n\n    classes  = [('%s: %s') % ('t'+ str(cl._type_id), cl.__name__ ) for cl in t_cls] \\\n             + [('%s: %s') % (cl.__name__, cl._js_cls) for cl in l_cls]\n\n    res = ', '.join(classes)\n    return unsafe('{ %s }' % res)\n\n\ndef comment_label(num_comments=None):\n    if not num_comments:\n        # generates \"comment\" the imperative verb\n        com_label = _(\"comment {verb}\")\n        com_cls = 'comments empty may-blank'\n    else:\n        # generates \"XX comments\" as a noun\n        com_label = ungettext(\"comment\", \"comments\", num_comments)\n        com_label = strings.number_label % dict(num=num_comments,\n                                                thing=com_label)\n        com_cls = 'comments may-blank'\n    return com_label, com_cls\n\ndef replace_render(listing, item, render_func):\n    def _replace_render(style = None, display = True):\n        \"\"\"\n        A helper function for listings to set uncachable attributes on a\n        rendered thing (item) to its proper display values for the current\n        context.\n        \"\"\"\n        style = style or c.render_style or 'html'\n        replacements = {}\n\n        if hasattr(item, 'child'):\n            if item.child:\n                replacements['childlisting'] = item.child.render(style=style)\n            else:\n                # Special case for when the comment tree wasn't built which\n                # occurs both in the inbox and spam page view of comments.\n                replacements['childlisting'] = None\n        else:\n            replacements['childlisting'] = ''\n\n        #only LinkListing has a show_nums attribute\n        if listing and hasattr(listing, \"show_nums\"):\n            if listing.show_nums and item.num > 0:\n                num = str(item.num)\n            else:\n                num = \"\"\n            replacements[\"num\"] = num\n\n        if getattr(item, \"rowstyle_cls\", None):\n            replacements[\"rowstyle\"] = item.rowstyle_cls\n\n        if hasattr(item, \"num_comments\"):\n            com_label, com_cls = comment_label(item.num_comments)\n            if style == \"compact\":\n                com_label = unicode(item.num_comments)\n            replacements['numcomments'] = com_label\n            replacements['commentcls'] = com_cls\n\n        if hasattr(item, \"num_children\"):\n            label = ungettext(\"child\", \"children\", item.num_children)\n            numchildren_text = strings.number_label % {'num': item.num_children,\n                                                       'thing': label}\n            replacements['numchildren_text'] = numchildren_text\n\n        replacements['display'] =  \"\" if display else \"style='display:none'\"\n\n        if hasattr(item, \"render_score\"):\n            # replace the score stub\n            (replacements['scoredislikes'],\n             replacements['scoreunvoted'],\n             replacements['scorelikes'])  = item.render_score\n\n        # compute the timesince here so we don't end up caching it\n        if hasattr(item, \"_date\"):\n            if hasattr(item, \"promoted\") and item.promoted is not None:\n                from r2.lib import promote\n                # promoted links are special in their date handling\n                replacements['timesince'] = \\\n                    simplified_timesince(item._date - promote.timezone_offset)\n            else:\n                replacements['timesince'] = simplified_timesince(item._date)\n\n        # compute the last edited time here so we don't end up caching it\n        if hasattr(item, \"editted\") and not isinstance(item.editted, bool):\n            replacements['lastedited'] = simplified_timesince(item.editted)\n\n        renderer = render_func or item.render\n        res = renderer(style = style, **replacements)\n\n        if isinstance(res, (str, unicode)):\n            rv = unsafe(res)\n            if g.debug:\n                for leftover in re.findall('<\\$>(.+?)(?:<|$)', rv):\n                    print \"replace_render didn't replace %s\" % leftover\n\n            return rv\n\n        return res\n\n    return _replace_render\n\ndef get_domain(cname=False, subreddit=True, no_www=False):\n    \"\"\"\n    returns the domain on the current subreddit, possibly including\n    the subreddit part of the path, suitable for insertion after an\n    \"http://\" and before a fullpath (i.e., something including the\n    first '/') in a template.  The domain is updated to include the\n    current port (request.port).  The effect of the arguments is:\n\n     * no_www: if the domain ends up being g.domain, the default\n       behavior is to prepend \"www.\" to the front of it (for akamai).\n       This flag will optionally disable it.\n\n     * cname: deprecated.\n\n     * subreddit: flags whether or not to append to the domain the\n       subreddit path (without the trailing path).\n\n    \"\"\"\n    # locally cache these lookups as this gets run in a loop in add_props\n    domain = g.domain\n\n    # c.domain_prefix is only set to non '' values, so we're safe to\n    # override if it is falsy. we need to check this here because this method\n    # might be getting called out of request, but c.domain_prefix is set in\n    # request in MinimalController.pre.\n    domain_prefix = c.domain_prefix or g.domain_prefix\n\n    site = c.site\n\n    if not no_www and domain_prefix:\n        domain = domain_prefix + \".\" + domain\n\n    if hasattr(request, \"port\") and request.port:\n        domain += \":\" + str(request.port)\n\n    if subreddit:\n        domain += site.path.rstrip('/')\n\n    return domain\n\n\ndef add_sr(\n        path, sr_path=True, nocname=False, force_hostname=False,\n        retain_extension=True, force_https=False,\n        force_extension=None):\n    \"\"\"\n    Given a path (which may be a full-fledged url or a relative path),\n    parses the path and updates it to include the subreddit path\n    according to the rules set by its arguments:\n\n     * sr_path: if a cname is not used for the domain, updates the\n       path to include c.site.path.\n\n     * nocname: deprecated.\n\n     * force_hostname: if True, force the url's hostname to be updated\n       even if it is already set in the path. If false, the path will still\n       have its domain updated if no hostname is specified in the url.\n\n     * retain_extension: if True, sets the extention according to\n       c.render_style.\n\n     * force_https: force the URL scheme to https\n\n    For caching purposes: note that this function uses:\n      c.render_style, c.site.name\n\n    \"\"\"\n    # don't do anything if it is just an anchor\n    if path.startswith(('#', 'javascript:')):\n        return path\n\n    u = UrlParser(path)\n    if sr_path:\n        u.path_add_subreddit(c.site)\n\n    if not u.hostname or force_hostname:\n        u.hostname = get_domain(subreddit=False)\n\n    if (c.secure and u.is_reddit_url()) or force_https:\n        u.scheme = \"https\"\n\n    if force_extension is not None:\n        u.set_extension(force_extension)\n    elif retain_extension:\n        if c.render_style == 'mobile':\n            u.set_extension('mobile')\n\n        elif c.render_style == 'compact':\n            u.set_extension('compact')\n\n    return u.unparse()\n\ndef join_urls(*urls):\n    \"\"\"joins a series of urls together without doubles slashes\"\"\"\n    if not urls:\n        return\n\n    url = urls[0]\n    for u in urls[1:]:\n        if not url.endswith('/'):\n            url += '/'\n        while u.startswith('/'):\n            u = utils.lstrips(u, '/')\n        url += u\n    return url\n\ndef style_line(button_width = None, bgcolor = \"\", bordercolor = \"\"):\n    style_line = ''\n    bordercolor = c.bordercolor or bordercolor\n    bgcolor     = c.bgcolor or bgcolor\n    if bgcolor:\n        style_line += \"background-color: #%s;\" % bgcolor\n    if bordercolor:\n        style_line += \"border: 1px solid #%s;\" % bordercolor\n    if button_width:\n        style_line += \"width: %spx;\" % button_width\n    return style_line\n\ndef choose_width(link, width):\n    if width:\n        return width - 5\n    else:\n        if hasattr(link, \"_ups\"):\n            return 100 + (10 * (len(str(link._ups - link._downs))))\n        else:\n            return 110\n\n# Appends to the list \"attrs\" a tuple of:\n# <priority (higher trumps lower), letter,\n#  css class, i18n'ed mouseover label, hyperlink (opt)>\ndef add_attr(attrs, kind, label=None, link=None, cssclass=None, symbol=None):\n    from r2.lib.template_helpers import static\n\n    symbol = symbol or kind\n\n    if kind == 'F':\n        priority = 1\n        cssclass = 'friend'\n        if not label:\n            label = _('friend')\n        if not link:\n            link = '/prefs/friends'\n    elif kind == 'S':\n        priority = 2\n        cssclass = 'submitter'\n        if not label:\n            label = _('submitter')\n        if not link:\n            raise ValueError (\"Need a link\")\n    elif kind == 'M':\n        priority = 3\n        cssclass = 'moderator'\n        if not label:\n            raise ValueError (\"Need a label\")\n        if not link:\n            raise ValueError (\"Need a link\")\n    elif kind == 'A':\n        priority = 4\n        cssclass = 'admin'\n        if not label:\n            label = _('reddit admin, speaking officially')\n    elif kind in ('X', '@'):\n        priority = 5\n        cssclass = 'gray'\n        if not label:\n            raise ValueError (\"Need a label\")\n    elif kind == 'V':\n        priority = 6\n        cssclass = 'green'\n        if not label:\n            raise ValueError (\"Need a label\")\n    elif kind == 'B':\n        priority = 7\n        cssclass = 'wrong'\n        if not label:\n            raise ValueError (\"Need a label\")\n    elif kind == 'special':\n        priority = 98\n    elif kind == \"cake\":\n        priority = 99\n        cssclass = \"cakeday\"\n        symbol = \"&#x1F370;\"\n        if not label:\n            raise ValueError (\"Need a label\")\n        if not link:\n            raise ValueError (\"Need a link\")\n    else:\n        raise ValueError (\"Got weird kind [%s]\" % kind)\n\n    attrs.append( (priority, symbol, cssclass, label, link) )\n\n\ndef add_admin_distinguish(distinguish_attribs_list):\n    add_attr(distinguish_attribs_list, 'A')\n\n\ndef add_moderator_distinguish(distinguish_attribs_list, subreddit):\n    link = '/r/%s/about/moderators' % subreddit.name\n    label = _('moderator of /r/%(reddit)s, speaking officially')\n    label %= {'reddit': subreddit.name}\n    add_attr(distinguish_attribs_list, 'M', label=label, link=link)\n\n\ndef add_friend_distinguish(distinguish_attribs_list, note=None):\n    if note:\n        label = u\"%s (%s)\" % (_(\"friend\"), _force_unicode(note))\n    else:\n        label = None\n    add_attr(distinguish_attribs_list, 'F', label)\n\n\ndef add_cakeday_distinguish(distinguish_attribs_list, user):\n    label = _(\"%(user)s just celebrated a reddit birthday!\")\n    label %= {\"user\": user.name}\n    link = \"/user/%s\" % user.name\n    add_attr(distinguish_attribs_list, kind=\"cake\", label=label, link=link)\n\n\ndef add_special_distinguish(distinguish_attribs_list, user):\n    args = user.special_distinguish()\n    args.pop('name')\n    if not args.get('kind'):\n        args['kind'] = 'special'\n    add_attr(distinguish_attribs_list, **args)\n\n\ndef add_submitter_distinguish(distinguish_attribs_list, link, subreddit):\n    permalink = link.make_permalink(subreddit)\n    add_attr(distinguish_attribs_list, 'S', link=permalink)\n\n\ndef search_url(query, subreddit, restrict_sr=\"off\", sort=None, recent=None, ref=None):\n    import urllib\n    query = _force_utf8(query)\n    url_query = {\"q\": query}\n    if ref:\n        url_query[\"ref\"] = ref\n    if restrict_sr:\n        url_query[\"restrict_sr\"] = restrict_sr\n    if sort:\n        url_query[\"sort\"] = sort\n    if recent:\n        url_query[\"t\"] = recent\n    path = \"/r/%s/search?\" % subreddit if subreddit else \"/search?\"\n    path += urllib.urlencode(url_query)\n    return path\n\n\ndef format_number(number, locale=None):\n    if not locale:\n        locale = c.locale or g.locale\n\n    return babel.numbers.format_number(number, locale=locale)\n\n\ndef format_percent(ratio, locale=None):\n    if not locale:\n        locale = c.locale or g.locale\n\n    return babel.numbers.format_percent(ratio, locale=locale)\n\n\ndef html_datetime(date):\n    # Strip off the microsecond to appease the HTML5 gods, since\n    # datetime.isoformat() returns too long of a microsecond value.\n    # http://www.whatwg.org/specs/web-apps/current-work/multipage/common-microsyntaxes.html#times\n    return date.astimezone(pytz.UTC).replace(microsecond=0).isoformat()\n\n\ndef js_timestamp(date):\n    return '%d' % (calendar.timegm(date.timetuple()) * 1000)\n\n\ndef simplified_timesince(date, include_tense=True):\n    if date > timeago(\"1 minute\"):\n        return _(\"just now\")\n\n    since = timesince(date)\n    if include_tense:\n        return _(\"%s ago\") % since\n    else:\n        return since\n\n\ndef display_link_karma(karma):\n    if not c.user_is_admin:\n        return max(karma, g.link_karma_display_floor)\n    return karma\n\n\ndef display_comment_karma(karma):\n    if not c.user_is_admin:\n        return max(karma, g.comment_karma_display_floor)\n    return karma\n\n\ndef format_html(format_string, *args, **kwargs):\n    \"\"\"\n    Similar to str % foo, but passes all arguments through conditional_websafe,\n    and calls 'unsafe' on the result. This function should be used instead\n    of str.format or % interpolation to build up small HTML fragments.\n\n    Example:\n\n      format_html(\"Are you %s? %s\", name, unsafe(checkbox_html))\n    \"\"\"\n    if args and kwargs:\n        raise ValueError(\"Can't specify both positional and keyword args\")\n    args_safe = tuple(map(conditional_websafe, args))\n    kwargs_gen = ((k, conditional_websafe(v)) for (k, v) in kwargs.iteritems())\n    kwargs_safe = dict(kwargs_gen)\n\n    format_args = args_safe or kwargs_safe\n    return unsafe(format_string % format_args)\n\n\ndef _ws(text, keep_spaces=False):\n    \"\"\"Helper function to get HTML escaped output from gettext\"\"\"\n    if keep_spaces:\n        return keep_space(_(text))\n    else:\n        return websafe(_(text))\n\n\ndef _wsf(format, keep_spaces=True, *args, **kwargs):\n    \"\"\"\n    format_html, but with an escaped, translated string as the format str\n\n    Sometimes trusted HTML needs to be included in a translatable string,\n    but we don't trust translators to write HTML themselves.\n\n    Example:\n\n      _wsf(\"Are you %(name)s? %(box)s\", name=name, box=unsafe(checkbox_html))\n    \"\"\"\n    format_trans = _ws(format, keep_spaces)\n    return format_html(format_trans, *args, **kwargs)\n\n\ndef get_linkflair_css_classes(thing, prefix=\"linkflair-\", on_class=\"has-linkflair\", off_class=\"no-linkflair\"):\n    has_linkflair =  thing.flair_text or thing.flair_css_class\n    show_linkflair = c.user.pref_show_link_flair\n    if has_linkflair and show_linkflair:\n        if thing.flair_css_class:\n            flair_css_classes = thing.flair_css_class.split()\n            prefixed_css_classes = [\"%s%s\" % (prefix, css_class) for css_class in flair_css_classes]\n            on_class = \"%s %s\" % (on_class, ' '.join(prefixed_css_classes))\n        return on_class\n    else:\n        return off_class\n\n\ndef update_query(base_url, **kw):\n    parsed = UrlParser(base_url)\n    parsed.update_query(**kw)\n    return parsed.unparse()\n"
  },
  {
    "path": "r2/r2/lib/totp.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\n\"\"\"An implementation of the RFC-6238 Time-Based One Time Password algorithm.\"\"\"\n\nimport time\nimport hmac\nimport base64\nimport struct\nimport hashlib\n\n\nPERIOD = 30\n\n\ndef make_hotp(secret, counter):\n    \"\"\"Generate an RFC-4226 HMAC-Based One Time Password.\"\"\"\n    key = base64.b32decode(secret)\n\n    # compute the HMAC digest of the counter with the secret key\n    counter_encoded = struct.pack(\">q\", counter)\n    hmac_result = hmac.HMAC(key, counter_encoded, hashlib.sha1).digest()\n\n    # do HOTP dynamic truncation (see RFC4226 5.3)\n    offset = ord(hmac_result[-1]) & 0x0f\n    truncated_hash = hmac_result[offset:offset + 4]\n    code_bits, = struct.unpack(\">L\", truncated_hash)\n    htop = (code_bits & 0x7fffffff) % 1000000\n\n    # pad it out as necessary\n    return \"%06d\" % htop\n\n\ndef make_totp(secret, skew=0, timestamp=None):\n    \"\"\"Generate an RFC-6238 Time-Based One Time Password.\"\"\"\n    timestamp = timestamp or time.time()\n    counter = timestamp // PERIOD\n    return make_hotp(secret, counter - skew)\n\n\ndef generate_secret():\n    \"\"\"Make a secret key suitable for use in TOTP.\"\"\"\n    from Crypto.Random import get_random_bytes\n    bytes = get_random_bytes(20)\n    encoded = base64.b32encode(bytes)\n    return encoded\n\n\nif __name__ == \"__main__\":\n    # based on RFC-6238 Appendix B (trimmed to six-digit OTPs)\n    secret = base64.b32encode(\"12345678901234567890\")\n    assert make_totp(secret, timestamp=59) == \"287082\"\n    assert make_totp(secret, timestamp=1111111109) == \"081804\"\n    assert make_totp(secret, timestamp=1111111111) == \"050471\"\n    assert make_totp(secret, timestamp=1234567890) == \"005924\"\n    assert make_totp(secret, timestamp=2000000000) == \"279037\"\n    assert make_totp(secret, timestamp=20000000000) == \"353130\"\n"
  },
  {
    "path": "r2/r2/lib/tracking.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom Crypto.Cipher import AES\nfrom Crypto.Random import get_random_bytes\nimport base64\nimport hashlib\nimport urllib\n\nfrom pylons import request\nfrom pylons import tmpl_context as c\nfrom pylons import app_globals as g\n\nfrom r2.lib.filters import _force_utf8\n\n\nKEY_SIZE = 16  # AES-128\nSALT_SIZE = KEY_SIZE * 2  # backwards compatibility\n\n\ndef _pad_message(text):\n    \"\"\"Return `text` padded out to a multiple of block_size bytes.\n\n    This uses the PKCS7 padding algorithm. The pad-bytes have a value of N\n    where N is the number of bytes of padding added. If the input string is\n    already a multiple of the block size, it will be padded with one full extra\n    block to make an unambiguous output string.\n\n    \"\"\"\n    block_size = AES.block_size\n    padding_size = (block_size - len(text) % block_size) or block_size\n    padding = chr(padding_size) * padding_size\n    return text + padding\n\n\ndef _unpad_message(text):\n    \"\"\"Return `text` with padding removed. The inverse of _pad_message.\"\"\"\n    if not text:\n        return \"\"\n\n    padding_size = ord(text[-1])\n    if padding_size > AES.block_size:\n        return \"\"\n\n    unpadded, padding = text[:-padding_size], text[-padding_size:]\n    if any(ord(x) != padding_size for x in padding):\n        return \"\"\n\n    return unpadded\n\n\ndef _make_cipher(initialization_vector, secret):\n    \"\"\"Return a block cipher object for use in `encrypt` and `decrypt`.\"\"\"\n    return AES.new(secret[:KEY_SIZE], AES.MODE_CBC,\n                   initialization_vector[:AES.block_size])\n\n\ndef encrypt(plaintext):\n    \"\"\"Return the message `plaintext` encrypted.\n\n    The encrypted message will have its salt prepended and will be URL encoded\n    to make it suitable for use in URLs and Cookies.\n\n    NOTE: this function is here for backwards compatibility. Please do not\n    use it for new code.\n\n    \"\"\"\n\n    salt = _make_salt()\n    return _encrypt(salt, plaintext, g.tracking_secret)\n\n\ndef _make_salt():\n    # we want SALT_SIZE letters of salt text, but we're generating random bytes\n    # so we'll calculate how many bytes we need to get SALT_SIZE characters of\n    # base64 output. because of padding, this only works for SALT_SIZE % 4 == 0\n    assert SALT_SIZE % 4 == 0\n    salt_byte_count = (SALT_SIZE / 4) * 3\n    salt_bytes = get_random_bytes(salt_byte_count)\n    return base64.b64encode(salt_bytes)\n\n\ndef _encrypt(salt, plaintext, secret):\n    cipher = _make_cipher(salt, secret)\n\n    padded = _pad_message(plaintext)\n    ciphertext = cipher.encrypt(padded)\n    encoded = base64.b64encode(ciphertext)\n\n    return urllib.quote_plus(salt + encoded, safe=\"\")\n\n\ndef decrypt(encrypted):\n    \"\"\"Decrypt `encrypted` and return the plaintext.\n\n    NOTE: like `encrypt` above, please do not use this function for new code.\n\n    \"\"\"\n\n    return _decrypt(encrypted, g.tracking_secret)\n\n\ndef _decrypt(encrypted, secret):\n    encrypted = urllib.unquote_plus(encrypted)\n    salt, encoded = encrypted[:SALT_SIZE], encrypted[SALT_SIZE:]\n    ciphertext = base64.b64decode(encoded)\n    cipher = _make_cipher(salt, secret)\n    padded = cipher.decrypt(ciphertext)\n    return _unpad_message(padded)\n\n\ndef get_site():\n    \"\"\"Return the name of the current \"site\" (subreddit).\"\"\"\n    return c.site.analytics_name if c.site else \"\"\n\n\ndef get_srpath():\n    \"\"\"Return the srpath of the current request.\n\n    The srpath is Subredditname-Action. e.g. sophiepotamus-GET_listing.\n\n    \"\"\"\n    name = get_site()\n    action = None\n    if c.render_style in (\"mobile\", \"compact\"):\n        action = c.render_style\n    else:\n        action = request.environ['pylons.routes_dict'].get('action')\n\n    if not action:\n        return name\n    return '-'.join((name, action))\n\n\ndef _get_encrypted_user_slug():\n    \"\"\"Return an encrypted string containing context info.\"\"\"\n    # The cname value (formerly c.cname) is expected by The traffic system.\n    cname = False\n    data = [\n        c.user._id36 if c.user_is_loggedin else \"\",\n        get_srpath(),\n        c.lang or \"\",\n        cname,\n    ]\n    return encrypt(\"|\".join(_force_utf8(s) for s in data))\n\n\ndef get_pageview_pixel_url():\n    \"\"\"Return a URL to use for tracking pageviews for the current request.\"\"\"\n    return g.tracker_url + \"?v=\" + _get_encrypted_user_slug()\n\n\ndef get_impression_pixel_url(codename):\n    \"\"\"Return a URL to use for tracking impressions of the given advert.\"\"\"\n    # TODO: use HMAC here\n    mac = codename + hashlib.sha1(codename + g.tracking_secret).hexdigest()\n    v_param = \"?v=%s&\" % _get_encrypted_user_slug()\n    hash_and_id_params = urllib.urlencode({\"hash\": mac,\n                                           \"id\": codename,})\n    return g.adframetracker_url + v_param + hash_and_id_params\n"
  },
  {
    "path": "r2/r2/lib/traffic/__init__.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom traffic import *\n"
  },
  {
    "path": "r2/r2/lib/traffic/emr_traffic.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom copy import copy\nfrom pylons import app_globals as g\nimport os\nfrom time import time, sleep\n\nfrom boto.emr.step import InstallPigStep, PigStep\nfrom boto.emr.bootstrap_action import BootstrapAction\n\nfrom r2.lib.emr_helpers import (\n    EmrException,\n    EmrJob,\n    get_live_clusters,\n    get_step_state,\n    LIVE_STATES,\n    COMPLETED,\n    PENDING,\n    NOTFOUND,\n)\n\n\nclass TrafficBase(EmrJob):\n\n    \"\"\"Base class for all traffic jobs.\n\n    Includes required bootstrap actions and setup steps.\n\n    \"\"\"\n\n    BOOTSTRAP_NAME = 'traffic binaries'\n    BOOTSTRAP_SCRIPT = os.path.join(g.TRAFFIC_SRC_DIR, 'traffic_bootstrap.sh')\n    _defaults = dict(master_instance_type='m1.small',\n                     slave_instance_type='c3.2xlarge', num_slaves=1,\n                     job_flow_role=g.emr_trafic_job_flow_role,\n                     service_role=g.emr_traffic_service_role,\n                     tags=g.emr_traffic_tags,\n                )\n\n    def __init__(self, emr_connection, jobflow_name, steps=None, **kw):\n        combined_kw = copy(self._defaults)\n        combined_kw.update(kw)\n        bootstrap_actions = self._bootstrap_actions()\n        setup_steps = self._setup_steps()\n        steps = steps or []\n        EmrJob.__init__(self, emr_connection, jobflow_name,\n                        bootstrap_actions=bootstrap_actions,\n                        setup_steps=setup_steps,\n                        steps=steps,\n                        **combined_kw)\n\n    @classmethod\n    def _bootstrap_actions(cls):\n        name = cls.BOOTSTRAP_NAME\n        path = cls.BOOTSTRAP_SCRIPT\n        bootstrap_action_args = [g.TRAFFIC_SRC_DIR, g.tracking_secret]\n        bootstrap = BootstrapAction(name, path, bootstrap_action_args)\n        return [bootstrap]\n\n    @classmethod\n    def _setup_steps(self):\n        return [InstallPigStep()]\n\n\nclass PigProcessHour(PigStep):\n    STEP_NAME = 'pig process hour'\n    PIG_FILE = os.path.join(g.TRAFFIC_SRC_DIR, 'mr_process_hour.pig')\n\n    def __init__(self, log_path, output_path):\n        self.log_path = log_path\n        self.output_path = output_path\n        self.name = '%s (%s)' % (self.STEP_NAME, self.log_path)\n        pig_args = ['-p', 'OUTPUT=%s' % self.output_path,\n                    '-p', 'LOGFILE=%s' % self.log_path]\n        PigStep.__init__(self, self.name, self.PIG_FILE, pig_args=pig_args)\n\n\nclass PigAggregate(PigStep):\n    STEP_NAME = 'pig aggregate'\n    PIG_FILE = os.path.join(g.TRAFFIC_SRC_DIR, 'mr_aggregate.pig')\n\n    def __init__(self, input_path, output_path):\n        self.input_path = input_path\n        self.output_path = output_path\n        self.name = '%s (%s)' % (self.STEP_NAME, self.input_path)\n        pig_args = ['-p', 'INPUT=%s' % self.input_path,\n                    '-p', 'OUTPUT=%s' % self.output_path]\n        PigStep.__init__(self, self.name, self.PIG_FILE, pig_args=pig_args)\n\n\nclass PigCoalesce(PigStep):\n    STEP_NAME = 'pig coalesce'\n    PIG_FILE = os.path.join(g.TRAFFIC_SRC_DIR, 'mr_coalesce.pig')\n\n    def __init__(self, input_path, output_path):\n        self.input_path = input_path\n        self.output_path = output_path\n        self.name = '%s (%s)' % (self.STEP_NAME, self.input_path)\n        pig_args = ['-p', 'INPUT=%s' % self.input_path,\n                    '-p', 'OUTPUT=%s' % self.output_path]\n        PigStep.__init__(self, self.name, self.PIG_FILE, pig_args=pig_args)\n\n\ndef _add_step(emr_connection, step, jobflow_name, **jobflow_kw):\n    \"\"\"Add step to a running jobflow.\n\n    Append the step onto a jobflow with the specified name if one exists,\n    otherwise create a new jobflow and run it. Returns the jobflowid.\n    NOTE: jobflow_kw will be used to configure the jobflow ONLY if a new\n    jobflow is created.\n\n    \"\"\"\n\n    running = get_live_clusters(emr_connection)\n\n    for cluster in running:\n        # NOTE: the existing cluster's bootstrap actions aren't checked so we\n        # are assuming that any cluster with the correct name is compatible\n        # with our new step\n        if cluster.name == jobflow_name:\n            jobflowid = cluster.id\n            emr_connection.add_jobflow_steps(jobflowid, step)\n            print 'Added %s to jobflow %s' % (step.name, jobflowid)\n            break\n    else:\n        base = TrafficBase(emr_connection, jobflow_name, steps=[step],\n                           **jobflow_kw)\n        base.run()\n        jobflowid = base.jobflowid\n        print 'Added %s to new jobflow %s' % (step.name, jobflowid)\n\n    return jobflowid\n\n\ndef _wait_for_step(emr_connection, step, jobflowid, sleeptime):\n    \"\"\"Poll EMR and wait for a step to finish.\"\"\"\n    sleep(180)\n    start = time()\n    step_state = get_step_state(emr_connection, jobflowid, step.name,\n                                update=True)\n    while step_state in LIVE_STATES + [PENDING]:\n        sleep(sleeptime)\n        step_state = get_step_state(emr_connection, jobflowid, step.name)\n    end = time()\n    print '%s took %0.2fs (exit: %s)' % (step.name, end - start, step_state)\n    return step_state\n\n\ndef run_traffic_step(emr_connection, step, jobflow_name,\n                     wait=True, sleeptime=60, retries=1, **jobflow_kw):\n    \"\"\"Run a traffic processing step.\n\n    Helper function to force all steps to be executed by the same jobflow\n    (jobflow_name). Also can hold until complete (wait) and retry on\n    failure (retries).\n\n    \"\"\"\n\n    jobflowid = _add_step(emr_connection, step, jobflow_name, **jobflow_kw)\n\n    if not wait:\n        return\n\n    attempts = 1\n    exit_state = _wait_for_step(emr_connection, step, jobflowid, sleeptime)\n    while attempts <= retries and exit_state != COMPLETED:\n        jobflowid = _add_step(emr_connection, step, jobflow_name, **jobflow_kw)\n        exit_state = _wait_for_step(emr_connection, step, jobflowid, sleeptime)\n        attempts += 1\n\n    if exit_state != COMPLETED:\n        msg = '%s failed (exit: %s)' % (step.name, exit_state)\n        if retries:\n            msg += 'retried %s times' % retries\n        raise EmrException(msg)\n\n\ndef extract_hour(emr_connection, jobflow_name, log_path, output_path,\n                 **jobflow_kw):\n    step = PigProcessHour(log_path, output_path)\n    run_traffic_step(emr_connection, step, jobflow_name, **jobflow_kw)\n\n\ndef aggregate_interval(emr_connection, jobflow_name, input_path, output_path,\n                       **jobflow_kw):\n    step = PigAggregate(input_path, output_path)\n    run_traffic_step(emr_connection, step, jobflow_name, **jobflow_kw)\n\n\ndef coalesce_interval(emr_connection, jobflow_name, input_path, output_path,\n                      **jobflow_kw):\n    step = PigCoalesce(input_path, output_path)\n    run_traffic_step(emr_connection, step, jobflow_name, **jobflow_kw)\n"
  },
  {
    "path": "r2/r2/lib/traffic/traffic.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport datetime\nimport calendar\nimport os\nfrom time import sleep\nimport urllib\n\nfrom boto.s3.connection import S3Connection\nfrom boto.emr.connection import EmrConnection\nfrom boto.exception import S3ResponseError\nfrom pylons import app_globals as g\nfrom sqlalchemy.exc import DataError\n\nfrom r2.lib.emr_helpers import (EmrException, terminate_jobflow,\n    modify_slave_count)\nfrom r2.lib.s3_helpers import get_text_from_s3, s3_key_exists, copy_to_s3\nfrom r2.lib.traffic.emr_traffic import (extract_hour, aggregate_interval,\n        coalesce_interval)\nfrom r2.lib.utils import tup\nfrom r2.models.traffic import (SitewidePageviews, PageviewsBySubreddit,\n        PageviewsBySubredditAndPath, PageviewsByLanguage,\n        ClickthroughsByCodename, TargetedClickthroughsByCodename,\n        AdImpressionsByCodename, TargetedImpressionsByCodename)\n\n\nRAW_LOG_DIR = g.RAW_LOG_DIR\nPROCESSED_DIR = g.PROCESSED_DIR\nAGGREGATE_DIR = g.AGGREGATE_DIR\nAWS_LOG_DIR = g.AWS_LOG_DIR\n\n# the \"or None\" business is so that a blank string becomes None to cause boto\n# to look for credentials in other places.\ns3_connection = S3Connection(g.TRAFFIC_ACCESS_KEY or None,\n                             g.TRAFFIC_SECRET_KEY or None)\nemr_connection = EmrConnection(g.TRAFFIC_ACCESS_KEY or None,\n                               g.TRAFFIC_SECRET_KEY or None)\n\ntraffic_categories = (SitewidePageviews, PageviewsBySubreddit,\n                      PageviewsBySubredditAndPath, PageviewsByLanguage,\n                      ClickthroughsByCodename, TargetedClickthroughsByCodename,\n                      AdImpressionsByCodename, TargetedImpressionsByCodename)\n\ntraffic_subdirectories = {\n    SitewidePageviews: 'sitewide',\n    PageviewsBySubreddit: 'subreddit',\n    PageviewsBySubredditAndPath: 'srpath',\n    PageviewsByLanguage: 'lang',\n    ClickthroughsByCodename: 'clicks',\n    TargetedClickthroughsByCodename: 'clicks_targeted',\n    AdImpressionsByCodename: 'thing',\n    TargetedImpressionsByCodename: 'thingtarget',\n}\n\n\ndef _get_processed_path(basedir, interval, category_cls, filename):\n    return os.path.join(basedir, interval,\n                        traffic_subdirectories[category_cls], filename)\n\n\ndef get_aggregate(interval, category_cls):\n    \"\"\"Return the aggregate output file from S3.\"\"\"\n    part = 0\n    data = {}\n\n    while True:\n        path = _get_processed_path(AGGREGATE_DIR, interval, category_cls,\n                                   'part-r-%05d' % part)\n        if not s3_key_exists(s3_connection, path):\n            break\n\n        # Sometimes S3 doesn't let us read immediately after key is written\n        for i in xrange(5):\n            try:\n                txt = get_text_from_s3(s3_connection, path)\n            except S3ResponseError as e:\n                print 'S3ResponseError on %s, retrying' % path\n                sleep(300)\n            else:\n                break\n        else:\n            print 'Could not retrieve %s' % path\n            raise e\n\n        for line in txt.splitlines():\n            tuples = line.rstrip('\\n').split('\\t')\n            group, uniques, pageviews = tuples[:-2], tuples[-2], tuples[-1]\n            if len(group) > 1:\n                group = tuple(group)\n            else:\n                group = group[0]\n            data[group] = (int(uniques), int(pageviews))\n\n        part += 1\n\n    if not data:\n        raise ValueError(\"No data for %s/%s\" % (interval,\n                                                category_cls.__name__))\n\n    return data\n\n\ndef report_interval(interval, background=True):\n    if background:\n        from multiprocessing import Process\n        p = Process(target=_report_interval, args=(interval,))\n        p.start()\n    else:\n        _report_interval(interval)\n\n\ndef _name_to_kw(category_cls, name):\n    \"\"\"Get the keywords needed to build an instance of traffic data.\"\"\"\n    def target_split(name):\n        \"\"\"Split a name that contains multiple words.\n\n        Name is (link,campaign-subreddit) where link and campaign are\n        thing fullnames. campaign and subreddit are each optional, so\n        the string could look like any of these:\n        (t3_bh,t8_ab-pics), (t3_bh,t8_ab), (t3_bh,-pics), (t3_bh,)\n        Also check for the old format (t3_by, pics)\n\n        \"\"\"\n\n        link_codename, target_info = name\n        campaign_codename = None\n        if not target_info:\n            subreddit = ''\n        elif target_info.find('-') != -1:\n            campaign_codename, subreddit = target_info.split('-', 1)\n        elif target_info.find('_') != -1:\n            campaign_codename = target_info\n            subreddit = ''\n        else:\n            subreddit = target_info\n        return {'codename': campaign_codename or link_codename,\n                'subreddit': subreddit}\n\n    d = {SitewidePageviews: lambda n: {},\n         PageviewsBySubreddit: lambda n: {'subreddit': n},\n         PageviewsBySubredditAndPath: lambda n: {'srpath': n},\n         PageviewsByLanguage: lambda n: {'lang': n},\n         ClickthroughsByCodename: lambda n: {'codename': name},\n         AdImpressionsByCodename: lambda n: {'codename': name},\n         TargetedClickthroughsByCodename: target_split,\n         TargetedImpressionsByCodename: target_split}\n    return d[category_cls](name)\n\n\ndef _report_interval(interval):\n    \"\"\"Read aggregated traffic from S3 and write to postgres.\"\"\"\n    from sqlalchemy.orm import scoped_session, sessionmaker\n    from r2.models.traffic import engine\n    Session = scoped_session(sessionmaker(bind=engine))\n\n    # determine interval_type from YYYY-MM[-DD][-HH]\n    pieces = interval.split('-')\n    pieces = [int(i) for i in pieces]\n    if len(pieces) == 4:\n        interval_type = 'hour'\n    elif len(pieces) == 3:\n        interval_type = 'day'\n        pieces.append(0)\n    elif len(pieces) == 2:\n        interval_type = 'month'\n        pieces.append(1)\n        pieces.append(0)\n    else:\n        raise\n\n    pg_interval = \"%04d-%02d-%02d %02d:00:00\" % tuple(pieces)\n    print 'reporting interval %s (%s)' % (pg_interval, interval_type)\n\n    # Read aggregates and write to traffic db\n    for category_cls in traffic_categories:\n        now = datetime.datetime.now()\n        print '*** %s - %s - %s' % (category_cls.__name__, interval, now)\n        data = get_aggregate(interval, category_cls)\n        len_data = len(data)\n        step = max(len_data / 5, 100)\n        for i, (name, (uniques, pageviews)) in enumerate(data.iteritems()):\n            try:\n                for n in tup(name):\n                    unicode(n)\n            except UnicodeDecodeError:\n                print '%s - %s - %s - %s' % (category_cls.__name__, name,\n                                             uniques, pageviews)\n                continue\n\n            if i % step == 0:\n                now = datetime.datetime.now()\n                print '%s - %s - %s/%s - %s' % (interval, category_cls.__name__,\n                                                i, len_data, now)\n\n            kw = {'date': pg_interval, 'interval': interval_type,\n                  'unique_count': uniques, 'pageview_count': pageviews}\n            kw.update(_name_to_kw(category_cls, name))\n            r = category_cls(**kw)\n\n            try:\n                Session.merge(r)\n                Session.commit()\n            except DataError:\n                Session.rollback()\n                continue\n\n    Session.remove()\n    now = datetime.datetime.now()\n    print 'finished reporting %s (%s) - %s' % (pg_interval, interval_type, now)\n\n\ndef process_pixel_log(log_path, fast=False):\n    \"\"\"Process an hourly pixel log file.\n\n    Extract data from raw hourly log and aggregate it and report it. Also\n    depending on the specific date and options, aggregate and report the day\n    and month. Setting fast=True is appropriate for backfilling as it\n    eliminates reduntant steps.\n\n    \"\"\"\n\n    if log_path.endswith('/*'):\n        log_dir = log_path[:-len('/*')]\n        date_fields = os.path.basename(log_dir).split('.', 1)[0].split('-')\n    else:\n        date_fields = os.path.basename(log_path).split('.', 1)[0].split('-')\n    year, month, day, hour = (int(i) for i in date_fields)\n    hour_date = '%s-%02d-%02d-%02d' % (year, month, day, hour)\n    day_date = '%s-%02d-%02d' % (year, month, day)\n    month_date = '%s-%02d' % (year, month)\n\n    # All logs from this day use the same jobflow\n    jobflow_name = 'Traffic Processing %s' % day_date\n\n    output_path = os.path.join(PROCESSED_DIR, 'hour', hour_date)\n    extract_hour(emr_connection, jobflow_name, log_path, output_path,\n                 log_uri=AWS_LOG_DIR)\n\n    input_path = os.path.join(PROCESSED_DIR, 'hour', hour_date)\n    output_path = os.path.join(AGGREGATE_DIR, hour_date)\n    aggregate_interval(emr_connection, jobflow_name, input_path, output_path,\n                       log_uri=AWS_LOG_DIR)\n    if not fast:\n        report_interval(hour_date)\n\n    if hour == 23 or (not fast and (hour == 0 or hour % 4 == 3)):\n        # Don't aggregate and report day on every hour\n        input_path = os.path.join(PROCESSED_DIR, 'hour', '%s-*' % day_date)\n        output_path = os.path.join(AGGREGATE_DIR, day_date)\n        aggregate_interval(emr_connection, jobflow_name, input_path,\n                           output_path, log_uri=AWS_LOG_DIR)\n        if not fast:\n            report_interval(day_date)\n\n    if hour == 23:\n        # Special tasks for final hour of the day\n        input_path = os.path.join(PROCESSED_DIR, 'hour', '%s-*' % day_date)\n        output_path = os.path.join(PROCESSED_DIR, 'day', day_date)\n        coalesce_interval(emr_connection, jobflow_name, input_path,\n                          output_path, log_uri=AWS_LOG_DIR)\n        terminate_jobflow(emr_connection, jobflow_name)\n\n        if not fast:\n            aggregate_month(month_date)\n            report_interval(month_date)\n\n\ndef aggregate_month(month_date):\n    jobflow_name = 'Traffic Processing %s' % month_date\n    input_path = os.path.join(PROCESSED_DIR, 'day', '%s-*' % month_date)\n    output_path = os.path.join(AGGREGATE_DIR, month_date)\n    aggregate_interval(emr_connection, jobflow_name, input_path, output_path,\n                       log_uri=AWS_LOG_DIR, slave_instance_type='m2.2xlarge')\n    terminate_jobflow(emr_connection, jobflow_name)\n\n\ndef process_month_hours(month_date, start_hour=0, days=None):\n    \"\"\"Process hourly logs from entire month.\n\n    Complete monthly backfill requires running [verify_month_inputs,]\n    process_month_hours, aggregate_month, [verify_month_outputs,] and\n    report_entire_month.\n\n    \"\"\"\n\n    year, month = month_date.split('-')\n    year, month = int(year), int(month)\n\n    days = days or xrange(1, calendar.monthrange(year, month)[1] + 1)\n    hours = xrange(start_hour, 24)\n\n    for day in days:\n        for hour in hours:\n            hour_date = '%04d-%02d-%02d-%02d' % (year, month, day, hour)\n            log_path = os.path.join(RAW_LOG_DIR, '%s.log.gz' % hour_date)\n            if not s3_key_exists(s3_connection, log_path):\n                log_path = os.path.join(RAW_LOG_DIR, '%s.log.bz2' % hour_date)\n                if not s3_key_exists(s3_connection, log_path):\n                    print 'Missing log for %s' % hour_date\n                    continue\n            print 'Processing %s' % log_path\n            process_pixel_log(log_path, fast=True)\n        hours = xrange(24)\n\n\ndef report_entire_month(month_date, start_hour=0, start_day=1):\n    \"\"\"Report all hours and days from month.\"\"\"\n    year, month = month_date.split('-')\n    year, month = int(year), int(month)\n    hours = xrange(start_hour, 24)\n\n    for day in xrange(start_day, calendar.monthrange(year, month)[1] + 1):\n        for hour in hours:\n            hour_date = '%04d-%02d-%02d-%02d' % (year, month, day, hour)\n            try:\n                report_interval(hour_date, background=False)\n            except ValueError:\n                print 'Failed for %s' % hour_date\n                continue\n        hours = xrange(24)\n        day_date = '%04d-%02d-%02d' % (year, month, day)\n        try:\n            report_interval(day_date, background=False)\n        except ValueError:\n            print 'Failed for %s' % day_date\n            continue\n    report_interval(month_date, background=False)\n\n\ndef verify_month_outputs(month_date):\n    \"\"\"Check existance of all hour, day, month aggregates for month_date.\"\"\"\n    year, month = month_date.split('-')\n    year, month = int(year), int(month)\n    missing = []\n\n    for day in xrange(1, calendar.monthrange(year, month)[1] + 1):\n        for hour in xrange(24):\n            hour_date = '%04d-%02d-%02d-%02d' % (year, month, day, hour)\n            for category_cls in traffic_categories:\n                for d in [AGGREGATE_DIR, os.path.join(PROCESSED_DIR, 'hour')]:\n                    path = _get_processed_path(d, hour_date, category_cls,\n                                               'part-r-00000')\n                    if not s3_key_exists(s3_connection, path):\n                        missing.append(hour_date)\n\n        day_date = '%04d-%02d-%02d' % (year, month, day)\n        for category_cls in traffic_categories:\n            for d in [AGGREGATE_DIR, os.path.join(PROCESSED_DIR, 'day')]:\n                path = _get_processed_path(d, day_date, category_cls,\n                                           'part-r-00000')\n                if not s3_key_exists(s3_connection, path):\n                    missing.append(day_date)\n\n    month_date = '%04d-%02d' % (year, month)\n    for c in traffic_categories:\n        path = _get_processed_path(AGGREGATE_DIR, month_date, category_cls,\n                                   'part-r-00000')\n        if not s3_key_exists(s3_connection, path):\n            missing.append(month_date)\n\n    for d in sorted(list(set(missing))):\n        print d\n\n\ndef verify_month_inputs(month_date):\n    \"\"\"Check existance of all hourly traffic logs for month_date.\"\"\"\n    year, month = month_date.split('-')\n    year, month = int(year), int(month)\n    missing = []\n\n    for day in xrange(1, calendar.monthrange(year, month)[1] + 1):\n        for hour in xrange(24):\n            hour_date = '%04d-%02d-%02d-%02d' % (year, month, day, hour)\n            log_path = os.path.join(RAW_LOG_DIR, '%s.log.gz' % hour_date)\n            if not s3_key_exists(s3_connection, log_path):\n                log_path = os.path.join(RAW_LOG_DIR, '%s.log.bz2' % hour_date)\n                if not s3_key_exists(s3_connection, log_path):\n                    missing.append(hour_date)\n\n    for d in missing:\n        print d\n\n\ndef process_hour(hour_date):\n    \"\"\"Process hour_date's traffic.\n\n    Can't fire at the very start of an hour because it takes time to bzip and\n    upload the file to S3. Check the bucket for the file and sleep if it\n    doesn't exist.\n\n    \"\"\"\n\n    SLEEPTIME = 180\n\n    log_dir = os.path.join(RAW_LOG_DIR, hour_date)\n    files_missing = [os.path.join(log_dir, '%s.log.bz2' % h)\n                     for h in g.TRAFFIC_LOG_HOSTS]\n    files_missing = [f for f in files_missing\n                       if not s3_key_exists(s3_connection, f)]\n\n    while files_missing:\n        print 'Missing log(s) %s, sleeping' % files_missing\n        sleep(SLEEPTIME)\n        files_missing = [f for f in files_missing\n                           if not s3_key_exists(s3_connection, f)]\n    process_pixel_log(os.path.join(log_dir, '*'))\n"
  },
  {
    "path": "r2/r2/lib/translation.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport json\nimport os\nimport token\nimport tokenize\n\nfrom babel.messages.extract import extract_javascript\nfrom cStringIO import StringIO\n\nimport babel.messages.frontend\nimport babel.messages.pofile\nimport pylons\n\nfrom pylons.i18n.translation import translation, LanguageError, NullTranslations\n\ntry:\n    import reddit_i18n\nexcept ImportError:\n    I18N_PATH = ''\nelse:\n    I18N_PATH = os.path.dirname(reddit_i18n.__file__)\n\n# Different from the default lang (as defined in the ini file)\n# Source language is what is in the source code\nSOURCE_LANG = 'en'\n\n\ndef _get_translator(lang, graceful_fail=False, **kwargs):\n    \"\"\"Utility method to get a valid translator object from a language name\"\"\"\n    from pylons import config\n\n    if not isinstance(lang, list):\n        lang = [lang]\n    try:\n        translator = translation(config['pylons.package'], I18N_PATH,\n                                 languages=lang, **kwargs)\n    except IOError, ioe:\n        if graceful_fail:\n            translator = NullTranslations()\n        else:\n            raise LanguageError('IOError: %s' % ioe)\n    translator.pylons_lang = lang\n    return translator\n\n\ndef set_lang(lang, graceful_fail=False, fallback_lang=None, **kwargs):\n    \"\"\"Set the i18n language used\"\"\"\n    registry = pylons.request.environ['paste.registry']\n    if not lang:\n        registry.replace(pylons.translator, NullTranslations())\n    else:\n        translator = _get_translator(lang, graceful_fail = graceful_fail, **kwargs)\n        base_lang, is_dialect, dialect = lang.partition(\"-\")\n        if is_dialect:\n            try:\n                base_translator = _get_translator(base_lang)\n            except LanguageError:\n                pass\n            else:\n                translator.add_fallback(base_translator)\n        if fallback_lang:\n            fallback_translator = _get_translator(fallback_lang,\n                                                  graceful_fail=True)\n            translator.add_fallback(fallback_translator)\n        registry.replace(pylons.translator, translator)\n\n\ndef load_data(lang_path, domain, extension='data'):\n    filename = os.path.join(lang_path, domain + '.' + extension)\n    with open(filename) as datafile:\n        data = json.load(datafile)\n    return data\n\n\ndef iter_langs(base_path=I18N_PATH):\n    if base_path:\n        # sorted() so that get_active_langs can check completion\n        # data on \"base\" languages of a dialect\n        for lang in sorted(os.listdir(base_path)):\n            full_path = os.path.join(base_path, lang, 'LC_MESSAGES')\n            if os.path.isdir(full_path):\n                yield lang, full_path\n\n\ndef get_active_langs(config, path=I18N_PATH, default_lang='en'):\n    trans = []\n    trans_name = {}\n    completions = {}\n    domain = config['pylons.package']\n\n    for lang, lang_path in iter_langs(path):\n        data = load_data(lang_path, domain)\n        name = [data['name'], '']\n        if data['_is_enabled'] and lang != default_lang:\n            trans.append(lang)\n            completion = float(data['num_completed']) / float(data['num_total'])\n            completions[lang] = completion\n            # This relies on iter_langs hitting the base_lang first\n            base_lang, is_dialect, dialect = lang.partition(\"-\")\n            if is_dialect:\n                if base_lang == SOURCE_LANG:\n                    # Source language has to be 100% complete\n                    base_completion = 1.0\n                else:\n                    base_completion = completions.get(base_lang, 0)\n                completion = max(completion, base_completion)\n            if completion < .5:\n                name[1] = ' (*)'\n        trans_name[lang] = name\n    trans.sort()\n    # insert the default language at the top of the list\n    trans.insert(0, default_lang)\n    if default_lang not in trans_name:\n        trans_name[default_lang] = default_lang\n    return trans, trans_name\n\n\ndef get_catalog(lang):\n    \"\"\"Return a Catalog object given the language code.\"\"\"\n    path = os.path.join(I18N_PATH, lang, \"LC_MESSAGES\", \"r2.po\")\n    with open(path, \"r\") as f:\n        return babel.messages.pofile.read_po(f)\n\n\ndef validate_plural_forms(plural_forms_str):\n    \"\"\"Ensure the gettext plural forms expression supplied is valid.\"\"\"\n\n    # this code is taken from the python stdlib; gettext.py:c2py\n    tokens = tokenize.generate_tokens(StringIO(plural_forms_str).readline)\n\n    try:\n        danger = [x for x in tokens if x[0] == token.NAME and x[1] != 'n']\n    except tokenize.TokenError:\n        raise ValueError, \\\n              'plural forms expression error, maybe unbalanced parenthesis'\n    else:\n        if danger:\n            raise ValueError, 'plural forms expression could be dangerous'\n\n\ndef extract_javascript_msgids(source):\n    \"\"\"Return message ids of translateable strings in JS source.\"\"\"\n\n    extracted = extract_javascript(\n        fileobj=StringIO(source),\n        keywords={\n            \"_\": None,\n            \"P_\": (1, 2),\n            \"N_\": None,\n            \"NP_\": (1, 2),\n        },\n        comment_tags={},\n        options={},\n    )\n\n    return [msg_id for line, func, msg_id, comments in extracted]\n"
  },
  {
    "path": "r2/r2/lib/trending.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport re\n\nfrom pylons import app_globals as g\n\nfrom r2.models.keyvalue import NamedGlobals\nfrom r2.models import NotFound, Subreddit, Thing\n\n_SUBREDDIT_RE = re.compile(r'/r/(\\w+)')\nTRENDING_SUBREDDITS_KEY = 'trending_subreddits'\n\n\ndef get_trending_subreddits():\n    return NamedGlobals.get(TRENDING_SUBREDDITS_KEY, None)\n\n\ndef update_trending_subreddits():\n    try:\n        trending_sr = Subreddit._by_name(g.config['trending_sr'])\n    except NotFound:\n        g.log.info(\"Unknown trending subreddit %r or trending_sr config \"\n                   \"not set. Not updating.\", g.config['trending_sr'])\n        return\n\n    link = _get_newest_link(trending_sr)\n    if not link:\n        g.log.info(\"Unable to find active link in subreddit %r. Not updating.\",\n                   g.config['trending_sr'])\n        return\n\n    subreddit_names = _SUBREDDIT_RE.findall(link.title)\n    trending_data = {\n        'subreddit_names': subreddit_names,\n        'permalink': link.make_permalink(trending_sr),\n        'link_id': link._id,\n    }\n    NamedGlobals.set(TRENDING_SUBREDDITS_KEY, trending_data)\n    g.log.debug(\"Trending subreddit data set to %r\", trending_data)\n\n\ndef _get_newest_link(sr):\n    for fullname in sr.get_links('new', 'all'):\n        link = Thing._by_fullname(fullname, data=True)\n        if not link._spam and not link._deleted:\n            return link\n\n    return None\n"
  },
  {
    "path": "r2/r2/lib/unicode.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\n\ndef _force_unicode(text):\n    if text == None:\n        return u''\n\n    if isinstance(text, unicode):\n        return text\n\n    try:\n        text = unicode(text, 'utf-8')\n    except UnicodeDecodeError:\n        text = unicode(text, 'latin1')\n    except TypeError:\n        text = unicode(text)\n    return text\n\n\ndef _force_utf8(text):\n    return str(_force_unicode(text).encode('utf8'))\n"
  },
  {
    "path": "r2/r2/lib/utils/__init__.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom utils import *\nfrom http_utils import *\nfrom reddit_agent_parser import Agent\n"
  },
  {
    "path": "r2/r2/lib/utils/_utils.pyx",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport re\nfrom datetime import datetime, timedelta\nfrom pylons.i18n import ungettext, _\nimport math\n\ncpdef str to_base(long q, str alphabet):\n    if q < 0: raise ValueError, \"must supply a positive integer\"\n    cdef long l\n    cdef long r\n    l = len(alphabet)\n    converted = []\n    while q != 0:\n        q, r = divmod(q, l)\n        converted.insert(0, alphabet[r])\n    return \"\".join(converted) or '0'\n\ncpdef str to36(long q):\n    return to_base(q, '0123456789abcdefghijklmnopqrstuvwxyz')\n\ndef tup(item, ret_is_single=False):\n    \"\"\"Forces casting of item to a tuple (for a list) or generates a\n    single element tuple (for anything else)\"\"\"\n    #return true for iterables, except for strings, which is what we want\n    if hasattr(item, '__iter__'):\n        return (item, False) if ret_is_single else item\n    else:\n        return ((item,), True) if ret_is_single else (item,)\n\ncdef _strips(str direction, text, remove):\n    if direction == 'l': \n        if text.startswith(remove): \n            return text[len(remove):]\n    elif direction == 'r':\n        if text.endswith(remove):   \n            return text[:-len(remove)]\n    else: \n        raise ValueError, \"Direction needs to be r or l.\"\n    return text\n\ncpdef rstrips(text, remove):\n    \"\"\"\n    removes the string `remove` from the right of `text`\n\n        >>> rstrips(\"foobar\", \"bar\")\n        'foo'\n    \n    \"\"\"\n    return _strips('r', text, remove)\n\ncpdef lstrips(text, remove):\n    \"\"\"\n    removes the string `remove` from the left of `text`\n    \n        >>> lstrips(\"foobar\", \"foo\")\n        'bar'\n    \n    \"\"\"\n    return _strips('l', text, remove)\n\ndef strips(text, remove):\n    \"\"\"removes the string `remove` from the both sides of `text`\n\n        >>> strips(\"foobarfoo\", \"foo\")\n        'bar'\n    \n    \"\"\"\n    return rstrips(lstrips(text, remove), remove)\n\nESCAPE = re.compile(r'[\\x00-\\x19\\\\\"\\b\\f\\n\\r\\t]')\nESCAPE_ASCII = re.compile(r'([\\\\\"/]|[^\\ -~])')\nESCAPE_DCT = {\n    # escape all forward slashes to prevent </script> attack\n    '/': '\\\\/',\n    '\\\\': '\\\\\\\\',\n    '\"': '\\\\\"',\n    '\\b': '\\\\b',\n    '\\f': '\\\\f',\n    '\\n': '\\\\n',\n    '\\r': '\\\\r',\n    '\\t': '\\\\t',\n    }\ndef _string2js_replace(match):\n    return ESCAPE_DCT[match.group(0)]\ndef string2js(s):\n    \"\"\"adapted from http://svn.red-bean.com/bob/simplejson/trunk/simplejson/encoder.py\"\"\"\n    for i in range(20):\n        ESCAPE_DCT.setdefault(chr(i), '\\\\u%04x' % (i,))\n\n    return '\"' + ESCAPE.sub(_string2js_replace, s) + '\"'\n\ndef timeago(str interval):\n    \"\"\"Returns a datetime object corresponding to time 'interval' in\n    the past.  Interval is of the same form as is returned by\n    timetext(), i.e., '10 seconds'.  The interval must be passed in in\n    English (i.e., untranslated) and the format is\n\n    [num] second|minute|hour|day|week|month|year(s)\n    \"\"\"\n    from pylons import app_globals as g\n    return datetime.now(g.tz) - timeinterval_fromstr(interval)\n\ndef timefromnow(interval):\n    \"The opposite of timeago\"\n    from pylons import app_globals as g\n    return datetime.now(g.tz) + timeinterval_fromstr(interval)\n\ndef timedelta_by_name(interval):\n    return timeinterval_fromstr('1 ' + interval)\n\ncdef dict timeintervald = dict(second = 1,\n                               minute = 60,\n                               hour   = 60 * 60,\n                               day    = 60 * 60 * 24,\n                               week   = 60 * 60 * 24 * 7,\n                               month  = 60 * 60 * 24 * 30,\n                               year   = 60 * 60 * 24 * 365)\ncpdef timeinterval_fromstr(str interval):\n    \"Used by timeago and timefromnow to generate timedeltas from friendly text\"\n    parts = interval.strip().split(' ')\n    if len(parts) == 1:\n        num = 1\n        period = parts[0]\n    elif len(parts) == 2:\n        num, period = parts\n        num = int(num)\n    else:\n        raise ValueError, 'format should be ([num] second|minute|etc)'\n    period = rstrips(period, 's')\n\n    d = timeintervald[period]\n    delta = num * d\n    return timedelta(0, delta)\n\ncdef class TimeText(object):\n    __slots__ = ('single', 'plural')\n    cdef str single, plural\n\n    def __init__(self, single, plural):\n        self.single = single\n        self.plural = plural\n\n    def __call__(self, n):\n        return ungettext(self.single, self.plural, n)\n\ntimechunks = (\n    (60 * 60 * 24 * 365, TimeText('year', 'years')),\n    (60 * 60 * 24 * 30,  TimeText('month', 'months')),\n    (60 * 60 * 24,       TimeText('day', 'days')),\n    (60 * 60,            TimeText('hour', 'hours')),\n    (60,                 TimeText('minute', 'minutes')),\n    (1,                  TimeText('second', 'seconds'))\n    )\ncdef timetext(delta, precision=None, bare=True):\n    \"\"\"\n    Takes a datetime object, returns the time between then and now\n    as a nicely formatted string, e.g \"10 minutes\"\n    Adapted from django which was adapted from\n    http://blog.natbat.co.uk/archive/2003/Jun/14/time_since\n    \"\"\"\n    delta = max(delta, timedelta(0))\n    cdef long since = delta.days * 24 * 60 * 60 + delta.seconds\n    cdef long count\n    cdef int i, seconds, n\n    cdef TimeText name, name2\n\n    for i, (seconds, name) in enumerate(timechunks):\n        count = since // seconds\n        if count != 0:\n            break\n\n    from r2.lib.strings import strings\n    if count == 0 and delta.seconds == 0 and delta != timedelta(0):\n        n = delta.microseconds // 1000\n        s = strings.time_label % dict(num=n,\n                                      time=ungettext(\"millisecond\",\n                                                     \"milliseconds\", n))\n    else:\n        s = strings.time_label % dict(num=count, time=name(int(count)))\n        if precision:\n            j = 0\n            while True:\n                j += 1\n                since -= seconds * count\n                if i + j >= len(timechunks):\n                    break\n                if timechunks[i + j][0] < precision:\n                    break\n                seconds, name = timechunks[i + j]\n                count = since // seconds\n                if count != 0:\n                    s += ', %d %s' % (count, name(count))\n\n    if not bare:\n        s += ' ' + _('ago')\n\n    return s\n\ndef timesince(d, precision=None):\n    from pylons import app_globals as g\n    return timetext(datetime.now(g.tz) - d, precision)\n\ndef timeuntil(d, precision=None):\n    from pylons import app_globals as g\n    return timetext(d - datetime.now(g.tz), precision)\n\ncpdef dict keymap(keys, callfn, mapfn = None, str prefix=''):\n    \"\"\"map a set of keys before a get_multi to return a dict using the\n       original unmapped keys\"\"\"\n\n    cdef dict km = {}\n    cdef dict res # the result back from the callfn\n    cdef dict ret = {} # our return value\n\n    km = map_keys(keys, mapfn, prefix)\n    res = callfn(km.keys())\n    ret = unmap_keys(res, km)\n\n    return ret\n\ncdef map_keys(keys, mapfn, str prefix):\n    if (mapfn and prefix) or (not mapfn and not prefix):\n        raise ValueError(\"Set one of mapfn or prefix\")\n\n    cdef dict km = {}\n    if mapfn:\n        for key in keys:\n            km[mapfn(key)] = key\n    else:\n        for key in keys:\n            km[prefix + str(key)] = key\n    return km\n\ncdef unmap_keys(mapped_keys, km):\n    cdef dict ret = {}\n    for key, value in mapped_keys.iteritems():\n        ret[km[key]] = value\n    return ret\n\ndef prefix_keys(keys, str prefix, callfn):\n    if len(prefix):\n        return keymap(keys, callfn, prefix=prefix)\n    else:\n        return callfn(keys)\n\ndef flatten(list lists):\n    \"\"\"[[1,2], [3], [4,5,6]] -> [1,2,3,4,5,6]\"\"\"\n    cdef list ret = []\n    cdef list l\n    \n    for l in lists:\n        ret.extend(l)\n\n    return ret\n\ncdef list _l(l):\n    \"\"\"Return a listified version of l, just returning l if it's\n       already listified\"\"\"\n    if isinstance(l, list):\n        return l\n    else:\n        return list(l)\n\ndef get_after(list fullnames, fullname, int num, reverse=False):\n    cdef int i\n\n    if reverse:\n        fullnames = _l(reversed(fullnames))\n\n    if not fullname:\n        return fullnames[:num]\n\n    for i, item in enumerate(fullnames):\n        if item == fullname:\n            return fullnames[i+1:i+num+1]\n\n    return fullnames[:num]\n"
  },
  {
    "path": "r2/r2/lib/utils/comment_tree_utils.pyx",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\n\ndef get_tree_details(dict tree):\n    cdef:\n        list cids = []\n        dict depth = {}\n        dict parents = {}\n        list child_ids\n\n    for parent_id in sorted(tree):\n        child_ids = tree[parent_id]\n\n        cids.extend(child_ids)\n\n        parents.update({child_id: parent_id for child_id in child_ids})\n\n        child_depth = depth.get(parent_id, -1) + 1\n        depth.update({child_id: child_depth for child_id in child_ids})\n\n    return cids, depth, parents\n\n\ndef calc_num_children(dict tree):\n    cdef:\n        dict num_children = {}\n        list child_ids\n\n    for parent_id in sorted(tree, reverse=True):\n        if parent_id is None:\n            continue\n\n        child_ids = tree[parent_id]\n        num_children[parent_id] = sum(\n            1 + num_children.get(child_id, 0) for child_id in tree[parent_id])\n    return num_children\n"
  },
  {
    "path": "r2/r2/lib/utils/feature_utils.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom pylons import app_globals as g\nfrom pylons import request\nfrom pylons import tmpl_context as c\n\nfrom r2.config import feature\n\n\ndef is_tracking_link_enabled(link=None, element_name=None):\n    if c.user_is_admin:\n        return False  # Less noise while admin mode enabled, esp. in usernotes\n    if element_name and element_name.startswith('trending_sr'):\n        return True\n    if feature.is_enabled('utm_comment_links'):\n        return True\n    return False\n\n"
  },
  {
    "path": "r2/r2/lib/utils/http_utils.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport pytz\nfrom datetime import datetime\n\nDATE_RFC822 = '%a, %d %b %Y %H:%M:%S %Z'\nDATE_RFC850 = '%A, %d-%b-%y %H:%M:%S %Z'\nDATE_ANSI = '%a %b %d %H:%M:%S %Y'\n\n\ndef read_http_date(date_str):\n    try:\n        date = datetime.strptime(date_str, DATE_RFC822)\n    except ValueError:\n        try:\n            date = datetime.strptime(date_str, DATE_RFC850)\n        except ValueError:\n            try:\n                date = datetime.strptime(date_str, DATE_ANSI)\n            except ValueError:\n                return None\n    date = date.replace(tzinfo = pytz.timezone('GMT'))\n    return date\n\n\ndef http_date_str(date):\n    date = date.astimezone(pytz.timezone('GMT'))\n    return date.strftime(DATE_RFC822)\n\n\ndef get_requests_resp_json(resp):\n    \"\"\"Kludge so we can use `requests` versions below or above 1.x\"\"\"\n    if callable(resp.json):\n        return resp.json()\n    return resp.json\n"
  },
  {
    "path": "r2/r2/lib/utils/reddit_agent_parser.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2016 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom httpagentparser import (\n    AndroidBrowser,\n    Browser,\n    detect as de,\n    DetectorBase,\n    detectorshub)\nimport re\nfrom inspect import isclass\n\n\ndef register_detector(cls):\n    \"\"\"Collector of all the reddit detectors.\"\"\"\n    detectorshub.register(cls())\n    return cls\n\n\nclass RedditDetectorBase(DetectorBase):\n    agent_string = None\n    version_string = '(\\.?\\d+)*'\n\n    def __init__(self):\n        if self.agent_string:\n            self.agent_regex = re.compile(self.agent_string.format(\n                look_for=self.look_for, version_string=self.version_string))\n        else:\n            self.agent_regex = None\n\n        self.version_regex = re.compile('(?P<version>{})'.format(\n            self.version_string))\n\n    def getVersion(self, agent, word):\n        match = None\n        if self.agent_regex:\n            match = self.agent_regex.search(agent)\n\n        if not match:\n            match = self.version_regex.search(agent)\n\n        if match and 'version' in match.groupdict().keys():\n            return match.group('version')\n\n    def detect(self, agent, result):\n        detected = super(RedditDetectorBase, self).detect(agent, result)\n\n        if not detected or not self.agent_regex:\n            return detected\n\n        match = self.agent_regex.search(agent)\n        groups = match.groupdict()\n        platform_name = groups.get('platform')\n        version = groups.get('pversion')\n\n        if platform_name:\n            platform = {}\n            platform['name'] = platform_name\n            if version:\n                platform['version'] = version\n            result['platform'] = platform\n\n        if self.is_app:\n            result['app_name'] = result['browser']['name']\n\n        return True\n\n\nclass RedditBrowser(RedditDetectorBase, Browser):\n    \"\"\"Base class for all reddit specific browsers.\"\"\"\n    # is_app denotes a client that is a native mobile application, but not a\n    # browser.\n    is_app = False\n\n\n@register_detector\nclass RedditIsFunDetector(RedditBrowser):\n    is_app = True\n    look_for = 'reddit is fun'\n    name = 'reddit is fun'\n    agent_string = ('^{look_for} \\((?P<platform>.*?)\\) '\n                    '(?P<version>{version_string})$')\n    override = [AndroidBrowser]\n\n\n@register_detector\nclass RedditAndroidDetector(RedditBrowser):\n    is_app = True\n    look_for = 'RedditAndroid'\n    name = 'Reddit: The Official App'\n    agent_string = '{look_for} (?P<version>{version_string})$'\n\n\n@register_detector\nclass RedditIOSDetector(RedditBrowser):\n    is_app = True\n    look_for = 'Reddit'\n    name = 'reddit iOS'\n    skip_if_found = ['Android']\n    agent_string = (\n        '{look_for}\\/Version (?P<version>{version_string})\\/Build '\n        '(?P<b_number>\\d+)\\/(?P<platform>.*?) Version '\n        '(?P<pversion>{version_string}) \\(Build .*?\\)')\n\n\n@register_detector\nclass AlienBlueDetector(RedditBrowser):\n    is_app = True\n    look_for = 'AlienBlue'\n    name = 'Alien Blue'\n    agent_string = (\n        '{look_for}\\/(?P<version>{version_string}) CFNetwork\\/'\n        '{version_string} (?P<platform>.*?)\\/(?P<pversion>{version_string})')\n\n\n@register_detector\nclass RelayForRedditDetector(RedditBrowser):\n    is_app = True\n    look_for = 'Relay by /u/DBrady'\n    name = 'relay for reddit'\n    agent_string = '{look_for} v(?P<version>{version_string})'\n\n\n@register_detector\nclass RedditSyncDetector(RedditBrowser):\n    is_app = True\n    look_for = 'reddit_sync'\n    name = 'Sync for reddit'\n    agent_string = (\n        'android:com\\.laurencedawson\\.{look_for}'\n        ':v(?P<version>{version_string}) \\(by /u/ljdawson\\)')\n\n\n@register_detector\nclass NarwhalForRedditDetector(RedditBrowser):\n    is_app = True\n    look_for = 'narwhal'\n    name = 'narwhal for reddit'\n    agent_string = '{look_for}-(?P<platform>.*?)\\/\\d+ by det0ur'\n\n\n@register_detector\nclass McRedditDetector(RedditBrowser):\n    is_app = True\n    look_for = 'McReddit'\n    name = 'McReddit'\n    agent_string = '{look_for} - Reddit Client for (?P<platform>.*?)$'\n\n\n@register_detector\nclass ReaditDetector(RedditBrowser):\n    look_for = 'Readit'\n    name = 'Readit'\n    agent_string = '(\\({look_for} for WP /u/MessageAcrossStudios\\) ?){{1,2}}'\n\n\n@register_detector\nclass BaconReaderDetector(RedditBrowser):\n    is_app = True\n    look_for = 'BaconReader'\n    name = 'Bacon Reader'\n    agent_string = (\n        '{look_for}\\/(?P<version>{version_string}) \\([a-zA-Z]+; '\n        '(?P<platform>.*?) (?P<pversion>{version_string}); '\n        'Scale\\/{version_string}\\)')\n\n\ndef detect(*args, **kw):\n    return de(*args, **kw)\n\n\nclass Agent(object):\n    __slots__ = (\n        \"agent_string\",\n        \"browser_name\",\n        \"browser_version\",\n        \"os_name\",\n        \"os_version\",\n        \"platform_name\",\n        \"platform_version\",\n        \"sub_platform_name\",\n        \"bot\",\n        \"app_name\",\n        \"is_mobile_browser\",\n    )\n\n    MOBILE_PLATFORMS = {'iOS', 'Windows', 'Android', 'BlackBerry'}\n\n    def __init__(self, **kw):\n        kw.setdefault(\"is_mobile_browser\", False)\n        for k in self.__slots__:\n            setattr(self, k, kw.get(k))\n\n    @classmethod\n    def parse(cls, ua):\n        agent = cls(agent_string=ua)\n        parsed = detect(ua)\n        for attr in (\"browser\", \"os\", \"platform\"):\n            d = parsed.get(attr)\n            if d:\n                for subattr in (\"name\", \"version\"):\n                    if subattr in d:\n                        key = \"%s_%s\" % (attr, subattr)\n                        setattr(agent, key, d[subattr])\n\n        agent.bot = parsed.get('bot')\n        dist = parsed.get('dist')\n        if dist:\n            agent.sub_platform_name = dist.get('name')\n\n        # if this is a known app, extract the app_name\n        agent.app_name = parsed.get('app_name')\n        agent.is_mobile_browser = agent.determine_mobile_browser()\n        return agent\n\n    def determine_mobile_browser(self):\n        if self.platform_name in self.MOBILE_PLATFORMS:\n            if self.sub_platform_name == 'IPad':\n                return False\n\n            if (\n                self.platform_name == 'Android' and\n                not (\n                    'Mobile' in self.agent_string or\n                    self.browser_name == 'Opera Mobile'\n                )\n            ):\n                return False\n\n            if (\n                self.platform_name == 'Windows' and\n                self.sub_platform_name != 'Windows Phone'\n            ):\n                return False\n\n            if 'Opera Mini' in self.agent_string:\n                return False\n\n            return True\n        return False\n\n    def to_dict(self):\n        d = {}\n        for k in self.__slots__:\n            if k != \"agent_string\":\n                v = getattr(self, k, None)\n                if v:\n                    d[k] = v\n        return d\n"
  },
  {
    "path": "r2/r2/lib/utils/utils.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport base64\nimport codecs\nimport ConfigParser\nimport cPickle as pickle\nimport functools\nimport itertools\nimport math\nimport os\nimport random\nimport re\nimport signal\nimport time\nimport traceback\n\nfrom collections import OrderedDict\nfrom copy import deepcopy\nfrom datetime import date, datetime, timedelta\nfrom decimal import Decimal\nfrom urllib import unquote_plus, unquote\nfrom urllib2 import urlopen, Request\nfrom urlparse import urlparse, urlunparse\n\nimport pytz\nimport snudown\nimport unidecode\nfrom r2.lib.utils import reddit_agent_parser\n\nfrom babel.dates import TIMEDELTA_UNITS\nfrom BeautifulSoup import BeautifulSoup, SoupStrainer\nfrom mako.filters import url_escape\nfrom pylons import request, config\nfrom pylons import tmpl_context as c\nfrom pylons import app_globals as g\nfrom pylons.i18n import ungettext, _\n\nfrom r2.lib.contrib import ipaddress\nfrom r2.lib.filters import _force_unicode, _force_utf8\nfrom r2.lib.require import require, require_split, RequirementException\nfrom r2.lib.utils._utils import *\n\niters = (list, tuple, set)\n\ndef randstr(length,\n            alphabet='abcdefghijklmnopqrstuvwxyz0123456789'):\n    \"\"\"Return a string made up of random chars from alphabet.\"\"\"\n    return ''.join(random.choice(alphabet) for _ in xrange(length))\n\n\nclass Storage(dict):\n    \"\"\"\n    A Storage object is like a dictionary except `obj.foo` can be used\n    in addition to `obj['foo']`.\n\n        >>> o = storage(a=1)\n        >>> o.a\n        1\n        >>> o['a']\n        1\n        >>> o.a = 2\n        >>> o['a']\n        2\n        >>> del o.a\n        >>> o.a\n        Traceback (most recent call last):\n            ...\n        AttributeError: 'a'\n\n    \"\"\"\n    def __getattr__(self, key):\n        try:\n            return self[key]\n        except KeyError, k:\n            raise AttributeError, k\n\n    def __setattr__(self, key, value):\n        self[key] = value\n\n    def __delattr__(self, key):\n        try:\n            del self[key]\n        except KeyError, k:\n            raise AttributeError, k\n\n    def __repr__(self):\n        return '<Storage ' + dict.__repr__(self) + '>'\n\nstorage = Storage\n\n\nclass Enum(Storage):\n    def __init__(self, *a):\n        self.name = tuple(a)\n        Storage.__init__(self, ((e, i) for i, e in enumerate(a)))\n    def __contains__(self, item):\n        if isinstance(item, int):\n            return item in self.values()\n        else:\n            return Storage.__contains__(self, item)\n\n\nclass class_property(object):\n    \"\"\"A decorator that combines @classmethod and @property.\n\n    http://stackoverflow.com/a/8198300/120999\n    \"\"\"\n    def __init__(self, function):\n        self.function = function\n    def __get__(self, instance, cls):\n        return self.function(cls)\n\n\nclass Results():\n    def __init__(self, sa_ResultProxy, build_fn, do_batch=False):\n        self.rp = sa_ResultProxy\n        self.fn = build_fn\n        self.do_batch = do_batch\n\n    @property\n    def rowcount(self):\n        return self.rp.rowcount\n\n    def _fetch(self, res):\n        if self.do_batch:\n            return self.fn(res)\n        else:\n            return [self.fn(row) for row in res]\n\n    def fetchall(self):\n        return self._fetch(self.rp.fetchall())\n\n    def fetchmany(self, n):\n        rows = self._fetch(self.rp.fetchmany(n))\n        if rows:\n            return rows\n        else:\n            raise StopIteration\n\n    def fetchone(self):\n        row = self.rp.fetchone()\n        if row:\n            if self.do_batch:\n                if isinstance(row, Storage):\n                    rows = (row,)\n                else:\n                    rows = tup(row)\n                return self.fn(rows)[0]\n            else:\n                return self.fn(row)\n        else:\n            raise StopIteration\n\nr_base_url = re.compile(\"(?i)(?:.+?://)?([^#]*[^#/])/?\")\nr_domain = re.compile(\"(?i)(?:.+?://)?([^/:#?]*)\")\nr_domain_prefix = re.compile('^www\\d*\\.')\n\n\ndef strip_www(domain):\n    stripped = domain\n    if domain.count('.') > 1:\n        prefix = r_domain_prefix.findall(domain)\n        if domain.startswith(\"www\") and len(prefix):\n            stripped = '.'.join(domain.split('.')[1:])\n    return stripped\n\n\ndef is_subdomain(subdomain, base):\n    \"\"\"Check if a domain is equal to or a subdomain of a base domain.\"\"\"\n    return subdomain == base or (\n        subdomain is not None and subdomain.endswith('.' + base))\n\n\nlang_re = re.compile(r\"\\A\\w\\w(-\\w\\w)?\\Z\")\n\n\ndef is_language_subdomain(subdomain):\n    return lang_re.match(subdomain)\n\n\ndef base_url(url):\n    res = r_base_url.findall(url)\n    if res and res[0]:\n        base = strip_www(res[0])\n    else:\n        base = url\n    return base.lower()\n\n\ndef domain(url):\n    \"\"\"\n        Takes a URL and returns the domain part, minus www., if\n        present\n    \"\"\"\n    match = r_domain.search(url)\n    if match:\n        domain = strip_www(match.group(1))\n    else:\n        domain = url\n    return domain.lower()\n\n\ndef extract_subdomain(host=None, base_domain=None):\n    \"\"\"Try to extract a subdomain from the request, as compared to g.domain.\n\n    host and base_domain exist as arguments primarily for the sake of unit\n    tests, although their usage should not be considered restrained to that.\n    \"\"\"\n    # These would be the argument defaults, but we need them evaluated at\n    # run-time, not definition-time.\n    if host is None:\n        host = request.host\n    if base_domain is None:\n        base_domain = g.domain\n\n    if not host:\n        return ''\n\n    end_index = host.find(base_domain) - 1 # For the conjoining dot.\n    # Is either the requested domain the same as the base domain, or the\n    # base is not a substring?\n    if end_index < 0:\n        return ''\n    return host[:end_index]\n\nr_path_component = re.compile(\".*?/(.*)\")\ndef path_component(s):\n    \"\"\"\n        takes a url http://www.foo.com/i/like/cheese and returns\n        i/like/cheese\n    \"\"\"\n    res = r_path_component.findall(base_url(s))\n    return (res and res[0]) or s\n\ndef get_title(url):\n    \"\"\"Fetch the contents of url and try to extract the page's title.\"\"\"\n    if not url or not url.startswith(('http://', 'https://')):\n        return None\n\n    try:\n        req = Request(url)\n        if g.useragent:\n            req.add_header('User-Agent', g.useragent)\n        opener = urlopen(req, timeout=15)\n\n        # determine the encoding of the response\n        for param in opener.info().getplist():\n            if param.startswith(\"charset=\"):\n                param_name, sep, charset = param.partition(\"=\")\n                codec = codecs.getreader(charset)\n                break\n        else:\n            codec = codecs.getreader(\"utf-8\")\n\n        with codec(opener, \"ignore\") as reader:\n            # Attempt to find the title in the first 1kb\n            data = reader.read(1024)\n            title = extract_title(data)\n\n            # Title not found in the first kb, try searching an additional 10kb\n            if not title:\n                data += reader.read(10240)\n                title = extract_title(data)\n\n        return title\n\n    except:\n        return None\n\ndef extract_title(data):\n    \"\"\"Try to extract the page title from a string of HTML.\n\n    An og:title meta tag is preferred, but will fall back to using\n    the <title> tag instead if one is not found. If using <title>,\n    also attempts to trim off the site's name from the end.\n    \"\"\"\n    bs = BeautifulSoup(data, convertEntities=BeautifulSoup.HTML_ENTITIES)\n    if not bs or not bs.html.head:\n        return\n    head_soup = bs.html.head\n\n    title = None\n\n    # try to find an og:title meta tag to use\n    og_title = (head_soup.find(\"meta\", attrs={\"property\": \"og:title\"}) or\n                head_soup.find(\"meta\", attrs={\"name\": \"og:title\"}))\n    if og_title:\n        title = og_title.get(\"content\")\n\n    # if that failed, look for a <title> tag to use instead\n    if not title and head_soup.title and head_soup.title.string:\n        title = head_soup.title.string\n\n        # remove end part that's likely to be the site's name\n        # looks for last delimiter char between spaces in strings\n        # delimiters: |, -, emdash, endash,\n        #             left- and right-pointing double angle quotation marks\n        reverse_title = title[::-1]\n        to_trim = re.search(u'\\s[\\u00ab\\u00bb\\u2013\\u2014|-]\\s',\n                            reverse_title,\n                            flags=re.UNICODE)\n\n        # only trim if it won't take off over half the title\n        if to_trim and to_trim.end() < len(title) / 2:\n            title = title[:-(to_trim.end())]\n\n    if not title:\n        return\n\n    # get rid of extraneous whitespace in the title\n    title = re.sub(r'\\s+', ' ', title, flags=re.UNICODE)\n\n    return title.encode('utf-8').strip()\n\nVALID_SCHEMES = ('http', 'https', 'ftp', 'mailto')\nvalid_dns = re.compile('\\A[-a-zA-Z0-9_]+\\Z')\ndef sanitize_url(url, require_scheme=False, valid_schemes=VALID_SCHEMES):\n    \"\"\"Validates that the url is of the form\n\n    scheme://domain/path/to/content#anchor?cruft\n\n    using the python built-in urlparse.  If the url fails to validate,\n    returns None.  If no scheme is provided and 'require_scheme =\n    False' is set, the url is returned with scheme 'http', provided it\n    otherwise validates\"\"\"\n\n    if not url:\n        return None\n\n    url = url.strip()\n    if url.lower() == 'self':\n        return url\n\n    try:\n        u = urlparse(url)\n        # first pass: make sure a scheme has been specified\n        if not require_scheme and not u.scheme:\n            # \"//example.com/\"\n            if u.hostname:\n                prepend = \"https:\" if c.secure else \"http:\"\n            # \"example.com/\"\n            else:\n                prepend = \"http://\"\n            url = prepend + url\n            u = urlparse(url)\n    except ValueError:\n        return None\n\n    if not u.scheme:\n        return None\n    if valid_schemes is not None and u.scheme not in valid_schemes:\n        return None\n\n    # if there is a scheme and no hostname, it is a bad url.\n    if not u.hostname:\n        return None\n    # work around CRBUG-464270\n    if len(u.hostname) > 255:\n        return None\n    # work around for Chrome crash with \"%%30%30\" - Sep 2015\n    if \"%00\" in unquote(u.path):\n        return None\n    if u.username is not None or u.password is not None:\n        return None\n\n    try:\n        idna_hostname = u.hostname.encode('idna')\n    except TypeError as e:\n        g.log.warning(\"Bad hostname given [%r]: %s\", u.hostname, e)\n        raise\n    except UnicodeError:\n        return None\n\n    # Make sure FQDNs like google.com. (with trailing dot) are allowed. This\n    # is necessary to support linking to bare TLDs.\n    if idna_hostname.endswith('.'):\n        idna_hostname = idna_hostname[:-1]\n\n    for label in idna_hostname.split('.'):\n        if not re.match(valid_dns, label):\n            return None\n\n    if idna_hostname != u.hostname:\n        url = urlunparse((u[0], idna_hostname, u[2], u[3], u[4], u[5]))\n    return url\n\ndef trunc_string(text, max_length, suffix='...'):\n    \"\"\"Truncate a string, attempting to split on a word-break.\n\n    If the first word is longer than max_length, then truncate within the word.\n\n    Adapted from http://stackoverflow.com/a/250406/120999 .\n    \"\"\"\n    if len(text) <= max_length:\n        return text\n    else:\n        hard_truncated = text[:(max_length - len(suffix))]\n        word_truncated = hard_truncated.rsplit(' ', 1)[0]\n        return word_truncated + suffix\n\n# Truncate a time to a certain number of minutes\n# e.g, trunc_time(5:52, 30) == 5:30\ndef trunc_time(time, mins, hours=None):\n    if hours is not None:\n        if hours < 1 or hours > 60:\n            raise ValueError(\"Hours %d is weird\" % mins)\n        time = time.replace(hour = hours * (time.hour / hours))\n\n    if mins < 1 or mins > 60:\n        raise ValueError(\"Mins %d is weird\" % mins)\n\n    return time.replace(minute = mins * (time.minute / mins),\n                        second = 0,\n                        microsecond = 0)\n\ndef long_datetime(datetime):\n    return datetime.astimezone(g.tz).ctime() + \" \" + str(g.tz)\n\ndef median(l):\n    if l:\n        s = sorted(l)\n        i = len(s) / 2\n        return s[i]\n\ndef query_string(dict):\n    pairs = []\n    for k,v in dict.iteritems():\n        if v is not None:\n            try:\n                k = url_escape(_force_unicode(k))\n                v = url_escape(_force_unicode(v))\n                pairs.append(k + '=' + v)\n            except UnicodeDecodeError:\n                continue\n    if pairs:\n        return '?' + '&'.join(pairs)\n    else:\n        return ''\n\n\n# Characters that might cause parsing differences in different implementations\n# Spaces only seem to cause parsing differences when occurring directly before\n# the scheme\nURL_PROBLEMATIC_RE = re.compile(\n    ur'(\\A\\x20|[\\x00-\\x19\\xA0\\u1680\\u180E\\u2000-\\u2029\\u205f\\u3000\\\\])',\n    re.UNICODE\n)\n\n\ndef paranoid_urlparser_method(check):\n    \"\"\"\n    Decorator for checks on `UrlParser` instances that need to be paranoid\n    \"\"\"\n    def check_wrapper(parser, *args, **kwargs):\n        return UrlParser.perform_paranoid_check(parser, check, *args, **kwargs)\n\n    return check_wrapper\n\n\nclass UrlParser(object):\n    \"\"\"\n    Wrapper for urlparse and urlunparse for making changes to urls.\n\n    All attributes present on the tuple-like object returned by\n    urlparse are present on this class, and are setable, with the\n    exception of netloc, which is instead treated via a getter method\n    as a concatenation of hostname and port.\n\n    Unlike urlparse, this class allows the query parameters to be\n    converted to a dictionary via the query_dict method (and\n    correspondingly updated via update_query).  The extension of the\n    path can also be set and queried.\n\n    The class also contains reddit-specific functions for setting,\n    checking, and getting a path's subreddit.\n    \"\"\"\n\n    __slots__ = ['scheme', 'path', 'params', 'query',\n                 'fragment', 'username', 'password', 'hostname', 'port',\n                 '_orig_url', '_orig_netloc', '_query_dict']\n\n    valid_schemes = ('http', 'https', 'ftp', 'mailto')\n\n    def __init__(self, url):\n        u = urlparse(url)\n        for s in self.__slots__:\n            if hasattr(u, s):\n                setattr(self, s, getattr(u, s))\n        self._orig_url    = url\n        self._orig_netloc = getattr(u, 'netloc', '')\n        self._query_dict  = None\n\n    def __eq__(self, other):\n        \"\"\"A loose equality method for UrlParsers.\n\n        In particular, this returns true for UrlParsers whose resultant urls\n        have the same query parameters, but in a different order.  These are\n        treated the same most of the time, but if you need strict equality,\n        compare the string results of unparse().\n        \"\"\"\n        if not isinstance(other, UrlParser):\n            return False\n\n        (s_scheme, s_netloc, s_path, s_params, s_query, s_fragment) = self._unparse()\n        (o_scheme, o_netloc, o_path, o_params, o_query, o_fragment) = other._unparse()\n        # Check all the parsed components for equality, except the query, which\n        # is easier to check in its pure-dictionary form.\n        if (s_scheme != o_scheme or\n                s_netloc != o_netloc or\n                s_path != o_path or\n                s_params != o_params or\n                s_fragment != o_fragment):\n            return False\n        # Coerce query dicts from OrderedDicts to standard dicts to avoid an\n        # order-sensitive comparison.\n        if dict(self.query_dict) != dict(other.query_dict):\n            return False\n\n        return True\n\n    def update_query(self, **updates):\n        \"\"\"Add or change query parameters.\"\"\"\n        # Since in HTTP everything's a string, coercing values to strings now\n        # makes equality testing easier.  Python will throw an error if you try\n        # to pass in a non-string key, so that's already taken care of for us.\n        updates = {k: _force_unicode(v) for k, v in updates.iteritems()}\n        self.query_dict.update(updates)\n\n    @property\n    def query_dict(self):\n        \"\"\"A dictionary of the current query parameters.\n\n        Keys and values pulled from the original url are un-url-escaped.\n\n        Modifying this function's return value will result in changes to the\n        unparse()-d url, but it's recommended instead to make any changes via\n        `update_query()`.\n        \"\"\"\n        if self._query_dict is None:\n            def _split(param):\n                p = param.split('=')\n                return (unquote_plus(p[0]),\n                        unquote_plus('='.join(p[1:])))\n            self._query_dict = OrderedDict(\n                                 _split(p) for p in self.query.split('&') if p)\n        return self._query_dict\n\n    def path_extension(self):\n        \"\"\"Fetches the current extension of the path.\n\n        If the url does not end in a file or the file has no extension, returns\n        an empty string.\n        \"\"\"\n        filename = self.path.split('/')[-1]\n        filename_parts = filename.split('.')\n        if len(filename_parts) == 1:\n            return ''\n\n        return filename_parts[-1]\n\n    def has_image_extension(self):\n        \"\"\"Guess if the url leads to an image.\"\"\"\n        extension = self.path_extension().lower()\n        return extension in {'gif', 'jpeg', 'jpg', 'png', 'tiff'}\n\n    def has_static_image_extension(self):\n        \"\"\"Guess if the url leads to a non-animated image.\"\"\"\n        extension = self.path_extension().lower()\n        return extension in {'jpeg', 'jpg', 'png', 'tiff'}\n\n    def set_extension(self, extension):\n        \"\"\"\n        Changes the extension of the path to the provided value (the\n        \".\" should not be included in the extension as a \".\" is\n        provided)\n        \"\"\"\n        pieces = self.path.split('/')\n        dirs = pieces[:-1]\n        base = pieces[-1].split('.')\n        base = '.'.join(base[:-1] if len(base) > 1 else base)\n        if extension:\n            base += '.' + extension\n        dirs.append(base)\n        self.path =  '/'.join(dirs)\n        return self\n\n    def canonicalize(self):\n        subdomain = extract_subdomain(self.hostname)\n        if subdomain == '' or is_language_subdomain(subdomain):\n            self.hostname = 'www.{0}'.format(g.domain)\n        if not self.path.endswith('/'):\n            self.path += '/'\n        self.scheme = 'https'\n\n    def switch_subdomain_by_extension(self, extension=None):\n        \"\"\"Change the subdomain to the one that fits an extension.\n\n        This should only be used on reddit URLs.\n\n        Arguments:\n\n        * extension: the template extension to which the middleware hints when\n          parsing the subdomain resulting from this function.\n\n        >>> u = UrlParser('http://www.reddit.com/r/redditdev')\n        >>> u.switch_subdomain_by_extension('compact')\n        >>> u.unparse()\n        'http://i.reddit.com/r/redditdev'\n\n        If `extension` is not provided or does not match any known extensions,\n        the default subdomain (`g.domain_prefix`) will be used.\n\n        Note that this will not remove any existing extensions; if you want to\n        ensure the explicit extension does not override the subdomain hint, you\n        should call `set_extension('')` first.\n        \"\"\"\n        new_subdomain = g.domain_prefix\n        for subdomain, subdomain_extension in g.extension_subdomains.iteritems():\n            if extension == subdomain_extension:\n                new_subdomain = subdomain\n                break\n        self.hostname = '%s.%s' % (new_subdomain, g.domain)\n\n    def unparse(self):\n        \"\"\"\n        Converts the url back to a string, applying all updates made\n        to the fields thereof.\n\n        Note: if a host name has been added and none was present\n        before, will enforce scheme -> \"http\" unless otherwise\n        specified.  Double-slashes are removed from the resultant\n        path, and the query string is reconstructed only if the\n        query_dict has been modified/updated.\n        \"\"\"\n        return urlunparse(self._unparse())\n\n    def _unparse(self):\n        q = query_string(self.query_dict).lstrip('?')\n\n        # make sure the port is not doubly specified\n        if getattr(self, 'port', None) and \":\" in self.hostname:\n            self.hostname = self.hostname.split(':')[0]\n\n        # if there is a netloc, there had better be a scheme\n        if self.netloc and not self.scheme:\n            self.scheme = \"http\"\n\n        return (self.scheme, self.netloc,\n                self.path.replace('//', '/'),\n                self.params, q, self.fragment)\n\n    def path_has_subreddit(self):\n        \"\"\"\n        utility method for checking if the path starts with a\n        subreddit specifier (namely /r/ or /subreddits/).\n        \"\"\"\n        return self.path.startswith(('/r/', '/subreddits/', '/reddits/'))\n\n    def get_subreddit(self):\n        \"\"\"checks if the current url refers to a subreddit and returns\n        that subreddit object.  The cases here are:\n\n          * the hostname is unset or is g.domain, in which case it\n            looks for /r/XXXX or /subreddits.  The default in this case\n            is Default.\n          * the hostname is a cname to a known subreddit.\n\n        On failure to find a subreddit, returns None.\n        \"\"\"\n        from r2.models import Subreddit, NotFound, DefaultSR\n        try:\n            if (not self.hostname or\n                    is_subdomain(self.hostname, g.domain) or\n                    self.hostname.startswith(g.domain)):\n                if self.path.startswith('/r/'):\n                    return Subreddit._by_name(self.path.split('/')[2])\n                else:\n                    return DefaultSR()\n            elif self.hostname:\n                return Subreddit._by_domain(self.hostname)\n        except NotFound:\n            pass\n        return None\n\n    def perform_paranoid_check(self, check, *args, **kwargs):\n        \"\"\"\n        Perform a check on a URL that needs to account for bugs in `unparse()`\n\n        If you need to account for quirks in browser URL parsers, you should\n        use this along with `is_web_safe_url()`. Trying to parse URLs like\n        a browser would just makes things really hairy.\n        \"\"\"\n        variants_to_check = (\n            self,\n            UrlParser(self.unparse())\n        )\n        # If the check doesn't pass on *every* variant, it's a fail.\n        return all(\n            check(variant, *args, **kwargs) for variant in variants_to_check\n        )\n\n    @paranoid_urlparser_method\n    def is_web_safe_url(self):\n        \"\"\"Determine if this URL could cause issues with different parsers\"\"\"\n\n        # There's no valid reason for this, and just serves to confuse UAs.\n        # and urllib2.\n        if self._orig_url.startswith(\"///\"):\n            return False\n\n        # Double-checking the above\n        if not self.hostname and self.path.startswith('//'):\n            return False\n\n        # A host-relative link with a scheme like `https:/baz` or `https:?quux`\n        if self.scheme and not self.hostname:\n            return False\n\n        # Credentials in the netloc? Not on reddit!\n        if \"@\" in self._orig_netloc:\n            return False\n\n        # `javascript://www.reddit.com/%0D%Aalert(1)` is not safe, obviously\n        if self.scheme and self.scheme.lower() not in self.valid_schemes:\n            return False\n\n        # Reject any URLs that contain characters known to cause parsing\n        # differences between parser implementations\n        for match in re.finditer(URL_PROBLEMATIC_RE, self._orig_url):\n            # XXX: Yuck. We have non-breaking spaces in title slugs! They\n            # should be safe enough to allow after three slashes. Opera 12's the\n            # only browser that trips over them, and it doesn't fall for\n            # `http:///foo.com/`.\n            # Check both in case unicode promotion fails\n            if match.group(0) in {u'\\xa0', '\\xa0'}:\n                if match.string[0:match.start(0)].count('/') < 3:\n                    return False\n            else:\n                return False\n\n        return True\n\n    def is_reddit_url(self, subreddit=None):\n        \"\"\"utility method for seeing if the url is associated with\n        reddit as we don't necessarily want to mangle non-reddit\n        domains\n\n        returns true only if hostname is nonexistant, a subdomain of\n        g.domain, or a subdomain of the provided subreddit's cname.\n        \"\"\"\n\n        valid_subdomain = (\n            not self.hostname or\n            is_subdomain(self.hostname, g.domain) or\n            (subreddit and subreddit.domain and\n                is_subdomain(self.hostname, subreddit.domain))\n        )\n\n        if not valid_subdomain or not self.hostname or not g.offsite_subdomains:\n            return valid_subdomain\n        return not any(\n            is_subdomain(self.hostname, \"%s.%s\" % (subdomain, g.domain))\n            for subdomain in g.offsite_subdomains\n        )\n\n    def path_add_subreddit(self, subreddit):\n        \"\"\"\n        Adds the subreddit's path to the path if another subreddit's\n        prefix is not already present.\n        \"\"\"\n        if not (self.path_has_subreddit()\n                or self.path.startswith(subreddit.user_path)):\n            self.path = (subreddit.user_path + self.path)\n        return self\n\n    @property\n    def netloc(self):\n        \"\"\"\n        Getter method which returns the hostname:port, or empty string\n        if no hostname is present.\n        \"\"\"\n        if not self.hostname:\n            return \"\"\n        elif getattr(self, \"port\", None):\n            return self.hostname + \":\" + str(self.port)\n        return self.hostname\n\n    def __repr__(self):\n        return \"<URL %s>\" % repr(self.unparse())\n\n    def domain_permutations(self, fragments=False, subdomains=True):\n        \"\"\"\n          Takes a domain like `www.reddit.com`, and returns a list of ways\n          that a user might search for it, like:\n          * www\n          * reddit\n          * com\n          * www.reddit.com\n          * reddit.com\n          * com\n        \"\"\"\n        ret = set()\n        if self.hostname:\n            r = self.hostname.split('.')\n\n            if subdomains:\n                for x in xrange(len(r)-1):\n                    ret.add('.'.join(r[x:len(r)]))\n\n            if fragments:\n                for x in r:\n                    ret.add(x)\n\n        return ret\n\n    @classmethod\n    def base_url(cls, url):\n        u = cls(url)\n\n        # strip off any www and lowercase the hostname:\n        netloc = strip_www(u.netloc.lower())\n\n        # http://code.google.com/web/ajaxcrawling/docs/specification.html\n        fragment = u.fragment if u.fragment.startswith(\"!\") else \"\"\n\n        return urlunparse((u.scheme.lower(), netloc,\n                           u.path, u.params, u.query, fragment))\n\n\ndef coerce_url_to_protocol(url, protocol='http'):\n    '''Given an absolute (but potentially protocol-relative) url, coerce it to\n    a protocol.'''\n    parsed_url = UrlParser(url)\n    parsed_url.scheme = protocol\n    return parsed_url.unparse()\n\ndef url_is_embeddable_image(url):\n    \"\"\"The url is on an oembed-friendly domain and looks like an image.\"\"\"\n    parsed_url = UrlParser(url)\n\n    if parsed_url.path_extension().lower() in {\"jpg\", \"gif\", \"png\", \"jpeg\"}:\n        if parsed_url.hostname not in g.known_image_domains:\n            return False\n        return True\n\n    return False\n\n\ndef url_to_thing(url):\n    \"\"\"Given a reddit URL, return the Thing to which it associates.\n\n    Examples:\n        /r/somesr - Subreddit\n        /r/somesr/comments/j2jx - Link\n        /r/somesr/comments/j2jx/slug/k2js - Comment\n    \"\"\"\n    from r2.models import Comment, Link, Message, NotFound, Subreddit, Thing\n    from r2.config.middleware import SubredditMiddleware\n    sr_pattern = SubredditMiddleware.sr_pattern\n\n    urlparser = UrlParser(_force_utf8(url))\n    if not urlparser.is_reddit_url():\n        return None\n\n    try:\n        sr_name = sr_pattern.match(urlparser.path).group(1)\n    except AttributeError:\n        sr_name = None\n\n    path = sr_pattern.sub('', urlparser.path)\n    if not path or path == '/':\n        if not sr_name:\n            return None\n\n        try:\n            return Subreddit._by_name(sr_name, data=True)\n        except NotFound:\n            return None\n\n    # potential TypeError raised here because of environ being None\n    # when calling outside of app context\n    try:\n        route_dict = config['routes.map'].match(path)\n    except TypeError:\n        return None\n\n    if not route_dict:\n        return None\n\n    try:\n        comment = route_dict.get('comment')\n        if comment:\n            return Comment._byID36(comment, data=True)\n\n        article = route_dict.get('article')\n        if article:\n            return Link._byID36(article, data=True)\n\n        msg = route_dict.get('mid')\n        if msg:\n            return Message._byID36(msg, data=True)\n    except (NotFound, ValueError):\n        return None\n\n    return None\n\n\ndef pload(fname, default = None):\n    \"Load a pickled object from a file\"\n    try:\n        f = file(fname, 'r')\n        d = pickle.load(f)\n    except IOError:\n        d = default\n    else:\n        f.close()\n    return d\n\ndef psave(fname, d):\n    \"Save a pickled object into a file\"\n    f = file(fname, 'w')\n    pickle.dump(d, f)\n    f.close()\n\ndef unicode_safe(res):\n    try:\n        return str(res)\n    except UnicodeEncodeError:\n        try:\n            return unicode(res).encode('utf-8')\n        except UnicodeEncodeError:\n            return res.decode('utf-8').encode('utf-8')\n\ndef decompose_fullname(fullname):\n    \"\"\"\n        decompose_fullname(\"t3_e4fa\") ->\n            (Thing, 3, 658918)\n    \"\"\"\n    from r2.lib.db.thing import Thing,Relation\n    if fullname[0] == 't':\n        type_class = Thing\n    elif fullname[0] == 'r':\n        type_class = Relation\n\n    type_id36, thing_id36 = fullname[1:].split('_')\n\n    type_id = int(type_id36,36)\n    id      = int(thing_id36,36)\n\n    return (type_class, type_id, id)\n\ndef cols(lst, ncols):\n    \"\"\"divides a list into columns, and returns the\n    rows. e.g. cols('abcdef', 2) returns (('a', 'd'), ('b', 'e'), ('c',\n    'f'))\"\"\"\n    nrows = int(math.ceil(1.*len(lst) / ncols))\n    lst = lst + [None for i in range(len(lst), nrows*ncols)]\n    cols = [lst[i:i+nrows] for i in range(0, nrows*ncols, nrows)]\n    rows = zip(*cols)\n    rows = [filter(lambda x: x is not None, r) for r in rows]\n    return rows\n\ndef fetch_things(t_class,since,until,batch_fn=None,\n                 *query_params, **extra_query_dict):\n    \"\"\"\n        Simple utility function to fetch all Things of class t_class\n        (spam or not, but not deleted) that were created from 'since'\n        to 'until'\n    \"\"\"\n\n    from r2.lib.db.operators import asc\n\n    if not batch_fn:\n        batch_fn = lambda x: x\n\n    query_params = ([t_class.c._date >= since,\n                     t_class.c._date <  until,\n                     t_class.c._spam == (True,False)]\n                    + list(query_params))\n    query_dict   = {'sort':  asc('_date'),\n                    'limit': 100,\n                    'data':  True}\n    query_dict.update(extra_query_dict)\n\n    q = t_class._query(*query_params,\n                        **query_dict)\n\n    orig_rules = deepcopy(q._rules)\n\n    things = list(q)\n    while things:\n        things = batch_fn(things)\n        for t in things:\n            yield t\n        q._rules = deepcopy(orig_rules)\n        q._after(t)\n        things = list(q)\n\n\ndef fetch_things2(query, chunk_size = 100, batch_fn = None, chunks = False):\n    \"\"\"Incrementally run query with a limit of chunk_size until there are\n    no results left. batch_fn transforms the results for each chunk\n    before returning.\"\"\"\n\n    assert query._sort, \"you must specify the sort order in your query!\"\n\n    orig_rules = deepcopy(query._rules)\n    query._limit = chunk_size\n    items = list(query)\n    done = False\n    while items and not done:\n        #don't need to query again at the bottom if we didn't get enough\n        if len(items) < chunk_size:\n            done = True\n\n        after = items[-1]\n\n        if batch_fn:\n            items = batch_fn(items)\n\n        if chunks:\n            yield items\n        else:\n            for i in items:\n                yield i\n\n        if not done:\n            query._rules = deepcopy(orig_rules)\n            query._after(after)\n            items = list(query)\n\n\ndef exponential_retrier(func_to_retry,\n                        exception_filter=lambda *args, **kw: True,\n                        retry_min_wait_ms=500,\n                        max_retries=5):\n    \"\"\"Call func_to_retry and return it's results.\n    If func_to_retry throws an exception, retry.\n\n    :param Function func_to_retry: Function to execute\n        and possibly retry.\n    :param exception_filter:  Only retry exceptions for\n        which this function returns True.  Always returns True by default.\n    :param int retry_min_wait_ms: Initial wait period\n        if an exception happens in milliseconds.\n        After each retry this value will be multiplied by 2\n        thus achieving exponential backoff algorithm.\n    :param int max_retries:  How many times to wait before\n        just re-throwing last exception.\n        Value of zero would result in no retry attempts.\n    \"\"\"\n    sleep_time = retry_min_wait_ms\n    num_retried = 0\n    while True:\n        try:\n            return func_to_retry()\n        # StopIteration should never be retried as its part of regular logic.\n        except StopIteration:\n            raise\n        except Exception as e:\n            g.log.exception(\"%d number retried\" % num_retried)\n            num_retried += 1\n            # if we ran out of retries or this Exception\n            # shouldnt be retried then raise the exception instead of sleeping\n            if num_retried > max_retries or not exception_filter(e):\n                raise\n\n            # convert to ms.  Use floating point literal for int -> float\n            time.sleep(sleep_time / 1000.0)\n            sleep_time *= 2\n\n\ndef fetch_things_with_retry(query,\n                            chunk_size=100,\n                            batch_fn=None,\n                            chunks=False,\n                            retry_min_wait_ms=500,\n                            max_retries=0):\n    \"\"\"Incrementally run query with a limit of chunk_size until there are\n    no results left. batch_fn transforms the results for each chunk\n    before returning.\n\n    If a query at some point generates an exception\n    retry it using exponential backoff.\n\n    By default retrying is turned off.\"\"\"\n\n    assert query._sort, \"you must specify the sort order in your query!\"\n\n    retrier = functools.partial(exponential_retrier,\n                                retry_min_wait_ms=retry_min_wait_ms,\n                                max_retries=max_retries)\n\n    orig_rules = deepcopy(query._rules)\n    query._limit = chunk_size\n    items = retrier(lambda: list(query))\n\n    done = False\n    while items and not done:\n        # don't need to query again at the bottom if we didn't get enough\n        if len(items) < chunk_size:\n            done = True\n\n        after = items[-1]\n\n        if batch_fn:\n                items = batch_fn(items)\n\n        if chunks:\n            yield items\n        else:\n            for i in items:\n                yield i\n\n        if not done:\n            query._rules = deepcopy(orig_rules)\n            query._after(after)\n            items = retrier(lambda: list(query))\n\n\ndef fix_if_broken(thing, delete = True, fudge_links = False):\n    from r2.models import Link, Comment, Subreddit, Message\n\n    # the minimum set of attributes that are required\n    attrs = dict((cls, cls._essentials)\n                 for cls\n                 in (Link, Comment, Subreddit, Message))\n\n    if thing.__class__ not in attrs:\n        raise TypeError\n\n    for attr in attrs[thing.__class__]:\n        try:\n            # try to retrieve the attribute\n            getattr(thing, attr)\n        except AttributeError:\n            if not delete:\n                raise\n\n            if isinstance(thing, Link) and fudge_links:\n                if attr == \"sr_id\":\n                    thing.sr_id = 6\n                    print \"Fudging %s.sr_id to %d\" % (thing._fullname,\n                                                      thing.sr_id)\n                elif attr == \"author_id\":\n                    thing.author_id = 8244672\n                    print \"Fudging %s.author_id to %d\" % (thing._fullname,\n                                                          thing.author_id)\n                else:\n                    print \"Got weird attr %s; can't fudge\" % attr\n\n            if not thing._deleted:\n                print \"%s is missing %r, deleting\" % (thing._fullname, attr)\n                thing._deleted = True\n\n            thing._commit()\n\n            if not fudge_links:\n                break\n\n\ndef find_recent_broken_things(from_time = None, to_time = None,\n                              delete = False):\n    \"\"\"\n        Occasionally (usually during app-server crashes), Things will\n        be partially written out to the database. Things missing data\n        attributes break the contract for these things, which often\n        breaks various pages. This function hunts for and destroys\n        them as appropriate.\n    \"\"\"\n    from r2.models import Link, Comment\n    from r2.lib.db.operators import desc\n\n    from_time = from_time or timeago('1 hour')\n    to_time = to_time or datetime.now(g.tz)\n\n    for cls in (Link, Comment):\n        q = cls._query(cls.c._date > from_time,\n                       cls.c._date < to_time,\n                       data=True,\n                       sort=desc('_date'))\n        for thing in fetch_things2(q):\n            fix_if_broken(thing, delete = delete)\n\n\ndef timeit(func):\n    \"Run some function, and return (RunTimeInSeconds,Result)\"\n    before=time.time()\n    res=func()\n    return (time.time()-before,res)\ndef lineno():\n    \"Returns the current line number in our program.\"\n    import inspect\n    print \"%s\\t%s\" % (datetime.now(),inspect.currentframe().f_back.f_lineno)\n\ndef IteratorFilter(iterator, fn):\n    for x in iterator:\n        if fn(x):\n            yield x\n\ndef UniqueIterator(iterator, key = lambda x: x):\n    \"\"\"\n    Takes an iterator and returns an iterator that returns only the\n    first occurence of each entry\n    \"\"\"\n    so_far = set()\n    def no_dups(x):\n        k = key(x)\n        if k in so_far:\n            return False\n        else:\n            so_far.add(k)\n            return True\n\n    return IteratorFilter(iterator, no_dups)\n\ndef safe_eval_str(unsafe_str):\n    return unsafe_str.replace('\\\\x3d', '=').replace('\\\\x26', '&')\n\nrx_whitespace = re.compile('\\s+', re.UNICODE)\nrx_notsafe = re.compile('\\W+', re.UNICODE)\nrx_underscore = re.compile('_+', re.UNICODE)\ndef title_to_url(title, max_length = 50):\n    \"\"\"Takes a string and makes it suitable for use in URLs\"\"\"\n    title = _force_unicode(title)           #make sure the title is unicode\n    title = rx_whitespace.sub('_', title)   #remove whitespace\n    title = rx_notsafe.sub('', title)       #remove non-printables\n    title = rx_underscore.sub('_', title)   #remove double underscores\n    title = title.strip('_')                #remove trailing underscores\n    title = title.lower()                   #lowercase the title\n\n    if len(title) > max_length:\n        #truncate to nearest word\n        title = title[:max_length]\n        last_word = title.rfind('_')\n        if (last_word > 0):\n            title = title[:last_word]\n    return title or \"_\"\n\n\ndef unicode_title_to_ascii(title, max_length=50):\n    title = _force_unicode(title)\n    title = unidecode.unidecode(title)\n    return title_to_url(title, max_length)\n\n\ndef dbg(s):\n    import sys\n    sys.stderr.write('%s\\n' % (s,))\n\ndef trace(fn):\n    def new_fn(*a,**kw):\n        ret = fn(*a,**kw)\n        dbg(\"Fn: %s; a=%s; kw=%s\\nRet: %s\"\n            % (fn,a,kw,ret))\n        return ret\n    return new_fn\n\ndef common_subdomain(domain1, domain2):\n    if not domain1 or not domain2:\n        return \"\"\n    domain1 = domain1.split(\":\")[0]\n    domain2 = domain2.split(\":\")[0]\n    if len(domain1) > len(domain2):\n        domain1, domain2 = domain2, domain1\n\n    if domain1 == domain2:\n        return domain1\n    else:\n        dom = domain1.split(\".\")\n        for i in range(len(dom), 1, -1):\n            d = '.'.join(dom[-i:])\n            if domain2.endswith(d):\n                return d\n    return \"\"\n\n\ndef url_links_builder(url, exclude=None, num=None, after=None, reverse=None,\n                      count=None, public_srs_only=False):\n    from r2.lib.template_helpers import add_sr\n    from r2.models import IDBuilder, Link, NotFound, Subreddit\n    from operator import attrgetter\n\n    if url.startswith('/'):\n        url = add_sr(url, force_hostname=True)\n\n    try:\n        links = Link._by_url(url, None)\n    except NotFound:\n        links = []\n\n    links = [ link for link in links\n                   if link._fullname != exclude ]\n\n    if public_srs_only and not c.user_is_admin:\n        subreddits = Subreddit._byID([link.sr_id for link in links], data=True)\n        links = [link for link in links\n                 if subreddits[link.sr_id].type not in Subreddit.private_types]\n\n    links.sort(key=attrgetter('num_comments'), reverse=True)\n\n    # don't show removed links in duplicates unless admin or mod\n    # or unless it's your own post\n    def include_link(link):\n        return (not link._spam or\n                (c.user_is_loggedin and\n                    (link.author_id == c.user._id or\n                        c.user_is_admin or\n                        link.subreddit.is_moderator(c.user))))\n\n    builder = IDBuilder([link._fullname for link in links], skip=True,\n                        keep_fn=include_link, num=num, after=after,\n                        reverse=reverse, count=count)\n\n    return builder\n\nclass TimeoutFunctionException(Exception):\n    pass\n\nclass TimeoutFunction:\n    \"\"\"Force an operation to timeout after N seconds. Works with POSIX\n       signals, so it's not safe to use in a multi-treaded environment\"\"\"\n    def __init__(self, function, timeout):\n        self.timeout = timeout\n        self.function = function\n\n    def handle_timeout(self, signum, frame):\n        raise TimeoutFunctionException()\n\n    def __call__(self, *args, **kwargs):\n        # can only be called from the main thread\n        old = signal.signal(signal.SIGALRM, self.handle_timeout)\n        signal.alarm(self.timeout)\n        try:\n            result = self.function(*args, **kwargs)\n        finally:\n            signal.alarm(0)\n            signal.signal(signal.SIGALRM, old)\n        return result\n\n\ndef to_date(d):\n    if isinstance(d, datetime):\n        return d.date()\n    return d\n\ndef to_datetime(d):\n    if type(d) == date:\n        return datetime(d.year, d.month, d.day)\n    return d\n\ndef in_chunks(it, size=25):\n    chunk = []\n    it = iter(it)\n    try:\n        while True:\n            chunk.append(it.next())\n            if len(chunk) >= size:\n                yield chunk\n                chunk = []\n    except StopIteration:\n        if chunk:\n            yield chunk\n\n\ndef progress(it, verbosity=100, key=repr, estimate=None, persec=True):\n    \"\"\"An iterator that yields everything from `it', but prints progress\n       information along the way, including time-estimates if\n       possible\"\"\"\n    from itertools import islice\n    from datetime import datetime\n    import sys\n\n    now = start = datetime.now()\n    elapsed = start - start\n\n    # try to guess at the estimate if we can\n    if estimate is None:\n        try:\n            estimate = len(it)\n        except:\n            pass\n\n    def timedelta_to_seconds(td):\n        return td.days * (24*60*60) + td.seconds + (float(td.microseconds) / 1000000)\n    def format_timedelta(td, sep=''):\n        ret = []\n        s = timedelta_to_seconds(td)\n        if s < 0:\n            neg = True\n            s *= -1\n        else:\n            neg = False\n\n        if s >= (24*60*60):\n            days = int(s//(24*60*60))\n            ret.append('%dd' % days)\n            s -= days*(24*60*60)\n        if s >= 60*60:\n            hours = int(s//(60*60))\n            ret.append('%dh' % hours)\n            s -= hours*(60*60)\n        if s >= 60:\n            minutes = int(s//60)\n            ret.append('%dm' % minutes)\n            s -= minutes*60\n        if s >= 1:\n            seconds = int(s)\n            ret.append('%ds' % seconds)\n            s -= seconds\n\n        if not ret:\n            return '0s'\n\n        return ('-' if neg else '') + sep.join(ret)\n    def format_datetime(dt, show_date=False):\n        if show_date:\n            return dt.strftime('%Y-%m-%d %H:%M')\n        else:\n            return dt.strftime('%H:%M:%S')\n    def deq(dt1, dt2):\n        \"Indicates whether the two datetimes' dates describe the same (day,month,year)\"\n        d1, d2 = dt1.date(), dt2.date()\n        return (    d1.day   == d2.day\n                and d1.month == d2.month\n                and d1.year  == d2.year)\n\n    sys.stderr.write('Starting at %s\\n' % (start,))\n\n    # we're going to islice it so we need to start an iterator\n    it = iter(it)\n\n    seen = 0\n    while True:\n        this_chunk = 0\n        thischunk_started = datetime.now()\n\n        # the simple bit: just iterate and yield\n        for item in islice(it, verbosity):\n            this_chunk += 1\n            seen += 1\n            yield item\n\n        if this_chunk < verbosity:\n            # we're done, the iterator is empty\n            break\n\n        now = datetime.now()\n        elapsed = now - start\n        thischunk_seconds = timedelta_to_seconds(now - thischunk_started)\n\n        if estimate:\n            # the estimate is based on the total number of items that\n            # we've processed in the total amount of time that's\n            # passed, so it should smooth over momentary spikes in\n            # speed (but will take a while to adjust to long-term\n            # changes in speed)\n            remaining = ((elapsed/seen)*estimate)-elapsed\n            completion = now + remaining\n            count_str = ('%d/%d %.2f%%'\n                         % (seen, estimate, float(seen)/estimate*100))\n            completion_str = format_datetime(completion, not deq(completion,now))\n            estimate_str = (' (%s remaining; completion %s)'\n                            % (format_timedelta(remaining),\n                               completion_str))\n        else:\n            count_str = '%d' % seen\n            estimate_str = ''\n\n        if key:\n            key_str = ': %s' % key(item)\n        else:\n            key_str = ''\n\n        # unlike the estimate, the persec count is the number per\n        # second for *this* batch only, without smoothing\n        if persec and thischunk_seconds > 0:\n            persec_str = ' (%.1f/s)' % (float(this_chunk)/thischunk_seconds,)\n        else:\n            persec_str = ''\n\n        sys.stderr.write('%s%s, %s%s%s\\n'\n                         % (count_str, persec_str,\n                            format_timedelta(elapsed), estimate_str, key_str))\n\n    now = datetime.now()\n    elapsed = now - start\n    elapsed_seconds = timedelta_to_seconds(elapsed)\n    if persec and seen > 0 and elapsed_seconds > 0:\n        persec_str = ' (@%.1f/sec)' % (float(seen)/elapsed_seconds)\n    else:\n        persec_str = ''\n    sys.stderr.write('Processed %d%s items in %s..%s (%s)\\n'\n                     % (seen,\n                        persec_str,\n                        format_datetime(start, not deq(start, now)),\n                        format_datetime(now, not deq(start, now)),\n                        format_timedelta(elapsed)))\n\nclass Hell(object):\n    def __str__(self):\n        return \"boom!\"\n\nclass Bomb(object):\n    @classmethod\n    def __getattr__(cls, key):\n        raise Hell()\n\n    @classmethod\n    def __setattr__(cls, key, val):\n        raise Hell()\n\n    @classmethod\n    def __repr__(cls):\n        raise Hell()\n\n\nclass SimpleSillyStub(object):\n    \"\"\"A simple stub object that does nothing when you call its methods.\"\"\"\n    def __nonzero__(self):\n        return False\n\n    def __getattr__(self, name):\n        return self.stub\n\n    def stub(self, *args, **kwargs):\n        pass\n\n    __exit__ = __enter__ = stub\n\n\ndef strordict_fullname(item, key='fullname'):\n    \"\"\"Sometimes we migrate AMQP queues from simple strings to pickled\n    dictionaries. During the migratory period there may be items in\n    the queue of both types, so this function tries to detect which\n    the item is. It shouldn't really be used on a given queue for more\n    than a few hours or days\"\"\"\n    try:\n        d = pickle.loads(item)\n    except:\n        d = {key: item}\n\n    if (not isinstance(d, dict)\n        or key not in d\n        or not isinstance(d[key], str)):\n        raise ValueError('Error trying to migrate %r (%r)'\n                         % (item, d))\n\n    return d\n\ndef thread_dump(*a):\n    import sys, traceback\n    from datetime import datetime\n\n    sys.stderr.write('%(t)s Thread Dump @%(d)s %(t)s\\n' % dict(t='*'*15,\n                                                               d=datetime.now()))\n\n    for thread_id, stack in sys._current_frames().items():\n        sys.stderr.write('\\t-- Thread ID: %s--\\n' %  (thread_id,))\n\n        for filename, lineno, fnname, line in traceback.extract_stack(stack):\n            sys.stderr.write('\\t\\t%(filename)s(%(lineno)d): %(fnname)s\\n'\n                             % dict(filename=filename, lineno=lineno, fnname=fnname))\n            sys.stderr.write('\\t\\t\\t%(line)s\\n' % dict(line=line))\n\n\ndef constant_time_compare(actual, expected):\n    \"\"\"\n    Returns True if the two strings are equal, False otherwise\n\n    The time taken is dependent on the number of characters provided\n    instead of the number of characters that match.\n\n    When we upgrade to Python 2.7.7 or newer, we should use hmac.compare_digest\n    instead.\n    \"\"\"\n    actual_len   = len(actual)\n    expected_len = len(expected)\n    result = actual_len ^ expected_len\n    if expected_len > 0:\n        for i in xrange(actual_len):\n            result |= ord(actual[i]) ^ ord(expected[i % expected_len])\n    return result == 0\n\n\ndef extract_urls_from_markdown(md):\n    \"Extract URLs that will be hot links from a piece of raw Markdown.\"\n\n    html = snudown.markdown(_force_utf8(md))\n    links = SoupStrainer(\"a\")\n\n    for link in BeautifulSoup(html, parseOnlyThese=links):\n        url = link.get('href')\n        if url:\n            yield url\n\n\ndef extract_user_mentions(text):\n    \"\"\"Return a set of all usernames (lowercased) mentioned in Markdown text.\n\n    This function works by processing the Markdown, and then looking through\n    all links in the resulting HTML. Any links that start with /u/ (as a\n    relative link) are considered to be a \"mention\", so this will mostly just\n    catch the links created by our auto-linking of /u/ and u/.\n\n    Note that the usernames are converted to lowercase and added to a set,\n    so only unique mentions will be returned.\n    \"\"\"\n    from r2.lib.validator import chkuser\n    usernames = set()\n\n    for url in extract_urls_from_markdown(text):\n        if not url.startswith(\"/u/\"):\n            continue\n\n        username = url[len(\"/u/\"):]\n        if not chkuser(username):\n            continue\n\n        usernames.add(username.lower())\n\n    return usernames\n\n\ndef summarize_markdown(md):\n    \"\"\"Get the first paragraph of some Markdown text, potentially truncated.\"\"\"\n\n    first_graf, sep, rest = md.partition(\"\\n\\n\")\n    return first_graf[:500]\n\n\ndef blockquote_text(text):\n    \"\"\"Wrap a chunk of Markdown text into a blockquote.\"\"\"\n    return \"\\n\".join(\"> \" + line for line in text.splitlines())\n\n\ndef find_containing_network(ip_ranges, address):\n    \"\"\"Find an IP network that contains the given address.\"\"\"\n    addr = ipaddress.ip_address(address)\n    for network in ip_ranges:\n        if addr in network:\n            return network\n    return None\n\n\ndef is_throttled(address):\n    \"\"\"Determine if an IP address is in a throttled range.\"\"\"\n    return bool(find_containing_network(g.throttles, address))\n\n\ndef parse_http_basic(authorization_header):\n    \"\"\"Parse the username/credentials out of an HTTP Basic Auth header.\n\n    Raises RequirementException if anything is uncool.\n    \"\"\"\n    auth_scheme, auth_token = require_split(authorization_header, 2)\n    require(auth_scheme.lower() == \"basic\")\n    try:\n        auth_data = base64.b64decode(auth_token)\n    except TypeError:\n        raise RequirementException\n    return require_split(auth_data, 2, \":\")\n\n\ndef simple_traceback(limit):\n    \"\"\"Generate a pared-down traceback that's human readable but small.\n\n    `limit` is how many frames of the stack to put in the traceback.\n\n    \"\"\"\n\n    stack_trace = traceback.extract_stack(limit=limit)[:-2]\n    return \"\\n\".join(\"-\".join((os.path.basename(filename),\n                               function_name,\n                               str(line_number),\n                              ))\n                     for filename, line_number, function_name, text\n                     in stack_trace)\n\n\ndef weighted_lottery(weights, _random=random.random):\n    \"\"\"Randomly choose a key from a dict where values are weights.\n\n    Weights should be non-negative numbers, and at least one weight must be\n    non-zero. The probability that a key will be selected is proportional to\n    its weight relative to the sum of all weights. Keys with zero weight will\n    be ignored.\n\n    Raises ValueError if weights is empty or contains a negative weight.\n    \"\"\"\n\n    total = sum(weights.itervalues())\n    if total <= 0:\n        raise ValueError(\"total weight must be positive\")\n\n    r = _random() * total\n    t = 0\n    for key, weight in weights.iteritems():\n        if weight < 0:\n            raise ValueError(\"weight for %r must be non-negative\" % key)\n        t += weight\n        if t > r:\n            return key\n\n    # this point should never be reached\n    raise ValueError(\n        \"weighted_lottery messed up: r=%r, t=%r, total=%r\" % (r, t, total))\n\n\nclass GoldPrice(object):\n    \"\"\"Simple price math / formatting type.\n\n    Prices are assumed to be USD at the moment.\n\n    \"\"\"\n    def __init__(self, decimal):\n        self.decimal = Decimal(decimal)\n\n    def __mul__(self, other):\n        return type(self)(self.decimal * other)\n\n    def __div__(self, other):\n        return type(self)(self.decimal / other)\n\n    def __str__(self):\n        return \"$%s\" % self.decimal.quantize(Decimal(\"1.00\"))\n\n    def __repr__(self):\n        return \"%s(%s)\" % (type(self).__name__, self)\n\n    @property\n    def pennies(self):\n        return int(self.decimal * 100)\n\n\ndef config_gold_price(v, key=None, data=None):\n    return GoldPrice(v)\n\n\ndef canonicalize_email(email):\n    \"\"\"Return the given email address without various localpart manglings.\n\n    a.s.d.f+something@gmail.com --> asdf@gmail.com\n\n    This is not at all RFC-compliant or correct. It's only intended to be a\n    quick heuristic to remove commonly used mangling techniques.\n\n    \"\"\"\n\n    if not email:\n        return \"\"\n\n    email = _force_utf8(email.lower())\n\n    localpart, at, domain = email.partition(\"@\")\n    if not at or \"@\" in domain:\n        return \"\"\n\n    localpart = localpart.replace(\".\", \"\")\n    localpart = localpart.partition(\"+\")[0]\n\n    return localpart + \"@\" + domain\n\n\ndef precise_format_timedelta(delta, locale, threshold=.85, decimals=2):\n    \"\"\"Like babel.dates.format_datetime but with adjustable precision\"\"\"\n    seconds = delta.total_seconds()\n\n    for unit, secs_per_unit in TIMEDELTA_UNITS:\n        value = abs(seconds) / secs_per_unit\n        if value >= threshold:\n            plural_form = locale.plural_form(value)\n            pattern = None\n            for choice in (unit + ':medium', unit):\n                patterns = locale._data['unit_patterns'].get(choice)\n                if patterns is not None:\n                    pattern = patterns[plural_form]\n                    break\n            if pattern is None:\n                return u''\n            decimals = int(decimals)\n            format_string = \"%.\" + str(decimals) + \"f\"\n            return pattern.replace('{0}', format_string % value)\n    return u''\n\n\ndef parse_ini_file(config_file):\n    \"\"\"Given an open file, read and parse it like an ini file.\"\"\"\n\n    parser = ConfigParser.RawConfigParser()\n    parser.optionxform = str  # ensure keys are case-sensitive as expected\n    parser.readfp(config_file)\n    return parser\n\ndef fuzz_activity(count):\n    \"\"\"Add some jitter to an activity metric to maintain privacy.\"\"\"\n    # decay constant is e**(-x / 60)\n    decay = math.exp(float(-count) / 60)\n    jitter = round(5 * decay)\n    return count + random.randint(0, jitter)\n\n\ndef shuffle_slice(x, start, stop=None):\n    \"\"\"Given a list, shuffle a portion of the list in-place, returning None.\n\n    This uses a knuth shuffle borrowed from http://stackoverflow.com/a/11706463\n    which is a slightly tweaked version of shuffle from the `random` stdlib:\n    https://hg.python.org/cpython/file/8962d1c442a6/Lib/random.py#l256\n    \"\"\"\n    if stop is None:\n        stop = len(x)\n\n    for i in reversed(xrange(start + 1, stop)):\n        j = random.randint(start, i)\n        x[i], x[j] = x[j], x[i]\n\n\n# port of https://docs.python.org/dev/library/itertools.html#itertools-recipes\ndef partition(pred, iterable):\n    \"Use a predicate to partition entries into false entries and true entries\"\n    # partition(is_odd, range(10)) --> 0 2 4 6 8   and  1 3 5 7 9\n    t1, t2 = itertools.tee(iterable)\n    return itertools.ifilterfalse(pred, t1), itertools.ifilter(pred, t2)\n\n# http://docs.python.org/2/library/itertools.html#recipes\ndef roundrobin(*iterables):\n    \"roundrobin('ABC', 'D', 'EF') --> A D E B F C\"\n    # Recipe credited to George Sakkis\n    pending = len(iterables)\n    nexts = itertools.cycle(iter(it).next for it in iterables)\n    while pending:\n        try:\n            for next in nexts:\n                yield next()\n        except StopIteration:\n            pending -= 1\n            nexts = itertools.cycle(itertools.islice(nexts, pending))\n\n\ndef lowercase_keys_recursively(subject):\n    \"\"\"Return a dict with all keys lowercased (recursively).\"\"\"\n    lowercased = dict()\n    for key, val in subject.iteritems():\n        if isinstance(val, dict):\n            val = lowercase_keys_recursively(val)\n        lowercased[key.lower()] = val\n\n    return lowercased\n\n\ndef sampled(live_config_var):\n    \"\"\"Wrap a function that should only actually run occasionally\n\n    The wrapped function will only actually execute at the rate\n    specified by the live_config sample rate given.\n\n    Example:\n\n    @sampled(\"foobar_sample_rate\")\n    def foobar():\n        ...\n\n    If g.live_config[\"foobar_sample_rate\"] is set to 0.5, foobar()\n    will only execute 50% of the time when it is called.\n\n    \"\"\"\n    def sampled_decorator(fn):\n        @functools.wraps(fn)\n        def sampled_fn(*a, **kw):\n            if random.random() > g.live_config[live_config_var]:\n                return None\n            else:\n                return fn(*a, **kw)\n        return sampled_fn\n    return sampled_decorator\n\n\ndef squelch_exceptions(fn):\n    \"\"\"Wrap a function to log and suppress all internal exceptions\n\n    When running in debug mode, the exception will be propagated, but\n    in production environments, the function exception will be logged,\n    then suppressed.\n\n    Use of this decorator is not an excuse to not handle exceptions\n\n    \"\"\"\n    @functools.wraps(fn)\n    def squelched_fn(*a, **kw):\n        try:\n            return fn(*a, **kw)\n        except BaseException:\n            if g.debug:\n                raise\n            else:\n                # log.exception will send a stack trace as well\n                g.log.exception(\"squelching exception\")\n    return squelched_fn\n\n\nEPOCH = datetime(1970, 1, 1, tzinfo=pytz.UTC)\n\n\ndef epoch_timestamp(dt):\n    \"\"\"Returns the number of seconds from the epoch to date.\n\n    :param datetime dt: datetime (with time zone)\n    :rtype: float\n    \"\"\"\n    return (dt - EPOCH).total_seconds()\n\n\ndef to_epoch_milliseconds(dt):\n    \"\"\"Returns the number of milliseconds from the epoch to date.\n\n    :param datetime dt: datetime (with time zone)\n    :rtype: int\n    \"\"\"\n    return int(math.floor(1000. * epoch_timestamp(dt)))\n\n\ndef from_epoch_milliseconds(ms):\n    \"\"\"Convert milliseconds from the epoch to UTC datetime.\n\n    :param int ms: milliseconds since the epoch\n    :rtype: :py:class:`datetime.datetime`\n    \"\"\"\n    seconds = int(ms / 1000.)\n    microseconds = (ms - 1000 * seconds) * 1000.\n    return EPOCH + timedelta(seconds=seconds, microseconds=microseconds)\n\n\ndef rate_limiter(max_per_second):\n    \"\"\"Limit number of calls to returned closure per second to max_per_second\n    algorithm adapted from here:\n        http://blog.gregburek.com/2011/12/05/Rate-limiting-with-decorators/\n    \"\"\"\n    min_interval = 1.0 / float(max_per_second)\n    # last_time_called needs to be a list so we can do a closure on it\n    last_time_called = [0.0]\n\n    def throttler():\n        elapsed = time.clock() - last_time_called[0]\n        left_to_wait = min_interval - elapsed\n        if left_to_wait > 0:\n            time.sleep(left_to_wait)\n        last_time_called[0] = time.clock()\n    return throttler\n\n\ndef rate_limited_generator(rate_limit_per_second, iterable):\n    \"\"\"Yield from iterable without going over rate limit\"\"\"\n    throttler = rate_limiter(rate_limit_per_second)\n    for i in iterable:\n        throttler()\n        yield i\n"
  },
  {
    "path": "r2/r2/lib/validator/__init__.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom pylons import config\n\nfrom validator import *\n\nif config['r2.import_private']:\n    from r2admin.lib.validator import *\n"
  },
  {
    "path": "r2/r2/lib/validator/preferences.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom pylons import tmpl_context as c\nfrom pylons import app_globals as g\n\nfrom r2.config import feature\nfrom r2.lib.menus import CommentSortMenu\nfrom r2.lib.validator.validator import (\n    VBoolean,\n    VInt,\n    VLang,\n    VOneOf,\n    VSRByName,\n)\nfrom r2.lib.errors import errors\nfrom r2.models import Subreddit, NotFound\n\n# Validators that map directly to Account._preference_attrs\n# The key MUST be the same string as the value in _preference_attrs\n# Non-preference validators should be added to to the controller\n# method directly (see PostController.POST_options)\nPREFS_VALIDATORS = dict(\n    pref_clickgadget=VBoolean('clickgadget'),\n    pref_organic=VBoolean('organic'),\n    pref_newwindow=VBoolean('newwindow'),\n    pref_public_votes=VBoolean('public_votes'),\n    pref_hide_from_robots=VBoolean('hide_from_robots'),\n    pref_hide_ups=VBoolean('hide_ups'),\n    pref_hide_downs=VBoolean('hide_downs'),\n    pref_over_18=VBoolean('over_18'),\n    pref_research=VBoolean('research'),\n    pref_numsites=VInt('numsites', 1, 100),\n    pref_lang=VLang('lang'),\n    pref_media=VOneOf('media', ('on', 'off', 'subreddit')),\n    # pref_media_preview=VOneOf('media_preview', ('on', 'off', 'subreddit')),\n    pref_compress=VBoolean('compress'),\n    pref_domain_details=VBoolean('domain_details'),\n    pref_min_link_score=VInt('min_link_score', -100, 100),\n    pref_min_comment_score=VInt('min_comment_score', -100, 100),\n    pref_num_comments=VInt('num_comments', 1, g.max_comments,\n                           default=g.num_comments),\n    pref_highlight_controversial=VBoolean('highlight_controversial'),\n    pref_default_comment_sort=VOneOf('default_comment_sort',\n                                     CommentSortMenu.visible_options()),\n    pref_ignore_suggested_sort=VBoolean(\"ignore_suggested_sort\"),\n    pref_show_stylesheets=VBoolean('show_stylesheets'),\n    pref_show_flair=VBoolean('show_flair'),\n    pref_show_link_flair=VBoolean('show_link_flair'),\n    pref_no_profanity=VBoolean('no_profanity'),\n    pref_label_nsfw=VBoolean('label_nsfw'),\n    pref_show_promote=VBoolean('show_promote'),\n    pref_mark_messages_read=VBoolean(\"mark_messages_read\"),\n    pref_threaded_messages=VBoolean(\"threaded_messages\"),\n    pref_collapse_read_messages=VBoolean(\"collapse_read_messages\"),\n    pref_email_messages=VBoolean(\"email_messages\"),\n    pref_private_feeds=VBoolean(\"private_feeds\"),\n    pref_store_visits=VBoolean('store_visits'),\n    pref_hide_ads=VBoolean(\"hide_ads\"),\n    pref_show_trending=VBoolean(\"show_trending\"),\n    pref_highlight_new_comments=VBoolean(\"highlight_new_comments\"),\n    pref_show_gold_expiration=VBoolean(\"show_gold_expiration\"),\n    pref_monitor_mentions=VBoolean(\"monitor_mentions\"),\n    pref_hide_locationbar=VBoolean(\"hide_locationbar\"),\n    pref_use_global_defaults=VBoolean(\"use_global_defaults\"),\n    pref_creddit_autorenew=VBoolean(\"creddit_autorenew\"),\n    pref_enable_default_themes=VBoolean(\"enable_default_themes\", False),\n    pref_default_theme_sr=VSRByName(\"theme_selector\", required=False,\n        return_srname=True),\n    pref_other_theme=VSRByName(\"other_theme\", required=False,\n        return_srname=True),\n    pref_beta=VBoolean('beta'),\n    pref_legacy_search=VBoolean('legacy_search'),\n    pref_threaded_modmail=VBoolean('threaded_modmail', False),\n)\n\n\ndef set_prefs(user, prefs):\n    for k, v in prefs.iteritems():\n        if k == 'pref_beta' and v and not getattr(user, 'pref_beta', False):\n            # If a user newly opted into beta, we want to subscribe them\n            # to the beta subreddit.\n            try:\n                sr = Subreddit._by_name(g.beta_sr)\n                if not sr.is_subscriber(user):\n                    sr.add_subscriber(user)\n            except NotFound:\n                g.log.warning(\"Could not find beta subreddit '%s'. It may \"\n                              \"need to be created.\" % g.beta_sr)\n\n        setattr(user, k, v)\n\ndef filter_prefs(prefs, user):\n    # replace stylesheet_override with other_theme if it doesn't exist\n    if feature.is_enabled('stylesheets_everywhere', user=user):\n        if not prefs[\"pref_default_theme_sr\"]:\n            if prefs.get(\"pref_other_theme\", False):\n                prefs[\"pref_default_theme_sr\"] = prefs[\"pref_other_theme\"]\n\n    for pref_key in prefs.keys():\n        if pref_key not in user._preference_attrs:\n            del prefs[pref_key]\n\n    #temporary. eventually we'll change pref_clickgadget to an\n    #integer preference\n    prefs['pref_clickgadget'] = 5 if prefs['pref_clickgadget'] else 0\n    if user.pref_show_promote is None:\n        prefs['pref_show_promote'] = None\n    elif not prefs.get('pref_show_promote'):\n        prefs['pref_show_promote'] = False\n\n    if not prefs.get(\"pref_over_18\") or not user.pref_over_18:\n        prefs['pref_no_profanity'] = True\n\n    if prefs.get(\"pref_no_profanity\") or user.pref_no_profanity:\n        prefs['pref_label_nsfw'] = True\n\n    # don't update the hide_ads pref if they don't have gold\n    if not user.gold:\n        del prefs['pref_hide_ads']\n        del prefs['pref_show_gold_expiration']\n\n    if not (user.gold or user.is_moderator_somewhere):\n        prefs['pref_highlight_new_comments'] = True\n\n    # check stylesheet override\n    if (feature.is_enabled('stylesheets_everywhere', user=user) and\n            prefs['pref_default_theme_sr']):\n        override_sr = Subreddit._by_name(prefs['pref_default_theme_sr'])\n        if not override_sr:\n            del prefs['pref_default_theme_sr']\n            if prefs['pref_enable_default_themes']:\n                c.errors.add(c.errors.add(errors.SUBREDDIT_REQUIRED, field=\"stylesheet_override\"))\n        else:\n            if override_sr.can_view(user):\n                prefs['pref_default_theme_sr'] = override_sr.name\n            else:\n                # don't update if they can't view the chosen subreddit\n                c.errors.add(errors.SUBREDDIT_NO_ACCESS, field='stylesheet_override')\n                del prefs['pref_default_theme_sr']\n"
  },
  {
    "path": "r2/r2/lib/validator/validator.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport cgi\nimport json\nfrom collections import OrderedDict\nfrom decimal import Decimal\n\nfrom pylons import request, response\nfrom pylons import tmpl_context as c\nfrom pylons import app_globals as g\nfrom pylons.i18n import _\nfrom pylons.controllers.util import abort\n\nfrom r2.config import feature\nfrom r2.config.extensions import api_type, is_api\nfrom r2.lib import utils, captcha, promote, totp, ratelimit\nfrom r2.lib.filters import unkeep_space, websafe, _force_unicode, _force_utf8\nfrom r2.lib.filters import markdown_souptest\nfrom r2.lib.db import tdb_cassandra\nfrom r2.lib.db.operators import asc, desc\nfrom r2.lib.souptest import (\n    SoupError,\n    SoupDetectedCrasherError,\n    SoupUnsupportedEntityError,\n)\nfrom r2.lib.template_helpers import add_sr\nfrom r2.lib.jsonresponse import JQueryResponse, JsonResponse\nfrom r2.lib.permissions import ModeratorPermissionSet\nfrom r2.models import *\nfrom r2.models.rules import MAX_RULES_PER_SUBREDDIT\nfrom r2.models.promo import Location\nfrom r2.lib.authorize import Address, CreditCard\nfrom r2.lib.utils import constant_time_compare\nfrom r2.lib.require import require, require_split, RequirementException\nfrom r2.lib import signing\n\nfrom r2.lib.errors import errors, RedditError, UserRequiredException\nfrom r2.lib.errors import VerifiedUserRequiredException\n\nfrom copy import copy\nfrom datetime import datetime, timedelta\nfrom curses.ascii import isprint\nimport re, inspect\nfrom itertools import chain\nfrom functools import wraps\n\n\ndef can_view_link_comments(article):\n    return (article.subreddit_slow.can_view(c.user) and\n            article.can_view_promo(c.user))\n\n\nclass Validator(object):\n    notes = None\n    default_param = None\n    def __init__(self, param=None, default=None, post=True, get=True, url=True,\n                 get_multiple=False, body=False, docs=None):\n        if param:\n            self.param = param\n        else:\n            self.param = self.default_param\n\n        self.default = default\n        self.post, self.get, self.url, self.docs = post, get, url, docs\n        self.get_multiple = get and get_multiple\n        self.body = body\n        self.has_errors = False\n\n    def set_error(self, error, msg_params={}, field=False, code=None):\n        \"\"\"\n        Adds the provided error to c.errors and flags that it is come\n        from the validator's param\n        \"\"\"\n        if field is False:\n            field = self.param\n\n        c.errors.add(error, msg_params=msg_params, field=field, code=code)\n        self.has_errors = True\n\n    def param_docs(self):\n        param_info = {}\n        for param in filter(None, tup(self.param)):\n            param_info[param] = None\n        return param_info\n\n    def __call__(self, url):\n        self.has_errors = False\n        a = []\n        if self.param:\n            for p in utils.tup(self.param):\n                # cgi.FieldStorage is falsy even if it has a filled value\n                # property. :(\n                post_val = request.POST.get(p)\n                if self.post and (post_val or\n                                  isinstance(post_val, cgi.FieldStorage)):\n                    val = request.POST[p]\n                elif ((self.get_multiple and\n                      (self.get_multiple == True or\n                       p in self.get_multiple)) and\n                      request.GET.getall(p)):\n                    val = request.GET.getall(p)\n                elif self.get and request.GET.get(p):\n                    val = request.GET[p]\n                elif self.url and url.get(p):\n                    val = url[p]\n                elif self.body:\n                    val = request.body\n                else:\n                    val = self.default\n                a.append(val)\n        try:\n            return self.run(*a)\n        except TypeError, e:\n            if str(e).startswith('run() takes'):\n                # Prepend our class name so we know *which* run()\n                raise TypeError('%s.%s' % (type(self).__name__, str(e)))\n            else:\n                raise\n\n\ndef build_arg_list(fn, env):\n    \"\"\"given a fn and and environment the builds a keyword argument list\n    for fn\"\"\"\n    kw = {}\n    argspec = inspect.getargspec(fn)\n\n    # if there is a **kw argument in the fn definition,\n    # just pass along the environment\n    if argspec[2]:\n        kw = env\n    #else for each entry in the arglist set the value from the environment\n    else:\n        #skip self\n        argnames = argspec[0][1:]\n        for name in argnames:\n            if name in env:\n                kw[name] = env[name]\n    return kw\n\ndef _make_validated_kw(fn, simple_vals, param_vals, env):\n    for validator in simple_vals:\n        validator(env)\n    kw = build_arg_list(fn, env)\n    for var, validator in param_vals.iteritems():\n        kw[var] = validator(env)\n    return kw\n\ndef set_api_docs(fn, simple_vals, param_vals, extra_vals=None):\n    doc = fn._api_doc = getattr(fn, '_api_doc', {})\n    param_info = doc.get('parameters', {})\n    notes = doc.get('notes', [])\n    for validator in chain(simple_vals, param_vals.itervalues()):\n        param_docs = validator.param_docs()\n        if validator.docs:\n            param_docs.update(validator.docs)\n        param_info.update(param_docs)\n        if validator.notes:\n            notes.append(validator.notes)\n    if extra_vals:\n        param_info.update(extra_vals)\n    doc['parameters'] = param_info\n    doc['notes'] = notes\n\ndef _validators_handle_csrf(simple_vals, param_vals):\n    for validator in chain(simple_vals, param_vals.itervalues()):\n        if getattr(validator, 'handles_csrf', False):\n            return True\n    return False\n\ndef validate(*simple_vals, **param_vals):\n    \"\"\"Validation decorator that delegates error handling to the controller.\n\n    Runs the validators specified and calls self.on_validation_error to\n    process each error. This allows controllers to define their own fatal\n    error processing logic.\n    \"\"\"\n    def val(fn):\n        @wraps(fn)\n        def newfn(self, *a, **env):\n            try:\n                kw = _make_validated_kw(fn, simple_vals, param_vals, env)\n            except RedditError as err:\n                self.on_validation_error(err)\n\n            for err in c.errors:\n                self.on_validation_error(c.errors[err])\n\n            try:\n                return fn(self, *a, **kw)\n            except RedditError as err:\n                self.on_validation_error(err)\n\n        set_api_docs(newfn, simple_vals, param_vals)\n        newfn.handles_csrf = _validators_handle_csrf(simple_vals, param_vals)\n        return newfn\n    return val\n\n\ndef api_validate(response_type=None, add_api_type_doc=False):\n    \"\"\"\n    Factory for making validators for API calls, since API calls come\n    in two flavors: responsive and unresponsive.  The machinary\n    associated with both is similar, and the error handling identical,\n    so this function abstracts away the kw validation and creation of\n    a Json-y responder object.\n    \"\"\"\n    def wrap(response_function):\n        def _api_validate(*simple_vals, **param_vals):\n            def val(fn):\n                @wraps(fn)\n                def newfn(self, *a, **env):\n                    renderstyle = request.params.get(\"renderstyle\")\n                    if renderstyle:\n                        c.render_style = api_type(renderstyle)\n                    elif not c.extension:\n                        # if the request URL included an extension, don't\n                        # touch the render_style, since it was already set by\n                        # set_extension. if no extension was provided, default\n                        # to response_type.\n                        c.render_style = api_type(response_type)\n\n                    # generate a response object\n                    if response_type == \"html\" and not request.params.get('api_type') == \"json\":\n                        responder = JQueryResponse()\n                    else:\n                        responder = JsonResponse()\n\n                    response.content_type = responder.content_type\n\n                    try:\n                        kw = _make_validated_kw(fn, simple_vals, param_vals, env)\n                        return response_function(self, fn, responder,\n                                                 simple_vals, param_vals, *a, **kw)\n                    except UserRequiredException:\n                        responder.send_failure(errors.USER_REQUIRED)\n                        return self.api_wrapper(responder.make_response())\n                    except VerifiedUserRequiredException:\n                        responder.send_failure(errors.VERIFIED_USER_REQUIRED)\n                        return self.api_wrapper(responder.make_response())\n\n                extra_param_vals = {}\n                if add_api_type_doc:\n                    extra_param_vals = {\n                        \"api_type\": \"the string `json`\",\n                    }\n\n                set_api_docs(newfn, simple_vals, param_vals, extra_param_vals)\n                newfn.handles_csrf = _validators_handle_csrf(simple_vals,\n                                                             param_vals)\n                return newfn\n            return val\n        return _api_validate\n    return wrap\n\n\n@api_validate(\"html\")\ndef noresponse(self, self_method, responder, simple_vals, param_vals, *a, **kw):\n    self_method(self, *a, **kw)\n    return self.api_wrapper({})\n\n@api_validate(\"html\")\ndef textresponse(self, self_method, responder, simple_vals, param_vals, *a, **kw):\n    return self_method(self, *a, **kw)\n\n@api_validate()\ndef json_validate(self, self_method, responder, simple_vals, param_vals, *a, **kw):\n    if c.extension != 'json':\n        abort(404)\n\n    val = self_method(self, responder, *a, **kw)\n    if val is None:\n        val = responder.make_response()\n    return self.api_wrapper(val)\n\ndef _validatedForm(self, self_method, responder, simple_vals, param_vals,\n                  *a, **kw):\n    # generate a form object\n    form = responder(request.POST.get('id', \"body\"))\n\n    # clear out the status line as a courtesy\n    form.set_text(\".status\", \"\")\n\n    # do the actual work\n    val = self_method(self, form, responder, *a, **kw)\n\n    # add data to the output on some errors\n    for validator in chain(simple_vals, param_vals.values()):\n        if (isinstance(validator, VCaptcha) and\n            (form.has_errors('captcha', errors.BAD_CAPTCHA) or\n             (form.has_error() and c.user.needs_captcha()))):\n            form.new_captcha()\n        elif (isinstance(validator, (VRatelimit, VThrottledLogin)) and\n              form.has_errors('ratelimit', errors.RATELIMIT)):\n            form.ratelimit(validator.seconds)\n    if val:\n        return val\n    else:\n        return self.api_wrapper(responder.make_response())\n\n@api_validate(\"html\", add_api_type_doc=True)\ndef validatedForm(self, self_method, responder, simple_vals, param_vals,\n                  *a, **kw):\n    return _validatedForm(self, self_method, responder, simple_vals, param_vals,\n                          *a, **kw)\n\n@api_validate(\"html\", add_api_type_doc=True)\ndef validatedMultipartForm(self, self_method, responder, simple_vals,\n                           param_vals, *a, **kw):\n    def wrapped_self_method(*a, **kw):\n        val = self_method(*a, **kw)\n        if val:\n            return val\n        else:\n            data = json.dumps(responder.make_response())\n            response.content_type = \"text/html\"\n            return ('<html><head><script type=\"text/javascript\">\\n'\n                    'parent.$.handleResponse()(%s)\\n'\n                    '</script></head></html>') % filters.websafe_json(data)\n    return _validatedForm(self, wrapped_self_method, responder, simple_vals,\n                          param_vals, *a, **kw)\n\n\njsonp_callback_rx = re.compile(\"\\\\A[\\\\w$\\\\.\\\"'[\\\\]]+\\\\Z\")\ndef valid_jsonp_callback(callback):\n    return jsonp_callback_rx.match(callback)\n\n\n#### validators ####\nclass nop(Validator):\n    def run(self, x):\n        return x\n\nclass VLang(Validator):\n    @staticmethod\n    def validate_lang(lang, strict=False):\n        if lang in g.all_languages:\n            return lang\n        else:\n            if not strict:\n                return g.lang\n            else:\n                raise ValueError(\"invalid language %r\" % lang)\n    def run(self, lang):\n        return VLang.validate_lang(lang)\n\n    def param_docs(self):\n        return {\n            self.param: \"a valid IETF language tag (underscore separated)\",\n        }\n\n\nclass VRequired(Validator):\n    def __init__(self, param, error, *a, **kw):\n        Validator.__init__(self, param, *a, **kw)\n        self._error = error\n\n    def error(self, e = None):\n        if not e: e = self._error\n        if e:\n            self.set_error(e)\n\n    def run(self, item):\n        if not item:\n            self.error()\n        else:\n            return item\n\nclass VThing(Validator):\n    def __init__(self, param, thingclass, redirect = True, *a, **kw):\n        Validator.__init__(self, param, *a, **kw)\n        self.thingclass = thingclass\n        self.redirect = redirect\n\n    def run(self, thing_id):\n        if thing_id:\n            try:\n                tid = int(thing_id, 36)\n                thing = self.thingclass._byID(tid, True)\n                if thing.__class__ != self.thingclass:\n                    raise TypeError(\"Expected %s, got %s\" %\n                                    (self.thingclass, thing.__class__))\n                return thing\n            except (NotFound, ValueError):\n                if self.redirect:\n                    abort(404, 'page not found')\n                else:\n                    return None\n\n    def param_docs(self):\n        return {\n            self.param: \"The base 36 ID of a \" + self.thingclass.__name__\n        }\n\nclass VLink(VThing):\n    def __init__(self, param, redirect = True, *a, **kw):\n        VThing.__init__(self, param, Link, redirect=redirect, *a, **kw)\n\nclass VPromoCampaign(VThing):\n    def __init__(self, param, redirect = True, *a, **kw):\n        VThing.__init__(self, param, PromoCampaign, *a, **kw)\n\nclass VCommentByID(VThing):\n    def __init__(self, param, redirect = True, *a, **kw):\n        VThing.__init__(self, param, Comment, redirect=redirect, *a, **kw)\n\n\nclass VAward(VThing):\n    def __init__(self, param, redirect = True, *a, **kw):\n        VThing.__init__(self, param, Award, redirect=redirect, *a, **kw)\n\nclass VAwardByCodename(Validator):\n    def run(self, codename, required_fullname=None):\n        if not codename:\n            return self.set_error(errors.NO_TEXT)\n\n        try:\n            a = Award._by_codename(codename)\n        except NotFound:\n            a = None\n\n        if a and required_fullname and a._fullname != required_fullname:\n            return self.set_error(errors.INVALID_OPTION)\n        else:\n            return a\n\nclass VTrophy(VThing):\n    def __init__(self, param, redirect = True, *a, **kw):\n        VThing.__init__(self, param, Trophy, redirect=redirect, *a, **kw)\n\nclass VMessage(Validator):\n    def run(self, message_id):\n        if message_id:\n            try:\n                aid = int(message_id, 36)\n                return Message._byID(aid, True)\n            except (NotFound, ValueError):\n                abort(404, 'page not found')\n\n\nclass VCommentID(Validator):\n    def run(self, cid):\n        if cid:\n            try:\n                cid = int(cid, 36)\n                return Comment._byID(cid, True)\n            except (NotFound, ValueError):\n                pass\n\nclass VMessageID(Validator):\n    def run(self, cid):\n        if cid:\n            try:\n                cid = int(cid, 36)\n                m = Message._byID(cid, True)\n                if not m.can_view_slow():\n                    abort(403, 'forbidden')\n                return m\n            except (NotFound, ValueError):\n                pass\n\nclass VCount(Validator):\n    def run(self, count):\n        if count is None:\n            count = 0\n        try:\n            return max(int(count), 0)\n        except ValueError:\n            return 0\n\n    def param_docs(self):\n        return {\n            self.param: \"a positive integer (default: 0)\",\n        }\n\n\nclass VLimit(Validator):\n    def __init__(self, param, default=25, max_limit=100, **kw):\n        self.default_limit = default\n        self.max_limit = max_limit\n        Validator.__init__(self, param, **kw)\n\n    def run(self, limit):\n        default = c.user.pref_numsites\n        if not default or c.render_style in (\"compact\", api_type(\"compact\")):\n            default = self.default_limit  # TODO: ini param?\n\n        if limit is None:\n            return default\n\n        try:\n            i = int(limit)\n        except ValueError:\n            return default\n\n        return min(max(i, 1), self.max_limit)\n\n    def param_docs(self):\n        return {\n            self.param: \"the maximum number of items desired \"\n                        \"(default: %d, maximum: %d)\" % (self.default_limit,\n                                                        self.max_limit),\n        }\n\nclass VCssMeasure(Validator):\n    measure = re.compile(r\"\\A\\s*[\\d\\.]+\\w{0,3}\\s*\\Z\")\n    def run(self, value):\n        return value if value and self.measure.match(value) else ''\n\n\nclass VLength(Validator):\n    only_whitespace = re.compile(r\"\\A\\s*\\Z\", re.UNICODE)\n\n    def __init__(self, param, max_length,\n                 min_length=0,\n                 empty_error = errors.NO_TEXT,\n                 length_error = errors.TOO_LONG,\n                 short_error=errors.TOO_SHORT,\n                 **kw):\n        Validator.__init__(self, param, **kw)\n        self.max_length = max_length\n        self.min_length = min_length\n        self.length_error = length_error\n        self.short_error = short_error\n        self.empty_error = empty_error\n\n    def run(self, text, text2 = ''):\n        text = text or text2\n        if self.empty_error and (not text or self.only_whitespace.match(text)):\n            self.set_error(self.empty_error, code=400)\n        elif len(text) > self.max_length:\n            self.set_error(self.length_error, {'max_length': self.max_length}, code=400)\n        elif len(text) < self.min_length:\n            self.set_error(self.short_error, {'min_length': self.min_length},\n                           code=400)\n        else:\n            return text\n\n    def param_docs(self):\n        return {\n            self.param:\n                \"a string no longer than %d characters\" % self.max_length,\n        }\n\nclass VUploadLength(VLength):\n    def run(self, upload, text2=''):\n        # upload is expected to be a FieldStorage object\n        if isinstance(upload, cgi.FieldStorage):\n            return VLength.run(self, upload.value, text2)\n        else:\n            self.set_error(self.empty_error, code=400)\n\n    def param_docs(self):\n        kibibytes = self.max_length / 1024\n        return {\n            self.param:\n                \"file upload with maximum size of %d KiB\" % kibibytes,\n        }\n\nclass VPrintable(VLength):\n    def run(self, text, text2 = ''):\n        text = VLength.run(self, text, text2)\n\n        if text is None:\n            return None\n\n        try:\n            if all(isprint(str(x)) for x in text):\n                return str(text)\n        except UnicodeEncodeError:\n            pass\n\n        self.set_error(errors.BAD_STRING, code=400)\n        return None\n\n    def param_docs(self):\n        return {\n            self.param: \"a string up to %d characters long,\"\n                        \" consisting of printable characters.\"\n                            % self.max_length,\n        }\n\nclass VTitle(VLength):\n    def __init__(self, param, max_length = 300, **kw):\n        VLength.__init__(self, param, max_length, **kw)\n\n    def param_docs(self):\n        return {\n            self.param: \"title of the submission. \"\n                        \"up to %d characters long\" % self.max_length,\n        }\n\nclass VMarkdown(Validator):\n    def __init__(self, param, renderer='reddit'):\n        Validator.__init__(self, param)\n        self.renderer = renderer\n\n    def run(self, text, text2=''):\n        text = text or text2\n        try:\n            markdown_souptest(text, renderer=self.renderer)\n            return text\n        except SoupError as e:\n            # Could happen if someone does `&#00;`. It's not a security issue,\n            # it's just unacceptable.\n            # TODO: give a better indication to the user of what happened\n            if isinstance(e, SoupUnsupportedEntityError):\n                abort(400)\n                return\n\n            import sys\n            user = \"???\"\n            if c.user_is_loggedin:\n                user = c.user.name\n\n            # work around CRBUG-464270\n            if isinstance(e, SoupDetectedCrasherError):\n                # We want a general idea of how often this is triggered, and\n                # by what\n                g.log.warning(\"CHROME HAX by %s: %s\" % (user, text))\n                abort(400)\n                return\n\n            g.log.error(\"HAX by %s: %s\" % (user, text))\n            s = sys.exc_info()\n            # reraise the original error with the original stack trace\n            raise s[1], None, s[2]\n\n    def param_docs(self):\n        return {\n            tup(self.param)[0]: \"raw markdown text\",\n        }\n\n\nclass VMarkdownLength(VMarkdown):\n    def __init__(self, param, renderer='reddit', max_length=10000,\n                 empty_error=errors.NO_TEXT, length_error=errors.TOO_LONG):\n        VMarkdown.__init__(self, param, renderer)\n        self.max_length = max_length\n        self.empty_error = empty_error\n        self.length_error = length_error\n\n    def run(self, text, text2=''):\n        text = text or text2\n        text = VLength(self.param, self.max_length,\n                       empty_error=self.empty_error,\n                       length_error=self.length_error).run(text)\n        if text:\n            return VMarkdown.run(self, text)\n        else:\n            return ''\n\n\nclass VSavedCategory(Validator):\n    savedcategory_rx = re.compile(r\"\\A[a-z0-9 _]{1,20}\\Z\")\n\n    def run(self, name):\n        if not name:\n            return\n        name = name.lower()\n        valid = self.savedcategory_rx.match(name)\n        if not valid:\n            self.set_error('BAD_SAVE_CATEGORY')\n            return\n        return name\n\n    def param_docs(self):\n        return {\n            self.param: \"a category name\",\n        }\n\n\nclass VSubredditName(VRequired):\n    def __init__(self, item, allow_language_srs=False, *a, **kw):\n        VRequired.__init__(self, item, errors.BAD_SR_NAME, *a, **kw)\n        self.allow_language_srs = allow_language_srs\n\n    def run(self, name):\n        if name:\n            name = sr_path_rx.sub('\\g<name>', name.strip())\n\n        valid_name = Subreddit.is_valid_name(\n            name, allow_language_srs=self.allow_language_srs)\n\n        if not valid_name:\n            self.set_error(self._error, code=400)\n            return\n\n        return str(name)\n\n    def param_docs(self):\n        return {\n            self.param: \"subreddit name\",\n        }\n\n\nclass VAvailableSubredditName(VSubredditName):\n    def run(self, name):\n        name = VSubredditName.run(self, name)\n        if name:\n            try:\n                a = Subreddit._by_name(name)\n                return self.error(errors.SUBREDDIT_EXISTS)\n            except NotFound:\n                return name\n\n\nclass VSRByName(Validator):\n    def __init__(self, sr_name, required=True, return_srname=False):\n        self.required = required\n        self.return_srname = return_srname\n        Validator.__init__(self, sr_name)\n\n    def run(self, sr_name):\n        if not sr_name:\n            if self.required:\n                self.set_error(errors.BAD_SR_NAME, code=400)\n        else:\n            sr_name = sr_path_rx.sub('\\g<name>', sr_name.strip())\n            try:\n                sr = Subreddit._by_name(sr_name)\n                if self.return_srname:\n                    return sr.name\n                else:\n                    return sr\n            except NotFound:\n                self.set_error(errors.SUBREDDIT_NOEXIST, code=400)\n\n    def param_docs(self):\n        return {\n            self.param: \"subreddit name\",\n        }\n\n\nclass VSRByNames(Validator):\n    \"\"\"Returns a dict mapping subreddit names to subreddit objects.\n\n    sr_names_csv - a comma delimited string of subreddit names\n    required - if true (default) an empty subreddit name list is an error\n\n    \"\"\"\n    def __init__(self, sr_names_csv, required=True):\n        self.required = required\n        Validator.__init__(self, sr_names_csv)\n\n    def run(self, sr_names_csv):\n        if sr_names_csv:\n            sr_names = [sr_path_rx.sub('\\g<name>', s.strip())\n                        for s in sr_names_csv.split(',')]\n            return Subreddit._by_name(sr_names)\n        elif self.required:\n            self.set_error(errors.BAD_SR_NAME, code=400)\n        return {}\n\n    def param_docs(self):\n        return {\n            self.param: \"comma-delimited list of subreddit names\",\n        }\n\n\nclass VSubredditTitle(Validator):\n    def run(self, title):\n        if not title:\n            self.set_error(errors.NO_TITLE)\n        elif len(title) > 100:\n            self.set_error(errors.TITLE_TOO_LONG)\n        else:\n            return title\n\nclass VSubredditDesc(Validator):\n    def run(self, description):\n        if description and len(description) > 500:\n            self.set_error(errors.DESC_TOO_LONG)\n        return unkeep_space(description or '')\n\n\nclass VAvailableSubredditRuleName(Validator):\n    def __init__(self, short_name, updating=False):\n        Validator.__init__(self, short_name)\n        self.updating = updating\n\n    def run(self, short_name):\n        short_name = VLength(\n            self.param,\n            max_length=50,\n            min_length=1,\n        ).run(short_name.strip())\n        if not short_name:\n            return None\n\n        if SubredditRules.get_rule(c.site, short_name):\n            self.set_error(errors.SR_RULE_EXISTS)\n        elif not self.updating:\n            number_rules = len(SubredditRules.get_rules(c.site))\n            if number_rules >= MAX_RULES_PER_SUBREDDIT:\n                self.set_error(errors.SR_RULE_TOO_MANY)\n                return None\n        return short_name\n\n\nclass VSubredditRule(Validator):\n    def run(self, short_name):\n        short_name = VLength(\n            self.param,\n            max_length=50,\n            min_length=1,\n        ).run(short_name.strip())\n        if not short_name:\n            self.set_error(errors.SR_RULE_DOESNT_EXIST)\n            return None\n\n        rule = SubredditRules.get_rule(c.site, short_name)\n        if not rule:\n            self.set_error(errors.SR_RULE_DOESNT_EXIST)\n        else:\n            return rule\n\n\nclass VAccountByName(VRequired):\n    def __init__(self, param, error = errors.USER_DOESNT_EXIST, *a, **kw):\n        VRequired.__init__(self, param, error, *a, **kw)\n\n    def run(self, name):\n        if name:\n            try:\n                return Account._by_name(name)\n            except NotFound: pass\n        return self.error()\n\n    def param_docs(self):\n        return {self.param: \"A valid, existing reddit username\"}\n\n\nclass VFriendOfMine(VAccountByName):\n    def run(self, name):\n        # Must be logged in\n        VUser().run()\n        maybe_friend = VAccountByName.run(self, name)\n        if maybe_friend:\n            friend_rel = Account.get_friend(c.user, maybe_friend)\n            if friend_rel:\n                return friend_rel\n            else:\n                self.error(errors.NOT_FRIEND)\n        return None\n\n\ndef fullname_regex(thing_cls = None, multiple = False):\n    pattern = \"[%s%s]\" % (Relation._type_prefix, Thing._type_prefix)\n    if thing_cls:\n        pattern += utils.to36(thing_cls._type_id)\n    else:\n        pattern += r\"[0-9a-z]+\"\n    pattern += r\"_[0-9a-z]+\"\n    if multiple:\n        pattern = r\"(%s *,? *)+\" % pattern\n    return re.compile(r\"\\A\" + pattern + r\"\\Z\")\n\nclass VByName(Validator):\n    # Lookup tdb_sql.Thing or tdb_cassandra.Thing objects by fullname.\n    splitter = re.compile('[ ,]+')\n    def __init__(self, param, thing_cls=None, multiple=False, limit=None,\n                 error=errors.NO_THING_ID, ignore_missing=False,\n                 backend='sql', **kw):\n        # Limit param only applies when multiple is True\n        if not multiple and limit is not None:\n            raise TypeError('multiple must be True when limit is set')\n        self.thing_cls = thing_cls\n        self.re = fullname_regex(thing_cls)\n        self.multiple = multiple\n        self.limit = limit\n        self._error = error\n        self.ignore_missing = ignore_missing\n        self.backend = backend\n\n        Validator.__init__(self, param, **kw)\n\n    def run(self, items):\n        if self.backend == 'cassandra':\n            # tdb_cassandra.Thing objects can't use the regex\n            if items and self.multiple:\n                items = [item for item in self.splitter.split(items)]\n                if self.limit and len(items) > self.limit:\n                    return self.set_error(errors.TOO_MANY_THING_IDS)\n            if items:\n                try:\n                    return tdb_cassandra.Thing._by_fullname(\n                        items,\n                        ignore_missing=self.ignore_missing,\n                        return_dict=False,\n                    )\n                except tdb_cassandra.NotFound:\n                    pass\n        else:\n            if items and self.multiple:\n                items = [item for item in self.splitter.split(items)\n                         if item and self.re.match(item)]\n                if self.limit and len(items) > self.limit:\n                    return self.set_error(errors.TOO_MANY_THING_IDS)\n            if items and (self.multiple or self.re.match(items)):\n                try:\n                    return Thing._by_fullname(\n                        items,\n                        return_dict=False,\n                        ignore_missing=self.ignore_missing,\n                        data=True,\n                    )\n                except NotFound:\n                    pass\n\n        return self.set_error(self._error)\n\n    def param_docs(self):\n        thingtype = (self.thing_cls or Thing).__name__.lower()\n        if self.multiple:\n            return {\n                self.param: (\"A comma-separated list of %s [fullnames]\"\n                             \"(#fullnames)\" % thingtype)\n            }\n        else:\n            return {\n                self.param: \"[fullname](#fullnames) of a %s\" % thingtype,\n            }\n\nclass VByNameIfAuthor(VByName):\n    def run(self, fullname):\n        thing = VByName.run(self, fullname)\n        if thing:\n            if c.user_is_loggedin and thing.author_id == c.user._id:\n                return thing\n        return self.set_error(errors.NOT_AUTHOR)\n\n    def param_docs(self):\n        return {\n            self.param: \"[fullname](#fullnames) of a thing created by the user\",\n        }\n\nclass VCaptcha(Validator):\n    default_param = ('iden', 'captcha')\n\n    def run(self, iden, solution):\n        if c.user.needs_captcha():\n            valid_captcha = captcha.valid_solution(iden, solution)\n            if not valid_captcha:\n                self.set_error(errors.BAD_CAPTCHA)\n            g.stats.action_event_count(\"captcha\", valid_captcha)\n\n    def param_docs(self):\n        return {\n            self.param[0]: \"the identifier of the CAPTCHA challenge\",\n            self.param[1]: \"the user's response to the CAPTCHA challenge\",\n        }\n\n\nclass VUser(Validator):\n    def run(self):\n        if not c.user_is_loggedin:\n            raise UserRequiredException\n\n\nclass VNotInTimeout(Validator):\n    def run(self, target_fullname=None, fatal=True, action_name=None,\n            details_text=\"\", target=None, subreddit=None):\n        if c.user_is_loggedin and c.user.in_timeout:\n            g.events.timeout_forbidden_event(\n                action_name,\n                details_text=details_text,\n                target=target,\n                target_fullname=target_fullname,\n                subreddit=subreddit,\n                request=request,\n                context=c,\n            )\n            if fatal:\n                request.environ['REDDIT_ERROR_NAME'] = 'IN_TIMEOUT'\n                abort(403, errors.IN_TIMEOUT)\n            return False\n\n\nclass VVerifyPassword(Validator):\n    def __init__(self, param, fatal=True, *a, **kw):\n        Validator.__init__(self, param, *a, **kw)\n        self.fatal = fatal\n\n    def run(self, password):\n        VUser().run()\n        if not valid_password(c.user, password):\n            if self.fatal:\n                abort(403)\n            self.set_error(errors.WRONG_PASSWORD)\n            return None\n        # bcrypt wants a bytestring\n        return _force_utf8(password)\n\n    def param_docs(self):\n        return {\n            self.param: \"the current user's password\",\n        }\n\n\nclass VModhash(Validator):\n    handles_csrf = True\n    default_param = 'uh'\n\n    def __init__(self, param=None, fatal=True, *a, **kw):\n        Validator.__init__(self, param, *a, **kw)\n        self.fatal = fatal\n\n    def run(self, modhash):\n        # OAuth authenticated requests do not require CSRF protection.\n        if c.oauth_user:\n            return\n\n        VUser().run()\n\n        if modhash is None:\n            modhash = request.headers.get('X-Modhash')\n\n        hook = hooks.get_hook(\"modhash.validate\")\n        result = hook.call_until_return(modhash=modhash)\n\n        # if no plugins validate the hash, just check if it's the user name\n        if result is None:\n            result = (modhash == c.user.name)\n\n        if not result:\n            g.stats.simple_event(\"event.modhash.invalid\")\n            if self.fatal:\n                abort(403)\n            self.set_error('INVALID_MODHASH')\n\n    def param_docs(self):\n        return {\n            '%s / X-Modhash header' % self.param: 'a [modhash](#modhashes)',\n        }\n\n\nclass VModhashIfLoggedIn(Validator):\n    handles_csrf = True\n    default_param = 'uh'\n\n    def __init__(self, param=None, fatal=True, *a, **kw):\n        Validator.__init__(self, param, *a, **kw)\n        self.fatal = fatal\n\n    def run(self, modhash):\n        if c.user_is_loggedin:\n            VModhash(fatal=self.fatal).run(modhash)\n\n    def param_docs(self):\n        return {\n            '%s / X-Modhash header' % self.param: 'a [modhash](#modhashes)',\n        }\n\n\nclass VAdmin(Validator):\n    def run(self):\n        if not c.user_is_admin:\n            abort(404, \"page not found\")\n\ndef make_or_admin_secret_cls(base_cls):\n    class VOrAdminSecret(base_cls):\n        handles_csrf = True\n\n        def run(self, secret=None):\n            '''If validation succeeds, return True if the secret was used,\n            False otherwise'''\n            if secret and constant_time_compare(secret,\n                                                g.secrets[\"ADMINSECRET\"]):\n                return True\n            super(VOrAdminSecret, self).run()\n\n            if request.method.upper() != \"GET\":\n                VModhash(fatal=True).run(request.POST.get(\"uh\"))\n\n            return False\n    return VOrAdminSecret\n\nVAdminOrAdminSecret = make_or_admin_secret_cls(VAdmin)\n\nclass VVerifiedUser(VUser):\n    def run(self):\n        VUser.run(self)\n        if not c.user.email_verified:\n            raise VerifiedUserRequiredException\n\nclass VGold(VUser):\n    notes = \"*Requires a subscription to [reddit gold](/gold/about)*\"\n    def run(self):\n        VUser.run(self)\n        if not c.user.gold:\n            abort(403, 'forbidden')\n\nclass VSponsorAdmin(VVerifiedUser):\n    \"\"\"\n    Validator which checks c.user_is_sponsor\n    \"\"\"\n    def user_test(self, thing):\n        return (thing.author_id == c.user._id)\n\n    def run(self, link_id = None):\n        VVerifiedUser.run(self)\n        if c.user_is_sponsor:\n            return\n        abort(403, 'forbidden')\n\nVSponsorAdminOrAdminSecret = make_or_admin_secret_cls(VSponsorAdmin)\n\nclass VSponsor(VUser):\n    \"\"\"\n    Not intended to be used as a check for c.user_is_sponsor, but\n    rather is the user allowed to use the sponsored link system.\n    If a link or campaign is passed in, it also checks whether the user is\n    allowed to edit that particular sponsored link.\n    \"\"\"\n    def user_test(self, thing):\n        return (thing.author_id == c.user._id)\n\n    def run(self, link_id=None, campaign_id=None):\n        assert not (link_id and campaign_id), 'Pass link or campaign, not both'\n\n        VUser.run(self)\n        if c.user_is_sponsor:\n            return\n        elif campaign_id:\n            pc = None\n            try:\n                if '_' in campaign_id:\n                    pc = PromoCampaign._by_fullname(campaign_id, data=True)\n                else:\n                    pc = PromoCampaign._byID36(campaign_id, data=True)\n            except (NotFound, ValueError):\n                pass\n            if pc:\n                link_id = pc.link_id\n        if link_id:\n            try:\n                if '_' in link_id:\n                    t = Link._by_fullname(link_id, True)\n                else:\n                    aid = int(link_id, 36)\n                    t = Link._byID(aid, True)\n                if self.user_test(t):\n                    return\n            except (NotFound, ValueError):\n                pass\n            abort(403, 'forbidden')\n\n\nclass VVerifiedSponsor(VSponsor):\n    def run(self, *args, **kwargs):\n        VVerifiedUser().run()\n\n        return super(VVerifiedSponsor, self).run(*args, **kwargs)\n\n\nclass VEmployee(VVerifiedUser):\n    \"\"\"Validate that user is an employee.\"\"\"\n    def run(self):\n        if not c.user.employee:\n            abort(403, 'forbidden')\n        VVerifiedUser.run(self)\n\n\nclass VSrModerator(Validator):\n    def __init__(self, fatal=True, perms=(), *a, **kw):\n        # If True, abort rather than setting an error\n        self.fatal = fatal\n        self.perms = utils.tup(perms)\n        super(VSrModerator, self).__init__(*a, **kw)\n\n    def run(self):\n        if not (c.user_is_loggedin\n                and c.site.is_moderator_with_perms(c.user, *self.perms)\n                or c.user_is_admin):\n            if self.fatal:\n                abort(403, \"forbidden\")\n            return self.set_error('MOD_REQUIRED', code=403)\n\n\nclass VCanDistinguish(VByName):\n    def run(self, thing_name, how):\n        if c.user_is_loggedin:\n            can_distinguish = False\n            item = VByName.run(self, thing_name)\n\n            if not item:\n                abort(404)\n\n            if item.author_id == c.user._id:\n                if isinstance(item, Message) and c.user.employee:\n                    return True\n                subreddit = item.subreddit_slow\n\n                if (how in (\"yes\", \"no\") and\n                        subreddit.can_distinguish(c.user)):\n                    can_distinguish = True\n                elif (how in (\"special\", \"no\") and\n                        c.user_special_distinguish):\n                    can_distinguish = True\n                elif (how in (\"admin\", \"no\") and\n                        c.user.employee):\n                    can_distinguish = True\n\n                if can_distinguish:\n                    # Don't allow distinguishing for users in timeout\n                    VNotInTimeout().run(target=item, subreddit=subreddit)\n                    return can_distinguish\n\n        abort(403,'forbidden')\n\n    def param_docs(self):\n        return {}\n\nclass VSrCanAlter(VByName):\n    def run(self, thing_name):\n        if c.user_is_admin:\n            return True\n        elif c.user_is_loggedin:\n            can_alter = False\n            subreddit = None\n            item = VByName.run(self, thing_name)\n\n            if item.author_id == c.user._id:\n                can_alter = True\n            elif item.promoted and c.user_is_sponsor:\n                can_alter = True\n            else:\n                # will throw a legitimate 500 if this isn't a link or\n                # comment, because this should only be used on links and\n                # comments\n                subreddit = item.subreddit_slow\n                if subreddit.can_distinguish(c.user):\n                    can_alter = True\n\n            if can_alter:\n                # Don't allow mod actions for users who are in timeout\n                VNotInTimeout().run(target=item, subreddit=subreddit)\n                return can_alter\n\n        abort(403,'forbidden')\n\nclass VSrCanBan(VByName):\n    def run(self, thing_name):\n        if c.user_is_admin:\n            return True\n        elif c.user_is_loggedin:\n            item = VByName.run(self, thing_name)\n\n            if isinstance(item, (Link, Comment)):\n                sr = item.subreddit_slow\n                if sr.is_moderator_with_perms(c.user, 'posts'):\n                    return True\n            elif isinstance(item, Message):\n                sr = item.subreddit_slow\n                if sr and sr.is_moderator_with_perms(c.user, 'mail'):\n                    return True\n        abort(403,'forbidden')\n\nclass VSrSpecial(VByName):\n    def run(self, thing_name):\n        if c.user_is_admin:\n            return True\n        elif c.user_is_loggedin:\n            item = VByName.run(self, thing_name)\n            # will throw a legitimate 500 if this isn't a link or\n            # comment, because this should only be used on links and\n            # comments\n            subreddit = item.subreddit_slow\n            if subreddit.is_special(c.user):\n                return True\n        abort(403,'forbidden')\n\n\nclass VSubmitParent(VByName):\n    def run(self, fullname, fullname2):\n        # for backwards compatibility (with iphone app)\n        fullname = fullname or fullname2\n        parent = VByName.run(self, fullname) if fullname else None\n\n        if not parent:\n            # for backwards compatibility (normally 404)\n            abort(403, \"forbidden\")\n\n        if not isinstance(parent, (Comment, Link, Message)):\n            # for backwards compatibility (normally 400)\n            abort(403, \"forbidden\")\n\n        if not c.user_is_loggedin:\n            # in practice this is handled by VUser\n            abort(403, \"forbidden\")\n\n        if parent.author_id in c.user.enemies:\n            self.set_error(errors.USER_BLOCKED)\n\n        if isinstance(parent, Message):\n            return parent\n\n        elif isinstance(parent, Link):\n            sr = parent.subreddit_slow\n\n            if parent.is_archived(sr):\n                self.set_error(errors.TOO_OLD)\n            elif parent.locked and not sr.can_distinguish(c.user):\n                self.set_error(errors.THREAD_LOCKED)\n\n            if self.has_errors or parent.can_comment_slow(c.user):\n                return parent\n\n        elif isinstance(parent, Comment):\n            sr = parent.subreddit_slow\n\n            if parent._deleted:\n                self.set_error(errors.DELETED_COMMENT)\n\n            elif parent._spam:\n                # Only author, mod or admin can reply to removed comments\n                can_reply = (c.user_is_loggedin and\n                             (parent.author_id == c.user._id or\n                              c.user_is_admin or\n                              sr.is_moderator(c.user)))\n                if not can_reply:\n                    self.set_error(errors.DELETED_COMMENT)\n\n            link = Link._byID(parent.link_id, data=True)\n\n            if link.is_archived(sr):\n                self.set_error(errors.TOO_OLD)\n            elif link.locked and not sr.can_distinguish(c.user):\n                self.set_error(errors.THREAD_LOCKED)\n\n            if self.has_errors or link.can_comment_slow(c.user):\n                return parent\n\n        abort(403, \"forbidden\")\n\n    def param_docs(self):\n        return {\n            self.param[0]: \"[fullname](#fullnames) of parent thing\",\n        }\n\nclass VSubmitSR(Validator):\n    def __init__(self, srname_param, linktype_param=None, promotion=False):\n        self.require_linktype = False\n        self.promotion = promotion\n\n        if linktype_param:\n            self.require_linktype = True\n            Validator.__init__(self, (srname_param, linktype_param))\n        else:\n            Validator.__init__(self, srname_param)\n\n    def run(self, sr_name, link_type = None):\n        if not sr_name:\n            self.set_error(errors.SUBREDDIT_REQUIRED)\n            return None\n\n        try:\n            sr_name = sr_path_rx.sub('\\g<name>', str(sr_name).strip())\n            sr = Subreddit._by_name(sr_name)\n        except (NotFound, AttributeError, UnicodeEncodeError):\n            self.set_error(errors.SUBREDDIT_NOEXIST)\n            return\n\n        if not c.user_is_loggedin or not sr.can_submit(c.user, self.promotion):\n            self.set_error(errors.SUBREDDIT_NOTALLOWED)\n            return\n\n        if not sr.allow_ads and self.promotion:\n            self.set_error(errors.SUBREDDIT_DISABLED_ADS)\n            return\n\n        if self.require_linktype:\n            if link_type not in ('link', 'self'):\n                self.set_error(errors.INVALID_OPTION)\n                return\n            elif link_type == \"link\" and not sr.can_submit_link(c.user):\n                self.set_error(errors.NO_LINKS)\n                return\n            elif link_type == \"self\" and not sr.can_submit_text(c.user):\n                self.set_error(errors.NO_SELFS)\n                return\n\n        return sr\n\n    def param_docs(self):\n        return {\n            self.param[0]: \"name of a subreddit\",\n        }\n\nclass VSubscribeSR(VByName):\n    def __init__(self, srid_param, srname_param):\n        VByName.__init__(self, (srid_param, srname_param))\n\n    def run(self, sr_id, sr_name):\n        if sr_id:\n            return VByName.run(self, sr_id)\n        elif not sr_name:\n            return\n\n        try:\n            sr = Subreddit._by_name(str(sr_name).strip())\n        except (NotFound, AttributeError, UnicodeEncodeError):\n            self.set_error(errors.SUBREDDIT_NOEXIST)\n            return\n\n        return sr\n\n    def param_docs(self):\n        return {\n            self.param[0]: \"the name of a subreddit\",\n        }\n\n\nRE_GTM_ID = re.compile(r\"^GTM-[A-Z0-9]+$\")\n\nclass VGTMContainerId(Validator):\n    def run(self, value):\n        if not value:\n            return g.googletagmanager\n\n        if RE_GTM_ID.match(value):\n            return value\n        else:\n            abort(404)\n\n\nclass VCollection(Validator):\n    def run(self, name):\n        collection = Collection.by_name(name)\n        if collection:\n            return collection\n        self.set_error(errors.COLLECTION_NOEXIST)\n\n\nclass VPromoTarget(Validator):\n    default_param = (\"targeting\", \"sr\", \"collection\")\n\n    def run(self, targeting, sr_name, collection_name):\n        if targeting == \"collection\" and collection_name == \"none\":\n            return Target(Frontpage.name)\n        elif targeting == \"none\":\n            return Target(Frontpage.name)\n        elif targeting == \"collection\":\n            collection = VCollection(\"collection\").run(collection_name)\n            if collection:\n                return Target(collection)\n            else:\n                # VCollection added errors so no need to do anything\n                return\n        elif targeting == \"one\":\n            sr = VSubmitSR(\"sr\", promotion=True).run(sr_name)\n            if sr:\n                return Target(sr.name)\n            else:\n                # VSubmitSR added errors so no need to do anything\n                return\n        else:\n            self.set_error(errors.INVALID_TARGET, field=\"targeting\")\n\n\nclass VOSVersion(Validator):\n    def __init__(self, param, os, *a, **kw):\n        Validator.__init__(self, param, *a, **kw)\n        self.os = os\n\n    def assign_error(self):\n        self.set_error(errors.INVALID_OS_VERSION, field=\"os_version\")\n\n    def run(self, version_range):\n        if not version_range:\n            return\n\n        # check that string conforms to `min,max` format\n        try:\n            min, max = version_range.split(',')\n        except ValueError:\n            self.assign_error()\n            return\n\n        # check for type errors\n        # (max can be empty string, otherwise both float)\n        type_errors = False\n        if max == '':\n            # check that min is a float\n            try:\n                min = float(min)\n            except ValueError:\n                type_errors = True\n        else:\n            # check that min and max are both floats\n            try:\n                min, max = float(min), float(max)\n                # ensure than min is less-or-equal-to max\n                if min > max:\n                    type_errors = True\n            except ValueError:\n                type_errors = True\n\n        if type_errors == True:\n            self.assign_error()\n            return\n\n        for endpoint in (min, max):\n            if endpoint != '':\n                # check that the version is in the global config\n                if endpoint not in getattr(g, '%s_versions' % self.os):\n                    self.assign_error()\n                    return\n\n        return [str(min), str(max)]\n\n\nMIN_PASSWORD_LENGTH = 6\n\nclass VPassword(Validator):\n    def run(self, password):\n        if not (password and len(password) >= MIN_PASSWORD_LENGTH):\n            self.set_error(errors.SHORT_PASSWORD, {\"chars\": MIN_PASSWORD_LENGTH})\n            self.set_error(errors.BAD_PASSWORD)\n        else:\n            return password.encode('utf8')\n\n    def param_docs(self):\n        return {\n            self.param[0]: \"the password\"\n        }\n\n\nclass VPasswordChange(VPassword):\n    def run(self, password, verify):\n        base = super(VPasswordChange, self).run(password)\n\n        if self.has_errors:\n            return base\n\n        if (verify != password):\n            self.set_error(errors.BAD_PASSWORD_MATCH)\n        else:\n            return base\n\n    def param_docs(self):\n        return {\n            self.param[0]: \"the new password\",\n            self.param[1]: \"the password again (for verification)\",\n        }\n\nMIN_USERNAME_LENGTH = 3\nMAX_USERNAME_LENGTH = 20\n\nuser_rx = re.compile(r\"\\A[\\w-]+\\Z\", re.UNICODE)\n\ndef chkuser(x):\n    if x is None:\n        return None\n    try:\n        if any(ch.isspace() for ch in x):\n            return None\n        return str(x) if user_rx.match(x) else None\n    except TypeError:\n        return None\n    except UnicodeEncodeError:\n        return None\n\nclass VUname(VRequired):\n    def __init__(self, item, *a, **kw):\n        VRequired.__init__(self, item, errors.BAD_USERNAME, *a, **kw)\n    def run(self, user_name):\n        length = 0 if not user_name else len(user_name)\n        if (length < MIN_USERNAME_LENGTH or length > MAX_USERNAME_LENGTH):\n            msg_params = {\n                'min': MIN_USERNAME_LENGTH,\n                'max': MAX_USERNAME_LENGTH,\n            }\n            self.set_error(errors.USERNAME_TOO_SHORT, msg_params=msg_params)\n            self.set_error(errors.BAD_USERNAME)\n            return\n        user_name = chkuser(user_name)\n        if not user_name:\n            self.set_error(errors.USERNAME_INVALID_CHARACTERS)\n            self.set_error(errors.BAD_USERNAME)\n            return\n        else:\n            try:\n                a = Account._by_name(user_name, True)\n                if a._deleted:\n                   return self.set_error(errors.USERNAME_TAKEN_DEL)\n                else:\n                   return self.set_error(errors.USERNAME_TAKEN)\n            except NotFound:\n                return user_name\n\n    def param_docs(self):\n        return {\n            self.param[0]: \"a valid, unused, username\",\n        }\n\nclass VLoggedOut(Validator):\n    def run(self):\n        if c.user_is_loggedin:\n            self.set_error(errors.LOGGED_IN)\n\n\nclass AuthenticationFailed(Exception):\n    pass\n\n\nclass LoginRatelimit(object):\n    def __init__(self, category, key):\n        self.category = category\n        self.key = key\n\n    def __str__(self):\n        return \"login-%s-%s\" % (self.category, self.key)\n\n    def __hash__(self):\n        return hash(str(self))\n\n\nclass VThrottledLogin(VRequired):\n    def __init__(self, params):\n        VRequired.__init__(self, params, error=errors.WRONG_PASSWORD)\n        self.vlength = VLength(\"user\", max_length=100)\n        self.seconds = None\n\n    def get_ratelimits(self, account):\n        is_previously_seen_ip = request.ip in [\n            j for i in IPsByAccount.get(account._id, column_count=1000)\n            for j in i.itervalues()\n        ]\n\n        # We want to maintain different rate-limit buckets depending on whether\n        # we have seen the IP logging in before.  If someone is trying to brute\n        # force an account from an unfamiliar location, we will rate limit\n        # *all* requests from unfamiliar locations that try to access the\n        # account, while still maintaining a separate rate-limit for IP\n        # addresses we have seen use the account before.\n        #\n        # Finally, we also rate limit IPs themselves that appear to be trying\n        # to log into accounts they have never logged into before.  This goes\n        # into a separately maintained bucket.\n        if is_previously_seen_ip:\n            ratelimits = {\n                LoginRatelimit(\"familiar\", account._id): g.RL_LOGIN_MAX_REQS,\n            }\n        else:\n            ratelimits = {\n                LoginRatelimit(\"unfamiliar\", account._id): g.RL_LOGIN_MAX_REQS,\n                LoginRatelimit(\"ip\", request.ip): g.RL_LOGIN_IP_MAX_REQS,\n            }\n\n        hooks.get_hook(\"login.ratelimits\").call(\n            ratelimits=ratelimits,\n            familiar=is_previously_seen_ip,\n        )\n\n        return ratelimits\n\n    def run(self, username, password):\n        ratelimits = {}\n\n        try:\n            if username:\n                username = username.strip()\n                username = self.vlength.run(username)\n                username = chkuser(username)\n\n            if not username:\n                raise AuthenticationFailed\n\n            try:\n                account = Account._by_name(username)\n            except NotFound:\n                raise AuthenticationFailed\n\n            hooks.get_hook(\"account.spotcheck\").call(account=account)\n            if account._banned:\n                raise AuthenticationFailed\n\n            # if already logged in, you're exempt from your own ratelimit\n            # (e.g. to allow account deletion regardless of DoS)\n            ratelimit_exempt = (account == c.user)\n            if not ratelimit_exempt:\n                time_slice = ratelimit.get_timeslice(g.RL_RESET_SECONDS)\n                ratelimits = self.get_ratelimits(account)\n                now = int(time.time())\n\n                for rl, max_requests in ratelimits.iteritems():\n                    try:\n                        failed_logins = ratelimit.get_usage(str(rl), time_slice)\n\n                        if failed_logins >= max_requests:\n                            self.seconds = time_slice.end - now\n                            period_end = datetime.utcfromtimestamp(\n                                time_slice.end).replace(tzinfo=pytz.UTC)\n                            remaining_text = utils.timeuntil(period_end)\n                            self.set_error(\n                                errors.RATELIMIT, {'time': remaining_text},\n                                field='ratelimit', code=429)\n                            g.stats.event_count('login.throttle', rl.category)\n                            return False\n                    except ratelimit.RatelimitError as e:\n                        g.log.info(\"ratelimitcache error (login): %s\", e)\n\n            try:\n                str(password)\n            except UnicodeEncodeError:\n                password = password.encode(\"utf8\")\n\n            if not valid_password(account, password):\n                raise AuthenticationFailed\n            g.stats.event_count('login', 'success')\n            return account\n        except AuthenticationFailed:\n            g.stats.event_count('login', 'failure')\n            if ratelimits:\n                for rl in ratelimits:\n                    try:\n                        ratelimit.record_usage(str(rl), time_slice)\n                    except ratelimit.RatelimitError as e:\n                        g.log.info(\"ratelimitcache error (login): %s\", e)\n            self.error()\n            return False\n\n    def param_docs(self):\n        return {\n            self.param[0]: \"a username\",\n            self.param[1]: \"the user's password\",\n        }\n\n\nclass VSanitizedUrl(Validator):\n    def run(self, url):\n        return utils.sanitize_url(url)\n\n    def param_docs(self):\n        return {self.param: \"a valid URL\"}\n\n\nclass VUrl(VRequired):\n    def __init__(self, item, allow_self=True, require_scheme=False,\n                 valid_schemes=utils.VALID_SCHEMES, *a, **kw):\n        self.allow_self = allow_self\n        self.require_scheme = require_scheme\n        self.valid_schemes = valid_schemes\n        VRequired.__init__(self, item, errors.NO_URL, *a, **kw)\n\n    def run(self, url):\n        if not url:\n            return self.error(errors.NO_URL)\n\n        url = utils.sanitize_url(url, require_scheme=self.require_scheme,\n                                 valid_schemes=self.valid_schemes)\n        if not url:\n            return self.error(errors.BAD_URL)\n\n        try:\n            url.encode('utf-8')\n        except UnicodeDecodeError:\n            return self.error(errors.BAD_URL)\n\n        if url == 'self':\n            if self.allow_self:\n                return url\n            else:\n                self.error(errors.BAD_URL)\n        else:\n            return url\n\n    def param_docs(self):\n        return {self.param: \"a valid URL\"}\n\n\nclass VRedirectUri(VUrl):\n    def __init__(self, item, valid_schemes=None, *a, **kw):\n        VUrl.__init__(self, item, allow_self=False, require_scheme=True,\n                      valid_schemes=valid_schemes, *a, **kw)\n\n    def param_docs(self):\n        doc = \"a valid URI\"\n        if self.valid_schemes:\n            doc += \" with one of the following schemes: \"\n            doc += \", \".join(self.valid_schemes)\n        return {self.param: doc}\n\n\nclass VShamedDomain(Validator):\n    def run(self, url):\n        if not url:\n            return\n\n        is_shamed, domain, reason = is_shamed_domain(url)\n\n        if is_shamed:\n            self.set_error(errors.DOMAIN_BANNED, dict(domain=domain,\n                                                      reason=reason))\n\nclass VExistingUname(VRequired):\n    def __init__(self, item, allow_deleted=False, *a, **kw):\n        self.allow_deleted = allow_deleted\n        VRequired.__init__(self, item, errors.NO_USER, *a, **kw)\n\n    def run(self, name):\n        if name:\n            name = name.strip()\n        if name and name.startswith('~') and c.user_is_admin:\n            try:\n                user_id = int(name[1:])\n                return Account._byID(user_id, True)\n            except (NotFound, ValueError):\n                self.error(errors.USER_DOESNT_EXIST)\n\n        # make sure the name satisfies our user name regexp before\n        # bothering to look it up.\n        name = chkuser(name)\n        if name:\n            try:\n                return Account._by_name(name)\n            except NotFound:\n                if self.allow_deleted:\n                    try:\n                        return Account._by_name(name, allow_deleted=True)\n                    except NotFound:\n                        pass\n\n                self.error(errors.USER_DOESNT_EXIST)\n        else:\n            self.error()\n\n    def param_docs(self):\n        return {\n            self.param: 'the name of an existing user'\n        }\n\nclass VMessageRecipient(VExistingUname):\n    def run(self, name):\n        if not name:\n            return self.error()\n        is_subreddit = False\n        if name.startswith('/r/'):\n            name = name[3:]\n            is_subreddit = True\n        elif name.startswith('#'):\n            name = name[1:]\n            is_subreddit = True\n\n        # A user in timeout should only be able to message us, the admins.\n        if (c.user.in_timeout and\n                not (is_subreddit and\n                     '/r/%s' % name == g.admin_message_acct)):\n            abort(403, 'forbidden')\n\n        if is_subreddit:\n            try:\n                s = Subreddit._by_name(name)\n                if isinstance(s, FakeSubreddit):\n                    raise NotFound, \"fake subreddit\"\n                if s._spam:\n                    raise NotFound, \"banned subreddit\"\n                if s.is_muted(c.user) and not c.user_is_admin:\n                    self.set_error(errors.USER_MUTED)\n                return s\n            except NotFound:\n                self.set_error(errors.SUBREDDIT_NOEXIST)\n        else:\n            account = VExistingUname.run(self, name)\n            if account and account._id in c.user.enemies:\n                self.set_error(errors.USER_BLOCKED)\n            else:\n                return account\n\nclass VUserWithEmail(VExistingUname):\n    def run(self, name):\n        user = VExistingUname.run(self, name)\n        if not user or not hasattr(user, 'email') or not user.email:\n            return self.error(errors.NO_EMAIL_FOR_USER)\n        return user\n\n\nclass VBoolean(Validator):\n    def run(self, val):\n        if val is True or val is False:\n            # val is already a bool object, no processing needed\n            return val\n        lv = str(val).lower()\n        if lv == 'off' or lv == '' or lv[0] in (\"f\", \"n\"):\n            return False\n        return bool(val)\n\n    def param_docs(self):\n        return {\n            self.param: 'boolean value',\n        }\n\nclass VNumber(Validator):\n    def __init__(self, param, min=None, max=None, coerce = True,\n                 error=errors.BAD_NUMBER, num_default=None,\n                 *a, **kw):\n        self.min = self.cast(min) if min is not None else None\n        self.max = self.cast(max) if max is not None else None\n        self.coerce = coerce\n        self.error = error\n        self.num_default = num_default\n        Validator.__init__(self, param, *a, **kw)\n\n    def cast(self, val):\n        raise NotImplementedError\n\n    def _set_error(self):\n        if self.max is None and self.min is None:\n            range = \"\"\n        elif self.max is None:\n            range = _(\"%(min)d to any\") % dict(min=self.min)\n        elif self.min is None:\n            range = _(\"any to %(max)d\") % dict(max=self.max)\n        else:\n            range = _(\"%(min)d to %(max)d\") % dict(min=self.min, max=self.max)\n        self.set_error(self.error, msg_params=dict(range=range))\n\n    def run(self, val):\n        if not val:\n            return self.num_default\n        try:\n            val = self.cast(val)\n            if self.min is not None and val < self.min:\n                if self.coerce:\n                    val = self.min\n                else:\n                    raise ValueError, \"\"\n            elif self.max is not None and val > self.max:\n                if self.coerce:\n                    val = self.max\n                else:\n                    raise ValueError, \"\"\n            return val\n        except ValueError:\n            self._set_error()\n\nclass VInt(VNumber):\n    def cast(self, val):\n        return int(val)\n\n    def param_docs(self):\n        if self.min is not None and self.max is not None:\n            description = \"an integer between %d and %d\" % (self.min, self.max)\n        elif self.min is not None:\n            description = \"an integer greater than %d\" % self.min\n        elif self.max is not None:\n            description = \"an integer less than %d\" % self.max\n        else:\n            description = \"an integer\"\n\n        if self.num_default is not None:\n            description += \" (default: %d)\" % self.num_default\n\n        return {self.param: description}\n\n\nclass VFloat(VNumber):\n    def cast(self, val):\n        return float(val)\n\n\nclass VDecimal(VNumber):\n    def cast(self, val):\n        return Decimal(val)\n\n\nclass VCssName(Validator):\n    \"\"\"\n    returns a name iff it consists of alphanumeric characters and\n    possibly \"-\", and is below the length limit.\n    \"\"\"\n\n    r_css_name = re.compile(r\"\\A[a-zA-Z0-9\\-]{1,100}\\Z\")\n\n    def run(self, name):\n        if name:\n            if self.r_css_name.match(name):\n                return name\n            else:\n                self.set_error(errors.BAD_CSS_NAME)\n        return ''\n\n    def param_docs(self):\n        return {\n            self.param: \"a valid subreddit image name\",\n        }\n\nclass VColor(Validator):\n    \"\"\"Validate a string as being a 6 digit hex color starting with #\"\"\"\n    color = re.compile(r\"\\A#[a-f0-9]{6}\\Z\", re.IGNORECASE)\n\n    def run(self, color):\n        if color:\n            if self.color.match(color):\n                return color.lower()\n            else:\n                self.set_error(errors.BAD_COLOR)\n        return ''\n\n    def param_docs(self):\n        return {\n            self.param: \"a 6-digit rgb hex color, e.g. `#AABBCC`\",\n        }\n\n\nclass VMenu(Validator):\n    def __init__(self, param, menu_cls, remember = True, **kw):\n        self.nav = menu_cls\n        self.remember = remember\n        param = (menu_cls.name, param)\n        Validator.__init__(self, param, **kw)\n\n    def run(self, sort, where):\n        if self.remember:\n            pref = \"%s_%s\" % (where, self.nav.name)\n            user_prefs = copy(c.user.sort_options) if c.user else {}\n            user_pref = user_prefs.get(pref)\n\n            # check to see if a default param has been set\n            if not sort:\n                sort = user_pref\n\n        # validate the sort\n        if sort not in self.nav._options:\n            sort = self.nav._default\n\n        # commit the sort if changed and if this is a POST request\n        if (self.remember and c.user_is_loggedin and sort != user_pref\n            and request.method.upper() == 'POST'):\n            user_prefs[pref] = sort\n            c.user.sort_options = user_prefs\n            user = c.user\n            user._commit()\n\n        return sort\n\n    def param_docs(self):\n        return {\n            self.param[0]: 'one of (%s)' % ', '.join(\"`%s`\" % s\n                                                  for s in self.nav._options),\n        }\n\n\nclass VRatelimit(Validator):\n    def __init__(self, rate_user=False, rate_ip=False, prefix='rate_',\n                 error=errors.RATELIMIT, fatal=False, *a, **kw):\n        self.rate_user = rate_user\n        self.rate_ip = rate_ip\n        self.name = prefix\n        self.cache_prefix = \"rl:%s\" % self.name\n        self.error = error\n        self.fatal = fatal\n        self.seconds = None\n        Validator.__init__(self, *a, **kw)\n\n    def run(self):\n        if g.disable_ratelimit:\n            return\n\n        if c.user_is_loggedin:\n            hook = hooks.get_hook(\"account.is_ratelimit_exempt\")\n            ratelimit_exempt = hook.call_until_return(account=c.user)\n            if ratelimit_exempt:\n                self._record_event(self.name, 'exempted')\n                return\n\n        to_check = []\n        if self.rate_user and c.user_is_loggedin:\n            to_check.append('user' + str(c.user._id36))\n            self._record_event(self.name, 'check_user')\n        if self.rate_ip:\n            to_check.append('ip' + str(request.ip))\n            self._record_event(self.name, 'check_ip')\n\n        r = g.ratelimitcache.get_multi(to_check, prefix=self.cache_prefix)\n        if r:\n            expire_time = max(r.values())\n            time = utils.timeuntil(expire_time)\n\n            g.log.debug(\"rate-limiting %s from %s\" % (self.name, r.keys()))\n            for key in r.keys():\n                if key.startswith('user'):\n                    self._record_event(self.name, 'user_limit_hit')\n                elif key.startswith('ip'):\n                    self._record_event(self.name, 'ip_limit_hit')\n\n            # when errors have associated field parameters, we'll need\n            # to add that here\n            if self.error == errors.RATELIMIT:\n                from datetime import datetime\n                delta = expire_time - datetime.now(g.tz)\n                self.seconds = delta.total_seconds()\n                if self.seconds < 3:  # Don't ratelimit within three seconds\n                    return\n                if self.fatal:\n                    abort(429)\n                self.set_error(errors.RATELIMIT, {'time': time},\n                               field='ratelimit', code=429)\n            else:\n                if self.fatal:\n                    abort(429)\n                self.set_error(self.error)\n\n    @classmethod\n    def ratelimit(cls, rate_user=False, rate_ip=False, prefix=\"rate_\",\n                  seconds=None):\n        name = prefix\n        cache_prefix = \"rl:%s\" % name\n\n        if seconds is None:\n            seconds = g.RL_RESET_SECONDS\n\n        expire_time = datetime.now(g.tz) + timedelta(seconds=seconds)\n\n        to_set = {}\n        if rate_user and c.user_is_loggedin:\n            to_set['user' + str(c.user._id36)] = expire_time\n            cls._record_event(name, 'set_user_limit')\n\n        if rate_ip:\n            to_set['ip' + str(request.ip)] = expire_time\n            cls._record_event(name, 'set_ip_limit')\n\n        g.ratelimitcache.set_multi(to_set, prefix=cache_prefix, time=seconds)\n\n    @classmethod\n    def _record_event(cls, name, event):\n        g.stats.event_count('VRatelimit.%s' % name, event, sample_rate=0.1)\n\n\nclass VRatelimitImproved(Validator):\n    \"\"\"Enforce ratelimits on a function.\n\n    This is a newer version of VRatelimit that uses the ratelimit lib.\n    \"\"\"\n\n    class RateLimit(ratelimit.RateLimit):\n        \"\"\"A RateLimit with defaults specialized for VRatelimitImproved.\n\n        Arguments:\n            event_action: The type of the action the user took, for logging.\n            event_type: Part of the key in the rate limit cache.\n            limit: The RateLimit.limit value. Allowed hits per batch of seconds.\n            seconds: The RateLimit.seconds value. How may seconds per batch.\n            event_id_fn: Nullary function that derives an id from the current\n                context.\n        \"\"\"\n        sample_rate = 0.1\n\n        def __init__(self,\n                     event_action, event_type, limit, seconds, event_id_fn):\n            ratelimit.RateLimit.__init__(self)\n            self.event_name = 'VRatelimitImproved.' + event_action\n            self.event_type = event_type\n            self.event_id_fn = event_id_fn\n            self.limit = limit\n            self.seconds = seconds\n\n        @property\n        def key(self):\n            return 'ratelimit-%s-%s' % (self.event_type, self.event_id_fn())\n\n    def __init__(self, user_limit=None, ip_limit=None, error=errors.RATELIMIT,\n                 *a, **kw):\n        \"\"\"\n        At least one of user_limit and ip_limit should be set for this function\n        to have any effect.\n\n        Arguments:\n            user_limit: RateLimit -- The per-user rate limit.\n            ip_limit: RateLimit -- The per-IP rate limit.\n            error -- the error message to use when the limit is exceeded.\n        \"\"\"\n        self.user_limit = user_limit\n        self.ip_limit = ip_limit\n        self.error = error\n\n        # _validatedForm passes self.seconds to the current form's javascript.\n        self.seconds = None\n        Validator.__init__(self, *a, **kw)\n\n    def run(self):\n        if g.disable_ratelimit:\n            return\n\n        if c.user_is_loggedin:\n            hook = hooks.get_hook(\"account.is_ratelimit_exempt\")\n            ratelimit_exempt = hook.call_until_return(account=c.user)\n            if ratelimit_exempt:\n                return\n\n        if self.user_limit and c.user_is_loggedin:\n            self._check_usage(self.user_limit)\n\n        if self.ip_limit:\n            self._check_usage(self.ip_limit)\n\n    def _check_usage(self, rate_limit):\n        \"\"\"Check ratelimit usage and set an error if necessary.\"\"\"\n        if rate_limit.check():\n            # Not rate limited.\n            return\n\n        g.log.debug('rate-limiting %s with %s used',\n                    rate_limit.key, rate_limit.get_usage())\n        # When errors have associated field parameters, we'll need\n        # to add that here.\n        if self.error == errors.RATELIMIT:\n            period_end = datetime.utcfromtimestamp(\n                rate_limit.timeslice.end).replace(tzinfo=pytz.UTC)\n            time = utils.timeuntil(period_end)\n            self.set_error(errors.RATELIMIT, {'time': time},\n                            field='ratelimit', code=429)\n        else:\n            self.set_error(self.error)\n\n    @classmethod\n    def ratelimit(cls, user_limit=None, ip_limit=None):\n        \"\"\"Record usage of a resource.\"\"\"\n        if user_limit and c.user_is_loggedin:\n            user_limit.record_usage()\n\n        if ip_limit:\n            ip_limit.record_usage()\n\n\nclass VShareRatelimit(VRatelimitImproved):\n    USER_LIMIT = VRatelimitImproved.RateLimit(\n        'share', 'user',\n        limit=g.RL_SHARE_MAX_REQS,\n        seconds=g.RL_RESET_SECONDS,\n        event_id_fn=lambda: c.user._id36)\n\n    IP_LIMIT = VRatelimitImproved.RateLimit(\n        'share', 'ip',\n        limit=g.RL_SHARE_MAX_REQS,\n        seconds=g.RL_RESET_SECONDS,\n        event_id_fn=lambda: request.ip)\n\n    def __init__(self):\n        super(VShareRatelimit, self).__init__(\n            user_limit=self.USER_LIMIT, ip_limit=self.IP_LIMIT)\n\n    @classmethod\n    def ratelimit(cls):\n        super(VShareRatelimit, cls).ratelimit(\n            user_limit=cls.USER_LIMIT, ip_limit=cls.IP_LIMIT)\n\n\nclass VCommentIDs(Validator):\n    def run(self, id_str):\n        if id_str:\n            try:\n                cids = [int(i, 36) for i in id_str.split(',')]\n                return cids\n            except ValueError:\n                abort(400)\n        return []\n\n    def param_docs(self):\n        return {\n            self.param: \"a comma-delimited list of comment ID36s\",\n        }\n\n\nclass VOneTimeToken(Validator):\n    def __init__(self, model, param, *args, **kwargs):\n        self.model = model\n        Validator.__init__(self, param, *args, **kwargs)\n\n    def run(self, key):\n        token = self.model.get_token(key)\n\n        if token:\n            return token\n        else:\n            self.set_error(errors.EXPIRED)\n            return None\n\nclass VOneOf(Validator):\n    def __init__(self, param, options = (), *a, **kw):\n        Validator.__init__(self, param, *a, **kw)\n        self.options = options\n\n    def run(self, val):\n        if self.options and val not in self.options:\n            self.set_error(errors.INVALID_OPTION, code=400)\n            return self.default\n        else:\n            return val\n\n    def param_docs(self):\n        return {\n            self.param: 'one of (%s)' % ', '.join(\"`%s`\" % s\n                                                  for s in self.options),\n        }\n\n\nclass VList(Validator):\n    def __init__(self, param, separator=\",\", choices=None,\n                 error=errors.INVALID_OPTION, *a, **kw):\n        Validator.__init__(self, param, *a, **kw)\n        self.separator = separator\n        self.choices = choices\n        self.error = error\n\n    def run(self, items):\n        if not items:\n            return None\n        all_values = items.split(self.separator)\n        if self.choices is None:\n            return all_values\n\n        values = []\n        for val in all_values:\n            if val in self.choices:\n                values.append(val)\n            else:\n                msg_params = {\"choice\": val}\n                self.set_error(self.error, msg_params=msg_params,\n                               code=400)\n        return values\n\n    # Not i18n'able, but param_docs are not currently i18n'ed\n    NICE_SEP = {\",\": \"comma\"}\n    def param_docs(self):\n        if self.choices:\n            msg = (\"A %(separator)s-separated list of items from \"\n                   \"this set:\\n\\n%(choices)s\")\n            choices = \"`\" + \"`  \\n`\".join(self.choices) + \"`\"\n        else:\n            msg = \"A %(separator)s-separated list of items\"\n            choices = None\n\n        sep = self.NICE_SEP.get(self.separator, self.separator)\n        docs = msg % {\"separator\": sep, \"choices\": choices}\n        return {self.param: docs}\n\n\nclass VFrequencyCap(Validator):\n    def run(self, frequency_capped='false', frequency_cap=None):\n\n        if frequency_capped == 'true':\n            if frequency_cap and int(frequency_cap) >= g.frequency_cap_min:\n                try:\n                    return frequency_cap\n                except (ValueError, TypeError):\n                    self.set_error(errors.INVALID_FREQUENCY_CAP, code=400)\n            else:\n                self.set_error(\n                    errors.FREQUENCY_CAP_TOO_LOW,\n                    {'min': g.frequency_cap_min},\n                    code=400\n                )\n        else:\n            return None\n\n\nclass VPriority(Validator):\n    def run(self, val):\n        if c.user_is_sponsor:\n            return (PROMOTE_PRIORITIES.get(val,\n                PROMOTE_DEFAULT_PRIORITY(context=c)))\n        elif feature.is_enabled('ads_auction'):\n            return PROMOTE_DEFAULT_PRIORITY(context=c)\n        else:\n            return PROMOTE_PRIORITIES['standard']\n\n\nclass VLocation(Validator):\n    default_param = (\"country\", \"region\", \"metro\")\n\n    def run(self, country, region, metro):\n        # some browsers are sending \"null\" rather than omitting the input when\n        # the select is disabled\n        country, region, metro = map(lambda val: None if val == \"null\" else val,\n                                     [country, region, metro])\n\n        if not (country or region or metro):\n            return None\n\n        # Sponsors should only be creating fixed-CPM campaigns, which we\n        # cannot calculate region specific inventory for\n        if c.user_is_sponsor and region and not (region and metro):\n            invalid_region = True\n        else:\n            invalid_region = False\n\n        # Non-sponsors can only create auctions (non-inventory), so they\n        # can target country, country/region, and country/region/metro\n        if not (country and not (region or metro) or\n                (country and region and not metro) or\n                (country and region and metro)):\n            invalid_geotargets = True\n        else:\n            invalid_geotargets = False\n\n        if (country not in g.locations or\n                region and region not in g.locations[country]['regions'] or\n                metro and metro not in g.locations[country]['regions'][region]['metros']):\n            nonexistent_geotarget = True\n        else:\n            nonexistent_geotarget = False\n\n        if invalid_region or invalid_geotargets or nonexistent_geotarget:\n            self.set_error(errors.INVALID_LOCATION, code=400, field='location')\n        else:\n            return Location(country, region, metro)\n\n\nclass VImageType(Validator):\n    def run(self, img_type):\n        if not img_type in ('png', 'jpg'):\n            return 'png'\n        return img_type\n\n    def param_docs(self):\n        return {\n            self.param: \"one of `png` or `jpg` (default: `png`)\",\n        }\n\n\nclass ValidEmail(Validator):\n    \"\"\"Validates a single email. Returns the email on success.\"\"\"\n\n    def run(self, email):\n        # Strip out leading/trailing whitespace, since the inclusion of that is\n        # a common and easily-fixable user error.\n        if email is not None:\n            email = email.strip()\n\n        if not email:\n            self.set_error(errors.NO_EMAIL)\n        elif not ValidEmails.email_re.match(email):\n            self.set_error(errors.BAD_EMAIL)\n        else:\n            return email\n\n\nclass ValidEmails(Validator):\n    \"\"\"Validates a list of email addresses passed in as a string and\n    delineated by whitespace, ',' or ';'.  Also validates quantity of\n    provided emails.  Returns a list of valid email addresses on\n    success\"\"\"\n\n    separator = re.compile(r'[^\\s,;]+')\n    email_re  = re.compile(r'\\A[^\\s@]+@[^\\s@]+\\.[^\\s@]+\\Z')\n\n    def __init__(self, param, num = 20, **kw):\n        self.num = num\n        Validator.__init__(self, param = param, **kw)\n\n    def run(self, emails0):\n        emails = set(self.separator.findall(emails0) if emails0 else [])\n        failures = set(e for e in emails if not self.email_re.match(e))\n        emails = emails - failures\n\n        # make sure the number of addresses does not exceed the max\n        if self.num > 0 and len(emails) + len(failures) > self.num:\n            # special case for 1: there should be no delineators at all, so\n            # send back original string to the user\n            if self.num == 1:\n                self.set_error(errors.BAD_EMAILS,\n                             {'emails': '\"%s\"' % emails0})\n            # else report the number expected\n            else:\n                self.set_error(errors.TOO_MANY_EMAILS,\n                             {'num': self.num})\n        # correct number, but invalid formatting\n        elif failures:\n            self.set_error(errors.BAD_EMAILS,\n                         {'emails': ', '.join(failures)})\n        # no emails\n        elif not emails:\n            self.set_error(errors.NO_EMAILS)\n        else:\n            # return single email if one is expected, list otherwise\n            return list(emails)[0] if self.num == 1 else emails\n\nclass ValidEmailsOrExistingUnames(Validator):\n    \"\"\"Validates a list of mixed email addresses and usernames passed in\n    as a string, delineated by whitespace, ',' or ';'.  Validates total\n    quantity too while we're at it.  Returns a tuple of the form\n    (e-mail addresses, user account objects)\"\"\"\n\n    def __init__(self, param, num=20, **kw):\n        self.num = num\n        Validator.__init__(self, param=param, **kw)\n\n    def run(self, items):\n        # Use ValidEmails separator to break the list up\n        everything = set(ValidEmails.separator.findall(items) if items else [])\n\n        # Use ValidEmails regex to divide the list into e-mail and other\n        emails = set(e for e in everything if ValidEmails.email_re.match(e))\n        failures = everything - emails\n\n        # Run the rest of the validator against the e-mails list\n        ve = ValidEmails(self.param, self.num)\n        if len(emails) > 0:\n            ve.run(\", \".join(emails))\n\n        # ValidEmails will add to c.errors for us, so do nothing if that fails\n        # Elsewise, on with the users\n        if not ve.has_errors:\n            users = set()  # set of accounts\n            validusers = set()  # set of usernames to subtract from failures\n\n            # Now steal from VExistingUname:\n            for uname in failures:\n                check = uname\n                if re.match('/u/', uname):\n                    check = check[3:]\n                veu = VExistingUname(check)\n                account = veu.run(check)\n                if account:\n                    validusers.add(uname)\n                    users.add(account)\n\n            # We're fine if all our failures turned out to be valid users\n            if len(users) == len(failures):\n                # ValidEmails checked to see if there were too many addresses,\n                # check to see if there's enough left-over space for users\n                remaining = self.num - len(emails)\n                if len(users) > remaining:\n                    if self.num == 1:\n                        # We only wanted one, and we got it as an e-mail,\n                        # so complain.\n                        self.set_error(errors.BAD_EMAILS,\n                                       {\"emails\": '\"%s\"' % items})\n                    else:\n                        # Too many total\n                        self.set_error(errors.TOO_MANY_EMAILS,\n                                       {\"num\": self.num})\n                elif len(users) + len(emails) == 0:\n                    self.set_error(errors.NO_EMAILS)\n                else:\n                    # It's all good!\n                    return (emails, users)\n            else:\n                failures = failures - validusers\n                self.set_error(errors.BAD_EMAILS,\n                               {'emails': ', '.join(failures)})\n\nclass VCnameDomain(Validator):\n    domain_re  = re.compile(r'\\A([\\w\\-_]+\\.)+[\\w]+\\Z')\n\n    def run(self, domain):\n        if (domain\n            and (not self.domain_re.match(domain)\n                 or domain.endswith('.' + g.domain)\n                 or domain.endswith('.' + g.media_domain)\n                 or len(domain) > 300)):\n            self.set_error(errors.BAD_CNAME)\n        elif domain:\n            try:\n                return str(domain).lower()\n            except UnicodeEncodeError:\n                self.set_error(errors.BAD_CNAME)\n\n    def param_docs(self):\n        # cnames are dead; don't advertise this.\n        return {}\n\n\nclass VDate(Validator):\n    \"\"\"\n    Date checker that accepts string inputs.\n\n    Error conditions:\n       * BAD_DATE on mal-formed date strings (strptime parse failure)\n\n    \"\"\"\n\n    def __init__(self, param, format=\"%m/%d/%Y\", required=True):\n        self.format = format\n        self.required = required\n        Validator.__init__(self, param)\n\n    def run(self, datestr):\n        if not datestr and not self.required:\n            return None\n\n        try:\n            dt = datetime.strptime(datestr, self.format)\n            return dt.replace(tzinfo=g.tz)\n        except (ValueError, TypeError):\n            self.set_error(errors.BAD_DATE)\n\n\nclass VDestination(Validator):\n    def __init__(self, param = 'dest', default = \"\", **kw):\n        Validator.__init__(self, param, default, **kw)\n\n    def run(self, dest):\n        if not dest:\n            dest = self.default or add_sr(\"/\")\n\n        ld = dest.lower()\n        if ld.startswith(('/', 'http://', 'https://')):\n            u = UrlParser(dest)\n\n            if u.is_reddit_url(c.site) and u.is_web_safe_url():\n                return dest\n\n        return \"/\"\n\n    def param_docs(self):\n        return {\n            self.param: 'destination url (must be same-domain)',\n        }\n\nclass ValidAddress(Validator):\n    def set_error(self, msg, field):\n        Validator.set_error(self, errors.BAD_ADDRESS,\n                            dict(message=msg), field = field)\n\n    def run(self, firstName, lastName, company, address,\n            city, state, zipCode, country, phoneNumber):\n        if not firstName:\n            self.set_error(_(\"please provide a first name\"), \"firstName\")\n        elif not lastName:\n            self.set_error(_(\"please provide a last name\"), \"lastName\")\n        elif not address:\n            self.set_error(_(\"please provide an address\"), \"address\")\n        elif not city:\n            self.set_error(_(\"please provide your city\"), \"city\")\n        elif not state:\n            self.set_error(_(\"please provide your state\"), \"state\")\n        elif not zipCode:\n            self.set_error(_(\"please provide your zip or post code\"), \"zip\")\n        elif not country:\n            self.set_error(_(\"please provide your country\"), \"country\")\n\n        # Make sure values don't exceed max length defined in the authorize.net\n        # xml schema: https://api.authorize.net/xml/v1/schema/AnetApiSchema.xsd\n        max_lengths = [\n            (firstName, 50, 'firstName'), # (argument, max len, form field name)\n            (lastName, 50, 'lastName'),\n            (company, 50, 'company'),\n            (address, 60, 'address'),\n            (city, 40, 'city'),\n            (state, 40, 'state'),\n            (zipCode, 20, 'zip'),\n            (country, 60, 'country'),\n            (phoneNumber, 255, 'phoneNumber')\n        ]\n        for (arg, max_length, form_field_name) in max_lengths:\n            if arg and len(arg) > max_length:\n                self.set_error(_(\"max length %d characters\" % max_length), form_field_name)\n\n        if not self.has_errors:\n            return Address(firstName = firstName,\n                           lastName = lastName,\n                           company = company or \"\",\n                           address = address,\n                           city = city, state = state,\n                           zip = zipCode, country = country,\n                           phoneNumber = phoneNumber or \"\")\n\nclass ValidCard(Validator):\n    valid_date = re.compile(r\"\\d\\d\\d\\d-\\d\\d\")\n    def set_error(self, msg, field):\n        Validator.set_error(self, errors.BAD_CARD,\n                            dict(message=msg), field = field)\n\n    def run(self, cardNumber, expirationDate, cardCode):\n        has_errors = False\n\n        cardNumber = cardNumber or \"\"\n        if not (cardNumber.isdigit() and 13 <= len(cardNumber) <= 16):\n            self.set_error(_(\"credit card numbers should be 13 to 16 digits\"),\n                           \"cardNumber\")\n            has_errors = True\n\n        if not self.valid_date.match(expirationDate or \"\"):\n            self.set_error(_(\"dates should be YYYY-MM\"), \"expirationDate\")\n            has_errors = True\n        else:\n            now = datetime.now(g.tz)\n            yyyy, mm = expirationDate.split(\"-\")\n            year = int(yyyy)\n            month = int(mm)\n            if month < 1 or month > 12:\n                self.set_error(_(\"month must be in the range 01..12\"), \"expirationDate\")\n                has_errors = True\n            elif datetime(year, month, 1) < datetime(now.year, now.month, 1):\n                self.set_error(_(\"expiration date must be in the future\"), \"expirationDate\")\n                has_errors = True\n\n        cardCode = cardCode or \"\"\n        if not (cardCode.isdigit() and 3 <= len(cardCode) <= 4):\n            self.set_error(_(\"card verification codes should be 3 or 4 digits\"),\n                           \"cardCode\")\n            has_errors = True\n\n        if not has_errors:\n            return CreditCard(cardNumber = cardNumber,\n                              expirationDate = expirationDate,\n                              cardCode = cardCode)\n\nclass VTarget(Validator):\n    target_re = re.compile(\"\\A[\\w_-]{3,20}\\Z\")\n    def run(self, name):\n        if name and self.target_re.match(name):\n            return name\n\n    def param_docs(self):\n        # this is just for htmllite and of no interest to api consumers\n        return {}\n\nclass VFlairAccount(VRequired):\n    def __init__(self, item, *a, **kw):\n        VRequired.__init__(self, item, errors.BAD_FLAIR_TARGET, *a, **kw)\n\n    def _lookup(self, name, allow_deleted):\n        try:\n            return Account._by_name(name, allow_deleted=allow_deleted)\n        except NotFound:\n            return None\n\n    def run(self, name):\n        if not name:\n            return self.error()\n        return (\n            self._lookup(name, False)\n            or self._lookup(name, True)\n            or self.error())\n\n    def param_docs(self):\n        return {self.param: _(\"a user by name\")}\n\nclass VFlairLink(VRequired):\n    def __init__(self, item, *a, **kw):\n        VRequired.__init__(self, item, errors.BAD_FLAIR_TARGET, *a, **kw)\n\n    def run(self, name):\n        if not name:\n            return self.error()\n        try:\n            return Link._by_fullname(name, data=True)\n        except NotFound:\n            return self.error()\n\n    def param_docs(self):\n        return {self.param: _(\"a [fullname](#fullname) of a link\")}\n\nclass VFlairCss(VCssName):\n    def __init__(self, param, max_css_classes=10, **kw):\n        self.max_css_classes = max_css_classes\n        VCssName.__init__(self, param, **kw)\n\n    def run(self, css):\n        if not css:\n            return css\n\n        names = css.split()\n        if len(names) > self.max_css_classes:\n            self.set_error(errors.TOO_MUCH_FLAIR_CSS)\n            return ''\n\n        for name in names:\n            if not self.r_css_name.match(name):\n                self.set_error(errors.BAD_CSS_NAME)\n                return ''\n\n        return css\n\nclass VFlairText(VLength):\n    def __init__(self, param, max_length=64, **kw):\n        VLength.__init__(self, param, max_length, **kw)\n\nclass VFlairTemplateByID(VRequired):\n    def __init__(self, param, **kw):\n        VRequired.__init__(self, param, None, **kw)\n\n    def run(self, flair_template_id):\n        try:\n            return FlairTemplateBySubredditIndex.get_template(\n                c.site._id, flair_template_id)\n        except tdb_cassandra.NotFound:\n            return None\n\nclass VOneTimePassword(Validator):\n    allowed_skew = [-1, 0, 1]  # allow a period of skew on either side of now\n    ratelimit = 3  # maximum number of tries per period\n\n    def __init__(self, param, required):\n        self.required = required\n        Validator.__init__(self, param)\n\n    @classmethod\n    def validate_otp(cls, secret, password):\n        # is the password a valid format and has it been used?\n        try:\n            key = \"otp:used_%s_%d\" % (c.user._id36, int(password))\n        except (TypeError, ValueError):\n            valid_and_unused = False\n        else:\n            # leave this key around for one more time period than the maximum\n            # number of time periods we'll check for valid passwords\n            key_ttl = totp.PERIOD * (len(cls.allowed_skew) + 1)\n            valid_and_unused = g.gencache.add(key, True, time=key_ttl)\n\n        # check the password (allowing for some clock-skew as 2FA-users\n        # frequently travel at relativistic velocities)\n        if valid_and_unused:\n            for skew in cls.allowed_skew:\n                expected_otp = totp.make_totp(secret, skew=skew)\n                if constant_time_compare(password, expected_otp):\n                    return True\n\n        return False\n\n    def run(self, password):\n        # does the user have 2FA configured?\n        secret = c.user.otp_secret\n        if not secret:\n            if self.required:\n                self.set_error(errors.NO_OTP_SECRET)\n            return\n\n        # do they have the otp cookie instead?\n        if c.otp_cached:\n            return\n\n        # make sure they're not trying this too much\n        if not g.disable_ratelimit:\n            current_password = totp.make_totp(secret)\n            otp_ratelimit = ratelimit.SimpleRateLimit(\n                name=\"otp_tries_%s_%s\" % (c.user._id36, current_password),\n                seconds=600,\n                limit=self.ratelimit,\n            )\n            if not otp_ratelimit.record_and_check():\n                self.set_error(errors.RATELIMIT, dict(time=\"30 seconds\"))\n                return\n\n        # check the password\n        if self.validate_otp(secret, password):\n            return\n\n        # if we got this far, their password was wrong, invalid or already used\n        self.set_error(errors.WRONG_PASSWORD)\n\nclass VOAuth2ClientID(VRequired):\n    default_param = \"client_id\"\n    default_param_doc = _(\"an app\")\n    def __init__(self, param=None, *a, **kw):\n        VRequired.__init__(self, param, errors.OAUTH2_INVALID_CLIENT, *a, **kw)\n\n    def run(self, client_id):\n        client_id = VRequired.run(self, client_id)\n        if client_id:\n            client = OAuth2Client.get_token(client_id)\n            if client and not client.deleted:\n                return client\n            else:\n                self.error()\n\n    def param_docs(self):\n        return {self.default_param: self.default_param_doc}\n\nclass VOAuth2ClientDeveloper(VOAuth2ClientID):\n    default_param_doc = _(\"an app developed by the user\")\n\n    def run(self, client_id):\n        client = super(VOAuth2ClientDeveloper, self).run(client_id)\n        if not client or not client.has_developer(c.user):\n            return self.error()\n        return client\n\nclass VOAuth2Scope(VRequired):\n    default_param = \"scope\"\n    def __init__(self, param=None, *a, **kw):\n        VRequired.__init__(self, param, errors.OAUTH2_INVALID_SCOPE, *a, **kw)\n\n    def run(self, scope):\n        scope = VRequired.run(self, scope)\n        if scope:\n            parsed_scope = OAuth2Scope(scope)\n            if parsed_scope.is_valid():\n                return parsed_scope\n            else:\n                self.error()\n\nclass VOAuth2RefreshToken(Validator):\n    def __init__(self, param, *a, **kw):\n        Validator.__init__(self, param, None, *a, **kw)\n\n    def run(self, refresh_token_id):\n        if refresh_token_id:\n            try:\n                token = OAuth2RefreshToken._byID(refresh_token_id)\n            except tdb_cassandra.NotFound:\n                self.set_error(errors.OAUTH2_INVALID_REFRESH_TOKEN)\n                return None\n            if not token.check_valid():\n                self.set_error(errors.OAUTH2_INVALID_REFRESH_TOKEN)\n                return None\n            return token\n        else:\n            return None\n\nclass VPermissions(Validator):\n    types = dict(\n        moderator=ModeratorPermissionSet,\n        moderator_invite=ModeratorPermissionSet,\n    )\n\n    def __init__(self, type_param, permissions_param, *a, **kw):\n        Validator.__init__(self, (type_param, permissions_param), *a, **kw)\n\n    def run(self, type, permissions):\n        permission_class = self.types.get(type)\n        if not permission_class:\n            self.set_error(errors.INVALID_PERMISSION_TYPE, field=self.param[0])\n            return (None, None)\n        try:\n            perm_set = permission_class.loads(permissions, validate=True)\n        except ValueError:\n            self.set_error(errors.INVALID_PERMISSIONS, field=self.param[1])\n            return (None, None)\n        return type, perm_set\n\n\nclass VJSON(Validator):\n    def run(self, json_str):\n        if not json_str:\n            return self.set_error('JSON_PARSE_ERROR', code=400)\n        else:\n            try:\n                return json.loads(json_str)\n            except ValueError:\n                return self.set_error('JSON_PARSE_ERROR', code=400)\n\n    def param_docs(self):\n        return {\n            self.param: \"JSON data\",\n        }\n\n\nclass VValidatedJSON(VJSON):\n    \"\"\"Apply validators to the values of JSON formatted data.\"\"\"\n    class ArrayOf(object):\n        \"\"\"A JSON array of objects with the specified schema.\"\"\"\n        def __init__(self, spec):\n            self.spec = spec\n\n        def run(self, data):\n            if not isinstance(data, list):\n                raise RedditError('JSON_INVALID', code=400)\n\n            validated_data = []\n            for item in data:\n                validated_data.append(self.spec.run(item))\n            return validated_data\n\n        def spec_docs(self):\n            spec_lines = []\n            spec_lines.append('[')\n            if hasattr(self.spec, 'spec_docs'):\n                array_docs = self.spec.spec_docs()\n            else:\n                array_docs = self.spec.param_docs()[self.spec.param]\n            for line in array_docs.split('\\n'):\n                spec_lines.append('  ' + line)\n            spec_lines[-1] += ','\n            spec_lines.append('  ...')\n            spec_lines.append(']')\n            return '\\n'.join(spec_lines)\n\n\n    class Object(object):\n        \"\"\"A JSON object with validators for specified fields.\"\"\"\n        def __init__(self, spec):\n            self.spec = spec\n\n        def run(self, data, ignore_missing=False):\n            if not isinstance(data, dict):\n                raise RedditError('JSON_INVALID', code=400)\n\n            validated_data = {}\n            for key, validator in self.spec.iteritems():\n                try:\n                    validated_data[key] = validator.run(data[key])\n                except KeyError:\n                    if ignore_missing:\n                        continue\n                    raise RedditError('JSON_MISSING_KEY', code=400,\n                                      msg_params={'key': key})\n            return validated_data\n\n        def spec_docs(self):\n            spec_docs = {}\n            for key, validator in self.spec.iteritems():\n                if hasattr(validator, 'spec_docs'):\n                    spec_docs[key] = validator.spec_docs()\n                elif hasattr(validator, 'param_docs'):\n                    spec_docs.update(validator.param_docs())\n                    if validator.docs:\n                        spec_docs.update(validator.docs)\n\n            # generate markdown json schema docs\n            spec_lines = []\n            spec_lines.append('{')\n            for key in sorted(spec_docs.keys()):\n                key_docs = spec_docs[key]\n                # indent any new lines\n                key_docs = key_docs.replace('\\n', '\\n  ')\n                spec_lines.append('  \"%s\": %s,' % (key, key_docs))\n            spec_lines.append('}')\n            return '\\n'.join(spec_lines)\n\n    class PartialObject(Object):\n        def run(self, data):\n            super_ = super(VValidatedJSON.PartialObject, self)\n            return super_.run(data, ignore_missing=True)\n\n    def __init__(self, param, spec, **kw):\n        VJSON.__init__(self, param, **kw)\n        self.spec = spec\n\n    def run(self, json_str):\n        data = VJSON.run(self, json_str)\n        if self.has_errors:\n            return\n\n        # Note: this relies on the fact that all validator errors are dumped\n        # into a global (c.errors) and then checked by @validate.\n        return self.spec.run(data)\n\n    def docs_model(self):\n        spec_md = self.spec.spec_docs()\n\n        # indent for code formatting\n        spec_md = '\\n'.join(\n            '    ' + line for line in spec_md.split('\\n')\n        )\n        return spec_md\n\n    def param_docs(self):\n        return {\n            self.param: 'json data:\\n\\n' + self.docs_model(),\n        }\n\n\nmulti_name_rx = re.compile(r\"\\A[A-Za-z0-9][A-Za-z0-9_]{1,20}\\Z\")\nmulti_name_chars_rx = re.compile(r\"[^A-Za-z0-9_]\")\n\nclass VMultiPath(Validator):\n    \"\"\"Validates a multireddit path. Returns a path info dictionary.\n    \"\"\"\n    def __init__(self, param, kinds=None, required=True, **kw):\n        Validator.__init__(self, param, **kw)\n        self.required = required\n        self.kinds = tup(kinds or ('f', 'm'))\n\n    @classmethod\n    def normalize(self, path):\n        if path[0] != '/':\n            path = '/' + path\n        path = path.lower().rstrip('/')\n        return path\n\n    def run(self, path):\n        if not path and not self.required:\n            return None\n        try:\n            require(path)\n            path = self.normalize(path)\n            require(path.startswith('/user/'))\n            prefix, owner, kind, name = require_split(path, 5, sep='/')[1:]\n            require(kind in self.kinds)\n            owner = chkuser(owner)\n            require(owner)\n        except RequirementException:\n            self.set_error('BAD_MULTI_PATH', code=400)\n            return\n\n        try:\n            require(multi_name_rx.match(name))\n        except RequirementException:\n            invalid_char = multi_name_chars_rx.search(name)\n            if invalid_char:\n                char = invalid_char.group()\n                if char == ' ':\n                    reason = _('no spaces allowed')\n                else:\n                    reason = _(\"invalid character: '%s'\") % char\n            elif name[0] == '_':\n                reason = _(\"can't start with a '_'\")\n            elif len(name) < 2:\n                reason = _('that name is too short')\n            elif len(name) > 21:\n                reason = _('that name is too long')\n            else:\n                reason = _(\"that name isn't going to work\")\n\n            self.set_error('BAD_MULTI_NAME', {'reason': reason}, code=400)\n            return\n\n        return {'path': path, 'prefix': prefix, 'owner': owner, 'name': name}\n\n    def param_docs(self):\n        return {\n            self.param: \"multireddit url path\",\n        }\n\n\nclass VMultiByPath(Validator):\n    \"\"\"Validates a multireddit path.  Returns a LabeledMulti.\n    \"\"\"\n    def __init__(self, param, require_view=True, require_edit=False, kinds=None):\n        Validator.__init__(self, param)\n        self.require_view = require_view\n        self.require_edit = require_edit\n        self.kinds = tup(kinds or ('f', 'm'))\n\n    def run(self, path):\n        path = VMultiPath.normalize(path)\n        if not path.startswith('/user/'):\n            return self.set_error('MULTI_NOT_FOUND', code=404)\n\n        name = path.split('/')[-1]\n        if not multi_name_rx.match(name):\n            return self.set_error('MULTI_NOT_FOUND', code=404)\n\n        try:\n            multi = LabeledMulti._byID(path)\n        except tdb_cassandra.NotFound:\n            return self.set_error('MULTI_NOT_FOUND', code=404)\n\n        if not multi or multi.kind not in self.kinds:\n            return self.set_error('MULTI_NOT_FOUND', code=404)\n        if not multi or (self.require_view and not multi.can_view(c.user)):\n            return self.set_error('MULTI_NOT_FOUND', code=404)\n        if self.require_edit and not multi.can_edit(c.user):\n            return self.set_error('MULTI_CANNOT_EDIT', code=403)\n\n        return multi\n\n    def param_docs(self):\n        return {\n            self.param: \"multireddit url path\",\n        }\n\n\nsr_path_rx = re.compile(r\"\\A(/?r/)?(?P<name>.*?)/?\\Z\")\nclass VSubredditList(Validator):\n\n    def __init__(self, param, limit=20, allow_language_srs=True):\n        Validator.__init__(self, param)\n        self.limit = limit\n        self.allow_language_srs = allow_language_srs\n\n    def run(self, subreddits):\n        if not subreddits:\n            return []\n\n        # extract subreddit name if path provided\n        subreddits = [sr_path_rx.sub('\\g<name>', sr.strip())\n                      for sr in subreddits.lower().strip().splitlines() if sr]\n\n        for name in subreddits:\n            valid_name = Subreddit.is_valid_name(\n                name, allow_language_srs=self.allow_language_srs)\n            if not valid_name:\n                return self.set_error(errors.BAD_SR_NAME, code=400)\n\n        unique_srs = set(subreddits)\n\n        if subreddits:\n            valid_srs = set(Subreddit._by_name(subreddits).keys())\n            if unique_srs - valid_srs:\n                return self.set_error(errors.SUBREDDIT_NOEXIST, code=400)\n\n        if len(unique_srs) > self.limit:\n            return self.set_error(\n                errors.TOO_MANY_SUBREDDITS, {'max': self.limit}, code=400)\n\n        # return list of subreddit names as entered\n        return subreddits\n\n    def param_docs(self):\n        return {\n            self.param: 'a list of subreddit names, line break delimited',\n        }\n\n\nclass VResultTypes(Validator):\n    \"\"\"\n    Validates a list of search result types, provided either as multiple\n    GET parameters or as a comma separated list.  Returns a set.\n    \"\"\"\n    def __init__(self, param):\n        Validator.__init__(self, param, get_multiple=True)\n        self.default = []\n        self.options = {'link', 'sr'}\n\n    def run(self, result_types):\n        if result_types and ',' in result_types[0]:\n            result_types = result_types[0].strip(',').split(',')\n\n        # invalid values are ignored\n        result_types = set(result_types) & self.options\n\n        # for backwards compatibility, api and legacy default to link results\n        if is_api():\n            result_types = result_types or {'link'}\n        elif feature.is_enabled('legacy_search') or c.user.pref_legacy_search:\n            result_types = {'link'}\n        else:\n            result_types = result_types or {'link', 'sr'}\n\n        return result_types\n\n    def param_docs(self):\n        return {\n            self.param: (\n                '(optional) comma-delimited list of result types '\n                '(`%s`)' % '`, `'.join(self.options)\n            ),\n        }\n\n\nclass VSigned(Validator):\n    \"\"\"Validate if the request is properly signed.\n\n    Checks the headers (mostly the User-Agent) are signed with\n    :py:function:`~r2.lib.signing.valid_ua_signature` and in the case\n    of POST and PUT ensure that any request.body included is also signed\n    via :py:function:`~r2.lib.signing.valid_body_signature`.\n\n    In :py:method:`run`, the signatures are combined as needed to generate a\n    final signature that is generally the combination of the two.\n    \"\"\"\n\n    def run(self):\n        signature = signing.valid_ua_signature(request)\n\n        # only check the request body when there should be one\n        if request.method.upper() in (\"POST\", \"PUT\"):\n            signature.update(signing.valid_post_signature(request))\n\n        # add a simple event for each error as it appears (independent of\n        # whether we're going to ignore them).\n        for code, field in signature.errors:\n            g.stats.simple_event(\n                \"signing.%s.invalid.%s\" % (field, code.lower())\n            )\n\n        # persistent skew problems on android suggest something deeper is\n        # wrong in v1.  Disable the expiration check for now!\n        if signature.platform == \"android\" and signature.version == 1:\n            signature.add_ignore(signing.ERRORS.EXPIRED_TOKEN)\n\n        return signature\n\n\ndef need_provider_captcha():\n    return False\n"
  },
  {
    "path": "r2/r2/lib/validator/wiki.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom os.path import normpath\nfrom functools import wraps\nimport datetime\nimport re\n\nfrom pylons.i18n import _\n\nfrom pylons import request\nfrom pylons import tmpl_context as c\nfrom pylons import app_globals as g\n\nfrom r2.models.wiki import WikiPage, WikiRevision, WikiBadRevision\nfrom r2.lib.validator import (\n    Validator,\n    VSrModerator,\n    set_api_docs,\n)\nfrom r2.lib.db import tdb_cassandra\n\nMAX_PAGE_NAME_LENGTH = g.wiki_max_page_name_length\n\nMAX_SEPARATORS = g.wiki_max_page_separators\n\ndef this_may_revise(page=None):\n    if not c.user_is_loggedin:\n        return False\n    \n    if c.user_is_admin:\n        return True\n    \n    return may_revise(c.site, c.user, page)\n\ndef this_may_view(page):\n    user = c.user if c.user_is_loggedin else None\n    if user and c.user_is_admin:\n        return True\n    return may_view(c.site, user, page)\n\ndef may_revise(sr, user, page=None):    \n    if sr.is_moderator_with_perms(user, 'wiki'):\n        # Mods may always contribute to non-config pages\n        if not page or not page.special:\n            return True\n    \n    if page and page.restricted and not page.special:\n        # People may not contribute to restricted pages\n        # (Except for special pages)\n        return False\n\n    if sr.is_wikibanned(user):\n        # Users who are wiki banned in the subreddit may not contribute\n        return False\n    \n    if sr.is_banned(user):\n        # If the user is banned from the subreddit, do not allow them to contribute\n        return False\n    \n    if page and not may_view(sr, user, page):\n        # Users who are not allowed to view the page may not contribute to the page\n        return False\n    \n    if user.wiki_override == False:\n        # global ban\n        return False\n    \n    if page and page.has_editor(user._id36):\n        # If the user is an editor on the page, they may edit\n        return True\n\n    if (page and page.special and\n            sr.is_moderator_with_perms(user, 'config')):\n        return True\n\n    if page and page.special:\n        # If this is a special page\n        # (and the user is not a mod or page editor)\n        # They should not be allowed to revise\n        return False\n    \n    if page and page.permlevel > 0:\n        # If the page is beyond \"anyone may contribute\"\n        # A normal user should not be allowed to revise\n        return False\n    \n    if sr.is_wikicontributor(user):\n        # If the user is a wiki contributor, they may revise\n        return True\n    \n    if sr.wikimode != 'anyone':\n        # If the user is not a page editor or wiki contributor,\n        # and the mode is not everyone,\n        # the user may not edit.\n        return False\n    \n    if not sr.wiki_can_submit(user):\n        # If the user can not submit to the subreddit\n        # They should not be able to contribute\n        return False\n\n    # Use global karma for the frontpage wiki\n    karma_sr = sr if sr.wiki_use_subreddit_karma else None\n\n    # Use link or comment karma, whichever is greater\n    karma = max(user.karma('link', karma_sr), user.karma('comment', karma_sr))\n\n    if karma < (sr.wiki_edit_karma or 0):\n        # If the user has too few karma, they should not contribute\n        return False\n    \n    age = (datetime.datetime.now(g.tz) - user._date).days\n    if age < (sr.wiki_edit_age or 0):\n        # If they user's account is too young\n        # They should not contribute\n        return False\n    \n    # Otherwise, allow them to contribute\n    return True\n\ndef may_view(sr, user, page):\n    # User being None means not logged in\n    mod = sr.is_moderator_with_perms(user, 'wiki') if user else False\n    \n    if mod:\n        # Mods may always view\n        return True\n    \n    if page.special:\n        level = WikiPage.get_special_view_permlevel(page.name)\n    else:\n        level = page.permlevel\n    \n    if level < 2:\n        # Everyone may view in levels below 2\n        return True\n    \n    if level == 2:\n        # Only mods may view in level 2\n        return mod\n    \n    # In any other obscure level,\n    # (This should not happen but just in case)\n    # nobody may view.\n    return False\n\ndef normalize_page(page):\n    # Ensure there is no side effect if page is None\n    page = page or \"\"\n    \n    # Replace spaces with underscores\n    page = page.replace(' ', '_')\n    \n    # Case insensitive page names\n    page = page.lower()\n    \n    # Normalize path (And avoid normalizing empty to \".\")\n    if page:\n        page = normpath(page)\n    \n    # Chop off initial \"/\", just in case it exists\n    page = page.lstrip('/')\n    \n    return page\n\nclass AbortWikiError(Exception):\n    pass\n\npage_match_regex = re.compile(r'^[\\w_\\-/]+\\Z')\n\nclass VWikiModerator(VSrModerator):\n    def __init__(self, fatal=False, *a, **kw):\n        VSrModerator.__init__(self, param='page', fatal=fatal, *a, **kw)\n\n    def run(self, page):\n        self.perms = ['wiki']\n        if page and WikiPage.is_special(page):\n            self.perms += ['config']\n        VSrModerator.run(self)\n\nclass VWikiPageName(Validator):\n    def __init__(self, param, error_on_name_normalized=False, *a, **kw):\n        self.error_on_name_normalized = error_on_name_normalized\n        Validator.__init__(self, param, *a, **kw)\n    \n    def run(self, page):\n        original_page = page\n        \n        try:\n            page = str(page) if page else \"\"\n        except UnicodeEncodeError:\n            return self.set_error('INVALID_PAGE_NAME', code=400)\n        \n        page = normalize_page(page)\n        \n        if page and not page_match_regex.match(page):\n            return self.set_error('INVALID_PAGE_NAME', code=400)\n        \n        # If no page is specified, give the index page\n        page = page or \"index\"\n        \n        if WikiPage.is_impossible(page):\n            return self.set_error('INVALID_PAGE_NAME', code=400)\n        \n        if self.error_on_name_normalized and page != original_page:\n            self.set_error('PAGE_NAME_NORMALIZED')\n        \n        return page\n\nclass VWikiPage(VWikiPageName):\n    def __init__(self, param, required=True, restricted=True, modonly=False,\n                 allow_hidden_revision=True, **kw):\n        self.restricted = restricted\n        self.modonly = modonly\n        self.allow_hidden_revision = allow_hidden_revision\n        self.required = required\n        VWikiPageName.__init__(self, param, **kw)\n    \n    def run(self, page):\n        page = VWikiPageName.run(self, page)\n        \n        if self.has_errors:\n            return\n        \n        if (not c.is_wiki_mod) and self.modonly:\n            return self.set_error('MOD_REQUIRED', code=403)\n        \n        try:\n            wp = self.validpage(page)\n        except AbortWikiError:\n            return\n\n        return wp\n    \n    def validpage(self, page):\n        try:\n            wp = WikiPage.get(c.site, page)\n            if self.restricted and wp.restricted:\n                if not (c.is_wiki_mod or wp.special):\n                    self.set_error('RESTRICTED_PAGE', code=403)\n                    raise AbortWikiError\n            if not this_may_view(wp):\n                self.set_error('MAY_NOT_VIEW', code=403)\n                raise AbortWikiError\n            return wp\n        except tdb_cassandra.NotFound:\n            if self.required:\n                self.set_error('PAGE_NOT_FOUND', code=404)\n                raise AbortWikiError\n            return None\n    \n    def validversion(self, version, pageid=None):\n        if not version:\n            return\n        try:\n            r = WikiRevision.get(version, pageid)\n            if r.admin_deleted and not c.user_is_admin:\n                self.set_error('INVALID_REVISION', code=404)\n                raise AbortWikiError\n            if not self.allow_hidden_revision and (r.is_hidden and not c.is_wiki_mod):\n                self.set_error('HIDDEN_REVISION', code=403)\n                raise AbortWikiError\n            return r\n        except (tdb_cassandra.NotFound, WikiBadRevision, ValueError):\n            self.set_error('INVALID_REVISION', code=404)\n            raise AbortWikiError\n\n    def param_docs(self, param=None):\n        return {param or self.param: _('the name of an existing wiki page')}\n\nclass VWikiPageAndVersion(VWikiPage):    \n    def run(self, page, *versions):\n        wp = VWikiPage.run(self, page)\n        if self.has_errors:\n            return\n        validated = []\n        for v in versions:\n            try:\n                validated += [self.validversion(v, wp._id) if v and wp else None]\n            except AbortWikiError:\n                return\n        return tuple([wp] + validated)\n    \n    def param_docs(self):\n        doc = dict.fromkeys(self.param, _('a wiki revision ID'))\n        doc.update(VWikiPage.param_docs(self, self.param[0]))\n        return doc\n\nclass VWikiPageRevise(VWikiPage):\n    def __init__(self, param, required=False, *k, **kw):\n        VWikiPage.__init__(self, param, required=required, *k, **kw)\n    \n    def may_not_create(self, page):\n        if not page:\n            # Should not happen, but just in case\n            self.set_error('EMPTY_PAGE_NAME', 403)\n            return\n        \n        page = normalize_page(page)\n        \n        if WikiPage.is_automatically_created(page):\n            return {'reason': 'PAGE_CREATED_ELSEWHERE'}\n        elif WikiPage.is_special(page):\n            if not (c.user_is_admin or\n                    c.site.is_moderator_with_perms(c.user, 'config')):\n                self.set_error('RESTRICTED_PAGE', code=403)\n                return\n        elif (not c.user_is_admin) and WikiPage.is_restricted(page):\n            self.set_error('RESTRICTED_PAGE', code=403)\n            return\n        elif page.count('/') > MAX_SEPARATORS:\n            return {'reason': 'PAGE_NAME_MAX_SEPARATORS', 'max_separators': MAX_SEPARATORS}\n        elif len(page) > MAX_PAGE_NAME_LENGTH:\n            return {'reason': 'PAGE_NAME_LENGTH', 'max_length': MAX_PAGE_NAME_LENGTH}\n    \n    def run(self, page, previous=None):\n        wp = VWikiPage.run(self, page)\n        if self.has_errors:\n            return\n        if not this_may_revise(wp):\n            if not wp:\n                return self.set_error('PAGE_NOT_FOUND', code=404)\n            # No abort code on purpose, controller will handle\n            self.set_error('MAY_NOT_REVISE')\n            return (None, None)\n        if not wp:\n            # No abort code on purpose, controller will handle\n            error = self.may_not_create(page)\n            if error:\n                self.set_error('WIKI_CREATE_ERROR', msg_params=error)\n            return (None, None)\n        if previous:\n            try:\n                prev = self.validversion(previous, wp._id)\n            except AbortWikiError:\n                return\n            return (wp, prev)\n        return (wp, None)\n    \n    def param_docs(self):\n        docs = {self.param[0]: _('the name of an existing page or a new page to create')}\n        if 'previous' in self.param:\n            docs['previous'] = _('the starting point revision for this edit')\n        return docs\n"
  },
  {
    "path": "r2/r2/lib/voting.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom collections import defaultdict\nfrom datetime import datetime\nimport json\n\nfrom pylons import tmpl_context as c, app_globals as g, request\n\nfrom r2.lib import amqp, hooks\nfrom r2.lib.eventcollector import Event\nfrom r2.lib.utils import epoch_timestamp, is_subdomain, UrlParser\nfrom r2.models import Account, Comment, Link, Subreddit\nfrom r2.models.last_modified import LastModified\nfrom r2.models.query_cache import CachedQueryMutator\nfrom r2.models.vote import Vote, VotesByAccount\n\nfrom r2.lib.geoip import organization_by_ips\n\ndef prequeued_vote_key(user, item):\n    return 'queuedvote:%s_%s' % (user._id36, item._fullname)\n\n\ndef update_vote_lookups(user, thing, direction):\n    \"\"\"Store info about the existence of this vote (before processing).\"\"\"\n    # set the vote in memcached so the UI gets updated immediately\n    key = prequeued_vote_key(user, thing)\n    grace_period = int(g.vote_queue_grace_period.total_seconds())\n    direction = Vote.serialize_direction(direction)\n    g.gencache.set(key, direction, time=grace_period+1)\n\n    # update LastModified immediately to help us cull prequeued_vote lookups\n    rel_cls = VotesByAccount.rel(thing.__class__)\n    LastModified.touch(user._fullname, rel_cls._last_modified_name)\n\n\ndef cast_vote(user, thing, direction, **data):\n    \"\"\"Register a vote and queue it for processing.\"\"\"\n    if not isinstance(thing, (Link, Comment)):\n        return\n\n    update_vote_lookups(user, thing, direction)\n\n    vote_data = {\n        \"user_id\": user._id,\n        \"thing_fullname\": thing._fullname,\n        \"direction\": direction,\n        \"date\": int(epoch_timestamp(datetime.now(g.tz))),\n    }\n\n    data['ip'] = getattr(request, \"ip\", None)\n    if data['ip'] is not None:\n        data['org'] = organization_by_ips(data['ip'])\n    vote_data['data'] = data\n\n    hooks.get_hook(\"vote.get_vote_data\").call(\n        data=vote_data[\"data\"],\n        user=user,\n        thing=thing,\n        request=request,\n        context=c,\n    )\n\n    # The vote event will actually be sent from an async queue processor, so\n    # we need to pull out the context data at this point\n    if not g.running_as_script:\n        vote_data[\"event_data\"] = {\n            \"context\": Event.get_context_data(request, c),\n            \"sensitive\": Event.get_sensitive_context_data(request, c),\n        }\n\n    try:\n        vote_dump = json.dumps(vote_data)\n    except UnicodeDecodeError:\n        g.log.error(\"Got weird unicode in the vote data: %r\", vote_data)\n        return\n\n    if isinstance(thing, Link):\n        queue = \"vote_link_q\"\n    elif isinstance(thing, Comment):\n        queue = \"vote_comment_q\"\n\n    amqp.add_item(queue, vote_dump)\n\n\ndef update_user_liked(vote):\n    from r2.lib.db.queries import get_disliked, get_liked\n\n    with CachedQueryMutator() as m:\n        # if this is a changed vote, remove from the previous cached\n        # query\n        if vote.previous_vote:\n            if vote.previous_vote.is_upvote:\n                m.delete(get_liked(vote.user), [vote.previous_vote])\n            elif vote.previous_vote.is_downvote:\n                m.delete(get_disliked(vote.user), [vote.previous_vote])\n\n        # and then add to the new cached query\n        if vote.is_upvote:\n            m.insert(get_liked(vote.user), [vote])\n        elif vote.is_downvote:\n            m.insert(get_disliked(vote.user), [vote])\n\n\ndef consume_link_vote_queue(qname=\"vote_link_q\"):\n    @g.stats.amqp_processor(qname)\n    def process_message(msg):\n        vote_data = json.loads(msg.body)\n        hook = hooks.get_hook('vote.validate_vote_data')\n        if hook.call_until_return(msg=msg, vote_data=vote_data) is False:\n            # Corrupt records in the queue. Ignore them.\n            print \"Ignoring invalid vote by %s on %s %s\" % (\n                    vote_data.get('user_id', '<unknown>'),\n                    vote_data.get('thing_fullname', '<unknown>'),\n                    vote_data)\n            return\n\n        timer = g.stats.get_timer(\"link_vote_processor\")\n        timer.start()\n\n        user = Account._byID(vote_data.pop(\"user_id\"))\n        link = Link._by_fullname(vote_data.pop(\"thing_fullname\"))\n\n        # create the vote and update the voter's liked/disliked under lock so\n        # that the vote state and cached query are consistent\n        lock_key = \"vote-%s-%s\" % (user._id36, link._fullname)\n        with g.make_lock(\"voting\", lock_key, timeout=5):\n            print \"Processing vote by %s on %s %s\" % (user, link, vote_data)\n\n            try:\n                vote = Vote(\n                    user,\n                    link,\n                    direction=vote_data[\"direction\"],\n                    date=datetime.utcfromtimestamp(vote_data[\"date\"]),\n                    data=vote_data[\"data\"],\n                    event_data=vote_data.get(\"event_data\"),\n                )\n            except TypeError as e:\n                # a vote on an invalid type got in the queue, just skip it\n                g.log.exception(\"Invalid type: %r\", e.message)\n                return\n\n            vote.commit()\n            timer.intermediate(\"create_vote_object\")\n\n            update_user_liked(vote)\n            timer.intermediate(\"voter_likes\")\n\n        vote_valid = vote.is_automatic_initial_vote or vote.effects.affects_score\n        link_valid = not (link._spam or link._deleted)\n        if vote_valid and link_valid:\n            add_to_author_query_q(link)\n            add_to_subreddit_query_q(link)\n            add_to_domain_query_q(link)\n\n        timer.stop()\n        timer.flush()\n\n    amqp.consume_items(qname, process_message, verbose=False)\n\n\n# these sorts can be changed by voting - we don't need to do \"new\" since that's\n# taken care of by new_link and doesn't change afterwards\nSORTS = [\"hot\", \"top\", \"controversial\"]\n\n\ndef add_to_author_query_q(link):\n    if g.shard_author_query_queues:\n        author_shard = link.author_id % 10\n        queue_name = \"author_query_%s_q\" % author_shard\n    else:\n        queue_name = \"author_query_q\"\n    amqp.add_item(queue_name, link._fullname)\n\n\ndef consume_author_query_queue(qname=\"author_query_q\", limit=1000):\n    @g.stats.amqp_processor(qname)\n    def process_message(msgs, chan):\n        \"\"\"Update get_submitted(), the Links by author precomputed query.\n\n        get_submitted() is a CachedResult which is stored in permacache. To\n        update these objects we need to do a read-modify-write which requires\n        obtaining a lock. Sharding these updates by author allows us to run\n        multiple consumers (but ideally just one per shard) to avoid lock\n        contention.\n\n        \"\"\"\n\n        from r2.lib.db.queries import add_queries, get_submitted\n\n        link_names = {msg.body for msg in msgs}\n        links = Link._by_fullname(link_names, return_dict=False)\n        print 'Processing %r' % (links,)\n\n        links_by_author_id = defaultdict(list)\n        for link in links:\n            links_by_author_id[link.author_id].append(link)\n\n        authors_by_id = Account._byID(links_by_author_id.keys())\n\n        for author_id, links in links_by_author_id.iteritems():\n            with g.stats.get_timer(\"link_vote_processor.author_queries\"):\n                author = authors_by_id[author_id]\n                add_queries(\n                    queries=[\n                        get_submitted(author, sort, 'all') for sort in SORTS],\n                    insert_items=links,\n                )\n\n    amqp.handle_items(qname, process_message, limit=limit)\n\n\ndef add_to_subreddit_query_q(link):\n    if g.shard_subreddit_query_queues:\n        subreddit_shard = link.sr_id % 10\n        queue_name = \"subreddit_query_%s_q\" % subreddit_shard\n    else:\n        queue_name = \"subreddit_query_q\"\n    amqp.add_item(queue_name, link._fullname)\n\n\ndef consume_subreddit_query_queue(qname=\"subreddit_query_q\", limit=1000):\n    @g.stats.amqp_processor(qname)\n    def process_message(msgs, chan):\n        \"\"\"Update get_links(), the Links by Subreddit precomputed query.\n\n        get_links() is a CachedResult which is stored in permacache. To\n        update these objects we need to do a read-modify-write which requires\n        obtaining a lock. Sharding these updates by subreddit allows us to run\n        multiple consumers (but ideally just one per shard) to avoid lock\n        contention.\n\n        \"\"\"\n\n        from r2.lib.db.queries import add_queries, get_links\n\n        link_names = {msg.body for msg in msgs}\n        links = Link._by_fullname(link_names, return_dict=False)\n        print 'Processing %r' % (links,)\n\n        links_by_sr_id = defaultdict(list)\n        for link in links:\n            links_by_sr_id[link.sr_id].append(link)\n\n        srs_by_id = Subreddit._byID(links_by_sr_id.keys(), stale=True)\n\n        for sr_id, links in links_by_sr_id.iteritems():\n            with g.stats.get_timer(\"link_vote_processor.subreddit_queries\"):\n                sr = srs_by_id[sr_id]\n                add_queries(\n                    queries=[get_links(sr, sort, \"all\") for sort in SORTS],\n                    insert_items=links,\n                )\n\n    amqp.handle_items(qname, process_message, limit=limit)\n\n\ndef add_to_domain_query_q(link):\n    parsed = UrlParser(link.url)\n    if not parsed.domain_permutations():\n        # no valid domains found\n        return\n\n    if g.shard_domain_query_queues:\n        domain_shard = hash(parsed.hostname) % 10\n        queue_name = \"domain_query_%s_q\" % domain_shard\n    else:\n        queue_name = \"domain_query_q\"\n    amqp.add_item(queue_name, link._fullname)\n\n\ndef consume_domain_query_queue(qname=\"domain_query_q\", limit=1000):\n    @g.stats.amqp_processor(qname)\n    def process_message(msgs, chan):\n        \"\"\"Update get_domain_links(), the Links by domain precomputed query.\n\n        get_domain_links() is a CachedResult which is stored in permacache. To\n        update these objects we need to do a read-modify-write which requires\n        obtaining a lock. Sharding these updates by domain allows us to run\n        multiple consumers (but ideally just one per shard) to avoid lock\n        contention.\n\n        \"\"\"\n\n        from r2.lib.db.queries import add_queries, get_domain_links\n\n        link_names = {msg.body for msg in msgs}\n        links = Link._by_fullname(link_names, return_dict=False)\n        print 'Processing %r' % (links,)\n\n        links_by_domain = defaultdict(list)\n        for link in links:\n            parsed = UrlParser(link.url)\n\n            # update the listings for all permutations of the link's domain\n            for domain in parsed.domain_permutations():\n                links_by_domain[domain].append(link)\n\n        for d, links in links_by_domain.iteritems():\n            with g.stats.get_timer(\"link_vote_processor.domain_queries\"):\n                add_queries(\n                    queries=[\n                        get_domain_links(d, sort, \"all\") for sort in SORTS],\n                    insert_items=links,\n                )\n\n    amqp.handle_items(qname, process_message, limit=limit)\n\n\ndef consume_comment_vote_queue(qname=\"vote_comment_q\"):\n    @g.stats.amqp_processor(qname)\n    def process_message(msg):\n        from r2.lib.comment_tree import write_comment_scores\n        from r2.lib.db.queries import (\n            add_queries,\n            add_to_commentstree_q,\n            get_comments,\n        )\n        from r2.models.builder import get_active_sort_orders_for_link\n\n        vote_data = json.loads(msg.body)\n        hook = hooks.get_hook('vote.validate_vote_data')\n        if hook.call_until_return(msg=msg, vote_data=vote_data) is False:\n            # Corrupt records in the queue. Ignore them.\n            print \"Ignoring invalid vote by %s on %s %s\" % (\n                    vote_data.get('user_id', '<unknown>'),\n                    vote_data.get('thing_fullname', '<unknown>'),\n                    vote_data)\n            return\n\n        timer = g.stats.get_timer(\"comment_vote_processor\")\n        timer.start()\n\n        user = Account._byID(vote_data.pop(\"user_id\"))\n        comment = Comment._by_fullname(vote_data.pop(\"thing_fullname\"))\n\n        print \"Processing vote by %s on %s %s\" % (user, comment, vote_data)\n\n        try:\n            vote = Vote(\n                user,\n                comment,\n                direction=vote_data[\"direction\"],\n                date=datetime.utcfromtimestamp(vote_data[\"date\"]),\n                data=vote_data[\"data\"],\n                event_data=vote_data.get(\"event_data\"),\n            )\n        except TypeError as e:\n            # a vote on an invalid type got in the queue, just skip it\n            g.log.exception(\"Invalid type: %r\", e.message)\n            return\n\n        vote.commit()\n        timer.intermediate(\"create_vote_object\")\n\n        vote_invalid = (not vote.effects.affects_score and\n            not vote.is_automatic_initial_vote)\n        comment_invalid = comment._spam or comment._deleted\n        if vote_invalid or comment_invalid:\n            timer.stop()\n            timer.flush()\n            return\n\n        author = Account._byID(comment.author_id)\n        add_queries(\n            queries=[get_comments(author, sort, 'all') for sort in SORTS],\n            insert_items=comment,\n        )\n        timer.intermediate(\"author_queries\")\n\n        update_threshold = g.live_config['comment_vote_update_threshold']\n        update_period = g.live_config['comment_vote_update_period']\n        skip_score_update = (comment.num_votes > update_threshold and\n            comment.num_votes % update_period != 0)\n\n        # skip updating scores if this was the automatic initial vote. those\n        # updates will be handled by new_comment. Also only update scores\n        # periodically once a comment has many votes.\n        if not vote.is_automatic_initial_vote and not skip_score_update:\n            # check whether this link is using precomputed sorts, if it is\n            # we'll need to push an update to commentstree_q\n            link = Link._byID(comment.link_id)\n            if get_active_sort_orders_for_link(link):\n                # send this comment to commentstree_q where we will update\n                # CommentScoresByLink, CommentTree (noop), and CommentOrderer\n                add_to_commentstree_q(comment)\n            else:\n                # the link isn't using precomputed sorts, so just update the\n                # scores\n                write_comment_scores(link, [comment])\n                timer.intermediate(\"update_scores\")\n\n        timer.stop()\n        timer.flush()\n\n    amqp.consume_items(qname, process_message, verbose=False)\n"
  },
  {
    "path": "r2/r2/lib/websockets.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\"\"\"Utilities for interfacing with the WebSocket server Sutro.\"\"\"\n\nimport datetime\nimport json\nimport urllib\nimport urlparse\n\nfrom baseplate.crypto import MessageSigner\nfrom pylons import app_globals as g\n\nfrom r2.lib import amqp\nfrom r2.lib.filters import websafe_json\n\n\n_WEBSOCKET_EXCHANGE = \"sutro\"\n\n\ndef send_broadcast(namespace, type, payload):\n    \"\"\"Broadcast an object to all WebSocket listeners in a namespace.\n\n    The message type is used to differentiate between different kinds of\n    payloads that may be sent. The payload will be encoded as a JSON object\n    before being sent to the client.\n\n    \"\"\"\n    frame = {\n        \"type\": type,\n        \"payload\": payload,\n    }\n    amqp.add_item(routing_key=namespace, body=json.dumps(frame),\n                  exchange=_WEBSOCKET_EXCHANGE)\n\n\ndef make_url(namespace, max_age):\n    \"\"\"Return a signed URL for the client to use for websockets.\n\n    The namespace determines which messages the client receives and max_age is\n    the number of seconds the URL is valid for.\n\n    \"\"\"\n\n    signer = MessageSigner(g.secrets[\"websocket\"])\n    signature = signer.make_signature(\n        namespace, max_age=datetime.timedelta(seconds=max_age))\n\n    query_string = urllib.urlencode({\n        \"m\": signature,\n    })\n\n    return urlparse.urlunparse((\"wss\", g.websocket_host, namespace,\n                               None, query_string, None))\n"
  },
  {
    "path": "r2/r2/lib/wrapped.pyx",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\n\nfrom datetime import datetime\nfrom hashlib import md5\nfrom itertools import chain\nimport random\nimport re\nimport types\n\nfrom r2.lib.cache import MemcachedError\nfrom r2.lib.utils import SimpleSillyStub\n\n\nCACHE_HIT_SAMPLE_RATE = 0.001\nRENDER_TIMER_SAMPLE_RATE = 0.001\n\nclass _TemplateUpdater(object):\n    # this class is just a hack to get around Cython's closure rules\n\n    __slots = ['d', 'start', 'end', 'template', 'pattern']\n\n    def __init__(self, d, start, end, template, pattern):\n        self.d = d\n        self.start, self.end = start, end\n        self.template = template\n        self.pattern = pattern\n\n    def update(self):\n        return self.pattern.sub(self._convert, self.template)\n\n    def _convert(self, m):\n        name = m.group(\"named\")\n        return self.d.get(name, self.start + name + self.end)\n\nclass StringTemplate(object):\n    \"\"\"\n    Simple-minded string templating, where variables of the for $____\n    in a strinf are replaced with values based on a dictionary.\n\n    Unline the built-in Template class, this supports an update method\n\n    We could use the built in python Template class for this, but\n    unfortunately it doesn't handle unicode as gracefully as we'd\n    like.\n    \"\"\"\n    start_delim = \"<$>\"\n    end_delim = \"</$>\"\n    pattern2 = r\"[_a-z][_a-z0-9]*\"\n    pattern2 = r\"%(start_delim)s(?:(?P<named>%(pattern)s))%(end_delim)s\" % \\\n               dict(pattern = pattern2,\n                    start_delim = re.escape(start_delim),\n                    end_delim = re.escape(end_delim),\n                    )\n    pattern2 = re.compile(pattern2,  re.UNICODE)\n\n    def __init__(self, template):\n        # for the nth time, we have to transform the string into\n        # unicode.  Otherwise, re.sub will choke on non-ascii\n        # characters.\n        try:\n            self.template = unicode(template)\n        except UnicodeDecodeError:\n            self.template = unicode(template, \"utf8\")\n\n    def update(self, d):\n        \"\"\"\n        Given a dictionary of replacement rules for the Template,\n        replace variables in the template (once!) and return an\n        updated Template.\n        \"\"\"\n        if d:\n            updater = _TemplateUpdater(d, self.start_delim, self.end_delim,\n                                       self.template, self.pattern2)\n            return self.__class__(updater.update())\n        return self\n\n    def finalize(self, d = {}):\n        \"\"\"\n        The same as update, except the dictionary is optional and the\n        object returned will be a unicode object.\n        \"\"\"\n        return self.update(d).template\n\n\nclass CacheStub(object):\n    \"\"\"\n    When using cached renderings, this class generates a stub based on\n    the hash of the Templated item passed into init for the style\n    specified.\n\n    This class is suitable as a stub object (in the case of API calls)\n    and wil render in a string form suitable for replacement with\n    StringTemplate in the case of normal rendering. \n    \"\"\"\n    def __init__(self, item, style):\n        self.name = \"h%s%s\" % (id(item), str(style).replace('-', '_'))\n\n    def __str__(self):\n        return StringTemplate.start_delim + self.name + \\\n               StringTemplate.end_delim\n\n    def __repr__(self):\n        return \"<%s: %s>\" % (self.__class__.__name__, self.name)\n\nclass CachedVariable(CacheStub):\n    \"\"\"\n    Same use as CacheStubs in normal templates, except it can be\n    applied to where we would normally put a '$____' variable by hand\n    in a template (file).\n    \"\"\"\n    def __init__(self, name):\n        self.name = name\n\n\nclass Templated(object):\n    \"\"\"\n    Replaces the Wrapped class (which has now a subclass and which\n    takes an thing to be wrapped).\n\n    Templated objects are suitable for rendering and caching, with a\n    render loop desgined to fetch other cached templates and insert\n    them into the current template.\n\n    \"\"\"\n\n    # is this template cachable (see CachedTemplate)\n    cachable = False\n    # attributes that will not be made into the cache key\n    cache_ignore = set()\n\n    def __repr__(self):\n        return \"<Templated: %s>\" % self.__class__.__name__\n    \n    def __init__(self, **context):\n        \"\"\"\n        uses context to init __dict__ (making this object a bit like a storage)\n        \"\"\"\n        for k, v in context.iteritems():\n            setattr(self, k, v)\n        if not hasattr(self, \"render_class\"):\n            self.render_class = self.__class__\n\n    def template(self, style='html'):\n        \"\"\"\n        Fetches template from the template manager\n        \"\"\"\n        from r2.config.templates import tpm\n\n        return tpm.get(self.render_class, style)\n\n    def template_is_null(self, style='html'):\n        template = self.template(style)\n        return getattr(template, \"is_null\", False)\n\n    def cache_key(self, *a):\n        \"\"\"\n        if cachable, this function is used to generate the cache key. \n        \"\"\"\n        raise NotImplementedError\n\n    @property\n    def render_class_name(self):\n        return self.render_class.__name__\n\n    def render_nocache(self, style):\n        \"\"\"\n        No-frills (or caching) rendering of the template.  The\n        template is returned as a subclass of StringTemplate and\n        therefore finalize() must be called on it to turn it into its\n        final form\n        \"\"\"\n        from filters import unsafe\n        from pylons import tmpl_context as c\n        from pylons import app_globals as g\n\n        if (self.cachable and\n                style != \"api\" and\n                random.random() < RENDER_TIMER_SAMPLE_RATE):\n            timer = g.stats.get_timer(name=\"render.%s\" % self.render_class_name)\n        else:\n            timer = SimpleSillyStub()\n\n        timer.start()\n        template = self.template(style)\n\n        # store the global render style (child templates might override it)\n        render_style = c.render_style\n        c.render_style = style\n\n        res = template.render(thing=self)\n        if not isinstance(res, StringTemplate):\n            res = StringTemplate(res)\n\n        # reset the global render style\n        c.render_style = render_style\n        timer.stop()\n        return res\n\n    def _render(self, style, **kwargs):\n        \"\"\"\n        Renders the current template with the current style.\n\n        if this is the first template to be rendered, it is will track\n        cachable templates, insert stubs for them in the output,\n        get_multi from the cache, and render the uncached templates.\n        Uncached but cachable templates are inserted back into the\n        cache with a set_multi.\n\n        NOTE: one of the interesting issues with this function is that\n        on each newly rendered thing, it is possible that that\n        rendering has in turn cause more cachable things to be\n        fetched.  Thus the first template to be rendered runs a loop\n        and keeps rendering until there is nothing left to render.\n        Then it updates the master template until it doesn't change.\n\n        NOTE 2: anything passed in as a kw to render (and thus\n        _render) will not be part of the cached version of the object,\n        and will substituted last.\n        \"\"\"\n        from pylons import tmpl_context as c\n        from pylons import app_globals as g\n\n        style = style or c.render_style or 'html'\n\n        # prepare (and store) the list of cachable items. \n        primary = False\n        if not isinstance(c.render_tracker, dict):\n            primary = True\n            c.render_tracker = {}\n\n        if (self.cachable and\n                not self.template_is_null(style) and\n                style != \"api\"):\n            # insert a stub for cachable non-primary templates\n            res = CacheStub(self, style)\n            cache_key = self.cache_key(style)\n            # in the tracker, we need to store:\n            #  The render cache key (res.name)\n            #  The memcached cache key(cache_key)\n            #  who I am (self) and what am I doing (style) with what\n            #  (kwargs)\n            c.render_tracker[res.name] = (cache_key, (self, (style, kwargs)))\n        else:\n            # either a primary template or not cachable, so render it\n            res = self.render_nocache(style)\n\n        # if this is the primary template, let the caching games begin\n        if primary:\n            # updates will be the (self-updated) list of all of\n            # the cached templates that have been cached or\n            # rendered.\n            updates = {}\n            # to_cache is just the keys of the cached templates\n            # that were not in the cache.\n            to_cache = set([])\n            while c.render_tracker:\n                # copy and wipe the tracker.  It'll get repopulated if\n                # any of the subsequent render()s call cached objects.\n                current = c.render_tracker\n                c.render_tracker = {}\n    \n                # do a multi-get.  NOTE: cache keys are the first item\n                # in the tuple that is the current dict's values.\n                # This dict cast will generate a new dict of cache_key\n                # to value\n                cached = self._read_cache(dict(current.values()))\n                # replacements will be a map of key -> rendered content\n                # for updateing the current set of updates\n                replacements = {}\n\n                new_updates = {}\n                # render items that didn't make it into the cached list\n                for key, (cache_key, others) in current.iteritems():\n                    # unbundle the remaining args\n                    item, (style, kw) = others\n                    if cache_key not in cached:\n                        # this had to be rendered, so cache it later\n                        to_cache.add(cache_key)\n                        # render the item and apply the stored kw args\n                        r = item.render_nocache(style)\n                    else:\n                        r = cached[cache_key]\n\n                    event_name = 'render-cache.%s' % item.render_class_name\n                    name = 'hit' if cache_key in cached else 'miss'\n                    g.stats.event_count(\n                        event_name, name, sample_rate=CACHE_HIT_SAMPLE_RATE)\n\n                    # store the unevaluated templates in\n                    # cached for caching\n                    replacements[key] = r.finalize(kw)\n                    new_updates[key] = (cache_key, (r, kw))\n\n                # update the updates so that when we can do the\n                # replacement in one pass.\n                \n                # NOTE: keep kw, but don't update based on them.\n                # We might have to cache these later, and we want\n                # to have things like $child present.\n                for k in updates.keys():\n                    cache_key, (value, kw) = updates[k]\n                    value = value.update(replacements)\n                    updates[k] = cache_key, (value, kw)\n\n                updates.update(new_updates)\n    \n            # at this point, we haven't touched res, but updates now\n            # has the list of all the updates we could conceivably\n            # want to make, and to_cache is the list of cache keys\n            # that we didn't find in the cache.\n\n            # cache content that was newly rendered\n            _to_cache = {}\n            for k, (v, kw) in updates.values():\n                if k in to_cache:\n                    _to_cache[k] = v\n            self._write_cache(_to_cache)\n\n            # edge case: this may be the primary tempalte and cachable\n            if isinstance(res, CacheStub):\n                res = updates[res.name][1][0]\n\n            # now we can update the updates to make use of their kw args.\n            _updates = {}\n            for k, (foo, (v, kw)) in updates.iteritems():\n                _updates[k] = v.finalize(kw)\n            updates = _updates\n\n            # update the response to use these values\n            # replace till we can't replace any more. \n            npasses = 0\n            while True:\n                npasses += 1\n                r = res\n                res = res.update(kwargs).update(updates)\n                semi_final = res.finalize()\n                if r.finalize() == res.finalize():\n                    res = semi_final\n                    break\n\n            # wipe out the render tracker object\n            c.render_tracker = None\n        elif not isinstance(res, CacheStub):\n            # we're done.  Update the template based on the args passed in\n            res = res.finalize(kwargs)\n\n        return res\n\n    def _write_cache(self, keys):\n        from pylons import app_globals as g\n\n        if not keys:\n            return\n\n        try:\n            g.rendercache.set_multi(keys, time=3600)\n        except MemcachedError as e:\n            g.log.warning(\"rendercache error: %s\", e)\n            return\n\n    def _read_cache(self, keys):\n        from pylons import app_globals as g\n\n        ret = g.rendercache.get_multi(keys)\n        return ret\n\n    def render(self, style = None, **kw):\n        from r2.lib.filters import unsafe\n        res = self._render(style, **kw)\n        return unsafe(res) if isinstance(res, str) else res\n\n\nclass Uncachable(Exception): pass\n\n_easy_cache_cls = set([bool, int, long, float, unicode, str, types.NoneType,\n                      datetime])\n\ndef make_cachable(v, style):\n    if v.__class__ in _easy_cache_cls or isinstance(v, type):\n        try:\n            return unicode(v)\n        except UnicodeDecodeError:\n            try:\n                return unicode(v, \"utf8\")\n            except (TypeError, UnicodeDecodeError):\n                return repr(v)\n    elif isinstance(v, (types.MethodType, CachedVariable)):\n       return\n    elif isinstance(v, (tuple, list, set)):\n        return repr([make_cachable(x, style) for x in v])\n    elif isinstance(v, dict):\n        ret = {}\n        for k in sorted(v.iterkeys()):\n            ret[k] = make_cachable(v[k], style)\n        return repr(ret)\n    elif hasattr(v, \"cache_key\"):\n        return v.cache_key(style)\n    else:\n        raise Uncachable, \"%s, %s\" % (v, type(v))\n\nclass CachedTemplate(Templated):\n    cachable = True\n\n    def template_hash(self, style):\n        template = self.template(style)\n\n        # if template debugging is on, there will be no hash and we can make the\n        # caching process-local\n        template_hash = getattr(template, \"hash\", id(self.__class__))\n        return template_hash\n\n    def cachable_attrs(self):\n        \"\"\"\n        Generates an iterator of attr names and their values for every\n        attr on this element that should be used in generating the cache key.\n        \"\"\"\n        ret = []\n        for k in sorted(self.__dict__):\n            if k not in self.cache_ignore and not k.startswith('_'):\n                ret.append((k, self.__dict__[k]))\n        return ret\n\n    def cache_key(self, style):\n        from pylons import request\n        from pylons import tmpl_context as c\n\n        # these values are needed to render any link on the site, and\n        # a menu is just a set of links, so we best cache against\n        # them.\n        keys = [\n            c.user_is_loggedin,\n            c.user_is_admin,\n            c.domain_prefix,\n            style,\n            c.secure,\n            c.lang,\n            c.site.user_path,\n            self.template_hash(style),\n        ]\n\n        if c.secure:\n            keys.append(request.host)\n\n        keys = [make_cachable(x, style) for x in keys]\n\n        # add all parameters sent into __init__, using their current value\n        auto_keys = [(k,  make_cachable(v, style))\n                     for k, v in self.cachable_attrs()]\n        keys.append(repr(auto_keys))\n        h = md5(u''.join(keys)).hexdigest()\n        return \"rend:%s:%s\" % (self.render_class_name, h)\n\n\nclass Wrapped(CachedTemplate):\n    # default to false, evaluate\n    cachable = False\n    cache_ignore = set(['lookups'])\n    \n    def cache_key(self, style):\n        if self.cachable:\n            for i, l in enumerate(self.lookups):\n                if hasattr(l, \"wrapped_cache_key\"):\n                    # setattr will force a __dict__ entry, but only if the\n                    # param doesn't start with \"_\"\n                    setattr(self, \"lookup%d_cache_key\" % i,\n                            ''.join(map(repr,\n                                        l.wrapped_cache_key(self, style))))\n        return CachedTemplate.cache_key(self, style)\n\n    def __init__(self, *lookups, **context):\n        self.lookups = lookups\n        # set the default render class to be based on the lookup\n        if self.__class__ == Wrapped and lookups:\n            self.render_class = lookups[0].__class__\n        else:\n            self.render_class = self.__class__\n        # this shouldn't be too surprising\n        self.cache_ignore = self.cache_ignore.union(\n            set(['cachable', 'render', 'cache_ignore', 'lookups']))\n        if (not self._any_hasattr(lookups, 'cachable') and \n            self._any_hasattr(lookups, 'wrapped_cache_key')):\n            self.cachable = True\n        if self.cachable:\n            for l in lookups:\n                if hasattr(l, \"cache_ignore\"):\n                    self.cache_ignore = self.cache_ignore.union(l.cache_ignore)\n            \n        Templated.__init__(self, **context)\n\n    def _any_hasattr(self, lookups, attr):\n        for l in lookups:\n            if hasattr(l, attr):\n                return True\n\n    def __repr__(self):\n        return \"<Wrapped: %s,  %s>\" % (self.__class__.__name__,\n                                       self.lookups)\n\n    def __getattr__(self, attr):\n        if attr == 'lookups':\n            raise AttributeError, attr\n\n        res = None\n        found = False\n        for lookup in self.lookups:\n            try:\n                res = getattr(lookup, attr)\n                found = True\n                break\n            except AttributeError:\n                pass\n\n        if not found:\n            raise AttributeError, \"%r has no %s\" % (self, attr)\n\n        setattr(self, attr, res)\n        return res\n\n    def __iter__(self):\n        if self.lookups and hasattr(self.lookups[0], \"__iter__\"):\n            return self.lookups[0].__iter__()\n        raise NotImplementedError\n\nclass Styled(CachedTemplate):\n    \"\"\"Rather than creating a separate template for every possible\n    menu/button style we might want to use, this class overrides the\n    render function to render only the <%def> in the template whose\n    name matches 'style'.\n\n    Additionally, when rendering, the '_id' and 'css_class' attributes\n    are intended to be used in the outermost container's id and class\n    tag.\n    \"\"\"\n\n    def __init__(self, style, _id = '', css_class = '', **kw):\n        self._id = _id\n        self.css_class = css_class\n        self.style = style\n        CachedTemplate.__init__(self, **kw)\n\n    def template(self, style='html'):\n        base_template = CachedTemplate.template(self, style)\n        template = base_template.get_def(self.style)\n        return template\n\n    def template_hash(self, style):\n        # use the hash of the base template so changes to the template file\n        # will be recognized\n        base_template = CachedTemplate.template(self, style)\n\n        # if template debugging is on, there will be no hash and we can make the\n        # caching process-local\n        template_hash = getattr(base_template, \"hash\", id(self.__class__))\n        return template_hash\n"
  },
  {
    "path": "r2/r2/lib/zookeeper.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport os\nimport json\nimport urllib\nimport functools\nimport zlib\n\nfrom kazoo.client import KazooClient\nfrom kazoo.security import make_digest_acl\nfrom kazoo.exceptions import NoNodeException\n\nfrom r2.lib import hooks\nfrom r2.lib.contrib import ipaddress\n\n\ndef connect_to_zookeeper(hostlist, credentials):\n    \"\"\"Create a connection to the ZooKeeper ensemble.\n\n    If authentication credentials are provided (as a two-tuple: username,\n    password), we will ensure that they are provided to the server whenever we\n    establish a connection.\n\n    \"\"\"\n\n    client = KazooClient(hostlist,\n                         timeout=5,\n                         max_retries=3,\n                         auth_data=[(\"digest\", \":\".join(credentials))])\n\n    # convenient helper function for making credentials\n    client.make_acl = functools.partial(make_digest_acl, *credentials)\n\n    client.start()\n    return client\n\n\nclass LiveConfig(object):\n    \"\"\"A read-only dictionary view of configuration retrieved from ZooKeeper.\n\n    The data will be parsed using the given configuration specs, exactly like\n    the ini file based configuration. When data is changed in ZooKeeper, the\n    data in this view will automatically update.\n\n    \"\"\"\n    def __init__(self, client, key):\n        self.data = {}\n\n        @client.DataWatch(key)\n        def watcher(data, stat):\n            if data and data.startswith(\"gzip\"):\n                data = zlib.decompress(data[len(\"gzip\"):])\n            self.data = json.loads(data or '{}')\n            hooks.get_hook(\"worker.live_config.update\").call()\n\n    def __getitem__(self, key):\n        return self.data[key]\n\n    def get(self, key, default=None):\n        return self.data.get(key, default)\n\n    def iteritems(self):\n        return self.data.iteritems()\n\n    def __repr__(self):\n        return \"<LiveConfig %r>\" % self.data\n\n\nclass LiveList(object):\n    \"\"\"A mutable set shared by all apps and backed by ZooKeeper.\"\"\"\n    def __init__(self, client, root, map_fn=None, reduce_fn=lambda L: L,\n                 watch=True):\n        self.client = client\n        self.root = root\n        self.map_fn = map_fn\n        self.reduce_fn = reduce_fn\n        self.is_watching = watch\n\n        acl = [self.client.make_acl(read=True, create=True, delete=True)]\n        self.client.ensure_path(self.root, acl)\n\n        if watch:\n            self.data = []\n\n            @client.ChildrenWatch(root)\n            def watcher(children):\n                self.data = self._normalize_children(children, reduce=True)\n\n    def _nodepath(self, item):\n        escaped = urllib.quote(str(item), safe=\":\")\n        return os.path.join(self.root, escaped)\n\n    def _normalize_children(self, children, reduce):\n        unquoted = (urllib.unquote(c) for c in children)\n        mapped = map(self.map_fn, unquoted)\n\n        if reduce:\n            return list(self.reduce_fn(mapped))\n        else:\n            return list(mapped)\n\n    def add(self, item):\n        path = self._nodepath(item)\n        self.client.ensure_path(path)\n\n    def remove(self, item):\n        path = self._nodepath(item)\n\n        try:\n            self.client.delete(path)\n        except NoNodeException:\n            raise ValueError(\"not in list\")\n\n    def get(self, reduce=True):\n        children = self.client.get_children(self.root)\n        return self._normalize_children(children, reduce)\n\n    def __iter__(self):\n        if not self.is_watching:\n            raise NotImplementedError()\n        return iter(self.data)\n\n    def __len__(self):\n        if not self.is_watching:\n            raise NotImplementedError()\n        return len(self.data)\n\n    def __repr__(self):\n        return \"<LiveList %r (%s)>\" % (self.data,\n                                       \"push\" if self.is_watching else \"pull\")\n\n\nclass ReducedLiveList(object):\n    \"\"\"Store a copy of the reduced data in addition to the full LiveList.\n\n    This is useful for cases where the map/reduce phase is slow and CPU\n    intensive. By storing the reduced data separately the map/reduce only needs\n    to be done by the process that's updating the list. All other processes\n    watch the reduced data node.\n\n    \"\"\"\n\n    def __init__(self, client, root, reduced_data_node, map_fn=None,\n                 reduce_fn=lambda L: L, to_json_fn=None, from_json_fn=None):\n        # don't watch the underlying LiveList because all updates are triggered\n        # on the reduced data node\n        self.live_list = LiveList(\n            client, root, map_fn=map_fn, reduce_fn=reduce_fn, watch=False)\n\n        self.client = client\n        self.root = root\n        self.reduced_data_node = reduced_data_node\n\n        acl = [self.client.make_acl(\n            read=True, write=True, create=True, delete=True)]\n        self.client.ensure_path(self.reduced_data_node, acl)\n\n        self.data = []\n        self.to_json_fn = to_json_fn\n        self.from_json_fn = from_json_fn\n\n        @client.DataWatch(self.reduced_data_node)\n        def watcher(json_data, stat):\n            if json_data and json_data.startswith(\"gzip\"):\n                json_data = zlib.decompress(json_data[len(\"gzip\"):])\n            self.data = self.from_json_fn(json_data or '{}')\n\n    def update(self):\n        data = self.live_list.get(reduce=True)\n        json_data = self.to_json_fn(data)\n        compressed_data = \"gzip\" + zlib.compress(json_data)\n        self.client.set(self.reduced_data_node, compressed_data)\n\n    def add(self, item):\n        self.live_list.add(item)\n        self.update()\n\n    def remove(self, item):\n        self.live_list.remove(item)\n        self.update()\n\n    def get(self, reduce=True):\n        if reduce:\n            return self.data\n        else:\n            return self.live_list.get(reduce=False)\n\n    def __iter__(self):\n        return iter(self.data)\n\n    def __len__(self):\n        return len(self.data)\n\n    def __repr__(self):\n        return \"<%s %r>\" % (self.__class__.__name__, self.data)\n\n\nclass IPNetworkLiveList(ReducedLiveList):\n    def __init__(self, client, root, reduced_data_node):\n        def ipnetwork_to_json(ipnetwork_list):\n            d = json.dumps([str(ipn) for ipn in ipnetwork_list])\n            return d\n\n        def json_to_ipnetwork(d):\n            ipnetwork_list = [ipaddress.ip_network(s) for s in json.loads(d)]\n            return ipnetwork_list\n\n        ReducedLiveList.__init__(\n            self, client, root, reduced_data_node,\n            map_fn=ipaddress.ip_network,\n            reduce_fn=ipaddress.collapse_addresses,\n            to_json_fn=ipnetwork_to_json,\n            from_json_fn=json_to_ipnetwork,\n        )\n"
  },
  {
    "path": "r2/r2/models/__init__.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom account import *\nfrom ip import *\nfrom link import *\nfrom listing import *\nfrom vote import *\nfrom report import *\nfrom rules import *\nfrom subreddit import *\nfrom flair import *\nfrom award import *\nfrom bidding import *\nfrom mail_queue import Email, has_opted_out, opt_count\nfrom gold import *\nfrom admintools import *\nfrom token import *\nfrom modaction import *\nfrom promo import *\n\n# r2.models.builder will import other models, so pulling its classes/vars into\n# r2.models needs to be done last to ensure that the models it depends\n# on are already loaded.\nfrom builder import *\n"
  },
  {
    "path": "r2/r2/models/account.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport bcrypt\nfrom collections import Counter, OrderedDict\nfrom datetime import datetime, timedelta\nimport hashlib\nimport hmac\nimport time\n\nfrom pylons import request\nfrom pylons import tmpl_context as c\nfrom pylons import app_globals as g\nfrom pycassa.system_manager import ASCII_TYPE, DATE_TYPE, UTF8_TYPE\n\nfrom r2.config import feature\nfrom r2.lib import amqp, filters, hooks\nfrom r2.lib.db.thing import Thing, Relation, NotFound\nfrom r2.lib.db.operators import lower\nfrom r2.lib.db.userrel import UserRel\nfrom r2.lib.db import tdb_cassandra\nfrom r2.lib.memoize import memoize\nfrom r2.lib.utils import (\n    randstr,\n    UrlParser,\n    constant_time_compare,\n    canonicalize_email,\n    tup,\n)\nfrom r2.models.bans import TempTimeout\nfrom r2.models.last_modified import LastModified\nfrom r2.models.modaction import ModAction\nfrom r2.models.trylater import TryLater\n\n\ntrylater_hooks = hooks.HookRegistrar()\nCOOKIE_TIMESTAMP_FORMAT = '%Y-%m-%dT%H:%M:%S'\n\n\nclass AccountExists(Exception):\n    pass\n\n\nclass Account(Thing):\n    _cache = g.thingcache\n    _data_int_props = Thing._data_int_props + ('link_karma', 'comment_karma',\n                                               'report_made', 'report_correct',\n                                               'report_ignored', 'spammer',\n                                               'reported', 'gold_creddits',\n                                               'inbox_count',\n                                               'num_payment_methods',\n                                               'num_failed_payments',\n                                               'num_gildings',\n                                               'admin_takedown_strikes',\n                                              )\n    _int_prop_suffix = '_karma'\n    _essentials = ('name', )\n    _defaults = dict(pref_numsites = 25,\n                     pref_newwindow = False,\n                     pref_clickgadget = 5,\n                     pref_store_visits = False,\n                     pref_public_votes = False,\n                     pref_hide_from_robots = False,\n                     pref_research = False,\n                     pref_hide_ups = False,\n                     pref_hide_downs = False,\n                     pref_min_link_score = -4,\n                     pref_min_comment_score = -4,\n                     pref_num_comments = g.num_comments,\n                     pref_highlight_controversial=False,\n                     pref_default_comment_sort = 'confidence',\n                     pref_lang = g.lang,\n                     pref_content_langs = (g.lang,),\n                     pref_over_18 = False,\n                     pref_compress = False,\n                     pref_domain_details = False,\n                     pref_organic = True,\n                     pref_no_profanity = True,\n                     pref_label_nsfw = True,\n                     pref_show_stylesheets = True,\n                     pref_enable_default_themes=False,\n                     pref_default_theme_sr=None,\n                     pref_show_flair = True,\n                     pref_show_link_flair = True,\n                     pref_mark_messages_read = True,\n                     pref_threaded_messages = True,\n                     pref_collapse_read_messages = False,\n                     pref_email_messages = False,\n                     pref_private_feeds = True,\n                     pref_force_https = False,\n                     pref_hide_ads = False,\n                     pref_show_trending=True,\n                     pref_highlight_new_comments = True,\n                     pref_monitor_mentions=True,\n                     pref_collapse_left_bar=False,\n                     pref_public_server_seconds=False,\n                     pref_ignore_suggested_sort=False,\n                     pref_beta=False,\n                     pref_legacy_search=False,\n                     mobile_compress = False,\n                     mobile_thumbnail = True,\n                     reported = 0,\n                     report_made = 0,\n                     report_correct = 0,\n                     report_ignored = 0,\n                     spammer = 0,\n                     sort_options = {},\n                     has_subscribed = False,\n                     pref_media = 'subreddit',\n                     pref_media_preview = 'subreddit',\n                     wiki_override = None,\n                     email = \"\",\n                     email_verified = False,\n                     nsfw_media_acknowledged = False,\n                     ignorereports = False,\n                     pref_show_promote = None,\n                     gold = False,\n                     gold_charter = False,\n                     gold_creddits = 0,\n                     num_gildings=0,\n                     cake_expiration=None,\n                     otp_secret=None,\n                     state=0,\n                     modmsgtime=None,\n                     inbox_count=0,\n                     banned_profile_visible=False,\n                     pref_use_global_defaults=False,\n                     pref_hide_locationbar=False,\n                     pref_creddit_autorenew=False,\n                     update_sent_messages=True,\n                     num_payment_methods=0,\n                     num_failed_payments=0,\n                     pref_show_snoovatar=False,\n                     gild_reveal_username=False,\n                     selfserve_cpm_override_pennies=None,\n                     pref_show_gold_expiration=False,\n                     admin_takedown_strikes=0,\n                     pref_threaded_modmail=False,\n                     in_timeout=False,\n                     has_used_mobile_app=False,\n                     disable_karma=False,\n                     )\n    _preference_attrs = tuple(k for k in _defaults.keys()\n                              if k.startswith(\"pref_\"))\n\n    @classmethod\n    def _cache_prefix(cls):\n        return \"account:\"\n\n    def preferences(self):\n        return {pref: getattr(self, pref) for pref in self._preference_attrs}\n\n    def __eq__(self, other):\n        if type(self) != type(other):\n            return False\n\n        return self._id == other._id\n\n    def __ne__(self, other):\n        return not self.__eq__(other)\n\n    def has_interacted_with(self, sr):\n        try:\n            r = SubredditParticipationByAccount.fast_query(self, [sr])\n        except tdb_cassandra.NotFound:\n            return False\n\n        return (self, sr) in r\n\n    def karma(self, kind, sr = None):\n        suffix = '_' + kind + '_karma'\n\n        #if no sr, return the sum\n        if sr is None:\n            total = 0\n            for k, v in self._t.iteritems():\n                if k.endswith(suffix):\n                    total += v\n\n            # link karma includes both \"link\" and \"self\" values\n            if kind == \"link\":\n                total += self.karma(\"self\")\n\n            return total\n\n        # if positive karma overall, default to MIN_UP_KARMA instead of 0\n        if self.karma(kind) > 0:\n            default_karma = g.MIN_UP_KARMA\n        else:\n            default_karma = 0\n\n        if kind == \"link\":\n            # link karma includes both \"link\" and \"self\", so it's a bit trickier\n            link_karma = getattr(self, sr.name + suffix, None)\n            self_karma = getattr(self, \"%s_self_karma\" % sr.name, None)\n\n            # return default value only if they have *neither* link nor self\n            if all(karma is None for karma in (link_karma, self_karma)):\n                return default_karma\n\n            return sum(karma for karma in (link_karma, self_karma) if karma)\n        else:\n            return getattr(self, sr.name + suffix, default_karma)\n\n    def incr_karma(self, kind, sr, amt):\n        # accounts can (manually) have their ability to gain/lose karma\n        # disabled, to prevent special accounts like AutoModerator from\n        # having a massive number of subreddit-karma attributes\n        if self.disable_karma:\n            return\n\n        if sr.name.startswith('_'):\n            g.log.info(\"Ignoring karma increase for subreddit %r\" % (sr.name,))\n            return\n\n        prop = '%s_%s_karma' % (sr.name, kind)\n        if hasattr(self, prop):\n            return self._incr(prop, amt)\n        else:\n            default_val = self.karma(kind, sr)\n            setattr(self, prop, default_val + amt)\n            self._commit()\n\n    @property\n    def link_karma(self):\n        return self.karma('link')\n\n    @property\n    def comment_karma(self):\n        return self.karma('comment')\n\n    def all_karmas(self, include_old=True):\n        \"\"\"Get all of the user's subreddit-specific karma totals.\n\n        Returns an OrderedDict keyed on subreddit name and containing\n        (link_karma, comment_karma) tuples, ordered by the combined total\n        descending.\n        \"\"\"\n        link_suffix = '_link_karma'\n        self_suffix = '_self_karma'\n        comment_suffix = '_comment_karma'\n\n        comment_karmas = Counter()\n        link_karmas = Counter()\n        combined_karmas = Counter()\n\n        for key, value in self._t.iteritems():\n            if key.endswith(link_suffix):\n                sr_name = key[:-len(link_suffix)]\n                link_karmas[sr_name] += value\n            elif key.endswith(self_suffix):\n                # self karma gets added to link karma too\n                sr_name = key[:-len(self_suffix)]\n                link_karmas[sr_name] += value\n            elif key.endswith(comment_suffix):\n                sr_name = key[:-len(comment_suffix)]\n                comment_karmas[sr_name] = value\n            else:\n                continue\n\n            combined_karmas[sr_name] += value\n\n        all_karmas = OrderedDict()\n        for sr_name, total in combined_karmas.most_common():\n            all_karmas[sr_name] = (link_karmas[sr_name],\n                                   comment_karmas[sr_name])\n\n        if include_old:\n            old_link_karma = self._t.get('link_karma', 0)\n            old_comment_karma = self._t.get('comment_karma', 0)\n            if old_link_karma or old_comment_karma:\n                all_karmas['ancient history'] = (old_link_karma,\n                                                 old_comment_karma)\n\n        return all_karmas\n\n    def update_last_visit(self, current_time):\n        from admintools import apply_updates\n\n        timer = g.stats.get_timer(\"account.update_last_visit\")\n        timer.start()\n\n        apply_updates(self, timer)\n\n        prev_visit = LastModified.get(self._fullname, \"Visit\")\n        timer.intermediate(\"get_last_modified\")\n\n        if prev_visit and current_time - prev_visit < timedelta(days=1):\n            timer.intermediate(\"set_last_modified.noop\")\n            timer.stop()\n            return\n\n        LastModified.touch(self._fullname, \"Visit\")\n        timer.intermediate(\"set_last_modified.done\")\n        timer.stop()\n\n    def make_cookie(self, timestr=None):\n        timestr = timestr or time.strftime(COOKIE_TIMESTAMP_FORMAT)\n        id_time = str(self._id) + ',' + timestr\n        to_hash = ','.join((id_time, self.password, g.secrets[\"SECRET\"]))\n        return id_time + ',' + hashlib.sha1(to_hash).hexdigest()\n\n    def make_admin_cookie(self, first_login=None, last_request=None):\n        first_login = first_login or datetime.utcnow().strftime(COOKIE_TIMESTAMP_FORMAT)\n        last_request = last_request or datetime.utcnow().strftime(COOKIE_TIMESTAMP_FORMAT)\n        hashable = ','.join((first_login, last_request, request.ip, request.user_agent, self.password))\n        mac = hmac.new(g.secrets[\"SECRET\"], hashable, hashlib.sha1).hexdigest()\n        return ','.join((first_login, last_request, mac))\n\n    def make_otp_cookie(self, timestamp=None):\n        timestamp = timestamp or datetime.utcnow().strftime(COOKIE_TIMESTAMP_FORMAT)\n        secrets = [request.user_agent, self.otp_secret, self.password]\n        signature = hmac.new(g.secrets[\"SECRET\"], ','.join([timestamp] + secrets), hashlib.sha1).hexdigest()\n\n        return \",\".join((timestamp, signature))\n\n    def needs_captcha(self):\n        if g.disable_captcha:\n            return False\n\n        hook = hooks.get_hook(\"account.is_captcha_exempt\")\n        captcha_exempt = hook.call_until_return(account=self)\n        if captcha_exempt:\n            return False\n\n        if self.link_karma >= g.live_config[\"captcha_exempt_link_karma\"]:\n            return False\n\n        if self.comment_karma >= g.live_config[\"captcha_exempt_comment_karma\"]:\n            return False\n\n        return True\n\n    @property\n    def can_create_subreddit(self):\n        hook = hooks.get_hook(\"account.can_create_subreddit\")\n        can_create = hook.call_until_return(account=self)\n        if can_create is not None:\n            return can_create\n\n        min_age = timedelta(days=g.live_config[\"create_sr_account_age_days\"])\n        if self._age < min_age:\n            return False\n\n        if (self.link_karma < g.live_config[\"create_sr_link_karma\"] and\n                self.comment_karma < g.live_config[\"create_sr_comment_karma\"]):\n            return False\n\n        return True\n\n    @classmethod\n    @memoize('account._by_name')\n    def _by_name_cache(cls, name, allow_deleted=False):\n        #relower name here, just in case\n        deleted = (True, False) if allow_deleted else False\n        q = cls._query(\n            lower(cls.c.name) == name.lower(),\n            cls.c._spam == (True, False),\n            cls.c._deleted == deleted,\n            data=True,\n        )\n\n        q._limit = 1\n        l = list(q)\n        if l:\n            return l[0]._id\n\n    @classmethod\n    def _by_name(cls, name, allow_deleted = False, _update = False):\n        #lower name here so there is only one cache\n        uid = cls._by_name_cache(name.lower(), allow_deleted, _update = _update)\n        if uid:\n            return cls._byID(uid, data=True)\n        else:\n            raise NotFound, 'Account %s' % name\n\n    @classmethod\n    def _names_to_ids(cls, names, ignore_missing=False, allow_deleted=False,\n                      _update=False):\n        for name in names:\n            uid = cls._by_name_cache(name.lower(), allow_deleted, _update=_update)\n            if not uid:\n                if ignore_missing:\n                    continue\n                raise NotFound('Account %s' % name)\n            yield uid\n\n    # Admins only, since it's not memoized\n    @classmethod\n    def _by_name_multiple(cls, name):\n        q = cls._query(lower(Account.c.name) == name.lower(),\n                       Account.c._spam == (True, False),\n                       Account.c._deleted == (True, False))\n        return list(q)\n\n    @property\n    def friends(self):\n        return self.friend_ids()\n\n    @property\n    def enemies(self):\n        return self.enemy_ids()\n\n    @property\n    def is_moderator_somewhere(self):\n        # modmsgtime can be:\n        #   - a date: the user is a mod somewhere and has unread modmail\n        #   - False: the user is a mod somewhere and has no unread modmail\n        #   - None: (the default) the user is not a mod anywhere\n        return self.modmsgtime is not None\n\n    def is_mutable(self, subreddit):\n        # Don't allow muting of other mods in the subreddit\n        if subreddit.is_moderator(self):\n            return False\n\n        # Don't allow muting of u/reddit or u/AutoModerator\n        return not (self == self.system_user() or\n            self == self.automoderator_user())\n\n    # Used on the goldmember version of /prefs/friends\n    @memoize('account.friend_rels')\n    def friend_rels_cache(self):\n        q = Friend._query(\n            Friend.c._thing1_id == self._id,\n            Friend.c._name == 'friend',\n            thing_data=True,\n        )\n        return list(f._id for f in q)\n\n    def friend_rels(self, _update = False):\n        rel_ids = self.friend_rels_cache(_update=_update)\n        try:\n            rels = Friend._byID_rel(rel_ids, return_dict=False,\n                                    eager_load = True, data = True,\n                                    thing_data = True)\n            rels = list(rels)\n        except NotFound:\n            if _update:\n                raise\n            else:\n                return self.friend_rels(_update=True)\n\n        if not _update:\n            sorted_1 = sorted([r._thing2_id for r in rels])\n            sorted_2 = sorted(list(self.friends))\n            if sorted_1 != sorted_2:\n                self.friend_ids(_update=True)\n                return self.friend_rels(_update=True)\n        return dict((r._thing2_id, r) for r in rels)\n\n    def add_friend_note(self, friend, note):\n        rels = self.friend_rels()\n        rel = rels[friend._id]\n        rel.note = note\n        rel._commit()\n\n    def _get_friend_ids_by(self, data_value_name, limit):\n        friend_ids = self.friend_ids()\n        if len(friend_ids) <= limit:\n            return friend_ids\n\n        with g.stats.get_timer(\"friends_query.%s\" % data_value_name):\n            result = self.sort_ids_by_data_value(\n                friend_ids, data_value_name, limit=limit, desc=True)\n\n        return result.fetchall()\n\n    @memoize(\"get_recently_submitted_friend_ids\", time=10*60)\n    def get_recently_submitted_friend_ids(self, limit=100):\n        return self._get_friend_ids_by(\"last_submit_time\", limit)\n\n    @memoize(\"get_recently_commented_friend_ids\", time=10*60)\n    def get_recently_commented_friend_ids(self, limit=100):\n        return self._get_friend_ids_by(\"last_comment_time\", limit)\n\n    def delete(self, delete_message=None):\n        self.delete_message = delete_message\n        self.delete_time = datetime.now(g.tz)\n        self._deleted = True\n        self._commit()\n\n        #update caches\n        Account._by_name(self.name, allow_deleted = True, _update = True)\n        #we need to catch an exception here since it will have been\n        #recently deleted\n        try:\n            Account._by_name(self.name, _update = True)\n        except NotFound:\n            pass\n\n        # Mark this account for immediate cleanup tasks\n        amqp.add_item('account_deleted', self._fullname)\n\n        # schedule further cleanup after a possible recovery period\n        TryLater.schedule(\"account_deletion\", self._id36,\n                          delay=timedelta(days=90))\n\n    # 'State' bitfield properties\n    @property\n    def _banned(self):\n        return self.state & 1\n\n    @_banned.setter\n    def _banned(self, value):\n        if value and not self._banned:\n            self.state |= 1\n            # Invalidate all cookies by changing the password\n            # First back up the password so we can reverse this\n            self.backup_password = self.password\n            # New PW doesn't matter, they can't log in with it anyway.\n            # Even if their PW /was/ 'banned' for some reason, this\n            # will change the salt and thus invalidate the cookies\n            change_password(self, 'banned')\n\n            # deauthorize all access tokens\n            from r2.models.token import OAuth2AccessToken\n            from r2.models.token import OAuth2RefreshToken\n\n            OAuth2AccessToken.revoke_all_by_user(self)\n            OAuth2RefreshToken.revoke_all_by_user(self)\n        elif not value and self._banned:\n            self.state &= ~1\n\n            # Undo the password thing so they can log in\n            self.password = self.backup_password\n\n            # They're on their own for OAuth tokens, though.\n\n        self._commit()\n\n    @property\n    def subreddits(self):\n        from subreddit import Subreddit\n        return Subreddit.user_subreddits(self)\n\n    def special_distinguish(self):\n        if self._t.get(\"special_distinguish_name\"):\n            return dict((k, self._t.get(\"special_distinguish_\"+k, None))\n                        for k in (\"name\", \"kind\", \"symbol\", \"cssclass\", \"label\", \"link\"))\n        else:\n            return None\n\n    def set_email(self, email):\n        old_email = self.email\n        self.email = email\n        self._commit()\n        AccountsByCanonicalEmail.update_email(self, old_email, email)\n\n    def canonical_email(self):\n        return canonicalize_email(self.email)\n\n    @classmethod\n    def system_user(cls):\n        try:\n            return cls._by_name(g.system_user)\n        except (NotFound, AttributeError):\n            return None\n\n    @classmethod\n    def automoderator_user(cls):\n        try:\n            return cls._by_name(g.automoderator_account)\n        except (NotFound, AttributeError):\n            return None\n\n    def use_subreddit_style(self, sr):\n        \"\"\"Return whether to show subreddit stylesheet depending on\n        individual selection if available, else use pref_show_stylesheets\"\"\"\n        # if FakeSubreddit, there is no stylesheet\n        if not hasattr(sr, '_id'):\n            return False\n        if not feature.is_enabled('stylesheets_everywhere'):\n            return self.pref_show_stylesheets\n        # if stylesheet isn't individually enabled/disabled, use global pref\n        return bool(getattr(self, \"sr_style_%s_enabled\" % sr._id,\n            self.pref_show_stylesheets))\n\n    def set_subreddit_style(self, sr, use_style):\n        if hasattr(sr, '_id'):\n            setattr(self, \"sr_style_%s_enabled\" % sr._id, use_style)\n            self._commit()\n\n    def flair_enabled_in_sr(self, sr_id):\n        return getattr(self, 'flair_%s_enabled' % sr_id, True)\n\n    def flair_text(self, sr_id, obey_disabled=False):\n        if obey_disabled and not self.flair_enabled_in_sr(sr_id):\n            return None\n        return getattr(self, 'flair_%s_text' % sr_id, None)\n\n    def flair_css_class(self, sr_id, obey_disabled=False):\n        if obey_disabled and not self.flair_enabled_in_sr(sr_id):\n            return None\n        return getattr(self, 'flair_%s_css_class' % sr_id, None)\n\n    def can_flair_in_sr(self, user, sr):\n        \"\"\"Return whether a user can set this one's flair in a subreddit.\"\"\"\n        can_assign_own = self._id == user._id and sr.flair_self_assign_enabled\n\n        return can_assign_own or sr.is_moderator_with_perms(user, \"flair\")\n\n    def set_flair(self, subreddit, text=None, css_class=None, set_by=None,\n            log_details=\"edit\"):\n        log_details = \"flair_%s\" % log_details\n        if not text and not css_class:\n            # set to None instead of potentially empty strings\n            text = css_class = None\n            subreddit.remove_flair(self)\n            log_details = \"flair_delete\"\n        elif not subreddit.is_flair(self):\n            subreddit.add_flair(self)\n\n        setattr(self, 'flair_%s_text' % subreddit._id, text)\n        setattr(self, 'flair_%s_css_class' % subreddit._id, css_class)\n        self._commit()\n\n        if set_by and set_by != self:\n            ModAction.create(subreddit, set_by, action='editflair',\n                target=self, details=log_details)\n\n    def get_trophy_id(self, uid):\n        '''Return the ID of the Trophy associated with the given \"uid\"\n\n        `uid` - The unique identifier for the Trophy to look up\n\n        '''\n        return getattr(self, 'received_trophy_%s' % uid, None)\n\n    def set_trophy_id(self, uid, trophy_id):\n        '''Recored that a user has received a Trophy with \"uid\"\n\n        `uid` - The trophy \"type\" that the user should only have one of\n        `trophy_id` - The ID of the corresponding Trophy object\n\n        '''\n        return setattr(self, 'received_trophy_%s' % uid, trophy_id)\n\n    @property\n    def employee(self):\n        \"\"\"Return if the user is an employee.\n\n        Being an employee grants them various special privileges.\n\n        \"\"\"\n        return (hasattr(self, 'name') and\n                (self.name in g.admins or\n                 self.name in g.sponsors or\n                 self.name in g.employees))\n\n    @property\n    def has_gold_subscription(self):\n        return bool(getattr(self, 'gold_subscr_id', None))\n\n    @property\n    def has_paypal_subscription(self):\n        return (self.has_gold_subscription and\n                not self.gold_subscr_id.startswith('cus_'))\n\n    @property\n    def has_stripe_subscription(self):\n        return (self.has_gold_subscription and\n                self.gold_subscr_id.startswith('cus_'))\n\n    @property\n    def gold_will_autorenew(self):\n        return (self.has_gold_subscription or\n                (self.pref_creddit_autorenew and self.gold_creddits > 0))\n\n    @property\n    def timeout_expiration(self):\n        \"\"\"Find the expiration date of the user's temp-timeout as a datetime\n        object.\n\n        Returns None if no temp-timeout found.\n        \"\"\"\n        if not self.in_timeout:\n            return None\n\n        return TempTimeout.search(self.name).get(self.name)\n\n    @property\n    def days_remaining_in_timeout(self):\n        if not self.in_timeout:\n            return 0\n\n        expires = self.timeout_expiration\n\n        if not expires:\n            return 0\n\n        # TryLater runs periodically, so if the suspension expires\n        # within that time, then the remaining number of days is 0\n        # which is the same as a permanent suspension. Return 1 day\n        # remaining if it already expired but the TryLater queue hasn't\n        # cleared it yet.\n        days_left = (expires - datetime.now(g.tz)).days + 1\n        return max(days_left, 1)\n\n    def incr_admin_takedown_strikes(self, amt=1):\n        return self._incr('admin_takedown_strikes', amt)\n\n    def get_style_override(self):\n        \"\"\"Return the subreddit selected for reddit theme.\n\n        If the user has a theme selected and enabled and also has\n        the feature flag enabled, return the subreddit name.\n        Otherwise, return None.\n        \"\"\"\n        # Experiment to change the default style to determine if\n        # engagement metrics change\n        if (feature.is_enabled(\"default_design\") and\n                feature.variant(\"default_design\") == \"nautclassic\"):\n            return \"nautclassic\"\n\n        if (feature.is_enabled(\"default_design\") and\n                feature.variant(\"default_design\") == \"serene\"):\n            return \"serene\"\n\n        # Reddit themes is not enabled for this user\n        if not feature.is_enabled('stylesheets_everywhere'):\n            return None\n\n        # Make sure they have the theme enabled\n        if not self.pref_enable_default_themes:\n            return None\n\n        return self.pref_default_theme_sr\n\n    def has_been_atoed(self):\n        \"\"\"Return true if this account has ever been required to reset their password\n        \"\"\"\n        return 'force_password_reset' in self._t\n\n\nclass FakeAccount(Account):\n    _nodb = True\n    pref_no_profanity = True\n\n    def __eq__(self, other):\n        return self is other\n\ndef valid_admin_cookie(cookie):\n    if g.read_only_mode:\n        return (False, None)\n\n    # parse the cookie\n    try:\n        first_login, last_request, hash = cookie.split(',')\n    except ValueError:\n        return (False, None)\n\n    # make sure it's a recent cookie\n    try:\n        first_login_time = datetime.strptime(first_login, COOKIE_TIMESTAMP_FORMAT)\n        last_request_time = datetime.strptime(last_request, COOKIE_TIMESTAMP_FORMAT)\n    except ValueError:\n        return (False, None)\n\n    cookie_age = datetime.utcnow() - first_login_time\n    if cookie_age.total_seconds() > g.ADMIN_COOKIE_TTL:\n        return (False, None)\n\n    idle_time = datetime.utcnow() - last_request_time\n    if idle_time.total_seconds() > g.ADMIN_COOKIE_MAX_IDLE:\n        return (False, None)\n\n    # validate\n    expected_cookie = c.user.make_admin_cookie(first_login, last_request)\n    return (constant_time_compare(cookie, expected_cookie),\n            first_login)\n\n\ndef valid_otp_cookie(cookie):\n    if g.read_only_mode:\n        return False\n\n    # parse the cookie\n    try:\n        remembered_at, signature = cookie.split(\",\")\n    except ValueError:\n        return False\n\n    # make sure it hasn't expired\n    try:\n        remembered_at_time = datetime.strptime(remembered_at, COOKIE_TIMESTAMP_FORMAT)\n    except ValueError:\n        return False\n\n    age = datetime.utcnow() - remembered_at_time\n    if age.total_seconds() > g.OTP_COOKIE_TTL:\n        return False\n\n    # validate\n    expected_cookie = c.user.make_otp_cookie(remembered_at)\n    return constant_time_compare(cookie, expected_cookie)\n\n\ndef valid_feed(name, feedhash, path):\n    if name and feedhash and path:\n        from r2.lib.template_helpers import add_sr\n        path = add_sr(path)\n        try:\n            user = Account._by_name(name)\n            if (user.pref_private_feeds and\n                constant_time_compare(feedhash, make_feedhash(user, path))):\n                return user\n        except NotFound:\n            pass\n\ndef make_feedhash(user, path):\n    return hashlib.sha1(\"\".join([user.name, user.password,\n                                 g.secrets[\"FEEDSECRET\"]])\n                   ).hexdigest()\n\ndef make_feedurl(user, path, ext = \"rss\"):\n    u = UrlParser(path)\n    u.update_query(user = user.name,\n                   feed = make_feedhash(user, path))\n    u.set_extension(ext)\n    return u.unparse()\n\ndef valid_password(a, password, compare_password=None):\n    # bail out early if the account or password's invalid\n    if not hasattr(a, 'name') or not hasattr(a, 'password') or not password:\n        return False\n\n    convert_password = False\n    if compare_password is None:\n        convert_password = True\n        compare_password = a.password\n\n    # standardize on utf-8 encoding\n    password = filters._force_utf8(password)\n\n    if compare_password.startswith('$2a$'):\n        # it's bcrypt.\n\n        try:\n            expected_hash = bcrypt.hashpw(password, compare_password)\n        except ValueError:\n            # password is invalid because it contains null characters\n            return False\n\n        if not constant_time_compare(compare_password, expected_hash):\n            return False\n\n        # if it's using the current work factor, we're done, but if it's not\n        # we'll have to rehash.\n        # the format is $2a$workfactor$salt+hash\n        work_factor = int(compare_password.split(\"$\")[2])\n        if work_factor == g.bcrypt_work_factor:\n            return a\n    else:\n        # alright, so it's not bcrypt. how old is it?\n        # if the length of the stored hash is 43 bytes, the sha-1 hash has a salt\n        # otherwise it's sha-1 with no salt.\n        salt = ''\n        if len(compare_password) == 43:\n            salt = compare_password[:3]\n        expected_hash = passhash(a.name, password, salt)\n\n        if not constant_time_compare(compare_password, expected_hash):\n            return False\n\n    # since we got this far, it's a valid password but in an old format\n    # let's upgrade it\n    if convert_password:\n        a.password = bcrypt_password(password)\n        a._commit()\n    return a\n\ndef bcrypt_password(password):\n    salt = bcrypt.gensalt(log_rounds=g.bcrypt_work_factor)\n    return bcrypt.hashpw(password, salt)\n\ndef passhash(username, password, salt = ''):\n    if salt is True:\n        salt = randstr(3)\n    tohash = '%s%s %s' % (salt, username, password)\n    return salt + hashlib.sha1(tohash).hexdigest()\n\ndef change_password(user, newpassword):\n    user.password = bcrypt_password(newpassword)\n    user._commit()\n    LastModified.touch(user._fullname, 'Password')\n    return True\n\n\ndef register(name, password, registration_ip):\n    # get a lock for registering an Account with this name to prevent\n    # simultaneous operations from creating multiple Accounts with the same name\n    with g.make_lock(\"account_register\", \"register_%s\" % name.lower()):\n        try:\n            account = Account._by_name(name)\n            raise AccountExists\n        except NotFound:\n            account = Account(\n                name=name,\n                password=bcrypt_password(password),\n                # new accounts keep the profanity filter settings until opting out\n                pref_no_profanity=True,\n                registration_ip=registration_ip,\n            )\n            account._commit()\n\n            # update Account._by_name to pick up this new name->Account\n            Account._by_name(name, _update=True)\n            Account._by_name(name, allow_deleted=True, _update=True)\n\n            return account\n\n\nclass Friend(Relation(Account, Account)):\n    _cache = g.thingcache\n\n    @classmethod\n    def _cache_prefix(cls):\n        return \"friend:\"\n\n\nAccount.__bases__ += (UserRel('friend', Friend, disable_reverse_ids_fn=True),\n                      UserRel('enemy', Friend, disable_reverse_ids_fn=False))\n\nclass DeletedUser(FakeAccount):\n    @property\n    def name(self):\n        return '[deleted]'\n\n    @property\n    def _deleted(self):\n        return True\n\n    def _fullname(self):\n        raise NotImplementedError\n\n    def _id(self):\n        raise NotImplementedError\n\n    def __setattr__(self, attr, val):\n        if attr == '_deleted':\n            pass\n        else:\n            object.__setattr__(self, attr, val)\n\n\nclass BlockedSubredditsByAccount(tdb_cassandra.DenormalizedRelation):\n    _use_db = True\n    _last_modified_name = 'block_subreddit'\n    _read_consistency_level = tdb_cassandra.CL.QUORUM\n    _write_consistency_level = tdb_cassandra.CL.QUORUM\n    _connection_pool = 'main'\n    _views = []\n\n    @classmethod\n    def value_for(cls, thing1, thing2):\n        return ''\n\n    @classmethod\n    def block(cls, user, sr):\n        cls.create(user, sr)\n\n    @classmethod\n    def unblock(cls, user, sr):\n        cls.destroy(user, sr)\n\n    @classmethod\n    def is_blocked(cls, user, sr):\n        try:\n            r = cls.fast_query(user, [sr])\n        except tdb_cassandra.NotFound:\n            return False\n        return (user, sr) in r\n\n\n@trylater_hooks.on(\"trylater.account_deletion\")\ndef deleted_account_cleanup(data):\n    from r2.models import Subreddit\n    from r2.models.admin_notes import AdminNotesBySystem\n    from r2.models.flair import Flair\n    from r2.models.token import OAuth2Client\n\n    for account_id36 in data.itervalues():\n        account = Account._byID36(account_id36, data=True)\n\n        if not account._deleted:\n            continue\n\n        # wipe the account's password and email address\n        account.password = \"\"\n        account.email = \"\"\n        account.email_verified = False\n\n        notes = \"\"\n\n        # \"noisy\" rel removals, we'll record all of these in the account's\n        # usernotes in case we need the information later\n        rel_removal_descriptions = {\n            \"moderator\": \"Unmodded\",\n            \"moderator_invite\": \"Cancelled mod invite\",\n            \"contributor\": \"Removed as contributor\",\n            \"banned\": \"Unbanned\",\n            \"wikibanned\": \"Un-wikibanned\",\n            \"wikicontributor\": \"Removed as wiki contributor\",\n        }\n        if account.has_subscribed:\n            rel_removal_descriptions[\"subscriber\"] = \"Unsubscribed\"\n\n        for rel_type, description in rel_removal_descriptions.iteritems():\n            try:\n                ids_fn = getattr(Subreddit, \"reverse_%s_ids\" % rel_type)\n                sr_ids = ids_fn(account)\n\n                sr_names = []\n                srs = Subreddit._byID(sr_ids, data=True, return_dict=False)\n                for subreddit in srs:\n                    remove_fn = getattr(subreddit, \"remove_\" + rel_type)\n                    remove_fn(account)\n                    sr_names.append(subreddit.name)\n\n                if description and sr_names:\n                    sr_list = \", \".join(sr_names)\n                    notes += \"* %s from %s\\n\" % (description, sr_list)\n            except Exception as e:\n                notes += \"* Error cleaning up %s rels: %s\\n\" % (rel_type, e)\n\n        # silent rel removals, no record left in the usernotes\n        rel_classes = {\n            \"flair\": Flair,\n            \"friend\": Friend,\n            \"enemy\": Friend,\n        }\n\n        for rel_name, rel_cls in rel_classes.iteritems():\n            try:\n                rels = rel_cls._query(\n                    rel_cls.c._thing2_id == account._id,\n                    rel_cls.c._name == rel_name,\n                    eager_load=True,\n                )\n                for rel in rels:\n                    remove_fn = getattr(rel._thing1, \"remove_\" + rel_name)\n                    remove_fn(account)\n            except Exception as e:\n                notes += \"* Error cleaning up %s rels: %s\\n\" % (rel_name, e)\n\n        # add the note with info about the major changes to the account\n        if notes:\n            AdminNotesBySystem.add(\n                system_name=\"user\",\n                subject=account.name,\n                note=\"Account deletion cleanup summary:\\n\\n%s\" % notes,\n                author=\"<automated>\",\n                when=datetime.now(g.tz),\n            )\n\n        account._commit()\n\n\nclass AccountsByCanonicalEmail(tdb_cassandra.View):\n    __metaclass__ = tdb_cassandra.ThingMeta\n\n    _use_db = True\n    _compare_with = UTF8_TYPE\n    _extra_schema_creation_args = dict(\n        key_validation_class=UTF8_TYPE,\n    )\n\n    @classmethod\n    def update_email(cls, account, old, new):\n        old, new = map(canonicalize_email, (old, new))\n\n        if old == new:\n            return\n\n        with cls._cf.batch() as b:\n            if old:\n                b.remove(old, {account._id36: \"\"})\n            if new:\n                b.insert(new, {account._id36: \"\"})\n\n    @classmethod\n    def get_accounts(cls, email_address):\n        canonical = canonicalize_email(email_address)\n        if not canonical:\n            return []\n        account_id36s = cls.get_time_sorted_columns(canonical).keys()\n        return Account._byID36(account_id36s, data=True, return_dict=False)\n\n\nclass SubredditParticipationByAccount(tdb_cassandra.DenormalizedRelation):\n    _use_db = True\n    _write_last_modified = False\n    _views = []\n    _extra_schema_creation_args = {\n        \"key_validation_class\": ASCII_TYPE,\n        \"default_validation_class\": DATE_TYPE,\n    }\n\n    @classmethod\n    def value_for(cls, thing1, thing2):\n        return datetime.now(g.tz)\n\n    @classmethod\n    def mark_participated(cls, account, subreddit):\n        cls.create(account, [subreddit])\n\n\nclass QuarantinedSubredditOptInsByAccount(tdb_cassandra.DenormalizedRelation):\n    _use_db = True\n    _last_modified_name = 'QuarantineSubredditOptin'\n    _read_consistency_level = tdb_cassandra.CL.QUORUM\n    _write_consistency_level = tdb_cassandra.CL.QUORUM\n    _extra_schema_creation_args = {\n        \"key_validation_class\": ASCII_TYPE,\n        \"default_validation_class\": DATE_TYPE,\n    }\n    _connection_pool = 'main'\n    _views = []\n\n    @classmethod\n    def value_for(cls, thing1, thing2):\n        return datetime.now(g.tz)\n\n    @classmethod\n    def opt_in(cls, account, subreddit):\n        if subreddit.quarantine:\n            cls.create(account, subreddit)\n\n    @classmethod\n    def opt_out(cls, account, subreddit):\n        if subreddit.is_subscriber(account):\n            subreddit.remove_subscriber(account)\n        cls.destroy(account, subreddit)\n\n    @classmethod\n    def is_opted_in(cls, user, subreddit):\n        try:\n            r = cls.fast_query(user, [subreddit])\n        except tdb_cassandra.NotFound:\n            return False\n        return (user, subreddit) in r\n"
  },
  {
    "path": "r2/r2/models/admin_notes.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport json\nfrom datetime import datetime\n\nfrom pycassa.system_manager import UTF8_TYPE, TIME_UUID_TYPE\nfrom pycassa.util import convert_uuid_to_time\nfrom pylons import app_globals as g\n\nfrom r2.lib.db import tdb_cassandra\n\n\nclass AdminNotesBySystem(tdb_cassandra.View):\n    _use_db = True\n    _read_consistency_level = tdb_cassandra.CL.QUORUM\n    _write_consistency_level = tdb_cassandra.CL.ONE\n    _compare_with = TIME_UUID_TYPE\n    _extra_schema_creation_args = {\n        \"key_validation_class\": UTF8_TYPE,\n        \"default_validation_class\": UTF8_TYPE,\n    }\n\n    @classmethod\n    def add(cls, system_name, subject, note, author, when=None):\n        if not when:\n            when = datetime.now(g.tz)\n        jsonpacked = json.dumps({\"note\": note, \"author\": author})\n        updatedict = {when: jsonpacked}\n        key = cls._rowkey(system_name, subject)\n        cls._set_values(key, updatedict)\n\n    @classmethod\n    def in_display_order(cls, system_name, subject):\n        key = cls._rowkey(system_name, subject)\n        try:\n            query = cls._cf.get(key, column_reversed=True)\n        except tdb_cassandra.NotFoundException:\n            return []\n        result = []\n        for uuid, json_blob in query.iteritems():\n            when = datetime.fromtimestamp(convert_uuid_to_time(uuid), tz=g.tz)\n            payload = json.loads(json_blob)\n            payload['when'] = when\n            result.append(payload)\n        return result\n\n    @classmethod\n    def _rowkey(cls, system_name, subject):\n        return \"%s:%s\" % (system_name, subject)\n"
  },
  {
    "path": "r2/r2/models/admintools.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom r2.lib import amqp\nfrom r2.lib.db import tdb_cassandra\nfrom r2.lib.db.thing import NotFound\nfrom r2.lib.errors import MessageError\nfrom r2.lib.utils import tup, fetch_things2\nfrom r2.lib.filters import websafe\nfrom r2.lib.hooks import HookRegistrar\nfrom r2.models import (\n    Account,\n    Comment,\n    Link,\n    Message,\n    NotFound,\n    Report,\n    Subreddit,\n)\nfrom r2.models.award import Award\nfrom r2.models.gold import append_random_bottlecap_phrase, creddits_lock\nfrom r2.models.token import AwardClaimToken\nfrom r2.models.wiki import WikiPage\n\nfrom _pylibmc import MemcachedError\nfrom pylons import config\nfrom pylons import tmpl_context as c\nfrom pylons import app_globals as g\nfrom pylons.i18n import _\n\nfrom datetime import datetime, timedelta\nfrom copy import copy\n\nadmintools_hooks = HookRegistrar()\n\nclass AdminTools(object):\n\n    def spam(self, things, auto=True, moderator_banned=False,\n             banner=None, date=None, train_spam=True, **kw):\n        from r2.lib.db import queries\n\n        all_things = tup(things)\n        new_things = [x for x in all_things if not x._spam]\n\n        Report.accept(all_things, True)\n\n        for t in all_things:\n            if getattr(t, \"promoted\", None) is not None:\n                g.log.debug(\"Refusing to mark promotion %r as spam\" % t)\n                continue\n\n            if not t._spam and train_spam:\n                note = 'spam'\n            elif not t._spam and not train_spam:\n                note = 'remove not spam'\n            elif t._spam and not train_spam:\n                note = 'confirm spam'\n            elif t._spam and train_spam:\n                note = 'reinforce spam'\n\n            t._spam = True\n\n            if moderator_banned:\n                t.verdict = 'mod-removed'\n            elif not auto:\n                t.verdict = 'admin-removed'\n\n            ban_info = copy(getattr(t, 'ban_info', {}))\n            if isinstance(banner, dict):\n                ban_info['banner'] = banner[t._fullname]\n            else:\n                ban_info['banner'] = banner\n            ban_info.update(auto=auto,\n                            moderator_banned=moderator_banned,\n                            banned_at=date or datetime.now(g.tz),\n                            **kw)\n            ban_info['note'] = note\n\n            t.ban_info = ban_info\n            t._commit()\n\n            if auto:\n                amqp.add_item(\"auto_removed\", t._fullname)\n\n        if not auto:\n            self.author_spammer(new_things, True)\n            self.set_last_sr_ban(new_things)\n\n        queries.ban(all_things, filtered=auto)\n\n        for t in all_things:\n            if auto:\n                amqp.add_item(\"auto_removed\", t._fullname)\n\n            if isinstance(t, Comment):\n                amqp.add_item(\"removed_comment\", t._fullname)\n            elif isinstance(t, Link):\n                amqp.add_item(\"removed_link\", t._fullname)\n\n    def unspam(self, things, moderator_unbanned=True, unbanner=None,\n               train_spam=True, insert=True):\n        from r2.lib.db import queries\n\n        things = tup(things)\n\n        # We want to make unban-all moderately efficient, so when\n        # mass-unbanning, we're going to skip the code below on links that\n        # are already not banned.  However, when someone manually clicks\n        # \"approve\" on an unbanned link, and there's just one, we want do\n        # want to run the code below. That way, the little green checkmark\n        # will have the right mouseover details, the reports will be\n        # cleared, etc.\n\n        if len(things) > 1:\n            things = [x for x in things if x._spam]\n\n        Report.accept(things, False)\n        for t in things:\n            ban_info = copy(getattr(t, 'ban_info', {}))\n            ban_info['unbanned_at'] = datetime.now(g.tz)\n            if unbanner:\n                ban_info['unbanner'] = unbanner\n            if ban_info.get('reset_used', None) == None:\n                ban_info['reset_used'] = False\n            else:\n                ban_info['reset_used'] = True\n            t.ban_info = ban_info\n            t._spam = False\n            if moderator_unbanned:\n                t.verdict = 'mod-approved'\n            else:\n                t.verdict = 'admin-approved'\n            t._commit()\n\n            if isinstance(t, Comment):\n                amqp.add_item(\"approved_comment\", t._fullname)\n            elif isinstance(t, Link):\n                amqp.add_item(\"approved_link\", t._fullname)\n\n        self.author_spammer(things, False)\n        self.set_last_sr_ban(things)\n        queries.unban(things, insert)\n    \n    def report(self, thing):\n        pass\n\n    def author_spammer(self, things, spam):\n        \"\"\"incr/decr the 'spammer' field for the author of every\n           passed thing\"\"\"\n        by_aid = {}\n        for thing in things:\n            if (hasattr(thing, 'author_id')\n                and not getattr(thing, 'ban_info', {}).get('auto',True)):\n                # only decrement 'spammer' for items that were not\n                # autobanned\n                by_aid.setdefault(thing.author_id, []).append(thing)\n\n        if by_aid:\n            authors = Account._byID(by_aid.keys(), data=True, return_dict=True)\n\n            for aid, author_things in by_aid.iteritems():\n                author = authors[aid]\n                author._incr('spammer', len(author_things) if spam else -len(author_things))\n\n    def set_last_sr_ban(self, things):\n        by_srid = {}\n        for thing in things:\n            if getattr(thing, 'sr_id', None) is not None:\n                by_srid.setdefault(thing.sr_id, []).append(thing)\n\n        if by_srid:\n            srs = Subreddit._byID(by_srid.keys(), data=True, return_dict=True)\n            for sr_id, sr_things in by_srid.iteritems():\n                sr = srs[sr_id]\n\n                sr.last_mod_action = datetime.now(g.tz)\n                sr._commit()\n                sr._incr('mod_actions', len(sr_things))\n\n    def adjust_gold_expiration(self, account, days=0, months=0, years=0):\n        now = datetime.now(g.display_tz)\n        if months % 12 == 0:\n            years += months / 12\n        else:\n            days += months * 31\n        days += years * 366\n\n        existing_expiration = getattr(account, \"gold_expiration\", None)\n        if existing_expiration is None or existing_expiration < now:\n            existing_expiration = now\n        account.gold_expiration = existing_expiration + timedelta(days)\n        \n        if account.gold_expiration > now and not account.gold:\n            self.engolden(account)\n        elif account.gold_expiration <= now and account.gold:\n            self.degolden(account)\n\n        account._commit()     \n\n    def engolden(self, account):\n        now = datetime.now(g.display_tz)\n        account.gold = True\n        description = \"Since \" + now.strftime(\"%B %Y\")\n        \n        trophy = Award.give_if_needed(\"reddit_gold\", account,\n                                     description=description,\n                                     url=\"/gold/about\")\n        if trophy and trophy.description.endswith(\"Member Emeritus\"):\n            trophy.description = description\n            trophy._commit()\n\n        account._commit()\n        account.friend_rels_cache(_update=True)\n\n    def degolden(self, account):\n        Award.take_away(\"reddit_gold\", account)\n        account.gold = False\n        account._commit()\n\n    def admin_list(self):\n        return list(g.admins)\n\n    def create_award_claim_code(self, unique_award_id, award_codename,\n                                description, url):\n        '''Create a one-time-use claim URL for a user to claim a trophy.\n\n        `unique_award_id` - A string that uniquely identifies the kind of\n                            Trophy the user would be claiming.\n                            See: token.py:AwardClaimToken.uid\n        `award_codename` - The codename of the Award the user will claim\n        `description` - The description the Trophy will receive\n        `url` - The URL the Trophy will receive\n\n        '''\n        award = Award._by_codename(award_codename)\n        token = AwardClaimToken._new(unique_award_id, award, description, url)\n        return token.confirm_url()\n\nadmintools = AdminTools()\n\ndef cancel_subscription(subscr_id):\n    q = Account._query(Account.c.gold_subscr_id == subscr_id, data=True)\n    l = list(q)\n    if len(l) != 1:\n        g.log.warning(\"Found %d matches for canceled subscription %s\"\n                      % (len(l), subscr_id))\n    for account in l:\n        account.gold_subscr_id = None\n        account._commit()\n        g.log.info(\"%s canceled their recurring subscription %s\" %\n                   (account.name, subscr_id))\n\ndef all_gold_users():\n    q = Account._query(Account.c.gold == True, Account.c._spam == (True, False),\n                       data=True, sort=\"_id\")\n    return fetch_things2(q)\n\ndef accountid_from_subscription(subscr_id):\n    if subscr_id is None:\n        return None\n\n    q = Account._query(Account.c.gold_subscr_id == subscr_id,\n                       Account.c._spam == (True, False),\n                       Account.c._deleted == (True, False), data=False)\n    l = list(q)\n    if l:\n        return l[0]._id\n    else:\n        return None\n\ndef update_gold_users():\n    now = datetime.now(g.display_tz)\n    warning_days = 3\n    renew_msg = _(\"[Click here for details on how to set up an \"\n                  \"automatically-renewing subscription or to renew.]\"\n                  \"(/gold) If you have any thoughts, complaints, \"\n                  \"rants, suggestions about reddit gold, please write \"\n                  \"to us at %(gold_email)s. Your feedback would be \"\n                  \"much appreciated.\\n\\nThank you for your past \"\n                  \"patronage.\") % {'gold_email': g.goldsupport_email}\n\n    for account in all_gold_users():\n        days_left = (account.gold_expiration - now).days\n        if days_left < 0:\n            if account.pref_creddit_autorenew:\n                with creddits_lock(account):\n                    if account.gold_creddits > 0:\n                        admintools.adjust_gold_expiration(account, days=31)\n                        account.gold_creddits -= 1\n                        account._commit()\n                        continue\n\n            admintools.degolden(account)\n\n            subject = _(\"Your reddit gold subscription has expired.\")\n            message = _(\"Your subscription to reddit gold has expired.\")\n            message += \"\\n\\n\" + renew_msg\n            message = append_random_bottlecap_phrase(message)\n\n            send_system_message(account, subject, message,\n                                distinguished='gold-auto')\n        elif days_left <= warning_days and not account.gold_will_autorenew:\n            hc_key = \"gold_expiration_notice-\" + account.name\n            already_warned = g.hardcache.get(hc_key)\n            if not already_warned:\n                g.hardcache.set(hc_key, True, 86400 * (warning_days + 1))\n                \n                subject = _(\"Your reddit gold subscription is about to \"\n                            \"expire!\")\n                message = _(\"Your subscription to reddit gold will be \"\n                            \"expiring soon.\")\n                message += \"\\n\\n\" + renew_msg\n                message = append_random_bottlecap_phrase(message)\n\n                send_system_message(account, subject, message,\n                                    distinguished='gold-auto')\n\n\ndef is_banned_domain(dom):\n    return None\n\ndef is_shamed_domain(dom):\n    return False, None, None\n\ndef bans_for_domain_parts(dom):\n    return []\n\n\ndef apply_updates(user, timer):\n    pass\n\n\ndef ip_span(ip):\n    ip = websafe(ip)\n    return '<!-- %s -->' % ip\n\n\ndef wiki_template(template_slug, sr=None):\n    \"\"\"Pull content from a subreddit's wiki page for internal use.\"\"\"\n    if not sr:\n        try:\n            sr = Subreddit._by_name(g.default_sr)\n        except NotFound:\n            return None\n\n    try:\n        wiki = WikiPage.get(sr, \"templates/%s\" % template_slug)\n    except tdb_cassandra.NotFound:\n        return None\n\n    return wiki._get(\"content\")\n\n\n@admintools_hooks.on(\"account.registered\")\ndef send_welcome_message(user):\n    welcome_title = wiki_template(\"welcome_title\")\n    welcome_message = wiki_template(\"welcome_message\")\n\n    if not welcome_title or not welcome_message:\n        g.log.warning(\"Unable to send welcome message: invalid wiki templates.\")\n        return\n\n    welcome_title = welcome_title.format(username=user.name)\n    welcome_message = welcome_message.format(username=user.name)\n\n    return send_system_message(user, welcome_title, welcome_message)\n\n\ndef send_system_message(user, subject, body, system_user=None,\n                        distinguished='admin', repliable=False,\n                        add_to_sent=True, author=None, signed=False):\n    from r2.lib.db import queries\n\n    if system_user is None:\n        system_user = Account.system_user()\n    if not system_user:\n        g.log.warning(\"Can't send system message \"\n                      \"- invalid system_user or g.system_user setting\")\n        return\n    if not author:\n        author = system_user\n\n    item, inbox_rel = Message._new(author, user, subject, body,\n                                   ip='0.0.0.0')\n    item.distinguished = distinguished\n    item.repliable = repliable\n    item.display_author = system_user._id\n    item.signed = signed\n    item._commit()\n\n    try:\n        queries.new_message(item, inbox_rel, add_to_sent=add_to_sent)\n    except MemcachedError:\n        raise MessageError('reddit_inbox')\n\n\nif config['r2.import_private']:\n    from r2admin.models.admintools import *\n"
  },
  {
    "path": "r2/r2/models/automoderator.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom datetime import timedelta\n\nfrom pycassa.cassandra.ttypes import NotFoundException\nfrom pycassa.system_manager import ASCII_TYPE, UTF8_TYPE\n\nfrom r2.lib.db import tdb_cassandra\n\n\nclass PerformedRulesByThing(tdb_cassandra.View):\n    \"\"\"Used to track which rules have previously matched a specific item.\"\"\"\n    _use_db = True\n    _connection_pool = \"main\"\n    _read_consistency_level = tdb_cassandra.CL.QUORUM\n    _write_consistency_level = tdb_cassandra.CL.QUORUM\n    _ttl = timedelta(days=3)\n    _extra_schema_creation_args = {\n        \"key_validation_class\": ASCII_TYPE,\n        \"column_name_class\": ASCII_TYPE,\n        \"default_validation_class\": UTF8_TYPE,\n    }\n\n    @classmethod\n    def _rowkey(cls, thing):\n        return thing._fullname\n\n    @classmethod\n    def mark_performed(cls, thing, rule):\n        rowkey = cls._rowkey(thing)\n        cls._set_values(rowkey, {rule.unique_id: ''})\n\n    @classmethod\n    def get_already_performed(cls, thing):\n        rowkey = cls._rowkey(thing)\n        try:\n            columns = cls._cf.get(rowkey)\n        except NotFoundException:\n            return []\n\n        return columns.keys()\n"
  },
  {
    "path": "r2/r2/models/award.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom pylons import app_globals as g\n\nfrom r2.lib.db.operators import asc, desc, lower\nfrom r2.lib.db.thing import Thing, Relation, NotFound\nfrom r2.lib.memoize import memoize\nfrom r2.models import Account\n\n\nclass Award(Thing):\n    _cache = g.thingcache\n    _defaults = dict(\n        awardtype='regular',\n        api_ok=False,\n    )\n\n    @classmethod\n    def _cache_prefix(cls):\n        return \"award:\"\n\n    @classmethod\n    @memoize('award.all_awards')\n    def _all_awards_cache(cls):\n        return [ a._id for a in Award._query(sort=asc('_date'), limit=100) ]\n\n    @classmethod\n    def _all_awards(cls, _update=False):\n        all = Award._all_awards_cache(_update=_update)\n        # Can't just return Award._byID() results because\n        # the ordering will be lost\n        d = Award._byID(all, data=True)\n        return [ d[id] for id in all ]\n\n    @classmethod\n    def _new(cls, codename, title, awardtype, imgurl, api_ok):\n        a = Award(codename=codename, title=title, awardtype=awardtype,\n                  imgurl=imgurl, api_ok=api_ok)\n        a._commit()\n        Award._all_awards_cache(_update=True)\n\n    @classmethod\n    def _by_codename(cls, codename):\n        q = cls._query(lower(Award.c.codename) == codename.lower())\n        q._limit = 1\n        award = list(q)\n\n        if award:\n            return cls._byID(award[0]._id, True)\n        else:\n            raise NotFound, 'Award %s' % codename\n\n    @classmethod\n    def give_if_needed(cls, codename, user,\n                       description=None, url=None):\n        \"\"\"Give an award to a user, unless they already have it.\n           Returns the trophy. Does nothing and prints nothing\n           (except for g.log.debug) if the award doesn't exist.\"\"\"\n\n        try:\n            award = Award._by_codename(codename)\n        except NotFound:\n            g.log.debug(\"No award named '%s'\" % codename)\n            return None\n\n        trophies = Trophy.by_account(user)\n\n        for trophy in trophies:\n            if trophy._thing2.codename == codename:\n                g.log.debug(\"%s already has %s\" % (user, codename))\n                return trophy\n\n        g.log.debug(\"Gave %s to %s\" % (codename, user))\n        return Trophy._new(user, award, description=description,\n                        url=url)\n\n    @classmethod\n    def take_away(cls, codename, user):\n        \"\"\"Takes an award out of a user's trophy case.  Returns silently\n           (except for g.log.debug) if there's no such award.\"\"\"\n\n        found = False\n\n        try:\n            award = Award._by_codename(codename)\n        except NotFound:\n            g.log.debug(\"No award named '%s'\" % codename)\n            return\n\n        trophies = Trophy.by_account(user)\n\n        for trophy in trophies:\n            if trophy._thing2.codename == codename:\n                if found:\n                    g.log.debug(\"%s had multiple %s awards!\" % (user, codename))\n                trophy._delete()\n                Trophy.by_account(user, _update=True)\n                Trophy.by_award(award, _update=True)\n                found = True\n\n        if found:\n            g.log.debug(\"Took %s from %s\" % (codename, user))\n        else:\n            g.log.debug(\"%s didn't have %s\" % (user, codename))\n\n\nclass FakeTrophy(object):\n    def __init__(self, recipient, award, description=None, url=None):\n        self._thing2 = award\n        self._thing1 = recipient\n        self.description = description\n        self.url = url\n        self.trophy_url = getattr(self, \"url\",\n                                  getattr(self._thing2, \"url\", None))\n        self._id = self._id36 = None\n\n\nclass Trophy(Relation(Account, Award)):\n    _cache = g.thingcache\n    _enable_fast_query = False\n\n    @classmethod\n    def _cache_prefix(cls):\n        return \"trophy:\"\n\n    @classmethod\n    def _new(cls, recipient, award, description=None, url=None):\n        # The \"name\" column of the Relation can't be a constant or else a\n        # given account would not be allowed to win a given award more than\n        # once.\n        t = Trophy(recipient, award, name=\"trophy\")\n        t._name = str(t._date)\n\n        if description:\n            t.description = description\n\n        if url:\n            t.url = url\n\n        t._commit()\n        t.update_caches()\n        return t\n    \n    def update_caches(self):\n        self.by_account(self._thing1, _update=True)\n        self.by_award(self._thing2, _update=True)\n\n    @classmethod\n    @memoize('trophy.by_account2')\n    def by_account_cache(cls, account_id):\n        q = Trophy._query(Trophy.c._thing1_id == account_id,\n                          sort = desc('_date'))\n        q._limit = 500\n        return [ t._id for t in q ]\n\n    @classmethod\n    def by_account(cls, account, _update=False):\n        rel_ids = cls.by_account_cache(account._id, _update=_update)\n        trophies = Trophy._byID_rel(rel_ids, data=True, eager_load=True,\n            thing_data=True, return_dict=False, ignore_missing=True)\n        return trophies\n\n    @classmethod\n    @memoize('trophy.by_award2')\n    def by_award_cache(cls, award_id):\n        q = Trophy._query(Trophy.c._thing2_id == award_id,\n                          sort = desc('_date'))\n        q._limit = 50\n        return [ t._id for t in q ]\n\n    @classmethod\n    def by_award(cls, award, _update=False):\n        rel_ids = cls.by_award_cache(award._id, _update=_update)\n        trophies = Trophy._byID_rel(rel_ids, data=True, eager_load=True,\n                                    thing_data=True, return_dict = False)\n        return trophies\n\n    @classmethod\n    def claim(cls, user, uid, award, description, url):\n        with g.make_lock(\"claim_award\", str(\"%s_%s\" % (user.name, uid))):\n            existing_trophy_id = user.get_trophy_id(uid)\n            if existing_trophy_id:\n                trophy = cls._byID(existing_trophy_id)\n                preexisting = True\n            else:\n                preexisting = False\n                trophy = cls._new(user, award, description=description,\n                                  url=url)\n                user.set_trophy_id(uid, trophy._id)\n                user._commit()\n        return trophy, preexisting\n\n    @property\n    def trophy_url(self):\n        return getattr(self, \"url\", getattr(self._thing2, \"url\", None))\n"
  },
  {
    "path": "r2/r2/models/bans.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom datetime import datetime\n\nfrom pycassa.util import convert_uuid_to_time\nfrom pylons import app_globals as g\n\nfrom r2.models.trylater import TryLaterBySubject\n\n\nclass UserTempBan(object):\n    @classmethod\n    def schedule(cls, victim, duration):\n        TryLaterBySubject.schedule(\n            cls.cancel_rowkey(),\n            cls.cancel_colkey(victim.name),\n            victim._fullname,\n            duration,\n        )\n\n    @classmethod\n    def unschedule(cls, victim):\n        TryLaterBySubject.unschedule(\n            cls.cancel_rowkey(),\n            cls.cancel_colkey(victim.name),\n            cls.schedule_rowkey(),\n        )\n\n    @classmethod\n    def search(cls, subjects):\n        results = TryLaterBySubject.search(cls.cancel_rowkey(), subjects)\n\n        def convert_uuid_to_datetime(uu):\n            return datetime.fromtimestamp(convert_uuid_to_time(uu), g.tz)\n        return {\n            name: convert_uuid_to_datetime(uu)\n                for name, uu in results.iteritems()\n        }\n\n    @classmethod\n    def cancel_colkey(cls, name):\n        return name\n\n\nclass TempTimeout(UserTempBan):\n    @classmethod\n    def cancel_rowkey(cls):\n        return \"untimeout\"\n\n    @classmethod\n    def schedule_rowkey(cls):\n        return \"untimeout\"\n"
  },
  {
    "path": "r2/r2/models/bidding.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport datetime\n\nfrom pylons import request\nfrom pylons import app_globals as g\nfrom sqlalchemy import (\n    and_,\n    Boolean,\n    BigInteger,\n    Column,\n    DateTime,\n    Date,\n    distinct,\n    Float,\n    func as safunc,\n    Integer,\n    String,\n)\nfrom sqlalchemy.dialects.postgresql.base import PGInet as Inet\nfrom sqlalchemy.ext.declarative import declarative_base\nfrom sqlalchemy.orm import sessionmaker\nfrom sqlalchemy.orm.exc import NoResultFound\n\nfrom r2.lib.db.thing import Thing, NotFound\nfrom r2.lib.memoize import memoize\nfrom r2.lib.utils import Enum, to_date, tup\nfrom r2.models.account import Account\nfrom r2.models import Link, Frontpage\n\n\nengine = g.dbm.get_engine('authorize')\n# Allocate a session maker for communicating object changes with the back end  \nSession = sessionmaker(autocommit = True, autoflush = True, bind = engine)\n# allocate a SQLalchemy base class for auto-creation of tables based\n# on class fields.  \n# NB: any class that inherits from this class will result in a table\n# being created, and subclassing doesn't work, hence the\n# object-inheriting interface classes.\nBase = declarative_base(bind = engine)\n\nclass Sessionized(object):\n    \"\"\"\n    Interface class for wrapping up the \"session\" in the 0.5 ORM\n    required for all database communication.  This allows subclasses\n    to have a \"query\" and \"commit\" method that doesn't require\n    managing of the session.\n    \"\"\"\n    session = Session()\n\n    def __init__(self, *a, **kw):\n        \"\"\"\n        Common init used by all other classes in this file.  Allows\n        for object-creation based on the __table__ field which is\n        created by Base (further explained in _disambiguate_args).\n        \"\"\"\n        for k, v in self._disambiguate_args(None, *a, **kw):\n            setattr(self, k.name, v)\n    \n    @classmethod\n    def _new(cls, *a, **kw):\n        \"\"\"\n        Just like __init__, except the new object is committed to the\n        db before being returned.\n        \"\"\"\n        obj = cls(*a, **kw)\n        obj._commit()\n        return obj\n\n    def _commit(self):\n        \"\"\"\n        Commits current object to the db.\n        \"\"\"\n        with self.session.begin():\n            self.session.add(self)\n\n    def _delete(self):\n        \"\"\"\n        Deletes current object from the db. \n        \"\"\"\n        with self.session.begin():\n            self.session.delete(self)\n\n    @classmethod\n    def query(cls, **kw):\n        \"\"\"\n        Ubiquitous class-level query function. \n        \"\"\"\n        q = cls.session.query(cls)\n        if kw:\n            q = q.filter_by(**kw)\n        return q\n\n    @classmethod\n    def _disambiguate_args(cls, filter_fn, *a, **kw):\n        \"\"\"\n        Used in _lookup and __init__ to interpret *a as being a list\n        of args to match columns in the same order as __table__.c\n\n        For example, if a class Foo has fields a and b, this function\n        allows the two to work identically:\n        \n        >>> foo = Foo(a = 'arg1', b = 'arg2')\n        >>> foo = Foo('arg1', 'arg2')\n\n        Additionally, this function invokes _make_storable on each of\n        the values in the arg list (including *a as well as\n        kw.values())\n\n        \"\"\"\n        args = []\n        if filter_fn is None:\n            cols = cls.__table__.c\n        else:\n            cols = filter(filter_fn, cls.__table__.c)\n        for k, v in zip(cols, a):\n            if not kw.has_key(k.name):\n                args.append((k, cls._make_storable(v)))\n            else:\n                raise TypeError,\\\n                      \"got multiple arguments for '%s'\" % k.name\n\n        cols = dict((x.name, x) for x in cls.__table__.c)\n        for k, v in kw.iteritems():\n            if cols.has_key(k):\n                args.append((cols[k], cls._make_storable(v)))\n        return args\n\n    @classmethod\n    def _make_storable(self, val):\n        if isinstance(val, Account):\n            return val._id\n        elif isinstance(val, Thing):\n            return val._fullname\n        else:\n            return val\n\n    @classmethod\n    def _lookup(cls, multiple, *a, **kw):\n        \"\"\"\n        Generates an executes a query where it matches *a to the\n        primary keys of the current class's table.\n\n        The primary key nature can be overridden by providing an\n        explicit list of columns to search.\n\n        This function is only a convenience function, and is called\n        only by one() and lookup().\n        \"\"\"\n        args = cls._disambiguate_args(lambda x: x.primary_key, *a, **kw)\n        res = cls.query().filter(and_(*[k == v for k, v in args]))\n        try:\n            res = res.all() if multiple else res.one()\n            # res.one() will raise NoResultFound, while all() will\n            # return an empty list.  This will make the response\n            # uniform\n            if not res:\n                raise NoResultFound\n        except NoResultFound: \n            raise NotFound, \"%s with %s\" % \\\n                (cls.__name__,\n                 \",\".join(\"%s=%s\" % x for x in args))\n        return res\n\n    @classmethod\n    def lookup(cls, *a, **kw):\n        \"\"\"\n        Returns all objects which match the kw list, or primary keys\n        that match the *a.\n        \"\"\"\n        return cls._lookup(True, *a, **kw)\n\n    @classmethod\n    def one(cls, *a, **kw):\n        \"\"\"\n        Same as lookup, but returns only one argument. \n        \"\"\"\n        return cls._lookup(False, *a, **kw)\n\n    @classmethod\n    def add(cls, key, *a):\n        try:\n            cls.one(key, *a)\n        except NotFound:\n            cls(key, *a)._commit()\n    \n    @classmethod\n    def delete(cls, key, *a):\n        try:\n            cls.one(key, *a)._delete()\n        except NotFound:\n            pass\n    \n    @classmethod\n    def get(cls, key):\n        try:\n            return cls.lookup(key)\n        except NotFound:\n            return []\n\nclass CustomerID(Sessionized, Base):\n    __tablename__  = \"authorize_account_id\"\n\n    account_id    = Column(BigInteger, primary_key = True,\n                           autoincrement = False)\n    authorize_id  = Column(BigInteger)\n\n    def __repr__(self):\n        return \"<AuthNetID(%s)>\" % self.authorize_id\n\n    @classmethod\n    def set(cls, user, _id):\n        try:\n            existing = cls.one(user)\n            existing.authorize_id = _id\n            existing._commit()\n        except NotFound:\n            cls(user, _id)._commit()\n    \n    @classmethod\n    def get_id(cls, user):\n        try:\n            return cls.one(user).authorize_id\n        except NotFound:\n            return\n\nclass PayID(Sessionized, Base):\n    __tablename__ = \"authorize_pay_id\"\n\n    account_id    = Column(BigInteger, primary_key = True,\n                           autoincrement = False)\n    pay_id        = Column(BigInteger, primary_key = True,\n                           autoincrement = False)\n\n    def __repr__(self):\n        return \"<%s(%d)>\" % (self.__class__.__name__, self.authorize_id)\n\n    @classmethod\n    def get_ids(cls, key):\n        return [int(x.pay_id) for x in cls.get(key)]\n\nclass Bid(Sessionized, Base):\n    __tablename__ = \"bids\"\n\n    STATUS        = Enum(\"AUTH\", \"CHARGE\", \"REFUND\", \"VOID\")\n\n    # will be unique from authorize\n    transaction   = Column(BigInteger, primary_key = True,\n                           autoincrement = False)\n\n    # identifying characteristics\n    account_id    = Column(BigInteger, index = True, nullable = False)\n    pay_id        = Column(BigInteger, index = True, nullable = False)\n    thing_id      = Column(BigInteger, index = True, nullable = False)\n\n    # breadcrumbs\n    ip            = Column(Inet)\n    date          = Column(DateTime(timezone = True), default = safunc.now(),\n                           nullable = False)\n\n    # bid information:\n    bid           = Column(Float, nullable = False)\n    charge        = Column(Float)\n\n    status        = Column(Integer, nullable = False,\n                           default = STATUS.AUTH)\n\n    # make this a primary key as well so that we can have more than\n    # one freebie per campaign\n    campaign      = Column(Integer, default = 0, primary_key = True)\n\n    @classmethod\n    def _new(cls, trans_id, user, pay_id, thing_id, bid, campaign = 0):\n        bid = Bid(trans_id, user, pay_id, \n                  thing_id, getattr(request, 'ip', '0.0.0.0'), bid = bid,\n                  campaign = campaign)\n        bid._commit()\n        return bid\n\n#    @classmethod\n#    def for_transactions(cls, transids):\n#        transids = filter(lambda x: x != 0, transids)\n#        if transids:\n#            q = cls.query()\n#            q = q.filter(or_(*[cls.transaction == i for i in transids]))\n#            return dict((p.transaction, p) for p in q)\n#        return {}\n\n    def set_status(self, status):\n        if self.status != status:\n            self.status = status\n            self._commit()\n\n    def auth(self):\n        self.set_status(self.STATUS.AUTH)\n\n    def is_auth(self):\n        return (self.status == self.STATUS.AUTH)\n\n    def void(self):\n        self.set_status(self.STATUS.VOID)\n\n    def is_void(self):\n        return (self.status == self.STATUS.VOID)\n\n    def charged(self):\n        self.charge = self.bid\n        self.set_status(self.STATUS.CHARGE)\n        self._commit()\n\n    def is_charged(self):\n        \"\"\"\n        Returns True if transaction has been charged with authorize.net or is\n        a freebie with \"charged\" status.\n        \"\"\"\n        return (self.status == self.STATUS.CHARGE)\n\n    def refund(self, amount):\n        current_charge = self.charge or self.bid    # needed if charged() not\n                                                    # setting charge attr\n        self.charge = current_charge - amount\n        self.set_status(self.STATUS.REFUND)\n        self._commit()\n\n    def is_refund(self):\n        return (self.status == self.STATUS.REFUND)\n\n    @property\n    def charge_amount(self):\n        return self.charge or self.bid\n\n\nclass PromotionWeights(Sessionized, Base):\n    __tablename__ = \"promotion_weight\"\n\n    thing_name = Column(String, primary_key = True,\n                        nullable = False, index = True)\n\n    promo_idx    = Column(BigInteger, index = True, autoincrement = False,\n                          primary_key = True)\n\n    sr_name    = Column(String, primary_key = True,\n                        nullable = True,  index = True)\n    date       = Column(Date(), primary_key = True,\n                        nullable = False, index = True)\n\n    # because we might want to search by account\n    account_id   = Column(BigInteger, index = True, autoincrement = False)\n\n    # bid and weight should always be the same, but they don't have to be\n    bid        = Column(Float, nullable = False)\n    weight     = Column(Float, nullable = False)\n    finished   = Column(Boolean)\n    # NOTE: bid, weight, finished columns are not used\n\n    @classmethod\n    def filter_sr_name(cls, sr_name):\n        # LEGACY: use empty string to indicate Frontpage\n        return '' if sr_name == Frontpage.name else sr_name\n\n    @classmethod\n    def reschedule(cls, link, campaign):\n        cls.delete(link, campaign)\n        cls.add(link, campaign)\n\n    @classmethod\n    def add(cls, link, campaign):\n        start_date = to_date(campaign.start_date)\n        end_date = to_date(campaign.end_date)\n        ndays = (end_date - start_date).days\n        # note that end_date is not included\n        dates = [start_date + datetime.timedelta(days=i) for i in xrange(ndays)]\n\n        sr_names = campaign.target.subreddit_names\n        sr_names = {cls.filter_sr_name(sr_name) for sr_name in sr_names}\n\n        with cls.session.begin():\n            for sr_name in sr_names:\n                for date in dates:\n                    obj = cls(\n                        thing_name=link._fullname,\n                        promo_idx=campaign._id,\n                        sr_name=sr_name,\n                        date=date,\n                        account_id=link.author_id,\n                        bid=0.,\n                        weight=0.,\n                        finished=False,\n                    )\n                    cls.session.add(obj)\n\n    @classmethod\n    def delete(cls, link, campaign):\n        query = cls.query(thing_name=link._fullname, promo_idx=campaign._id)\n        with cls.session.begin():\n            for item in query:\n                cls.session.delete(item)\n\n    @classmethod\n    def _filter_query(cls, query, start, end=None, link=None,\n                      author_id=None, sr_names=None):\n        start = to_date(start)\n\n        if end:\n            end = to_date(end)\n            query = query.filter(and_(cls.date >= start, cls.date < end))\n        else:\n            query = query.filter(cls.date == start)\n\n        if link:\n            query = query.filter(cls.thing_name == link._fullname)\n\n        if author_id:\n            query = query.filter(cls.account_id == author_id)\n\n        if sr_names:\n            sr_names = [cls.filter_sr_name(sr_name) for sr_name in sr_names]\n            query = query.filter(cls.sr_name.in_(sr_names))\n\n        return query\n\n    @classmethod\n    def get_campaign_ids(cls, start, end=None, link=None, author_id=None,\n                         sr_names=None):\n        query = cls.session.query(distinct(cls.promo_idx))\n        query = cls._filter_query(query, start, end, link, author_id, sr_names)\n        return {i[0] for i in query}\n\n    @classmethod\n    def get_link_names(cls, start, end=None, link=None, author_id=None,\n                       sr_names=None):\n        query = cls.session.query(distinct(cls.thing_name))\n        query = cls._filter_query(query, start, end, link, author_id, sr_names)\n        return {i[0] for i in query}\n\n\n# do all the leg work of creating/connecting to tables\nif g.db_create_tables:\n    Base.metadata.create_all()\n\n"
  },
  {
    "path": "r2/r2/models/builder.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom collections import defaultdict, namedtuple\nfrom copy import deepcopy\nimport datetime\nimport heapq\nfrom random import shuffle\nimport time\n\nfrom pylons import request\nfrom pylons import tmpl_context as c\nfrom pylons import app_globals as g\nfrom pylons.i18n import _\n\nfrom r2.config import feature\nfrom r2.config.extensions import API_TYPES, RSS_TYPES\nfrom r2.lib import hooks\nfrom r2.lib.comment_tree import (\n    conversation,\n    get_comment_scores,\n    moderator_messages,\n    sr_conversation,\n    subreddit_messages,\n    tree_sort_fn,\n    user_messages,\n)\nfrom r2.lib.wrapped import Wrapped\nfrom r2.lib.db import operators, tdb_cassandra\nfrom r2.lib.filters import _force_unicode\nfrom r2.lib.jsontemplates import get_trimmed_sr_dicts\nfrom r2.lib.utils import (\n    long_datetime,\n    SimpleSillyStub,\n    Storage,\n    to36,\n    tup,\n)\n\nfrom r2.models import (\n    Account,\n    Comment,\n    CommentSavesByAccount,\n    Link,\n    LinkSavesByAccount,\n    Message,\n    MoreChildren,\n    MoreMessages,\n    MoreRecursion,\n    Subreddit,\n    Thing,\n    wiki,\n)\nfrom r2.models.admintools import ip_span\nfrom r2.models.comment_tree import CommentTree\nfrom r2.models.flair import Flair\nfrom r2.models.listing import Listing\nfrom r2.models.vote import Vote\n\n\nEXTRA_FACTOR = 1.5\nMAX_RECURSION = 10\n\n\nclass InconsistentCommentTreeError(Exception):\n  pass\n\n\nclass Builder(object):\n    def __init__(self, wrap=Wrapped, prewrap_fn=None, keep_fn=None, stale=True,\n                 spam_listing=False):\n        self.wrap = wrap\n        self.prewrap_fn = prewrap_fn\n        self.keep_fn = keep_fn\n        self.stale = stale\n        self.spam_listing = spam_listing\n\n    def keep_item(self, item):\n        if self.keep_fn:\n            return self.keep_fn(item)\n        else:\n            return item.keep_item(item)\n\n    def wrap_items(self, items):\n        from r2.lib.db import queries\n        from r2.lib.template_helpers import (\n            add_friend_distinguish,\n            add_admin_distinguish,\n            add_moderator_distinguish,\n            add_cakeday_distinguish,\n            add_special_distinguish,\n        )\n\n        user = c.user if c.user_is_loggedin else None\n        aids = set(l.author_id for l in items if hasattr(l, 'author_id')\n                   and l.author_id is not None)\n\n        authors = Account._byID(aids, data=True, stale=self.stale)\n        now = datetime.datetime.now(g.tz)\n        cakes = {a._id for a in authors.itervalues()\n                       if a.cake_expiration and a.cake_expiration >= now}\n        friend_rels = user.friend_rels() if user and user.gold else {}\n\n        subreddits = Subreddit.load_subreddits(items, stale=self.stale)\n        can_ban_set = set()\n\n        if user:\n            for sr_id, sr in subreddits.iteritems():\n                if sr.can_ban(user):\n                    can_ban_set.add(sr_id)\n\n        #get likes/dislikes\n        try:\n            likes = queries.get_likes(user, items)\n        except tdb_cassandra.TRANSIENT_EXCEPTIONS as e:\n            g.log.warning(\"Cassandra vote lookup failed: %r\", e)\n            likes = {}\n\n        types = {}\n        wrapped = []\n\n        for item in items:\n            w = self.wrap(item)\n            wrapped.append(w)\n            # add for caching (plus it should be bad form to use _\n            # variables in templates)\n            w.fullname = item._fullname\n            types.setdefault(w.render_class, []).append(w)\n\n            w.author = None\n            w.friend = False\n            w.enemy = False\n\n            w.distinguished = None\n            if hasattr(item, \"distinguished\"):\n                if item.distinguished == 'yes':\n                    w.distinguished = 'moderator'\n                elif item.distinguished in ('admin', 'special',\n                                            'gold', 'gold-auto'):\n                    w.distinguished = item.distinguished\n\n            if getattr(item, \"author_id\", None):\n                w.author = authors.get(item.author_id)\n\n            if hasattr(item, \"sr_id\") and item.sr_id is not None:\n                w.subreddit = subreddits[item.sr_id]\n\n            distinguish_attribs_list = []\n\n            # if display_author exists, then w.author is unknown to the\n            # receiver, so we can't check for friend or cakeday\n            author_is_hidden = hasattr(item, 'display_author')\n\n            if user and w.author:\n                # the enemy flag will trigger keep_item to fail in Printable\n                if w.author._id in user.enemies:\n                    w.enemy = True\n\n                elif not author_is_hidden and w.author._id in user.friends:\n                    w.friend = True\n                    if item.author_id in friend_rels:\n                        note = getattr(friend_rels[w.author._id], \"note\", None)\n                    else:\n                        note = None\n                    add_friend_distinguish(distinguish_attribs_list, note)\n\n            if (w.distinguished == 'admin' and w.author):\n                add_admin_distinguish(distinguish_attribs_list)\n\n            if w.distinguished == 'moderator':\n                add_moderator_distinguish(distinguish_attribs_list, w.subreddit)\n\n            if w.distinguished == 'special':\n                add_special_distinguish(distinguish_attribs_list, w.author)\n\n            if (not author_is_hidden and\n                    w.author and w.author._id in cakes and not c.profilepage):\n                add_cakeday_distinguish(distinguish_attribs_list, w.author)\n\n            w.attribs = distinguish_attribs_list\n\n            user_vote_dir = likes.get((user, item))\n\n            if user_vote_dir == Vote.DIRECTIONS.up:\n                w.likes = True\n            elif user_vote_dir == Vote.DIRECTIONS.down:\n                w.likes = False\n            else:\n                w.likes = None\n\n            w.upvotes = item._ups\n            w.downvotes = item._downs\n\n            total_votes = max(item.num_votes, 1)\n            w.upvote_ratio = float(item._ups) / total_votes\n\n            w.is_controversial = self._is_controversial(w)\n\n            w.score = w.upvotes - w.downvotes\n\n            if user_vote_dir == Vote.DIRECTIONS.up:\n                base_score = w.score - 1\n            elif user_vote_dir == Vote.DIRECTIONS.down:\n                base_score = w.score + 1\n            else:\n                base_score = w.score\n\n            # store the set of available scores based on the vote\n            # for ease of i18n when there is a label\n            w.voting_score = [(base_score + x - 1) for x in range(3)]\n\n            w.deleted = item._deleted\n\n            w.link_notes = []\n\n            if c.user_is_admin:\n                if item._deleted:\n                    w.link_notes.append(\"deleted link\")\n                if getattr(item, \"verdict\", None):\n                    if not item.verdict.endswith(\"-approved\"):\n                        w.link_notes.append(w.verdict)\n\n            if c.user_is_admin and getattr(item, 'ip', None):\n                w.ip_span = ip_span(item.ip)\n            else:\n                w.ip_span = \"\"\n\n            # if the user can ban things on a given subreddit, or an\n            # admin, then allow them to see that the item is spam, and\n            # add the other spam-related display attributes\n            w.show_reports = False\n            w.show_spam    = False\n            w.can_ban      = False\n            w.use_big_modbuttons = self.spam_listing\n\n            if (c.user_is_admin\n                or (user\n                    and hasattr(item,'sr_id')\n                    and item.sr_id in can_ban_set)):\n                if getattr(item, \"promoted\", None) is None:\n                    w.can_ban = True\n\n                ban_info = getattr(item, 'ban_info', {})\n                w.unbanner = ban_info.get('unbanner')\n\n                if item._spam:\n                    w.show_spam = True\n                    w.moderator_banned = ban_info.get('moderator_banned', False)\n                    w.autobanned = ban_info.get('auto', False)\n                    w.banner = ban_info.get('banner')\n                    w.banned_at = ban_info.get(\"banned_at\", None)\n                    if ban_info.get('note', None) and w.banner:\n                        w.banner += ' (%s)' % ban_info['note']\n                    w.use_big_modbuttons = True\n                    if getattr(w, \"author\", None) and w.author._spam:\n                        w.show_spam = \"author\"\n\n                    if c.user == w.author and c.user._spam:\n                        w.show_spam = False\n                        w._spam = False\n                        w.use_big_modbuttons = False\n\n                elif (getattr(item, 'reported', 0) > 0\n                      and (not getattr(item, 'ignore_reports', False) or\n                           c.user_is_admin)):\n                    w.show_reports = True\n                    w.use_big_modbuttons = True\n\n                    # report_count isn't used in any template, but add it to\n                    # the Wrapped so it's pulled into the render cache key in\n                    # instances when reported will be used in the template\n                    w.report_count = item.reported\n\n            w.approval_checkmark = None\n            if w.can_ban:\n                verdict = getattr(w, \"verdict\", None)\n                if verdict in ('admin-approved', 'mod-approved'):\n                    approver = None\n                    approval_time = None\n                    baninfo = getattr(w, \"ban_info\", None)\n                    if baninfo:\n                        approver = baninfo.get(\"unbanner\", None)\n                        approval_time = baninfo.get(\"unbanned_at\", None)\n\n                    approver = approver or _(\"a moderator\")\n\n                    if approval_time:\n                        text = _(\"approved by %(who)s at %(when)s\") % {\n                                    \"who\": approver,\n                                    \"when\": long_datetime(approval_time)}\n                    else:\n                        text = _(\"approved by %s\") % approver\n                    w.approval_checkmark = text\n\n            hooks.get_hook(\"builder.wrap_items\").call(item=item, wrapped=w)\n\n        # recache the user object: it may be None if user is not logged in,\n        # whereas now we are happy to have the UnloggedUser object\n        user = c.user\n        for cls in types.keys():\n            cls.add_props(user, types[cls])\n\n        return wrapped\n\n    def get_items(self):\n        raise NotImplementedError\n\n    def convert_items(self, items):\n        \"\"\"Convert a list of items to the desired output format\"\"\"\n        if self.prewrap_fn:\n            items = [self.prewrap_fn(i) for i in items]\n\n        if self.wrap:\n            items = self.wrap_items(items)\n        else:\n            # make a copy of items so the converted items can be mutated without\n            # changing the original items\n            items = items[:]\n        return items\n\n    def valid_after(self, after):\n        \"\"\"\n        Return whether `after` could ever be shown to the user.\n\n        Necessary because an attacker could use info about the presence\n        and position of `after` within a listing to leak info about `after`s\n        that the attacker could not normally access.\n        \"\"\"\n        w = self.convert_items((after,))[0]\n        return not self.must_skip(w)\n\n    def item_iter(self, a):\n        \"\"\"Iterates over the items returned by get_items\"\"\"\n        raise NotImplementedError\n\n    def must_skip(self, item):\n        \"\"\"whether or not to skip any item regardless of whether the builder\n        was contructed with skip=true\"\"\"\n        user = c.user if c.user_is_loggedin else None\n\n        if hasattr(item, \"promoted\") and item.promoted is not None:\n            return False\n\n        # can_view_slow only exists for Messages, but checking was_comment\n        # is also necessary because items may also be comments that are being\n        # viewed from the inbox page where their render class is overridden.\n        # This check needs to be done before looking at whether they can view\n        # the subreddit, or modmail to/from private subreddits that the user\n        # doesn't have access to will be skipped.\n        if hasattr(item, 'can_view_slow') and not item.was_comment:\n            return not item.can_view_slow()\n\n        if hasattr(item, 'subreddit') and not item.subreddit.can_view(user):\n            return True\n\n    def _is_controversial(self, wrapped):\n        \"\"\"Determine if an item meets all criteria to display as controversial.\"\"\"\n\n        # A sample-size threshold before posts can be considered controversial\n        num_votes = wrapped.upvotes + wrapped.downvotes\n        if num_votes < g.live_config['cflag_min_votes']:\n            return False\n\n        # If an item falls within a boundary of upvote ratios, it's controversial\n        # e.g. 0.4 < x < 0.6\n        lower = g.live_config['cflag_lower_bound']\n        upper = g.live_config['cflag_upper_bound']\n        if lower <= wrapped.upvote_ratio <= upper:\n            return True\n\n        return False\n\n\nclass QueryBuilder(Builder):\n    def __init__(self, query, skip=False, num=None, sr_detail=None, count=0,\n                 after=None, reverse=False, **kw):\n        self.query = query\n        self.skip = skip\n        self.num = num\n        self.sr_detail = sr_detail\n        self.start_count = count or 0\n        self.after = after\n        self.reverse = reverse\n        Builder.__init__(self, **kw)\n\n    def __repr__(self):\n        return \"<%s(%r)>\" % (self.__class__.__name__, self.query)\n\n    def item_iter(self, a):\n        \"\"\"Iterates over the items returned by get_items\"\"\"\n        for i in a[0]:\n            yield i\n\n    def init_query(self):\n        q = self.query\n\n        if self.reverse:\n            q._reverse()\n\n        q._data = True\n        self.orig_rules = deepcopy(q._rules)\n        if self.after:\n            q._after(self.after)\n\n    def fetch_more(self, last_item, num_have):\n        done = False\n        q = self.query\n        if self.num:\n            num_need = self.num - num_have\n            if num_need <= 0:\n                #will cause the loop below to break\n                return True, None\n            else:\n                #q = self.query\n                #check last_item if we have a num because we may need to iterate\n                if last_item:\n                    q._rules = deepcopy(self.orig_rules)\n                    q._after(last_item)\n                    last_item = None\n                q._limit = max(int(num_need * EXTRA_FACTOR), self.num // 2, 1)\n        else:\n            done = True\n        new_items = list(q)\n\n        return done, new_items\n\n    def get_items(self):\n        self.init_query()\n\n        num_have = 0\n        done = False\n        items = []\n        count = self.start_count\n        fetch_after = None\n        loopcount = 0\n        stopped_early = False\n\n        while not done:\n            done, fetched_items = self.fetch_more(fetch_after, num_have)\n\n            #log loop\n            loopcount += 1\n            if loopcount == 20:\n                done = True\n                stopped_early = True\n\n            #no results, we're done\n            if not fetched_items:\n                break\n\n            #if fewer results than we wanted, we're done\n            elif self.num and len(fetched_items) < self.num - num_have:\n                done = True\n\n            # Wrap the fetched items if necessary\n            new_items = self.convert_items(fetched_items)\n\n            #skip and count\n            while new_items and (not self.num or num_have < self.num):\n                i = new_items.pop(0)\n\n                if not (self.must_skip(i) or\n                        self.skip and not self.keep_item(i)):\n                    items.append(i)\n                    num_have += 1\n                    count = count - 1 if self.reverse else count + 1\n                    if self.wrap:\n                        i.num = count\n\n            fetch_after = fetched_items[-1]\n\n        # Is there a next page or not?\n        have_next = True\n        if self.num and num_have < self.num and not stopped_early:\n            have_next = False\n\n        if getattr(self, \"sr_detail\", False) and c.render_style in API_TYPES:\n            items_by_subreddit = defaultdict(list)\n            for item in items:\n                if isinstance(item.lookups[0], Link):\n                    items_by_subreddit[item.subreddit].append(item)\n\n            srs = items_by_subreddit.keys()\n            sr_dicts = get_trimmed_sr_dicts(srs, c.user)\n\n            for sr, sr_items in items_by_subreddit.iteritems():\n                sr_detail = sr_dicts[sr._id]\n                for item in sr_items:\n                    item.sr_detail = sr_detail\n\n        # Make sure first_item and last_item refer to things in items\n        # NOTE: could retrieve incorrect item if there were items with\n        # duplicate _id\n        first_item = None\n        last_item = None\n        if items:\n            if self.start_count > 0:\n                first_item = items[0]\n            last_item = items[-1]\n\n        if self.reverse:\n            items.reverse()\n            last_item, first_item = first_item, have_next and last_item\n            before_count = count\n            after_count = self.start_count - 1\n        else:\n            last_item = have_next and last_item\n            before_count = self.start_count + 1\n            after_count = count\n\n        #listing is expecting (things, prev, next, bcount, acount)\n        return (items,\n                first_item,\n                last_item,\n                before_count,\n                after_count)\n\nclass IDBuilder(QueryBuilder):\n    def thing_lookup(self, names):\n        return Thing._by_fullname(names, data=True, return_dict=False,\n                                  stale=self.stale)\n\n    def init_query(self):\n        names = list(tup(self.query))\n\n        after = self.after._fullname if self.after else None\n\n        self.names = self._get_after(names,\n                                     after,\n                                     self.reverse)\n\n    @staticmethod\n    def _get_after(l, after, reverse):\n        names = list(l)\n\n        if reverse:\n            names.reverse()\n\n        if after:\n            try:\n                i = names.index(after)\n            except ValueError:\n                names = ()\n            else:\n                names = names[i + 1:]\n\n        return names\n\n    def fetch_more(self, last_item, num_have):\n        done = False\n        names = self.names\n        if self.num:\n            num_need = self.num - num_have\n            if num_need <= 0:\n                return True, None\n            else:\n                if last_item:\n                    last_item = None\n                slice_size = max(int(num_need * EXTRA_FACTOR), self.num // 2, 1)\n        else:\n            slice_size = len(names)\n            done = True\n\n        self.names, new_names = names[slice_size:], names[:slice_size]\n        new_items = self.thing_lookup(new_names)\n        return done, new_items\n\n\nclass ActionBuilder(IDBuilder):\n    def init_query(self):\n        self.actions = {}\n        ids = []\n        for id, date, action in self.query:\n            ids.append(id)\n            self.actions[id] = action\n        self.query = ids\n\n        super(ActionBuilder, self).init_query()\n\n    def thing_lookup(self, names):\n        items = super(ActionBuilder, self).thing_lookup(names)\n\n        for item in items:\n            if item._fullname in self.actions:\n                item.action_type = self.actions[item._fullname]\n        return items\n\n\nclass CampaignBuilder(IDBuilder):\n    \"\"\"Build on a list of PromoTuples.\"\"\"\n    @staticmethod\n    def _get_after(promo_tuples, after, reverse):\n        promo_tuples = list(promo_tuples)\n\n        if not after:\n            return promo_tuples\n\n        if reverse:\n            promo_tuples.reverse()\n\n        fullname_to_index = {pt.link: i for i, pt in enumerate(promo_tuples)}\n        try:\n            i = fullname_to_index[after]\n        except KeyError:\n            promo_tuples = ()\n        else:\n            promo_tuples = promo_tuples[i + 1:]\n\n        return promo_tuples\n\n    def thing_lookup(self, tuples):\n        links = Link._by_fullname([t.link for t in tuples], data=True,\n                                  return_dict=True, stale=self.stale)\n\n        return [Storage({'thing': links[t.link],\n                         '_id': links[t.link]._id,\n                         '_fullname': links[t.link]._fullname,\n                         'weight': t.weight,\n                         'campaign': t.campaign}) for t in tuples]\n\n    def wrap_items(self, items):\n        links = [i.thing for i in items]\n        wrapped = IDBuilder.wrap_items(self, links)\n        by_link = defaultdict(list)\n        for w in wrapped:\n            by_link[w._fullname].append(w)\n\n        ret = []\n        for i in items:\n            w = by_link[i.thing._fullname].pop()\n            w.campaign = i.campaign\n            w.weight = i.weight\n            ret.append(w)\n\n        return ret\n\n    def valid_after(self, after):\n        # CampaignBuilder has special wrapping logic to operate on\n        # PromoTuples and PromoCampaigns. `after` is just a Link, so bypass\n        # the special wrapping logic and use the base class.\n        if self.prewrap_fn:\n            after = self.prewrap_fn(after)\n        if self.wrap:\n            after = Builder.wrap_items(self, (after,))[0]\n        return not self.must_skip(after)\n\n\nclass ModActionBuilder(QueryBuilder):\n    def wrap_items(self, items):\n        wrapped = []\n        by_render_class = defaultdict(list)\n\n        for item in items:\n            w = self.wrap(item)\n            wrapped.append(w)\n            w.fullname = item._fullname\n            by_render_class[w.render_class].append(w)\n\n        for render_class, _items in by_render_class.iteritems():\n            render_class.add_props(c.user, _items)\n\n        return wrapped\n\n\nclass SimpleBuilder(IDBuilder):\n    def thing_lookup(self, names):\n        return names\n\n    def init_query(self):\n        items = list(tup(self.query))\n\n        if self.reverse:\n            items.reverse()\n\n        if self.after:\n            for i, item in enumerate(items):\n                if item._id == self.after:\n                    self.names = items[i + 1:]\n                    break\n            else:\n                self.names = ()\n        else:\n            self.names = items\n\n    def get_items(self):\n        items, prev_item, next_item, bcount, acount = IDBuilder.get_items(self)\n        prev_item_id = prev_item._id if prev_item else None\n        next_item_id = next_item._id if next_item else None\n        return (items, prev_item_id, next_item_id, bcount, acount)\n\n\nclass SearchBuilder(IDBuilder):\n    def __init__(self, query, skip_deleted_authors=True, **kw):\n        self.skip_deleted_authors = skip_deleted_authors\n        IDBuilder.__init__(self, query, **kw)\n\n    def init_query(self):\n        self.skip = True\n\n        self.start_time = time.time()\n\n        self.results = self.query.run()\n        names = list(self.results.docs)\n        self.total_num = self.results.hits\n        self.subreddit_facets = self.results.subreddit_facets\n\n        after = self.after._fullname if self.after else None\n\n        self.names = self._get_after(names,\n                                     after,\n                                     self.reverse)\n\n    def keep_item(self, item):\n        # doesn't use the default keep_item because we want to keep\n        # things that were voted on, even if they've chosen to hide\n        # them in normal listings\n        user = c.user if c.user_is_loggedin else None\n\n        if item._spam or item._deleted:\n            return False\n        # If checking (wrapped) links, filter out banned subreddits\n        elif hasattr(item, 'subreddit') and item.subreddit.spammy():\n            return False\n        elif (hasattr(item, 'subreddit') and\n              not c.user_is_admin and\n              not item.subreddit.is_exposed(user)):\n            return False\n        elif (self.skip_deleted_authors and\n              getattr(item, \"author\", None) and item.author._deleted):\n            return False\n        elif isinstance(item.lookups[0], Subreddit) and not item.is_exposed(user):\n            return False\n\n        # show NSFW to API and RSS users unless obey_over18=true\n        is_api_or_rss = (c.render_style in API_TYPES\n                         or c.render_style in RSS_TYPES)\n        if is_api_or_rss:\n            include_over18 = not c.obey_over18 or c.over18\n        elif feature.is_enabled('safe_search'):\n            include_over18 = c.over18\n        else:\n            include_over18 = True\n\n        is_nsfw = (item.over_18 or\n            (hasattr(item, 'subreddit') and item.subreddit.over_18))\n        if is_nsfw and not include_over18:\n            return False\n\n        return True\n\n\nclass WikiRevisionBuilder(QueryBuilder):\n    show_extended = True\n\n    def __init__(self, revisions, user=None, sr=None, page=None, **kw):\n        self.user = user\n        self.sr = sr\n        self.page = page\n        QueryBuilder.__init__(self, revisions, **kw)\n\n    def wrap_items(self, items):\n        from r2.lib.validator.wiki import this_may_revise\n        types = {}\n        wrapped = []\n        extended = self.show_extended and c.is_wiki_mod\n        extended = extended and this_may_revise(self.page)\n        for item in items:\n            w = self.wrap(item)\n            w.show_extended = extended\n            w.show_compare = self.show_extended\n            types.setdefault(w.render_class, []).append(w)\n            wrapped.append(w)\n\n        user = c.user\n        for cls in types.keys():\n            cls.add_props(user, types[cls])\n\n        return wrapped\n\n    def must_skip(self, item):\n        return item.admin_deleted and not c.user_is_admin\n\n    def keep_item(self, item):\n        from r2.lib.validator.wiki import may_view\n        return ((not item.is_hidden) and\n                may_view(self.sr, self.user, item.wikipage))\n\nclass WikiRecentRevisionBuilder(WikiRevisionBuilder):\n    show_extended = False\n\n    def must_skip(self, item):\n        if WikiRevisionBuilder.must_skip(self, item):\n            return True\n        item_age = datetime.datetime.now(g.tz) - item.date\n        return item_age.days >= wiki.WIKI_RECENT_DAYS\n\n\nCommentTuple = namedtuple(\"CommentTuple\",\n    [\"comment_id\", \"depth\", \"parent_id\", \"num_children\", \"child_ids\"])\n\n\nMissingChildrenTuple = namedtuple(\"MissingChildrenTuple\",\n    [\"num_children\", \"child_ids\"])\n\n\nclass CommentOrdererBase(object):\n    def __init__(self, link, sort, max_comments, max_depth, timer):\n        self.link = link\n        self.sort = sort\n        self.rev_sort = isinstance(sort, operators.desc)\n        self.max_comments = max_comments\n        self.max_depth = max_depth\n        self.timer = timer\n\n    def get_comment_order(self):\n        \"\"\"Return a list of CommentTuples in tree insertion order.\n\n        Also add a MissingChildrenTuple to the end of the list if there\n        are missing root level comments.\n\n        \"\"\"\n\n        with g.stats.get_timer('comment_tree.get.1') as comment_tree_timer:\n            comment_tree = CommentTree.by_link(self.link, comment_tree_timer)\n            sort_name = self.sort.col\n            sorter = get_comment_scores(\n                self.link, sort_name, comment_tree.cids, comment_tree_timer)\n            comment_tree_timer.intermediate('get_scores')\n\n        if isinstance(self.sort, operators.shuffled):\n            # randomize the scores of top level comments\n            top_level_ids = comment_tree.tree.get(None, [])\n            top_level_scores = [\n                sorter[comment_id] for comment_id in top_level_ids]\n            shuffle(top_level_scores)\n            for i, comment_id in enumerate(top_level_ids):\n                sorter[comment_id] = top_level_scores[i]\n\n        self.timer.intermediate(\"load_storage\")\n\n        comment_tree = self.modify_comment_tree(comment_tree)\n        self.timer.intermediate(\"modify_comment_tree\")\n\n        initial_candidates, offset_depth = self.get_initial_candidates(comment_tree)\n\n        comment_tuples = self.get_initial_comment_list(comment_tree)\n        if comment_tuples:\n            # some comments have bypassed the sorting/inserting process, remove\n            # them from `initial_candidates` so they won't be inserted again\n            comment_tuple_ids = {\n                comment_tuple.comment_id for comment_tuple in comment_tuples}\n            initial_candidates = [\n                comment_id for comment_id in initial_candidates\n                if comment_id not in comment_tuple_ids\n            ]\n\n        candidates = []\n        self.update_candidates(candidates, sorter, initial_candidates)\n        self.timer.intermediate(\"pick_candidates\")\n\n        # choose which comments to show\n        while candidates and len(comment_tuples) < self.max_comments:\n            sort_val, comment_id = heapq.heappop(candidates)\n            if comment_id not in comment_tree.cids:\n                continue\n\n            comment_depth = comment_tree.depth[comment_id] - offset_depth\n            if comment_depth >= self.max_depth:\n                continue\n\n            child_ids = comment_tree.tree.get(comment_id, [])\n\n            comment_tuples.append(CommentTuple(\n                comment_id=comment_id,\n                depth=comment_depth,\n                parent_id=comment_tree.parents[comment_id],\n                num_children=comment_tree.num_children[comment_id],\n                child_ids=child_ids,\n            ))\n\n            child_depth = comment_depth + 1\n            if child_depth < self.max_depth:\n                self.update_candidates(candidates, sorter, child_ids)\n\n        self.timer.intermediate(\"pick_comments\")\n\n        # add all not-selected top level comments to the comment_tuples list\n        # so we can make MoreChildren for them later\n        top_level_not_visible = {\n            comment_id for sort_val, comment_id in candidates\n            if comment_tree.depth.get(comment_id, 0) - offset_depth == 0\n        }\n\n        if top_level_not_visible:\n            num_children_not_visible = sum(\n                1 + comment_tree.num_children[comment_id]\n                for comment_id in top_level_not_visible\n            )\n            comment_tuples.append(MissingChildrenTuple(\n                num_children=num_children_not_visible,\n                child_ids=top_level_not_visible,\n            ))\n\n        self.timer.intermediate(\"handle_morechildren\")\n        return comment_tuples\n\n    def modify_comment_tree(self, comment_tree):\n        \"\"\"Potentially rewrite parts of comment_tree.\"\"\"\n        return comment_tree\n\n    def get_initial_candidates(self, comment_tree):\n        \"\"\"Return comments to start building the tree from and offset_depth.\"\"\"\n        raise NotImplementedError\n\n    def get_initial_comment_list(self, comment_tree):\n        \"\"\"Return the starting list of CommentTuples, possibly inserting some\n        and bypassing the regular sorting/inserting process.\"\"\"\n        return []\n\n    def update_candidates(self, candidates, sorter, to_add=None):\n        for comment in (comment for comment in tup(to_add)\n                                if comment in sorter):\n            sort_val = -sorter[comment] if self.rev_sort else sorter[comment]\n            heapq.heappush(candidates, (sort_val, comment))\n\n\nSORT_OPERATOR_BY_NAME = {\n    \"new\": operators.desc('_date'),\n    \"old\": operators.asc('_date'),\n    \"controversial\": operators.desc('_controversy'),\n    \"confidence\": operators.desc('_confidence'),\n    \"qa\": operators.desc('_qa'),\n    \"hot\": operators.desc('_hot'),\n    \"top\": operators.desc('_score'),\n    \"random\": operators.shuffled('_confidence'),\n}\n\n\nclass CommentOrderer(CommentOrdererBase):\n    def get_initial_candidates(self, comment_tree):\n        \"\"\"Build the tree starting from all root level comments.\"\"\"\n        initial_candidates = comment_tree.tree.get(None, [])\n        if initial_candidates:\n            offset_depth = min(comment_tree.depth[comment_id]\n                for comment_id in initial_candidates)\n        else:\n            offset_depth = 0\n        return initial_candidates, offset_depth\n\n    def get_initial_comment_list(self, comment_tree):\n        \"\"\"Promote the sticky comment, if any.\"\"\"\n        comment_tuples = []\n\n        if self.link.sticky_comment_id:\n            root_level_comments = comment_tree.tree.get(None, [])\n            sticky_comment_id = self.link.sticky_comment_id\n            if sticky_comment_id in root_level_comments:\n                comment_tuples.append(CommentTuple(\n                    comment_id=sticky_comment_id,\n                    depth=0,\n                    parent_id=None,\n                    num_children=comment_tree.num_children[sticky_comment_id],\n                    child_ids=comment_tree.tree.get(sticky_comment_id, []),\n                ))\n            else:\n                g.log.warning(\"Non-top-level sticky comment detected on \"\n                              \"link %r.\", self.link)\n        return comment_tuples\n\n    def cache_key(self):\n        key = \"order:{link}_{operator}{column}\".format(\n            link=self.link._id36,\n            operator=self.sort.__class__.__name__,\n            column=self.sort.col,\n        )\n        return key\n\n    @classmethod\n    def write_cache(cls, link, sort, timer):\n        comment_orderer = cls(link, sort,\n            max_comments=g.max_comments_gold,\n            max_depth=MAX_RECURSION,\n            timer=timer,\n        )\n        comment_tuples = comment_orderer._get_comment_order()\n\n        key = comment_orderer.cache_key()\n        existing_tuples = g.permacache.get(key) or []\n\n        if comment_tuples != existing_tuples:\n            # don't write cache if the order hasn't changed\n            g.permacache.set(key, comment_tuples)\n\n    def should_read_cache(self):\n        if self.link.precomputed_sorts:\n            precomputed_sorts = [\n                SORT_OPERATOR_BY_NAME[sort_name]\n                for sort_name in self.link.precomputed_sorts\n                if sort_name in SORT_OPERATOR_BY_NAME\n            ]\n            return self.sort in precomputed_sorts\n        else:\n            return False\n\n    def read_cache(self):\n        key = self.cache_key()\n        comment_tuples = g.permacache.get(key) or []\n        self.timer.intermediate(\"read_precomputed\")\n\n        # precomputed order might have returned more than max_comments. before\n        # dealing with that we need to preserve the MissingChildrenTuple for\n        # missing root level comments\n        if comment_tuples and isinstance(comment_tuples[-1], MissingChildrenTuple):\n            mct = comment_tuples.pop(-1)\n            top_level_not_visible = mct.child_ids\n            num_children_not_visible = mct.num_children\n        else:\n            top_level_not_visible = set()\n            num_children_not_visible = 0\n\n        # precomputed order uses the default max_depth. filter the list\n        # if we need a different max_depth. NOTE: we may end up with fewer\n        # comments than were requested.\n        if self.max_depth < MAX_RECURSION:\n            comment_tuples = [\n                comment_tuple for comment_tuple in comment_tuples\n                if comment_tuple.depth < self.max_depth\n            ]\n\n        if len(comment_tuples) > self.max_comments:\n            top_level_not_visible.update({\n                comment_tuple.comment_id\n                for comment_tuple in comment_tuples[self.max_comments:]\n                if comment_tuple.depth == 0\n            })\n            num_children_not_visible += sum(\n                1 + comment_tuple.num_children\n                for comment_tuple in comment_tuples[self.max_comments:]\n                if comment_tuple.depth == 0\n            )\n            comment_tuples = comment_tuples[:self.max_comments]\n\n        if top_level_not_visible:\n            comment_tuples.append(MissingChildrenTuple(\n                num_children=num_children_not_visible,\n                child_ids=top_level_not_visible,\n            ))\n\n        self.timer.intermediate(\"prune_precomputed\")\n\n        return comment_tuples\n\n    def _get_comment_order(self):\n        return CommentOrdererBase.get_comment_order(self)\n\n    def get_comment_order(self):\n        num_comments = self.link.num_comments\n        if num_comments == 0:\n            bucket = \"0\"\n        elif num_comments >= 100:\n            bucket = \"100_plus\"\n        else:\n            bucket_start = num_comments / 5 * 5\n            bucket_end = bucket_start + 5\n            bucket = \"%s_%s\" % (bucket_start, bucket_end)\n\n        # record the number of comments on this link so we can get an idea of\n        # what value to use for 'precomputed_comment_sort_min_comments'\n        g.stats.simple_event(\"CommentOrderer.num_comments.%s\" % bucket)\n\n        if self.link.num_comments <= 0:\n            return []\n\n        if self.should_read_cache():\n            with g.stats.get_timer(\"CommentOrderer.read_cache\") as timer:\n                return self.read_cache()\n        else:\n            if bucket == \"100_plus\":\n                for sort_name, operator in SORT_OPERATOR_BY_NAME.iteritems():\n                    if operator == self.sort:\n                        break\n                else:\n                    sort_name = \"None\"\n                g.stats.simple_event(\"CommentOrderer.100_plus_sort.%s\" % sort_name)\n\n            timer_name = \"CommentOrderer.by_num_comments.%s\" % bucket\n            with g.stats.get_timer(timer_name) as timer:\n                return self._get_comment_order()\n\n\nclass QACommentOrderer(CommentOrderer):\n    def _get_comment_order(self):\n        \"\"\"Filter out the comments we don't want to show in QA sort.\n\n        QA sort only displays comments that are:\n        1. Top-level\n        2. Responses from the OP(s)\n        3. Within one level of an OP reply\n        4. Distinguished\n\n        All ancestors of comments meeting the above rules will also be shown.\n        This ensures the question responded to by OP is shown.\n\n        \"\"\"\n\n        comment_tuples = CommentOrdererBase.get_comment_order(self)\n        if not comment_tuples:\n            return comment_tuples\n        elif isinstance(comment_tuples[-1], MissingChildrenTuple):\n            missing_children_tuple = comment_tuples.pop()\n        else:\n            missing_children_tuple = None\n\n        special_responder_ids = self.link.responder_ids\n\n        # unfortunately we need to look up all the Comments for QA\n        comment_ids = {ct.comment_id for ct in comment_tuples}\n        comments_by_id = Comment._byID(comment_ids, data=True)\n\n        # figure out which comments will be kept (all others are discarded)\n        kept_comment_ids = set()\n        for comment_tuple in comment_tuples:\n            if comment_tuple.depth == 0:\n                kept_comment_ids.add(comment_tuple.comment_id)\n                continue\n\n            comment = comments_by_id[comment_tuple.comment_id]\n            parent = comments_by_id[comment.parent_id] if comment.parent_id else None\n\n            if comment.author_id in special_responder_ids:\n                kept_comment_ids.add(comment_tuple.comment_id)\n                continue\n\n            if parent and parent.author_id in special_responder_ids:\n                kept_comment_ids.add(comment_tuple.comment_id)\n                continue\n\n            if hasattr(comment, \"distinguished\") and comment.distinguished != \"no\":\n                kept_comment_ids.add(comment_tuple.comment_id)\n                continue\n\n        # add all ancestors to kept_comment_ids\n        for comment_id in sorted(kept_comment_ids):\n            # sort the comments so we start with the most root level comments\n            comment = comments_by_id[comment_id]\n            parent_id = comment.parent_id\n\n            counter = 0\n            while (parent_id and\n                        parent_id not in kept_comment_ids and\n                        counter < g.max_comment_parent_walk):\n                kept_comment_ids.add(parent_id)\n                counter += 1\n\n                comment = comments_by_id[parent_id]\n                parent_id = comment.parent_id\n\n        # remove all comment tuples that aren't in kept_comment_ids\n        comment_tuples = [comment_tuple for comment_tuple in comment_tuples\n            if comment_tuple.comment_id in kept_comment_ids\n        ]\n\n        if missing_children_tuple:\n            comment_tuples.append(missing_children_tuple)\n\n        return comment_tuples\n\n\ndef get_active_sort_orders_for_link(link):\n    # only activate precomputed sorts for links with enough comments.\n    # (value of 0 means not active for any value of link.num_comments)\n    min_comments = g.live_config['precomputed_comment_sort_min_comments']\n    if min_comments <= 0 or link.num_comments < min_comments:\n        return set()\n\n    active_sorts = set(g.live_config['precomputed_comment_sorts'])\n    if g.live_config['precomputed_comment_suggested_sort']:\n        suggested_sort = link.sort_if_suggested()\n        if suggested_sort:\n            active_sorts.add(suggested_sort)\n\n    return active_sorts\n\n\ndef write_comment_orders(link):\n    # we don't really care about getting detailed timings here, the entire\n    # process will be timed by the caller\n    timer = SimpleSillyStub()\n\n    precomputed_sorts = set()\n    for sort_name in get_active_sort_orders_for_link(link):\n        sort = SORT_OPERATOR_BY_NAME.get(sort_name)\n        if not sort:\n            continue\n\n        if sort_name == \"qa\":\n            QACommentOrderer.write_cache(link, sort, timer)\n        else:\n            CommentOrderer.write_cache(link, sort, timer)\n\n        precomputed_sorts.add(sort_name)\n\n    if precomputed_sorts:\n        g.stats.simple_event(\"CommentOrderer.write_comment_orders.write\")\n    else:\n        g.stats.simple_event(\"CommentOrderer.write_comment_orders.noop\")\n\n    # replace empty set with None to match the Link._defaults value\n    link.precomputed_sorts = precomputed_sorts or None\n    link._commit()\n\n\nclass PermalinkCommentOrderer(CommentOrdererBase):\n    def __init__(self, link, sort, max_comments, max_depth, timer, comment,\n                 context):\n        CommentOrdererBase.__init__(\n            self, link, sort, max_comments, max_depth, timer)\n        self.comment = comment\n        self.context = context\n\n    @classmethod\n    def get_path_to_comment(cls, comment, context, comment_tree):\n        \"\"\"Return the path back to top level from comment.\n\n        Restrict the path to a maximum of `context` levels deep.\"\"\"\n\n        if comment._id not in comment_tree.cids:\n            # the comment isn't in the tree\n            raise InconsistentCommentTreeError\n\n        comment_id = comment._id\n        path = []\n        while comment_id and len(path) <= context:\n            path.append(comment_id)\n            try:\n                comment_id = comment_tree.parents[comment_id]\n            except KeyError:\n                # the comment's parent is missing from the tree. this might\n                # just mean that the child was added to the tree first and\n                # the tree will be correct when the parent is added.\n                raise InconsistentCommentTreeError\n\n        # reverse the list so the first element is the most root level comment\n        path.reverse()\n        return path\n\n    def modify_comment_tree(self, comment_tree):\n        path = self.get_path_to_comment(\n            self.comment, self.context, comment_tree)\n\n        # work through the path in reverse starting with the requested comment\n        for comment_id in reversed(path):\n            # rewrite parent's tree so it leads only to the requested comment\n            parent_id = comment_tree.parents[comment_id]\n            comment_tree.tree[parent_id] = [comment_id]\n\n            # rewrite parent's num_children to count only this branch\n            if parent_id is not None:\n                branch_num_children = comment_tree.num_children[comment_id]\n                comment_tree.num_children[parent_id] = branch_num_children + 1\n\n        return comment_tree\n\n    def get_initial_candidates(self, comment_tree):\n        \"\"\"Start the tree from the first ancestor of requested comment.\"\"\"\n        path = self.get_path_to_comment(\n            self.comment, self.context, comment_tree)\n\n        # get_path_to_comment returns path ordered from ancestor to\n        # selected comment\n        root_comment = path[0]\n        initial_candidates = [root_comment]\n        offset_depth = comment_tree.depth[root_comment]\n        return initial_candidates, offset_depth\n\n\nclass ChildrenCommentOrderer(CommentOrdererBase):\n    def __init__(self, link, sort, max_comments, max_depth, timer, children):\n        CommentOrdererBase.__init__(\n            self, link, sort, max_comments, max_depth, timer)\n        self.children = children\n\n    def get_initial_candidates(self, comment_tree):\n        \"\"\"Start the tree from the requested children.\"\"\"\n\n        children = [\n            comment_id for comment_id in self.children\n            if comment_id in comment_tree.depth\n        ]\n\n        if children:\n            children_depth = min(\n                comment_tree.depth[comment_id] for comment_id in children)\n\n            children = [\n                comment_id for comment_id in children\n                if comment_tree.depth[comment_id] == children_depth\n            ]\n\n        initial_candidates = children\n\n        # BUG: current viewing depth isn't considered, so requesting children\n        # of a deep comment can return nothing. the fix is to send the current\n        # offset_depth along with the MoreChildren request\n        offset_depth = 0\n\n        return initial_candidates, offset_depth\n\n\ndef make_child_listing():\n    l = Listing(builder=None, nextprev=None)\n    l.things = []\n    child = Wrapped(l)\n    return child\n\n\ndef add_to_child_listing(parent, child_thing):\n    if not hasattr(parent, 'child'):\n        child = make_child_listing()\n        child.parent_name = \"deleted\" if parent.deleted else parent._fullname\n        parent.child = child\n\n    parent.child.things.append(child_thing)\n\n\nclass CommentBuilder(Builder):\n    \"\"\"Build (lookup and wrap) comments for display.\"\"\"\n    def __init__(self, link, sort, comment=None, children=None, context=None,\n                 load_more=True, continue_this_thread=True,\n                 max_depth=MAX_RECURSION, edits_visible=True, num=None,\n                 show_deleted=False, **kw):\n        self.link = link\n        self.sort = sort\n\n        # arguments for permalink mode\n        self.comment = comment\n        self.context = context or 0\n\n        # argument for morechildren mode\n        self.children = children\n\n        # QA mode only activates for the full comments view\n        self.in_qa_mode = sort.col == '_qa' and not (comment or children)\n\n        self.load_more = load_more\n        self.max_depth = max_depth\n        self.show_deleted = show_deleted or c.user_is_admin\n\n        # uncollapse everything in QA mode because the sorter will prune\n        self.uncollapse_all = c.user_is_admin or self.in_qa_mode\n        self.edits_visible = edits_visible\n        self.num = num\n        self.continue_this_thread = continue_this_thread\n\n        self.comments = None\n        Builder.__init__(self, **kw)\n\n    def get_items(self):\n        if self.comments is None:\n            self._get_comments()\n        return self._make_wrapped_tree()\n\n    def _get_comments(self):\n        self.load_comment_order()\n        comment_ids = {\n            comment_tuple.comment_id\n            for comment_tuple in self.ordered_comment_tuples\n        }\n        self.comments = Comment._byID(\n            comment_ids, data=True, return_dict=False, stale=self.stale)\n        self.timer.intermediate(\"lookup_comments\")\n\n    def load_comment_order(self):\n        self.timer = g.stats.get_timer(\"CommentBuilder.get_items\")\n        self.timer.start()\n\n        if self.comment:\n            orderer = PermalinkCommentOrderer(\n                self.link, self.sort, self.num, self.max_depth, self.timer,\n                self.comment, self.context)\n\n            try:\n                comment_tuples = orderer.get_comment_order()\n            except InconsistentCommentTreeError:\n                g.log.error(\"Hack - self.comment (%d) not in depth. Defocusing...\"\n                            % self.comment._id)\n                self.comment = None\n                orderer = CommentOrderer(\n                    self.link, self.sort, self.num, self.max_depth, self.timer)\n                comment_tuples = orderer.get_comment_order()\n\n        elif self.children:\n            orderer = ChildrenCommentOrderer(\n                self.link, self.sort, self.num, self.max_depth, self.timer,\n                self.children)\n            comment_tuples = orderer.get_comment_order()\n        elif self.in_qa_mode:\n            orderer = QACommentOrderer(\n                self.link, self.sort, self.num, self.max_depth, self.timer)\n            comment_tuples = orderer.get_comment_order()\n        else:\n            orderer = CommentOrderer(\n                self.link, self.sort, self.num, self.max_depth, self.timer)\n            comment_tuples = orderer.get_comment_order()\n\n        if (comment_tuples and\n                isinstance(comment_tuples[-1], MissingChildrenTuple)):\n            mct = comment_tuples.pop(-1)\n            missing_root_comments = mct.child_ids\n            missing_root_count = mct.num_children\n        else:\n            missing_root_comments = set()\n            missing_root_count = 0\n\n        self.ordered_comment_tuples = comment_tuples\n        self.missing_root_comments = missing_root_comments\n        self.missing_root_count = missing_root_count\n\n    def keep_item(self, item):\n        if not self.show_deleted:\n            if item.deleted and not item.num_children:\n                return False\n        return item.keep_item(item)\n\n    def _make_wrapped_tree(self):\n        timer = self.timer\n        ordered_comment_tuples = self.ordered_comment_tuples\n        missing_root_comments = self.missing_root_comments\n        missing_root_count = self.missing_root_count\n\n        if not ordered_comment_tuples:\n            self.timer.stop()\n            return []\n\n        comment_order = [\n            comment_tuple.comment_id for comment_tuple in ordered_comment_tuples\n        ]\n\n        wrapped = self.make_wrapped_items(ordered_comment_tuples)\n        timer.intermediate(\"wrap_comments\")\n\n        wrapped_by_id = {\n            comment._id: comment for comment in wrapped}\n        self.uncollapse_special_comments(wrapped_by_id)\n\n        redacted_ids = set()\n\n        visible_ids = {\n            comment._id for comment in wrapped\n            if not getattr(comment, 'hidden', False)\n        }\n\n        final = []\n        for comment_id in comment_order:\n            comment = wrapped_by_id[comment_id]\n\n            if getattr(comment, 'hidden', False):\n                continue\n\n            if not self.keep_item(comment):\n                redacted_ids.add(comment_id)\n                continue\n\n            # add the comment as a child of its parent or to the top level of\n            # the tree if it has no parent or the parent isn't in the listing\n            parent = wrapped_by_id.get(comment.parent_id)\n            if parent:\n                add_to_child_listing(parent, comment)\n            else:\n                final.append(comment)\n\n        self.timer.intermediate(\"build_comments\")\n\n        if not self.load_more:\n            timer.stop()\n            return final\n\n        # add MoreRecursion and MoreChildren last so they'll be the last item in\n        # a comment's child listing\n        for comment in wrapped:\n            if (self.continue_this_thread and\n                    comment.depth == self.max_depth - 1 and\n                    comment.num_children > 0):\n                # only comments with depth < max_depth are visible\n                # if this comment is as deep as we can go and has children then\n                # we need to insert a MoreRecursion child\n                mr = MoreRecursion(self.link, depth=0, parent_id=comment._id)\n                w = Wrapped(mr)\n                add_to_child_listing(comment, w)\n            elif comment.depth < self.max_depth - 1:\n                missing_child_ids = (\n                    set(comment.child_ids) - visible_ids - redacted_ids\n                )\n                if missing_child_ids:\n                    missing_depth = comment.depth + 1\n                    mc = MoreChildren(self.link, self.sort, depth=missing_depth,\n                            parent_id=comment._id)\n                    w = Wrapped(mc)\n                    visible_count = sum(\n                        1 + wrapped_by_id[child_id].num_children\n                        for child_id in comment.child_ids\n                        if child_id in visible_ids\n                    )\n                    w.count = comment.num_children - visible_count\n                    w.children.extend(missing_child_ids)\n                    add_to_child_listing(comment, w)\n\n        if missing_root_comments:\n            mc = MoreChildren(self.link, self.sort, depth=0, parent_id=None)\n            w = Wrapped(mc)\n            w.count = missing_root_count\n            w.children.extend(missing_root_comments)\n            final.append(w)\n\n        self.timer.intermediate(\"build_morechildren\")\n        self.timer.stop()\n        return final\n\n    def make_wrapped_items(self, comment_tuples):\n        wrapped = Builder.wrap_items(self, self.comments)\n        wrapped_by_id = {comment._id: comment for comment in wrapped}\n\n        for comment_tuple in comment_tuples:\n            comment = wrapped_by_id[comment_tuple.comment_id]\n            comment.num_children = comment_tuple.num_children\n            comment.child_ids = comment_tuple.child_ids\n            comment.depth = comment_tuple.depth\n            comment.edits_visible = self.edits_visible\n\n            if self.children:\n                # rewrite the parent links to use anchor tags because the parent\n                # is on the page but wasn't included in this batch\n                if comment.parent_id:\n                    comment.parent_permalink = '#' + to36(comment.parent_id)\n\n        return wrapped\n\n    def uncollapse_special_comments(self, wrapped_by_id):\n        \"\"\"Undo collapsing for special comments.\n\n        The builder may have set `collapsed` and `hidden` attributes for\n        comments that we want to ensure are shown.\n\n        \"\"\"\n\n        if self.uncollapse_all:\n            dont_collapse = set(wrapped_by_id.keys())\n        elif self.comment:\n            dont_collapse = set([self.comment._id])\n            parent_id = self.comment.parent_id\n            while parent_id:\n                dont_collapse.add(parent_id)\n                if parent_id in wrapped_by_id:\n                    parent_id = wrapped_by_id[parent_id].parent_id\n                else:\n                    parent_id = None\n        elif self.children:\n            dont_collapse = set(self.children)\n        else:\n            dont_collapse = set()\n\n        # we only care about preventing collapse of wrapped comments\n        dont_collapse &= set(wrapped_by_id.keys())\n\n        maybe_collapse = set(wrapped_by_id.keys()) - dont_collapse\n\n        for comment_id in maybe_collapse:\n            comment = wrapped_by_id[comment_id]\n            if comment.distinguished and comment.distinguished != \"no\":\n                dont_collapse.add(comment_id)\n\n        maybe_collapse -= dont_collapse\n\n        # ensure all ancestors of dont_collapse comments are not collapsed\n        if maybe_collapse:\n            for comment_id in sorted(dont_collapse):\n                # sort comments so we start with the most root level comments\n                comment = wrapped_by_id[comment_id]\n                parent_id = comment.parent_id\n\n                counter = 0\n                while (parent_id and\n                            parent_id not in dont_collapse and\n                            parent_id in wrapped_by_id and\n                            counter < g.max_comment_parent_walk):\n                    dont_collapse.add(parent_id)\n                    counter += 1\n\n                    comment = wrapped_by_id[parent_id]\n                    parent_id = comment.parent_id\n\n        for comment_id in dont_collapse:\n            comment = wrapped_by_id[comment_id]\n            if comment.collapsed:\n                comment.collapsed = False\n                comment.hidden = False\n\n    def item_iter(self, a):\n        for i in a:\n            yield i\n            if hasattr(i, 'child'):\n                for j in self.item_iter(i.child.things):\n                    yield j\n\n\nclass MessageBuilder(Builder):\n    def __init__(self, skip=True, num=None, parent=None, after=None,\n                 reverse=False, threaded=False, **kw):\n        self.skip = skip\n        self.num = num\n        self.parent = parent\n        self.after = after\n        self.reverse = reverse\n        self.threaded = threaded\n        Builder.__init__(self, **kw)\n\n    def get_tree(self):\n        raise NotImplementedError, \"get_tree\"\n\n    def valid_after(self, after):\n        w = self.convert_items((after,))[0]\n        return self._viewable_message(w)\n\n    def _viewable_message(self, m):\n        if (c.user_is_admin or\n                getattr(m, \"author_id\", 0) == c.user._id or\n                getattr(m, \"to_id\", 0) == c.user._id):\n            return True\n\n        # m is wrapped at this time, so it should have an SR\n        subreddit = getattr(m, \"subreddit\", None)\n        if subreddit and subreddit.is_moderator_with_perms(c.user, 'mail'):\n            return True\n\n        return False\n\n    def _apply_pagination(self, tree):\n        if self.parent or self.num is None:\n            return tree, None, None\n\n        prev_item = None\n        next_item = None\n\n        if self.after:\n            # truncate the tree to only show before/after requested message\n            if self.reverse:\n                next_item = self.after._id\n                tree = [\n                    (parent_id, child_ids) for parent_id, child_ids in tree\n                    if tree_sort_fn((parent_id, child_ids)) >= next_item\n                ]\n\n                # special handling for after+reverse (before link): truncate\n                # the tree so it has num messages before the requested one\n                if len(tree) > self.num:\n                    first_id, first_children = tree[-(self.num + 1)]\n                    prev_item = tree_sort_fn((first_id, first_children))\n                    tree = tree[-self.num:]\n            else:\n                prev_item = self.after._id\n                tree = [\n                    (parent_id, child_ids) for parent_id, child_ids in tree\n                    if tree_sort_fn((parent_id, child_ids)) < prev_item\n                ]\n\n        if len(tree) > self.num:\n            # truncate the tree to show only num conversations\n            tree = tree[:self.num]\n            last_id, last_children = tree[-1]\n            next_item = tree_sort_fn((last_id, last_children))\n        return tree, prev_item, next_item\n\n    @classmethod\n    def should_collapse(cls, message):\n        # don't collapse this message if it has a new direct child\n        if hasattr(message, \"child\"):\n            has_new_child = any(child.new for child in message.child.things)\n        else:\n            has_new_child = False\n\n        return (message.is_collapsed and\n            not message.new and\n            not has_new_child)\n\n    def get_items(self):\n        tree = self.get_tree()\n        tree, prev_item, next_item = self._apply_pagination(tree)\n\n        message_ids = []\n        for parent_id, child_ids in tree:\n            message_ids.append(parent_id)\n            message_ids.extend(child_ids)\n\n        if prev_item:\n            message_ids.append(prev_item)\n\n        messages = Message._byID(message_ids, data=True, return_dict=False)\n        wrapped = {m._id: m for m in self.wrap_items(messages)}\n\n        if prev_item:\n            prev_item = wrapped[prev_item]\n        if next_item:\n            next_item = wrapped[next_item]\n\n        final = []\n        for parent_id, child_ids in tree:\n            if parent_id not in wrapped:\n                continue\n\n            parent = wrapped[parent_id]\n\n            if not self._viewable_message(parent):\n                continue\n\n            children = [\n                wrapped[child_id] for child_id in child_ids\n                if child_id in wrapped\n            ]\n\n            depth = {parent_id: 0}\n            substitute_parents = {}\n\n            if (children and self.skip and not self.threaded and\n                    not self.parent and not parent.new and parent.is_collapsed):\n                for i, child in enumerate(children):\n                    if child.new or not child.is_collapsed:\n                        break\n                else:\n                    i = -1\n                # in flat view replace collapsed chain with MoreMessages\n                child = make_child_listing()\n                child.parent_name = \"deleted\" if parent.deleted else parent._fullname\n                parent = Wrapped(MoreMessages(parent, child))\n                children = children[i:]\n\n            for child in sorted(children, key=lambda child: child._id):\n                # iterate from the root outwards so we can check the depth\n                if self.threaded:\n                    try:\n                        child_parent = wrapped[child.parent_id]\n                    except KeyError:\n                        # the stored comment tree was missing this message's\n                        # parent, treat it as a top level reply\n                        child_parent = parent\n                else:\n                    # for flat view all messages are decendants of the\n                    # parent message\n                    child_parent = parent\n                parent_depth = depth[child_parent._id]\n                child_depth = parent_depth + 1\n                depth[child._id] = child_depth\n\n                if child_depth == MAX_RECURSION:\n                    # current message is at maximum depth level, all its\n                    # children will be displayed as children of its parent\n                    substitute_parents[child._id] = child_parent._id\n\n                if child_depth > MAX_RECURSION:\n                    child_parent_id = substitute_parents[child.parent_id]\n                    substitute_parents[child._id] = child_parent_id\n                    child_parent = wrapped[child_parent_id]\n\n                child.is_child = True\n                add_to_child_listing(child_parent, child)\n\n            for child in children:\n                # look over the children again to decide whether they can be\n                # collapsed\n                child.threaded = self.threaded\n                child.collapsed = self.should_collapse(child)\n\n            if self.threaded and children:\n                most_recent_child_id = max(child._id for child in children)\n                most_recent_child = wrapped[most_recent_child_id]\n                most_recent_child.most_recent = True\n\n            parent.is_parent = True\n            parent.threaded = self.threaded\n            parent.collapsed = self.should_collapse(parent)\n            final.append(parent)\n\n        return (final, prev_item, next_item, len(final), len(final))\n\n    def item_iter(self, builder_items):\n        items = builder_items[0]\n\n        def _item_iter(_items):\n            for i in _items:\n                yield i\n                if hasattr(i, \"child\"):\n                    for j in _item_iter(i.child.things):\n                        yield j\n\n        return _item_iter(items)\n\n\nclass ModeratorMessageBuilder(MessageBuilder):\n    def __init__(self, user, **kw):\n        self.user = user\n        MessageBuilder.__init__(self, **kw)\n\n    def get_tree(self):\n        if self.parent:\n            sr = Subreddit._byID(self.parent.sr_id)\n            return sr_conversation(sr, self.parent)\n        sr_ids = Subreddit.reverse_moderator_ids(self.user)\n        return moderator_messages(sr_ids)\n\n\nclass MultiredditMessageBuilder(MessageBuilder):\n    def __init__(self, sr, **kw):\n        self.sr = sr\n        MessageBuilder.__init__(self, **kw)\n\n    def get_tree(self):\n        if self.parent:\n            sr = Subreddit._byID(self.parent.sr_id)\n            return sr_conversation(sr, self.parent)\n        return moderator_messages(self.sr.sr_ids)\n\n\nclass TopCommentBuilder(CommentBuilder):\n    \"\"\"A comment builder to fetch only the top-level, non-spam,\n       non-deleted comments\"\"\"\n    def __init__(self, link, sort, num=None, wrap=Wrapped):\n        CommentBuilder.__init__(self, link, sort, load_more=False,\n            continue_this_thread=False, max_depth=1, wrap=wrap, num=num)\n\n    def get_items(self):\n        final = CommentBuilder.get_items(self)\n        return [ cm for cm in final if not cm.deleted ]\n\n\nclass SrMessageBuilder(MessageBuilder):\n    def __init__(self, sr, **kw):\n        self.sr = sr\n        MessageBuilder.__init__(self, **kw)\n\n    def get_tree(self):\n        if self.parent:\n            return sr_conversation(self.sr, self.parent)\n        return subreddit_messages(self.sr)\n\n\nclass UserMessageBuilder(MessageBuilder):\n    def __init__(self, user, **kw):\n        self.user = user\n        MessageBuilder.__init__(self, **kw)\n\n    def _viewable_message(self, message):\n        is_author = message.author_id == c.user._id\n        if not c.user_is_admin:\n            if not is_author and message._spam:\n                return False\n\n            if message.author_id in self.user.enemies:\n                return False\n\n            # do not show messages which were deleted on recipient\n            if (hasattr(message, \"del_on_recipient\") and\n                    message.to_id == c.user._id and message.del_on_recipient):\n                return False\n\n        return super(UserMessageBuilder, self)._viewable_message(message)\n\n    def get_tree(self):\n        if self.parent:\n            return conversation(self.user, self.parent)\n        return user_messages(self.user)\n\n    def valid_after(self, after):\n        # Messages that have been spammed are still valid afters\n        w = self.convert_items((after,))[0]\n        return MessageBuilder._viewable_message(self, w)\n\n\nclass UserListBuilder(QueryBuilder):\n    def thing_lookup(self, rels):\n        accounts = Account._byID([rel._thing2_id for rel in rels], data=True)\n        for rel in rels:\n            rel._thing2 = accounts.get(rel._thing2_id)\n        return rels\n\n    def must_skip(self, item):\n        return item.user._deleted\n\n    def valid_after(self, after):\n        # Users that have been deleted are still valid afters\n        return True\n\n    def wrap_items(self, rels):\n        return [self.wrap(rel) for rel in rels]\n\nclass SavedBuilder(IDBuilder):\n    def wrap_items(self, items):\n        categories = LinkSavesByAccount.fast_query(c.user, items).items()\n        categories += CommentSavesByAccount.fast_query(c.user, items).items()\n        categories = {item[1]._id: category for item, category in categories if category}\n        wrapped = QueryBuilder.wrap_items(self, items)\n        for w in wrapped:\n            category = categories.get(w._id, '')\n            w.savedcategory = category\n        return wrapped\n\n\nclass FlairListBuilder(UserListBuilder):\n    def init_query(self):\n        q = self.query\n\n        if self.reverse:\n            q._reverse()\n\n        q._data = True\n        self.orig_rules = deepcopy(q._rules)\n        # FlairLists use Accounts for afters\n        if self.after:\n            if self.reverse:\n                q._filter(Flair.c._thing2_id < self.after._id)\n            else:\n                q._filter(Flair.c._thing2_id > self.after._id)\n"
  },
  {
    "path": "r2/r2/models/comment_tree.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom collections import defaultdict\n\nfrom pylons import app_globals as g\n\nfrom r2.lib.utils import SimpleSillyStub\nfrom r2.lib.utils.comment_tree_utils import get_tree_details, calc_num_children\nfrom r2.models.link import Comment\n\n\n\"\"\"Storage for comment trees\n\nCommentTree is a class that provides an interface to the actual storage.\nWhatever the underlying storage is, it must be able to generate the following\nstructures:\n* tree: dict of comment id -> list of child comment ids. The `None` entry is\n  top level comments\n* cids: list of all comment ids in the comment tree\n* depth: dict of comment id -> depth\n* parents: dict of comment id -> parent comment id\n* num_children: dict of comment id -> number of descendant comments, not just\n  direct children\n\nCommentTreePermacache uses permacache as the storage, and stores just the tree\nstructure. The cids, depth, parents and num_children are generated on the fly\nfrom the tree.\n\nAttempts were made to move to a different data model that would take advantage\nof the column based storage of Cassandra and eliminate the need for locking when\nadding a comment to the comment tree.\n\nCommentTreeStorageV2: for each comment, write a column where the column name is\n(parent_comment id, comment_id) and the column value is a counter giving the\nsize of the subtree rooted at the comment. This data model was abandoned because\ncounters ended up being unreliable and the shards put too much GC pressure on\nthe Cassandra JVM.\n\nCommentTreeStorageV3: for each comment, write a column where the column name is\n(depth, parent_comment_id, comment_id) and the column value is not used. This\ndata model was abandoned because of more unexpected GC problems after longer\ntime periods and generally insufficient regular-case performance.\n\n\"\"\"\n\n\nclass CommentTreePermacache(object):\n    @classmethod\n    def _permacache_key(cls, link):\n        return 'comments_' + str(link._id)\n\n    @classmethod\n    def _mutation_context(cls, link):\n        \"\"\"Return a lock for use during read-modify-write operations\"\"\"\n        key = 'comment_lock_' + str(link._id)\n        return g.make_lock(\"comment_tree\", key)\n\n    @classmethod\n    def prepare_new_storage(cls, link):\n        \"\"\"Write an empty tree to permacache\"\"\"\n        with cls._mutation_context(link) as lock:\n            # read-modify-write, so get the lock\n            existing_tree = cls._load_tree(link)\n            if not existing_tree:\n                # don't overwrite an existing non-empty tree\n                tree = {}\n                cls._write_tree(link, tree, lock)\n\n    @classmethod\n    def _load_tree(cls, link):\n        key = cls._permacache_key(link)\n        tree = g.permacache.get(key)\n        return tree or {}   # assume empty tree on miss\n\n    @classmethod\n    def _write_tree(cls, link, tree, lock):\n        assert lock.have_lock\n        key = cls._permacache_key(link)\n        g.permacache.set(key, tree)\n\n    @classmethod\n    def get_tree_pieces(cls, link, timer):\n        tree = cls._load_tree(link)\n        timer.intermediate('load')\n\n        cids, depth, parents = get_tree_details(tree)\n        num_children = calc_num_children(tree)\n        num_children = defaultdict(int, num_children)\n        timer.intermediate('calculate')\n\n        return cids, tree, depth, parents, num_children\n\n    @classmethod\n    def add_comments(cls, link, comments):\n        with cls._mutation_context(link) as lock:\n            # adding comments requires read-modify-write, so get the lock\n            tree = cls._load_tree(link)\n            cids, _, _ = get_tree_details(tree)\n\n            # skip any comments that are already in the stored tree and convert\n            # to a set to remove any duplicate comments\n            comments = {\n                comment for comment in comments\n                if comment._id not in cids\n            }\n\n            if not comments:\n                return\n\n            # warn on any comments whose parents are missing from the tree\n            # because they will never be displayed unless their parent is\n            # added. this can happen in normal operation if there are multiple\n            # queue consumers and a child is processed before its parent.\n            parent_ids = set(cids) | {comment._id for comment in comments}\n            possible_orphan_comments = {\n                comment for comment in comments\n                if (comment.parent_id and comment.parent_id not in parent_ids)\n            }\n            if possible_orphan_comments:\n                g.log.error(\"comment_tree_inconsistent: %s %s\", link,\n                    possible_orphan_comments)\n                g.stats.simple_event('comment_tree_inconsistent')\n\n            for comment in comments:\n                tree.setdefault(comment.parent_id, []).append(comment._id)\n\n            cls._write_tree(link, tree, lock)\n\n    @classmethod\n    def rebuild(cls, link, comments):\n        \"\"\"Generate a tree from comments and overwrite any existing tree.\"\"\"\n        with cls._mutation_context(link) as lock:\n            # not reading, but we should block other read-modify-write\n            # operations to avoid being clobbered by their write\n            tree = {}\n            for comment in comments:\n                tree.setdefault(comment.parent_id, []).append(comment._id)\n\n            cls._write_tree(link, tree, lock)\n\n\nclass CommentTree:\n    def __init__(self, link, cids, tree, depth, parents, num_children):\n        self.link = link\n        self.cids = cids\n        self.tree = tree\n        self.depth = depth\n        self.parents = parents\n        self.num_children = num_children\n\n    @classmethod\n    def by_link(cls, link, timer=None):\n        if timer is None:\n            timer = SimpleSillyStub()\n\n        pieces = CommentTreePermacache.get_tree_pieces(link, timer)\n        cids, tree, depth, parents, num_children = pieces\n        comment_tree = cls(link, cids, tree, depth, parents, num_children)\n        return comment_tree\n\n    @classmethod\n    def on_new_link(cls, link):\n        CommentTreePermacache.prepare_new_storage(link)\n\n    @classmethod\n    def add_comments(cls, link, comments):\n        CommentTreePermacache.add_comments(link, comments)\n\n    @classmethod\n    def rebuild(cls, link):\n        # retrieve all the comments for the link\n        q = Comment._query(\n            Comment.c.link_id == link._id,\n            Comment.c._deleted == (True, False),\n            Comment.c._spam == (True, False),\n            optimize_rules=True,\n        )\n        comments = list(q)\n\n        # remove any comments with missing parents\n        comment_ids = {comment._id for comment in comments}\n        comments = [\n            comment for comment in comments\n            if not comment.parent_id or comment.parent_id in comment_ids \n        ]\n\n        CommentTreePermacache.rebuild(link, comments)\n\n        link.num_comments = sum(1 for c in comments if not c._deleted)\n        link._commit()\n"
  },
  {
    "path": "r2/r2/models/flair.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport uuid\n\nfrom pylons import app_globals as g\n\nfrom r2.lib.db import tdb_cassandra\nfrom r2.lib.db.thing import Relation\nfrom r2.lib.db.userrel import UserRel\nfrom r2.lib.utils import to36\nfrom r2.models import Account, Subreddit\n\n\nUSER_FLAIR = 'USER_FLAIR'\nLINK_FLAIR = 'LINK_FLAIR'\n\n\nclass Flair(Relation(Subreddit, Account)):\n    _cache = g.thingcache\n\n    @classmethod\n    def _cache_prefix(cls):\n        return \"flair:\"\n\n\nSubreddit.__bases__ += (\n    UserRel(\n        name='flair',\n        relation=Flair,\n        disable_ids_fn=True,\n        disable_reverse_ids_fn=True,\n    ),\n)\n\n\nclass FlairTemplate(tdb_cassandra.Thing):\n    \"\"\"A template for some flair.\"\"\"\n    _defaults = dict(text='',\n                     css_class='',\n                     text_editable=False,\n                    )\n\n    _bool_props = ('text_editable',)\n\n    _use_db = True\n    _connection_pool = 'main'\n\n    @classmethod\n    def _new(cls, text='', css_class='', text_editable=False):\n        if text is None:\n            text = ''\n        if css_class is None:\n            css_class = ''\n        ft = cls(text=text, css_class=css_class, text_editable=text_editable)\n        ft._commit()\n        return ft\n\n    def _commit(self, *a, **kw):\n        # Make sure an _id is always assigned before committing.\n        if not self._id:\n            self._id = str(uuid.uuid1())\n        return tdb_cassandra.Thing._commit(self, *a, **kw)\n\n    def covers(self, other_template):\n        \"\"\"Returns true if other_template is a subset of this one.\n\n        The value for other_template may be another FlairTemplate, or a tuple\n        of (text, css_class). The latter case is treated like a FlairTemplate\n        that doesn't permit editable text.\n\n        For example, if self permits editable text, then this method will return\n        True as long as just the css_classes match. On the other hand, if self\n        doesn't permit editable text but other_template does, this method will\n        return False.\n        \"\"\"\n        if isinstance(other_template, FlairTemplate):\n            text_editable = other_template.text_editable\n            text, css_class = other_template.text, other_template.css_class\n        else:\n            text_editable = False\n            text, css_class = other_template\n\n        if self.css_class != css_class:\n            return False\n        return self.text_editable or (not text_editable and self.text == text)\n\n\nclass FlairTemplateBySubredditIndex(tdb_cassandra.Thing):\n    \"\"\"Lists of FlairTemplate IDs for a subreddit.\n\n    The FlairTemplate references are stored as an arbitrary number of attrs.\n    The lexicographical ordering of these attr names gives the ordering for\n    flair templates within the subreddit.\n    \"\"\"\n\n    MAX_FLAIR_TEMPLATES = 350\n\n    _int_props = ('sr_id',)\n    _use_db = True\n    _connection_pool = 'main'\n\n    _key_prefixes = {\n        USER_FLAIR: 'ft_',\n        LINK_FLAIR: 'link_ft_',\n    }\n\n    @classmethod\n    def _new(cls, sr_id, flair_type=USER_FLAIR):\n        idx = cls(_id=to36(sr_id), sr_id=sr_id)\n        idx._commit()\n        return idx\n\n    @classmethod\n    def by_sr(cls, sr_id, create=False):\n        try:\n            return cls._byID(to36(sr_id))\n        except tdb_cassandra.NotFound:\n            if create:\n                return cls._new(sr_id)\n            raise\n\n    @classmethod\n    def create_template(cls, sr_id, text='', css_class='', text_editable=False,\n                        flair_type=USER_FLAIR):\n        idx = cls.by_sr(sr_id, create=True)\n\n        if len(idx._index_keys(flair_type)) >= cls.MAX_FLAIR_TEMPLATES:\n            raise OverflowError\n\n        ft = FlairTemplate._new(text=text, css_class=css_class,\n                                text_editable=text_editable)\n        idx.insert(ft._id, flair_type=flair_type)\n        return ft\n\n    @classmethod\n    def get_template_ids(cls, sr_id, flair_type=USER_FLAIR):\n        try:\n            return list(cls.by_sr(sr_id).iter_template_ids(flair_type))\n        except tdb_cassandra.NotFound:\n            return []\n\n    @classmethod\n    def get_template(cls, sr_id, ft_id, flair_type=None):\n        if flair_type:\n            flair_types = [flair_type]\n        else:\n            flair_types = [USER_FLAIR, LINK_FLAIR]\n        for flair_type in flair_types:\n            if ft_id in cls.get_template_ids(sr_id, flair_type=flair_type):\n                return FlairTemplate._byID(ft_id)\n        return None\n\n    @classmethod\n    def clear(cls, sr_id, flair_type=USER_FLAIR):\n        try:\n            idx = cls.by_sr(sr_id)\n        except tdb_cassandra.NotFound:\n            # Everything went better than expected.\n            return\n\n        for k in idx._index_keys(flair_type):\n            del idx[k]\n            # TODO: delete the orphaned FlairTemplate row\n\n        idx._commit()\n\n    def _index_keys(self, flair_type):\n        keys = set(self._dirties.iterkeys())\n        keys |= frozenset(self._orig.iterkeys())\n        keys -= self._deletes\n        key_prefix = self._key_prefixes[flair_type]\n        return [k for k in keys if k.startswith(key_prefix)]\n\n    @classmethod\n    def _make_index_key(cls, position, flair_type):\n        return '%s%08d' % (cls._key_prefixes[flair_type], position)\n\n    def iter_template_ids(self, flair_type):\n        return (getattr(self, key)\n                for key in sorted(self._index_keys(flair_type)))\n\n    def insert(self, ft_id, position=None, flair_type=USER_FLAIR):\n        \"\"\"Insert template reference into index at position.\n\n        A position value of None means to simply append.\n        \"\"\"\n        ft_ids = list(self.iter_template_ids(flair_type))\n        if position is None:\n            position = len(ft_ids)\n        if position < 0 or position > len(ft_ids):\n            raise IndexError(position)\n        ft_ids.insert(position, ft_id)\n\n        # Rewrite ALL the things.\n        for k in self._index_keys(flair_type):\n            del self[k]\n        for i, ft_id in enumerate(ft_ids):\n            setattr(self, self._make_index_key(i, flair_type), ft_id)\n        self._commit()\n\n    def delete_by_id(self, ft_id, flair_type=None):\n        if flair_type:\n            flair_types = [flair_type]\n        else:\n            flair_types = [USER_FLAIR, LINK_FLAIR]\n        for flair_type in flair_types:\n            if self._delete_by_id(ft_id, flair_type):\n                return True\n        g.log.debug(\"couldn't find %s to delete\", ft_id)\n        return False\n\n    def _delete_by_id(self, ft_id, flair_type):\n        for key in self._index_keys(flair_type):\n            ft = getattr(self, key)\n            if ft == ft_id:\n                # TODO: delete the orphaned FlairTemplate row\n                g.log.debug('deleting ft %s (%s)', ft, key)\n                del self[key]\n                self._commit()\n                return True\n        return False\n"
  },
  {
    "path": "r2/r2/models/gold.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom r2.lib.db.tdb_sql import make_metadata, index_str, create_table\n\nimport json\nimport pytz\nimport uuid\n\nfrom pycassa import NotFoundException\nfrom pycassa.system_manager import ASCII_TYPE, INT_TYPE, TIME_UUID_TYPE, UTF8_TYPE\nfrom pycassa.util import convert_uuid_to_time\nfrom pylons import tmpl_context as c\nfrom pylons import app_globals as g\nfrom pylons.i18n import _, ungettext\nfrom datetime import datetime\nimport sqlalchemy as sa\nfrom sqlalchemy.exc import IntegrityError, OperationalError\nfrom sqlalchemy.ext.declarative import declarative_base\nfrom sqlalchemy.orm import scoped_session, sessionmaker\nfrom sqlalchemy.sql.expression import select\nfrom sqlalchemy.sql.functions import sum as sa_sum\n\nfrom r2.lib.utils import GoldPrice, randstr, to_date\nimport re\nfrom random import choice\nfrom time import time\n\nfrom r2.lib.db import tdb_cassandra\nfrom r2.lib.db.tdb_cassandra import NotFound, view_of\nfrom r2.models import Account\nfrom r2.models.subreddit import Frontpage\nfrom r2.models.wiki import WikiPage, WikiPageIniItem\nfrom r2.lib.memoize import memoize\n\nimport stripe\n\ngold_bonus_cutoff = datetime(2010,7,27,0,0,0,0,g.tz)\ngold_static_goal_cutoff = datetime(2013, 11, 7, tzinfo=g.display_tz)\n\nNON_REVENUE_STATUSES = (\"declined\", \"chargeback\", \"fudge\", \"invalid\",\n                        \"refunded\", \"reversed\")\n\nENGINE_NAME = 'authorize'\n\nENGINE = g.dbm.get_engine(ENGINE_NAME)\nMETADATA = make_metadata(ENGINE)\nTIMEZONE = pytz.timezone(\"America/Los_Angeles\")\n\nSession = scoped_session(sessionmaker(bind=ENGINE))\nBase = declarative_base(bind=ENGINE)\n\ngold_table = sa.Table('reddit_gold', METADATA,\n                      sa.Column('trans_id', sa.String, nullable = False,\n                                primary_key = True),\n                      # status can be: invalid, unclaimed, claimed\n                      sa.Column('status', sa.String, nullable = False),\n                      sa.Column('date', sa.DateTime(timezone=True),\n                                nullable = False,\n                                default = sa.func.now()),\n                      sa.Column('payer_email', sa.String, nullable = False),\n                      sa.Column('paying_id', sa.String, nullable = False),\n                      sa.Column('pennies', sa.Integer, nullable = False),\n                      sa.Column('secret', sa.String, nullable = True),\n                      sa.Column('account_id', sa.String, nullable = True),\n                      sa.Column('days', sa.Integer, nullable = True),\n                      sa.Column('subscr_id', sa.String, nullable = True),\n                      sa.Column('gilding_type', sa.String, nullable = True))\n\nindices = [index_str(gold_table, 'status', 'status'),\n           index_str(gold_table, 'date', 'date'),\n           index_str(gold_table, 'account_id', 'account_id'),\n           index_str(gold_table, 'secret', 'secret'),\n           index_str(gold_table, 'payer_email', 'payer_email'),\n           index_str(gold_table, 'subscr_id', 'subscr_id')]\ncreate_table(gold_table, indices)\n\n\nclass GoldRevenueGoalByDate(object):\n    __metaclass__ = tdb_cassandra.ThingMeta\n\n    _use_db = True\n    _cf_name = \"GoldRevenueGoalByDate\"\n    _read_consistency_level = tdb_cassandra.CL.ONE\n    _write_consistency_level = tdb_cassandra.CL.ALL\n    _extra_schema_creation_args = {\n        \"column_name_class\": UTF8_TYPE,\n        \"default_validation_class\": INT_TYPE,\n    }\n    _compare_with = UTF8_TYPE\n    _type_prefix = None\n\n    ROWKEY = '1'\n\n    @staticmethod\n    def _colkey(date):\n        return date.strftime(\"%Y-%m-%d\")\n\n    @classmethod\n    def set(cls, date, goal):\n        cls._cf.insert(cls.ROWKEY, {cls._colkey(date): int(goal)})\n\n    @classmethod\n    def get(cls, date):\n        \"\"\"Gets the goal for a date, or the nearest previous goal.\"\"\"\n        try:\n            colkey = cls._colkey(date)\n            col = cls._cf.get(\n                cls.ROWKEY,\n                column_reversed=True,\n                column_start=colkey,\n                column_count=1,\n            )\n            return col.values()[0]\n        except NotFoundException:\n            return None\n\n\nclass GildedCommentsByAccount(tdb_cassandra.DenormalizedRelation):\n    _use_db = True\n    _last_modified_name = 'Gilding'\n    _views = []\n\n    @classmethod\n    def value_for(cls, thing1, thing2):\n        return ''\n\n    @classmethod\n    def gild(cls, user, thing):\n        cls.create(user, [thing])\n\n\nclass GildedLinksByAccount(tdb_cassandra.DenormalizedRelation):\n    _use_db = True\n    _last_modified_name = 'Gilding'\n    _views = []\n\n    @classmethod\n    def value_for(cls, thing1, thing2):\n        return ''\n\n    @classmethod\n    def gild(cls, user, thing):\n        cls.create(user, [thing])\n\n\n@view_of(GildedCommentsByAccount)\n@view_of(GildedLinksByAccount)\nclass GildingsByThing(tdb_cassandra.View):\n    _use_db = True\n    _extra_schema_creation_args = {\n        \"key_validation_class\": UTF8_TYPE,\n        \"column_name_class\": UTF8_TYPE,\n    }\n\n    @classmethod\n    def get_gilder_ids(cls, thing):\n        columns = cls.get_time_sorted_columns(thing._fullname)\n        return [int(account_id, 36) for account_id in columns.iterkeys()]\n\n    @classmethod\n    def create(cls, user, things):\n        for thing in things:\n            cls._set_values(thing._fullname, {user._id36: \"\"})\n\n    @classmethod\n    def delete(cls, user, things):\n        # gildings cannot be undone\n        raise NotImplementedError()\n\n\n@view_of(GildedCommentsByAccount)\n@view_of(GildedLinksByAccount)\nclass GildingsByDay(tdb_cassandra.View):\n    _use_db = True\n    _compare_with = TIME_UUID_TYPE\n    _extra_schema_creation_args = {\n        \"key_validation_class\": ASCII_TYPE,\n        \"column_name_class\": TIME_UUID_TYPE,\n        \"default_validation_class\": UTF8_TYPE,\n    }\n\n    @staticmethod\n    def _rowkey(date):\n        return date.strftime(\"%Y-%m-%d\")\n\n    @classmethod\n    def get_gildings(cls, date):\n        key = cls._rowkey(date)\n        columns = cls.get_time_sorted_columns(key)\n        gildings = []\n        for name, json_blob in columns.iteritems():\n            timestamp = convert_uuid_to_time(name)\n            date = datetime.utcfromtimestamp(timestamp).replace(tzinfo=g.tz)\n\n            gilding = json.loads(json_blob)\n            gilding[\"date\"] = date\n            gilding[\"user\"] = int(gilding[\"user\"], 36)\n            gildings.append(gilding)\n        return gildings\n\n    @classmethod\n    def create(cls, user, things):\n        key = cls._rowkey(datetime.now(g.tz))\n\n        columns = {}\n        for thing in things:\n            columns[uuid.uuid1()] = json.dumps({\n                \"user\": user._id36,\n                \"thing\": thing._fullname,\n            })\n        cls._set_values(key, columns)\n\n    @classmethod\n    def delete(cls, user, things):\n        # gildings cannot be undone\n        raise NotImplementedError()\n\n\ndef create_unclaimed_gold (trans_id, payer_email, paying_id,\n                           pennies, days, secret, date,\n                           subscr_id = None):\n\n    try:\n        gold_table.insert().execute(trans_id=str(trans_id),\n                                    subscr_id=subscr_id,\n                                    status=\"unclaimed\",\n                                    payer_email=payer_email,\n                                    paying_id=paying_id,\n                                    pennies=pennies,\n                                    days=days,\n                                    secret=str(secret),\n                                    date=date\n                                    )\n    except IntegrityError:\n        rp = gold_table.update(\n            sa.and_(gold_table.c.status == 'uncharged',\n                    gold_table.c.trans_id == str(trans_id)),\n            values = {\n                gold_table.c.status: \"unclaimed\",\n                gold_table.c.payer_email: payer_email,\n                gold_table.c.paying_id: paying_id,\n                gold_table.c.pennies: pennies,\n                gold_table.c.days: days,\n                gold_table.c.secret:secret,\n                gold_table.c.subscr_id : subscr_id\n                },\n            ).execute()\n\n\ndef create_claimed_gold (trans_id, payer_email, paying_id,\n                         pennies, days, secret, account_id, date,\n                         subscr_id = None, status=\"claimed\"):\n    gold_table.insert().execute(trans_id=trans_id,\n                                subscr_id=subscr_id,\n                                status=status,\n                                payer_email=payer_email,\n                                paying_id=paying_id,\n                                pennies=pennies,\n                                days=days,\n                                secret=secret,\n                                account_id=account_id,\n                                date=date)\n\n\ndef create_gift_gold(giver_id, recipient_id, days, date,\n            signed, note=None, gilding_type=None):\n    trans_id = \"X%d%s-%s\" % (int(time()), randstr(2), 'S' if signed else 'A')\n    gold_table.insert().execute(\n        trans_id=trans_id,\n        status=\"gift\",\n        paying_id=giver_id,\n        payer_email='',\n        pennies=0,\n        days=days,\n        account_id=recipient_id,\n        date=date,\n        secret=note,\n        gilding_type=gilding_type,\n    )\n\n\ndef create_gold_code(trans_id, payer_email, paying_id, pennies, days, date):\n    if not trans_id:\n        trans_id = \"GC%d%s\" % (int(time()), randstr(2))\n\n    valid_chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'\n    # keep picking new codes until we find an unused one\n    while True:\n        code = randstr(10, alphabet=valid_chars)\n\n        s = sa.select([gold_table],\n                      sa.and_(gold_table.c.secret == code.lower(),\n                              gold_table.c.status == 'unclaimed'))\n        res = s.execute().fetchall()\n        if not res:\n            gold_table.insert().execute(\n                trans_id=trans_id,\n                status='unclaimed',\n                payer_email=payer_email,\n                paying_id=paying_id,\n                pennies=pennies,\n                days=days,\n                secret=code.lower(),\n                date=date)\n            return code\n\n\ndef account_by_payingid(paying_id):\n    s = sa.select([sa.distinct(gold_table.c.account_id)],\n                  gold_table.c.paying_id == paying_id)\n    res = s.execute().fetchall()\n\n    if len(res) != 1:\n        return None\n\n    return int(res[0][0])\n\n# returns None if the ID was never valid\n# returns \"already claimed\" if it's already been claimed\n# Otherwise, it's valid and the function claims it, returning a tuple with:\n#   * the number of days\n#   * the subscr_id, if any\ndef claim_gold(secret, account_id):\n    if not secret:\n        return None\n\n    # The donation email has the code at the end of the sentence,\n    # so they might get sloppy and catch the period or some whitespace.\n    secret = secret.strip(\". \")\n    secret = secret.replace(\"-\", \"\").lower()\n\n    rp = gold_table.update(sa.and_(gold_table.c.status == 'unclaimed',\n                                   gold_table.c.secret == secret),\n                           values = {\n                                      gold_table.c.status: 'claimed',\n                                      gold_table.c.account_id: account_id,\n                                    },\n                           ).execute()\n    if rp.rowcount == 0:\n        just_claimed = False\n    elif rp.rowcount == 1:\n        just_claimed = True\n    else:\n        raise ValueError(\"rowcount == %d?\" % rp.rowcount)\n\n    s = sa.select([gold_table.c.days, gold_table.c.subscr_id],\n                  gold_table.c.secret == secret,\n                  limit = 1)\n    rows = s.execute().fetchall()\n\n    if not rows:\n        return None\n    elif just_claimed:\n        return (rows[0].days, rows[0].subscr_id)\n    else:\n        return \"already claimed\"\n\ndef check_by_email(email):\n    s = sa.select([gold_table.c.status,\n                           gold_table.c.secret,\n                           gold_table.c.days,\n                           gold_table.c.account_id],\n                          gold_table.c.payer_email == email)\n    return s.execute().fetchall()\n\n\ndef has_prev_subscr_payments(subscr_id):\n    s = sa.select([gold_table], gold_table.c.subscr_id == subscr_id)\n    return bool(s.execute().fetchall())\n\n\ndef retrieve_gold_transaction(transaction_id):\n    s = sa.select([gold_table], gold_table.c.trans_id == transaction_id)\n    res = s.execute().fetchall()\n    if res:\n        return res[0]   # single row per transaction_id\n\n\ndef update_gold_transaction(transaction_id, status):\n    rp = gold_table.update(gold_table.c.trans_id == str(transaction_id),\n                           values={gold_table.c.status: status}).execute()\n\n\ndef transactions_by_user(user):\n    s = sa.select([gold_table], gold_table.c.account_id == str(user._id))\n    res = s.execute().fetchall()\n    return res\n\n\ndef gold_payments_by_user(user):\n    transactions = transactions_by_user(user)\n\n    # filter out received gifts\n    transactions = [trans for trans in transactions\n                          if not trans.trans_id.startswith(('X', 'M'))]\n\n    return transactions\n\n\ndef gold_received_by_user(user):\n    transactions = transactions_by_user(user)\n    transactions = [trans for trans in transactions\n                          if trans.trans_id.startswith('X')]\n    return transactions\n\n\ndef days_to_pennies(days):\n    if days < 366:\n        months = days / 31\n        return months * g.gold_month_price.pennies\n    else:\n        years = days / 366\n        return years * g.gold_year_price.pennies\n\n\ndef append_random_bottlecap_phrase(message):\n    \"\"\"Appends a random \"bottlecap\" phrase from the wiki page.\n\n    The wiki page should be an unordered list with each item a separate\n    bottlecap.\n    \"\"\"\n\n    bottlecap = None\n    try:\n        wp = WikiPage.get(Frontpage, g.wiki_page_gold_bottlecaps)\n\n        split_list = re.split('^[*-] ', wp.content, flags=re.MULTILINE)\n        choices = [item.strip() for item in split_list if item.strip()]\n        if len(choices):\n            bottlecap = choice(choices)\n    except NotFound:\n        pass\n\n    if bottlecap:\n        message += '\\n\\n> ' + bottlecap\n    return message\n\n\ndef gold_revenue_multi(dates):\n    date_expr = sa.func.date_trunc('day',\n                    sa.func.timezone(TIMEZONE.zone, gold_table.c.date))\n    query = (select([date_expr, sa_sum(gold_table.c.pennies)])\n                .where(~ gold_table.c.status.in_(NON_REVENUE_STATUSES))\n                .where(date_expr.in_(dates))\n                .group_by(date_expr)\n            )\n    return {truncated_time.date(): pennies\n                for truncated_time, pennies in ENGINE.execute(query)}\n\n\n@memoize(\"gold-revenue-volatile\", time=600, stale=True)\ndef gold_revenue_volatile(date):\n    return gold_revenue_multi([date]).get(date, 0)\n\n\n@memoize(\"gold-revenue-steady\", stale=True)\ndef gold_revenue_steady(date):\n    return gold_revenue_multi([date]).get(date, 0)\n\n\n@memoize(\"gold-goal\", stale=True)\ndef gold_goal_on(date):\n    \"\"\"Returns the gold revenue goal (in pennies) for a given date.\"\"\"\n    goal = GoldRevenueGoalByDate.get(date)\n\n    if not goal:\n        return 0\n\n    return float(goal)\n\n\ndef account_from_stripe_customer_id(stripe_customer_id):\n    q = Account._query(Account.c.gold_subscr_id == stripe_customer_id,\n                       Account.c._spam == (True, False), data=True)\n    return next(iter(q), None)\n\n\n@memoize(\"subscription-details\", time=60)\ndef _get_subscription_details(stripe_customer_id):\n    stripe.api_key = g.secrets['stripe_secret_key']\n    customer = stripe.Customer.retrieve(stripe_customer_id)\n\n    if getattr(customer, 'deleted', False):\n        return {}\n\n    subscription = customer.subscription\n    card = customer.active_card\n    end = datetime.fromtimestamp(subscription.current_period_end).date()\n    last4 = card.last4\n    pennies = subscription.plan.amount\n\n    return {\n        'next_charge_date': end,\n        'credit_card_last4': last4,\n        'pennies': pennies,\n    }\n\n\ndef get_subscription_details(user):\n    if not getattr(user, 'gold_subscr_id', None):\n        return\n\n    return _get_subscription_details(user.gold_subscr_id)\n\n\ndef paypal_subscription_url():\n    return \"https://www.paypal.com/cgi-bin/webscr?cmd=_subscr-find&alias=%s\" % g.goldpayment_email\n\n\ndef get_discounted_price(gold_price):\n    discount = float(getattr(g, 'BTC_DISCOUNT', '0'))\n    price = (gold_price.pennies * (1 - discount)) / 100.\n    return GoldPrice(\"%.2f\" % price)\n\n\ndef make_gold_message(thing, user_gilded):\n    from r2.models import Comment\n\n    if thing.gildings == 0 or thing._spam or thing._deleted:\n        return None\n\n    author = Account._byID(thing.author_id, data=True)\n    if not author._deleted:\n        author_name = author.name\n    else:\n        author_name = _(\"[deleted]\")\n\n    if c.user_is_loggedin and thing.author_id == c.user._id:\n        if isinstance(thing, Comment):\n            gilded_message = ungettext(\n                \"a redditor gifted you a month of reddit gold for this \"\n                \"comment.\",\n                \"redditors have gifted you %(months)d months of reddit gold \"\n                \"for this comment.\",\n                thing.gildings\n            )\n        else:\n            gilded_message = ungettext(\n                \"a redditor gifted you a month of reddit gold for this \"\n                \"submission.\",\n                \"redditors have gifted you %(months)d months of reddit gold \"\n                \"for this submission.\",\n                thing.gildings\n            )\n    elif user_gilded:\n        if isinstance(thing, Comment):\n            gilded_message = ungettext(\n                \"you have gifted reddit gold to %(recipient)s for this \"\n                \"comment.\",\n                \"you and other redditors have gifted %(months)d months of \"\n                \"reddit gold to %(recipient)s for this comment.\",\n                thing.gildings\n            )\n        else:\n            gilded_message = ungettext(\n                \"you have gifted reddit gold to %(recipient)s for this \"\n                \"submission.\",\n                \"you and other redditors have gifted %(months)d months of \"\n                \"reddit gold to %(recipient)s for this submission.\",\n                thing.gildings\n            )\n    else:\n        if isinstance(thing, Comment):\n            gilded_message = ungettext(\n                \"a redditor has gifted reddit gold to %(recipient)s for this \"\n                \"comment.\",\n                \"redditors have gifted %(months)d months of reddit gold to \"\n                \"%(recipient)s for this comment.\",\n                thing.gildings\n            )\n        else:\n            gilded_message = ungettext(\n                \"a redditor has gifted reddit gold to %(recipient)s for this \"\n                \"submission.\",\n                \"redditors have gifted %(months)d months of reddit gold to \"\n                \"%(recipient)s for this submission.\",\n                thing.gildings\n            )\n\n    return gilded_message % dict(\n        recipient=author_name,\n        months=thing.gildings,\n    )\n\n\ndef creddits_lock(user):\n    return g.make_lock(\"gold_creddits\", \"creddits_%s\" % user._id)\n\n\nPENNIES_PER_SERVER_SECOND = {\n    datetime.strptime(datestr, \"%Y/%m/%d\").date(): v\n    for datestr, v in g.live_config['pennies_per_server_second'].iteritems()\n}\n\n\ndef calculate_server_seconds(pennies, date):\n    cutoff_dates = sorted(PENNIES_PER_SERVER_SECOND.keys())\n    date = to_date(date)\n    key = max(filter(lambda cutoff_date: date >= cutoff_date, cutoff_dates))\n    rate = PENNIES_PER_SERVER_SECOND[key]\n\n    # for simplicity all payment processor fees are $0.30 + 2.9%\n    net_pennies = pennies * (1 - 0.029) - 30\n\n    return net_pennies / rate\n\n\ndef get_current_value_of_month():\n    price = g.gold_month_price.pennies\n    now = datetime.now(g.display_tz)\n    seconds = calculate_server_seconds(price, now)\n    return seconds\n\n\nclass StylesheetsEverywhere(WikiPageIniItem):\n    @classmethod\n    def _get_wiki_config(cls):\n        return Frontpage, g.wiki_page_stylesheets_everywhere\n\n    def __init__(self, id, tagline, thumbnail_url, preview_url, is_enabled=True):\n        self.id = id\n        self.tagline = tagline\n        self.thumbnail_url = thumbnail_url\n        self.preview_url = preview_url\n        self.is_enabled = is_enabled\n        self.checked = False\n"
  },
  {
    "path": "r2/r2/models/ip.py",
    "content": "import datetime\n\nfrom pycassa.batch import Mutator\nfrom pycassa.system_manager import ASCII_TYPE\nfrom pylons import app_globals as g\nimport pytz\n\nfrom r2.lib.contrib.ipaddress import ip_address\nfrom r2.lib.db import tdb_cassandra\n\n\n__all__ = [\"IPsByAccount\", \"AccountsByIP\"]\n\n\nCONNECTION_POOL = g.cassandra_pools['main']\n\n\nCF_TTL = datetime.timedelta(days=100).total_seconds()\n\n\nclass IPsByAccount(tdb_cassandra.View):\n\n    _use_db = True\n    _extra_schema_creation_args = {\n        \"key_validation_class\": ASCII_TYPE,\n        \"default_validation_class\": ASCII_TYPE,\n    }\n    _compare_with = tdb_cassandra.DateType()\n    _ttl = CF_TTL\n\n    @classmethod\n    def set(cls, account_id, ip, date=None):\n        if date is None:\n            date = datetime.datetime.now(g.tz)\n        cls._set_values(str(account_id), {date: ip})\n\n    @classmethod\n    def get(cls,\n            account_id,\n            column_start=None,\n            column_finish=None,\n            column_count=100,\n            column_reversed=True):\n        \"\"\"Get the last accessed times of an account by IP address.\n\n        Returns a list of dicts of the last accessed times of an account by\n        IP address, most recent first.\n\n        Example:\n\n            >>> IPsByAccount.get(52)\n            [\n                {datetime.datetime(2016, 1, 24, 6, 23, 0, 326000, tzinfo=<UTC>): '127.0.0.3'},\n                {datetime.datetime(2016, 1, 24, 6, 22, 58, 983000, tzinfo=<UTC>): '127.0.0.2'},\n            ]\n\n        Pagination is done based on the date of the entry.  For instance, to\n        continue getting results from the previous set:\n\n            >>> IPsByAccount.get(52, column_start=datetime.datetime(\n                    2016, 1, 24, 6, 22, 58, 983000))\n            [\n                {datetime.datetime(2016, 1, 24, 6, 21, 50, 121000, tzinfo=<UTC>): '127.0.0.1'},\n            ]\n        \"\"\"\n        column_start = column_start or \"\"\n        column_finish = column_finish or \"\"\n        results = []\n        query = tdb_cassandra.ColumnQuery(\n            cls, (str(account_id),),\n            column_start=column_start,\n            column_finish=column_finish,\n            column_count=column_count,\n            column_reversed=column_reversed)\n        for date_ip in query:\n            for dt, ip in date_ip.iteritems():\n                results.append({dt.replace(tzinfo=pytz.utc): ip})\n        return results\n\n\nclass AccountsByIP(tdb_cassandra.View):\n\n    _use_db = True\n    _extra_schema_creation_args = {\n        \"key_validation_class\": ASCII_TYPE,\n        \"default_validation_class\": ASCII_TYPE,\n    }\n    _compare_with = tdb_cassandra.DateType()\n    _ttl = CF_TTL\n\n    @classmethod\n    def set(cls, ip, account_id, date=None):\n        if date is None:\n            date = datetime.datetime.now(g.tz)\n        cls._set_values(ip, {date: str(account_id)})\n\n    @classmethod\n    def get(cls,\n            ip,\n            column_start=None,\n            column_finish=None,\n            column_count=100,\n            column_reversed=True):\n        \"\"\"Get the times an IP address has accessed various account IDs.\n\n        Returns a list of dicts of the times an IP address has accessed\n        various account IDs, most recent first:\n\n        Example:\n\n            >>> AccountsByIP.get('127.0.0.1')\n            [\n                {datetime.datetime(2016, 1, 22, 23, 28, 21, 286000, tzinfo=<UTC>): 52},\n                {datetime.datetime(2016, 1, 22, 23, 28, 24, 301000, tzinfo=<UTC>): 53},\n            ]\n\n        Pagination is also supported.  See the documentation for\n        ``IPsByAccount.get``.\n        \"\"\"\n        column_start = column_start or \"\"\n        column_finish = column_finish or \"\"\n        results = []\n        query = tdb_cassandra.ColumnQuery(\n            cls, (ip,),\n            column_start=column_start,\n            column_finish=column_finish,\n            column_count=column_count,\n            column_reversed=column_reversed)\n        for date_account in query:\n            for dt, account in date_account.iteritems():\n                results.append({dt.replace(tzinfo=pytz.utc): int(account)})\n        return results\n\n\ndef set_account_ip(account_id, ip, date=None):\n    \"\"\"Set an IP address as having accessed an account.\n\n    Updates all underlying datastores.\n    \"\"\"\n    # don't store private IPs, send event + string so we can investigate this\n    if ip_address(ip).is_private:\n        g.stats.simple_event('ip.private_ip_storage_prevented')\n        g.stats.count_string('private_ip_storage_prevented', ip)\n        return\n\n    if date is None:\n        date = datetime.datetime.now(g.tz)\n    m = Mutator(CONNECTION_POOL)\n    m.insert(IPsByAccount._cf, str(account_id), {date: ip}, ttl=CF_TTL)\n    m.insert(AccountsByIP._cf, ip, {date: str(account_id)}, ttl=CF_TTL)\n    m.send()\n"
  },
  {
    "path": "r2/r2/models/keyvalue.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport json\n\nfrom pycassa import NotFoundException\nfrom pycassa.system_manager import UTF8_TYPE\n\nfrom r2.lib.db.tdb_cassandra import ThingMeta\n\nNoDefault = object()\n\nclass KeyValueStore(object):\n    __metaclass__ = ThingMeta\n\n    _use_db = False\n    _cf_name = None\n    _type_prefix = None\n    _compare_with = UTF8_TYPE\n\n    _extra_schema_creation_args = dict(\n        key_validation_class=UTF8_TYPE,\n        default_validation_class=UTF8_TYPE,\n    )\n\n    @classmethod\n    def get(cls, key, default=NoDefault):\n        try:\n            return json.loads(cls._cf.get(key)[\"data\"])\n        except NotFoundException:\n            if default is not NoDefault:\n                return default\n            else:\n                raise\n\n    @classmethod\n    def set(cls, key, data):\n        cls._cf.insert(key, {\"data\": json.dumps(data)})\n\n\nclass NamedGlobals(KeyValueStore):\n    _use_db = True\n\n"
  },
  {
    "path": "r2/r2/models/last_modified.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport datetime\n\nfrom pylons import app_globals as g\nfrom pycassa.system_manager import ASCII_TYPE, DATE_TYPE\n\nfrom r2.lib.db import tdb_cassandra\nfrom r2.lib.utils import tup\n\n\nclass LastModified(tdb_cassandra.View):\n    _use_db = True\n    _value_type = \"date\"\n    _connection_pool = \"main\"\n    _read_consistency_level = tdb_cassandra.CL.ONE\n    _extra_schema_creation_args = dict(key_validation_class=ASCII_TYPE,\n                                       default_validation_class=DATE_TYPE)\n\n    @classmethod\n    def touch(cls, fullname, names):\n        names = tup(names)\n        now = datetime.datetime.now(g.tz)\n        values = dict.fromkeys(names, now)\n        cls._set_values(fullname, values)\n        return now\n\n    @classmethod\n    def get(cls, fullname, name, touch_if_not_set=False):\n        try:\n            obj = cls._byID(fullname)\n        except tdb_cassandra.NotFound:\n            if touch_if_not_set:\n                time = cls.touch(fullname, name)\n                return time\n            else:\n                return None\n\n        return getattr(obj, name, None)\n\n    @classmethod\n    def get_multi(cls, fullnames, name):\n        res = cls._byID(fullnames, return_dict=True)\n\n        return dict((k, getattr(v, name, None))\n                    for k, v in res.iteritems())\n"
  },
  {
    "path": "r2/r2/models/link.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom pycassa.system_manager import ASCII_TYPE, UTF8_TYPE\nfrom pycassa.types import LongType\n\nfrom r2.config import feature\nfrom r2.lib.db.thing import (\n    Thing, Relation, NotFound, MultiRelation, CreationError)\nfrom r2.lib.db.operators import desc\nfrom r2.lib.errors import RedditError\nfrom r2.lib.tracking import (\n    get_site,\n)\nfrom r2.lib.utils import (\n    base_url,\n    domain,\n    epoch_timestamp,\n    feature_utils,\n    strip_www,\n    timesince,\n    title_to_url,\n    tup,\n    UrlParser,\n)\nfrom account import (\n    Account,\n    BlockedSubredditsByAccount,\n    DeletedUser,\n    SubredditParticipationByAccount,\n)\nfrom subreddit import (\n    DefaultSR,\n    DomainSR,\n    FakeSubreddit,\n    Subreddit,\n    SubredditsActiveForFrontPage,\n)\nfrom printable import Printable\nfrom r2.config import extensions\nfrom r2.lib.memoize import memoize\nfrom r2.lib.wrapped import Wrapped\nfrom r2.lib.filters import _force_utf8, _force_unicode\nfrom r2.lib import hooks, utils\nfrom mako.filters import url_escape\nfrom r2.lib.strings import strings, Score\nfrom r2.lib.db import tdb_cassandra, sorts\nfrom r2.lib.db.tdb_cassandra import view_of\nfrom r2.lib.utils import sanitize_url\nfrom r2.models.gold import (\n    GildedCommentsByAccount,\n    GildedLinksByAccount,\n    make_gold_message,\n)\nfrom r2.models.modaction import ModAction\nfrom r2.models.subreddit import MultiReddit\nfrom r2.models.trylater import TryLater\nfrom r2.models.query_cache import CachedQueryMutator\nfrom r2.models.promo import PROMOTE_STATUS\nfrom r2.models.vote import Vote\n\nfrom pylons import request\nfrom pylons import tmpl_context as c\nfrom pylons import app_globals as g\nfrom pylons.i18n import _\nfrom datetime import datetime, timedelta\nfrom hashlib import md5\nimport simplejson as json\n\nimport random, re\nimport pycassa\nfrom collections import defaultdict\nfrom itertools import cycle\nfrom pycassa.cassandra.ttypes import NotFoundException\nfrom pycassa.system_manager import (\n    ASCII_TYPE,\n    DOUBLE_TYPE,\n)\nimport pytz\n\nNOTIFICATION_EMAIL_DELAY = timedelta(hours=1)\n\nclass LinkExists(Exception): pass\n\n\nclass Link(Thing, Printable):\n    _cache = g.thingcache\n    _data_int_props = Thing._data_int_props + (\n        'num_comments', 'reported', 'gildings')\n    _defaults = dict(is_self=False,\n                     suggested_sort=None,\n                     over_18=False,\n                     over_18_override=False,\n                     reported=0, num_comments=0,\n                     moderator_banned=False,\n                     banned_before_moderator=False,\n                     media_object=None,\n                     secure_media_object=None,\n                     preview_object=None,\n                     media_url=None,\n                     gifts_embed_url=None,\n                     media_autoplay=False,\n                     domain_override=None,\n                     third_party_tracking=None,\n                     third_party_tracking_2=None,\n                     promoted=None,\n                     payment_flagged_reason=\"\",\n                     fraud=None,\n                     managed_promo=False,\n                     pending=False,\n                     disable_comments=False,\n                     locked=False,\n                     selftext='',\n                     sendreplies=True,\n                     ip='0.0.0.0',\n                     flair_text=None,\n                     flair_css_class=None,\n                     contest_mode=False,\n                     sticky_comment_id=None,\n                     ignore_reports=False,\n                     gildings=0,\n                     mobile_ad_url=\"\",\n                     admin_takedown=False,\n                     removed_link_child=None,\n                     precomputed_sorts=None,\n                     )\n    _essentials = ('sr_id', 'author_id')\n    _nsfw = re.compile(r\"\\bnsf[wl]\\b\", re.I)\n\n    SELFTEXT_MAX_LENGTH = 40000\n\n    is_votable = True\n\n    @classmethod\n    def _cache_prefix(cls):\n        return \"link:\"\n\n    def __init__(self, *a, **kw):\n        Thing.__init__(self, *a, **kw)\n\n    @property\n    def affects_karma_type(self):\n        if self.is_self:\n            return \"self\"\n\n        return \"link\"\n\n    @property\n    def body(self):\n        if self.is_self:\n            return self.selftext\n        else:\n            raise AttributeError\n\n    @property\n    def has_thumbnail(self):\n        return self._t.get('has_thumbnail', hasattr(self, 'thumbnail_url'))\n\n    @property\n    def is_nsfw(self):\n        return self.over_18\n\n    @property\n    def is_embeddable(self):\n        return not self.is_nsfw and self.subreddit_slow.is_embeddable\n\n    @classmethod\n    def _by_url(cls, url, sr):\n        if isinstance(sr, FakeSubreddit):\n            sr = None\n\n        link_ids = LinksByUrlAndSubreddit.get_link_ids(url, sr)\n        links = Link._byID(link_ids, data=True, return_dict=False)\n        links = [l for l in links if not l._deleted]\n\n        if not links:\n            raise NotFound('Link \"%s\"' % url)\n\n        return links\n\n    def already_submitted_link(self, url, title):\n        permalink = self.make_permalink_slow()\n        p = UrlParser(permalink)\n        p.update_query(already_submitted=\"true\", submit_url=url,\n                       submit_title=title)\n        return p.unparse()\n\n    @classmethod\n    def resubmit_link(cls, url, title):\n        p = UrlParser(\"/submit\")\n        p.update_query(resubmit=\"true\", url=url, title=title)\n        return p.unparse()\n\n    @classmethod\n    def _submit(cls, is_self, title, content, author, sr, ip,\n                sendreplies=True):\n        from r2.lib.voting import cast_vote\n        from r2.models import admintools\n        from r2.models.comment_tree import CommentTree\n\n        if is_self:\n            url = \"self\"\n            selftext = content\n        else:\n            url = content\n            selftext = cls._defaults[\"selftext\"]\n\n        over_18 = False\n        if cls._nsfw.search(title):\n            over_18 = True\n\n        # determine whether the post should go straight into spam\n        spam = author._spam\n        if is_self:\n            spam_filter_level = sr.spam_selfposts\n        else:\n            spam_filter_level = sr.spam_links\n        if spam_filter_level == \"all\" and not sr.is_special(author):\n            spam = True\n\n        l = cls(\n            _ups=1,\n            title=title,\n            url=url,\n            selftext=selftext,\n            _spam=spam,\n            author_id=author._id,\n            sendreplies=sendreplies,\n            sr_id=sr._id,\n            lang=sr.lang,\n            ip=ip,\n            is_self=is_self,\n            over_18=over_18,\n        )\n\n        l._commit()\n        # Note: this does not provide atomicity, so for self posts when\n        # a request dies after the previous line but before the next line\n        # you will have a link whose URL is literally 'self'\n        if is_self:\n            l.url = l.make_permalink_slow()\n            l._commit()\n        else:\n            LinksByUrlAndSubreddit.add_link(l)\n\n        LinksByAccount.add_link(author, l)\n        SubredditParticipationByAccount.mark_participated(author, sr)\n        SubredditsActiveForFrontPage.mark_new_post(sr)\n        author.last_submit_time = int(epoch_timestamp(datetime.now(g.tz)))\n        author._commit()\n\n        # if the link is coming in removed, we still need to run it through\n        # admintools.spam() to set data properly and update queries\n        if l._spam:\n            if author._spam:\n                g.stats.simple_event('spam.autoremove.link')\n                reason = \"banned user\"\n            elif spam_filter_level == \"all\":\n                reason = \"subreddit setting\"\n\n            admintools.spam(l, banner=reason)\n\n        hooks.get_hook('link.new').call(link=l)\n\n        CommentTree.on_new_link(l)\n\n        cast_vote(author, l, Vote.DIRECTIONS.up)\n\n        return l\n\n    def set_content(self, is_self, content):\n        if not self.promoted:\n            raise ValueError(\"set_content is only supported for promoted links\")\n\n        was_self = self.is_self\n        self.is_self = is_self\n\n        if is_self:\n            if not was_self:\n                LinksByUrlAndSubreddit.remove_link(self)\n\n            self.url = self.make_permalink_slow()\n            self.selftext = content\n        else:\n            if not was_self:\n                LinksByUrlAndSubreddit.remove_link(self)\n\n            self.url = content\n            self.selftext = self._defaults.get(\"selftext\", \"\")\n            LinksByUrlAndSubreddit.add_link(self)\n\n        self._commit()\n\n    def _save(self, user, category=None):\n        LinkSavesByAccount._save(user, self, category)\n\n    def _unsave(self, user):\n        LinkSavesByAccount._unsave(user, self)\n\n    def _hide(self, user):\n        LinkHidesByAccount._hide(user, self)\n\n    def _unhide(self, user):\n        LinkHidesByAccount._unhide(user, self)\n\n    def _commit(self):\n        # If we've updated the (denormalized) preview object, we also need to\n        # update the metadata that keeps track of the denormalizations.\n        if 'preview_object' in self._dirties:\n            (old_val, val) = self._dirties['preview_object']\n            if old_val:\n                LinksByImage.remove_link(old_val['uid'], self)\n            if val:\n                LinksByImage.add_link(val['uid'], self)\n        Thing._commit(self)\n\n    def link_domain(self):\n        if self.is_self:\n            return 'self'\n        else:\n            return domain(self.url)\n\n    @property\n    def num_comments(self):\n        # Paper over obviously broken comment counts (those that are negative).\n        return max(self.__getattr__('num_comments'), 0)\n\n    def keep_item(self, wrapped):\n        user = c.user if c.user_is_loggedin else None\n        if not c.user_is_admin and self._deleted:\n            return False\n\n        is_mod = wrapped.subreddit.is_moderator(user)\n\n        if not (c.user_is_admin or (isinstance(c.site, DomainSR) and is_mod)):\n            if self._spam and (not user or\n                               (user and self.author_id != user._id)):\n                return False\n\n        if not (c.user_is_admin or is_mod) and wrapped.enemy:\n            return False\n\n        if user and not c.ignore_hide_rules:\n            if wrapped.hidden:\n                return False\n\n            # determine if the post can be auto-hidden due to voting/score\n            if self.author_id == user._id:\n                # not if it's the user's own post\n                allow_auto_hide = False\n            elif wrapped.stickied and not wrapped.different_sr:\n                # not if it's stickied and we're inside the subreddit\n                allow_auto_hide = False\n            else:\n                allow_auto_hide = True\n\n            if (allow_auto_hide and\n                    ((user.pref_hide_ups and wrapped.likes == True) or\n                     (user.pref_hide_downs and wrapped.likes == False) or\n                     wrapped.score < user.pref_min_link_score)):\n                return False\n\n        # show NSFW to API and RSS users unless obey_over18=true\n        is_api = c.render_style in extensions.API_TYPES\n        is_rss = c.render_style in extensions.RSS_TYPES\n        if (is_api or is_rss) and not c.obey_over18:\n            return True\n\n        is_nsfw = wrapped.over_18 or wrapped.subreddit.over_18\n        return c.over18 or not is_nsfw\n\n    cache_ignore = {\n        'subreddit',\n        'num_comments',\n        'link_child',\n        'fresh',\n        'media_object',\n        'secure_media_object',\n    }.union(Printable.cache_ignore)\n\n    @staticmethod\n    def wrapped_cache_key(wrapped, style):\n        s = Printable.wrapped_cache_key(wrapped, style)\n        if wrapped.promoted is not None:\n            s.extend([\n                getattr(wrapped, \"promote_status\", -1),\n                getattr(wrapped, \"disable_comments\", False),\n                getattr(wrapped, \"media_override\", False),\n                c.user_is_sponsor,\n                wrapped.url,\n                repr(wrapped.title),\n            ])\n\n        if style == \"htmllite\":\n             s.extend([\n                 request.GET.has_key('twocolumn'),\n                 c.link_target,\n            ])\n        elif style == \"xml\":\n            s.append(request.GET.has_key(\"nothumbs\"))\n        elif style == \"compact\":\n            s.append(c.permalink_page)\n\n        # add link flair to the key if the user and site have enabled it and it\n        # exists\n        if (c.user.pref_show_link_flair and\n                c.site.link_flair_position and\n                (wrapped.flair_text or wrapped.flair_css_class)):\n            s.append(wrapped.flair_text)\n            s.append(wrapped.flair_css_class)\n            s.append(c.site.link_flair_position)\n\n        if wrapped.locked:\n            s.append('locked')\n\n        return s\n\n    def make_permalink(self, sr, force_domain=False):\n        from r2.lib.template_helpers import get_domain\n        p = \"comments/%s/%s/\" % (self._id36, title_to_url(self.title))\n        # promoted links belong to a separate subreddit and shouldn't\n        # include that in the path\n        if self.promoted is not None:\n            if force_domain:\n                permalink_domain = get_domain(subreddit=False)\n                res = \"%s://%s/%s\" % (g.default_scheme, permalink_domain, p)\n            else:\n                res = \"/%s\" % p\n        elif not force_domain:\n            res = \"/r/%s/%s\" % (sr.name, p)\n        elif sr != c.site or force_domain:\n            permalink_domain = get_domain(subreddit=False)\n            res = \"%s://%s/r/%s/%s\" % (g.default_scheme, permalink_domain,\n                                       sr.name, p)\n        else:\n            res = \"/%s\" % p\n\n        # WARNING: If we ever decide to add any ?foo=bar&blah parameters\n        # here, Comment.make_permalink will need to be updated or else\n        # it will fail.\n\n        return res\n\n    def make_canonical_link(self, sr, subdomain='www'):\n        domain = '%s.%s' % (subdomain, g.domain)\n        path = 'comments/%s/%s/' % (self._id36, title_to_url(self.title))\n        return '%s://%s/r/%s/%s' % (g.default_scheme, domain, sr.name, path)\n\n    def make_permalink_slow(self, force_domain=False):\n        return self.make_permalink(self.subreddit_slow,\n                                   force_domain=force_domain)\n\n    def markdown_link_slow(self):\n        title = _force_unicode(self.title)\n        title = title.replace(\"[\", r\"\\[\")\n        title = title.replace(\"]\", r\"\\]\")\n        return \"[%s](%s)\" % (title, self.make_permalink_slow())\n\n    @classmethod\n    def tracking_link(cls,\n                      link,\n                      wrapped_thing=None,\n                      element_name=None,\n                      context=None,\n                      site_name=None):\n        \"\"\"Add utm query parameters to reddit.com links to track navigation.\n\n        context => ?utm_medium (listing page, post listing on hybrid page)\n        site_name => ?utm_name (subreddit that user is currently browsing)\n        element_name => ?utm_content (what element leads to this link)\n        \"\"\"\n\n        if (c.user_is_admin or\n                not feature.is_enabled('utm_comment_links')):\n            return link\n\n        urlparser = UrlParser(link)\n        if not urlparser.path:\n            # `href=\"#some_anchor\"`\n            return link\n        if urlparser.scheme == 'javascript':\n            return link\n        if not urlparser.is_reddit_url():\n            return link\n\n        query_params = {}\n\n        query_params[\"utm_source\"] = \"reddit\"\n\n        if context is None:\n            if (hasattr(wrapped_thing, 'context') and\n                    wrapped_thing.context != cls.get_default_context()):\n                context = wrapped_thing.context\n            else:\n                context = request.route_dict[\"controller\"]\n        if context:\n            query_params[\"utm_medium\"] = context\n\n        if element_name:\n            query_params[\"utm_content\"] = element_name\n\n        if site_name is None:\n            site_name = get_site()\n        if site_name:\n            query_params[\"utm_name\"] = site_name\n\n        query_params = {k: v for (k, v) in query_params.iteritems() if (\n                        v is not None)}\n\n        if query_params:\n            urlparser.update_query(**query_params)\n            return urlparser.unparse()\n        return link\n\n    def _gild(self, user):\n        now = datetime.now(g.tz)\n\n        self._incr(\"gildings\")\n        self.subreddit_slow.add_gilding_seconds()\n\n        GildedLinksByAccount.gild(user, self)\n\n        from r2.lib.db import queries\n        with CachedQueryMutator() as m:\n            gilding = utils.Storage(thing=self, date=now)\n            m.insert(queries.get_all_gilded_links(), [gilding])\n            m.insert(queries.get_gilded_links(self.sr_id), [gilding])\n            m.insert(queries.get_gilded_user_links(self.author_id),\n                     [gilding])\n            m.insert(queries.get_user_gildings(user), [gilding])\n\n        hooks.get_hook('link.gild').call(link=self, gilder=user)\n\n    @staticmethod\n    def _should_expunge_selftext(link):\n        if not link._spam:\n            return False\n        if not c.user_is_loggedin:\n            return True\n        if c.user_is_admin:\n            return False\n        if c.user == link.author:\n            return False\n        if link.can_ban:\n            return False\n        return True\n\n    @classmethod\n    def update_nofollow(cls, user, wrapped):\n        user_is_loggedin = c.user_is_loggedin\n        for item in wrapped:\n            if user_is_loggedin and item.author_id == user._id:\n                item.nofollow = False\n            elif item._spam or item.author._spam:\n                item.nofollow = True\n            else:\n                item.nofollow = False\n\n        hooks.get_hook('link.update_nofollow').call(\n            user=user,\n            wrapped=wrapped,\n        )\n\n    @classmethod\n    def add_props(cls, user, wrapped):\n        from r2.lib.pages import make_link_child\n        from r2.lib.count import incr_counts\n        from r2.lib import media\n        from r2.lib.utils import timeago\n        from r2.lib.template_helpers import get_domain, unsafe, format_html\n        from r2.models.report import Report\n        from r2.lib.wrapped import CachedVariable\n\n        # referencing c's getattr is cheap, but not as cheap when it\n        # is in a loop that calls it 30 times on 25-200 things.\n        user_is_admin = c.user_is_admin\n        user_is_loggedin = c.user_is_loggedin\n        pref_media = user.pref_media\n        pref_media_preview = user.pref_media_preview\n        site = c.site\n\n        saved = hidden = visited = {}\n\n        if user_is_admin:\n            # Checking if a domain's banned isn't even cheap\n            urls = [item.url for item in wrapped if hasattr(item, 'url')]\n            # bans_for_domain_parts is just a generator; convert to a set for\n            # easy use of 'intersection'\n            from r2.models.admintools import bans_for_domain_parts\n            banned_domains = {ban.domain\n                              for ban in bans_for_domain_parts(urls)}\n\n        if user_is_loggedin:\n            gilded = [thing for thing in wrapped if thing.gildings > 0]\n            try:\n                user_gildings = GildedLinksByAccount.fast_query(user, gilded)\n            except tdb_cassandra.TRANSIENT_EXCEPTIONS as e:\n                g.log.warning(\"Cassandra gilding lookup failed: %r\", e)\n                user_gildings = {}\n\n            try:\n                saved = LinkSavesByAccount.fast_query(user, wrapped)\n                hidden = LinkHidesByAccount.fast_query(user, wrapped)\n\n                if user.gold and user.pref_store_visits:\n                    visited = LinkVisitsByAccount.fast_query(user, wrapped)\n\n            except tdb_cassandra.TRANSIENT_EXCEPTIONS as e:\n                # saved or hidden or may have been done properly, so go ahead\n                # with what we do have\n                g.log.warning(\"Cassandra save/hide/visited lookup failed: %r\", e)\n\n        # determine which subreddits the user could assign link flair in\n        if user_is_loggedin:\n            srs = {item.subreddit for item in wrapped\n                                  if item.subreddit.link_flair_position}\n            mod_flair_srids = {sr._id for sr in srs\n                               if (user_is_admin or\n                                   sr.is_moderator_with_perms(c.user, 'flair'))}\n            author_flair_srids = {sr._id for sr in srs\n                                  if sr.link_flair_self_assign_enabled}\n\n        if user_is_loggedin:\n            srs = {item.subreddit for item in wrapped}\n            is_moderator_srids = {sr._id for sr in srs if sr.is_moderator(user)}\n        else:\n            is_moderator_srids = set()\n\n        # set the nofollow state where needed\n        cls.update_nofollow(user, wrapped)\n\n        for item in wrapped:\n            show_media = False\n            if not hasattr(item, \"score_fmt\"):\n                item.score_fmt = Score.number_only\n            if c.render_style in ('compact', extensions.api_type(\"compact\")):\n                item.score_fmt = Score.safepoints\n            item.pref_compress = user.pref_compress\n            if user.pref_compress:\n                item.extra_css_class = \"compressed\"\n                item.score_fmt = Score.safepoints\n            elif pref_media == 'on' and not user.pref_compress:\n                show_media = True\n            elif pref_media == 'subreddit' and item.subreddit.show_media:\n                show_media = True\n            elif item.promoted and item.has_thumbnail:\n                if user_is_loggedin and item.author_id == user._id:\n                    show_media = True\n                elif pref_media != 'off' and not user.pref_compress:\n                    show_media = True\n\n            show_media_preview = False\n            if feature.is_enabled('autoexpand_media_previews'):\n                if pref_media_preview == \"on\":\n                    show_media_preview = True\n                elif pref_media_preview == \"subreddit\" and item.subreddit.show_media_preview:\n                    show_media_preview = True\n\n            item.over_18 = item.over_18 or item.subreddit.over_18\n            item.nsfw = item.over_18 and user.pref_label_nsfw\n\n            item.quarantine = item.subreddit.quarantine\n\n            item.is_author = (user == item.author)\n\n            item.thumbnail_sprited = False\n\n            if item.quarantine:\n                item.thumbnail = \"\"\n                item.preview_image = None\n            # always show a promo author their own thumbnail\n            elif item.promoted and (user_is_admin or item.is_author) and item.has_thumbnail:\n                item.thumbnail = media.thumbnail_url(item)\n                item.preview_image = getattr(item, 'preview_object', None)\n            elif user.pref_no_profanity and item.over_18 and not c.site.over_18:\n                if show_media:\n                    item.thumbnail = \"nsfw\"\n                    item.thumbnail_sprited = True\n                else:\n                    item.thumbnail = \"\"\n                item.preview_image = None\n            elif not show_media:\n                item.thumbnail = \"\"\n                item.preview_image = None\n            elif (item._deleted or\n                  item._spam and item._date < timeago(\"6 hours\")):\n                item.thumbnail = \"default\"\n                item.thumbnail_sprited = True\n                item.preview_image = None\n            elif item.has_thumbnail:\n                item.thumbnail = media.thumbnail_url(item)\n                item.preview_image = getattr(item, 'preview_object', None)\n            elif item.is_self:\n                item.thumbnail = \"self\"\n                item.thumbnail_sprited = True\n                item.preview_image = getattr(item, 'preview_object', None)\n            else:\n                item.thumbnail = \"default\"\n                item.thumbnail_sprited = True\n                item.preview_image = getattr(item, 'preview_object', None)\n\n            item.show_media_preview = show_media_preview\n\n            item.score = max(0, item.score)\n\n            if item.domain_override:\n                item.domain = item.domain_override\n            else:\n                item.domain = (domain(item.url) if not item.is_self\n                               else 'self.' + item.subreddit.name)\n\n            if user_is_loggedin:\n                item.user_gilded = (user, item) in user_gildings\n                item.saved = (user, item) in saved\n                item.hidden = (user, item) in hidden\n                item.visited = (user, item) in visited\n\n            else:\n                item.user_gilded = False\n                item.saved = item.hidden = item.visited = False\n\n            if c.permalink_page or c.profilepage:\n                item.gilded_message = make_gold_message(item, item.user_gilded)\n            else:\n                item.gilded_message = ''\n\n            item.can_gild = (\n                c.user_is_loggedin and\n                # you can't gild your own submission\n                not (item.author and\n                     item.author._id == user._id) and\n                # no point in showing the button for things you've already gilded\n                not item.user_gilded and\n                # ick, if the author deleted their account we shouldn't waste gold\n                not item.author._deleted and\n                # some subreddits can have gilding disabled\n                item.subreddit.allow_gilding and\n                not item._deleted\n            )\n\n            # set an attribute on the Wrapped item so that it will be\n            # added to the render cache key\n            if item.can_ban:\n                item.ignore_reports_key = item.ignore_reports\n\n            if c.user_is_loggedin and c.user.in_timeout:\n                item.mod_reports, item.user_reports = [], []\n            else:\n                item.mod_reports, item.user_reports = Report.get_reports(item)\n\n            item.num = None\n            item.permalink = item.make_permalink(item.subreddit)\n            if item.is_self:\n                item.url = item.make_permalink(item.subreddit,\n                                               force_domain=True)\n\n            if g.shortdomain:\n                item.shortlink = g.shortdomain + '/' + item._id36\n\n            item.domain_str = None\n            if c.user.pref_domain_details:\n                urlparser = UrlParser(item.url)\n                if (not item.is_self and urlparser.is_reddit_url() and\n                        urlparser.is_web_safe_url()):\n                    url_subreddit = urlparser.get_subreddit()\n                    if (url_subreddit and\n                            not isinstance(url_subreddit, DefaultSR)):\n                        item.domain_str = ('{0}/r/{1}'\n                                           .format(item.domain,\n                                                   url_subreddit.name))\n                elif isinstance(item.media_object, dict):\n                    try:\n                        author_url = item.media_object['oembed']['author_url']\n                        if domain(author_url) == item.domain:\n                            urlparser = UrlParser(author_url)\n                            item.domain_str = strip_www(urlparser.hostname)\n                            item.domain_str += urlparser.path\n                    except KeyError:\n                        pass\n\n                    if not item.domain_str:\n                        try:\n                            author = item.media_object['oembed']['author_name']\n                            author = _force_unicode(author)\n                            item.domain_str = (_force_unicode('{0}: {1}')\n                                               .format(item.domain, author))\n                        except KeyError:\n                            pass\n\n            if not item.domain_str:\n                item.domain_str = item.domain\n\n            item.user_is_moderator = item.sr_id in is_moderator_srids\n\n            # do we hide the score?\n            if user_is_admin:\n                item.hide_score = False\n            elif user_is_loggedin and item.user_is_moderator:\n                item.hide_score = False\n            elif item.promoted and item.score <= 0:\n                item.hide_score = True\n            elif user == item.author:\n                item.hide_score = False\n            elif item._date > timeago(\"2 hours\"):\n                item.hide_score = True\n            else:\n                item.hide_score = False\n\n            # is this link a member of a different (non-c.site) subreddit?\n            item.different_sr = (isinstance(site, FakeSubreddit) or\n                                 site.name != item.subreddit.name)\n\n            item.stickied = item.is_stickied(item.subreddit)\n\n            # we only want to style a sticky specially if we're inside the\n            # subreddit that it's stickied in (not in places like front page)\n            item.use_sticky_style = item.stickied and not item.different_sr\n\n            item.subreddit_path = item.subreddit.path\n            item.domain_path = \"/domain/%s/\" % item.domain\n            if item.is_self:\n                item.domain_path = item.subreddit_path\n\n            # attach video or selftext as needed\n            item.link_child, item.editable = make_link_child(item, show_media_preview)\n            item.feature_media_previews = feature.is_enabled(\"media_previews\")\n\n            if item.is_self and not item.promoted:\n                item.href_url = item.permalink\n            else:\n                item.href_url = item.url\n\n            item.fresh = not any((item.likes != None,\n                                  item.saved,\n                                  item.visited,\n                                  item.hidden,\n                                  item._deleted,\n                                  item._spam))\n\n            # bits that we will render stubs (to make the cached\n            # version more flexible)\n            item.num_text = CachedVariable(\"num\")\n            item.commentcls = CachedVariable(\"commentcls\")\n            item.comment_label = CachedVariable(\"numcomments\")\n            item.lastedited = CachedVariable(\"lastedited\")\n\n            item.as_deleted = False\n            if item.deleted and not c.user_is_admin:\n                item.author = DeletedUser()\n                item.as_deleted = True\n                item.selftext = '[deleted]'\n\n            item.archived = item.is_archived(item.subreddit)\n            item.votable = not item.archived\n\n            item.expunged = False\n            if item.is_self:\n                item.expunged = Link._should_expunge_selftext(item)\n\n            item.editted = getattr(item, \"editted\", False)\n\n            if user_is_loggedin:\n                can_mod_flair = item.subreddit._id in mod_flair_srids\n                can_author_flair = (item.is_author and\n                                    item.subreddit._id in author_flair_srids)\n                item.can_flair = can_mod_flair or can_author_flair\n            else:\n                item.can_flair = False\n\n            taglinetext = ''\n            if item.different_sr:\n                author_text = format_html(\" <span>%s</span>\",\n                                          _(\"by %(author)s to %(reddit)s\"))\n            else:\n                author_text = format_html(\" <span>%s</span>\",\n                                          _(\"by %(author)s\"))\n            if item.editted:\n                if item.score_fmt in (Score.points, Score.safepoints):\n                    taglinetext = format_html(\"<span>%s</span>\",\n                                              _(\"%(score)s submitted %(when)s \"\n                                                \"%(lastedited)s\"))\n                    taglinetext = unsafe(taglinetext + author_text)\n                elif item.different_sr:\n                    taglinetext = _(\"submitted %(when)s %(lastedited)s \"\n                                    \"by %(author)s to %(reddit)s\")\n                else:\n                    taglinetext = _(\"submitted %(when)s %(lastedited)s \"\n                                    \"by %(author)s\")\n            else:\n                if item.score_fmt in (Score.points, Score.safepoints):\n                    taglinetext = format_html(\"<span>%s</span>\",\n                                              _(\"%(score)s submitted %(when)s\"))\n                    taglinetext = unsafe(taglinetext + author_text)\n                elif item.different_sr:\n                    taglinetext = _(\"submitted %(when)s by %(author)s \"\n                                    \"to %(reddit)s\")\n                else:\n                    taglinetext = _(\"submitted %(when)s by %(author)s\")\n            item.taglinetext = taglinetext\n\n            if item.is_author:\n                item.should_incr_counts = False\n\n            if user_is_admin:\n                # Link notes\n                url = getattr(item, 'url')\n                # Pull just the relevant portions out of the url\n                urlf = sanitize_url(_force_unicode(url))\n                if urlf:\n                    urlp = UrlParser(urlf)\n                    hostname = urlp.hostname\n                    if hostname:\n                        parts = (hostname.encode(\"utf-8\").rstrip(\".\").\n                            split(\".\"))\n                        subparts = {\".\".join(parts[y:])\n                                    for y in xrange(len(parts))}\n                        if subparts.intersection(banned_domains):\n                            item.link_notes.append('banned domain')\n\n            # This is passed in promotedlink.html\n            item.ads_auction_enabled = feature.is_enabled('ads_auction')\n\n            if feature_utils.is_tracking_link_enabled(item):\n                # Split cache for template rendered with tracking link\n                item.use_tracking_link = True\n\n        if user_is_loggedin:\n            incr_counts(wrapped)\n\n\n        # Run this last\n        Printable.add_props(user, wrapped)\n\n    @classmethod\n    def get_default_context(cls):\n        return request.route_dict[\"action_name\"]\n\n    @property\n    def post_hint(self):\n        \"\"\"Returns a string that suggests the content of this link.\n\n        As a hint, this is lossy and may be inaccurate in some cases.\n\n        Currently one of:\n            * self\n            * video (a video file, like an mp4)\n            * image (an image file, like a gif or png)\n            * rich:video (a video embedded in HTML - like youtube or vimeo)\n            * link (catch-all)\n        \"\"\"\n        if self.is_self:\n            return 'self'\n\n        try:\n            oembed_type = self.media_object['oembed']['type']\n        except (KeyError, TypeError):\n            oembed_type = None\n\n        if oembed_type == 'photo':\n            return 'image'\n\n        if oembed_type == 'video':\n            return 'rich:video'\n\n        if oembed_type in {'link', 'rich'}:\n            return 'link'\n\n        p = UrlParser(self.url)\n        if p.has_image_extension():\n            return 'image'\n\n        if p.path_extension().lower() in {'mp4', 'webm'}:\n            return 'video'\n\n        return 'link'\n\n    @property\n    def subreddit_slow(self):\n        # The subreddit is often already on the wrapped link as .subreddit\n        # If available, that should be used instead of calling this\n        return Subreddit._byID(self.sr_id, stale=True)\n\n    @property\n    def author_slow(self):\n        \"\"\"Returns the link's author.\"\"\"\n        # The author is often already on the wrapped link as .author\n        # If available, that should be used instead of calling this\n        return Account._byID(self.author_id, data=True, return_dict=False)\n\n    @property\n    def responder_ids(self):\n        \"\"\"Returns an iterable of the OP and other official responders in a\n        thread.\n\n        Designed for Q&A-type threads (eg /r/iama).\n        \"\"\"\n        return (self.author_id,)\n\n    @property\n    def archived_slow(self):\n        sr = self.subreddit_slow\n        return self.is_archived(sr)\n\n    def is_archived(self, sr):\n        return self._age >= sr.archive_age\n\n    def can_view_promo(self, user):\n        if self.promoted:\n            # promos are visible only if the user is either the author\n            # or the link is live/previously live.\n            is_author = c.user_is_loggedin and user._id == self.author_id\n            return (c.user_is_sponsor or\n                    is_author or\n                    self.promote_status >= PROMOTE_STATUS.promoted)\n\n        # not a promo, therefore it is visible\n        # this preserves the original behavior of `visible_promo()`\n        return True\n\n    def can_comment_slow(self, user):\n        sr = self.subreddit_slow\n\n        if self.is_archived(sr):\n            return False\n\n        if self.locked and not sr.can_distinguish(user):\n            return False\n\n        return sr.can_comment(user) and self.can_view_promo(user)\n\n    def sort_if_suggested(self, sr=None):\n        \"\"\"Returns a sort, if the link or its subreddit has suggested one.\"\"\"\n        if self.suggested_sort == \"blank\":\n            # A suggested sort of \"blank\" means explicitly empty: Do not obey\n            # the subreddit's suggested sort, either.\n            return None\n\n        if self.suggested_sort:\n            return self.suggested_sort\n\n        sr = sr or self.subreddit_slow\n        if sr.suggested_comment_sort:\n            return sr.suggested_comment_sort\n\n        return None\n\n    def can_flair_slow(self, user):\n        \"\"\"Returns whether the specified user can flair this link\"\"\"\n        site = self.subreddit_slow\n        can_assign_own = (self.author_id == user._id and\n                          site.link_flair_self_assign_enabled)\n\n        return site.is_moderator_with_perms(user, 'flair') or can_assign_own\n\n    def set_flair(self, text=None, css_class=None, set_by=None):\n        self.flair_text = text\n        self.flair_css_class = css_class\n        self._commit()\n        self.update_search_index()\n\n        if set_by and set_by._id != self.author_id:\n            ModAction.create(self.subreddit_slow, set_by, action='editflair',\n                target=self, details='flair_edit')\n\n    def set_sticky_comment(self, comment, set_by=None):\n        \"\"\"Given a comment, set it as the sticky (top) comment for this link.\n\n        Only one comment may be stickied at a time, and stickied comments must\n        be top level. `set_by` is an optional Account, which if set will add\n        a ModAction event to the mod log.\n\n        Raises `RedditError` on an attempt to sticky non-top-level comments.\n        \"\"\"\n        if not comment.is_stickyable:\n            raise RedditError('COMMENT_NOT_STICKYABLE', code=400)\n\n        if self.sticky_comment_id == comment._id:\n            return\n\n        # Remove the current sticky if it exists before setting a new one\n        self.remove_sticky_comment(set_by=set_by)\n\n        self.sticky_comment_id = comment._id\n        self._commit()\n\n        if set_by:\n            ModAction.create(\n                self.subreddit_slow,\n                set_by,\n                action='sticky',\n                target=comment,\n            )\n\n    def remove_sticky_comment(self, comment=None, set_by=None):\n        \"\"\"Remove the sticky (top) comment for this link, if it exists.\n\n        `comment` is an optional argument that, if set, will ensure the\n        sticky comment matches. If it does not match, it will not remove. If\n        `comment` is unset, it will remove regardless of ID.\n\n        `set_by` is an optional Account, which if set will add a ModAction\n        event to the mod log.\n        \"\"\"\n        if self.sticky_comment_id is None:\n            return  # nothing to do\n\n        if comment and self.sticky_comment_id != comment._id:\n            return\n\n        prev_sticky_comment = Comment._byID(self.sticky_comment_id)\n\n        self.sticky_comment_id = None\n        self._commit()\n\n        if set_by:\n            ModAction.create(\n                self.subreddit_slow,\n                set_by,\n                action='unsticky',\n                target=prev_sticky_comment,\n            )\n\n    @classmethod\n    def _utf8_encode(cls, value):\n        \"\"\"\n        Returns a deep copy of the parameter, UTF-8-encoding any strings\n        encountered.\n        \"\"\"\n        if isinstance(value, dict):\n            return {cls._utf8_encode(key): cls._utf8_encode(value)\n                    for key, value in value.iteritems()}\n        elif isinstance(value, list):\n            return [cls._utf8_encode(item)\n                    for item in value]\n        elif isinstance(value, unicode):\n            return value.encode('utf-8')\n        else:\n            return value\n\n    # There's an issue where pickling fails for collections with string values\n    # that have unicode codepoints between 128 and 256.  Encoding the strings\n    # as UTF-8 before storing them works around this.\n    def set_media_object(self, value):\n        self.media_object = Link._utf8_encode(value)\n\n    def set_secure_media_object(self, value):\n        self.secure_media_object = Link._utf8_encode(value)\n\n    def set_preview_object(self, value):\n        self.preview_object = Link._utf8_encode(value)\n\n    def is_stickyable(self):\n        if self._deleted or self._spam:\n            return False\n\n        return True\n\n    @property\n    def is_stickied_slow(self):\n        return self.is_stickied(self.subreddit_slow)\n\n    def is_stickied(self, subreddit):\n        if not subreddit.sticky_fullnames:\n            return False\n\n        if self._fullname in subreddit.sticky_fullnames:\n            return True\n\n        return False\n\n\nclass LinksByUrlAndSubreddit(tdb_cassandra.View):\n    _use_db = True\n    _connection_pool = 'main'\n    _read_consistency_level = tdb_cassandra.CL.ONE\n    _compare_with = LongType()\n    _extra_schema_creation_args = {\n        \"key_validation_class\": UTF8_TYPE,\n    }\n\n    @classmethod\n    def make_canonical_url(cls, url):\n        if not utils.domain(url) in g.case_sensitive_domains:\n            keyurl = _force_utf8(UrlParser.base_url(url.lower()))\n        else:\n            # Convert only hostname to lowercase\n            up = UrlParser(url)\n            up.hostname = up.hostname.lower()\n            keyurl = _force_utf8(UrlParser.base_url(up.unparse()))\n\n        # Cassandra max key length is 65535, truncate url if it's near that\n        # (leaving some space for the prefix)\n        keyurl = keyurl[:65000]\n\n        return keyurl\n\n    @classmethod\n    def make_sr_rowkey(cls, canonical_url, sr_id):\n        return \"{sr}:{url}\".format(sr=sr_id, url=canonical_url)\n\n    @classmethod\n    def make_all_rowkey(cls, canonical_url):\n        return \"all:{url}\".format(url=canonical_url)\n\n    @classmethod\n    def add_link(cls, link):\n        canonical_url = cls.make_canonical_url(link.url)\n        sr_rowkey = cls.make_sr_rowkey(canonical_url, link.sr_id)\n        all_rowkey = cls.make_all_rowkey(canonical_url)\n        column = {link._id: \"\"}\n        cls._set_values(sr_rowkey, column)\n        cls._set_values(all_rowkey, column)\n\n    @classmethod\n    def remove_link(cls, link):\n        canonical_url = cls.make_canonical_url(link.url)\n        sr_rowkey = cls.make_sr_rowkey(canonical_url, link.sr_id)\n        all_rowkey = cls.make_all_rowkey(canonical_url)\n        column = {link._id: \"\"}\n        cls._remove(sr_rowkey, column)\n        cls._remove(all_rowkey, column)\n\n    @classmethod\n    def get_link_ids(cls, url, sr=None, limit=1000):\n        canonical_url = cls.make_canonical_url(url)\n        if sr:\n            rowkey = cls.make_sr_rowkey(canonical_url, sr._id)\n        else:\n            rowkey = cls.make_all_rowkey(canonical_url)\n        try:\n            columns = cls._cf.get(\n                rowkey, column_reversed=True, column_count=limit)\n        except tdb_cassandra.NotFoundException:\n            return []\n\n        link_ids = columns.keys()\n        return link_ids\n\n\n# Note that there are no instances of PromotedLink or LinkCompressed,\n# so overriding their methods here will not change their behaviour\n# (except for add_props). These classes are used to override the\n# render_class on a Wrapped to change the template used for rendering\n\nclass PromotedLink(Link):\n    _nodb = True\n\n    # embeds are editable by users (advertisers) so they can change and should\n    # be considered in the render cache key\n    cache_ignore = Link.cache_ignore - {\"media_object\", \"secure_media_object\"}\n\n    @classmethod\n    def add_props(cls, user, wrapped):\n        Link.add_props(user, wrapped)\n\n        for item in wrapped:\n            item.nofollow = True\n            item.user_is_sponsor = c.user_is_sponsor\n\n            status = \"promoted\"\n\n            # change the status class if the viewer is the author or a sponsor\n            if item.is_author or item.user_is_sponsor:\n                try:\n                    status = PROMOTE_STATUS.name[item.promote_status]\n                except (AttributeError, IndexError):\n                    pass\n\n            item.rowstyle_cls = \"link %s\" % status\n\n        # Run this last\n        Printable.add_props(user, wrapped)\n\n\nclass ReadNextLink(Link):\n    _nodb = True\n\n\nclass SearchResultLink(Link):\n    _nodb = True\n\n    @classmethod\n    def add_props(cls, user, wrapped):\n        Link.add_props(user, wrapped)\n        for item in wrapped:\n            url = UrlParser(item.permalink)\n            url.update_query(ref=\"search_posts\")\n            item.permalink = url.unparse()\n        Printable.add_props(user, wrapped)\n\n\nclass LegacySearchResultLink(Link):\n    _nodb = True\n\n    @classmethod\n    def add_props(cls, user, wrapped):\n        Link.add_props(user, wrapped)\n        for item in wrapped:\n            url = UrlParser(item.permalink)\n            url.update_query(ref=\"search_posts\")\n            item.permalink = url.unparse()\n            item.render_css_class = 'link'\n        Printable.add_props(user, wrapped)\n\n\nclass Comment(Thing, Printable):\n    _cache = g.thingcache\n    _data_int_props = Thing._data_int_props + ('reported', 'gildings')\n    _defaults = dict(\n        reported=0,\n        parent_id=None,\n        moderator_banned=False,\n        new=False,\n        gildings=0,\n        banned_before_moderator=False,\n        ignore_reports=False,\n        sendreplies=True,\n        admin_takedown=False,\n    )\n    _essentials = ('link_id', 'author_id')\n\n    is_votable = True\n\n    @classmethod\n    def _cache_prefix(cls):\n        return \"comment:\"\n\n    def _markdown(self):\n        pass\n\n    @property\n    def affects_karma_type(self):\n        return \"comment\"\n\n    @classmethod\n    def _new(cls, author, link, parent, body, ip):\n        from r2.lib.emailer import message_notification_email\n        from r2.lib.voting import cast_vote\n\n        subreddit = link.subreddit_slow\n\n        # determine whether the comment should go straight into spam\n        spam = False\n        if link.promoted and link.author_id == author._id:\n            # don't spam promoted link authors commenting on their own promos\n            spam = False\n        elif author._spam:\n            spam = True\n            g.stats.simple_event('spam.autoremove.comment')\n        elif (subreddit.spam_comments == \"all\" and\n                not subreddit.is_special(author)):\n            spam = True\n\n        comment = Comment(\n            _ups=1,\n            body=body,\n            link_id=link._id,\n            sr_id=link.sr_id,\n            author_id=author._id,\n            ip=ip,\n            _spam=spam,\n        )\n\n        # these props aren't relations\n        if parent:\n            comment.parent_id = parent._id\n\n        comment._commit()\n\n        link._incr('num_comments', 1)\n\n        to = None\n        name = 'inbox'\n        if parent and parent.sendreplies:\n            to = Account._byID(parent.author_id, True)\n        if not parent and not link._deleted and link.sendreplies:\n            to = Account._byID(link.author_id, True)\n            name = 'selfreply'\n\n        g.events.comment_event(comment, request=request, context=c)\n\n        cast_vote(author, comment, Vote.DIRECTIONS.up)\n\n        if link.num_comments < 20 or link.num_comments % 10 == 0:\n            # link's number of comments changed so re-index it, but don't bother\n            # re-indexing so often when it gets many comments\n            link.update_search_index(boost_only=True)\n\n        CommentsByAccount.add_comment(author, comment)\n        SubredditParticipationByAccount.mark_participated(author, subreddit)\n        author.last_comment_time = int(epoch_timestamp(datetime.now(g.tz)))\n        author._commit()\n\n        def should_send():\n            # don't send the message to author if replying to own comment\n            if author._id == to._id:\n                return False\n            # only global admins can be message spammed\n            if to.name in g.admins:\n                return True\n            # don't send the message if spam\n            # don't send the message if the recipient has blocked the author\n            if comment._spam or author._id in to.enemies:\n                return False\n            return True\n\n        inbox_rel = None\n        if to and should_send():\n            # Record the inbox relation and give the user an orangered\n            inbox_rel = Inbox._add(to, comment, name, orangered=True)\n\n            if to.pref_email_messages:\n                data = {\n                    'to': to._id36,\n                    'from': '/u/%s' % author.name,\n                    'comment': comment._fullname,\n                    'permalink': comment.make_permalink_slow(force_domain=True),\n                }\n                data = json.dumps(data)\n                TryLater.schedule('message_notification_email', data,\n                                  NOTIFICATION_EMAIL_DELAY)\n\n        hooks.get_hook('comment.new').call(comment=comment)\n\n        return (comment, inbox_rel)\n\n    def _save(self, user, category=None):\n        CommentSavesByAccount._save(user, self, category)\n\n    def _unsave(self, user):\n        CommentSavesByAccount._unsave(user, self)\n\n    @property\n    def link_slow(self):\n        \"\"\"Fetch a comment's Link and return it.\n\n        In most cases the Link is already on the wrapped comment (as .link),\n        and that should be used when possible.\n        \"\"\"\n        return Link._byID(self.link_id, data=True, return_dict=False)\n\n    @property\n    def subreddit_slow(self):\n        # When the Comment is Wrapped the subreddit is available as .subreddit\n        # and that should be used\n        return Subreddit._byID(self.sr_id, stale=True)\n\n    @property\n    def author_slow(self):\n        \"\"\"Returns the comment's author.\"\"\"\n        # The author is often already on the wrapped comment as .author\n        # If available, that should be used instead of calling this\n        return Account._byID(self.author_id, data=True, return_dict=False)\n\n    @property\n    def archived_slow(self):\n        sr = self.subreddit_slow\n        return self.is_archived(sr)\n\n    def is_archived(self, sr):\n        return self._age >= sr.archive_age\n\n    @property\n    def is_stickyable(self):\n        if self.parent_id is not None:\n            return False\n\n        return True\n\n    def keep_item(self, wrapped):\n        if c.user_is_admin:\n            return True\n\n        if c.user_is_loggedin:\n            if wrapped.subreddit.is_moderator(c.user):\n                return True\n            if wrapped.author_id == c.user._id:\n                return True\n            if wrapped.author_id in c.user.enemies:\n                return False\n\n        return True\n\n    cache_ignore = set((\n        \"subreddit\",\n        \"link\",\n        \"to\",\n        \"num_children\",\n        \"depth\",\n        \"child_ids\",\n    )).union(Printable.cache_ignore)\n\n    @staticmethod\n    def wrapped_cache_key(wrapped, style):\n        s = Printable.wrapped_cache_key(wrapped, style)\n        s.extend([wrapped.body])\n        s.extend([hasattr(wrapped, \"link\") and wrapped.link.contest_mode])\n        if hasattr(wrapped, \"link\") and wrapped.link.locked:\n            s.append('locked')\n        return s\n\n    def make_permalink(self, link, sr=None, context=None, anchor=False,\n                       force_domain=False):\n        url = link.make_permalink(sr, force_domain=force_domain) + self._id36\n        if context:\n            url += \"?context=%d\" % context\n        if anchor:\n            url += \"#%s\" % self._id36\n        return url\n\n    def make_permalink_slow(self, context=None, anchor=False,\n                            force_domain=False):\n        l = Link._byID(self.link_id, data=True)\n        return self.make_permalink(l, l.subreddit_slow,\n                                   context=context, anchor=anchor,\n                                   force_domain=force_domain)\n\n    def _gild(self, user):\n        now = datetime.now(g.tz)\n\n        self._incr(\"gildings\")\n        self.subreddit_slow.add_gilding_seconds()\n\n        GildedCommentsByAccount.gild(user, self)\n\n        from r2.lib.db import queries\n        with CachedQueryMutator() as m:\n            gilding = utils.Storage(thing=self, date=now)\n            m.insert(queries.get_all_gilded_comments(), [gilding])\n            m.insert(queries.get_gilded_comments(self.sr_id), [gilding])\n            m.insert(queries.get_gilded_user_comments(self.author_id),\n                     [gilding])\n            m.insert(queries.get_user_gildings(user), [gilding])\n\n        hooks.get_hook('comment.gild').call(comment=self, gilder=user)\n\n    def _qa(self, children, responder_ids):\n        \"\"\"Sort a comment according to the Q&A-type sort.\n\n        Arguments:\n\n        * children -- a list of the children of this comment.\n        * responder_ids -- a set of ids of users categorized as \"answerers\" for\n          this thread.\n        \"\"\"\n        # This sort type only makes sense for comments, unlike the other sorts\n        # that can be applied to any Things, which is why it's defined here\n        # instead of in Thing.\n\n        op_children = [c for c in children if c.author_id in responder_ids]\n        score = sorts.qa(self._ups, self._downs, len(self.body), op_children)\n\n        # When replies to a question, we want to rank OP replies higher than\n        # non-OP replies (generally).  This is a rough way to do so.\n        # Don't add extra scoring when we've already added it due to replies,\n        # though (because an OP responds to themselves).\n        if self.author_id in responder_ids and not op_children:\n            score *= 2\n\n        return score\n\n    @classmethod\n    def update_nofollow(cls, user, wrapped):\n        user_is_loggedin = c.user_is_loggedin\n        for item in wrapped:\n            if user_is_loggedin and item.author_id == user._id:\n                item.nofollow = False\n            elif item._spam or item.link._spam or item.author._spam:\n                item.nofollow = True\n            else:\n                item.nofollow = False\n\n        hooks.get_hook(\"comment.update_nofollow\").call(\n            user=user,\n            wrapped=wrapped,\n        )\n\n    @classmethod\n    def add_props(cls, user, wrapped):\n        from r2.lib.template_helpers import add_submitter_distinguish, get_domain\n        from r2.lib.utils import timeago\n        from r2.lib.wrapped import CachedVariable\n        from r2.lib.pages import WrappedUser\n        from r2.models.report import Report\n\n        #fetch parent links\n        links = Link._byID(set(l.link_id for l in wrapped), data=True,\n                           return_dict=True, stale=True)\n\n        # fetch authors\n        authors = Account._byID(set(l.author_id for l in links.values()), data=True,\n                                return_dict=True, stale=True)\n\n        #get srs for comments that don't have them (old comments)\n        for cm in wrapped:\n            if not hasattr(cm, 'sr_id'):\n                cm.sr_id = links[cm.link_id].sr_id\n\n        subreddits = {item.subreddit for item in wrapped}\n\n        if c.user_is_loggedin:\n            is_moderator_subreddits = {\n                sr._id for sr in subreddits if sr.is_moderator(user)}\n            can_reply_srs = set(\n                sr._id for sr in subreddits if sr.can_comment(user))\n            can_distinguish_srs = set(\n                sr._id for sr in subreddits if sr.can_distinguish(user))\n            promo_sr_id = Subreddit.get_promote_srid()\n            if promo_sr_id:\n                can_reply_srs.add(promo_sr_id)\n        else:\n            is_moderator_subreddits = set()\n            can_reply_srs = set()\n            can_distinguish_srs = set()\n\n        cids = dict((w._id, w) for w in wrapped)\n        parent_ids = set(cm.parent_id for cm in wrapped\n                         if getattr(cm, 'parent_id', None)\n                         and cm.parent_id not in cids)\n        parents = Comment._byID(\n            parent_ids, data=True, stale=True, ignore_missing=True)\n\n        profilepage = c.profilepage\n        user_is_admin = c.user_is_admin\n        user_is_loggedin = c.user_is_loggedin\n        focal_comment = c.focal_comment\n        site = c.site\n\n        if user_is_loggedin:\n            gilded = [thing for thing in wrapped if thing.gildings > 0]\n            try:\n                user_gildings = GildedCommentsByAccount.fast_query(user, gilded)\n            except tdb_cassandra.TRANSIENT_EXCEPTIONS as e:\n                g.log.warning(\"Cassandra gilding lookup failed: %r\", e)\n                user_gildings = {}\n\n            try:\n                saved = CommentSavesByAccount.fast_query(user, wrapped)\n            except tdb_cassandra.TRANSIENT_EXCEPTIONS as e:\n                g.log.warning(\"Cassandra comment save lookup failed: %r\", e)\n                saved = {}\n        else:\n            user_gildings = {}\n            saved = {}\n\n        for item in wrapped:\n            # for caching:\n            item.profilepage = c.profilepage\n            item.link = Wrapped(links.get(item.link_id))\n            item.link.author = authors.get(item.link.author_id)\n            item.show_admin_context = user_is_admin\n\n            if not hasattr(item, 'subreddit'):\n                item.subreddit = item.subreddit_slow\n\n        cls.update_nofollow(user, wrapped)\n\n        for item in wrapped:\n            if item.author_id == item.link.author_id and not item.link._deleted:\n                add_submitter_distinguish(item.attribs, item.link, item.subreddit)\n\n            if not hasattr(item, 'target'):\n                item.target = None\n\n            parent = None\n            if item.parent_id:\n                if item.parent_id in parents:\n                    parent = parents[item.parent_id]\n                elif item.parent_id in cids:\n                    parent = cids[item.parent_id]\n\n            if parent and not parent.deleted:\n                if item.parent_id in cids:\n                    # parent is displayed on the page, use an anchor tag\n                    item.parent_permalink = '#' + utils.to36(item.parent_id)\n                else:\n                    item.parent_permalink = parent.make_permalink(item.link, item.subreddit)\n            else:\n                item.parent_permalink = None\n\n            item.archived = item.is_archived(item.subreddit)\n\n            link_is_archived = item.link.is_archived(item.subreddit)\n            link_is_locked = item.link.locked\n            sr_can_distinguish = item.sr_id in can_distinguish_srs\n            sr_can_reply = item.sr_id in can_reply_srs\n\n            if user_is_loggedin:\n                item.can_reply = (\n                    not link_is_archived and\n                    (not link_is_locked or sr_can_distinguish) and\n                    sr_can_reply\n                )\n            else:\n                item.can_reply = not link_is_archived and not link_is_locked\n\n            item.can_embed = c.can_embed or False\n\n            if user_is_loggedin:\n                item.user_gilded = (user, item) in user_gildings\n                item.saved = (user, item) in saved\n            else:\n                item.user_gilded = False\n                item.saved = False\n            item.gilded_message = make_gold_message(item, item.user_gilded)\n\n            item.can_gild = (\n                # you can't gild your own comment\n                not (c.user_is_loggedin and\n                     item.author and\n                     item.author._id == user._id) and\n                # no point in showing the button for things you've already gilded\n                not item.user_gilded and\n                # ick, if the author deleted their account we shouldn't waste gold\n                not item.author._deleted and\n                # some subreddits can have gilding disabled\n                item.subreddit.allow_gilding\n            )\n\n            if c.user_is_loggedin and c.user.in_timeout:\n                item.mod_reports, item.user_reports = [], []\n            else:\n                item.mod_reports, item.user_reports = Report.get_reports(item)\n\n            # not deleted on profile pages,\n            # deleted if spam and not author or admin\n            item.deleted = (not profilepage and\n                           (item._deleted or\n                            (item._spam and\n                             item.author != user and\n                             not item.show_spam)))\n\n            item.have_form = not item.deleted\n\n            extra_css = ''\n            if item.deleted:\n                extra_css += \"grayed\"\n                if not user_is_admin:\n                    item.author = DeletedUser()\n                    item.gildings = 0\n                    item.distinguished = None\n                    # If removed by an admin or moderator, distinguish that\n                    # from being deleted by the user.\n                    if item._spam:\n                        item.body = '[removed]'\n                    else:\n                        item.body = '[deleted]'\n\n            if focal_comment == item._id36:\n                extra_css += \" border\"\n\n            if profilepage:\n                item.nsfw = user.pref_label_nsfw and (item.link.is_nsfw or item.subreddit.over_18)\n\n                link_author = item.link.author\n                if ((item.link._deleted or link_author._deleted) and\n                        not user_is_admin):\n                    link_author = DeletedUser()\n                item.link_author = WrappedUser(link_author)\n                item.full_comment_path = item.link.make_permalink(item.subreddit)\n                item.full_comment_count = item.link.num_comments\n\n                if item.sr_id == Subreddit.get_promote_srid():\n                    item.taglinetext = _(\"%(link)s by %(author)s [sponsored link]\")\n                else:\n                    item.taglinetext = _(\"%(link)s by %(author)s in %(subreddit)s\")\n\n            else:\n                # these aren't used so set them to constant values to avoid\n                # invalidating items in render cache\n                item.full_comment_path = ''\n                item.full_comment_count = 0\n\n            item.subreddit_path = item.subreddit.path\n\n            # always use the default collapse threshold in contest mode threads\n            # if the user has a custom collapse threshold\n            if (item.link.contest_mode and\n                    user.pref_min_comment_score is not None):\n                min_score = Account._defaults['pref_min_comment_score']\n            else:\n                min_score = user.pref_min_comment_score\n\n            item.collapsed = False\n            distinguished = item.distinguished and item.distinguished != \"no\"\n            prevent_collapse = profilepage or user_is_admin or distinguished\n\n            if (item.deleted and item.subreddit.collapse_deleted_comments and\n                    not prevent_collapse):\n                item.collapsed = True\n            elif item.score < min_score and not prevent_collapse:\n                item.collapsed = True\n                item.collapsed_reason = _(\"comment score below threshold\")\n            elif user_is_loggedin and item.author_id in c.user.enemies:\n                if \"grayed\" not in extra_css:\n                    extra_css += \" grayed\"\n                item.collapsed = True\n                item.collapsed_reason = _(\"blocked user\")\n\n            item.editted = getattr(item, \"editted\", False)\n\n            item.is_sticky = (item.link.sticky_comment_id == item._id)\n\n            item.render_css_class = \"comment\"\n\n            #will get updated in builder\n            item.num_children = 0\n            item.numchildren_text = CachedVariable(\"numchildren_text\")\n            item.score_fmt = Score.points\n            item.permalink = item.make_permalink(item.link, item.subreddit)\n\n            item.is_author = (user == item.author)\n            item.is_focal = (focal_comment == item._id36)\n\n            item.votable = item._age < item.subreddit.archive_age\n\n            hide_period = ('{0} minutes'\n                          .format(item.subreddit.comment_score_hide_mins))\n\n            if item.is_sticky or item.link.contest_mode:\n                item.score_hidden = True\n            elif item._date > timeago(hide_period):\n                item.score_hidden = not item.is_author\n            else:\n                item.score_hidden = False\n\n            item.user_is_moderator = item.sr_id in is_moderator_subreddits\n\n            if item.score_hidden and c.user_is_loggedin:\n                if c.user_is_admin or item.user_is_moderator:\n                    item.score_hidden = False\n\n            if item.score_hidden:\n                item.upvotes = 1\n                item.downvotes = 0\n                item.score = 1\n                item.voting_score = [1, 1, 1]\n                item.render_css_class += \" score-hidden\"\n\n            # in contest mode, use only upvotes for the score if the subreddit\n            # has been (manually) set to do so\n            if (item.link.contest_mode and\n                    item.subreddit.contest_mode_upvotes_only and\n                    not item.score_hidden):\n                item.score = item._ups\n                item.voting_score = [\n                    item.score - 1, item.score, item.score + 1]\n                item.collapsed = False\n\n            if item.is_author:\n                item.inbox_replies_enabled = item.sendreplies\n\n            #will seem less horrible when add_props is in pages.py\n            from r2.lib.pages import UserText\n            item.usertext = UserText(item, item.body,\n                                     editable=item.is_author,\n                                     nofollow=item.nofollow,\n                                     target=item.target,\n                                     extra_css=extra_css,\n                                     have_form=item.have_form)\n\n            item.lastedited = CachedVariable(\"lastedited\")\n\n        # Run this last\n        Printable.add_props(user, wrapped)\n\n    def update_search_index(self, boost_only=False):\n        # no-op because Comments are not indexed\n        return\n\n\nclass CommentScoresByLink(tdb_cassandra.View):\n    _use_db = True\n    _connection_pool = 'main'\n    _read_consistency_level = tdb_cassandra.CL.ONE\n    _fetch_all_columns = True\n\n    _extra_schema_creation_args = {\n        \"column_name_class\": ASCII_TYPE,\n        \"default_validation_class\": DOUBLE_TYPE,\n        \"key_validation_class\": ASCII_TYPE,\n    }\n    _value_type = \"bytes\"\n    _compare_with = ASCII_TYPE\n\n    @classmethod\n    def _rowkey(cls, link, sort):\n        assert sort.startswith('_')\n        return '%s%s' % (link._id36, sort)\n\n    @classmethod\n    def set_scores(cls, link, sort, scores_by_comment):\n        rowkey = cls._rowkey(link, sort)\n        cls._set_values(rowkey, scores_by_comment)\n\n    @classmethod\n    def get_scores(cls, link, sort):\n        rowkey = cls._rowkey(link, sort)\n        try:\n            return CommentScoresByLink._byID(rowkey)._values()\n        except tdb_cassandra.NotFound:\n            return {}\n\n\nclass MoreMessages(Printable):\n    cachable = False\n    display = \"\"\n    new = False\n    was_comment = False\n    is_collapsed = True\n\n    def __init__(self, parent, child):\n        self.parent = parent\n        self.child = child\n\n    @staticmethod\n    def wrapped_cache_key(item, style):\n        return False\n\n    @property\n    def _fullname(self):\n        return self.parent._fullname\n\n    @property\n    def _id36(self):\n        return self.parent._id36\n\n    @property\n    def _id(self):\n        return self.parent._id\n\n    @property\n    def subject(self):\n        return self.parent.subject\n\n    @property\n    def childlisting(self):\n        return self.child\n\n    @property\n    def to(self):\n        return self.parent.to\n\n    @property\n    def author(self):\n        return self.parent.author\n\n    @property\n    def user_is_recipient(self):\n        return self.parent.user_is_recipient\n\n    @property\n    def sr_id(self):\n        return self.parent.sr_id\n\n    @property\n    def subreddit(self):\n        return self.parent.subreddit\n\n    @property\n    def accent_color(self):\n        return getattr(self.parent, \"accent_color\", None)\n\n\nclass MoreComments(Printable):\n    cachable = False\n    display = \"\"\n\n    @staticmethod\n    def wrapped_cache_key(item, style):\n        return False\n\n    def __init__(self, link, depth, parent_id=None):\n        if parent_id is not None:\n            id36 = utils.to36(parent_id)\n            self.parent_id = parent_id\n            self.parent_name = \"t%s_%s\" % (utils.to36(Comment._type_id), id36)\n            self.parent_permalink = link.make_permalink_slow() + id36\n        self.link_name = link._fullname\n        self.link_id = link._id\n        self.depth = depth\n        self.children = []\n        self.count = 0\n\n    @property\n    def _fullname(self):\n        return \"t%s_%s\" % (utils.to36(Comment._type_id), self._id36)\n\n    @property\n    def _id36(self):\n        return utils.to36(self.children[0]) if self.children else '_'\n\n\nclass MoreRecursion(MoreComments):\n    pass\n\n\nclass MoreChildren(MoreComments):\n    def __init__(self, link, sort_operator, depth, parent_id=None):\n        from r2.lib.menus import CommentSortMenu\n        self.sort = CommentSortMenu.sort(sort_operator)\n        MoreComments.__init__(self, link, depth, parent_id)\n\n\nclass Message(Thing, Printable):\n    _cache = g.thingcache\n    _defaults = dict(reported=0,\n                     was_comment=False,\n                     parent_id=None,\n                     new=False,\n                     first_message=None,\n                     to_id=None,\n                     sr_id=None,\n                     to_collapse=None,\n                     author_collapse=None,\n                     from_sr=False,\n                     display_author=None,\n                     display_to=None,\n                     email_id=None,\n                     sent_via_email=False,\n                     del_on_recipient=False,\n                     )\n    _data_int_props = Thing._data_int_props + ('reported',)\n    _essentials = ('author_id',)\n    cache_ignore = set([\"to\", \"subreddit\"]).union(Printable.cache_ignore)\n\n    @classmethod\n    def _cache_prefix(cls):\n        return \"message:\"\n\n    @classmethod\n    def _new(cls, author, to, subject, body, ip, parent=None, sr=None,\n             from_sr=False, can_send_email=True, sent_via_email=False,\n             email_id=None):\n        from r2.lib.emailer import message_notification_email\n        from r2.lib.message_to_email import queue_modmail_email\n\n        m = Message(subject=subject, body=body, author_id=author._id, new=True,\n                    ip=ip, from_sr=from_sr, sent_via_email=sent_via_email,\n                    email_id=email_id)\n        m._spam = author._spam\n\n        if author._spam:\n            g.stats.simple_event('spam.autoremove.message')\n\n        sr_id = None\n        # check to see if the recipient is a subreddit and swap args accordingly\n        if to and isinstance(to, Subreddit):\n            if from_sr:\n                raise CreationError(\"Cannot send from SR to SR\")\n            to_subreddit = True\n            to, sr = None, to\n        else:\n            to_subreddit = False\n\n        if sr:\n            sr_id = sr._id\n\n        if parent:\n            m.parent_id = parent._id\n            if parent.first_message:\n                m.first_message = parent.first_message\n            else:\n                m.first_message = parent._id\n\n            if parent.sr_id:\n                sr_id = parent.sr_id\n\n            if parent.display_author and not getattr(parent, \"signed\", False):\n                m.display_to = parent.display_author\n\n        if not to and not sr_id:\n            raise CreationError(\"Message created with neither to nor sr_id\")\n        if from_sr and not sr_id:\n            raise CreationError(\"Message sent from_sr without setting sr\")\n\n        m.to_id = to._id if to else None\n        if sr_id is not None:\n            m.sr_id = sr_id\n\n        m._commit()\n\n        hooks.get_hook('message.new').call(message=m)\n\n        MessagesByAccount.add_message(author, m)\n\n        if sr_id and not sr:\n            sr = Subreddit._byID(sr_id)\n\n        if to_subreddit:\n            SubredditParticipationByAccount.mark_participated(author, sr)\n\n        if sr_id:\n            g.stats.simple_event(\"modmail.received_message\")\n\n        inbox_rel = []\n\n        inbox_hook = hooks.get_hook(\"message.skip_inbox\")\n        skip_inbox = inbox_hook.call_until_return(message=m)\n        if skip_inbox:\n            m._spam = True\n            m._commit()\n\n        if not skip_inbox and sr_id:\n            if parent or to_subreddit or from_sr:\n                inbox_rel.append(ModeratorInbox._add(sr, m))\n\n            if sr.is_moderator(author):\n                m.distinguished = 'yes'\n                m._commit()\n\n            if (can_send_email and\n                    sr.name in g.live_config['modmail_forwarding_email']):\n                queue_modmail_email(m)\n\n        if author.name in g.admins:\n            m.distinguished = 'admin'\n            m._commit()\n\n        # if there is a \"to\" we may have to create an inbox relation as well\n        # also, only global admins can be message spammed.\n        if not skip_inbox and to and (not m._spam or to.name in g.admins):\n            # if \"to\" is not a sr moderator they need to be notified\n            if not sr_id or not sr.is_moderator(to):\n                inbox_rel.append(Inbox._add(to, m, 'inbox'))\n\n                if (\n                    to.pref_email_messages and\n                    m.author_id not in to.enemies and\n                    to._id != m.author_id\n                ):\n                    from r2.lib.template_helpers import get_domain\n                    if from_sr:\n                        sender_name = '/r/%s' % sr.name\n                    else:\n                        sender_name = '/u/%s' % author.name\n                    permalink = 'http://%(domain)s%(path)s' % {\n                        'domain': get_domain(),\n                        'path': m.permalink,\n                    }\n                    data = {\n                        'to': to._id36,\n                        'from': sender_name,\n                        'comment': m._fullname,\n                        'permalink': permalink,\n                    }\n                    data = json.dumps(data)\n                    TryLater.schedule('message_notification_email', data,\n                                      NOTIFICATION_EMAIL_DELAY)\n\n        # update user inboxes for non-mods involved in a modmail conversation\n        if not skip_inbox and sr_id and m.first_message:\n            first_message = Message._byID(m.first_message, data=True)\n            first_sender = Account._byID(first_message.author_id, data=True)\n            first_sender_modmail = sr.is_moderator_with_perms(\n                first_sender, 'mail')\n\n            if (first_sender != author and\n                    first_sender != to and\n                    not first_sender_modmail):\n                inbox_rel.append(Inbox._add(first_sender, m, 'inbox'))\n\n            if first_message.to_id:\n                first_recipient = Account._byID(first_message.to_id, data=True)\n                first_recipient_modmail = sr.is_moderator_with_perms(\n                    first_recipient, 'mail')\n                if (first_recipient != author and\n                        first_recipient != to and\n                        not first_recipient_modmail):\n                    inbox_rel.append(Inbox._add(first_recipient, m, 'inbox'))\n\n        if sr_id:\n            g.events.modmail_event(m, request=request, context=c)\n        else:\n            g.events.message_event(m, request=request, context=c)\n\n        return (m, inbox_rel)\n\n    @property\n    def permalink(self):\n        return \"/message/messages/%s\" % self._id36\n\n    def make_permalink(self, force_domain=False):\n        from r2.lib.template_helpers import get_domain\n        p = self.permalink\n        if force_domain:\n            permalink_domain = get_domain(subreddit=False)\n            res = \"%s://%s%s\" % (g.default_scheme, permalink_domain, p)\n        else:\n            res = p\n        return res\n\n    def make_permalink_slow(self, context=None, anchor=False, force_domain=False):\n        return self.make_permalink(force_domain)\n\n    def can_view_slow(self):\n        if c.user_is_loggedin:\n            if (c.user_is_admin or\n                    c.user._id in (self.author_id, self.to_id)):\n                return True\n            elif self.sr_id:\n                sr = Subreddit._byID(self.sr_id, data=True, stale=True)\n\n                if sr.is_moderator_with_perms(c.user, 'mail'):\n                    return True\n                elif self.first_message:\n                    first = Message._byID(self.first_message, data=True)\n                    return c.user._id in (first.author_id, first.to_id)\n\n    def get_muted_user_in_conversation(self):\n        \"\"\"Return the muted user involved in a modmail conversation (if any).\"\"\"\n        if not self.sr_id:\n            return None\n\n        sr = self.subreddit_slow\n\n        if self.first_message:\n            first = Message._byID(self.first_message, data=True)\n        else:\n            first = self\n\n        first_author = first.author_slow\n        first_recipient = first.recipient_slow if first.to_id else None\n\n        if sr.is_muted(first_author):\n            return first_author\n        elif first_recipient and sr.is_muted(first_recipient):\n            return first_recipient\n        else:\n            return None\n\n    @classmethod\n    def add_props(cls, user, wrapped):\n        from r2.lib.db import queries\n\n        # make sure there is a sr_id set:\n        for w in wrapped:\n            if not hasattr(w, \"sr_id\"):\n                w.sr_id = None\n\n        to_ids = {w.to_id for w in wrapped if w.to_id}\n        other_account_ids = {w.display_author or w.display_to for w in wrapped\n            if not (w.was_comment or w.sr_id) and\n                (w.display_author or w.display_to)}\n        account_ids = to_ids | other_account_ids\n        accounts = Account._byID(account_ids, data=True)\n\n        link_ids = {w.link_id for w in wrapped if w.was_comment}\n        links = Link._byID(link_ids, data=True)\n\n        srs = {w.subreddit._id: w.subreddit for w in wrapped if w.sr_id}\n\n        parent_ids = {w.parent_id for w in wrapped\n            if w.parent_id and w.was_comment}\n        parents = Comment._byID(parent_ids, data=True)\n\n        # load full modlist for all subreddit messages\n        mods_by_srid = {sr._id: sr.moderator_ids() for sr in srs.itervalues()}\n        user_mod_sr_ids = {sr_id for sr_id, mod_ids in mods_by_srid.iteritems()\n            if user._id in mod_ids}\n\n        # special handling for mod replies to mod PMs\n        mod_message_authors = {}\n        mod_messages = [\n            item for item in wrapped\n            if (item.to_id is None and\n                    item.sr_id and\n                    item.parent_id and\n                    (c.user_is_admin or item.sr_id in user_mod_sr_ids))\n        ]\n        if mod_messages:\n            parent_ids = [item.parent_id for item in mod_messages]\n            parents = Message._byID(parent_ids, data=True, return_dict=True)\n            author_ids = {item.author_id for item in parents.itervalues()}\n            authors = Account._byID(author_ids, data=True, return_dict=True)\n\n            for item in mod_messages:\n                parent = parents[item.parent_id]\n                author = authors[parent.author_id]\n                mod_message_authors[item._id] = author\n\n        # load the unread list to determine message newness\n        unread = set(queries.get_unread_inbox(user))\n\n        # load the unread mod list for the same reason\n        mod_msg_srs = {srs[w.sr_id] for w in wrapped\n            if w.sr_id and not w.was_comment and w.sr_id in user_mod_sr_ids}\n        mod_unread = set(\n            queries.get_unread_subreddit_messages_multi(mod_msg_srs))\n\n        # load blocked subreddits\n        sr_blocks = BlockedSubredditsByAccount.fast_query(user, srs.values())\n        blocked_srids = {sr._id for _user, sr in sr_blocks.iterkeys()}\n\n        can_set_unread = (user.pref_mark_messages_read and\n                            c.extension not in (\"rss\", \"xml\", \"api\", \"json\"))\n        to_set_unread = []\n\n        # accent colors for color coded modmail\n        sr_colors = None\n        if isinstance(c.site, FakeSubreddit):\n            mod_sr_ids = Subreddit.reverse_moderator_ids(user)\n            if len(mod_sr_ids) > 1:\n                sr_colors = dict(zip(mod_sr_ids, cycle(Subreddit.ACCENT_COLORS)))\n\n        for item in wrapped:\n            user_is_recipient = item.to_id == user._id\n            user_is_sender = (item.author_id == user._id and\n                not getattr(item, \"display_author\", None))\n            sent_by_sr = item.sr_id and getattr(item, 'from_sr', None)\n            sent_to_sr = item.sr_id and not item.to_id\n\n            item.to = accounts[item.to_id] if item.to_id else None\n            item.is_mention = False\n            item.is_collapsed = None\n            item.score_fmt = Score.none\n            item.hide_author = False\n\n            if item.was_comment:\n                item.user_is_recipient = user_is_recipient\n                link = links[item.link_id]\n                sr = srs[link.sr_id]\n                item.to_collapse = False\n                item.author_collapse = False\n                item.link_title = link.title\n                item.permalink = item.lookups[0].make_permalink(link, sr=sr)\n                item.link_permalink = link.make_permalink(sr)\n                item.full_comment_count = link.num_comments\n                parent = parents[item.parent_id] if item.parent_id else None\n\n                if parent:\n                    item.parent = parent._fullname\n                    item.parent_permalink = parent.make_permalink(link, sr)\n\n                if parent and parent.author_id == user._id:\n                    item.subject = _('comment reply')\n                elif not parent and link.author_id == user._id:\n                    item.subject = _('post reply')\n                else:\n                    item.subject = _('username mention')\n                    item.is_mention = True\n\n                item.taglinetext = _(\n                    \"from %(author)s via %(subreddit)s sent %(when)s\")\n            elif item.sr_id:\n                item.user_is_recipient = not user_is_sender\n                item.user_is_moderator = item.sr_id in user_mod_sr_ids\n\n                if sr_colors and item.user_is_moderator:\n                    item.accent_color = sr_colors.get(item.sr_id)\n\n                if item.subreddit.is_muted(item.author):\n                    item.sr_muted = True\n\n                if sent_by_sr:\n                    if item.sr_id in blocked_srids:\n                        item.subject = _('[message from blocked subreddit]')\n                        item.sr_blocked = True\n                        item.is_collapsed = True\n\n                    # use special handling of admin distinguish because ALL\n                    # messages from admin accounts are marked for admin\n                    # distinguish, but we only want to use the admin distinguish\n                    # on messages sent from /r/reddit.com\n                    if (item.distinguished == \"admin\" and\n                            \"/r/%s\" % item.subreddit.name == g.admin_message_acct):\n                        subreddit_distinguish = \"admin\"\n                    elif (item.distinguished == \"moderator\" or\n                            item.distinguished == \"admin\"):\n                        subreddit_distinguish = \"moderator\"\n                    else:\n                        subreddit_distinguish = None\n\n                    if item.sent_via_email:\n                        item.hide_author = True\n                        item.distinguished = \"yes\"\n                        item.taglinetext = _(\n                            \"subreddit message via %(subreddit)s sent %(when)s\")\n                    elif not item.user_is_moderator and not c.user_is_admin:\n                        item.author = item.subreddit\n                        item.hide_author = True\n                        item.taglinetext = _(\n                            \"subreddit message via %(subreddit)s sent %(when)s\")\n                        item.subreddit_distinguish = subreddit_distinguish\n                    elif user_is_sender:\n                        item.taglinetext = _(\n                            \"to %(dest)s via %(subreddit)s sent %(when)s\")\n                        item.subreddit_distinguish = subreddit_distinguish\n                    else:\n                        item.taglinetext = _(\n                            \"from %(author)s via %(subreddit)s to %(dest)s sent\"\n                            \" %(when)s\")\n                        # don't set item.subreddit_distinguish because any\n                        # distinguish will be associated with the author\n                else:\n                    if item._id in mod_message_authors:\n                        # let moderators see the original author when a regular\n                        # user responds to a modmail message from subreddit.\n                        # item.to_id is not set, but we found the original\n                        # sender by inspecting the parent message\n                        item.to = mod_message_authors[item._id]\n\n                    if user_is_recipient:\n                        item.taglinetext = _(\n                            \"from %(author)s via %(subreddit)s sent %(when)s\")\n                    elif user_is_sender and sent_to_sr:\n                        item.taglinetext = _(\"to %(subreddit)s sent %(when)s\")\n                    elif user_is_sender:\n                        item.taglinetext = _(\n                            \"to %(dest)s via %(subreddit)s sent %(when)s\")\n                    elif sent_to_sr:\n                        item.taglinetext = _(\n                            \"from %(author)s to %(subreddit)s sent %(when)s\")\n                    else:\n                        item.taglinetext = _(\n                            \"from %(author)s via %(subreddit)s to %(dest)s sent\"\n                            \" %(when)s\")\n            else:\n                item.user_is_recipient = user_is_recipient\n\n                if item.display_author:\n                    item.author = accounts[item.display_author]\n\n                if item.display_to:\n                    item.to = accounts[item.display_to]\n                    if item.to_id == user._id:\n                        item.body = (strings.anonymous_gilder_warning +\n                            _force_unicode(item.body))\n\n                if user_is_recipient:\n                    item.taglinetext = _(\"from %(author)s sent %(when)s\")\n                elif user_is_sender:\n                    item.taglinetext = _(\"to %(dest)s sent %(when)s\")\n                else:\n                    item.taglinetext = _(\n                        \"to %(dest)s from %(author)s sent %(when)s\")\n\n            if user_is_sender:\n                item.new = False\n            elif item._fullname in unread:\n                item.new = True\n\n                if can_set_unread:\n                    to_set_unread.append(item.lookups[0])\n            else:\n                item.new = item._fullname in mod_unread\n\n            if not item.new:\n                if item.user_is_recipient:\n                    item.is_collapsed = item.to_collapse\n                if item.author_id == user._id:\n                    item.is_collapsed = item.author_collapse\n                if user.pref_collapse_read_messages:\n                    item.is_collapsed = (item.is_collapsed is not False)\n\n            if item.author_id in user.enemies and not item.was_comment:\n                item.is_collapsed = True\n                if not c.user_is_admin:\n                    item.subject = _('[message from blocked user]')\n                    item.body = _('[unblock user to see this message]')\n\n            if item.sr_id and item.to:\n                item.to_is_moderator = item.to._id in mods_by_srid[item.sr_id]\n\n        if to_set_unread:\n            queries.set_unread(to_set_unread, user, unread=False)\n\n        Printable.add_props(user, wrapped)\n\n    @property\n    def subreddit_slow(self):\n        from subreddit import Subreddit\n        if self.sr_id:\n            return Subreddit._byID(self.sr_id, data=True)\n\n    @property\n    def author_slow(self):\n        \"\"\"Returns the message's author.\"\"\"\n        # The author is often already on the wrapped message as .author\n        # If available, that should be used instead of calling this\n        return Account._byID(self.author_id, data=True, return_dict=False)\n\n    @property\n    def recipient_slow(self):\n        \"\"\"Returns the message's recipient.\"\"\"\n        return Account._byID(self.to_id, data=True, return_dict=False)\n\n    @staticmethod\n    def wrapped_cache_key(wrapped, style):\n        s = Printable.wrapped_cache_key(wrapped, style)\n        s.extend([wrapped.new, wrapped.collapsed])\n        return s\n\n    def keep_item(self, wrapped):\n        if c.user_is_admin:\n            return True\n        # do not keep message which were deleted on recipient\n        if (isinstance(self, Message) and\n                self.to_id == c.user._id and self.del_on_recipient):\n            return False\n        return not wrapped.enemy\n\n\nclass _SaveHideByAccount(tdb_cassandra.DenormalizedRelation):\n    @classmethod\n    def value_for(cls, thing1, thing2):\n        return ''\n\n    @classmethod\n    def _cached_queries(cls, user, thing):\n        return []\n\n    @classmethod\n    def _savehide(cls, user, things, **kw):\n        things = tup(things)\n        now = datetime.now(g.tz)\n        with CachedQueryMutator() as m:\n            for thing in things:\n                # action_date is only used by the cached queries as the sort\n                # value, we don't want to write it. Report.new(link) needs to\n                # incr link.reported but will fail if the link is dirty.\n                thing.__setattr__('action_date', now, make_dirty=False)\n                for q in cls._cached_queries(user, thing, **kw):\n                    m.insert(q, [thing])\n        cls.create(user, things, **kw)\n\n    @classmethod\n    def destroy(cls, user, things, **kw):\n        things = tup(things)\n        cls._cf.remove(user._id36, (things._id36 for things in things))\n\n        for view in cls._views:\n            view.destroy(user, things, **kw)\n\n    @classmethod\n    def _unsavehide(cls, user, things, **kw):\n        things = tup(things)\n        with CachedQueryMutator() as m:\n            for thing in things:\n                for q in cls._cached_queries(user, thing, **kw):\n                    m.delete(q, [thing])\n        cls.destroy(user, things, **kw)\n\n\nclass _ThingSavesByAccount(_SaveHideByAccount):\n    _read_consistency_level = tdb_cassandra.CL.QUORUM\n    _write_consistency_level = tdb_cassandra.CL.QUORUM\n\n    @classmethod\n    def value_for(cls, thing1, thing2, category=None):\n        return category or ''\n\n    @classmethod\n    def _remove_from_category_listings(cls, user, things, category):\n        things = tup(things)\n        oldcategories = cls.fast_query(user, things)\n        changedthings = []\n        for thing in things:\n            oldcategory = oldcategories.get((user, thing)) or None\n            if oldcategory != category:\n                changedthings.append(thing)\n        cls._unsavehide(user, changedthings, categories=oldcategories)\n\n    @classmethod\n    def _save(cls, user, things, category=None):\n        category = category.lower() if category else None\n        cls._remove_from_category_listings(user, things, category=category)\n        cls._savehide(user, things, category=category)\n\n    @classmethod\n    def _unsave(cls, user, things):\n        # Ensure we delete from existing category cached queries\n        categories = cls.fast_query(user, tup(things))\n        cls._unsavehide(user, things, categories=categories)\n\n    @classmethod\n    def _unsavehide(cls, user, things, categories=None):\n        things = tup(things)\n        with CachedQueryMutator() as m:\n            for thing in things:\n                category = categories.get((user, thing)) if categories else None\n                for q in cls._cached_queries(user, thing, category=category):\n                    m.delete(q, [thing])\n        cls.destroy(user, things, categories=categories)\n\n    @classmethod\n    def _cached_queries_category(cls, user, thing,\n                                 querycatfn, queryfn,\n                                 category=None, only_category=False):\n        from r2.lib.db import queries\n        cached_queries = []\n        if not only_category:\n            cached_queries = [queryfn(user, 'none'), queryfn(user, thing.sr_id)]\n        if category:\n            cached_queries.append(querycatfn(user, 'none', category))\n            cached_queries.append(querycatfn(user, thing.sr_id, category))\n        return cached_queries\n\nclass LinkSavesByAccount(_ThingSavesByAccount):\n    _use_db = True\n    _last_modified_name = 'Save'\n    _views = []\n\n    @classmethod\n    def _cached_queries(cls, user, thing, **kw):\n        from r2.lib.db import queries\n        return cls._cached_queries_category(\n            user,\n            thing,\n            queries.get_categorized_saved_links,\n            queries.get_saved_links,\n            **kw)\n\nclass CommentSavesByAccount(_ThingSavesByAccount):\n    _use_db = True\n    _last_modified_name = 'CommentSave'\n    _views = []\n\n    @classmethod\n    def _cached_queries(cls, user, thing, **kw):\n        from r2.lib.db import queries\n        return cls._cached_queries_category(\n            user,\n            thing,\n            queries.get_categorized_saved_comments,\n            queries.get_saved_comments,\n            **kw)\n\nclass _ThingHidesByAccount(_SaveHideByAccount):\n    @classmethod\n    def _hide(cls, user, things):\n        cls._savehide(user, things)\n\n    @classmethod\n    def _unhide(cls, user, things):\n        cls._unsavehide(user, things)\n\n\nclass LinkHidesByAccount(_ThingHidesByAccount):\n    _use_db = True\n    _last_modified_name = 'Hide'\n    _views = []\n\n    @classmethod\n    def _cached_queries(cls, user, thing):\n        from r2.lib.db import queries\n        return [queries.get_hidden_links(user)]\n\nclass LinkVisitsByAccount(_SaveHideByAccount):\n    _use_db = True\n    _last_modified_name = 'Visit'\n    _views = []\n    _ttl = timedelta(days=7)\n    _write_consistency_level = tdb_cassandra.CL.ONE\n\n    @classmethod\n    def _visit(cls, user, things):\n        cls._savehide(user, things)\n\n    @classmethod\n    def _unvisit(cls, user, things):\n        cls._unsavehide(user, things)\n\nclass _ThingSavesBySubreddit(tdb_cassandra.View):\n    @classmethod\n    def _rowkey(cls, user, thing):\n        return user._id36\n\n    @classmethod\n    def _column(cls, user, thing):\n        return {utils.to36(thing.sr_id): ''}\n\n    @classmethod\n    def get_saved_values(cls, user):\n        rowkey = cls._rowkey(user, None)\n        try:\n            columns = cls._cf.get(rowkey,\n                                  column_count=tdb_cassandra.max_column_count)\n        except NotFoundException:\n            return []\n\n        return columns.keys()\n\n    @classmethod\n    def get_saved_subreddits(cls, user):\n        sr_id36s = cls.get_saved_values(user)\n        srs = Subreddit._byID36(sr_id36s, return_dict=False, data=True)\n        return sorted([sr.name for sr in srs])\n\n    @classmethod\n    def create(cls, user, things, **kw):\n        for thing in things:\n            rowkey = cls._rowkey(user, thing)\n            column = cls._column(user, thing)\n            cls._set_values(rowkey, column)\n\n    @classmethod\n    def _check_empty(cls, user, sr_id):\n        return False\n\n    @classmethod\n    def destroy(cls, user, things, **kw):\n        # See if thing's sr is present anymore\n        sr_ids = set([thing.sr_id for thing in things])\n        for sr_id in set(sr_ids):\n            if cls._check_empty(user, sr_id):\n                cls._cf.remove(user._id36, [utils.to36(sr_id)])\n\nclass _ThingSavesByCategory(_ThingSavesBySubreddit):\n    @classmethod\n    def create(cls, user, things, category=None):\n        if not category:\n            return\n        for thing in things:\n            rowkey = cls._rowkey(user, thing)\n            column = {category: None}\n            cls._set_values(rowkey, column)\n\n    @classmethod\n    def _get_query_fn():\n        raise NotImplementedError\n\n    @classmethod\n    def _check_empty(cls, user, category):\n        from r2.lib.db import queries\n        q = cls._get_query_fn()(user, 'none', category)\n        q.fetch()\n        return not q.data\n\n    @classmethod\n    def get_saved_categories(cls, user):\n        return cls.get_saved_values(user)\n\n    @classmethod\n    def destroy(cls, user, things, categories=None):\n        if not categories:\n            return\n        for category in set(categories.values()):\n            if not category or not cls._check_empty(user, category):\n                continue\n            cls._cf.remove(user._id36, [category])\n\n@view_of(LinkSavesByAccount)\nclass LinkSavesByCategory(_ThingSavesByCategory):\n    _use_db = True\n\n    @classmethod\n    def _get_query_fn(cls):\n        from r2.lib.db import queries\n        return queries.get_categorized_saved_links\n\n@view_of(LinkSavesByAccount)\nclass LinkSavesBySubreddit(_ThingSavesBySubreddit):\n    _use_db = True\n\n    @classmethod\n    def _check_empty(cls, user, sr_id):\n        from r2.lib.db import queries\n        q = queries.get_saved_links(user, sr_id)\n        q.fetch()\n        return not q.data\n\n\n@view_of(CommentSavesByAccount)\nclass CommentSavesBySubreddit(_ThingSavesBySubreddit):\n    _use_db = True\n\n    @classmethod\n    def _check_empty(cls, user, sr_id):\n        from r2.lib.db import queries\n        q = queries.get_saved_comments(user, sr_id)\n        q.fetch()\n        return not q.data\n\n@view_of(CommentSavesByAccount)\nclass CommentSavesByCategory(_ThingSavesByCategory):\n    _use_db = True\n\n    @classmethod\n    def _get_query_fn(cls):\n        from r2.lib.db import queries\n        return queries.get_categorized_saved_comments\n\nclass LinksByImage(tdb_cassandra.View):\n    _use_db = True\n\n    # If a popular site uses the same oembed image everywhere (*cough* reddit),\n    # we may have a shitton of links pointing to the same image.\n    _fetch_all_columns = True\n\n    _extra_schema_creation_args = {\n        'key_validation_class': ASCII_TYPE,\n    }\n\n    @classmethod\n    def _rowkey(cls, image_uid):\n        return image_uid\n\n    @classmethod\n    def add_link(cls, image_uid, link):\n        rowkey = cls._rowkey(image_uid)\n        column = {link._id36: ''}\n        cls._set_values(rowkey, column)\n\n    @classmethod\n    def remove_link(cls, image_uid, link):\n        \"\"\"A weakly-guaranteed removal of the record tying a Link to an image.\"\"\"\n        rowkey = cls._rowkey(image_uid)\n        columns = (link._id36,)\n        cls._remove(rowkey, columns)\n\n    @classmethod\n    def get_link_id36s(cls, image_uid):\n        rowkey = cls._rowkey(image_uid)\n        try:\n            columns = cls._byID(rowkey)._values()\n        except NotFoundException:\n            return []\n        return columns.iterkeys()\n\n\n_CommentInbox = Relation(Account, Comment)\n_CommentInbox._defaults = {\n    \"new\": True,\n}\n_CommentInbox._cache = g.thingcache\n_CommentInbox._cache_prefix = classmethod(lambda cls: \"inboxcomment:\")\n\n\n_MessageInbox = Relation(Account, Message)\n_MessageInbox._defaults = {\n    \"new\": True,\n}\n_MessageInbox._cache = g.thingcache\n_MessageInbox._cache_prefix = classmethod(lambda cls: \"inboxmessage:\")\n\n\nclass Inbox(MultiRelation('inbox', _CommentInbox, _MessageInbox)):\n    @classmethod\n    def _add(cls, to, obj, name, orangered=True):\n        if isinstance(obj, Comment):\n            assert name in (\"inbox\", \"selfreply\", \"mention\")\n        elif isinstance(obj, Message):\n            assert name == \"inbox\"\n\n        # don't orangered (ever!) for enemies\n        if obj.author_id in to.enemies:\n            orangered = False\n        # don't get an orangered for messaging yourself.\n        elif to._id == obj.author_id:\n            orangered = False\n\n        i = Inbox(to, obj, name)\n        i._commit()\n\n        if orangered:\n            to._incr('inbox_count', 1)\n\n        return i\n\n    @classmethod\n    def possible_recipients(cls, obj):\n        \"\"\"Determine all possible recipients of Inboxes for this object.\n           `obj` may be one of (Comment, Message).\n        \"\"\"\n\n        possible_recipients = []\n        if isinstance(obj, Comment):\n            # Item is a comment. Eligible types of inboxes: mentions,\n            # selfreply (which can exist on all posts if sendreplies=True),\n            # inbox (which is a comment reply)\n\n            parent_id = getattr(obj, 'parent_id', None)\n            if parent_id:\n                # Comment reply\n                parent_comment = Comment._byID(parent_id, data=True)\n                possible_recipients.append(parent_comment.author_id)\n            else:\n                # Selfreply\n                # Do not check sendreplies, as they may have flagged it off\n                # between when the comment was created and when we are checking\n                parent_link = Link._byID(obj.link_id, data=True)\n                possible_recipients.append(parent_link.author_id)\n\n            mentions = utils.extract_user_mentions(obj.body)\n            if len(mentions) <= g.butler_max_mentions:\n                possible_recipients.extend(Account._names_to_ids(\n                    mentions,\n                    ignore_missing=True,\n                ))\n        elif isinstance(obj, Message):\n            if obj.to_id:\n                possible_recipients.append(obj.to_id)\n        else:\n            g.log.warning(\"Unknown object type for recipients: %r\", obj)\n\n        return possible_recipients\n\n\n    @classmethod\n    def get_rels(cls, user, things):\n        things = tup(things)\n        messages = [t for t in things if isinstance(t, Message)]\n        comments = [t for t in things if isinstance(t, Comment)]\n\n        res = {}\n\n        if messages:\n            inbox_rel_cls = cls.rel(Account, Message)\n            message_res = inbox_rel_cls._fast_query(\n                thing1s=user,\n                thing2s=messages,\n                name=\"inbox\",\n            )\n            res.update(message_res)\n\n        if comments:\n            inbox_rel_cls = cls.rel(Account, Comment)\n            comment_res = inbox_rel_cls._fast_query(\n                thing1s=user,\n                thing2s=comments,\n                name=(\"inbox\", \"selfreply\", \"mention\"),\n            )\n            res.update(comment_res)\n\n        # _fast_query returns a dict of {(t1, t2, name): rel}, with rel of None\n        # if the relation doesn't exist\n        inbox_rels = [inbox_rel for inbox_rel in res.itervalues() if inbox_rel]\n        return inbox_rels\n\n    @classmethod\n    def set_unread(cls, inbox_rels, unread=True):\n        inbox_rels = tup(inbox_rels)\n        unread_count_by_user = defaultdict(int)\n        for inbox_rel in inbox_rels:\n            if inbox_rel.new != unread:\n                user = inbox_rel._thing1\n                unread_count_by_user[user] += 1 if unread else -1\n                inbox_rel.new = unread\n                inbox_rel._commit()\n\n        for user, unread_count in unread_count_by_user.iteritems():\n            if unread_count == 0:\n                continue\n\n            if user.inbox_count + unread_count < 0:\n                g.log.info(\n                    \"Inbox count for %r would be negative: %d + %d. Zeroing.\",\n                    user.name,\n                    user.inbox_count,\n                    unread_count,\n                )\n                g.stats.simple_event(\"inbox_counts.negative_total_fix\")\n                unread_count = -user.inbox_count\n\n            user._incr('inbox_count', unread_count)\n\n\nclass ModeratorInbox(Relation(Subreddit, Message)):\n    _cache = g.thingcache\n    _defaults = {\n        \"new\": True,\n    }\n\n    @classmethod\n    def _cache_prefix(cls):\n        return \"modinbox:\"\n\n    @classmethod\n    def _add(cls, sr, obj):\n        i = cls(sr, obj, name=\"inbox\")\n        i._commit()\n        return i\n\n    @classmethod\n    def get_rels(cls, sr, messages):\n        messages = tup(messages)\n        res = ModeratorInbox._fast_query(\n            thing1s=sr,\n            thing2s=messages,\n            name=\"inbox\",\n        )\n        # _fast_query returns a dict of {(t1, t2, name): rel}, with rel of None\n        # if the relation doesn't exist\n        inbox_rels = [inbox_rel for inbox_rel in res.itervalues() if inbox_rel]\n        return inbox_rels\n\n    @classmethod\n    def set_unread(cls, inbox_rels, unread=True):\n        inbox_rels = tup(inbox_rels)\n        for inbox_rel in inbox_rels:\n            if inbox_rel.new != unread:\n                inbox_rel.new = unread\n                inbox_rel._commit()\n\n\nclass CommentsByAccount(tdb_cassandra.DenormalizedRelation):\n    _use_db = True\n    _write_last_modified = False\n    _views = []\n\n    @classmethod\n    def value_for(cls, thing1, thing2):\n        return ''\n\n    @classmethod\n    def add_comment(cls, account, comment):\n        cls.create(account, [comment])\n\n\nclass LinksByAccount(tdb_cassandra.DenormalizedRelation):\n    _use_db = True\n    _write_last_modified = False\n    _views = []\n\n    @classmethod\n    def value_for(cls, thing1, thing2):\n        return ''\n\n    @classmethod\n    def add_link(cls, account, link):\n        cls.create(account, [link])\n\n\nclass MessagesByAccount(tdb_cassandra.DenormalizedRelation):\n    _use_db = True\n    _write_last_modified = False\n    _views = []\n\n    @classmethod\n    def value_for(cls, thing1, thing2):\n        return ''\n\n    @classmethod\n    def add_message(cls, account, message):\n        cls.create(account, [message])\n\n\nclass CommentVisitsByUser(tdb_cassandra.View):\n    _use_db = True\n    _connection_pool = 'main'\n    _read_consistency_level = tdb_cassandra.CL.ONE\n    _write_consistency_level = tdb_cassandra.CL.ONE\n    _ttl = timedelta(days=2)\n    _compare_with = tdb_cassandra.DateType()\n    _extra_schema_creation_args = {\n        \"key_validation_class\": ASCII_TYPE,\n    }\n    MAX_VISITS = 10\n\n    @classmethod\n    def _rowkey(cls, user, link):\n        return \"%s-%s\" % (user._id36, link._id36)\n\n    @classmethod\n    def get_previous_visits(cls, user, link):\n        rowkey = cls._rowkey(user, link)\n        try:\n            columns = cls._cf.get(\n                rowkey, column_count=cls.MAX_VISITS, column_reversed=True)\n        except NotFoundException:\n            return []\n        # NOTE: dates return from pycassa are UTC but missing their timezone\n        dates = [date.replace(tzinfo=pytz.UTC) for date in columns.keys()]\n        return sorted(dates)\n\n    @classmethod\n    def add_visit(cls, user, link, visit_time):\n        rowkey = cls._rowkey(user, link)\n        column = {visit_time: ''}\n        cls._set_values(rowkey, column)\n\n    @classmethod\n    def get_and_update(cls, user, link, visit_time):\n        visits = cls.get_previous_visits(user, link)\n        if visits:\n            previous_visit = visits[-1]\n            time_since_previous = visit_time - previous_visit\n\n            if time_since_previous.total_seconds() <= g.comment_visits_period:\n                visits.pop()\n                return visits\n\n        cls.add_visit(user, link, visit_time)\n        return visits\n"
  },
  {
    "path": "r2/r2/models/listing.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom account import *\nfrom link import *\nfrom vote import *\nfrom report import *\nfrom subreddit import DefaultSR, AllSR, Frontpage, Subreddit\nfrom pylons import i18n, request\nfrom pylons import app_globals as g\nfrom pylons.i18n import _\n\nfrom r2.config import feature\nfrom r2.lib.wrapped import Wrapped, CachedVariable\nfrom r2.lib import utils\nfrom r2.lib.db import operators\nfrom r2.models import rules\n\nfrom collections import namedtuple\nfrom copy import deepcopy, copy\nimport time\n\n\nclass Listing(object):\n    # class used in Javascript to manage these objects\n    _js_cls = \"Listing\"\n\n    def __init__(self, builder, nextprev = True, next_link = True,\n                 prev_link = True, params = None, **kw):\n        self.builder = builder\n        self.nextprev = nextprev\n        self.next_link = True\n        self.prev_link = True\n        self.next = None\n        self.prev = None\n        self.params = params or request.GET.copy()\n        self._max_num = 1\n\n    @property\n    def max_score(self):\n        scores = [x.score for x in self.things if hasattr(x, 'score')]\n        return max(scores) if scores else 0\n\n    @property\n    def max_num(self):\n        return self._max_num\n\n    def get_items(self, *a, **kw):\n        \"\"\"Wrapper around builder's get_items that caches the rendering.\"\"\"\n        from r2.lib.template_helpers import replace_render\n        builder_items = self.builder.get_items(*a, **kw)\n        for item in self.builder.item_iter(builder_items):\n            # rewrite the render method\n            if c.render_style != \"api\" and not hasattr(item, \"render_replaced\"):\n                item.render = replace_render(self, item, item.render)\n                item.render_replaced = True\n        return builder_items\n\n    def listing(self, next_suggestions=None):\n        self.things, prev, next, bcount, acount = self.get_items()\n\n        self.next_suggestions = next_suggestions\n        self._max_num = max(acount, bcount)\n        self.after = None\n        self.before = None\n\n        if self.nextprev and self.prev_link and prev and bcount > 1:\n            p = self.params.copy()\n            p.update({'after':None, 'before':prev._fullname, 'count':bcount})\n            self.before = prev._fullname\n            self.prev = (request.path + utils.query_string(p))\n            p_first = self.params.copy()\n            p_first.update({'after':None, 'before':None, 'count':None})\n            self.first = (request.path + utils.query_string(p_first))\n        if self.nextprev and self.next_link and next:\n            p = self.params.copy()\n            p.update({'after':next._fullname, 'before':None, 'count':acount})\n            self.after = next._fullname\n            self.next = (request.path + utils.query_string(p))\n\n        for count, thing in enumerate(self.things):\n            thing.rowstyle_cls = getattr(thing, 'rowstyle_cls', \"\")\n            thing.rowstyle_cls += ' ' + ('even' if (count % 2) else 'odd')\n            thing.rowstyle = CachedVariable(\"rowstyle\")\n\n        #TODO: need name for template -- must be better way\n        return Wrapped(self)\n\n    def __iter__(self):\n        return iter(self.things)\n\nclass TableListing(Listing): pass\n\nclass ModActionListing(TableListing): pass\n\nclass WikiRevisionListing(TableListing): pass\n\nclass UserListing(TableListing):\n    type = ''\n    _class = ''\n    title = ''\n    form_title = ''\n    destination = 'friend'\n    has_add_form = True\n    headers = None\n    permissions_form = None\n\n    def __init__(self,\n                 builder,\n                 show_jump_to=False,\n                 show_not_found=False,\n                 jump_to_value=None,\n                 addable=True, **kw):\n        self.addable = addable\n        self.show_not_found = show_not_found\n        self.show_jump_to = show_jump_to\n        self.jump_to_value = jump_to_value\n        TableListing.__init__(self, builder, **kw)\n\n    @property\n    def container_name(self):\n        return c.site._fullname\n\nclass FriendListing(UserListing):\n    type = 'friend'\n\n    @property\n    def _class(self):\n        return '' if not c.user.gold else 'gold-accent rounded'\n\n    @property\n    def headers(self):\n        if c.user.gold:\n            return (_('user'), '', _('note'), _('friendship'), '')\n\n    @property\n    def form_title(self):\n        return _('add a friend')\n\n    @property\n    def container_name(self):\n        return c.user._fullname\n\n\nclass EnemyListing(UserListing):\n    type = 'enemy'\n    has_add_form = False\n\n    @property\n    def title(self):\n        return _('blocked users')\n\n    @property\n    def container_name(self):\n        return c.user._fullname\n\nclass BannedListing(UserListing):\n    type = 'banned'\n\n    def __init__(self, builder, show_jump_to=False, show_not_found=False,\n            jump_to_value=None, addable=True, **kw):\n        self.rules = rules.SubredditRules.get_rules(c.site)\n        self.system_rules = rules.SITEWIDE_RULES\n        UserListing.__init__(self, builder, show_jump_to, show_not_found,\n            jump_to_value, addable, **kw)\n\n    @classmethod\n    def populate_from_tempbans(cls, item, tempbans=None):\n        if not tempbans:\n            return\n        time = tempbans.get(item.user.name)\n        if time:\n            delay = time - datetime.now(g.tz)\n            item.tempban = max(delay.days, 0)\n\n    @property\n    def form_title(self):\n        return _(\"ban users\")\n\n    @property\n    def title(self):\n        return _(\"users banned from\"\n                 \" /r/%(subreddit)s\") % dict(subreddit=c.site.name)\n\n    def get_items(self, *a, **kw):\n        items = UserListing.get_items(self, *a, **kw)\n        wrapped_items = items[0]\n        names = [item.user.name for item in wrapped_items]\n        tempbans = c.site.get_tempbans(self.type, names)\n        for wrapped in wrapped_items:\n            BannedListing.populate_from_tempbans(wrapped, tempbans)\n        return items\n\n\nclass MutedListing(UserListing):\n    type = 'muted'\n\n    @classmethod\n    def populate_from_muted(cls, item, muted=None):\n        if not muted:\n            return\n        time = muted.get(item.user.name)\n        if time:\n            delay = time - datetime.now(g.tz)\n            item.muted = max(int(delay.total_seconds()), 0)\n\n    @property\n    def form_title(self):\n        return _(\"mute users\")\n\n    @property\n    def title(self):\n        return _(\"users muted from\"\n                 \" /r/%(subreddit)s\") % dict(subreddit=c.site.name)\n\n    def get_items(self, *a, **kw):\n        items = UserListing.get_items(self, *a, **kw)\n        wrapped_items = items[0]\n        names = [item.user.name for item in wrapped_items]\n        muted = c.site.get_muted_items(names)\n        for wrapped in wrapped_items:\n            MutedListing.populate_from_muted(wrapped, muted)\n        return items\n\n\nclass WikiBannedListing(BannedListing):\n    type = 'wikibanned'\n\n    @property\n    def form_title(self):\n        return _(\"ban wiki contibutors\")\n\n    @property\n    def title(self):\n        return _(\"wiki contibutors banned from\"\n                 \" /r/%(subreddit)s\") % dict(subreddit=c.site.name)\n\nclass ContributorListing(UserListing):\n    type = 'contributor'\n\n    @property\n    def title(self):\n        return _(\"approved submitters for\"\n                 \" /r/%(subreddit)s\") % dict(subreddit=c.site.name)\n\n    @property\n    def form_title(self):\n        return _(\"add approved submitter\")\n\nclass WikiMayContributeListing(ContributorListing):\n    type = 'wikicontributor'\n\n    @property\n    def title(self):\n        return _(\"approved wiki contributors\"\n                 \" for /r/%(subreddit)s\") % dict(subreddit=c.site.name)\n\n    @property\n    def form_title(self):\n        return _(\"add approved wiki contributor\")\n\nclass InvitedModListing(UserListing):\n    type = 'moderator_invite'\n    form_title = _('invite moderator')\n    remove_self_title = _('you are a moderator of this subreddit. %(action)s')\n\n    @property\n    def permissions_form(self):\n        from r2.lib.permissions import ModeratorPermissionSet\n        from r2.lib.pages import ModeratorPermissions\n        return ModeratorPermissions(\n            user=None,\n            permissions_type=self.type,\n            permissions=ModeratorPermissionSet(all=True),\n            editable=True,\n            embedded=True,\n        )\n\n    @property\n    def title(self):\n        return _(\"invited moderators for\"\n                 \" %(subreddit)s\") % dict(subreddit=c.site.name)\n\nclass ModListing(InvitedModListing):\n    type = 'moderator'\n    form_title = _('force add moderator')\n\n    @property\n    def has_add_form(self):\n        return c.user_is_admin\n\n    @property\n    def can_remove_self(self):\n        return c.user_is_loggedin and c.site.is_moderator(c.user)\n\n    @property\n    def has_invite(self):\n        return c.user_is_loggedin and c.site.is_moderator_invite(c.user)\n\n    @property\n    def title(self):\n        return _(\"moderators of /r/%(subreddit)s\") % dict(subreddit=c.site.name)\n\nclass LinkListing(Listing):\n    def __init__(self, *a, **kw):\n        Listing.__init__(self, *a, **kw)\n\n        self.show_nums = kw.get('show_nums', False)\n\n    def listing(self, *args, **kwargs):\n        wrapped = Listing.listing(self, *args, **kwargs)\n        self.rank_width = len(str(self.max_num)) * 1.1\n        self.midcol_width = max(len(str(self.max_score)), 2) + 1.1\n        return wrapped\n\n\nclass SearchListing(LinkListing):\n    def __init__(self, *a, **kw):\n        LinkListing.__init__(self, *a, **kw)\n        self.heading = kw.get('heading', None)\n        self.nav_menus = kw.get('nav_menus', None)\n\n    def listing(self, legacy_render_class=False, *args, **kwargs):\n        wrapped = LinkListing.listing(self, *args, **kwargs)\n        if hasattr(self.builder, 'subreddit_facets'):\n            self.subreddit_facets = self.builder.subreddit_facets\n        if hasattr(self.builder, 'start_time'):\n            self.timing = time.time() - self.builder.start_time\n\n        if legacy_render_class:\n            wrapped.render_class = LinkListing\n\n        return wrapped\n\n\nclass ReadNextListing(Listing):\n    pass\n\n\nclass NestedListing(Listing):\n    def __init__(self, *a, **kw):\n        Listing.__init__(self, *a, **kw)\n\n        self.num = kw.get('num', g.num_comments)\n        self.parent_name = kw.get('parent_name')\n\n    def listing(self):\n        ##TODO use the local builder with the render cache. this may\n        ##require separating the builder's get_items and tree-building\n        ##functionality\n        wrapped_items = self.get_items()\n\n        self.things = wrapped_items\n\n        #make into a tree thing\n        return Wrapped(self)\n\nSpotlightTuple = namedtuple('SpotlightTuple',\n                            ['link', 'is_promo', 'campaign', 'weight'])\n\nclass SpotlightListing(Listing):\n    # class used in Javascript to manage these objects\n    _js_cls = \"OrganicListing\"\n\n    def __init__(self, *a, **kw):\n        self.nextprev   = False\n        self.show_nums  = True\n        self._parent_max_num   = kw.get('max_num', 0)\n        self._parent_max_score = kw.get('max_score', 0)\n        self.interestbar = kw.get('interestbar')\n        self.interestbar_prob = kw.get('interestbar_prob', 0.)\n        self.show_promo = kw.get('show_promo', False)\n        keywords = kw.get('keywords', [])\n        self.keywords = '+'.join([keyword if keyword else Frontpage.name\n                                 for keyword in keywords])\n        self.navigable = kw.get('navigable', True)\n        self.things = kw.get('organic_links', [])\n        self.show_placeholder = isinstance(c.site, (DefaultSR, AllSR))\n\n    def get_items(self):\n        from r2.lib.template_helpers import replace_render\n        things = self.things\n        for t in things:\n            if not hasattr(t, \"render_replaced\"):\n                t.render = replace_render(self, t, t.render)\n                t.render_replaced = True\n        return things, None, None, 0, 0\n\n    def listing(self):\n        res = Listing.listing(self)\n        for t in res.things:\n            t.num_text = \"\"\n        return Wrapped(self)\n"
  },
  {
    "path": "r2/r2/models/mail_queue.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport datetime\nimport hashlib\nimport time\nimport email.utils\nfrom email.MIMEText import MIMEText\nfrom email.errors import HeaderParseError\n\nimport sqlalchemy as sa\nfrom sqlalchemy.dialects.postgresql.base import PGInet\n\nfrom r2.lib.db.tdb_sql import make_metadata, index_str, create_table\nfrom r2.lib.utils import Enum, tup\nfrom r2.lib.memoize import memoize\nfrom pylons import request\nfrom pylons import app_globals as g\nfrom pylons.i18n import _\n\ndef mail_queue(metadata):\n    return sa.Table(g.db_app_name + '_mail_queue', metadata,\n                    sa.Column(\"uid\", sa.Integer,\n                              sa.Sequence('queue_id_seq'), primary_key=True),\n\n                    # unique hash of the message to carry around\n                    sa.Column(\"msg_hash\", sa.String),\n\n                    # the id of the account who started it\n                    sa.Column('account_id', sa.BigInteger),\n\n                    # the name (not email) for the from\n                    sa.Column('from_name', sa.String),\n\n                    # the \"To\" address of the email\n                    sa.Column('to_addr', sa.String),\n\n                    # the \"From\" address of the email\n                    sa.Column('fr_addr', sa.String),\n\n                    # the \"Reply-To\" address of the email\n                    sa.Column('reply_to', sa.String),\n\n                    # fullname of the thing\n                    sa.Column('fullname', sa.String),\n\n                    # when added to the queue\n                    sa.Column('date',\n                              sa.DateTime(timezone = True),\n                              nullable = False),\n\n                    # IP of original request\n                    sa.Column('ip', PGInet),\n\n                    # enum of kind of event\n                    sa.Column('kind', sa.Integer),\n\n                    # any message that may have been included\n                    sa.Column('body', sa.String),\n\n                    )\n\ndef sent_mail_table(metadata, name = 'sent_mail'):\n    return sa.Table(g.db_app_name + '_' + name, metadata,\n                    # tracking hash of the email\n                    sa.Column('msg_hash', sa.String, primary_key=True),\n\n                    # the account who started it\n                    sa.Column('account_id', sa.BigInteger),\n\n                    # the \"To\" address of the email\n                    sa.Column('to_addr', sa.String),\n\n                    # the \"From\" address of the email\n                    sa.Column('fr_addr', sa.String),\n\n                    # the \"reply-to\" address of the email\n                    sa.Column('reply_to', sa.String),\n\n                    # IP of original request\n                    sa.Column('ip', PGInet),\n\n                    # fullname of the reference thing\n                    sa.Column('fullname', sa.String),\n\n                    # send date\n                    sa.Column('date',\n                              sa.DateTime(timezone = True),\n                              default = sa.func.now(),\n                              nullable = False),\n\n                    # enum of kind of event\n                    sa.Column('kind', sa.Integer),\n\n                    )\n\n\ndef opt_out(metadata):\n    return sa.Table(g.db_app_name + '_opt_out', metadata,\n                    sa.Column('email', sa.String, primary_key = True),\n                    # when added to the list\n                    sa.Column('date',\n                              sa.DateTime(timezone = True),\n                              default = sa.func.now(),\n                              nullable = False),\n                    # why did they do it!?\n                    sa.Column('msg_hash', sa.String),\n                    )\n\nclass EmailHandler(object):\n    def __init__(self, force = False):\n        engine = g.dbm.get_engine('email')\n        self.metadata = make_metadata(engine)\n        self.queue_table = mail_queue(self.metadata)\n        indices = [index_str(self.queue_table, \"date\", \"date\"),\n                   index_str(self.queue_table, 'kind', 'kind')]\n        create_table(self.queue_table, indices)\n\n        self.opt_table = opt_out(self.metadata)\n        indices = [index_str(self.opt_table, 'email', 'email')]\n        create_table(self.opt_table, indices)\n\n        self.track_table = sent_mail_table(self.metadata)\n        self.reject_table = sent_mail_table(self.metadata, name = \"reject_mail\")\n\n        def sent_indices(tab):\n            indices = [index_str(tab, 'to_addr', 'to_addr'),\n                       index_str(tab, 'date', 'date'),\n                       index_str(tab, 'ip', 'ip'),\n                       index_str(tab, 'kind', 'kind'),\n                       index_str(tab, 'fullname', 'fullname'),\n                       index_str(tab, 'account_id', 'account_id'),\n                       index_str(tab, 'msg_hash', 'msg_hash'),\n                       ]\n\n        create_table(self.track_table, sent_indices(self.track_table))\n        create_table(self.reject_table, sent_indices(self.reject_table))\n\n    def __repr__(self):\n        return \"<email-handler>\"\n\n    def has_opted_out(self, email):\n        o = self.opt_table\n        s = sa.select([o.c.email], o.c.email == email, limit = 1)\n        res = s.execute()\n        return bool(res.fetchall())\n\n\n    def opt_out(self, msg_hash):\n        \"\"\"Adds the recipient of the email to the opt-out list and returns\n        that address.\"\"\"\n        email = self.get_recipient(msg_hash)\n        if email:\n            o = self.opt_table\n            try:\n                o.insert().values({o.c.email: email,\n                                   o.c.msg_hash: msg_hash}).execute()\n                g.stats.simple_event('share.opt_out')\n\n                #clear caches\n                has_opted_out(email, _update = True)\n                opt_count(_update = True)\n                return (email, True)\n            except sa.exc.DBAPIError:\n                return (email, False)\n        return (None, False)\n\n    def opt_in(self, msg_hash):\n        \"\"\"Removes recipient of the email from the opt-out list\"\"\"\n        email = self.get_recipient(msg_hash)\n        if email:\n            o = self.opt_table\n            if self.has_opted_out(email):\n                sa.delete(o, o.c.email == email).execute()\n                g.stats.simple_event('share.opt_in')\n\n                #clear caches\n                has_opted_out(email, _update = True)\n                opt_count(_update = True)\n                return (email, True)\n            else:\n                return (email, False)\n        return (None, False)\n\n    def get_recipient(self, msg_hash):\n        t = self.track_table\n        s = sa.select([t.c.to_addr], t.c.msg_hash == msg_hash).execute()\n        res = s.fetchall()\n        return res[0][0] if res and res[:1] else None\n\n\n    def add_to_queue(self, user, emails, from_name, fr_addr, kind,\n                     date = None, ip = None,\n                     body = \"\", reply_to = \"\", thing = None):\n        s = self.queue_table\n        hashes = []\n        if not date:\n            date = datetime.datetime.now(g.tz)\n        if not ip:\n            ip = getattr(request, \"ip\", \"127.0.0.1\")\n        for email in tup(emails):\n            uid = user._id if user else 0\n            tid = thing._fullname if thing else \"\"\n            key = hashlib.sha1(str((email, from_name, uid, tid, ip, kind, body,\n                               datetime.datetime.now(g.tz)))).hexdigest()\n            s.insert().values({s.c.to_addr : email,\n                               s.c.account_id : uid,\n                               s.c.from_name : from_name,\n                               s.c.fr_addr : fr_addr,\n                               s.c.reply_to : reply_to,\n                               s.c.fullname: tid,\n                               s.c.ip : ip,\n                               s.c.kind: kind,\n                               s.c.body: body,\n                               s.c.date : date,\n                               s.c.msg_hash : key}).execute()\n            hashes.append(key)\n        return hashes\n\n\n    def from_queue(self, max_date, batch_limit = 50, kind = None):\n        from r2.models import Account, Thing\n        keep_trying = True\n        min_id = None\n        s = self.queue_table\n        while keep_trying:\n            where = [s.c.date < max_date]\n            if min_id:\n                where.append(s.c.uid > min_id)\n            if kind:\n                where.append(s.c.kind == kind)\n\n            res = sa.select([s.c.to_addr, s.c.account_id,\n                             s.c.from_name, s.c.fullname, s.c.body,\n                             s.c.kind, s.c.ip, s.c.date, s.c.uid,\n                             s.c.msg_hash, s.c.fr_addr, s.c.reply_to],\n                            sa.and_(*where),\n                            order_by = s.c.uid, limit = batch_limit).execute()\n            res = res.fetchall()\n\n            if not res: break\n\n            # batch load user accounts\n            aids = [x[1] for x in res if x[1] > 0]\n            accts = Account._byID(aids, data = True,\n                                  return_dict = True) if aids else {}\n\n            # batch load things\n            tids = [x[3] for x in res if x[3]]\n            things = Thing._by_fullname(tids, data = True,\n                                        return_dict = True) if tids else {}\n\n            # get the lower bound date for next iteration\n            min_id = max(x[8] for x in res)\n\n            # did we not fetch them all?\n            keep_trying = (len(res) == batch_limit)\n\n            for (addr, acct, fname, fulln, body, kind, ip, date, uid,\n                 msg_hash, fr_addr, reply_to) in res:\n                yield (accts.get(acct), things.get(fulln), addr,\n                       fname, date, ip, kind, msg_hash, body,\n                       fr_addr, reply_to)\n\n    def clear_queue(self, max_date, kind = None):\n        s = self.queue_table\n        where = [s.c.date < max_date]\n        if kind:\n            where.append([s.c.kind == kind])\n        sa.delete(s, sa.and_(*where)).execute()\n\n\nclass Email(object):\n    handler = EmailHandler()\n\n    # Do not modify in any way other than appending new items!\n    # Database tables storing mail stuff use an int column as an index into \n    # this Enum, so anything other than appending new items breaks mail history.\n    Kind = Enum(\"SHARE\", \"FEEDBACK\", \"ADVERTISE\", \"OPTOUT\", \"OPTIN\",\n                \"VERIFY_EMAIL\", \"RESET_PASSWORD\",\n                \"BID_PROMO\",\n                \"ACCEPT_PROMO\",\n                \"REJECT_PROMO\",\n                \"QUEUED_PROMO\",\n                \"LIVE_PROMO\",\n                \"FINISHED_PROMO\",\n                \"NEW_PROMO\",\n                \"NERDMAIL\",\n                \"GOLDMAIL\",\n                \"PASSWORD_CHANGE\",\n                \"EMAIL_CHANGE\",\n                \"REFUNDED_PROMO\",\n                \"VOID_PAYMENT\",\n                \"GOLD_GIFT_CODE\",\n                \"SUSPICIOUS_PAYMENT\",\n                \"FRAUD_ALERT\",\n                \"USER_FRAUD\",\n                \"MESSAGE_NOTIFICATION\",\n                \"ADS_ALERT\",\n                \"EDITED_LIVE_PROMO\",\n                )\n\n    # Do not remove anything from this dictionary!  See above comment.\n    subjects = {\n        Kind.SHARE : _(\"[reddit] %(user)s has shared a link with you\"),\n        Kind.FEEDBACK : _(\"[feedback] feedback from '%(user)s'\"),\n        Kind.ADVERTISE :  _(\"[advertising] feedback from '%(user)s'\"),\n        Kind.OPTOUT : _(\"[reddit] email removal notice\"),\n        Kind.OPTIN  : _(\"[reddit] email addition notice\"),\n        Kind.RESET_PASSWORD : _(\"[reddit] reset your password\"),\n        Kind.VERIFY_EMAIL : _(\"[reddit] verify your email address\"),\n        Kind.BID_PROMO : _(\"[reddit] your budget has been accepted\"),\n        Kind.ACCEPT_PROMO : _(\"[reddit] your promotion has been accepted\"),\n        Kind.REJECT_PROMO : _(\"[reddit] your promotion has been rejected\"),\n        Kind.QUEUED_PROMO : _(\"[reddit] your promotion has been charged\"),\n        Kind.LIVE_PROMO   : _(\"[reddit] your promotion is now live\"),\n        Kind.FINISHED_PROMO : _(\"[reddit] your promotion has finished\"),\n        Kind.NEW_PROMO : _(\"[reddit] your promotion has been created\"),\n        Kind.EDITED_LIVE_PROMO : _(\"[reddit] your promotion edit is being approved\"),\n        Kind.NERDMAIL : _(\"[reddit] hey, nerd!\"),\n        Kind.GOLDMAIL : _(\"[reddit] reddit gold activation link\"),\n        Kind.PASSWORD_CHANGE : _(\"[reddit] your password has been changed\"),\n        Kind.EMAIL_CHANGE : _(\"[reddit] your email address has been changed\"),\n        Kind.REFUNDED_PROMO: _(\"[reddit] your campaign didn't get enough impressions\"),\n        Kind.VOID_PAYMENT: _(\"[reddit] your payment has been voided\"),\n        Kind.GOLD_GIFT_CODE: _(\"[reddit] your reddit gold gift code\"),\n        Kind.SUSPICIOUS_PAYMENT: _(\"[selfserve] suspicious payment alert\"),\n        Kind.FRAUD_ALERT: _(\"[selfserve] fraud alert\"),\n        Kind.USER_FRAUD: _(\"[selfserve] a user has committed fraud\"),\n        Kind.MESSAGE_NOTIFICATION: _(\"[reddit] message notification\"),\n        Kind.ADS_ALERT: _(\"[reddit] Ads Alert\"),\n        }\n\n    def __init__(self, user, thing, email, from_name, date, ip,\n                 kind, msg_hash, body = '', from_addr = '',\n                 reply_to = ''):\n        self.user = user\n        self.thing = thing\n        self.to_addr = email\n        self.fr_addr = from_addr\n        self._from_name = from_name\n        self.date = date\n        self.ip = ip\n        self.kind = kind\n        self.sent = False\n        self.body = body\n        self.msg_hash = msg_hash\n        self.reply_to = reply_to\n        self.subject = self.subjects.get(kind, \"\")\n        try:\n            self.subject = self.subject % dict(user = self.from_name())\n        except UnicodeDecodeError:\n            self.subject = self.subject % dict(user = \"a user\")\n\n\n    def from_name(self):\n        if not self.user:\n            name = \"%(name)s\"\n        elif self._from_name != self.user.name:\n            name = \"%(name)s (%(uname)s)\"\n        else:\n            name = \"%(uname)s\"\n        return name % dict(name = self._from_name,\n                           uname = self.user.name if self.user else '')\n\n    @classmethod\n    def get_unsent(cls, max_date, batch_limit = 50, kind = None):\n        for e in cls.handler.from_queue(max_date, batch_limit = batch_limit,\n                                        kind = kind):\n            yield cls(*e)\n\n    def should_queue(self):\n        return (not self.user  or not self.user._spam) and \\\n               (not self.thing or not self.thing._spam) and \\\n               (self.kind == self.Kind.OPTOUT or\n                not has_opted_out(self.to_addr))\n\n    def set_sent(self, date = None, rejected = False):\n        if not self.sent:\n            self.date = date or datetime.datetime.now(g.tz)\n            t = self.handler.reject_table if rejected else self.handler.track_table\n            try:\n                t.insert().values({t.c.account_id:\n                                       self.user._id if self.user else 0,\n                                   t.c.to_addr :   self.to_addr,\n                                   t.c.fr_addr :   self.fr_addr,\n                                   t.c.reply_to :  self.reply_to,\n                                   t.c.ip :        self.ip,\n                                   t.c.fullname:\n                                       self.thing._fullname if self.thing else \"\",\n                                   t.c.date:       self.date,\n                                   t.c.kind :      self.kind,\n                                   t.c.msg_hash :  self.msg_hash,\n                                   }).execute()\n            except:\n                print \"failed to send message\"\n\n            self.sent = True\n\n    def to_MIMEText(self):\n        def utf8(s, reject_newlines=True):\n            if reject_newlines and '\\n' in s:\n                raise HeaderParseError(\n                    'header value contains unexpected newline: {!r}'.format(s))\n            return s.encode('utf8') if isinstance(s, unicode) else s\n\n        fr = '\"%s\" <%s>' % (\n            self.from_name().replace('\"', ''),\n            self.fr_addr.replace('>', ''),\n        )\n\n        # Addresses that start with a dash could confuse poorly-written\n        # software's argument parsers, and thus are disallowed by default in\n        # Postfix: http://www.postfix.org/postconf.5.html#allow_min_user\n        if not fr.startswith('-') and not self.to_addr.startswith('-'):\n            msg = MIMEText(utf8(self.body, reject_newlines=False))\n            msg.set_charset('utf8')\n            msg['To']      = utf8(self.to_addr)\n            msg['From']    = utf8(fr)\n            msg['Subject'] = utf8(self.subject)\n            timestamp = time.mktime(self.date.timetuple())\n            msg['Date'] = utf8(email.utils.formatdate(timestamp))\n            if self.user:\n                msg['X-Reddit-username'] = utf8(self.user.name)\n            msg['X-Reddit-ID'] = self.msg_hash\n            if self.reply_to:\n                msg['Reply-To'] = utf8(self.reply_to)\n            return msg\n        return None\n\n@memoize('r2.models.mail_queue.has_opted_out')\ndef has_opted_out(email):\n    o = Email.handler.opt_table\n    s = sa.select([o.c.email], o.c.email == email, limit = 1)\n    res = s.execute()\n    return bool(res.fetchall())\n\n\n@memoize('r2.models.mail_queue.opt_count')\ndef opt_count():\n    o = Email.handler.opt_table\n    s = sa.select([sa.func.count(o.c.email)])\n    res = s.execute().fetchone()\n    return int(res[0])\n"
  },
  {
    "path": "r2/r2/models/media_cache.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2013-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport collections\nimport json\n\nfrom datetime import (\n    datetime,\n    timedelta,\n)\nfrom pycassa.system_manager import ASCII_TYPE, UTF8_TYPE\nfrom r2.lib.db import tdb_cassandra\n\n\nMedia = collections.namedtuple('_Media', (\"media_object\",\n                                          \"secure_media_object\",\n                                          \"preview_object\",\n                                          \"thumbnail_url\",\n                                          \"thumbnail_size\"))\n\nERROR_MEDIA = Media(None, None, None, None, None)\n\n\nclass MediaByURL(tdb_cassandra.View):\n    _use_db = True\n    _connection_pool = 'main'\n    _ttl = timedelta(minutes=720)\n\n    _read_consistency_level = tdb_cassandra.CL.QUORUM\n    _write_consistency_level = tdb_cassandra.CL.QUORUM\n    _int_props = {\"thumbnail_width\", \"thumbnail_height\"}\n    _date_props = {\"last_modified\"}\n    _extra_schema_creation_args = {\n        \"key_validation_class\": ASCII_TYPE,\n        \"column_name_class\": UTF8_TYPE,\n    }\n\n    _defaults = {\n        \"state\": \"enqueued\",\n        \"error\": \"\",\n        \"thumbnail_url\": \"\",\n        \"thumbnail_width\": 0,\n        \"thumbnail_height\": 0,\n        \"media_object\": \"\",\n        \"secure_media_object\": \"\",\n        \"preview_object\": \"\",\n        \"last_modified\": datetime.utcfromtimestamp(0),\n    }\n\n    @classmethod\n    def _rowkey(cls, url, **kwargs):\n        return (\n            url +\n            # pipe is not allowed in URLs, so use it as a delimiter\n            \"|\" +\n\n            # append the extra cache keys in kwargs as a canonical JSON string\n            json.dumps(\n                kwargs,\n                ensure_ascii=True,\n                encoding=\"ascii\",\n                indent=None,\n                separators=(\",\", \":\"),\n                sort_keys=True,\n            )\n        )\n\n    @classmethod\n    def add_placeholder(cls, url, **kwargs):\n        rowkey = cls._rowkey(url, **kwargs)\n        cls._set_values(rowkey, {\n            \"state\": \"enqueued\",\n            \"error\": \"\",\n            \"last_modified\": datetime.utcnow(),\n        })\n\n    @classmethod\n    def add(cls, url, media, **kwargs):\n        rowkey = cls._rowkey(url, **kwargs)\n        columns = cls._defaults.copy()\n\n        columns.update({\n            \"state\": \"processed\",\n            \"error\": \"\",\n            \"last_modified\": datetime.utcnow(),\n        })\n\n        if media.thumbnail_url and media.thumbnail_size:\n            columns.update({\n                \"thumbnail_url\": media.thumbnail_url,\n                \"thumbnail_width\": media.thumbnail_size[0],\n                \"thumbnail_height\": media.thumbnail_size[1],\n            })\n\n        if media.media_object:\n            columns.update({\n                \"media_object\": json.dumps(media.media_object),\n            })\n\n        if media.secure_media_object:\n            columns.update({\n                \"secure_media_object\": (json.\n                                        dumps(media.secure_media_object)),\n            })\n\n        if media.preview_object:\n            columns.update({\n                \"preview_object\": json.dumps(media.preview_object),\n            })\n\n        cls._set_values(rowkey, columns)\n\n    @classmethod\n    def add_error(cls, url, error, **kwargs):\n        rowkey = cls._rowkey(url, **kwargs)\n        columns = {\n            \"error\": error,\n            \"state\": \"processed\",\n            \"last_modified\": datetime.utcnow(),\n        }\n        cls._set_values(rowkey, columns)\n\n    @classmethod\n    def get(cls, url, max_cache_age=None, **kwargs):\n        rowkey = cls._rowkey(url, **kwargs)\n        try:\n            temp = cls._byID(rowkey)\n\n            # Return None if this cache entry is too old\n            if (max_cache_age is not None and\n                datetime.datetime.utcnow() - temp.last_modified >\n                max_cache_age):\n                return None\n            else:\n                return temp\n        except tdb_cassandra.NotFound:\n            return None\n\n    @property\n    def media(self):\n        if self.state == \"processed\":\n            if not self.error:\n                media_object = secure_media_object = preview_object = None\n                thumbnail_url = thumbnail_size = None\n\n                if (self.thumbnail_width and self.thumbnail_height and\n                    self.thumbnail_url):\n                    thumbnail_url = self.thumbnail_url\n                    thumbnail_size = (self.thumbnail_width,\n                                      self.thumbnail_height)\n\n                if self.media_object:\n                    media_object = json.loads(self.media_object)\n\n                if self.secure_media_object:\n                    secure_media_object = json.loads(self.secure_media_object)\n\n                if self.preview_object:\n                    preview_object = json.loads(self.preview_object)\n\n                return Media(media_object, secure_media_object, preview_object,\n                             thumbnail_url, thumbnail_size)\n            else:\n                return ERROR_MEDIA\n        else:\n            return None\n"
  },
  {
    "path": "r2/r2/models/modaction.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom datetime import timedelta\nimport itertools\nfrom uuid import UUID\n\nfrom pycassa.system_manager import TIME_UUID_TYPE\nfrom pylons import request\nfrom pylons import tmpl_context as c\nfrom pylons import app_globals as g\nfrom pylons.i18n import _\n\nfrom r2.lib.db import tdb_cassandra\nfrom r2.lib.utils import tup\n\n\nclass ModAction(tdb_cassandra.UuidThing):\n    \"\"\"\n    Columns:\n    sr_id - Subreddit id36\n    mod_id - Account id36 of moderator\n    action - specific name of action, must be in ModAction.actions\n    target_fullname - optional fullname of the target of the action\n    details - subcategory available for some actions, must show up in \n    description - optional user\n    \"\"\"\n\n    _read_consistency_level = tdb_cassandra.CL.ONE\n    _use_db = True\n    _connection_pool = 'main'\n    _ttl = timedelta(days=120)\n    _str_props = ('sr_id36', 'mod_id36', 'target_fullname', 'action', 'details', \n                  'description')\n    _defaults = {}\n\n    actions = ('banuser', 'unbanuser', 'removelink', 'approvelink', \n               'removecomment', 'approvecomment', 'addmoderator',\n               'invitemoderator', 'uninvitemoderator', 'acceptmoderatorinvite',\n               'removemoderator', 'addcontributor', 'removecontributor',\n               'editsettings', 'editflair', 'distinguish', 'marknsfw',\n               'wikibanned', 'wikicontributor', 'wikiunbanned', 'wikipagelisted',\n               'removewikicontributor', 'wikirevise', 'wikipermlevel',\n               'ignorereports', 'unignorereports', 'setpermissions',\n               'setsuggestedsort', 'sticky', 'unsticky', 'setcontestmode',\n               'unsetcontestmode', 'lock', 'unlock', 'muteuser', 'unmuteuser',\n               'createrule', 'editrule', 'deleterule')\n\n    _menu = {'banuser': _('ban user'),\n             'unbanuser': _('unban user'),\n             'removelink': _('remove post'),\n             'approvelink': _('approve post'),\n             'removecomment': _('remove comment'),\n             'approvecomment': _('approve comment'),\n             'addmoderator': _('add moderator'),\n             'removemoderator': _('remove moderator'),\n             'invitemoderator': _('invite moderator'),\n             'uninvitemoderator': _('uninvite moderator'),\n             'acceptmoderatorinvite': _('accept moderator invite'),\n             'addcontributor': _('add contributor'),\n             'removecontributor': _('remove contributor'),\n             'editsettings': _('edit settings'),\n             'editflair': _('edit flair'),\n             'distinguish': _('distinguish'),\n             'marknsfw': _('mark nsfw'),\n             'wikibanned': _('ban from wiki'),\n             'wikiunbanned': _('unban from wiki'),\n             'wikicontributor': _('add wiki contributor'),\n             'wikipagelisted': _('delist/relist wiki pages'),\n             'removewikicontributor': _('remove wiki contributor'),\n             'wikirevise': _('wiki revise page'),\n             'wikipermlevel': _('wiki page permissions'),\n             'ignorereports': _('ignore reports'),\n             'unignorereports': _('unignore reports'),\n             'setpermissions': _('permissions'),\n             'setsuggestedsort': _('set suggested sort'),\n             'sticky': _('sticky post'),\n             'unsticky': _('unsticky post'),\n             'setcontestmode': _('set contest mode'),\n             'unsetcontestmode': _('unset contest mode'),\n             'lock': _('lock post'),\n             'unlock': _('unlock post'),\n             'muteuser': _('mute user'),\n             'unmuteuser': _('unmute user'),\n             'createrule': _('create rule'),\n             'editrule': _('edit rule'),\n             'deleterule': _('delete rule'),\n            }\n\n    _text = {'banuser': _('banned'),\n             'wikibanned': _('wiki banned'),\n             'wikiunbanned': _('unbanned from wiki'),\n             'wikicontributor': _('added wiki contributor'),\n             'removewikicontributor': _('removed wiki contributor'),\n             'unbanuser': _('unbanned'),\n             'removelink': _('removed'),\n             'approvelink': _('approved'),\n             'removecomment': _('removed'),\n             'approvecomment': _('approved'),\n             'addmoderator': _('added moderator'),\n             'removemoderator': _('removed moderator'),\n             'invitemoderator': _('invited moderator'),\n             'uninvitemoderator': _('uninvited moderator'),\n             'acceptmoderatorinvite': _('accepted moderator invitation'),\n             'addcontributor': _('added approved contributor'),\n             'removecontributor': _('removed approved contributor'),\n             'editsettings': _('edited settings'),\n             'editflair': _('edited flair'),\n             'wikirevise': _('edited wiki page'),\n             'wikipermlevel': _('changed wiki page permission level'),\n             'wikipagelisted': _('changed wiki page listing preference'),\n             'distinguish': _('distinguished'),\n             'marknsfw': _('marked nsfw'),\n             'ignorereports': _('ignored reports'),\n             'unignorereports': _('unignored reports'),\n             'setpermissions': _('changed permissions on'),\n             'setsuggestedsort': _('set suggested sort'),\n             'sticky': _('stickied'),\n             'unsticky': _('unstickied'),\n             'setcontestmode': _('set contest mode on'),\n             'unsetcontestmode': _('unset contest mode on'),\n             'lock': _('locked'),\n             'unlock': _('unlocked'),\n             'muteuser': _('muted'),\n             'unmuteuser': _('unmuted'),\n             'createrule': _('created rule'),\n             'editrule': _('edited rule'),\n             'deleterule': _('deleted rule'),\n            }\n\n    _details_text = {# approve comment/link\n                     'unspam': _('unspam'),\n                     'confirm_ham': _('approved'),\n                     # remove comment/link\n                     'confirm_spam': _('confirmed spam'),\n                     'remove': _('removed not spam'),\n                     'spam': _('removed spam'),\n                     # removemoderator\n                     'remove_self': _('removed self'),\n                     # editsettings\n                     'title': _('title'),\n                     'public_description': _('description'),\n                     'description': _('sidebar'),\n                     'lang': _('language'),\n                     'type': _('type'),\n                     'link_type': _('link type'),\n                     'submit_link_label': _('submit link button label'),\n                     'submit_text_label': _('submit text post button label'),\n                     'comment_score_hide_mins': _('comment score hide period'),\n                     'over_18': _('toggle viewers must be over 18'),\n                     'allow_top': _('toggle allow in default/trending lists'),\n                     'show_media': _('toggle show thumbnail images of content'),\n                     'public_traffic': _('toggle public traffic stats page'),\n                     'collapse_deleted_comments': _('toggle collapse deleted/removed comments'),\n                     'exclude_banned_modqueue': _('toggle exclude banned users\\' posts from modqueue'),\n                     'domain': _('domain'),\n                     'show_cname_sidebar': _('toggle show sidebar from cname'),\n                     'css_on_cname': _('toggle custom CSS from cname'),\n                     'header_title': _('header title'),\n                     'stylesheet': _('stylesheet'),\n                     'del_header': _('delete header image'),\n                     'del_image': _('delete image'),\n                     'del_icon': _('delete icon image'),\n                     'del_banner': _('delete banner image'),\n                     'upload_image_header': _('upload header image'),\n                     'upload_image_icon': _('upload icon image'),\n                     'upload_image_banner': _('upload banner image'),\n                     'upload_image': _('upload image'),\n                     # editflair\n                     'flair_edit': _('add/edit flair'),\n                     'flair_delete': _('delete flair'),\n                     'flair_csv': _('edit by csv'),\n                     'flair_enabled': _('toggle flair enabled'),\n                     'flair_position': _('toggle user flair position'),\n                     'link_flair_position': _('toggle link flair position'),\n                     'flair_self_enabled': _('toggle user assigned flair enabled'),\n                     'link_flair_self_enabled': _('toggle submitter assigned link flair enabled'),\n                     'flair_template': _('add/edit flair templates'),\n                     'flair_delete_template': _('delete flair template'),\n                     'flair_clear_template': _('clear flair templates'),\n                     # distinguish/nsfw\n                     'remove': _('remove'),\n                     'ignore_reports': _('ignore reports'),\n                     # permissions\n                     'permission_moderator': _('set permissions on moderator'),\n                     'permission_moderator_invite': _('set permissions on moderator invitation')}\n\n    # NOTE: Wrapped ModAction objects are not cachable because wrapped_cache_key\n    # is not defined\n\n    @classmethod\n    def create(cls, sr, mod, action, details=None, target=None, description=None):\n        from r2.models import DefaultSR\n\n        if not action in cls.actions:\n            raise ValueError(\"Invalid ModAction: %s\" % action)\n\n        # Front page should insert modactions into the base sr\n        sr = sr._base if isinstance(sr, DefaultSR) else sr\n\n        kw = dict(sr_id36=sr._id36, mod_id36=mod._id36, action=action)\n\n        if target:\n            kw['target_fullname'] = target._fullname\n        if details:\n            kw['details'] = details\n        if description:\n            kw['description'] = description\n\n        ma = cls(**kw)\n        ma._commit()\n\n        g.events.mod_event(\n            modaction=ma,\n            subreddit=sr,\n            mod=mod,\n            target=target,\n            request=request if c.user_is_loggedin else None,\n            context=c if c.user_is_loggedin else None,\n        )\n\n        return ma\n\n    def _on_create(self):\n        \"\"\"\n        Update all Views.\n        \"\"\"\n\n        views = (ModActionBySR, ModActionBySRMod, ModActionBySRAction, \n                 ModActionBySRActionMod)\n\n        for v in views:\n            v.add_object(self)\n\n    @classmethod\n    def get_actions(cls, srs, mod=None, action=None, after=None, reverse=False, count=1000):\n        \"\"\"\n        Get a ColumnQuery that yields ModAction objects according to\n        specified criteria.\n        \"\"\"\n        if after and isinstance(after, basestring):\n            after = cls._byID(UUID(after))\n        elif after and isinstance(after, UUID):\n            after = cls._byID(after)\n\n        if not isinstance(after, cls):\n            after = None\n\n        srs = tup(srs)\n\n        if not mod and not action:\n            rowkeys = [sr._id36 for sr in srs]\n            q = ModActionBySR.query(rowkeys, after=after, reverse=reverse, count=count)\n        elif mod:\n            mods = tup(mod)\n            key = '%s_%s' if not action else '%%s_%%s_%s' % action\n            rowkeys = itertools.product([sr._id36 for sr in srs],\n                [mod._id36 for mod in mods])\n            rowkeys = [key % (sr, mod) for sr, mod in rowkeys]\n            view = ModActionBySRActionMod if action else ModActionBySRMod\n            q = view.query(rowkeys, after=after, reverse=reverse, count=count)\n        else:\n            rowkeys = ['%s_%s' % (sr._id36, action) for sr in srs]\n            q = ModActionBySRAction.query(rowkeys, after=after, reverse=reverse, count=count)\n\n        return q\n\n    @property\n    def details_text(self):\n        text = \"\"\n        if getattr(self, \"details\", None):\n            text += self._details_text.get(self.details, self.details)\n        if getattr(self, \"description\", None):\n            if text:\n                text += \": \"\n            text += self.description\n        return text\n\n    @classmethod\n    def add_props(cls, user, wrapped):\n        from r2.lib.db.thing import Thing\n        from r2.lib.menus import QueryButton\n        from r2.lib.pages import WrappedUser\n        from r2.models import (\n            Account,\n            Link,\n            ModSR,\n            MultiReddit,\n            Subreddit,\n        )\n\n        target_names = {item.target_fullname for item in wrapped\n                            if hasattr(item, \"target_fullname\")}\n        targets = Thing._by_fullname(target_names, data=True)\n\n        # get moderators\n        moderators = Account._byID36({item.mod_id36 for item in wrapped},\n                                     data=True)\n\n        # get authors for targets that are Links or Comments\n        target_author_names = {target.author_id for target in targets.values()\n                                    if hasattr(target, \"author_id\")}\n        target_authors = Account._byID(target_author_names, data=True)\n\n        # get parent links for targets that are Comments\n        parent_link_names = {target.link_id for target in targets.values()\n                                    if hasattr(target, \"link_id\")}\n        parent_links = Link._byID(parent_link_names, data=True)\n\n        # get subreddits\n        srs = Subreddit._byID36({item.sr_id36 for item in wrapped}, data=True)\n\n        for item in wrapped:\n            item.moderator = moderators[item.mod_id36]\n            item.subreddit = srs[item.sr_id36]\n            item.text = cls._text.get(item.action, '')\n            item.target = None\n            item.target_author = None\n\n            if hasattr(item, \"target_fullname\") and item.target_fullname:\n                item.target = targets[item.target_fullname]\n\n                if hasattr(item.target, \"author_id\"):\n                    author_name = item.target.author_id\n                    item.target_author = target_authors[author_name]\n\n                if hasattr(item.target, \"link_id\"):\n                    parent_link_name = item.target.link_id\n                    item.parent_link = parent_links[parent_link_name]\n\n                if isinstance(item.target, Account):\n                    item.target_author = item.target\n\n        if c.render_style == \"html\":\n            request_path = request.path\n\n            # make wrapped users for targets that are accounts\n            user_targets = filter(lambda target: isinstance(target, Account),\n                                  targets.values())\n            wrapped_user_targets = {user._fullname: WrappedUser(user)\n                                    for user in user_targets}\n\n            for item in wrapped:\n                if isinstance(item.target, Account):\n                    user_name = item.target._fullname\n                    item.wrapped_user_target = wrapped_user_targets[user_name]\n\n                css_class = 'modactions %s' % item.action\n                action_button = QueryButton(\n                    '', item.action, query_param='type', css_class=css_class)\n                action_button.build(base_path=request_path)\n                item.action_button = action_button\n\n                mod_button = QueryButton(\n                    item.moderator.name, item.moderator.name, query_param='mod')\n                mod_button.build(base_path=request_path)\n                item.mod_button = mod_button\n\n                if isinstance(c.site, ModSR) or isinstance(c.site, MultiReddit):\n                    rgb = item.subreddit.get_rgb()\n                    item.bgcolor = 'rgb(%s,%s,%s)' % rgb\n                    item.is_multi = True\n                else:\n                    item.bgcolor = \"rgb(255,255,255)\"\n                    item.is_multi = False\n\n\nclass ModActionBySR(tdb_cassandra.View):\n    _use_db = True\n    _connection_pool = 'main'\n    _compare_with = TIME_UUID_TYPE\n    _view_of = ModAction\n    _ttl = timedelta(days=90)\n    _read_consistency_level = tdb_cassandra.CL.ONE\n\n    @classmethod\n    def _rowkey(cls, ma):\n        return ma.sr_id36\n\nclass ModActionBySRMod(tdb_cassandra.View):\n    _use_db = True\n    _connection_pool = 'main'\n    _compare_with = TIME_UUID_TYPE\n    _view_of = ModAction\n    _ttl = timedelta(days=90)\n    _read_consistency_level = tdb_cassandra.CL.ONE\n\n    @classmethod\n    def _rowkey(cls, ma):\n        return '%s_%s' % (ma.sr_id36, ma.mod_id36)\n\nclass ModActionBySRActionMod(tdb_cassandra.View):\n    _use_db = True\n    _connection_pool = 'main'\n    _compare_with = TIME_UUID_TYPE\n    _view_of = ModAction\n    _ttl = timedelta(days=90)\n    _read_consistency_level = tdb_cassandra.CL.ONE\n\n    @classmethod\n    def _rowkey(cls, ma):\n        return '%s_%s_%s' % (ma.sr_id36, ma.mod_id36, ma.action)\n\nclass ModActionBySRAction(tdb_cassandra.View):\n    _use_db = True\n    _connection_pool = 'main'\n    _compare_with = TIME_UUID_TYPE\n    _view_of = ModAction\n    _ttl = timedelta(days=90)\n    _read_consistency_level = tdb_cassandra.CL.ONE\n\n    @classmethod\n    def _rowkey(cls, ma):\n        return '%s_%s' % (ma.sr_id36, ma.action)\n"
  },
  {
    "path": "r2/r2/models/printable.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom pylons import request\nfrom pylons import tmpl_context as c\n\nfrom r2.lib.strings import Score\nfrom r2.lib import hooks\n\n\nclass Printable(object):\n    show_spam = False\n    show_reports = False\n    is_special = False\n    can_ban = False\n    deleted = False\n    rowstyle_cls = ''\n    collapsed = False\n    author = None\n    margin = 0\n    is_focal = False\n    childlisting = None\n    cache_ignore = set(['c', 'author', 'score_fmt', 'child',\n                        # displayed score is cachable, so remove score\n                        # related fields.\n                        'voting_score', 'display_score',\n                        'render_score', 'score', '_score', \n                        'upvotes', '_ups',\n                        'downvotes', '_downs',\n                        'subreddit_slow', '_deleted', '_spam',\n                        'cachable', 'make_permalink', 'permalink',\n                        'timesince',\n                        'num',  # listings only, replaced by CachedVariable\n                        'rowstyle_cls',  # listings only, replaced by CachedVariable\n                        'upvote_ratio',\n                        'should_incr_counts',\n                        'keep_item',\n                        ])\n\n    @classmethod\n    def update_nofollow(cls, user, wrapped):\n        pass\n\n    @classmethod\n    def add_props(cls, user, wrapped):\n        from r2.lib.wrapped import CachedVariable\n        for item in wrapped:\n            # insert replacement variable for timesince to allow for\n            # caching of thing templates\n            item.display = CachedVariable(\"display\")\n            item.timesince = CachedVariable(\"timesince\")\n            item.childlisting = CachedVariable(\"childlisting\")\n\n            score_fmt = getattr(item, \"score_fmt\", Score.number_only)\n            item.display_score = map(score_fmt, item.voting_score)\n\n            if item.cachable:\n                item.render_score  = item.display_score\n                item.display_score = map(CachedVariable,\n                                         [\"scoredislikes\", \"scoreunvoted\",\n                                          \"scorelikes\"])\n\n        hooks.get_hook(\"add_props\").call(items=wrapped)\n\n    @property\n    def permalink(self, *a, **kw):\n        raise NotImplementedError\n\n    def keep_item(self, wrapped):\n        return True\n\n    @staticmethod\n    def wrapped_cache_key(wrapped, style):\n        s = [wrapped._fullname, wrapped._spam]\n\n        # Printables can contain embedded WrappedUsers, which need to consider\n        # the site and user's flair settings. Add something to the key\n        # indicating there might be flair--we haven't built the WrappedUser yet\n        # so we can't check to see if there's actually flair.\n        if c.site.flair_enabled and c.user.pref_show_flair:\n            s.append('user_flair_enabled')\n\n        if style == 'htmllite':\n            s.extend([c.bgcolor, c.bordercolor, \n                      request.GET.has_key('style'),\n                      request.GET.get(\"expanded\"),\n                      getattr(wrapped, 'embed_voting_style', None)])\n        return s\n"
  },
  {
    "path": "r2/r2/models/promo.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom collections import OrderedDict\nfrom datetime import datetime\nfrom uuid import uuid1\n\nfrom pycassa.system_manager import INT_TYPE, TIME_UUID_TYPE, UTF8_TYPE\nfrom pylons import tmpl_context as c\nfrom pylons import app_globals as g\nfrom pylons.i18n import _, N_\n\nfrom r2.config import feature\nfrom r2.lib.unicode import _force_unicode\nfrom r2.lib.db import tdb_cassandra\nfrom r2.lib.db.thing import Thing\nfrom r2.lib.utils import Enum, to_datetime\nfrom r2.models.subreddit import Subreddit, Frontpage\n\n\nPROMOTE_STATUS = Enum(\"unpaid\", \"unseen\", \"accepted\", \"rejected\",\n                      \"pending\", \"promoted\", \"finished\", \"edited_live\")\n\nPROMOTE_COST_BASIS = Enum('fixed_cpm', 'cpm', 'cpc',)\n\n\nclass PriorityLevel(object):\n    name = ''\n    _text = N_('')\n    _description = N_('')\n    default = False\n    inventory_override = False\n\n    def __repr__(self):\n        return \"<PriorityLevel %s>\" % self.name\n\n    @property\n    def text(self):\n        return _(self._text) if self._text else ''\n\n    @property\n    def description(self):\n        return _(self._description) if self._description else ''\n\n\nclass HighPriority(PriorityLevel):\n    name = 'high'\n    _text = N_('highest')\n\n\nclass MediumPriority(PriorityLevel):\n    name = 'standard'\n    _text = N_('standard')\n    default = True\n\n\nclass RemnantPriority(PriorityLevel):\n    name = 'remnant'\n    _text = N_('remnant')\n    _description = N_('lower priority, impressions are not guaranteed')\n    inventory_override = True\n\n\nclass HousePriority(PriorityLevel):\n    name = 'house'\n    _text = N_('house')\n    _description = N_('non-CPM, displays in all unsold impressions')\n    inventory_override = True\n\n\nclass AuctionPriority(PriorityLevel):\n    name = 'auction'\n    _text = N_('auction')\n    _description = N_('auction priority; all self-serve are auction priority')\n    inventory_override = True\n\n\nHIGH, MEDIUM, REMNANT, HOUSE, AUCTION = (HighPriority(), MediumPriority(),\n                                         RemnantPriority(), HousePriority(),\n                                         AuctionPriority(),)\nPROMOTE_PRIORITIES = OrderedDict((p.name, p) for p in (HIGH, MEDIUM, REMNANT,\n                                                       HOUSE, AUCTION,))\n\n\ndef PROMOTE_DEFAULT_PRIORITY(context=None):\n    if (context and (not feature.is_enabled('ads_auction') or\n                     context.user_is_sponsor)):\n        return MEDIUM\n    else:\n        return AUCTION\n\nclass Location(object):\n    DELIMITER = '-'\n    def __init__(self, country, region=None, metro=None):\n        self.country = country or None\n        self.region = region or None\n        self.metro = metro or None\n\n    def __repr__(self):\n        return '<%s (%s/%s/%s)>' % (self.__class__.__name__, self.country,\n                                    self.region, self.metro)\n\n    def to_code(self):\n        fields = [self.country, self.region, self.metro]\n        return self.DELIMITER.join(i or '' for i in fields)\n\n    @classmethod\n    def from_code(cls, code):\n        country, region, metro = [i or None for i in code.split(cls.DELIMITER)]\n        return cls(country, region, metro)\n\n    def contains(self, other):\n        if not self.country:\n            # self is set of all countries, it includes all possible\n            # values of other.country\n            return True\n        elif not other or not other.country:\n            # self is more specific than other\n            return False\n        else:\n            # both self and other specify a country\n            if self.country != other.country:\n                # countries don't match\n                return False\n            else:\n                # countries match\n                if not self.metro:\n                    # self.metro is set of all metros within country, it\n                    # includes all possible values of other.metro\n                    return True\n                elif not other.metro:\n                    # self is more specific than other\n                    return False\n                else:\n                    return self.metro == other.metro\n\n    def __eq__(self, other):\n        if not isinstance(other, Location):\n            return False\n\n        return (self.country == other.country and\n                self.region == other.region and\n                self.metro == other.metro)\n\n    def __ne__(self, other):\n        return not self.__eq__(other)\n\n\ndef calc_impressions(total_budget_pennies, cpm_pennies):\n    return int(total_budget_pennies / cpm_pennies * 1000)\n\n\nNO_TRANSACTION = 0\n\n\nclass Collection(object):\n    def __init__(self, name, sr_names, over_18=False, description=None,\n            is_spotlight=False):\n        self.name = name\n        self.over_18 = over_18\n        self.sr_names = sr_names\n        self.description = description\n        self.is_spotlight = is_spotlight\n\n    @classmethod\n    def by_name(cls, name):\n        return CollectionStorage.get_collection(name)\n\n    @classmethod\n    def get_all(cls):\n        \"\"\"\n        Return collections in this order:\n        1. SFW/NSFW\n        2. Spotlighted\n        3. Alphabetical\n        \"\"\"\n        all_collections = CollectionStorage.get_all()\n        sorted_collections = sorted(all_collections, key=lambda collection:\n            (collection.over_18, -collection.is_spotlight,\n            collection.name.lower()))\n        return sorted_collections\n\n    def __repr__(self):\n        return \"<%s: %s>\" % (self.__class__.__name__, self.name)\n\n\nclass CollectionStorage(tdb_cassandra.View):\n    _use_db = True\n    _connection_pool = 'main'\n    _extra_schema_creation_args = {\n        \"key_validation_class\": UTF8_TYPE,\n        \"column_name_class\": UTF8_TYPE,\n        \"default_validation_class\": UTF8_TYPE,\n    }\n    _compare_with = UTF8_TYPE\n    _read_consistency_level = tdb_cassandra.CL.ONE\n    _write_consistency_level = tdb_cassandra.CL.QUORUM\n    SR_NAMES_DELIM = '|'\n\n    @classmethod\n    def _from_columns(cls, name, columns):\n        description = columns['description']\n        sr_names = columns['sr_names'].split(cls.SR_NAMES_DELIM)\n        over_18 = columns.get(\"over_18\") == \"True\"\n        is_spotlight = columns.get(\"is_spotlight\") == \"True\"\n        return Collection(name, sr_names, over_18=over_18,\n            description=description, is_spotlight=is_spotlight)\n\n    @classmethod\n    def _to_columns(cls, description, srs, over_18, is_spotlight):\n        columns = {\n            'description': description,\n            'sr_names': cls.SR_NAMES_DELIM.join(sr.name for sr in srs),\n            'over_18': str(over_18),\n            'is_spotlight': str(is_spotlight),\n        }\n        return columns\n\n    @classmethod\n    def set(cls, name, description, srs, over_18=False, is_spotlight=False):\n        rowkey = name\n        columns = cls._to_columns(description, srs, over_18, is_spotlight)\n        cls._set_values(rowkey, columns)\n\n    @classmethod\n    def _set_attributes(cls, name, attributes):\n        rowkey = name\n        for key in attributes:\n            if not hasattr(Collection.by_name(name), key):\n                raise AttributeError('No attribute on %s called %s'\n                    % (name, key))\n\n        columns = attributes\n        cls._set_values(rowkey, columns)\n\n    @classmethod\n    def set_over_18(cls, name, over_18):\n        cls._set_attributes(name, {'over_18': str(over_18)})\n\n    @classmethod\n    def set_is_spotlight(cls, name, is_spotlight):\n        cls._set_attributes(name, {'is_spotlight': str(is_spotlight)})\n\n    @classmethod\n    def get_collection(cls, name):\n        if not name:\n            return None\n\n        rowkey = name\n        try:\n            columns = cls._cf.get(rowkey)\n        except tdb_cassandra.NotFoundException:\n            return None\n\n        return cls._from_columns(name, columns)\n\n    @classmethod\n    def get_all(cls):\n        ret = []\n        for name, columns in cls._cf.get_range():\n            ret.append(cls._from_columns(name, columns))\n        return ret\n\n    @classmethod\n    def delete(cls, name):\n        rowkey = name\n        cls._cf.remove(rowkey)\n\n\nclass Target(object):\n    \"\"\"Wrapper around either a Collection or a Subreddit name\"\"\"\n    def __init__(self, target):\n        if isinstance(target, Collection):\n            self.collection = target\n            self.is_collection = True\n        elif isinstance(target, basestring):\n            self.subreddit_name = target\n            self.is_collection = False\n        else:\n            raise ValueError(\"target must be a Collection or Subreddit name\")\n\n        # defer looking up subreddits, we might only need their names\n        self._subreddits = None\n\n    @property\n    def over_18(self):\n        if self.is_collection:\n            return self.collection.over_18\n        else:\n            subreddits = self.subreddits_slow\n            return subreddits and subreddits[0].over_18\n\n    @property\n    def subreddit_names(self):\n        if self.is_collection:\n            return self.collection.sr_names\n        else:\n            return [self.subreddit_name]\n\n    @property\n    def subreddits_slow(self):\n        if self._subreddits is not None:\n            return self._subreddits\n\n        sr_names = self.subreddit_names\n        srs = Subreddit._by_name(sr_names).values()\n        self._subreddits = srs\n        return srs\n\n    def __eq__(self, other):\n        if self.is_collection != other.is_collection:\n            return False\n\n        return set(self.subreddit_names) == set(other.subreddit_names)\n\n    def __ne__(self, other):\n        return not self.__eq__(other)\n\n    @property\n    def pretty_name(self):\n        if self.is_collection:\n            return _(\"collection: %(name)s\") % {'name': self.collection.name}\n        elif self.subreddit_name == Frontpage.name:\n            return _(\"frontpage\")\n        else:\n            return \"/r/%s\" % self.subreddit_name\n\n    def __repr__(self):\n        return \"<%s: %s>\" % (self.__class__.__name__, self.pretty_name)\n\n\nclass PromoCampaign(Thing):\n    _cache = g.thingcache\n    _defaults = dict(\n        priority_name=PROMOTE_DEFAULT_PRIORITY().name,\n        trans_id=NO_TRANSACTION,\n        trans_ip=None,\n        trans_ip_country=None,\n        trans_billing_country=None,\n        trans_country_match=None,\n        location_code=None,\n        platform='desktop',\n        mobile_os_names=None,\n        ios_device_names=None,\n        ios_version_names=None,\n        android_device_names=None,\n        android_version_names=None,\n        frequency_cap=None,\n        has_served=False,\n        paused=False,\n        total_budget_pennies=0,\n        cost_basis=PROMOTE_COST_BASIS.fixed_cpm,\n        bid_pennies=g.default_bid_pennies,\n        adserver_spent_pennies=0,\n    )\n\n    # special attributes that shouldn't set Thing data attributes because they\n    # have special setters that set other data attributes\n    _derived_attrs = (\n        \"location\",\n        \"priority\",\n        \"target\",\n        \"mobile_os\",\n        \"ios_devices\",\n        \"ios_version_range\",\n        \"android_devices\",\n        \"android_version_range\",\n        \"is_auction\",\n    )\n\n    SR_NAMES_DELIM = '|'\n    SUBREDDIT_TARGET = \"subreddit\"\n    MOBILE_TARGET_DELIM = ','\n\n    @classmethod\n    def _cache_prefix(cls):\n        return \"campaign:\"\n\n    def __getattr__(self, attr):\n        val = super(PromoCampaign, self).__getattr__(attr)\n\n        if (attr == 'total_budget_pennies' and hasattr(self, 'bid') and\n                not getattr(self, 'bid_migrated', False)):\n            old_bid = int(super(PromoCampaign, self).__getattr__('bid') * 100)\n            self.total_budget_pennies = old_bid\n            self.bid_migrated = True\n            return self.total_budget_pennies\n\n        if (attr == 'bid_pennies' and hasattr(self, 'cpm') and\n                not getattr(self, 'cpm_migrated', False)):\n            old_cpm = super(PromoCampaign, self).__getattr__('cpm')\n            self.bid_pennies = old_cpm\n            self.cpm_migrated = True\n            return self.bid_pennies\n\n        if attr in ('start_date', 'end_date'):\n            val = to_datetime(val)\n            if not val.tzinfo:\n                val = val.replace(tzinfo=g.tz)\n        return val\n\n    def __setattr__(self, attr, val, make_dirty=True):\n        if attr in self._derived_attrs:\n            object.__setattr__(self, attr, val)\n        else:\n            Thing.__setattr__(self, attr, val, make_dirty=make_dirty)\n\n    def __getstate__(self):\n        \"\"\"\n        Remove _target before returning object state for pickling.\n\n        Thing objects are pickled for caching. The state of the object is\n        obtained by calling the __getstate__ method. Remove the _target\n        attribute because it may contain Subreddits or other non-trivial objects\n        that shouldn't be included.\n\n        \"\"\"\n\n        state = self.__dict__\n        if \"_target\" in state:\n            state = {k: v for k, v in state.iteritems() if k != \"_target\"}\n        return state\n\n    @property\n    def is_auction(self):\n        if (self.cost_basis is not PROMOTE_COST_BASIS.fixed_cpm):\n            return True\n\n        return False\n\n    def priority_name_from_priority(self, priority):\n        if not priority in PROMOTE_PRIORITIES.values():\n            raise ValueError(\"%s is not a valid priority\" % priority.name)\n        return priority.name\n\n    @classmethod\n    def location_code_from_location(cls, location):\n        return location.to_code() if location else None\n\n    @classmethod\n    def unpack_target(cls, target):\n        \"\"\"Convert a Target into attributes suitable for storage.\"\"\"\n        sr_names = target.subreddit_names\n        target_sr_names = cls.SR_NAMES_DELIM.join(sr_names)\n        target_name = (target.collection.name if target.is_collection\n                                              else cls.SUBREDDIT_TARGET)\n        return target_sr_names, target_name\n\n    @classmethod\n    def create(cls, link, target, start_date, end_date,\n               frequency_cap, priority, location,\n               platform, mobile_os, ios_devices, ios_version_range,\n               android_devices, android_version_range, total_budget_pennies,\n               cost_basis, bid_pennies):\n        pc = PromoCampaign(\n            link_id=link._id,\n            start_date=start_date,\n            end_date=end_date,\n            trans_id=NO_TRANSACTION,\n            owner_id=link.author_id,\n            total_budget_pennies=total_budget_pennies,\n            cost_basis=cost_basis,\n            bid_pennies=bid_pennies,\n        )\n        pc.frequency_cap = frequency_cap\n        pc.priority = priority\n        pc.location = location\n        pc.target = target\n        pc.platform = platform\n        pc.mobile_os = mobile_os\n        pc.ios_devices = ios_devices\n        pc.ios_version_range = ios_version_range\n        pc.android_devices = android_devices\n        pc.android_version_range = android_version_range\n        pc._commit()\n        return pc\n\n    @classmethod\n    def _by_link(cls, link_id):\n        '''\n        Returns an iterable of campaigns associated with link_id or an empty\n        list if there are none.\n        '''\n        return cls._query(PromoCampaign.c.link_id == link_id, data=True)\n\n    @classmethod\n    def _by_user(cls, account_id):\n        '''\n        Returns an iterable of all campaigns owned by account_id or an empty\n        list if there are none.\n        '''\n        return cls._query(PromoCampaign.c.owner_id == account_id, data=True)\n\n    @property\n    def ndays(self):\n        return (self.end_date - self.start_date).days\n\n    @property\n    def impressions(self):\n        if self.cost_basis == PROMOTE_COST_BASIS.fixed_cpm:\n            return calc_impressions(self.total_budget_pennies, self.bid_pennies)\n\n        return 0\n\n    @property\n    def priority(self):\n        return PROMOTE_PRIORITIES[self.priority_name]\n\n    @priority.setter\n    def priority(self, priority):\n        self.priority_name = self.priority_name_from_priority(priority)\n\n    @property\n    def location(self):\n        if self.location_code is not None:\n            return Location.from_code(self.location_code)\n        else:\n            return None\n\n    @location.setter\n    def location(self, location):\n        self.location_code = self.location_code_from_location(location)\n\n    @property\n    def target(self):\n        if hasattr(self, \"_target\"):\n            return self._target\n\n        sr_names = self.target_sr_names.split(self.SR_NAMES_DELIM)\n        if self.target_name == self.SUBREDDIT_TARGET:\n            sr_name = sr_names[0]\n            target = Target(sr_name)\n        else:\n            collection = Collection(self.target_name, sr_names)\n            target = Target(collection)\n\n        self._target = target\n        return target\n\n    @target.setter\n    def target(self, target):\n        self.target_sr_names, self.target_name = self.unpack_target(target)\n\n        # set _target so we don't need to lookup on subsequent access\n        self._target = target\n\n    def _mobile_target_getter(self, target):\n        if not target:\n            return None\n        else:\n            return target.split(self.MOBILE_TARGET_DELIM)\n\n    def _mobile_target_setter(self, target_names):\n        if not target_names:\n            return None\n        else:\n            return self.MOBILE_TARGET_DELIM.join(target_names)\n\n    @property\n    def mobile_os(self):\n        return self._mobile_target_getter(self.mobile_os_names)\n\n    @mobile_os.setter\n    def mobile_os(self, mobile_os_names):\n        self.mobile_os_names = self._mobile_target_setter(mobile_os_names)\n\n    @property\n    def ios_devices(self):\n        return self._mobile_target_getter(self.ios_device_names)\n\n    @ios_devices.setter\n    def ios_devices(self, ios_device_names):\n        self.ios_device_names = self._mobile_target_setter(ios_device_names)\n\n    @property\n    def android_devices(self):\n        return self._mobile_target_getter(self.android_device_names)\n\n    @android_devices.setter\n    def android_devices(self, android_device_names):\n        self.android_device_names = self._mobile_target_setter(android_device_names)\n\n    @property\n    def ios_version_range(self):\n        return self._mobile_target_getter(self.ios_version_names)\n\n    @ios_version_range.setter\n    def ios_version_range(self, ios_version_names):\n        self.ios_version_names = self._mobile_target_setter(ios_version_names)\n\n    @property\n    def android_version_range(self):\n        return self._mobile_target_getter(self.android_version_names)\n\n    @android_version_range.setter\n    def android_version_range(self, android_version_names):\n        self.android_version_names = self._mobile_target_setter(android_version_names)\n\n    @property\n    def location_str(self):\n        if not self.location:\n            return ''\n        elif self.location.region:\n            country = self.location.country\n            region = self.location.region\n            if self.location.metro:\n                metro_str = (g.locations[country]['regions'][region]\n                             ['metros'][self.location.metro]['name'])\n                return '/'.join([country, region, metro_str])\n            else:\n                region_name = g.locations[country]['regions'][region]['name']\n                return ('%s, %s' % (region_name, country))\n        else:\n            return g.locations[self.location.country]['name']\n\n    @property\n    def is_paid(self):\n        return self.trans_id != 0 or self.priority == HOUSE\n\n    def is_freebie(self):\n        return self.trans_id < 0\n\n    def is_live_now(self):\n        now = datetime.now(g.tz)\n        return self.start_date < now and self.end_date > now\n\n    @property\n    def is_house(self):\n       return self.priority == HOUSE\n\n    @property\n    def total_budget_dollars(self):\n        return self.total_budget_pennies / 100.\n\n    @property\n    def bid_dollars(self):\n        return self.bid_pennies / 100.\n\n    def delete(self):\n        self._deleted = True\n        self._commit()\n\n\ndef backfill_campaign_targets():\n    from r2.lib.db.operators import desc\n    from r2.lib.utils import fetch_things2\n\n    q = PromoCampaign._query(sort=desc(\"_date\"), data=True)\n    for campaign in fetch_things2(q):\n        sr_name = campaign.sr_name or Frontpage.name\n        campaign.target = Target(sr_name)\n        campaign._commit()\n\nclass PromotionLog(tdb_cassandra.View):\n    _use_db = True\n    _connection_pool = 'main'\n    _compare_with = TIME_UUID_TYPE\n\n    @classmethod\n    def _rowkey(cls, link):\n        return link._fullname\n\n    @classmethod\n    def add(cls, link, text):\n        name = c.user.name if c.user_is_loggedin else \"<AUTOMATED>\"\n        now = datetime.now(g.tz).strftime(\"%Y-%m-%d %H:%M:%S\")\n        text = \"[%s: %s] %s\" % (name, now, text)\n        rowkey = cls._rowkey(link)\n        column = {uuid1(): _force_unicode(text)}\n        cls._set_values(rowkey, column)\n        return text\n\n    @classmethod\n    def get(cls, link):\n        rowkey = cls._rowkey(link)\n        try:\n            row = cls._byID(rowkey)\n        except tdb_cassandra.NotFound:\n            return []\n        tuples = sorted(row._values().items(), key=lambda t: t[0].time)\n        return [t[1] for t in tuples]\n\n\nclass PromotionPrices(tdb_cassandra.View):\n    \"\"\"\n    Check all the following potentially specially priced conditions:\n    * metro level targeting\n    * country level targeting (but not if the metro targeting is used)\n    * collection targeting\n    * frontpage targeting\n    * subreddit targeting\n\n    The price is the maximum price for all matching conditions. If no special\n    conditions are met use the global price.\n\n    \"\"\"\n\n    _use_db = True\n    _connection_pool = 'main'\n    _read_consistency_level = tdb_cassandra.CL.ONE\n    _write_consistency_level = tdb_cassandra.CL.ALL\n    _extra_schema_creation_args = {\n        \"key_validation_class\": UTF8_TYPE,\n        \"column_name_class\": UTF8_TYPE,\n        \"default_validation_class\": INT_TYPE,\n    }\n\n    COLLECTION_DEFAULT = g.cpm_selfserve_collection.pennies\n    SUBREDDIT_DEFAULT = g.cpm_selfserve.pennies\n    COUNTRY_DEFAULT = g.cpm_selfserve_geotarget_country.pennies\n    METRO_DEFAULT = g.cpm_selfserve_geotarget_metro.pennies\n\n    @classmethod\n    def _rowkey_and_column_from_target(cls, target):\n        rowkey = column_name = None\n\n        if isinstance(target, Target):\n            if target.is_collection:\n                rowkey = \"COLLECTION\"\n                column_name = target.collection.name\n            else:\n                rowkey = \"SUBREDDIT\"\n                column_name = target.subreddit_name\n\n        if not rowkey or not column_name:\n            raise ValueError(\"target must be Target\")\n\n        return rowkey, column_name\n\n    @classmethod\n    def _rowkey_and_column_from_location(cls, location):\n        if not isinstance(location, Location):\n            raise ValueError(\"location must be Location\")\n\n        if location.metro:\n            rowkey = \"METRO\"\n            # NOTE: the column_name will also be the key used in the frontend\n            # to determine pricing\n            column_name = ''.join(map(str, (location.country, location.metro)))\n        else:\n            rowkey = \"COUNTRY\"\n            column_name = location.country\n        return rowkey, column_name\n\n    @classmethod\n    def set_target_price(cls, target, cpm):\n        rowkey, column_name = cls._rowkey_and_column_from_target(target)\n        cls._cf.insert(rowkey, {column_name: cpm})\n\n    @classmethod\n    def set_location_price(cls, location, cpm):\n        rowkey, column_name = cls._rowkey_and_column_from_location(location)\n        cls._cf.insert(rowkey, {column_name: cpm})\n\n    @classmethod\n    def lookup_target_price(cls, target, default):\n        rowkey, column_name = cls._rowkey_and_column_from_target(target)\n        target_price = cls._lookup_price(rowkey, column_name)\n        return target_price or default\n\n    @classmethod\n    def lookup_location_price(cls, location, default):\n        rowkey, column_name = cls._rowkey_and_column_from_location(location)\n        location_price = cls._lookup_price(rowkey, column_name)\n        return location_price or default\n\n    @classmethod\n    def _lookup_price(cls, rowkey, column_name):\n        try:\n            columns = cls._cf.get(rowkey, columns=[column_name])\n        except tdb_cassandra.NotFoundException:\n            columns = {}\n\n        return columns.get(column_name)\n\n    @classmethod\n    def get_price(cls, user, target, location):\n        if user.selfserve_cpm_override_pennies:\n            return user.selfserve_cpm_override_pennies\n\n        prices = []\n\n        # set location specific prices or use defaults\n        if location and location.metro:\n            metro_price = cls.lookup_location_price(location, cls.METRO_DEFAULT)\n            prices.append(metro_price)\n        elif location:\n            country_price = cls.lookup_location_price(\n                location, cls.COUNTRY_DEFAULT)\n            prices.append(country_price)\n\n        # set target specific prices or use default\n        if (not target.is_collection and\n                target.subreddit_name == Frontpage.name):\n            # Frontpage is priced as a collection\n            prices.append(cls.COLLECTION_DEFAULT)\n        elif target.is_collection:\n            collection_price = cls.lookup_target_price(\n                target, cls.COLLECTION_DEFAULT)\n            prices.append(collection_price)\n        else:\n            subreddit_price = cls.lookup_target_price(\n                target, cls.SUBREDDIT_DEFAULT)\n            prices.append(subreddit_price)\n\n        return max(prices)\n\n    @classmethod\n    def get_price_dict(cls, user):\n        if user.selfserve_cpm_override_pennies:\n            r = {\n                \"COLLECTION\": {},\n                \"SUBREDDIT\": {},\n                \"COUNTRY\": {},\n                \"METRO\": {},\n                \"COLLECTION_DEFAULT\": user.selfserve_cpm_override_pennies,\n                \"SUBREDDIT_DEFAULT\": user.selfserve_cpm_override_pennies,\n                \"COUNTRY_DEFAULT\": user.selfserve_cpm_override_pennies,\n                \"METRO_DEFAULT\": user.selfserve_cpm_override_pennies,\n            }\n        else:\n            r = {\n                \"COLLECTION\": {},\n                \"SUBREDDIT\": {},\n                \"COUNTRY\": {},\n                \"METRO\": {},\n                \"COLLECTION_DEFAULT\": g.cpm_selfserve_collection.pennies,\n                \"SUBREDDIT_DEFAULT\": g.cpm_selfserve.pennies,\n                \"COUNTRY_DEFAULT\": g.cpm_selfserve_geotarget_country.pennies,\n                \"METRO_DEFAULT\": g.cpm_selfserve_geotarget_metro.pennies,\n            }\n\n            try:\n                collections = cls._cf.get(\"COLLECTION\")\n            except tdb_cassandra.NotFoundException:\n                collections = {}\n\n            try:\n                subreddits = cls._cf.get(\"SUBREDDIT\")\n            except tdb_cassandra.NotFoundException:\n                subreddits = {}\n\n            try:\n                countries = cls._cf.get(\"COUNTRY\")\n            except tdb_cassandra.NotFoundException:\n                countries = {}\n\n            try:\n                metros = cls._cf.get(\"METRO\")\n            except tdb_cassandra.NotFoundException:\n                metros = {}\n\n            for name, cpm in collections.iteritems():\n                r[\"COLLECTION\"][name] = cpm\n\n            for name, cpm in subreddits.iteritems():\n                r[\"SUBREDDIT\"][name] = cpm\n\n            for name, cpm in countries.iteritems():\n                r[\"COUNTRY\"][name] = cpm\n\n            for name, cpm in metros.iteritems():\n                r[\"METRO\"][name] = cpm\n\n        return r\n"
  },
  {
    "path": "r2/r2/models/promo_metrics.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom itertools import product\n\nfrom pycassa.types import IntegerType\n\nfrom r2.lib.db import tdb_cassandra\nfrom r2.lib.utils import tup\n\n\nclass PromoMetrics(tdb_cassandra.View):\n    '''\n    Cassandra data store for promotion metrics. Used for inventory prediction.\n\n    Usage:\n      # set metric value for many subreddits at once\n      > PromoMetrics.set('min_daily_pageviews.GET_listing',\n                          {'funny': 63432, 'pics': 48829, 'books': 4})\n\n      # get metric value for one subreddit\n      > res = PromoMetrics.get('min_daily_pageviews.GET_listing', 'funny')\n      {'funny': 1234}\n\n      # get metric value for many subreddits\n      > res = PromoMetrics.get('min_daily_pageviews.GET_listing',\n                               ['funny', 'pics'])\n      {'funny':1234, 'pics':4321}\n\n      # get metric values for all subreddits\n      > res = PromoMetrics.get('min_daily_pageviews.GET_listing')\n    '''\n    _use_db = True\n    _value_type = 'int'\n    _fetch_all_columns = True\n\n    @classmethod\n    def get(cls, metric_name, sr_names=None):\n        sr_names = tup(sr_names)\n        try:\n            metric = cls._byID(metric_name, properties=sr_names)\n            return metric._values()  # might have additional values\n        except tdb_cassandra.NotFound:\n            return {}\n\n    @classmethod\n    def set(cls, metric_name, values_by_sr):\n        cls._set_values(metric_name, values_by_sr)\n\n\nclass LocationPromoMetrics(tdb_cassandra.View):\n    _use_db = True\n    _write_consistency_level = tdb_cassandra.CL.QUORUM\n    _read_consistency_level = tdb_cassandra.CL.ONE\n    _extra_schema_creation_args = {\n        \"default_validation_class\": IntegerType(),\n    }\n\n    @classmethod\n    def _rowkey(cls, location):\n        fields = [location.country, location.region, location.metro]\n        return '-'.join(map(lambda field: field or '', fields))\n\n    @classmethod\n    def _column_name(cls, sr):\n        return sr.name\n\n    @classmethod\n    def get(cls, srs, locations):\n        srs, srs_is_single = tup(srs, ret_is_single=True)\n        locations, locations_is_single = tup(locations, ret_is_single=True)\n        is_single = srs_is_single and locations_is_single\n\n        rowkeys = {location: cls._rowkey(location) for location in locations}\n        columns = {sr: cls._column_name(sr) for sr in srs}\n        rcl = cls._read_consistency_level\n        metrics = cls._cf.multiget(rowkeys.values(), columns.values(),\n                                   read_consistency_level=rcl)\n        ret = {}\n\n        for sr, location in product(srs, locations):\n            rowkey = rowkeys[location]\n            column = columns[sr]\n            impressions = metrics.get(rowkey, {}).get(column, 0)\n            ret[(sr, location)] = impressions\n\n        if is_single:\n            return ret.values()[0]\n        else:\n            return ret\n\n    @classmethod\n    def set(cls, metrics):\n        wcl = cls._write_consistency_level\n        with cls._cf.batch(write_consistency_level=wcl) as b:\n            for location, sr, impressions in metrics:\n                rowkey = cls._rowkey(location)\n                column = {cls._column_name(sr): impressions}\n                b.insert(rowkey, column)\n"
  },
  {
    "path": "r2/r2/models/query_cache.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\"\"\"\nThis module provides a Cassandra-backed lockless query cache.  Rather than\ndoing complicated queries on the fly to populate listings, a list of items that\nwould be in that listing are maintained in Cassandra for fast lookup.  The\nresult can then be fed to IDBuilder to generate a final result.\n\nWhenever an operation occurs that would modify the contents of the listing, the\nlisting should be updated somehow.  In some cases, this can be done by directly\nmutating the listing and in others it must be done offline in batch processing\njobs.\n\n\"\"\"\n\nimport json\nimport random\nimport datetime\nimport collections\n\nfrom pylons import app_globals as g\nfrom pycassa.system_manager import ASCII_TYPE, UTF8_TYPE\nfrom pycassa.batch import Mutator\n\nfrom r2.models import Thing\nfrom r2.lib.db import tdb_cassandra\nfrom r2.lib.db.operators import asc, desc, BooleanOp\nfrom r2.lib.db.sorts import epoch_seconds\nfrom r2.lib.utils import flatten, to36\n\n\nCONNECTION_POOL = g.cassandra_pools['main']\nPRUNE_CHANCE = g.querycache_prune_chance\nMAX_CACHED_ITEMS = 1000\nLOG = g.log\n\n\nclass ThingTupleComparator(object):\n    \"\"\"A callable usable for comparing sort-data in a cached query.\n\n    The query cache stores minimal sort data on each thing to be able to order\n    the items in a cached query.  This class provides the ordering for those\n    thing tuples.\n\n    \"\"\"\n\n    def __init__(self, sorts):\n        self.sorts = sorts\n\n    def __call__(self, t1, t2):\n        for i, s in enumerate(self.sorts):\n            # t1 and t2 are tuples of (fullname, *sort_cols), so we\n            # can get the value to compare right out of the tuple\n            v1, v2 = t1[i + 1], t2[i + 1]\n            if v1 != v2:\n                return cmp(v1, v2) if isinstance(s, asc) else cmp(v2, v1)\n        #they're equal\n        return 0\n\n\nclass _CachedQueryBase(object):\n    def __init__(self, sort):\n        self.sort = sort\n        self.sort_cols = [s.col for s in self.sort]\n        self.data = []\n        self._fetched = False\n\n    def fetch(self, force=False):\n        \"\"\"Fill the cached query's sorted item list from Cassandra.\n\n        If the query has already been fetched, this method is a no-op unless\n        force=True.\n\n        \"\"\"\n        if not force and self._fetched:\n            return\n\n        self._fetch()\n        self._sort_data()\n        self._fetched = True\n\n    def _fetch(self):\n        raise NotImplementedError()\n\n    def _sort_data(self):\n        comparator = ThingTupleComparator(self.sort_cols)\n        self.data.sort(cmp=comparator)\n\n    def __iter__(self):\n        self.fetch()\n\n        for x in self.data[:MAX_CACHED_ITEMS]:\n            yield x[0]\n\n\nclass CachedQuery(_CachedQueryBase):\n    \"\"\"A materialized view of a complex query.\n\n    Complicated queries can take way too long to sort in the databases.  This\n    class provides a fast-access view of a given listing's items.  The cache\n    stores each item's ID and a minimal subset of its data as required for\n    sorting.\n\n    Each time the listing is fetched, it is sorted. Because of this, we need to\n    ensure the listing does not grow too large.  On each insert, a \"pruning\"\n    can occur (with a configurable probability) which will remove excess items\n    from the end of the listing.\n\n    Use CachedQueryMutator to make changes to the cached query's item list.\n\n    \"\"\"\n\n    def __init__(self, model, key, sort, filter_fn, is_precomputed):\n        self.model = model\n        self.key = key\n        self.filter = filter_fn\n        self.timestamps = None  # column timestamps, for safe pruning\n        self.is_precomputed = is_precomputed\n        super(CachedQuery, self).__init__(sort)\n\n    def _make_item_tuple(self, item):\n        \"\"\"Return an item tuple from the result of a query.\n\n        The item tuple is used to sort the items in a query without having to\n        look them up.\n\n        \"\"\"\n        filtered_item = self.filter(item)\n        lst = [filtered_item._fullname]\n        for col in self.sort_cols:\n            # take the property of the original\n            attr = getattr(item, col)\n            # convert dates to epochs to take less space\n            if isinstance(attr, datetime.datetime):\n                attr = epoch_seconds(attr)\n            lst.append(attr)\n        return tuple(lst)\n\n    def _fetch(self):\n        self._fetch_multi([self])\n\n    @classmethod\n    def _fetch_multi(self, queries):\n        \"\"\"Fetch the unsorted query results for multiple queries at once.\n\n        In the case of precomputed queries, do an extra lookup first to\n        determine which row key to find the latest precomputed values for the\n        query in.\n\n        \"\"\"\n\n        by_model = collections.defaultdict(list)\n        for q in queries:\n            by_model[q.model].append(q)\n\n        cached_queries = {}\n        for model, queries in by_model.iteritems():\n            pure, need_mangling = [], []\n            for q in queries:\n                if not q.is_precomputed:\n                    pure.append(q.key)\n                else:\n                    need_mangling.append(q.key)\n\n            mangled = model.index_mangle_keys(need_mangling)\n            fetched = model.get(pure + mangled.keys())\n            for key, values in fetched.iteritems():\n                key = mangled.get(key, key)\n                cached_queries[key] = values\n\n        for q in queries:\n            cached_query = cached_queries.get(q.key)\n            if cached_query:\n                q.data, q.timestamps = cached_query\n\n    def _cols_from_things(self, things):\n        cols = {}\n        for thing in things:\n            t = self._make_item_tuple(thing)\n            cols[t[0]] = tuple(t[1:])\n        return cols\n\n    def _insert(self, mutator, things):\n        if not things:\n            return\n\n        cols = self._cols_from_things(things)\n        self.model.insert(mutator, self.key, cols)\n\n    def _replace(self, mutator, things, ttl):\n        cols = self._cols_from_things(things)\n        self.model.replace(mutator, self.key, cols, ttl)\n\n    def _delete(self, mutator, things):\n        if not things:\n            return\n\n        fullnames = [self.filter(x)._fullname for x in things]\n        self.model.remove(mutator, self.key, fullnames)\n\n    def _prune(self, mutator):\n        to_keep = [t[0] for t in self.data[:MAX_CACHED_ITEMS]]\n        to_prune = [t[0] for t in self.data[MAX_CACHED_ITEMS:]]\n\n        if to_prune:\n            oldest_keep = min(self.timestamps[_id] for _id in to_keep)\n            fast_prunable = [_id for _id in to_prune\n                if self.timestamps[_id] < oldest_keep]\n\n            num_to_prune = len(to_prune)\n            num_fast_prunable = len(fast_prunable)\n            num_unpruned_if_fast = num_to_prune - num_fast_prunable\n            if (num_fast_prunable > num_to_prune * 0.5 and\n                    num_unpruned_if_fast < MAX_CACHED_ITEMS * 0.5):\n                # do a fast prune if we can remove a good number of items but\n                # don't let the cached query grow too large\n                newest_prune = max(self.timestamps[_id] for _id in fast_prunable)\n                self.model.remove_older_than(mutator, self.key, newest_prune)\n                event_name = 'fast_pruned'\n                num_pruned = num_fast_prunable\n            else:\n                # if something has gone wrong with previous prunings, there may\n                # be a lot of items to prune.\n                #\n                # On each attempt we have PRUNE_CHANCE likelihood that we will\n                # get to prune. Assume that each prune attempt occurs as the\n                # result of adding one item to the `CachedQuery`. So, to prevent\n                # unbounded growth we need to remove on average at least one\n                # item per prune attempt.\n                # so:\n                # N_avg = 1 = PRUNE_CHANCE * PRUNE_SIZE\n                # PRUNE_SIZE = 1 / PRUNE_CHANCE\n                # We'll multiply this value by 1.5 to ensure that we return\n                # quickly to the maximum allowed size.\n                prune_size = int(1.5 * 1 / PRUNE_CHANCE)\n                to_prune = to_prune[-prune_size:]\n\n                self.model.remove_if_unchanged(mutator, self.key,\n                                               to_prune, self.timestamps)\n                event_name = 'pruned'\n                num_pruned = len(to_prune)\n\n            cf_name = self.model.__name__\n            query_name = self.key.split('.')[0]\n            counter_key = \"cache.%s.%s\" % (cf_name, query_name)\n            counter = g.stats.get_counter(counter_key)\n            if counter:\n                counter.increment(event_name, delta=num_pruned)\n\n    @classmethod\n    def _prune_multi(cls, queries):\n        cls._fetch_multi(queries)\n\n        with Mutator(CONNECTION_POOL) as m:\n            for q in queries:\n                q._sort_data()\n                q._prune(m)\n\n    def __hash__(self):\n        return hash(self.key)\n\n    def __eq__(self, other):\n        return self.key == other.key\n\n    def __ne__(self, other):\n        return not self.__eq__(other)\n\n    def __repr__(self):\n        return \"%s(%s, %r)\" % (self.__class__.__name__,\n                               self.model.__name__, self.key)\n\n\nclass MergedCachedQuery(_CachedQueryBase):\n    \"\"\"A cached query built by merging multiple sub-queries.\n\n    Merged queries can be read, but cannot be modified as it is not easy to\n    determine from a given item which sub-query should get modified.\n\n    \"\"\"\n\n    def __init__(self, queries):\n        self.queries = queries\n\n        if queries:\n            sort = queries[0].sort\n            assert all(sort == q.sort for q in queries)\n        else:\n            sort = []\n        super(MergedCachedQuery, self).__init__(sort)\n\n    def _fetch(self):\n        CachedQuery._fetch_multi(self.queries)\n        self.data = flatten([q.data for q in self.queries])\n\n\nclass CachedQueryMutator(object):\n    \"\"\"Utility to manipulate cached queries with batching.\n\n    This implements the context manager protocol so it can be used with the\n    with statement for clean batches.\n\n    \"\"\"\n\n    def __init__(self):\n        self.mutator = Mutator(CONNECTION_POOL)\n        self.to_prune = set()\n\n    def __enter__(self):\n        return self\n\n    def __exit__(self, type, value, traceback):\n        self.send()\n\n    def insert(self, query, things):\n        \"\"\"Insert items into the given cached query.\n\n        If the items are already in the query, they will have their sorts\n        updated.\n\n        This will sometimes trigger pruning with a configurable probability\n        (see g.querycache_prune_chance).\n\n        \"\"\"\n        if not things:\n            return\n\n        LOG.debug(\"Inserting %r into query %r\", things, query)\n\n        assert not query.is_precomputed\n        query._insert(self.mutator, things)\n\n        if (random.random() / len(things)) < PRUNE_CHANCE:\n            self.to_prune.add(query)\n\n    def replace(self, query, things, ttl=None):\n        \"\"\"Replace a precomputed query with a new set of things.\n\n        The query index will be updated. If a TTL is specified, it will be\n        applied to all columns generated by this action allowing old\n        precomputed queries to fall away after they're no longer useful.\n\n        \"\"\"\n        assert query.is_precomputed\n\n        if isinstance(ttl, datetime.timedelta):\n            ttl = ttl.total_seconds()\n\n        query._replace(self.mutator, things, ttl)\n\n    def delete(self, query, things):\n        \"\"\"Remove things from the query.\"\"\"\n        if not things:\n            return\n\n        LOG.debug(\"Deleting %r from query %r\", things, query)\n\n        query._delete(self.mutator, things)\n\n    def send(self):\n        \"\"\"Commit the mutations batched up so far and potentially do pruning.\n\n        This is automatically called by __exit__ when used as a context\n        manager.\n\n        \"\"\"\n        self.mutator.send()\n\n        if self.to_prune:\n            LOG.debug(\"Pruning queries %r\", self.to_prune)\n            CachedQuery._prune_multi(self.to_prune)\n\n\ndef filter_identity(x):\n    \"\"\"Return the same thing given.\n\n    Use this as the filter_fn of simple Thing-based cached queries so that\n    the enumerated things will be returned for rendering.\n\n    \"\"\"\n    return x\n\n\ndef filter_thing2(x):\n    \"\"\"Return the thing2 of a given relationship.\n\n    Use this as the filter_fn of a cached Relation query so that the related\n    things will be returned for rendering.\n\n    \"\"\"\n    return x._thing2\n\n\ndef filter_thing(x):\n    \"\"\"Return \"thing\" from a proxy object.\n\n    Use this as the filter_fn when some object that's not a Thing or Relation\n    is used as the basis of a cached query.\n\n    \"\"\"\n    return x.thing\n\n\ndef _is_query_precomputed(query):\n    \"\"\"Return if this query must be updated offline in a batch job.\n\n    Simple queries can be modified in place in the query cache, but ones\n    with more complicated eligibility criteria, such as a time limit (\"top\n    this month\") cannot be modified this way and must instead be\n    recalculated periodically.  Rather than replacing a single row\n    repeatedly, the precomputer stores in a new row every time it runs and\n    updates an index of the latest run.\n\n    \"\"\"\n\n    # visit all the nodes in the rule tree to see if there are time limitations\n    # if we find one, this query is one that must be precomputed\n    rules = list(query._rules)\n    while rules:\n        rule = rules.pop()\n\n        if isinstance(rule, BooleanOp):\n            rules.extend(rule.ops)\n            continue\n\n        if rule.lval.name == \"_date\":\n            return True\n    return False\n\n\nclass FakeQuery(object):\n    \"\"\"A somewhat query-like object for conveying sort information.\"\"\"\n\n    def __init__(self, sort, precomputed=False):\n        self._sort = sort\n        self.precomputed = precomputed\n\n\ndef cached_query(model, filter_fn=filter_identity):\n    \"\"\"Decorate a function describing a cached query.\n\n    The decorated function is expected to follow the naming convention common\n    in queries.py -- \"get_something\".  The cached query's key will be generated\n    from the combination of the function name and its arguments separated by\n    periods.\n\n    The decorated function should return a raw thingdb query object\n    representing the query that is being cached. If there is no valid\n    underlying query to build off of, a FakeQuery specifying the correct\n    sorting criteria for the enumerated objects can be returned.\n\n    \"\"\"\n    def cached_query_decorator(fn):\n        def cached_query_wrapper(*args):\n            # build the row key from the function name and arguments\n            assert fn.__name__.startswith(\"get_\")\n            row_key_components = [fn.__name__[len('get_'):]]\n\n            if len(args) > 0:\n                # we want to accept either a Thing or a thing's ID at this\n                # layer, but the query itself should always get just an ID\n                if isinstance(args[0], Thing):\n                    args = list(args)\n                    args[0] = args[0]._id\n\n                if isinstance(args[0], (int, long)):\n                    serialized = to36(args[0])\n                else:\n                    serialized = str(args[0])\n                row_key_components.append(serialized)\n\n            row_key_components.extend(str(x) for x in args[1:])\n            row_key = '.'.join(row_key_components)\n\n            query = fn(*args)\n\n            query_sort = query._sort\n            try:\n                is_precomputed = query.precomputed\n            except AttributeError:\n                is_precomputed = _is_query_precomputed(query)\n\n            return CachedQuery(model, row_key, query_sort, filter_fn,\n                               is_precomputed)\n        return cached_query_wrapper\n    return cached_query_decorator\n\n\ndef merged_cached_query(fn):\n    \"\"\"Decorate a function describing a cached query made up of others.\n\n    The decorated function should return a sequence of cached queries whose\n    results will be merged together into a final listing.\n\n    \"\"\"\n    def merge_wrapper(*args, **kwargs):\n        queries = fn(*args, **kwargs)\n        return MergedCachedQuery(queries)\n    return merge_wrapper\n\n\nclass _BaseQueryCache(object):\n    \"\"\"The model through which cached queries to interact with Cassandra.\n\n    Each cached query is stored as a distinct row in Cassandra.  The row key is\n    given by higher level code (see the cached_query decorator above).  Each\n    item in the materialized result of the query is stored as a separate\n    column.  Each column name is the fullname of the item, while each value is\n    the stuff CachedQuery needs to be able to sort the items (see\n    CachedQuery._make_item_tuple).\n\n    \"\"\"\n\n    __metaclass__ = tdb_cassandra.ThingMeta\n    _connection_pool = 'main'\n    _extra_schema_creation_args = dict(key_validation_class=ASCII_TYPE,\n                                       default_validation_class=UTF8_TYPE)\n    _compare_with = ASCII_TYPE\n    _use_db = False\n    _type_prefix = None\n    _cf_name = None\n\n    @classmethod\n    def get(cls, keys):\n        \"\"\"Retrieve the items in a set of cached queries.\n\n        For each cached query, this returns the thing tuples and the column\n        timestamps for them.  The latter is useful for conditional removal\n        during pruning.\n\n        \"\"\"\n        rows = cls._cf.multiget(keys, include_timestamp=True,\n                                column_count=tdb_cassandra.max_column_count)\n\n        res = {}\n        for row, columns in rows.iteritems():\n            data = []\n            timestamps = []\n\n            for (key, (value, timestamp)) in columns.iteritems():\n                value = json.loads(value)\n                data.append((key,) + tuple(value))\n                timestamps.append((key, timestamp))\n\n            res[row] = (data, dict(timestamps))\n\n        return res\n\n    @classmethod\n    def index_mangle_keys(cls, keys):\n        if not keys:\n            return {}\n\n        index_keys = [\"/\".join((key, \"index\")) for key in keys]\n        rows = cls._cf.multiget(index_keys,\n                                column_reversed=True,\n                                column_count=1)\n\n        res = {}\n        for key, columns in rows.iteritems():\n            root_key = key.rsplit(\"/\")[0]\n            index_component = columns.keys()[0]\n            mangled = \"/\".join((root_key, index_component))\n            res[mangled] = root_key\n        return res\n\n    @classmethod\n    @tdb_cassandra.will_write\n    def insert(cls, mutator, key, columns, ttl=None):\n        \"\"\"Insert things into the cached query.\n\n        This works as an upsert; if the thing already exists, it is updated. If\n        not, it is actually inserted.\n\n        \"\"\"\n        updates = dict((key, json.dumps(value))\n                       for key, value in columns.iteritems())\n        mutator.insert(cls._cf, key, updates, ttl=ttl)\n\n    @classmethod\n    @tdb_cassandra.will_write\n    def replace(cls, mutator, key, columns, ttl):\n        # XXX: this assumes that precomputed queries aren't updated at a\n        # frequency / simultaneously in a way that could collide.\n        job_key = datetime.datetime.now(g.tz).isoformat()\n        cls.insert(mutator, key + \"/\" + job_key, columns, ttl=ttl)\n        mutator.insert(cls._cf, key + \"/index\", {job_key: \"\"}, ttl=ttl)\n\n    @classmethod\n    @tdb_cassandra.will_write\n    def remove(cls, mutator, key, columns):\n        \"\"\"Unconditionally remove things from the cached query.\"\"\"\n        mutator.remove(cls._cf, key, columns=columns)\n\n    @classmethod\n    @tdb_cassandra.will_write\n    def remove_if_unchanged(cls, mutator, key, columns, timestamps):\n        \"\"\"Remove things from the cached query if unchanged.\n\n        If the things have been changed since the specified timestamps, they\n        will not be removed.  This is useful for avoiding race conditions while\n        pruning.\n\n        \"\"\"\n        for col in columns:\n            mutator.remove(cls._cf, key, columns=[col],\n                           timestamp=timestamps.get(col))\n\n    @classmethod\n    @tdb_cassandra.will_write\n    def remove_older_than(cls, mutator, key, removal_timestamp):\n        \"\"\"Remove things older than the specified timestamp.\n\n        Removing specific columns can cause tombstones to build up. When a row\n        has tons of tombstones fetching that row gets slow because Cassandra\n        must retrieve all the tombstones as well. Issuing a row remove with\n        the timestamp specified clears out all the columns modified before\n        that timestamp and somehow doesn't result in tombstones being left\n        behind. This behavior was verified via request tracing.\n\n        \"\"\"\n\n        mutator.remove(cls._cf, key, timestamp=removal_timestamp)\n\n\nclass UserQueryCache(_BaseQueryCache):\n    \"\"\"A query cache column family for user-keyed queries.\"\"\"\n    _use_db = True\n\n\nclass SubredditQueryCache(_BaseQueryCache):\n    \"\"\"A query cache column family for subreddit-keyed queries.\"\"\"\n    _use_db = True\n"
  },
  {
    "path": "r2/r2/models/recommend.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport pycassa\nimport time\n\nfrom collections import defaultdict\nfrom datetime import datetime, timedelta\nfrom itertools import chain\nfrom pylons import app_globals as g\n\nfrom r2.lib.db import tdb_cassandra\nfrom r2.lib.db.tdb_cassandra import max_column_count\nfrom r2.lib.utils import utils, tup\nfrom r2.models import Account, LabeledMulti, Subreddit\nfrom r2.lib.pages import ExploreItem\n\nVIEW = 'imp'\nCLICK = 'clk'\nDISMISS = 'dis'\nFEEDBACK_ACTIONS = [VIEW, CLICK, DISMISS]\n\n# how long to keep each type of feedback\nFEEDBACK_TTL = {VIEW: timedelta(hours=6).total_seconds(),  # link lifetime\n                CLICK: timedelta(minutes=30).total_seconds(),  # one session\n                DISMISS: timedelta(days=60).total_seconds()}  # two months\n\n\nclass AccountSRPrefs(object):\n    \"\"\"Class for managing user recommendation preferences.\n\n    Builds a user profile on-the-fly based on the user's subscriptions,\n    multireddits, and recent interactions with the recommender UI.\n\n    Likes are used to generate recommendations, dislikes to filter out\n    unwanted results, and recent views to make sure the same subreddits aren't\n    recommended too often.\n\n    \"\"\"\n\n    def __init__(self):\n        self.likes = set()\n        self.dislikes = set()\n        self.recent_views = set()\n\n    @classmethod\n    def for_user(cls, account):\n        \"\"\"Return a new AccountSRPrefs obj populated with user's data.\"\"\"\n        prefs = cls()\n        multis = LabeledMulti.by_owner(account)\n        multi_srs = set(chain.from_iterable(multi.srs for multi in multis))\n        feedback = AccountSRFeedback.for_user(account)\n        # subscriptions and srs in the user's multis become likes\n        subscriptions = Subreddit.user_subreddits(account, limit=None)\n        prefs.likes.update(utils.to36(sr_id) for sr_id in subscriptions)\n        prefs.likes.update(sr._id36 for sr in multi_srs)\n        # recent clicks on explore tab items are also treated as likes\n        prefs.likes.update(feedback[CLICK])\n        # dismissed recommendations become dislikes\n        prefs.dislikes.update(feedback[DISMISS])\n        # dislikes take precedence over likes\n        prefs.likes = prefs.likes.difference(prefs.dislikes)\n        # recently recommended items won't be shown again right away\n        prefs.recent_views.update(feedback[VIEW])\n        return prefs\n\n\nclass AccountSRFeedback(tdb_cassandra.DenormalizedRelation):\n    \"\"\"Column family for storing users' recommendation feedback.\"\"\"\n\n    _use_db = True\n    _views = []\n    _write_last_modified = False\n    _read_consistency_level = tdb_cassandra.CL.QUORUM\n    _write_consistency_level = tdb_cassandra.CL.QUORUM\n\n    @classmethod\n    def for_user(cls, account):\n        \"\"\"Return dict mapping each feedback type to a set of sr id36s.\"\"\"\n\n        feedback = defaultdict(set)\n        try:\n            row = AccountSRFeedback._cf.get(account._id36,\n                                            column_count=max_column_count)\n        except pycassa.NotFoundException:\n            return feedback\n        for colkey, colval in row.iteritems():\n            action, sr_id36 = colkey.split('.')\n            feedback[action].add(sr_id36)\n        return feedback\n\n    @classmethod\n    def record_feedback(cls, account, srs, action):\n        if action not in FEEDBACK_ACTIONS:\n            g.log.error('Unrecognized feedback: %s' % action)\n            return\n        srs = tup(srs)\n        # update user feedback record, setting appropriate ttls\n        fb_rowkey = account._id36\n        fb_colkeys = ['%s.%s' % (action, sr._id36) for sr in srs]\n        col_data = {col: '' for col in fb_colkeys}\n        ttl = FEEDBACK_TTL.get(action, 0)\n        if ttl > 0:\n            AccountSRFeedback._cf.insert(fb_rowkey, col_data, ttl=ttl)\n        else:\n            AccountSRFeedback._cf.insert(fb_rowkey, col_data)\n\n    @classmethod\n    def record_views(cls, account, srs):\n        cls.record_feedback(account, srs, VIEW)\n\n\nclass ExploreSettings(tdb_cassandra.Thing):\n    \"\"\"Column family for storing users' view prefs for the /explore page.\"\"\"\n    _use_db = True\n    _bool_props = ('personalized', 'discovery', 'rising', 'nsfw')\n\n    @classmethod\n    def for_user(cls, account):\n        \"\"\"Return user's prefs or default prefs if user has none.\"\"\"\n        try:\n            return cls._byID(account._id36)\n        except tdb_cassandra.NotFound:\n            return DefaultExploreSettings()\n\n    @classmethod\n    def record_settings(cls,\n                        user,\n                        personalized=False,\n                        discovery=False,\n                        rising=False,\n                        nsfw=False):\n        \"\"\"Update or create settings for user.\"\"\"\n        try:\n            settings = cls._byID(user._id36)\n        except tdb_cassandra.NotFound:\n            settings = ExploreSettings(\n                _id=user._id36,\n                personalized=personalized,\n                discovery=discovery,\n                rising=rising,\n                nsfw=nsfw,\n            )\n        else:\n            settings.personalized = personalized\n            settings.discovery = discovery\n            settings.rising = rising\n            settings.nsfw = nsfw\n        settings._commit()\n\n\nclass DefaultExploreSettings(object):\n    \"\"\"Default values to use when no settings have been saved for the user.\"\"\"\n    def __init__(self):\n        self.personalized = True\n        self.discovery = True\n        self.rising = True\n        self.nsfw = False\n"
  },
  {
    "path": "r2/r2/models/report.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom collections import Counter\n\nfrom r2.lib.db.thing import Relation, MultiRelation\nfrom r2.lib.utils import tup\nfrom r2.models import Link, Comment, Message, Subreddit, Account\n\nfrom pylons import tmpl_context as c\nfrom pylons import app_globals as g\n\n\n_LinkReport = Relation(Account, Link)\n_CommentReport = Relation(Account, Comment)\n_SubredditReport = Relation(Account, Subreddit)\n_MessageReport = Relation(Account, Message)\nREPORT_RELS = (_LinkReport, _CommentReport, _SubredditReport, _MessageReport)\n\nfor report_cls in REPORT_RELS:\n    report_cls._cache = g.thingcache\n\n_LinkReport._cache_prefix = classmethod(lambda cls: \"reportlink:\")\n_CommentReport._cache_prefix = classmethod(lambda cls: \"reportcomment:\")\n_SubredditReport._cache_prefix = classmethod(lambda cls: \"reportsr:\")\n_MessageReport._cache_prefix = classmethod(lambda cls: \"reportmessage:\")\n\n\nclass Report(MultiRelation('report', *REPORT_RELS)):\n    _field = 'reported'\n\n    @classmethod\n    def new(cls, user, thing, reason=None):\n        from r2.lib.db import queries\n\n        # check if this report exists already!\n        rel = cls.rel(user, thing)\n        q = rel._fast_query(user, thing, ['-1', '0', '1'])\n        q = [ report for (tupl, report) in q.iteritems() if report ]\n        if q:\n            # stop if we've seen this before, so that we never get the\n            # same report from the same user twice\n            oldreport = q[0]\n            g.log.debug(\"Ignoring duplicate report %s\" % oldreport)\n            return oldreport\n\n        kw = {}\n        if reason:\n            kw['reason'] = reason\n\n        r = Report(user, thing, '0', **kw)\n\n        # mark item as reported\n        try:\n            thing._incr(cls._field)\n        except (ValueError, TypeError):\n            g.log.error(\"%r has bad field %r = %r\" % (thing, cls._field,\n                         getattr(thing, cls._field, \"(nonexistent)\")))\n            raise\n\n        r._commit()\n\n        if hasattr(thing, 'author_id'):\n            author = Account._byID(thing.author_id, data=True)\n            author._incr('reported')\n\n        if not getattr(thing, \"ignore_reports\", False):\n            # update the reports queue if it exists\n            queries.new_report(thing, r)\n\n            # if the thing is already marked as spam, accept the report\n            if thing._spam:\n                cls.accept(thing)\n\n        return r\n\n    @classmethod\n    def for_thing(cls, thing):\n        rel = cls.rel(Account, thing.__class__)\n        rels = rel._query(rel.c._thing2_id == thing._id, data=True)\n\n        return list(rels)\n\n    @classmethod\n    def accept(cls, things, correct = True):\n        from r2.lib.db import queries\n\n        things = tup(things)\n\n        things_by_cls = {}\n        for thing in things:\n            things_by_cls.setdefault(thing.__class__, []).append(thing)\n\n        for thing_cls, cls_things in things_by_cls.iteritems():\n            to_clear = []\n            # look up all of the reports for each thing\n            rel_cls = cls.rel(Account, thing_cls)\n            thing_ids = [t._id for t in cls_things]\n            rels = rel_cls._query(rel_cls.c._thing2_id == thing_ids)\n            for r in rels:\n                if r._name == '0':\n                    r._name = '1' if correct else '-1'\n                    r._commit()\n\n            for thing in cls_things:\n                if thing.reported > 0:\n                    thing.reported = 0\n                    thing._commit()\n                    to_clear.append(thing)\n\n            queries.clear_reports(to_clear, rels)\n\n    @classmethod\n    def get_reports(cls, wrapped, max_user_reasons=20):\n        \"\"\"Get two lists of mod and user reports on the item.\"\"\"\n        if (wrapped.reported > 0 and\n                (wrapped.can_ban or\n                 getattr(wrapped, \"promoted\", None) and c.user_is_sponsor)):\n            from r2.models import SRMember\n\n            reports = cls.for_thing(wrapped.lookups[0])\n\n            query = SRMember._query(SRMember.c._thing1_id == wrapped.sr_id,\n                                    SRMember.c._name == \"moderator\")\n            mod_dates = {rel._thing2_id: rel._date for rel in query}\n\n            if g.automoderator_account:\n                automoderator = Account._by_name(g.automoderator_account)\n            else:\n                automoderator = None\n\n            mod_reports = []\n            user_reports = []\n\n            for report in reports:\n                # always include AutoModerator reports\n                if automoderator and report._thing1_id == automoderator._id:\n                    mod_reports.append(report)\n                # include in mod reports if made after the user became a mod\n                elif (report._thing1_id in mod_dates and\n                        report._date >= mod_dates[report._thing1_id]):\n                    mod_reports.append(report)\n                else:\n                    user_reports.append(report)\n\n            # mod reports return as tuples with (reason, name)\n            mods = Account._byID([report._thing1_id\n                                  for report in mod_reports],\n                                 data=True, return_dict=True)\n            mod_reports = [(getattr(report, \"reason\", None),\n                            mods[report._thing1_id].name)\n                            for report in mod_reports]\n\n            # user reports return as tuples with (reason, count)\n            user_reports = Counter([getattr(report, \"reason\", None)\n                                    for report in user_reports])\n            user_reports = user_reports.most_common(max_user_reasons)\n\n            return mod_reports, user_reports\n        else:\n            return [], []\n\n    @classmethod\n    def get_reasons(cls, wrapped):\n        \"\"\"Transition method in case API clients were already using this.\"\"\"\n        if wrapped.can_ban and wrapped.reported > 0:\n            return [(\"This attribute is deprecated. Please use mod_reports \"\n                     \"and user_reports instead.\")]\n        else:\n            return []\n"
  },
  {
    "path": "r2/r2/models/rules.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport json\nimport pytz\nimport time\nfrom datetime import datetime\nfrom pycassa.system_manager import UTF8_TYPE\nfrom pylons.i18n import _\n\nfrom r2.lib.db import tdb_cassandra\n\nOLD_SITEWIDE_RULES = [\n    _(\"spam\"),\n    _(\"vote manipulation\"),\n    _(\"personal information\"),\n    _(\"sexualizing minors\"),\n    _(\"breaking reddit\"),\n]\n\nSITEWIDE_RULES = [\n    _(\"Spam\"),\n    _(\"Personal and confidential information\"),\n    _(\"Threatening, harassing, or inciting violence\"),\n]\nMAX_RULES_PER_SUBREDDIT = 10\n\n\nclass SubredditRules(tdb_cassandra.View):\n    _use_db = True\n    _extra_schema_creation_args = {\n        \"key_validation_class\": UTF8_TYPE,\n        \"column_name_class\": UTF8_TYPE,\n        \"default_validation_class\": UTF8_TYPE,\n    }\n    _compare_with = UTF8_TYPE\n    _read_consistency_level = tdb_cassandra.CL.ONE\n    _write_consistency_level = tdb_cassandra.CL.ONE\n    _connection_pool = \"main\"\n\n    @classmethod\n    def get_rule_blob(self, short_name, description, priority, kind,\n            created_utc=None):\n        if not created_utc:\n            created_utc = time.mktime(datetime.now(pytz.UTC).timetuple())\n\n        rule_params = {\n            \"description\": description,\n            \"priority\": priority,\n            \"created_utc\": created_utc,\n        }\n        if kind and kind != 'all':\n            rule_params[\"kind\"] = kind\n\n        jsonpacked = json.dumps(rule_params)\n        blob = {short_name: jsonpacked}\n        return blob\n\n    @classmethod\n    def create(self, subreddit, short_name, description, kind=None,\n            created_utc=None):\n        \"\"\"Create a rule and append to the end of the priority list.\"\"\"\n        try:\n            priority = len(list(self._cf.get(subreddit._id36)))\n        except tdb_cassandra.NotFoundException:\n            priority = 0\n\n        if priority >= MAX_RULES_PER_SUBREDDIT:\n            return\n\n        blob = self.get_rule_blob(short_name, description, priority,\n            kind, created_utc)\n        self._set_values(subreddit._id36, blob)\n\n    @classmethod\n    def remove_rule(self, subreddit, short_name):\n        \"\"\"Remove a rule and update priorities of remaining rules.\"\"\"\n        self._remove(subreddit._id36, [short_name])\n\n        rules = self.get_rules(subreddit)\n        blobs = {}\n        for index, rule in enumerate(rules):\n            if rule[\"priority\"] != index:\n                blobs.update(self.get_rule_blob(\n                    short_name=rule[\"short_name\"],\n                    description=rule[\"description\"],\n                    priority=index,\n                    kind=rule.get(\"kind\"),\n                    created_utc=rule[\"created_utc\"],\n                ))\n        self._set_values(subreddit._id36, blobs)\n\n    @classmethod\n    def update(self, subreddit, old_short_name, short_name, description,\n            kind=None):\n        \"\"\"Update the short_name or description of a rule.\"\"\"\n        rules = self._cf.get(subreddit._id36)\n        if old_short_name != short_name:\n            old_rule = rules.get(old_short_name, None)\n            self._remove(subreddit._id36, [old_short_name])\n        else:\n            old_rule = rules.get(short_name, None)\n        if not old_rule:\n            return False\n\n        old_rule = json.loads(old_rule)\n        if not old_rule.get(\"created_utc\"):\n                old_rule[\"created_utc\"] = time.mktime(\n                    datetime.strptime(\n                        old_rule.pop(\"when\")[:-6], \"%Y-%m-%d %H:%M:%S.%f\"\n                    ).timetuple())\n\n        blob = self.get_rule_blob(\n            short_name=short_name,\n            description=description,\n            priority=old_rule[\"priority\"],\n            kind=kind,\n            created_utc=old_rule[\"created_utc\"],\n        )\n        self._set_values(subreddit._id36, blob)\n\n    @classmethod\n    def reorder(self, subreddit, short_name, priority):\n        \"\"\"Update the priority spot of a rule\n\n        Move an existing rule to the desired spot in the rules\n        list and then update the priority of the rules.\n        \"\"\"\n        rule_to_reorder = self.get_rule(subreddit, short_name)\n        if not rule_to_reorder:\n            return False\n\n        self._remove(subreddit._id36, [short_name])\n        rules = self.get_rules(subreddit)\n\n        priority = min(priority, len(rules))\n        current_priority_index = 0\n        blobs = {}\n        blobs.update(self.get_rule_blob(\n                short_name=rule_to_reorder[\"short_name\"],\n                description=rule_to_reorder[\"description\"],\n                priority=priority,\n                kind=rule_to_reorder.get(\"kind\"),\n                created_utc=rule_to_reorder[\"created_utc\"],\n        ))\n\n        for rule in rules:\n            # Placeholder for rule_to_reorder's new priority\n            if priority == current_priority_index:\n                current_priority_index += 1\n\n            if rule[\"priority\"] != current_priority_index:\n                blobs.update(self.get_rule_blob(\n                    short_name=rule[\"short_name\"],\n                    description=rule[\"description\"],\n                    priority=current_priority_index,\n                    kind=rule.get(\"kind\"),\n                    created_utc=rule[\"created_utc\"],\n                ))\n            current_priority_index += 1\n        self._set_values(subreddit._id36, blobs)\n\n    @classmethod\n    def get_rule(self, subreddit, short_name):\n        \"\"\"Return rule associated with short_name or None.\"\"\"\n        try:\n            rules = self._cf.get(subreddit._id36)\n        except tdb_cassandra.NotFoundException:\n            return None\n        rule = rules.get(short_name, None)\n        if not rule:\n            return None\n        rule = json.loads(rule)\n        rule[\"short_name\"] = short_name\n        return rule\n\n    @classmethod\n    def get_rules(self, subreddit, kind=None):\n        \"\"\"Return list of rules sorted by priority.\n\n        If kind is empty, then all the rules apply.\n        \"\"\"\n        try:\n            query = self._cf.get(subreddit._id36)\n        except tdb_cassandra.NotFoundException:\n            return []\n\n        result = []\n        for uuid, json_blob in query.iteritems():\n            payload = json.loads(json_blob)\n            if not payload.get(\"created_utc\"):\n                payload[\"created_utc\"] = time.mktime(\n                    datetime.strptime(\n                        payload.pop(\"when\")[:-6], \"%Y-%m-%d %H:%M:%S.%f\"\n                    ).timetuple())\n            payload[\"short_name\"] = uuid\n\n            if not kind:\n                result.append(payload)\n            elif kind in payload.get(\"kind\", kind):\n                result.append(payload)\n\n        return sorted(result, key=lambda t: t[\"priority\"])\n"
  },
  {
    "path": "r2/r2/models/subreddit.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom __future__ import with_statement\n\nimport base64\nimport collections\nimport datetime\nimport itertools\nimport json\nimport re\nimport struct\n\nfrom pycassa import types\nfrom pycassa.util import convert_uuid_to_time\nfrom pycassa.system_manager import ASCII_TYPE, DATE_TYPE, FLOAT_TYPE, UTF8_TYPE\nfrom pylons import request\nfrom pylons import tmpl_context as c\nfrom pylons import app_globals as g\nfrom pylons.i18n import _, N_\nfrom thrift.protocol.TProtocol import TProtocolException\nfrom thrift.Thrift import TApplicationException\nfrom thrift.transport.TTransport import TTransportException\n\nfrom r2.config import feature\nfrom r2.lib.db.thing import Thing, Relation, NotFound\nfrom account import (\n    Account,\n    FakeAccount,\n    QuarantinedSubredditOptInsByAccount,\n)\nfrom printable import Printable\nfrom r2.lib.db.userrel import UserRel, MigratingUserRel\nfrom r2.lib.db.operators import lower, or_, and_, not_, desc\nfrom r2.lib.errors import RedditError\nfrom r2.lib.geoip import get_request_location\nfrom r2.lib.memoize import memoize\nfrom r2.lib.permissions import ModeratorPermissionSet\nfrom r2.lib.utils import (\n    UrlParser,\n    in_chunks,\n    summarize_markdown,\n    timeago,\n    to36,\n    tup,\n    unicode_title_to_ascii,\n)\nfrom r2.lib.cache import MemcachedError\nfrom r2.lib.sgm import sgm\nfrom r2.lib.strings import strings, Score\nfrom r2.lib.filters import _force_unicode\nfrom r2.lib.db import tdb_cassandra\nfrom r2.lib.db.tdb_sql import CreationError\nfrom r2.models.wiki import WikiPage, ImagesByWikiPage\nfrom r2.models.trylater import TryLater, TryLaterBySubject\nfrom r2.lib.merge import ConflictException\nfrom r2.lib.cache import CL_ONE\nfrom r2.lib import hooks\nfrom r2.models.query_cache import MergedCachedQuery\nfrom r2.models.rules import SubredditRules\nimport pycassa\n\nfrom r2.models.keyvalue import NamedGlobals\nfrom r2.models.wiki import WikiPage\nimport os.path\nimport random\n\ntrylater_hooks = hooks.HookRegistrar()\n\n\ndef get_links_sr_ids(sr_ids, sort, time):\n    from r2.lib.db import queries\n\n    if not sr_ids:\n        return []\n\n    results = [queries._get_links(sr_id, sort, time) for sr_id in sr_ids]\n    return queries.merge_results(*results)\n\n\ndef get_user_location():\n    \"\"\"Determine country of origin for the current user\n\n    This is provided via a call to geoip.get_request_location unless the\n    user has opted into the global default location.\n    \"\"\"\n    # The default location is just the unset one\n    if c.user and c.user.pref_use_global_defaults:\n        return \"\"\n\n    # this call has the side effect of memoizing on c.location\n    return get_request_location(request, c)\n\n\nsubreddit_rx = re.compile(r\"\\A[A-Za-z0-9][A-Za-z0-9_]{2,20}\\Z\")\nlanguage_subreddit_rx = re.compile(r\"\\A[a-z]{2}\\Z\")\ntime_subreddit_rx = re.compile(r\"\\At:[A-Za-z0-9][A-Za-z0-9_]{2,22}\\Z\")\n\n\nclass BaseSite(object):\n    _defaults = dict(\n        static_path=g.static_path,\n        header=None,\n        header_title='',\n        login_required=False,\n        sticky_fullnames=None,\n    )\n\n    def __getattr__(self, name):\n        if name in self._defaults:\n            return self._defaults[name]\n        raise AttributeError\n\n    @property\n    def path(self):\n        return \"/r/%s/\" % self.name\n\n    @property\n    def user_path(self):\n        return self.path\n\n    @property\n    def analytics_name(self):\n        return self.name\n\n    @property\n    def allows_referrers(self):\n        return True\n\n    def is_moderator_with_perms(self, user, *perms):\n        rel = self.is_moderator(user)\n        if rel:\n            return all(rel.has_permission(perm) for perm in perms)\n\n    def is_limited_moderator(self, user):\n        rel = self.is_moderator(user)\n        return bool(rel and not rel.is_superuser())\n\n    def is_unlimited_moderator(self, user):\n        rel = self.is_moderator(user)\n        return bool(rel and rel.is_superuser())\n\n    def get_links(self, sort, time):\n        from r2.lib.db import queries\n        return queries.get_links(self, sort, time)\n\n    def get_spam(self, include_links=True, include_comments=True):\n        from r2.lib.db import queries\n        return queries.get_spam(self, user=c.user, include_links=include_links,\n                                include_comments=include_comments)\n\n    def get_reported(self, include_links=True, include_comments=True):\n        from r2.lib.db import queries\n        return queries.get_reported(self, user=c.user,\n                                    include_links=include_links,\n                                    include_comments=include_comments)\n\n    def get_modqueue(self, include_links=True, include_comments=True):\n        from r2.lib.db import queries\n        return queries.get_modqueue(self, user=c.user,\n                                    include_links=include_links,\n                                    include_comments=include_comments)\n\n    def get_unmoderated(self):\n        from r2.lib.db import queries\n        return queries.get_unmoderated(self, user=c.user)\n\n    def get_edited(self, include_links=True, include_comments=True):\n        from r2.lib.db import queries\n        return queries.get_edited(self, user=c.user,\n                                  include_links=include_links,\n                                  include_comments=include_comments)\n\n    def get_all_comments(self):\n        from r2.lib.db import queries\n        return queries.get_sr_comments(self)\n\n    def get_gilded(self):\n        from r2.lib.db import queries\n        return queries.get_gilded(self._id)\n\n    @classmethod\n    def get_modactions(cls, srs, mod=None, action=None):\n        # Get a query that will yield ModAction objects with mod and action\n        from r2.models import ModAction\n        return ModAction.get_actions(srs, mod=mod, action=action)\n\n    def get_live_promos(self):\n        raise NotImplementedError\n\n\nclass SubredditExists(Exception): pass\n\n\nclass Subreddit(Thing, Printable, BaseSite):\n    _cache = g.thingcache\n\n    # Note: As of 2010/03/18, nothing actually overrides the static_path\n    # attribute, even on a cname. So c.site.static_path should always be\n    # the same as g.static_path.\n    _defaults = dict(BaseSite._defaults,\n        stylesheet_url=\"\",\n        stylesheet_url_http=\"\",\n        stylesheet_url_https=\"\",\n        header_size=None,\n        allow_top=False, # overridden in \"_new\"\n        reported=0,\n        valid_votes=0,\n        show_media=False,\n        show_media_preview=True,\n        domain=None,\n        suggested_comment_sort=None,\n        wikimode=\"disabled\",\n        wiki_edit_karma=100,\n        wiki_edit_age=0,\n        over_18=False,\n        exclude_banned_modqueue=False,\n        mod_actions=0,\n        # do we allow self-posts, links only, or any?\n        link_type='any', # one of ('link', 'self', 'any')\n        sticky_fullnames=None,\n        submit_link_label='',\n        submit_text_label='',\n        comment_score_hide_mins=0,\n        flair_enabled=True,\n        flair_position='right', # one of ('left', 'right')\n        link_flair_position='', # one of ('', 'left', 'right')\n        flair_self_assign_enabled=False,\n        link_flair_self_assign_enabled=False,\n        use_quotas=True,\n        description=\"\",\n        public_description=\"\",\n        submit_text=\"\",\n        public_traffic=False,\n        spam_links='high',\n        spam_selfposts='high',\n        spam_comments='low',\n        archive_age=g.ARCHIVE_AGE,\n        gilding_server_seconds=0,\n        contest_mode_upvotes_only=False,\n        collapse_deleted_comments=False,\n        icon_img='',\n        icon_size=None,\n        banner_img='',\n        banner_size=None,\n        key_color='',\n        hide_ads=False,\n        ban_count=0,\n        quarantine=False,\n    )\n\n    # special attributes that shouldn't set Thing data attributes because they\n    # have special setters that set other data attributes\n    _derived_attrs = (\n        'related_subreddits',\n    )\n\n    _essentials = ('type', 'name', 'lang')\n    _data_int_props = Thing._data_int_props + ('mod_actions', 'reported',\n                                               'wiki_edit_karma', 'wiki_edit_age',\n                                               'gilding_server_seconds',\n                                               'ban_count')\n\n    sr_limit = 50\n    gold_limit = 100\n    DEFAULT_LIMIT = object()\n\n    ICON_EXACT_SIZE = (256, 256)\n    BANNER_MIN_SIZE = (640, 192)\n    BANNER_MAX_SIZE = (1280, 384)\n    BANNER_ASPECT_RATIO = 10.0 / 3\n\n    valid_types = {\n        'archived',\n        'employees_only',\n        'gold_only',\n        'gold_restricted',\n        'private',\n        'public',\n        'restricted',\n    }\n\n    # this holds the subreddit types where content is not accessible\n    # unless you are a contributor or mod\n    private_types = {\n        'employees_only',\n        'gold_only',\n        'private',\n    }\n\n    KEY_COLORS = collections.OrderedDict([\n        ('#ea0027', N_('red')),\n        ('#ff4500', N_('orangered')),\n        ('#ff8717', N_('orange')),\n        ('#ffb000', N_('mango')),\n        ('#94e044', N_('lime')),\n        ('#46d160', N_('green')),\n        ('#0dd3bb', N_('mint')),\n        ('#25b79f', N_('teal')),\n        ('#24a0ed', N_('blue')),\n        ('#0079d3', N_('alien blue')),\n        ('#ff66ac', N_('pink')),\n        ('#7e53c1', N_('purple')),\n        ('#ddbd37', N_('gold')),\n        ('#a06a42', N_('brown')),\n        ('#efefed', N_('pale grey')),\n        ('#a5a4a4', N_('grey')),\n        ('#545452', N_('dark grey')),\n        ('#222222', N_('semi black')),\n    ])\n    ACCENT_COLORS = (\n        '#f44336', # red\n        '#9c27b0', # purple\n        '#3f51b5', # indigo\n        '#03a9f4', # light blue\n        '#009688', # teal\n        '#8bc34a', # light green\n        '#ffeb3b', # yellow\n        '#ff9800', # orange\n        '#795548', # brown\n        '#607d8b', # blue grey\n        '#e91e63', # pink\n        '#673ab7', # deep purple\n        '#2196f3', # blue\n        '#00bcd4', # cyan\n        '#4caf50', # green\n        '#cddc39', # lime\n        '#ffc107', # amber\n        '#ff5722', # deep orange\n        '#9e9e9e', # grey\n    )\n\n    MAX_STICKIES = 2\n\n    @classmethod\n    def _cache_prefix(cls):\n        return \"sr:\"\n\n    def __setattr__(self, attr, val, make_dirty=True):\n        if attr in self._derived_attrs:\n            object.__setattr__(self, attr, val)\n        else:\n            Thing.__setattr__(self, attr, val, make_dirty=make_dirty)\n\n    # note: for purposely unrenderable reddits (like promos) set author_id = -1\n    @classmethod\n    def _new(cls, name, title, author_id, ip, lang = g.lang, type = 'public',\n             over_18 = False, **kw):\n        if not cls.is_valid_name(name):\n            raise ValueError(\"bad subreddit name\")\n        with g.make_lock(\"create_sr\", 'create_sr_' + name.lower()):\n            try:\n                sr = Subreddit._by_name(name)\n                raise SubredditExists\n            except NotFound:\n                if \"allow_top\" not in kw:\n                    kw['allow_top'] = True\n                sr = Subreddit(name = name,\n                               title = title,\n                               lang = lang,\n                               type = type,\n                               over_18 = over_18,\n                               author_id = author_id,\n                               ip = ip,\n                               **kw)\n                sr._commit()\n\n                #clear cache\n                Subreddit._by_name(name, _update = True)\n                return sr\n\n    @classmethod\n    def is_valid_name(cls, name, allow_language_srs=False, allow_time_srs=False,\n                      allow_reddit_dot_com=False):\n        if not name:\n            return False\n\n        if allow_reddit_dot_com and name.lower() == \"reddit.com\":\n            return True\n\n        valid = bool(subreddit_rx.match(name))\n\n        if not valid and allow_language_srs:\n            valid = bool(language_subreddit_rx.match(name))\n\n        if not valid and allow_time_srs:\n            valid = bool(time_subreddit_rx.match(name))\n\n        return valid\n\n    _specials = {}\n\n    SRNAME_NOTFOUND = \"n\"\n    SRNAME_TTL = int(datetime.timedelta(hours=12).total_seconds())\n\n    @classmethod\n    def _by_name(cls, names, stale=False, _update = False):\n        '''\n        Usages:\n        1. Subreddit._by_name('funny') # single sr name\n        Searches for a single subreddit. Returns a single Subreddit object or\n        raises NotFound if the subreddit doesn't exist.\n        2. Subreddit._by_name(['aww','iama']) # list of sr names\n        Searches for a list of subreddits. Returns a dict mapping srnames to\n        Subreddit objects. Items that were not found are ommitted from the dict.\n        If no items are found, an empty dict is returned.\n        '''\n        names, single = tup(names, True)\n\n        to_fetch = {}\n        ret = {}\n\n        for name in names:\n            try:\n                ascii_only = str(name.decode(\"ascii\", errors=\"ignore\"))\n            except UnicodeEncodeError:\n                continue\n\n            lname = ascii_only.lower()\n\n            if lname in cls._specials:\n                ret[name] = cls._specials[lname]\n            else:\n                valid_name = cls.is_valid_name(lname, allow_language_srs=True,\n                                               allow_time_srs=True,\n                                               allow_reddit_dot_com=True)\n                if valid_name:\n                    to_fetch[lname] = name\n                else:\n                    g.log.debug(\"Subreddit._by_name() ignoring invalid srname: %s\", lname)\n\n        if to_fetch:\n            if not _update:\n                srids_by_name = g.gencache.get_multi(\n                    to_fetch.keys(), prefix='srid:', stale=True)\n            else:\n                srids_by_name = {}\n\n            missing_srnames = set(to_fetch.keys()) - set(srids_by_name.keys())\n            if missing_srnames:\n                for srnames in in_chunks(missing_srnames, size=10):\n                    q = cls._query(\n                        lower(cls.c.name) == srnames,\n                        cls.c._spam == (True, False),\n                        # subreddits can't actually be deleted, but the combo\n                        # of allowing for deletion and turning on optimize_rules\n                        # gets rid of an unnecessary join on the thing table\n                        cls.c._deleted == (True, False),\n                        limit=len(srnames),\n                        optimize_rules=True,\n                        data=True,\n                    )\n                    with g.stats.get_timer('subreddit_by_name'):\n                        fetched = {sr.name.lower(): sr._id for sr in q}\n                    srids_by_name.update(fetched)\n\n                    still_missing = set(srnames) - set(fetched)\n                    fetched.update((name, cls.SRNAME_NOTFOUND) for name in still_missing)\n                    try:\n                        g.gencache.set_multi(\n                            keys=fetched,\n                            prefix='srid:',\n                            time=cls.SRNAME_TTL,\n                        )\n                    except MemcachedError:\n                        pass\n\n            srs = {}\n            srids = [v for v in srids_by_name.itervalues() if v != cls.SRNAME_NOTFOUND]\n            if srids:\n                srs = cls._byID(srids, data=True, return_dict=False, stale=stale)\n\n            for sr in srs:\n                ret[to_fetch[sr.name.lower()]] = sr\n\n        if ret and single:\n            return ret.values()[0]\n        elif not ret and single:\n            raise NotFound, 'Subreddit %s' % name\n        else:\n            return ret\n\n    @classmethod\n    @memoize('subreddit._by_domain')\n    def _by_domain_cache(cls, name):\n        q = cls._query(cls.c.domain == name,\n                       limit = 1)\n        l = list(q)\n        if l:\n            return l[0]._id\n\n    @classmethod\n    def _by_domain(cls, domain, _update = False):\n        sr_id = cls._by_domain_cache(_force_unicode(domain).lower(),\n                                     _update = _update)\n        if sr_id:\n            return cls._byID(sr_id, True)\n        else:\n            return None\n\n    @property\n    def allowed_types(self):\n        if self.link_type == \"any\":\n            return set((\"link\", \"self\"))\n        return set((self.link_type,))\n\n    @property\n    def allows_referrers(self):\n        return self.type in {'public', 'restricted',\n                             'gold_restricted', 'archived'}\n\n    @property\n    def author_slow(self):\n        if self.author_id:\n            return Account._byID(self.author_id, data=True)\n        else:\n            return None\n\n    def add_moderator(self, user, **kwargs):\n        if not user.modmsgtime:\n            user.modmsgtime = False\n            user._commit()\n\n        hook = hooks.get_hook(\"subreddit.add_moderator\")\n        hook.call(subreddit=self, user=user)\n\n        return super(Subreddit, self).add_moderator(user, **kwargs)\n\n    def remove_moderator(self, user, **kwargs):\n        hook = hooks.get_hook(\"subreddit.remove_moderator\")\n        hook.call(subreddit=self, user=user)\n\n        ret = super(Subreddit, self).remove_moderator(user, **kwargs)\n\n        is_mod_somewhere = bool(Subreddit.reverse_moderator_ids(user))\n        if not is_mod_somewhere:\n            user.modmsgtime = None\n            user._commit()\n\n        return ret\n\n    @property\n    def moderators(self):\n        return self.moderator_ids()\n\n    def moderators_with_perms(self):\n        return collections.OrderedDict(\n            (r._thing2_id, r.get_permissions())\n            for r in self.each_moderator())\n\n    def moderator_invites_with_perms(self):\n        return collections.OrderedDict(\n            (r._thing2_id, r.get_permissions())\n            for r in self.each_moderator_invite())\n\n    def fetch_stylesheet_source(self):\n        try:\n            return WikiPage.get(self, 'config/stylesheet')._get('content','')\n        except tdb_cassandra.NotFound:\n            return \"\"\n\n    @property\n    def prev_stylesheet(self):\n        try:\n            return WikiPage.get(self, 'config/stylesheet')._get('revision','')\n        except tdb_cassandra.NotFound:\n            return ''\n\n    @property\n    def wikibanned(self):\n        return self.wikibanned_ids()\n\n    @property\n    def wikicontributor(self):\n        return self.wikicontributor_ids()\n\n    @property\n    def _should_wiki(self):\n        return True\n\n    @property\n    def subscribers(self):\n        return self.subscriber_ids()\n\n    @property\n    def wiki_use_subreddit_karma(self):\n        return True\n\n    @property\n    def hide_subscribers(self):\n        return self.name.lower() in g.hide_subscribers_srs\n\n    @property\n    def hide_contributors(self):\n        return self.type in {'employees_only', 'gold_only'}\n\n    @property\n    def hide_num_users_info(self):\n        return self.quarantine\n\n    @property\n    def _related_multipath(self):\n        return '/r/%s/m/related' % self.name.lower()\n\n    @property\n    def related_subreddits(self):\n        try:\n            multi = LabeledMulti._byID(self._related_multipath)\n        except tdb_cassandra.NotFound:\n            multi = None\n        return  [sr.name for sr in multi.srs] if multi else []\n\n    @property\n    def allow_ads(self):\n        return not (self.hide_ads or self.quarantine)\n\n    @property\n    def discoverable(self):\n        return self.allow_top and not self.quarantine\n\n    @property\n    def community_rules(self):\n        return SubredditRules.get_rules(self)\n\n    @related_subreddits.setter\n    def related_subreddits(self, related_subreddits):\n        try:\n            multi = LabeledMulti._byID(self._related_multipath)\n        except tdb_cassandra.NotFound:\n            if not related_subreddits:\n                return\n            multi = LabeledMulti.create(self._related_multipath, self)\n\n        if related_subreddits:\n            srs = Subreddit._by_name(related_subreddits)\n            try:\n                sr_props = {srs[sr_name]: {} for sr_name in related_subreddits}\n            except KeyError as e:\n                raise NotFound, 'Subreddit %s' % e.args[0]\n\n            multi.clear_srs()\n            multi.add_srs(sr_props)\n            multi._commit()\n        else:\n            multi.delete()\n\n    activity_contexts = (\n        \"logged_in\",\n    )\n    SubredditActivity = collections.namedtuple(\n        \"SubredditActivity\", activity_contexts)\n\n    def record_visitor_activity(self, context, visitor_id):\n        \"\"\"Record a visit to this subreddit in the activity service.\n\n        This is used to show \"here now\" numbers. Multiple contexts allow us\n        to bucket different kinds of visitors (logged-in vs. logged-out etc.)\n\n        :param str context: The category of visitor. Must be one of\n            Subreddit.activity_contexts.\n        :param str visitor_id: A unique identifier for this visitor within the\n            given context.\n\n        \"\"\"\n        assert context in self.activity_contexts\n\n        # we don't actually support other contexts yet\n        assert self.activity_contexts == (\"logged_in\",)\n\n        if not c.activity_service:\n            return\n\n        try:\n            c.activity_service.record_activity(self._fullname, visitor_id)\n        except (TApplicationException, TProtocolException, TTransportException):\n            pass\n\n    def count_activity(self):\n        \"\"\"Count activity in this subreddit in all known contexts.\n\n        :returns: a named tuple of activity information for each context.\n\n        \"\"\"\n        # we don't actually support other contexts yet\n        assert self.activity_contexts == (\"logged_in\",)\n\n        if not c.activity_service:\n            return None\n\n        try:\n            # TODO: support batch lookup of multiple contexts (requires changes\n            # to activity service)\n            with c.activity_service.retrying(attempts=4, budget=0.1) as svc:\n                activity = svc.count_activity(self._fullname)\n            return self.SubredditActivity(activity)\n        except (TApplicationException, TProtocolException, TTransportException):\n            return None\n\n    def spammy(self):\n        return self._spam\n\n    def is_contributor(self, user):\n        if self.type == 'employees_only':\n            return user.employee\n        else:\n            return super(Subreddit, self).is_contributor(user)\n\n    def can_comment(self, user):\n        if c.user_is_admin:\n            return True\n\n        override = hooks.get_hook(\"subreddit.can_comment\").call_until_return(\n                                                            sr=self, user=user)\n\n        if override is not None:\n            return override\n        elif self.is_banned(user):\n            return False\n        elif self.type == 'gold_restricted' and user.gold:\n            return True\n        elif self.type in ('public','restricted'):\n            return True\n        elif self.is_moderator(user) or self.is_contributor(user):\n            #private requires contributorship\n            return True\n        elif self.type == 'gold_only':\n            return user.gold or user.gold_charter\n        else:\n            return False\n\n    def wiki_can_submit(self, user):\n        return self.can_submit(user)\n\n    def can_submit(self, user, promotion=False):\n        if c.user_is_admin:\n            return True\n        elif self.is_banned(user) and not promotion:\n            return False\n        elif self.spammy():\n            return False\n        elif self.type == 'public':\n            return True\n        elif self.is_moderator(user) or self.is_contributor(user):\n            #restricted/private require contributorship\n            return True\n        elif self.type == 'gold_only':\n            return user.gold or user.gold_charter\n        elif self.type == 'gold_restricted' and user.gold:\n            return True\n        elif self.type == 'restricted' and promotion:\n            return True\n        else:\n            return False\n\n    def can_submit_link(self, user):\n        if c.user_is_admin or self.is_moderator_with_perms(user, \"posts\"):\n            return True\n        return \"link\" in self.allowed_types\n\n    def can_submit_text(self, user):\n        if c.user_is_admin or self.is_moderator_with_perms(user, \"posts\"):\n            return True\n        return \"self\" in self.allowed_types\n\n    def can_ban(self, user):\n        return (user\n                and (c.user_is_admin\n                     or self.is_moderator_with_perms(user, 'posts')))\n\n    def can_mute(self, muter, user):\n        return (user.is_mutable(self) and\n            (c.user_is_admin or\n                self.is_moderator_with_perms(muter, 'access', 'mail'))\n        )\n\n    def can_distinguish(self,user):\n        return (user\n                and (c.user_is_admin\n                     or self.is_moderator_with_perms(user, 'posts')))\n\n    def can_change_stylesheet(self, user):\n        if c.user_is_loggedin:\n            return (\n                c.user_is_admin or self.is_moderator_with_perms(user, 'config'))\n        else:\n            return False\n\n    def parse_css(self, content, verify=True):\n        from r2.lib import cssfilter\n        from r2.lib.template_helpers import (\n            make_url_protocol_relative,\n            static,\n        )\n\n        if g.css_killswitch or (verify and not self.can_change_stylesheet(c.user)):\n            return (None, None)\n\n        if not content:\n            return ([], \"\")\n\n        # parse in regular old http mode\n        images = ImagesByWikiPage.get_images(self, \"config/stylesheet\")\n\n        if self.quarantine:\n            images = {name: static('blank.png') for name, url in images.iteritems()}\n\n        protocol_relative_images = {\n            name: make_url_protocol_relative(url)\n            for name, url in images.iteritems()}\n        parsed, errors = cssfilter.validate_css(\n            content,\n            protocol_relative_images,\n        )\n\n        return (errors, parsed)\n\n    def change_css(self, content, parsed, prev=None, reason=None, author=None, force=False):\n        from r2.models import ModAction\n        from r2.lib.media import upload_stylesheet\n\n        if not author:\n            author = c.user\n\n        if content is None:\n            content = ''\n        try:\n            wiki = WikiPage.get(self, 'config/stylesheet')\n        except tdb_cassandra.NotFound:\n            wiki = WikiPage.create(self, 'config/stylesheet')\n        wr = wiki.revise(content, previous=prev, author=author._id36, reason=reason, force=force)\n\n        if parsed:\n            self.stylesheet_url = upload_stylesheet(parsed)\n            self.stylesheet_url_http = \"\"\n            self.stylesheet_url_https = \"\"\n        else:\n            self.stylesheet_url = \"\"\n            self.stylesheet_url_http = \"\"\n            self.stylesheet_url_https = \"\"\n        self._commit()\n\n        if wr:\n            ModAction.create(self, author, action='wikirevise', details='Updated subreddit stylesheet')\n\n        return wr\n\n    def is_special(self, user):\n        return (user\n                and (c.user_is_admin\n                     or self.is_moderator(user)\n                     or self.is_contributor(user)))\n\n    def should_ratelimit(self, user, kind):\n        if self.is_special(user):\n            return False\n\n        hook = hooks.get_hook(\"account.is_ratelimit_exempt\")\n        ratelimit_exempt = hook.call_until_return(account=c.user)\n        if ratelimit_exempt:\n            return False\n\n        if kind == 'comment':\n            rl_karma = g.MIN_RATE_LIMIT_COMMENT_KARMA\n        else:\n            rl_karma = g.MIN_RATE_LIMIT_KARMA\n\n        return user.karma(kind, self) < rl_karma\n\n    def can_view(self, user):\n        if c.user_is_admin:\n            return True\n\n        if self.spammy() or not self.is_exposed(user):\n            return False\n        else:\n            return self.is_allowed_to_view(user)\n\n    def can_view_in_modlist(self, user):\n        if c.user_is_admin:\n            return True\n        elif self.spammy():\n            return False\n        else:\n            return self.is_allowed_to_view(user)\n\n    def is_allowed_to_view(self, user):\n        \"\"\"Returns whether user can view based on permissions and settings\"\"\"\n        if self.type in ('public', 'restricted',\n                         'gold_restricted', 'archived'):\n            return True\n        elif c.user_is_loggedin:\n            if self.type == 'gold_only':\n                return (user.gold or\n                    user.gold_charter or\n                    self.is_moderator(user) or\n                    self.is_moderator_invite(user))\n\n            return (self.is_contributor(user) or\n                    self.is_moderator(user) or\n                    self.is_moderator_invite(user))\n\n    def is_exposed(self, user):\n        \"\"\"Return whether user is opted in to the subreddit's content.\n\n        If a subreddit is quarantined, users must opt-in before viewing its\n        content. Logged out users cannot opt-in, and all users are considered\n        opted-in to non-quarantined subreddits.\n        \"\"\"\n        if not self.quarantine:\n            return True\n        elif not user:\n            return False\n        elif (user.email_verified and\n              QuarantinedSubredditOptInsByAccount.is_opted_in(user, self)):\n            return True\n\n        return False\n\n    @property\n    def is_embeddable(self):\n        return (self.type not in Subreddit.private_types and\n                not self.over_18 and not self._spam and not self.quarantine)\n\n    def can_demod(self, bully, victim):\n        bully_rel = self.get_moderator(bully)\n        if bully_rel is not None and bully == victim:\n            # mods can always demod themselves\n            return True\n        victim_rel = self.get_moderator(victim)\n        return (\n            bully_rel is not None\n            and victim_rel is not None\n            and bully_rel.is_superuser()  # limited mods can't demod\n            and bully_rel._date <= victim_rel._date)\n\n    @classmethod\n    def load_subreddits(cls, links, return_dict = True, stale=False):\n        \"\"\"returns the subreddits for a list of links. it also preloads the\n        permissions for the current user.\"\"\"\n        srids = set(l.sr_id for l in links\n                    if getattr(l, \"sr_id\", None) is not None)\n        subreddits = {}\n        if srids:\n            subreddits = cls._byID(srids, data=True, stale=stale)\n\n        if subreddits and c.user_is_loggedin:\n            # dict( {Subreddit,Account,name} -> Relationship )\n            SRMember._fast_query(subreddits.values(), (c.user,), ('moderator',),\n                                 data=True)\n\n        return subreddits if return_dict else subreddits.values()\n\n    def keep_for_rising(self, sr_id):\n        \"\"\"Return whether or not to keep a thing in rising for this SR.\"\"\"\n        return sr_id == self._id\n\n    @classmethod\n    def get_sr_user_relations(cls, user, srs):\n        \"\"\"Return SubredditUserRelations for the user and subreddits.\n\n        The SubredditUserRelation objects indicate whether the user is a\n        moderator, contributor, subscriber, banned, or muted. This method\n        batches the lookups of all the relations for all the subreddits.\n\n        \"\"\"\n\n        moderator_srids = set()\n        contributor_srids = set()\n        banned_srids = set()\n        muted_srids = set()\n        subscriber_srids = cls.user_subreddits(user, limit=None)\n\n        if user and c.user_is_loggedin:\n            res = SRMember._fast_query(\n                thing1s=srs,\n                thing2s=user,\n                name=[\"moderator\", \"contributor\", \"banned\", \"muted\"],\n            )\n            # _fast_query returns a dict of {(t1, t2, name): rel}, with rel of\n            # None if the relation doesn't exist\n            rels = [rel for rel in res.itervalues() if rel]\n            for rel in rels:\n                rel_name = rel._name\n                sr_id = rel._thing1_id\n\n                if rel_name == \"moderator\":\n                    moderator_srids.add(sr_id)\n                elif rel_name == \"contributor\":\n                    contributor_srids.add(sr_id)\n                elif rel_name == \"banned\":\n                    banned_srids.add(sr_id)\n                elif rel_name == \"muted\":\n                    muted_srids.add(sr_id)\n\n        ret = {}\n        for sr in srs:\n            sr_id = sr._id\n            ret[sr_id] = SubredditUserRelations(\n                subscriber=sr_id in subscriber_srids,\n                moderator=sr_id in moderator_srids,\n                contributor=sr_id in contributor_srids,\n                banned=sr_id in banned_srids,\n                muted=sr_id in muted_srids,\n            )\n        return ret\n\n    @classmethod\n    def add_props(cls, user, wrapped):\n        srs = {item.lookups[0] for item in wrapped}\n        sr_user_relations = cls.get_sr_user_relations(user, srs)\n\n        for item in wrapped:\n            relations = sr_user_relations[item._id]\n            item.subscriber = relations.subscriber\n            item.moderator = relations.moderator\n            item.contributor = relations.contributor\n            item.banned = relations.banned\n            item.muted = relations.muted\n\n            if item.hide_subscribers and not c.user_is_admin:\n                item._ups = 0\n\n            item.score_hidden = (\n                not item.can_view(user) or\n                item.hide_num_users_info\n            )\n\n            item.score = item._ups\n\n            # override \"voting\" score behavior (it will override the use of\n            # item.score in builder.py to be ups-downs)\n            item.likes = item.subscriber or None\n            base_score = item.score - (1 if item.likes else 0)\n            item.voting_score = [(base_score + x - 1) for x in range(3)]\n            item.score_fmt = Score.subscribers\n\n            #will seem less horrible when add_props is in pages.py\n            from r2.lib.pages import UserText\n            if item.public_description or item.description:\n                text = (item.public_description or\n                        summarize_markdown(item.description))\n                item.public_description_usertext = UserText(item, text)\n            else:\n                item.public_description_usertext = None\n\n\n        Printable.add_props(user, wrapped)\n\n    cache_ignore = {\n        \"description\",\n        \"public_description\",\n        \"subscribers\",\n    }.union(Printable.cache_ignore)\n\n    @staticmethod\n    def wrapped_cache_key(wrapped, style):\n        s = Printable.wrapped_cache_key(wrapped, style)\n        return s\n\n    @classmethod\n    def default_subreddits(cls, ids=True):\n        \"\"\"Return the subreddits a user with no subscriptions would see.\"\"\"\n        location = get_user_location()\n        srids = LocalizedDefaultSubreddits.get_defaults(location)\n\n        srs = Subreddit._byID(srids, data=True, return_dict=False, stale=True)\n        srs = filter(lambda sr: sr.allow_top, srs)\n\n        if ids:\n            return [sr._id for sr in srs]\n        else:\n            return srs\n\n    @classmethod\n    def featured_subreddits(cls):\n        \"\"\"Return the curated list of subreddits shown during onboarding.\"\"\"\n        location = get_user_location()\n        srids = LocalizedFeaturedSubreddits.get_featured(location)\n\n        srs = Subreddit._byID(srids, data=True, return_dict=False, stale=True)\n        srs = filter(lambda sr: sr.discoverable, srs)\n\n        return srs\n\n    @classmethod\n    @memoize('random_reddits', time = 1800)\n    def random_reddits_cached(cls, user_name, sr_ids, limit):\n        # First filter out any subreddits that don't have a new enough post\n        # to be included in the front page (just doing this may remove enough\n        # to get below the limit anyway)\n        sr_ids = SubredditsActiveForFrontPage.filter_inactive_ids(sr_ids)\n        if len(sr_ids) <= limit:\n            return sr_ids\n\n        return random.sample(sr_ids, limit)\n\n    @classmethod\n    def random_reddits(cls, user_name, sr_ids, limit):\n        \"\"\"Select a random subset from sr_ids.\n\n        Used for limiting the number of subscribed subreddits shown on a user's\n        front page. Selection is cached for a while so the front page doesn't\n        jump around.\n\n        \"\"\"\n\n        if not limit:\n            return sr_ids\n\n        # if the user is subscribed to them, the automatic subreddits should\n        # always be in the front page set and not count towards the limit\n        if g.automatic_reddits:\n            automatics = Subreddit._by_name(\n                g.automatic_reddits, stale=True).values()\n            automatic_ids = [sr._id for sr in automatics if sr._id in sr_ids]\n            sr_ids = [sr_id for sr_id in sr_ids if sr_id not in automatic_ids]\n        else:\n            automatic_ids = []\n\n        if len(sr_ids) > limit:\n            sr_ids = sorted(sr_ids)\n            sr_ids = cls.random_reddits_cached(user_name, sr_ids, limit)\n\n        return sr_ids + automatic_ids\n\n    @classmethod\n    def random_reddit(cls, over18=False, user=None):\n        if over18:\n            sr_ids = NamedGlobals.get(\"popular_over_18_sr_ids\")\n        else:\n            sr_ids = NamedGlobals.get(\"popular_sr_ids\")\n\n        if user:\n            excludes = set(cls.user_subreddits(user, limit=None))\n            sr_ids = list(set(sr_ids) - excludes)\n\n        if not sr_ids:\n            return Subreddit._by_name(g.default_sr)\n\n        sr_id = random.choice(sr_ids)\n        sr = Subreddit._byID(sr_id, data=True)\n        return sr\n\n    @classmethod\n    def update_popular_subreddits(cls, limit=5000):\n        q = cls._query(cls.c.type == \"public\", sort=desc('_downs'), limit=limit,\n                       data=True)\n        srs = list(q)\n\n        # split the list into two based on whether the subreddit is 18+ or not\n        sr_ids = []\n        over_18_sr_ids = []\n\n        # /r/promos is public but has special handling to make it unviewable\n        promo_sr_id = cls.get_promote_srid()\n\n        for sr in srs:\n            if not sr.discoverable:\n                continue\n\n            if sr._id == promo_sr_id:\n                continue\n\n            if not sr.over_18:\n                sr_ids.append(sr._id)\n            else:\n                over_18_sr_ids.append(sr._id)\n\n        NamedGlobals.set(\"popular_sr_ids\", sr_ids)\n        NamedGlobals.set(\"popular_over_18_sr_ids\", over_18_sr_ids)\n\n    @classmethod\n    def random_subscription(cls, user):\n        if user.has_subscribed:\n            sr_ids = Subreddit.subscribed_ids_by_user(user)\n        else:\n            sr_ids = Subreddit.default_subreddits(ids=True)\n\n        return (Subreddit._byID(random.choice(sr_ids), data=True)\n                if sr_ids else Subreddit._by_name(g.default_sr))\n\n    @classmethod\n    def user_subreddits(cls, user, ids=True, limit=DEFAULT_LIMIT):\n        \"\"\"\n        subreddits that appear in a user's listings. If the user has\n        subscribed, returns the stored set of subscriptions.\n\n        limit - if it's Subreddit.DEFAULT_LIMIT, limits to 50 subs\n                (100 for gold users)\n                if it's None, no limit is used\n                if it's an integer, then that many subs will be returned\n\n        Otherwise, return the default set.\n        \"\"\"\n        # Limit the number of subs returned based on user status,\n        # if no explicit limit was passed\n        if limit is Subreddit.DEFAULT_LIMIT:\n            if user and user.gold:\n                # Goldies get extra subreddits\n                limit = Subreddit.gold_limit\n            else:\n                limit = Subreddit.sr_limit\n\n        # note: for user not logged in, the fake user account has\n        # has_subscribed == False by default.\n        if user and user.has_subscribed:\n            sr_ids = Subreddit.subscribed_ids_by_user(user)\n            sr_ids = cls.random_reddits(user.name, sr_ids, limit)\n\n            return sr_ids if ids else Subreddit._byID(sr_ids,\n                                                      data=True,\n                                                      return_dict=False,\n                                                      stale=True)\n        else:\n            return cls.default_subreddits(ids=ids)\n\n\n    # Used to pull all of the SRs a given user moderates or is a contributor\n    # to (which one is controlled by query_param)\n    @classmethod\n    def special_reddits(cls, user, query_param):\n        lookup = getattr(cls, 'reverse_%s_ids' % query_param)\n        return lookup(user)\n\n    @classmethod\n    def subscribe_defaults(cls, user):\n        if not user.has_subscribed:\n            user.has_subscribed = True\n            user._commit()\n            srs = cls.user_subreddits(user=None, ids=False, limit=None)\n            cls.subscribe_multiple(user, srs)\n\n    def keep_item(self, wrapped):\n        if c.user_is_admin:\n            return True\n\n        user = c.user if c.user_is_loggedin else None\n        return self.can_view(user)\n\n    def __eq__(self, other):\n        if type(self) != type(other):\n            return False\n\n        if isinstance(self, FakeSubreddit):\n            return self is other\n\n        return self._id == other._id\n\n    def __ne__(self, other):\n        return not self.__eq__(other)\n\n    @staticmethod\n    def get_all_mod_ids(srs):\n        from r2.lib.db.thing import Merge\n        srs = tup(srs)\n        queries = [\n            SRMember._simple_query(\n                [\"_thing2_id\"],\n                SRMember.c._thing1_id == sr._id,\n                SRMember.c._name == 'moderator',\n            ) for sr in srs\n        ]\n\n        merged = Merge(queries)\n        return [rel._thing2_id for rel in list(merged)]\n\n    def update_moderator_permissions(self, user, **kwargs):\n        \"\"\"Grants or denies permissions to this moderator.\n\n        Does nothing if the given user is not a moderator. Args are named\n        parameters with bool or None values (use None to all back to the default\n        for a permission).\n        \"\"\"\n        rel = self.get_moderator(user)\n        if rel:\n            rel.update_permissions(**kwargs)\n            rel._commit()\n\n    def add_rel_note(self, type, user, note):\n        rel = getattr(self, \"get_%s\" % type)(user)\n        if not rel:\n            raise ValueError(\"User is not %s.\" % type)\n        rel.note = note\n        rel._commit()\n\n    def get_live_promos(self):\n        from r2.lib import promote\n        return promote.get_live_promotions([self.name])\n\n    def schedule_unban(self, kind, victim, banner, duration):\n        return SubredditTempBan.schedule(\n            self,\n            kind,\n            victim,\n            banner,\n            datetime.timedelta(days=duration),\n        )\n\n    def unschedule_unban(self, victim, type):\n        SubredditTempBan.unschedule(self.name, victim.name, type)\n\n    def get_tempbans(self, type=None, names=None):\n        return SubredditTempBan.search(self.name, type, names)\n\n    def get_muted_items(self, names=None):\n        return MutedAccountsBySubreddit.search(self, names)\n\n    def add_gilding_seconds(self):\n        from r2.models.gold import get_current_value_of_month\n        seconds = get_current_value_of_month()\n        self._incr(\"gilding_server_seconds\", int(seconds))\n\n    @property\n    def allow_gilding(self):\n        return not self.quarantine\n\n    @classmethod\n    def get_promote_srid(cls):\n        try:\n            return cls._by_name(g.promo_sr_name, stale=True)._id\n        except NotFound:\n            return None\n\n    def is_subscriber(self, user):\n        try:\n            return bool(SubscribedSubredditsByAccount.fast_query(user, self))\n        except tdb_cassandra.NotFound:\n            return False\n\n    def add_subscriber(self, user):\n        SubscribedSubredditsByAccount.create(user, self)\n        SubscriptionsByDay.create(self, user)\n        add_legacy_subscriber(self, user)\n        self._incr('_ups', 1)\n\n    @classmethod\n    def subscribe_multiple(cls, user, srs):\n        SubscribedSubredditsByAccount.create(user, srs)\n        SubscriptionsByDay.create(srs, user)\n        add_legacy_subscriber(srs, user)\n        for sr in srs:\n            sr._incr('_ups', 1)\n\n    def remove_subscriber(self, user):\n        SubscribedSubredditsByAccount.destroy(user, self)\n        remove_legacy_subscriber(self, user)\n        self._incr('_ups', -1)\n\n    @classmethod\n    def subscribed_ids_by_user(cls, user):\n        return SubscribedSubredditsByAccount.get_all_sr_ids(user)\n\n    @classmethod\n    def reverse_subscriber_ids(cls, user):\n        # This is just for consistency with all the other UserRel types\n        return cls.subscribed_ids_by_user(user)\n\n    def get_rgb(self, fade=0.8):\n        r = int(256 - (hash(str(self._id)) % 256)*(1-fade))\n        g = int(256 - (hash(str(self._id) + ' ') % 256)*(1-fade))\n        b = int(256 - (hash(str(self._id) + '  ') % 256)*(1-fade))\n        return (r, g, b)\n\n    def set_sticky(self, link, log_user=None, num=None):\n        unstickied_fullnames = []\n\n        if not self.sticky_fullnames:\n            self.sticky_fullnames = [link._fullname]\n        else:\n            # don't re-sticky something that's already stickied\n            if link._fullname in self.sticky_fullnames:\n                return\n\n            # XXX: have to work with a copy of the list instead of modifying\n            #   it directly, because it doesn't get marked as \"dirty\" and\n            #   saved properly unless we assign a new list to the attr\n            sticky_fullnames = self.sticky_fullnames[:]\n\n            # if a particular slot was specified and is in use, replace it\n            if num and num <= len(sticky_fullnames):\n                unstickied_fullnames.append(sticky_fullnames[num-1])\n                sticky_fullnames[num-1] = link._fullname\n            else:\n                # either didn't specify a slot or it's empty, just append\n\n                # if we're already at the max number of stickies, remove\n                # the bottom-most to make room for this new one\n                if self.has_max_stickies:\n                    unstickied_fullnames.extend(\n                        sticky_fullnames[self.MAX_STICKIES-1:])\n                    sticky_fullnames = sticky_fullnames[:self.MAX_STICKIES-1]\n\n                sticky_fullnames.append(link._fullname)\n\n            self.sticky_fullnames = sticky_fullnames\n\n        self._commit()\n\n        if log_user:\n            from r2.models import Link, ModAction\n            for fullname in unstickied_fullnames:\n                unstickied = Link._by_fullname(fullname)\n                ModAction.create(self, log_user, \"unsticky\",\n                    target=unstickied, details=\"replaced\")\n            ModAction.create(self, log_user, \"sticky\", target=link)\n\n    def remove_sticky(self, link, log_user=None):\n        # XXX: have to work with a copy of the list instead of modifying\n        #   it directly, because it doesn't get marked as \"dirty\" and\n        #   saved properly unless we assign a new list to the attr\n        sticky_fullnames = self.sticky_fullnames[:]\n        try:\n            sticky_fullnames.remove(link._fullname)\n        except ValueError:\n            return\n\n        self.sticky_fullnames = sticky_fullnames\n        self._commit()\n\n        if log_user:\n            from r2.models import ModAction\n            ModAction.create(self, log_user, \"unsticky\", target=link)\n\n    @property\n    def has_max_stickies(self):\n        if not self.sticky_fullnames:\n            return False\n        return len(self.sticky_fullnames) >= self.MAX_STICKIES\n\n\nclass SubscribedSubredditsByAccount(tdb_cassandra.DenormalizedRelation):\n    _use_db = True\n    _write_last_modified = False\n    _read_consistency_level = tdb_cassandra.CL.ONE\n    _write_consistency_level = tdb_cassandra.CL.QUORUM\n    _connection_pool = 'main'\n    _views = []\n    _extra_schema_creation_args = {\n        \"default_validation_class\": DATE_TYPE,\n    }\n\n    @classmethod\n    def value_for(cls, user, sr):\n        return datetime.datetime.now(g.tz)\n\n    @classmethod\n    def get_all_sr_ids(cls, user):\n        key = cls.__name__ + user._id36\n        sr_ids = g.cassandra_local_cache.get(key)\n        if sr_ids is None:\n            r = cls._cf.xget(user._id36)\n            sr_ids = [int(sr_id36, 36) for sr_id36, val in r]\n            g.cassandra_local_cache.set(key, sr_ids)\n\n        return sr_ids\n\n\nclass SubscriptionsByDay(tdb_cassandra.View):\n    _use_db = True\n    _connection_pool = 'main'\n    _compare_with = types.CompositeType(types.AsciiType(), types.AsciiType())\n    _extra_schema_creation_args = {\n        \"key_validation_class\": DATE_TYPE,\n    }\n\n    @classmethod\n    def create(cls, srs, user):\n        rowkey = datetime.datetime.now(g.tz).replace(\n            hour=0,\n            minute=0,\n            second=0,\n            microsecond=0,\n        )\n        srs = tup(srs)\n        columns = {(sr._id36, user._id36): \"\" for sr in srs}\n        cls._cf.insert(rowkey, columns)\n\n    @classmethod\n    def get_all_counts(cls, date):\n        date = date.replace(\n            hour=0,\n            minute=0,\n            second=0,\n            microsecond=0,\n            tzinfo=g.tz,\n        )\n\n        gen = cls._cf.xget(date)\n        (prev_sr_id36, user_id36), val = next(gen)\n\n        count = 1\n        for (sr_id36, user_id36), val in gen:\n            if sr_id36 == prev_sr_id36:\n                count += 1\n            else:\n                yield (prev_sr_id36, count)\n                prev_sr_id36 = sr_id36\n                count = 1\n        yield (prev_sr_id36, count)\n\n    @classmethod\n    def write_counts(cls, days_ago=1):\n        from sqlalchemy.orm import scoped_session, sessionmaker\n        from r2.models.traffic import SubscriptionsBySubreddit, engine\n\n        Session = scoped_session(sessionmaker(bind=engine))\n\n        date = datetime.datetime.now(g.tz) - datetime.timedelta(days=days_ago)\n        pg_date = date.replace(\n            hour=0,\n            minute=0,\n            second=0,\n            microsecond=0,\n            tzinfo=None,\n        )\n        print \"writing subscribers for %s\" % date\n\n        num_srs = 0\n        num_subscribers = 0\n        for sr_id36, count in cls.get_all_counts(date):\n            sr = Subreddit._byID36(sr_id36, data=True)\n            row = SubscriptionsBySubreddit(\n                subreddit=sr.name,\n                date=pg_date,\n                subscriber_count=count,\n            )\n            Session.merge(row)\n            Session.commit()\n            num_srs += 1\n            num_subscribers += count\n        print \"%s subscribers in %s subreddits\" % (num_subscribers, num_srs)\n        Session.remove()\n\n\nclass FakeSubreddit(BaseSite):\n    _defaults = dict(Subreddit._defaults,\n        link_flair_position='right',\n        flair_enabled=False,\n    )\n\n    def __init__(self):\n        BaseSite.__init__(self)\n\n    def keep_for_rising(self, sr_id):\n        return False\n\n    @property\n    def _should_wiki(self):\n        return False\n\n    @property\n    def allow_gilding(self):\n        return True\n\n    @property\n    def allow_ads(self):\n        return True\n\n    def is_moderator(self, user):\n        if c.user_is_loggedin and c.user_is_admin:\n            return FakeSRMember(ModeratorPermissionSet)\n\n    def can_view(self, user):\n        return True\n\n    def can_comment(self, user):\n        return False\n\n    def can_submit(self, user, promotion=False):\n        return False\n\n    def can_change_stylesheet(self, user):\n        return False\n\n    def is_banned(self, user):\n        return False\n\n    def is_muted(self, user):\n        return False\n\n    def get_all_comments(self):\n        from r2.lib.db import queries\n        return queries.get_all_comments()\n\n    def get_gilded(self):\n        raise NotImplementedError()\n\n    def spammy(self):\n        return False\n\nclass FriendsSR(FakeSubreddit):\n    name = 'friends'\n    title = 'friends'\n    _defaults = dict(\n        FakeSubreddit._defaults,\n        login_required=True,\n    )\n\n    def get_links(self, sort, time):\n        from r2.lib.db import queries\n\n        friends = c.user.get_recently_submitted_friend_ids()\n        if not friends:\n            return []\n\n        # with the precomputer enabled, this Subreddit only supports\n        # being sorted by 'new'. it would be nice to have a\n        # cleaner UI than just blatantly ignoring their sort,\n        # though\n        sort = 'new'\n        time = 'all'\n\n        friends = Account._byID(friends, return_dict=False)\n\n        crs = [queries.get_submitted(friend, sort, time)\n               for friend in friends]\n        return queries.MergedCachedResults(crs)\n\n    def get_all_comments(self):\n        from r2.lib.db import queries\n\n        friends = c.user.get_recently_commented_friend_ids()\n        if not friends:\n            return []\n\n        # with the precomputer enabled, this Subreddit only supports\n        # being sorted by 'new'. it would be nice to have a\n        # cleaner UI than just blatantly ignoring their sort,\n        # though\n        sort = 'new'\n        time = 'all'\n\n        friends = Account._byID(friends,\n                                return_dict=False)\n\n        crs = [queries.get_comments(friend, sort, time)\n               for friend in friends]\n        return queries.MergedCachedResults(crs)\n\n    def get_gilded(self):\n        from r2.lib.db.queries import get_gilded_users\n\n        friends = c.user.friend_ids()\n\n        if not friends:\n            return []\n\n        return get_gilded_users(friends)\n\n\nclass AllSR(FakeSubreddit):\n    name = 'all'\n    title = 'all subreddits'\n    path = '/r/all'\n\n    def keep_for_rising(self, sr_id):\n        return True\n\n    def get_links(self, sort, time):\n        from r2.models import Link\n        from r2.lib.db import queries\n        q = Link._query(\n            sort=queries.db_sort(sort),\n            read_cache=True,\n            write_cache=True,\n            cache_time=60,\n            data=True,\n            filter_primary_sort_only=True,\n        )\n        if time != 'all':\n            q._filter(queries.db_times[time])\n        return q\n\n    def get_all_comments(self):\n        from r2.lib.db import queries\n        return queries.get_all_comments()\n\n    def get_gilded(self):\n        from r2.lib.db import queries\n        return queries.get_all_gilded()\n\n    def get_reported(self, include_links=True, include_comments=True):\n        from r2.lib.db import queries\n        from r2.lib.db.thing import Merge\n        qs = []\n\n        if include_links:\n            qs.append(queries.get_reported_links(None))\n\n        if include_comments:\n            qs.append(queries.get_reported_comments(None))\n\n        return MergedCachedQuery(qs)\n\nclass AllMinus(AllSR):\n    analytics_name = \"all\"\n    name = _(\"%s (filtered)\") % \"all\"\n\n    def __init__(self, srs):\n        AllSR.__init__(self)\n        self.exclude_srs = srs\n        self.exclude_sr_ids = [sr._id for sr in srs]\n\n    def keep_for_rising(self, sr_id):\n        return sr_id not in self.exclude_sr_ids\n\n    @property\n    def title(self):\n        sr_names = ', '.join(sr.name for sr in self.exclude_srs)\n        return 'all subreddits except ' + sr_names\n\n    @property\n    def path(self):\n        return '/r/all-' + '-'.join(sr.name for sr in self.exclude_srs)\n\n    def get_links(self, sort, time):\n        from r2.models import Link\n        from r2.lib.db.operators import not_\n        q = AllSR.get_links(self, sort, time)\n        if c.user.gold and self.exclude_sr_ids:\n            q._filter(not_(Link.c.sr_id.in_(self.exclude_sr_ids)))\n        return q\n\n\nclass Filtered(object):\n    unfiltered_path = None\n\n    @property\n    def path(self):\n        return '/me/f/%s' % self.filtername\n\n    @property\n    def title(self):\n        return self.name\n\n    @property\n    def name(self):\n        return _(\"%s (filtered)\") % self.filtername\n\n    @property\n    def multi_path(self):\n        return ('/user/%s/f/%s' % (c.user.name, self.filtername)).lower()\n\n    def _get_filtered_subreddits(self):\n        try:\n            multi = LabeledMulti._byID(self.multi_path)\n        except tdb_cassandra.NotFound:\n            multi = None\n        filtered_srs = multi.srs if multi else []\n        return sorted(filtered_srs, key=lambda sr: sr.name)\n\n\nclass AllFiltered(Filtered, AllMinus):\n    unfiltered_path = '/r/all'\n    filtername = 'all'\n\n    def __init__(self):\n        filters = self._get_filtered_subreddits() if c.user.gold else []\n        AllMinus.__init__(self, filters)\n\n\nclass _DefaultSR(FakeSubreddit):\n    analytics_name = 'frontpage'\n    #notice the space before reddit.com\n    name = ' reddit.com'\n    path = '/'\n    header = g.default_header_url\n\n    def _get_sr_ids(self):\n        if not c.defaultsr_cached_sr_ids:\n            user = c.user if c.user_is_loggedin else None\n            c.defaultsr_cached_sr_ids = Subreddit.user_subreddits(user)\n        return c.defaultsr_cached_sr_ids\n\n    def keep_for_rising(self, sr_id):\n        return sr_id in self._get_sr_ids()\n\n    def is_moderator(self, user):\n        return False\n\n    def get_links(self, sort, time):\n        sr_ids = self._get_sr_ids()\n        return get_links_sr_ids(sr_ids, sort, time)\n\n    @property\n    def title(self):\n        return _(g.short_description)\n\n# This is the base class for the instantiated front page reddit\nclass DefaultSR(_DefaultSR):\n    @property\n    def _base(self):\n        try:\n            return Subreddit._by_name(g.default_sr, stale=True)\n        except NotFound:\n            return None\n\n    def wiki_can_submit(self, user):\n        return True\n\n    @property\n    def wiki_use_subreddit_karma(self):\n        return False\n\n    @property\n    def _should_wiki(self):\n        return True\n\n    @property\n    def wikimode(self):\n        return self._base.wikimode if self._base else \"disabled\"\n\n    @property\n    def wiki_edit_karma(self):\n        return self._base.wiki_edit_karma\n\n    @property\n    def wiki_edit_age(self):\n        return self._base.wiki_edit_age\n\n    def is_wikicontributor(self, user):\n        return self._base.is_wikicontributor(user)\n\n    def is_wikibanned(self, user):\n        return self._base.is_wikibanned(user)\n\n    def is_wikicreate(self, user):\n        return self._base.is_wikicreate(user)\n\n    @property\n    def _fullname(self):\n        return \"t5_6\"\n\n    @property\n    def _id36(self):\n        return self._base._id36\n\n    @property\n    def type(self):\n        return self._base.type if self._base else \"public\"\n\n    @property\n    def header(self):\n        return (self._base and self._base.header) or _DefaultSR.header\n\n    @property\n    def header_title(self):\n        return (self._base and self._base.header_title) or \"\"\n\n    @property\n    def header_size(self):\n        return (self._base and self._base.header_size) or None\n\n    @property\n    def stylesheet_url(self):\n        return self._base.stylesheet_url if self._base else \"\"\n\n    @property\n    def stylesheet_url_http(self):\n        return self._base.stylesheet_url_http if self._base else \"\"\n\n    @property\n    def stylesheet_url_https(self):\n        return self._base.stylesheet_url_https if self._base else \"\"\n\n    def get_all_comments(self):\n        from r2.lib.db.queries import _get_sr_comments, merge_results\n        sr_ids = Subreddit.user_subreddits(c.user)\n        results = [_get_sr_comments(sr_id) for sr_id in sr_ids]\n        return merge_results(*results)\n\n    def get_gilded(self):\n        from r2.lib.db.queries import get_gilded\n        return get_gilded(Subreddit.user_subreddits(c.user))\n\n    def get_live_promos(self):\n        from r2.lib import promote\n        srs = Subreddit.user_subreddits(c.user, ids=False)\n        # '' is for promos targeted to the frontpage\n        sr_names = [self.name] + [sr.name for sr in srs]\n        return promote.get_live_promotions(sr_names)\n\n\nclass MultiReddit(FakeSubreddit):\n    name = 'multi'\n    header = \"\"\n    _defaults = dict(\n        FakeSubreddit._defaults,\n        weighting_scheme=\"classic\",\n    )\n\n    # See comment in normalized_hot before adding new values here.\n    AGEWEIGHTS = {\n        \"classic\": 0.0,\n        \"fresh\": 0.15,\n    }\n\n    def __init__(self, path=None, srs=None):\n        FakeSubreddit.__init__(self)\n        if path is not None:\n            self._path = path\n        self._srs = srs or []\n\n    @property\n    def srs(self):\n        return self._srs\n\n    @property\n    def sr_ids(self):\n        return [sr._id for sr in self.srs]\n\n    @property\n    def kept_sr_ids(self):\n        return [sr._id for sr in self.srs if not sr._spam]\n\n    @property\n    def banned_sr_ids(self):\n        return [sr._id for sr in self.srs if sr._spam]\n\n    @property\n    def allows_referrers(self):\n        return all(sr.allows_referrers for sr in self.srs)\n\n    def keep_for_rising(self, sr_id):\n        return sr_id in self.kept_sr_ids\n\n    def is_moderator(self, user):\n        if not user:\n            return False\n\n        # Get moderator SRMember relations for all in srs\n        # if a relation doesn't exist there will be a None entry in the\n        # returned dict\n        mod_rels = SRMember._fast_query(self.srs, user, 'moderator', data=True)\n        if None in mod_rels.values():\n            return False\n        else:\n            return FakeSRMember(ModeratorPermissionSet)\n\n    def srs_with_perms(self, user, *perms):\n        return [sr for sr in self.srs\n                if sr.is_moderator_with_perms(user, *perms) and not sr._spam]\n\n    @property\n    def title(self):\n        return _('posts from %s') % ', '.join(sr.name for sr in self.srs)\n\n    @property\n    def path(self):\n        return self._path\n\n    @property\n    def over_18(self):\n        return any(sr.over_18 for sr in self.srs)\n\n    @property\n    def ageweight(self):\n        return self.AGEWEIGHTS.get(self.weighting_scheme, 0.0)\n\n    def get_links(self, sort, time):\n        return get_links_sr_ids(self.kept_sr_ids, sort, time)\n\n    def get_all_comments(self):\n        from r2.lib.db.queries import _get_sr_comments, merge_results\n        results = [_get_sr_comments(sr_id) for sr_id in self.kept_sr_ids]\n        return merge_results(*results)\n\n    def get_gilded(self):\n        from r2.lib.db.queries import get_gilded\n        return get_gilded(self.kept_sr_ids)\n\n    def get_live_promos(self):\n        from r2.lib import promote\n        srs = Subreddit._byID(self.kept_sr_ids, return_dict=False)\n        sr_names = [sr.name for sr in srs]\n        return promote.get_live_promotions(sr_names)\n\n\nclass TooManySubredditsError(Exception):\n    pass\n\n\nclass BaseLocalizedSubreddits(tdb_cassandra.View):\n    \"\"\"Mapping of location to subreddit ids\"\"\"\n    _use_db = False\n    _compare_with = ASCII_TYPE\n    _read_consistency_level = tdb_cassandra.CL.QUORUM\n    _write_consistency_level = tdb_cassandra.CL.QUORUM\n    _extra_schema_creation_args = {\n        \"key_validation_class\": ASCII_TYPE,\n        \"default_validation_class\": ASCII_TYPE,\n    }\n    GLOBAL = \"GLOBAL\"\n\n    @classmethod\n    def _rowkey(cls, location):\n        return str(location)\n\n    @classmethod\n    def lookup(cls, keys, update=False):\n        def _lookup(keys):\n            rows = cls._cf.multiget(keys)\n            ret = {}\n            for key in keys:\n                columns = rows[key] if key in rows else {}\n                id36s = columns.keys()\n                ret[key] = id36s\n            return ret\n\n        id36s_by_location = sgm(\n            cache=g.gencache,\n            keys=keys,\n            miss_fn=_lookup,\n            prefix=cls.CACHE_PREFIX,\n            stale=True,\n            _update=update,\n            ignore_set_errors=True,\n        )\n        ids_by_location = {location: [int(id36, 36) for id36 in id36s]\n                           for location, id36s in id36s_by_location.iteritems()}\n        return ids_by_location\n\n    @classmethod\n    def set_srs(cls, location, srs):\n        rowkey = cls._rowkey(location)\n        columns = {sr._id36: '' for sr in srs}\n\n        # update cassandra\n        try:\n            existing = cls._cf.get(rowkey)\n        except tdb_cassandra.NotFoundException:\n            existing = {}\n\n        cls._set_values(rowkey, columns)\n        removed_srid36s = set(existing.keys()) - set(columns.keys())\n        cls._remove(rowkey, removed_srid36s)\n\n        # update cache\n        id36s = columns.keys()\n        g.gencache.set_multi({rowkey: id36s}, prefix=cls.CACHE_PREFIX)\n\n    @classmethod\n    def set_global_srs(cls, srs):\n        location = cls.GLOBAL\n        cls.set_srs(location, srs)\n\n    @classmethod\n    def get_srids(cls, location):\n        if not location:\n            return []\n\n        rowkey = cls._rowkey(location)\n        ids_by_location = cls.lookup([rowkey])\n        srids = ids_by_location[rowkey]\n        return srids\n\n    @classmethod\n    def get_global_defaults(cls):\n        return cls.get_srids(cls.GLOBAL)\n\n    @classmethod\n    def get_localized_srs(cls, location):\n        location_key = cls._rowkey(location) if location else None\n        global_key = cls._rowkey(cls.GLOBAL)\n        keys = filter(None, [location_key, global_key])\n\n        ids_by_location = cls.lookup(keys)\n\n        if location_key and ids_by_location[location_key]:\n            c.used_localized_defaults = True\n            return ids_by_location[location_key]\n        else:\n            return ids_by_location[global_key]\n\n\nclass LocalizedDefaultSubreddits(BaseLocalizedSubreddits):\n    _use_db = True\n    _type_prefix = \"LocalizedDefaultSubreddits\"\n    CACHE_PREFIX = \"defaultsrs:\"\n\n    @classmethod\n    def get_defaults(cls, location):\n        return cls.get_localized_srs(location)\n\n\nclass LocalizedFeaturedSubreddits(BaseLocalizedSubreddits):\n    _use_db = True\n    _type_prefix = \"LocalizedFeaturedSubreddits\"\n    CACHE_PREFIX = \"featuredsrs:\"\n\n    @classmethod\n    def get_featured(cls, location):\n        return cls.get_localized_srs(location)\n\n\nclass LabeledMulti(tdb_cassandra.Thing, MultiReddit):\n    \"\"\"Thing with special columns that hold Subreddit ids and properties.\"\"\"\n    _use_db = True\n    _views = []\n    _bool_props = ('is_symlink', )\n    _defaults = dict(\n        MultiReddit._defaults,\n        visibility='private',\n        is_symlink=False,\n        description_md='',\n        display_name='',\n        copied_from=None,\n        key_color=\"#cee3f8\",  # A lovely shade of blue\n        icon_id='',\n        weighting_scheme=\"classic\",\n    )\n    _extra_schema_creation_args = {\n        \"key_validation_class\": UTF8_TYPE,\n        \"column_name_class\": UTF8_TYPE,\n        \"default_validation_class\": UTF8_TYPE,\n        \"column_validation_classes\": {\n            \"date\": pycassa.system_manager.DATE_TYPE,\n        },\n    }\n    _float_props = (\n        \"base_normalized_age_weight\",\n    )\n    _compare_with = UTF8_TYPE\n    _read_consistency_level = tdb_cassandra.CL.ONE\n    _write_consistency_level = tdb_cassandra.CL.QUORUM\n\n    SR_PREFIX = 'SR_'\n    MAX_SR_COUNT = 100\n\n    def __init__(self, _id=None, *args, **kwargs):\n        tdb_cassandra.Thing.__init__(self, _id, *args, **kwargs)\n        MultiReddit.__init__(self)\n        self._owner = None\n\n    @classmethod\n    def _byID(cls, ids, return_dict=True, properties=None, load_subreddits=True,\n              load_linked_multis=True):\n        ret = super(cls, cls)._byID(ids, return_dict=False,\n                                    properties=properties)\n        if not ret:\n            # the falsy return object must be converted to the proper type\n            # based on whether ids was an iterable and return_dict\n            if ret == []:\n                if return_dict:\n                    return {}\n                else:\n                    return []\n            else:\n                return\n\n        ret = cls._load(ret, load_subreddits=load_subreddits,\n                        load_linked_multis=load_linked_multis)\n        if isinstance(ret, cls):\n            return ret\n        elif return_dict:\n            return {thing._id: thing for thing in ret}\n        else:\n            return ret\n\n    @classmethod\n    def _load(cls, things, load_subreddits=True, load_linked_multis=True):\n        things, single = tup(things, ret_is_single=True)\n\n        # some objects are being loaded for the first time and need basic setup\n        never_loaded = [t for t in things if not t._owner]\n        if never_loaded:\n            owner_fullnames = set(t.owner_fullname for t in never_loaded)\n            owners = Thing._by_fullname(\n                owner_fullnames, data=True, return_dict=True)\n            for t in things:\n                if t in never_loaded:\n                    t._owner = owners[t.owner_fullname]\n                    t._srs_loaded = False\n                    t._linked_multi = None\n\n        if load_linked_multis:\n            needs_linked_multis = [t.copied_from for t in things\n                                   if t.is_symlink and not t._linked_multi]\n            if needs_linked_multis:\n                multis = LabeledMulti._byID(needs_linked_multis, return_dict=True)\n                for t in things:\n                    if t.copied_from in needs_linked_multis:\n                        t._linked_multi = multis[t.copied_from]\n\n        # some objects may have been retrieved from cache and need srs\n        if load_subreddits:\n            needs_srs = [t for t in things if not t._srs_loaded]\n            if needs_srs:\n                sr_ids = set(\n                    itertools.chain.from_iterable(t.sr_ids for t in needs_srs))\n                srs = Subreddit._byID(\n                    sr_ids, data=True, return_dict=True, stale=True)\n                for t in things:\n                    if t in needs_srs:\n                        t._srs = [srs[sr_id] for sr_id in t.sr_ids]\n                        t._srs_loaded = True\n\n        return things[0] if single else things\n\n    @property\n    def linked_multi(self):\n        return self._linked_multi\n\n    @property\n    def sr_ids(self):\n        return self.sr_props.keys()\n\n    @property\n    def srs(self):\n        if self.is_symlink:\n            if (not self.copied_from or self.copied_from == self._id\n                    or not self.linked_multi):\n                raise RedditError(\"Upstream symlinked multi can't be retrieved.\")\n            if not self.linked_multi.can_view(self.owner):\n                raise RedditError(\"Upstream symlinked multi is not visible.\")\n\n            return self.linked_multi.srs\n\n        if not self._srs_loaded:\n            g.log.error(\"%s: accessed subreddits without loading\", self)\n            self._srs = Subreddit._byID(\n                self.sr_ids, data=True, return_dict=False)\n        return self._srs\n\n    @property\n    def owner(self):\n        return self._owner\n\n    @property\n    def sr_columns(self):\n        # limit to max subreddit count, allowing a little fudge room for\n        # cassandra inconsistency\n        if self.is_symlink:\n            if not getattr(self, '_linked_multi', None):\n                self._linked_multi = LabeledMulti._byID(self.copied_from)\n            return self.linked_multi.sr_columns\n\n        remaining = self.MAX_SR_COUNT + 10\n        sr_columns = {}\n        for k, v in self._t.iteritems():\n            if not k.startswith(self.SR_PREFIX):\n                continue\n\n            sr_columns[k] = v\n\n            remaining -= 1\n            if remaining <= 0:\n                break\n        return sr_columns\n\n    @property\n    def kind(self):\n        return self._id.split('/')[3]\n\n    @property\n    def sr_props(self):\n        return self.columns_to_sr_props(self.sr_columns)\n\n    @property\n    def path(self):\n        if isinstance(self.owner, Account):\n            return '/user/%(username)s/%(kind)s/%(multiname)s' % {\n                'username': self.owner.name,\n                'kind': self.kind,\n                'multiname': self.name,\n            }\n        if isinstance(self.owner, Subreddit):\n            return '/r/%(srname)s/%(kind)s/%(multiname)s' % {\n                'srname': self.owner.name,\n                'kind': self.kind,\n                'multiname': self.name,\n            }\n\n    @property\n    def user_path(self):\n        if self.owner == c.user:\n            return '/me/%s/%s' % (self.kind, self.name)\n        else:\n            return self.path\n\n    @property\n    def name(self):\n        return self._id.split('/')[-1]\n\n    @property\n    def analytics_name(self):\n        # classify as \"multi\" (as for unnamed multis) until our traffic system\n        # is smarter\n        return 'multi'\n\n    @property\n    def allows_referrers(self):\n        if not self.is_public():\n            return False\n        return super(LabeledMulti, self).allows_referrers\n\n    @property\n    def title(self):\n        if isinstance(self.owner, Account):\n            return _('%s subreddits curated by /u/%s') % (self.name, self.owner.name)\n        return _('%s subreddits') % self.name\n\n    def is_public(self):\n        return self.visibility == \"public\"\n\n    def is_hidden(self):\n        return self.visibility == \"hidden\"\n\n    def can_view(self, user):\n        if c.user_is_admin:\n            return True\n\n        if self.is_public():\n            return True\n\n        if isinstance(user, FakeAccount):\n            return False\n\n        # subreddit multireddit (mod can view)\n        if isinstance(self.owner, Subreddit):\n            return self.owner.is_moderator_with_perms(user, 'config')\n\n        return user == self.owner\n\n    def can_edit(self, user):\n        if isinstance(user, FakeAccount):\n            return False\n\n        # subreddit multireddit (admin can edit)\n        if isinstance(self.owner, Subreddit):\n            return (c.user_is_admin or\n                    self.owner.is_moderator_with_perms(user, 'config'))\n\n        if c.user_is_admin and self.owner == Account.system_user():\n            return True\n\n        return user == self.owner\n\n    @property\n    def icon_url(self):\n        from r2.lib.template_helpers import static\n        if self.icon_id:\n            path = \"multi_icons/{}.png\".format(self.icon_id.replace(\" \", \"_\"))\n            return static(path)\n        else:\n            return None\n\n    def set_icon_by_name(self, name):\n        \"\"\"Set this multi's icon information by icon name\n\n        Note: tdb_cassandra.Thing doesn't support property.setter properly;\n        it appears to write through directly to self._t['icon_name'].\n\n        \"\"\"\n        if not name:\n            self.icon_id = ''\n        elif name in g.multi_icons:\n            self.icon_id = name\n        else:\n            raise ValueError(\"invalid multi icon name\")\n\n    @classmethod\n    def by_owner(cls, owner, kinds=None, load_subreddits=True):\n        try:\n            multi_ids = LabeledMultiByOwner._byID(owner._fullname)._t.keys()\n        except tdb_cassandra.NotFound:\n            return []\n\n        kinds = ('m',) if not kinds else kinds\n        multis = cls._byID(\n            multi_ids, return_dict=False, load_subreddits=load_subreddits)\n        return [multi for multi in multis if multi.kind in kinds]\n\n    @classmethod\n    def create(cls, path, owner):\n        obj = cls(_id=path, owner_fullname=owner._fullname)\n        obj._commit()\n        obj._owner = owner\n        obj._srs_loaded = False\n        return obj\n\n    @classmethod\n    def copy(cls, path, multi, owner, symlink=False):\n        if symlink:\n            # remove all the sr_ids from the properties\n            props = {k: v for k, v in multi._t.iteritems()\n                     if k not in multi.sr_columns.keys()}\n            props[\"is_symlink\"] = True\n        else:\n            props = multi._t\n\n        obj = cls(_id=path, **props)\n        obj._srs = multi._srs\n        obj._srs_loaded = multi._srs_loaded\n        obj.owner_fullname = owner._fullname\n        obj.copied_from = multi.path.lower()\n        obj._commit()\n        obj._linked_multi = multi if symlink else None\n        obj._owner = owner\n\n        return obj\n\n    @classmethod\n    def slugify(cls, owner, display_name, type_=\"m\"):\n        \"\"\"Generate user multi path from display name.\"\"\"\n        slug = unicode_title_to_ascii(display_name)\n        if isinstance(owner, Subreddit):\n            prefix = \"/r/\" + owner.name + \"/\" + type_ + \"/\"\n        else:\n            prefix = \"/user/\" + owner.name + \"/\" + type_ + \"/\"\n        new_path = prefix + slug\n        try:\n            existing = LabeledMultiByOwner._byID(owner._fullname)._t.keys()\n        except tdb_cassandra.NotFound:\n            existing = []\n        count = 0\n        while new_path in existing:\n            count += 1\n            new_path = prefix + slug + str(count)\n        return new_path\n\n    @classmethod\n    def sr_props_to_columns(cls, sr_props):\n        columns = {}\n        sr_ids = []\n        for sr_id, props in sr_props.iteritems():\n            if isinstance(sr_id, BaseSite):\n                sr_id = sr_id._id\n            sr_ids.append(sr_id)\n            columns[cls.SR_PREFIX + str(sr_id)] = json.dumps(props)\n        return sr_ids, columns\n\n    @classmethod\n    def columns_to_sr_props(cls, columns):\n        ret = {}\n        for s, sr_prop_dump in columns.iteritems():\n            sr_id = long(s.strip(cls.SR_PREFIX))\n            sr_props = json.loads(sr_prop_dump)\n            ret[sr_id] = sr_props\n        return ret\n\n    def _on_create(self):\n        for view in self._views:\n            view.add_object(self)\n\n    def unlink(self):\n        if not self.is_symlink:\n            return\n\n        self._srs = self.srs\n        sr_props = dict.fromkeys(self.srs, {})\n        sr_ids, sr_columns = self.sr_props_to_columns(sr_props)\n        for attr, val in sr_columns.iteritems():\n            self.__setattr__(attr, val)\n\n        self.is_symlink = False\n\n    def add_srs(self, sr_props):\n        \"\"\"Add/overwrite subreddit(s).\"\"\"\n        if self.is_symlink:\n            self.unlink()\n        sr_ids, sr_columns = self.sr_props_to_columns(sr_props)\n\n        if len(set(sr_columns) | set(self.sr_columns)) > self.MAX_SR_COUNT:\n            raise TooManySubredditsError\n\n        new_sr_ids = set(sr_ids) - set(self.sr_ids)\n        new_srs = Subreddit._byID(\n            new_sr_ids, data=True, return_dict=False, stale=True)\n        self._srs.extend(new_srs)\n\n        for attr, val in sr_columns.iteritems():\n            self.__setattr__(attr, val)\n\n    def del_srs(self, sr_ids):\n        \"\"\"Delete subreddit(s).\"\"\"\n        if self.is_symlink:\n            self.unlink()\n\n        sr_props = dict.fromkeys(tup(sr_ids), {})\n        sr_ids, sr_columns = self.sr_props_to_columns(sr_props)\n\n        for key in sr_columns.iterkeys():\n            self.__delitem__(key)\n\n        self._srs = [sr for sr in self._srs if sr._id not in sr_ids]\n\n    def clear_srs(self):\n        self.del_srs(self.sr_ids)\n\n    def delete(self):\n        # Do we want to actually delete objects?\n        self._destroy()\n        for view in self._views:\n            rowkey = view._rowkey(self)\n            column = view._obj_to_column(self)\n            view._remove(rowkey, column)\n\n\n@tdb_cassandra.view_of(LabeledMulti)\nclass LabeledMultiByOwner(tdb_cassandra.View):\n    _use_db = True\n\n    @classmethod\n    def _rowkey(cls, lm):\n        return lm.owner_fullname\n\n\nclass RandomReddit(FakeSubreddit):\n    name = 'random'\n    header = \"\"\n\nclass RandomNSFWReddit(FakeSubreddit):\n    name = 'randnsfw'\n    header = \"\"\n\nclass RandomSubscriptionReddit(FakeSubreddit):\n    name = 'myrandom'\n    header = \"\"\n\nclass ModContribSR(MultiReddit):\n    name  = None\n    title = None\n    query_param = None\n    _defaults = dict(\n        MultiReddit._defaults,\n        login_required=True,\n    )\n\n    def __init__(self):\n        # Can't lookup srs right now, c.user not set\n        MultiReddit.__init__(self)\n\n    @property\n    def sr_ids(self):\n        if c.user_is_loggedin:\n            return Subreddit.special_reddits(c.user, self.query_param)\n        else:\n            return []\n\n    @property\n    def srs(self):\n        return Subreddit._byID(self.sr_ids, data=True, return_dict=False)\n\n    @property\n    def allows_referrers(self):\n        return False\n\n\nclass ModSR(ModContribSR):\n    name  = \"subreddits you moderate\"\n    title = \"subreddits you moderate\"\n    query_param = \"moderator\"\n    path = \"/r/mod\"\n\n    def is_moderator(self, user):\n        return FakeSRMember(ModeratorPermissionSet)\n\n\nclass ModMinus(ModSR):\n    analytics_name = \"mod\"\n\n    def __init__(self, exclude_srs):\n        ModSR.__init__(self)\n        self.exclude_srs = exclude_srs\n        self.exclude_sr_ids = [sr._id for sr in exclude_srs]\n\n    @property\n    def sr_ids(self):\n        sr_ids = super(ModMinus, self).sr_ids\n        return [sr_id for sr_id in sr_ids if not sr_id in self.exclude_sr_ids]\n\n    @property\n    def name(self):\n        exclude_text = ', '.join(sr.name for sr in self.exclude_srs)\n        return 'subreddits you moderate except ' + exclude_text\n\n    @property\n    def title(self):\n        return self.name\n\n    @property\n    def path(self):\n        return '/r/mod-' + '-'.join(sr.name for sr in self.exclude_srs)\n\n\nclass ModFiltered(Filtered, ModMinus):\n    unfiltered_path = '/r/mod'\n    filtername = 'mod'\n\n    def __init__(self):\n        ModMinus.__init__(self, self._get_filtered_subreddits())\n\n\nclass ContribSR(ModContribSR):\n    name  = \"contrib\"\n    title = \"communities you're approved on\"\n    query_param = \"contributor\"\n    path = \"/r/contrib\"\n\n\nclass DomainSR(FakeSubreddit):\n    @property\n    def path(self):\n        return '/domain/' + self.domain\n\n    def __init__(self, domain):\n        FakeSubreddit.__init__(self)\n        domain = domain.lower()\n        self.domain = domain\n        self.name = domain\n        self.title = _(\"%(domain)s on %(reddit.com)s\") % {\n            \"domain\": domain, \"reddit.com\": g.domain}\n        try:\n            idn = domain.decode('idna')\n            if idn != domain:\n                self.idn = idn\n        except UnicodeError:\n            # If we were given a bad domain name (e.g. xn--.com) we'll get an\n            # error here. These domains are invalid to register so it should\n            # be fine to ignore the error.\n            pass\n\n    def get_links(self, sort, time):\n        from r2.lib.db import queries\n        return queries.get_domain_links(self.domain, sort, time)\n\n    @property\n    def allow_gilding(self):\n        return False\n\n\nclass SearchResultSubreddit(Subreddit):\n    _nodb = True\n\n    @classmethod\n    def add_props(cls, user, wrapped):\n        from r2.controllers.reddit_base import UnloggedUser\n        Subreddit.add_props(user, wrapped)\n        for item in wrapped:\n            url = UrlParser(item.path)\n            url.update_query(ref=\"search_subreddits\")\n            item.search_path = url.unparse()\n            can_view = item.can_view(user)\n            if isinstance(user, UnloggedUser):\n                can_comment = item.type == \"public\"\n            else:\n                can_comment = item.can_comment(user)\n            if not can_view:\n                item.display_type = \"private\"\n            elif item.type == \"archived\":\n                item.display_type = \"archived\"\n            elif not can_comment:\n                item.display_type = \"restricted\"\n            else:\n                item.display_type = \"public\"\n        Printable.add_props(user, wrapped)\n\nFrontpage = DefaultSR()\nFriends = FriendsSR()\nMod = ModSR()\nContrib = ContribSR()\nAll = AllSR()\nRandom = RandomReddit()\nRandomNSFW = RandomNSFWReddit()\nRandomSubscription = RandomSubscriptionReddit()\n\n# add to _specials so they can be retrieved with Subreddit._by_name, e.g.\n# Subreddit._by_name(\"all\")\nSubreddit._specials.update({\n    sr.name: sr for sr in (\n        Friends,\n        RandomNSFW,\n        RandomSubscription,\n        Random,\n        Contrib,\n        All,\n        Frontpage,\n    )\n})\n\n# some subreddits have unfortunate names\nSubreddit._specials['mod'] = Mod\n\n\nSubredditUserRelations = collections.namedtuple(\n    \"SubredditUserRelations\",\n    [\"subscriber\", \"moderator\", \"contributor\", \"banned\", \"muted\"],\n)\n\n\nclass SRMember(Relation(Subreddit, Account)):\n    _defaults = dict(encoded_permissions=None)\n    _permission_class = None\n    _cache = g.srmembercache\n    _rel_cache = g.srmembercache\n\n    @classmethod\n    def _cache_prefix(cls):\n        return \"srmember:\"\n\n    @classmethod\n    def _rel_cache_prefix(cls):\n        return \"srmemberrel:\"\n\n    def has_permission(self, perm):\n        \"\"\"Returns whether this member has explicitly been granted a permission.\n        \"\"\"\n        return self.get_permissions().get(perm, False)\n\n    def get_permissions(self):\n        \"\"\"Returns permission set for this member (or None if N/A).\"\"\"\n        if not self._permission_class:\n            raise NotImplementedError\n        return self._permission_class.loads(self.encoded_permissions)\n\n    def update_permissions(self, **kwargs):\n        \"\"\"Grants or denies permissions to this member.\n\n        Args are named parameters with bool or None values (use None to disable\n        granting or denying the permission). After calling this method,\n        the relation will be _dirty until _commit is called.\n        \"\"\"\n        if not self._permission_class:\n            raise NotImplementedError\n        perm_set = self._permission_class.loads(self.encoded_permissions)\n        if perm_set is None:\n            perm_set = self._permission_class()\n        for k, v in kwargs.iteritems():\n            if v is None:\n                if k in perm_set:\n                    del perm_set[k]\n            else:\n                perm_set[k] = v\n        self.encoded_permissions = perm_set.dumps()\n\n    def set_permissions(self, perm_set):\n        \"\"\"Assigns a permission set to this relation.\"\"\"\n        self.encoded_permissions = perm_set.dumps()\n\n    def is_superuser(self):\n        return self.get_permissions().is_superuser()\n\n\nclass FakeSRMember:\n    \"\"\"All-permission granting stub for SRMember, used by FakeSubreddits.\"\"\"\n    def __init__(self, permission_class):\n        self.permission_class = permission_class\n\n    def has_permission(self, perm):\n        return True\n\n    def get_permissions(self):\n        return self.permission_class(all=True)\n\n    def is_superuser(self):\n        return True\n\n\nSubreddit.__bases__ += (\n    UserRel('moderator', SRMember,\n            permission_class=ModeratorPermissionSet),\n    UserRel('moderator_invite', SRMember,\n            permission_class=ModeratorPermissionSet),\n    UserRel('contributor', SRMember, disable_ids_fn=True),\n    UserRel('banned', SRMember, disable_ids_fn=True),\n    UserRel('muted', SRMember, disable_ids_fn=True),\n    UserRel('wikibanned', SRMember),\n    UserRel('wikicontributor', SRMember),\n)\n\n\ndef add_legacy_subscriber(srs, user):\n    srs = tup(srs)\n    for sr in srs:\n        rel = SRMember(sr, user, \"subscriber\")\n        try:\n            rel._commit()\n        except CreationError:\n            break\n\n\ndef remove_legacy_subscriber(sr, user):\n    rels = SRMember._fast_query([sr], [user], \"subscriber\")\n    rel = rels.get((sr, user, \"subscriber\"))\n    if rel:\n        rel._delete()\n\n\nclass SubredditTempBan(object):\n    def __init__(self, sr, kind, victim, banner, duration):\n        self.sr = sr._id36\n        self._srname = sr.name\n        self.who = victim._id36\n        self._whoname = victim.name\n        self.type = kind\n        self.banner = banner._id36\n        self.duration = duration\n\n    @classmethod\n    def schedule(cls, sr, kind, victim, banner, duration):\n        info = {\n            'sr': sr._id36,\n            'who': victim._id36,\n            'type': kind,\n            'banner': banner._id36,\n        }\n        result = TryLaterBySubject.schedule(\n            cls.cancel_rowkey(sr.name, kind),\n            cls.cancel_colkey(victim.name),\n            json.dumps(info),\n            duration,\n            trylater_rowkey=cls.schedule_rowkey(),\n        )\n        return {victim.name: result.keys()[0]}\n\n    @classmethod\n    def cancel_colkey(cls, name):\n        return name\n\n    @classmethod\n    def cancel_rowkey(cls, name, type):\n        return \"srunban:%s:%s\" % (name, type)\n\n    @classmethod\n    def schedule_rowkey(cls):\n        return \"srunban\"\n\n    @classmethod\n    def search(cls, srname, bantype, subjects):\n        results = TryLaterBySubject.search(cls.cancel_rowkey(srname, bantype),\n                                           subjects)\n\n        def convert_uuid_to_datetime(uu):\n            return datetime.datetime.fromtimestamp(convert_uuid_to_time(uu),\n                                                   g.tz)\n        return {\n            name: convert_uuid_to_datetime(uu)\n                for name, uu in results.iteritems()\n        }\n\n    @classmethod\n    def unschedule(cls, srname, victim_name, bantype):\n        TryLaterBySubject.unschedule(\n            cls.cancel_rowkey(srname, bantype),\n            cls.cancel_colkey(victim_name),\n            cls.schedule_rowkey(),\n        )\n\n\n@trylater_hooks.on('trylater.srunban')\ndef on_subreddit_unban(data):\n    from r2.models.modaction import ModAction\n    for blob in data.itervalues():\n        baninfo = json.loads(blob)\n        container = Subreddit._byID36(baninfo['sr'], data=True)\n        victim = Account._byID36(baninfo['who'], data=True)\n        banner = Account._byID36(baninfo['banner'], data=True)\n        kind = baninfo['type']\n        remove_function = getattr(container, 'remove_' + kind)\n        new = remove_function(victim)\n        g.log.info(\"Unbanned %s from %s\", victim.name, container.name)\n\n        if new:\n            action = dict(\n                banned='unbanuser',\n                wikibanned='wikiunbanned',\n            ).get(kind, None)\n            ModAction.create(container, banner, action, target=victim,\n                             description=\"was temporary\")\n\n\nclass MutedAccountsBySubreddit(object):\n    @classmethod\n    def mute(cls, sr, user, muter, parent_message=None):\n        NUM_HOURS = 72\n\n        from r2.lib.db import queries\n        from r2.models import Message, ModAction\n        info = {\n            'sr': sr._id36,\n            'who': user._id36,\n            'muter': muter._id36,\n        }\n\n        result = TryLaterBySubject.schedule(\n            cls.cancel_rowkey(sr),\n            cls.cancel_colkey(user),\n            json.dumps(info),\n            datetime.timedelta(hours=NUM_HOURS),\n            trylater_rowkey=cls.schedule_rowkey(),\n        )\n\n        #if the user has interacted with the subreddit before, message them\n        if user.has_interacted_with(sr):\n            subject = \"You have been muted from r/%(subredditname)s\"\n            subject %= dict(subredditname=sr.name)\n            message = (\"You have been [temporarily muted](%(muting_link)s) \"\n                \"from r/%(subredditname)s. You will not be able to message \"\n                \"the moderators of r/%(subredditname)s for %(num_hours)s hours.\")\n            message %= dict(\n                muting_link=\"https://reddit.zendesk.com/hc/en-us/articles/205269739\",\n                subredditname=sr.name,\n                num_hours=NUM_HOURS,\n            )\n            if parent_message:\n                subject = parent_message.subject\n                re = \"re: \"\n                if not subject.startswith(re):\n                    subject = re + subject\n\n            item, inbox_rel = Message._new(muter, user, subject, message,\n                request.ip, parent=parent_message, sr=sr, from_sr=True)\n            queries.new_message(item, inbox_rel, update_modmail=True)\n\n        return {user.name: result.keys()[0]}\n\n    @classmethod\n    def cancel_colkey(cls, user):\n        return user.name\n\n    @classmethod\n    def cancel_rowkey(cls, subreddit):\n        return \"srmute:%s\" % subreddit.name\n\n    @classmethod\n    def schedule_rowkey(cls):\n        return \"srmute\"\n\n    @classmethod\n    def search(cls, subreddit, subjects):\n        results = TryLaterBySubject.search(cls.cancel_rowkey(subreddit),\n                                           subjects)\n\n        return {\n            name: datetime.datetime.fromtimestamp(convert_uuid_to_time(uu),\n                    g.tz)\n                for name, uu in results.iteritems()\n        }\n\n    @classmethod\n    def unmute(cls, sr, user, automatic=False):\n        from r2.models import ModAction\n\n        TryLaterBySubject.unschedule(\n            cls.cancel_rowkey(sr),\n            cls.cancel_colkey(user),\n            cls.schedule_rowkey(),\n        )\n\n        if automatic:\n            unmuter = Account.system_user()\n            ModAction.create(sr, unmuter, 'unmuteuser', target=user)\n\n\n@trylater_hooks.on('trylater.srmute')\ndef unmute_hook(data):\n    for blob in data.itervalues():\n        muteinfo = json.loads(blob)\n        subreddit = Subreddit._byID36(muteinfo['sr'], data=True)\n        user = Account._byID36(muteinfo['who'], data=True)\n\n        subreddit.remove_muted(user)\n        MutedAccountsBySubreddit.unmute(subreddit, user, automatic=True)\n\n\nclass SubredditsActiveForFrontPage(tdb_cassandra.View):\n    \"\"\"Tracks which subreddits currently have valid frontpage posts.\n\n    The front page's \"hot\" page only includes posts that are newer than\n    g.HOT_PAGE_AGE, so there's no point including subreddits in it if they\n    haven't had a post inside that period. Since we pick random subsets of\n    users' subscriptions when they subscribe to more subreddits than we\n    build the page from, this means that inactive subreddits can effectively\n    \"waste\" some of these slots, since they may not have any posts that can\n    possibly be added to the page.\n\n    This CF will get an entry inserted for each subreddit whenever a new\n    post is made in that subreddit, with a TTL equal to g.HOT_PAGE_AGE. We\n    will then be able to query it to determine which subreddits don't have\n    any posts recent enough to contribute to the front page, and exclude\n    them from consideration for a user's front page set.\n    \"\"\"\n\n    _use_db = True\n    _connection_pool = \"main\"\n    _ttl = datetime.timedelta(days=g.HOT_PAGE_AGE)\n    _extra_schema_creation_args = {\n        \"key_validation_class\": ASCII_TYPE,\n    }\n    _read_consistency_level = tdb_cassandra.CL.ONE\n    _write_consistency_level = tdb_cassandra.CL.QUORUM\n\n    ROWKEY = \"1\"\n\n    @classmethod\n    def mark_new_post(cls, subreddit):\n        cls._set_values(cls.ROWKEY, {subreddit._id36: \"\"})\n\n    @classmethod\n    def filter_inactive_ids(cls, subreddit_ids):\n        sr_id36s = [to36(sr_id) for sr_id in subreddit_ids]\n        try:\n            results = cls._cf.get(cls.ROWKEY, columns=sr_id36s)\n        except tdb_cassandra.NotFoundException:\n            results = {}\n\n        num_filtered = len(subreddit_ids) - len(results)\n        g.stats.simple_event(\"frontpage.filter_inactive\", delta=num_filtered)\n\n        return [int(sr_id36, 36) for sr_id36 in results.keys()]\n"
  },
  {
    "path": "r2/r2/models/token.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport datetime\nimport functools\nfrom os import urandom\nfrom base64 import urlsafe_b64encode\n\nfrom pycassa.system_manager import ASCII_TYPE, DATE_TYPE, UTF8_TYPE\n\nfrom pylons import tmpl_context as c\nfrom pylons import app_globals as g\nfrom pylons.i18n import _\n\nfrom r2.lib import hooks\nfrom r2.lib.db import tdb_cassandra\nfrom r2.lib.db.thing import NotFound\nfrom r2.models.account import Account\n\ndef generate_token(size):\n    return urlsafe_b64encode(urandom(size)).rstrip(\"=\")\n\n\nclass Token(tdb_cassandra.Thing):\n    \"\"\"A unique randomly-generated token used for authentication.\"\"\"\n\n    _extra_schema_creation_args = dict(\n        key_validation_class=ASCII_TYPE,\n        default_validation_class=UTF8_TYPE,\n        column_validation_classes=dict(\n            date=DATE_TYPE,\n            used=ASCII_TYPE\n        )\n    )\n\n    @classmethod\n    def _new(cls, **kwargs):\n        if \"_id\" not in kwargs:\n            kwargs[\"_id\"] = cls._generate_unique_token()\n\n        token = cls(**kwargs)\n        token._commit()\n        return token\n\n    @classmethod\n    def _generate_unique_token(cls):\n        for i in range(3):\n            token = generate_token(cls.token_size)\n            try:\n                cls._byID(token)\n            except tdb_cassandra.NotFound:\n                return token\n            else:\n                continue\n        raise ValueError\n\n    @classmethod\n    def get_token(cls, _id):\n        if _id is None:\n            return None\n        try:\n            return cls._byID(_id)\n        except tdb_cassandra.NotFound:\n            return None\n\n\nclass ConsumableToken(Token):\n    _defaults = dict(used=False)\n    _bool_props = (\"used\",)\n    _warn_on_partial_ttl = False\n\n    @classmethod\n    def get_token(cls, _id):\n        token = super(ConsumableToken, cls).get_token(_id)\n        if token and not token.used:\n            return token\n        else:\n            return None\n\n    def consume(self):\n        self.used = True\n        self._commit()\n\n\nclass OAuth2Scope:\n    scope_info = {\n        None: {\n            \"id\": None,\n            \"name\": _(\"Any Scope\"),\n            \"description\": _(\"Endpoint is accessible with any combination \"\n                \"of other OAuth 2 scopes.\"),\n        },\n        \"account\": {\n            \"id\": \"account\",\n            \"name\": _(\"Update account information\"),\n            \"description\": _(\"Update preferences and related account \"\n                \"information. Will not have access to your email or \"\n                \"password.\"),\n        },\n        \"creddits\": {\n            \"id\": \"creddits\",\n            \"name\": _(\"Spend reddit gold creddits\"),\n            \"description\": _(\"Spend my reddit gold creddits on giving \"\n                \"gold to other users.\"),\n        },\n        \"edit\": {\n            \"id\": \"edit\",\n            \"name\": _(\"Edit Posts\"),\n            \"description\": _(\"Edit and delete my comments and submissions.\"),\n        },\n        \"flair\": {\n            \"id\": \"flair\",\n            \"name\": _(\"Manage My Flair\"),\n            \"description\": _(\"Select my subreddit flair. \"\n                             \"Change link flair on my submissions.\"),\n        },\n        \"history\": {\n            \"id\": \"history\",\n            \"name\": _(\"History\"),\n            \"description\": _(\n                \"Access my voting history and comments or submissions I've\"\n                \" saved or hidden.\"),\n        },\n        \"identity\": {\n            \"id\": \"identity\",\n            \"name\": _(\"My Identity\"),\n            \"description\": _(\"Access my reddit username and signup date.\"),\n        },\n        \"modcontributors\": {\n            \"id\": \"modcontributors\",\n            \"name\": _(\"Approve submitters and ban users\"),\n            \"description\": _(\n                \"Add/remove users to approved submitter lists and \"\n                \"ban/unban or mute/unmute users from subreddits I moderate.\"\n            ),\n        },\n        \"modflair\": {\n            \"id\": \"modflair\",\n            \"name\": _(\"Moderate Flair\"),\n            \"description\": _(\n                \"Manage and assign flair in subreddits I moderate.\"),\n        },\n        \"modposts\": {\n            \"id\": \"modposts\",\n            \"name\": _(\"Moderate Posts\"),\n            \"description\": _(\n                \"Approve, remove, mark nsfw, and distinguish content\"\n                \" in subreddits I moderate.\"),\n        },\n        \"modconfig\": {\n            \"id\": \"modconfig\",\n            \"name\": _(\"Moderate Subreddit Configuration\"),\n            \"description\": _(\n                \"Manage the configuration, sidebar, and CSS\"\n                \" of subreddits I moderate.\"),\n        },\n        \"modlog\": {\n            \"id\": \"modlog\",\n            \"name\": _(\"Moderation Log\"),\n            \"description\": _(\n                \"Access the moderation log in subreddits I moderate.\"),\n        },\n        \"modothers\": {\n            \"id\": \"modothers\",\n            \"name\": _(\"Invite or remove other moderators\"),\n            \"description\": _(\n                \"Invite or remove other moderators from subreddits I moderate.\"\n            ),\n        },\n        \"modself\": {\n            \"id\": \"modself\",\n            \"name\": _(\"Make changes to your subreddit moderator \"\n                      \"and contributor status\"),\n            \"description\": _(\n                \"Accept invitations to moderate a subreddit. Remove myself as \"\n                \"a moderator or contributor of subreddits I moderate or \"\n                \"contribute to.\"\n            ),\n        },\n        \"modtraffic\": {\n            \"id\": \"modtraffic\",\n            \"name\": _(\"Subreddit Traffic\"),\n            \"description\": _(\"Access traffic stats in subreddits I moderate.\"),\n        },\n        \"modwiki\": {\n            \"id\": \"modwiki\",\n            \"name\": _(\"Moderate Wiki\"),\n            \"description\": _(\n                \"Change editors and visibility of wiki pages\"\n                \" in subreddits I moderate.\"),\n        },\n        \"mysubreddits\": {\n            \"id\": \"mysubreddits\",\n            \"name\": _(\"My Subreddits\"),\n            \"description\": _(\n                \"Access the list of subreddits I moderate, contribute to,\"\n                \" and subscribe to.\"),\n        },\n        \"privatemessages\": {\n            \"id\": \"privatemessages\",\n            \"name\": _(\"Private Messages\"),\n            \"description\": _(\n                \"Access my inbox and send private messages to other users.\"),\n        },\n        \"read\": {\n            \"id\": \"read\",\n            \"name\": _(\"Read Content\"),\n            \"description\": _(\"Access posts and comments through my account.\"),\n        },\n        \"report\": {\n            \"id\": \"report\",\n            \"name\": _(\"Report content\"),\n            \"description\": _(\"Report content for rules violations. \"\n                             \"Hide & show individual submissions.\"),\n        },\n        \"save\": {\n            \"id\": \"save\",\n            \"name\": _(\"Save Content\"),\n            \"description\": _(\"Save and unsave comments and submissions.\"),\n        },\n        \"submit\": {\n            \"id\": \"submit\",\n            \"name\": _(\"Submit Content\"),\n            \"description\": _(\"Submit links and comments from my account.\"),\n        },\n        \"subscribe\": {\n            \"id\": \"subscribe\",\n            \"name\": _(\"Edit My Subscriptions\"),\n            \"description\": _('Manage my subreddit subscriptions. Manage '\n                '\"friends\" - users whose content I follow.'),\n        },\n        \"vote\": {\n            \"id\": \"vote\",\n            \"name\": _(\"Vote\"),\n            \"description\":\n                _(\"Submit and change my votes on comments and submissions.\"),\n        },\n        \"wikiedit\": {\n            \"id\": \"wiki\",\n            \"name\": _(\"Wiki Editing\"),\n            \"description\": _(\"Edit wiki pages on my behalf\"),\n        },\n        \"wikiread\": {\n            \"id\": \"wikiread\",\n            \"name\": _(\"Read Wiki Pages\"),\n            \"description\": _(\"Read wiki pages through my account\"),\n        },\n    }\n\n    # Special scope, granted implicitly to clients with app_type == \"script\"\n    FULL_ACCESS = \"*\"\n\n    class InsufficientScopeError(StandardError):\n        pass\n\n    def __init__(self, scope_str=None, subreddits=None, scopes=None):\n        if scope_str:\n            self._parse_scope_str(scope_str)\n        elif subreddits is not None or scopes is not None:\n            self.subreddit_only = bool(subreddits)\n            self.subreddits = subreddits\n            self.scopes = scopes\n        else:\n            self.subreddit_only = False\n            self.subreddits = set()\n            self.scopes = set()\n\n    def _parse_scope_str(self, scope_str):\n        srs, sep, scopes = scope_str.rpartition(':')\n        if sep:\n            self.subreddit_only = True\n            self.subreddits = set(srs.split('+'))\n        else:\n            self.subreddit_only = False\n            self.subreddits = set()\n        self.scopes = set(scopes.replace(',', ' ').split(' '))\n\n    def __str__(self):\n        if self.subreddit_only:\n            sr_part = '+'.join(sorted(self.subreddits)) + ':'\n        else:\n            sr_part = ''\n        return sr_part + ' '.join(sorted(self.scopes))\n\n    def has_access(self, subreddit, required_scopes):\n        if self.FULL_ACCESS in self.scopes:\n            return True\n        if self.subreddit_only and subreddit not in self.subreddits:\n            return False\n        return (self.scopes >= required_scopes)\n\n    def has_any_scope(self, required_scopes):\n        if self.FULL_ACCESS in self.scopes:\n            return True\n\n        return bool(self.scopes & required_scopes)\n\n    def is_valid(self):\n        return all(scope in self.scope_info for scope in self.scopes)\n\n    def details(self):\n        if self.FULL_ACCESS in self.scopes:\n            scopes = self.scope_info.keys()\n        else:\n            scopes = self.scopes\n        return [(scope, self.scope_info[scope]) for scope in scopes]\n\n    @classmethod\n    def merge_scopes(cls, scopes):\n        \"\"\"Return a by-subreddit dict representing merged OAuth2Scopes.\n\n        Takes an iterable of OAuth2Scopes. For each of those,\n        if it defines scopes on multiple subreddits, it is split\n        into one OAuth2Scope per subreddit. If multiple passed in\n        OAuth2Scopes reference the same scopes, they'll be combined.\n\n        \"\"\"\n        merged = {}\n        for scope in scopes:\n            srs = scope.subreddits if scope.subreddit_only else (None,)\n            for sr in srs:\n                if sr in merged:\n                    merged[sr].scopes.update(scope.scopes)\n                else:\n                    new_scope = cls()\n                    new_scope.subreddits = {sr}\n                    new_scope.scopes = scope.scopes\n                    if sr is not None:\n                        new_scope.subreddit_only = True\n                    merged[sr] = new_scope\n        return merged\n\n\ndef extra_oauth2_scope(*scopes):\n    \"\"\"Wrap a function so that it only returns data if user has all `scopes`\n\n    When not in an OAuth2 context, function returns normally.\n    In an OAuth2 context, the function will not be run unless the user\n    has granted all scopes required of this function. Instead, the function\n    will raise an OAuth2Scope.InsufficientScopeError.\n\n    \"\"\"\n    def extra_oauth2_wrapper(fn):\n        @functools.wraps(fn)\n        def wrapper_fn(*a, **kw):\n            if not c.oauth_user:\n                # Not in an OAuth2 context, run function normally\n                return fn(*a, **kw)\n            elif c.oauth_scope.has_access(c.site.name, set(scopes)):\n                # In an OAuth2 context, and have scope for this function\n                return fn(*a, **kw)\n            else:\n                # In an OAuth2 context, but don't have scope\n                raise OAuth2Scope.InsufficientScopeError(scopes)\n        return wrapper_fn\n    return extra_oauth2_wrapper\n\n\nclass OAuth2Client(Token):\n    \"\"\"A client registered for OAuth2 access\"\"\"\n    max_developers = 20\n    token_size = 10\n    client_secret_size = 20\n    _bool_props = (\n        \"deleted\",\n    )\n    _float_props = (\n        \"max_reqs_sec\",\n    )\n    _defaults = dict(name=\"\",\n                     deleted=False,\n                     description=\"\",\n                     about_url=\"\",\n                     icon_url=\"\",\n                     secret=\"\",\n                     redirect_uri=\"\",\n                     app_type=\"web\",\n                     max_reqs_sec=g.RL_OAUTH_AVG_REQ_PER_SEC,\n                    )\n    _use_db = True\n    _connection_pool = \"main\"\n\n    _developer_colname_prefix = 'has_developer_'\n\n    APP_TYPES = (\"web\", \"installed\", \"script\")\n    PUBLIC_APP_TYPES = (\"installed\",)\n\n    @classmethod\n    def _new(cls, **kwargs):\n        if \"secret\" not in kwargs:\n            kwargs[\"secret\"] = generate_token(cls.client_secret_size)\n        return super(OAuth2Client, cls)._new(**kwargs)\n\n    @property\n    def _developer_ids(self):\n        for k, v in self._t.iteritems():\n            if k.startswith(self._developer_colname_prefix) and v:\n                try:\n                    yield int(k[len(self._developer_colname_prefix):], 36)\n                except ValueError:\n                    pass\n\n    @property\n    def _max_reqs(self):\n        return self.max_reqs_sec * g.RL_OAUTH_RESET_SECONDS\n\n    @property\n    def _developers(self):\n        \"\"\"Returns a list of users who are developers of this client.\"\"\"\n\n        devs = Account._byID(list(self._developer_ids), return_dict=False)\n        return [dev for dev in devs if not dev._deleted]\n\n    def _developer_colname(self, account):\n        \"\"\"Developer access is granted by way of adding a column with the\n        account's ID36 to the client object.  This function returns the\n        column name for a given Account.\n        \"\"\"\n\n        return ''.join((self._developer_colname_prefix, account._id36))\n\n    def has_developer(self, account):\n        \"\"\"Returns a boolean indicating whether or not the supplied Account is a developer of this application.\"\"\"\n\n        if account._deleted:\n            return False\n        else:\n            return getattr(self, self._developer_colname(account), False)\n\n    def add_developer(self, account, force=False):\n        \"\"\"Grants developer access to the supplied Account.\"\"\"\n\n        dev_ids = set(self._developer_ids)\n        if account._id not in dev_ids:\n            if not force and len(dev_ids) >= self.max_developers:\n                raise OverflowError('max developers reached')\n            setattr(self, self._developer_colname(account), True)\n            self._commit()\n\n        # Also update index\n        OAuth2ClientsByDeveloper._set_values(account._id36, {self._id: ''})\n\n    def remove_developer(self, account):\n        \"\"\"Revokes the supplied Account's developer access.\"\"\"\n\n        if hasattr(self, self._developer_colname(account)):\n            del self[self._developer_colname(account)]\n            if not len(self._developers):\n                # No developers remain, delete the client\n                self.deleted = True\n            self._commit()\n\n        # Also update index\n        try:\n            cba = OAuth2ClientsByDeveloper._byID(account._id36)\n            del cba[self._id]\n        except (tdb_cassandra.NotFound, KeyError):\n            pass\n        else:\n            cba._commit()\n\n    @classmethod\n    def _by_developer(cls, account):\n        \"\"\"Returns a (possibly empty) list of clients for which Account is a developer.\"\"\"\n\n        if account._deleted:\n            return []\n\n        try:\n            cba = OAuth2ClientsByDeveloper._byID(account._id36)\n        except tdb_cassandra.NotFound:\n            return []\n\n        clients = cls._byID(cba._values().keys())\n        return [client for client in clients.itervalues()\n                if not client.deleted and client.has_developer(account)]\n\n    @classmethod\n    def _by_user(cls, account):\n        \"\"\"Returns a (possibly empty) list of client-scope-expiration triples for which Account has outstanding access tokens.\"\"\"\n\n        refresh_tokens = {\n            token._id: token for token in OAuth2RefreshToken._by_user(account)\n            if token.check_valid()}\n        access_tokens = [token for token in OAuth2AccessToken._by_user(account)\n                         if token.check_valid()]\n\n        tokens = refresh_tokens.values()\n        tokens.extend(token for token in access_tokens\n                      if token.refresh_token not in refresh_tokens)\n\n        clients = cls._byID([token.client_id for token in tokens])\n        return [(clients[token.client_id], OAuth2Scope(token.scope),\n                 token.date + datetime.timedelta(seconds=token._ttl)\n                     if token._ttl else None)\n                for token in tokens]\n\n    @classmethod\n    def _by_user_grouped(cls, account):\n        token_tuples = cls._by_user(account)\n        clients = {}\n        for client, scope, expiration in token_tuples:\n            if client._id in clients:\n                client_data = clients[client._id]\n                client_data['scopes'].append(scope)\n            else:\n                client_data = {'scopes': [scope], 'access_tokens': 0,\n                               'refresh_tokens': 0, 'client': client}\n                clients[client._id] = client_data\n            if expiration:\n                client_data['access_tokens'] += 1\n            else:\n                client_data['refresh_tokens'] += 1\n\n        for client_data in clients.itervalues():\n            client_data['scopes'] = OAuth2Scope.merge_scopes(client_data['scopes'])\n\n        return clients\n\n    def revoke(self, account):\n        \"\"\"Revoke all of the outstanding OAuth2AccessTokens associated with this client and user Account.\"\"\"\n\n        for token in OAuth2RefreshToken._by_user(account):\n            if token.client_id == self._id:\n                token.revoke()\n        for token in OAuth2AccessToken._by_user(account):\n            if token.client_id == self._id:\n                token.revoke()\n\n    def is_confidential(self):\n        return self.app_type not in self.PUBLIC_APP_TYPES\n\n    def is_first_party(self):\n        return self.has_developer(Account.system_user())\n\n\nclass OAuth2ClientsByDeveloper(tdb_cassandra.View):\n    \"\"\"Index providing access to the list of OAuth2Clients of which an Account is a developer.\"\"\"\n\n    _use_db = True\n    _type_prefix = 'OAuth2ClientsByDeveloper'\n    _view_of = OAuth2Client\n    _connection_pool = 'main'\n\n\nclass OAuth2AuthorizationCode(ConsumableToken):\n    \"\"\"An OAuth2 authorization code for completing authorization flow\"\"\"\n    token_size = 20\n    _ttl = datetime.timedelta(minutes=10)\n    _defaults = dict(ConsumableToken._defaults.items() + [\n                         (\"client_id\", \"\"),\n                         (\"redirect_uri\", \"\"),\n                         (\"scope\", \"\"),\n                         (\"refreshable\", False)])\n    _bool_props = ConsumableToken._bool_props + (\"refreshable\",)\n    _warn_on_partial_ttl = False\n    _use_db = True\n    _connection_pool = \"main\"\n\n    @classmethod\n    def _new(cls, client_id, redirect_uri, user_id, scope, refreshable):\n        return super(OAuth2AuthorizationCode, cls)._new(\n                client_id=client_id,\n                redirect_uri=redirect_uri,\n                user_id=user_id,\n                scope=str(scope),\n                refreshable=refreshable)\n\n    @classmethod\n    def use_token(cls, _id, client_id, redirect_uri):\n        token = cls.get_token(_id)\n        if token and (token.client_id == client_id and\n                      token.redirect_uri == redirect_uri):\n            token.consume()\n            return token\n        else:\n            return None\n\n\nclass OAuth2AccessToken(Token):\n    \"\"\"An OAuth2 access token for accessing protected resources\"\"\"\n    token_size = 20\n    _ttl = datetime.timedelta(minutes=60)\n    _defaults = dict(scope=\"\",\n                     token_type=\"bearer\",\n                     refresh_token=\"\",\n                     user_id=\"\",\n                    )\n    _use_db = True\n    _connection_pool = \"main\"\n\n    @classmethod\n    def _new(cls, client_id, user_id, scope, refresh_token=None, device_id=None):\n        try:\n            user_id_prefix = int(user_id, 36)\n        except (ValueError, TypeError):\n            user_id_prefix = \"\"\n        _id = \"%s-%s\" % (user_id_prefix, cls._generate_unique_token())\n        return super(OAuth2AccessToken, cls)._new(\n                     _id=_id,\n                     client_id=client_id,\n                     user_id=user_id,\n                     scope=str(scope),\n                     refresh_token=refresh_token,\n                     device_id=device_id,\n        )\n\n    @classmethod\n    def _by_user_view(cls):\n        return OAuth2AccessTokensByUser\n\n    def _on_create(self):\n        hooks.get_hook(\"oauth2.create_token\").call(token=self)\n\n        # update the by-user view\n        if self.user_id:\n            self._by_user_view()._set_values(str(self.user_id), {self._id: ''})\n\n        return super(OAuth2AccessToken, self)._on_create()\n\n    def check_valid(self):\n        \"\"\"Returns boolean indicating whether or not this access token is still valid.\"\"\"\n\n        # Has the token been revoked?\n        if getattr(self, 'revoked', False):\n            return False\n\n        # Is the OAuth2Client still valid?\n        try:\n            client = OAuth2Client._byID(self.client_id)\n            if client.deleted:\n                raise NotFound\n        except AttributeError:\n            g.log.error(\"bad token %s: %s\", self, self._t)\n            raise\n        except NotFound:\n            return False\n\n        # Is the user account still valid?\n        if self.user_id:\n            try:\n                account = Account._byID36(self.user_id)\n                if account._deleted:\n                    raise NotFound\n            except NotFound:\n                return False\n\n        return True\n\n    def revoke(self):\n        \"\"\"Revokes (invalidates) this access token.\"\"\"\n\n        self.revoked = True\n        self._commit()\n\n        if self.user_id:\n            try:\n                tba = self._by_user_view()._byID(self.user_id)\n                del tba[self._id]\n            except (tdb_cassandra.NotFound, KeyError):\n                # Not fatal, since self.check_valid() will still be False.\n                pass\n            else:\n                tba._commit()\n\n        hooks.get_hook(\"oauth2.revoke_token\").call(token=self)\n\n    @classmethod\n    def revoke_all_by_user(cls, account):\n        \"\"\"Revokes all access tokens for a given user Account.\"\"\"\n        tokens = cls._by_user(account)\n        for token in tokens:\n            token.revoke()\n\n    @classmethod\n    def _by_user(cls, account):\n        \"\"\"Returns a (possibly empty) list of valid access tokens for a given user Account.\"\"\"\n\n        try:\n            tba = cls._by_user_view()._byID(account._id36)\n        except tdb_cassandra.NotFound:\n            return []\n\n        tokens = cls._byID(tba._values().keys())\n        return [token for token in tokens.itervalues() if token.check_valid()]\n\nclass OAuth2AccessTokensByUser(tdb_cassandra.View):\n    \"\"\"Index listing the outstanding access tokens for an account.\"\"\"\n\n    _use_db = True\n    _ttl = OAuth2AccessToken._ttl\n    _type_prefix = 'OAuth2AccessTokensByUser'\n    _view_of = OAuth2AccessToken\n    _connection_pool = 'main'\n\n\nclass OAuth2RefreshToken(OAuth2AccessToken):\n    \"\"\"A refresh token for obtaining new access tokens for the same grant.\"\"\"\n\n    _type_prefix = None\n    _ttl = None\n\n    def _on_create(self):\n        if self.user_id:\n            self._by_user_view()._set_values(str(self.user_id), {self._id: ''})\n\n        # skip OAuth2AccessToken._on_create to avoid \"oauth2.create_token\" hook\n        return Token._on_create(self)\n\n    @classmethod\n    def _by_user_view(cls):\n        return OAuth2RefreshTokensByUser\n\n    def revoke(self):\n        super(OAuth2RefreshToken, self).revoke()\n        account = Account._byID36(self.user_id)\n        access_tokens = OAuth2AccessToken._by_user(account)\n        for token in access_tokens:\n            if token.refresh_token == self._id:\n                token.revoke()\n\nclass OAuth2RefreshTokensByUser(tdb_cassandra.View):\n    \"\"\"Index listing the outstanding refresh tokens for an account.\"\"\"\n\n    _use_db = True\n    _ttl = OAuth2RefreshToken._ttl\n    _type_prefix = 'OAuth2RefreshTokensByUser'\n    _view_of = OAuth2RefreshToken\n    _connection_pool = 'main'\n\n\nclass EmailVerificationToken(ConsumableToken):\n    _use_db = True\n    _connection_pool = \"main\"\n    _ttl = datetime.timedelta(hours=12)\n    token_size = 20\n\n    @classmethod\n    def _new(cls, user):\n        return super(EmailVerificationToken, cls)._new(user_id=user._fullname,\n                                                       email=user.email)\n\n    def valid_for_user(self, user):\n        return self.email == user.email\n\n\nclass PasswordResetToken(ConsumableToken):\n    _use_db = True\n    _connection_pool = \"main\"\n    _ttl = datetime.timedelta(hours=12)\n    token_size = 20\n\n    @classmethod\n    def _new(cls, user):\n        return super(PasswordResetToken, cls)._new(user_id=user._fullname,\n                                                   email_address=user.email,\n                                                   password=user.password)\n\n    def valid_for_user(self, user):\n        return (self.email_address == user.email and\n                self.password == user.password)\n\n\nclass AwardClaimToken(ConsumableToken):\n    token_size = 20\n    _ttl = datetime.timedelta(days=30)\n    _defaults = dict(ConsumableToken._defaults.items() + [\n                         (\"awardfullname\", \"\"),\n                         (\"description\", \"\"),\n                         (\"url\", \"\"),\n                         (\"uid\", \"\")])\n    _use_db = True\n    _connection_pool = \"main\"\n\n    @classmethod\n    def _new(cls, uid, award, description, url):\n        '''Create an AwardClaimToken with the given parameters\n\n        `uid` - A string that uniquely identifies the kind of\n                Trophy the user would be claiming.*\n        `award_codename` - The codename of the Award the user will claim\n        `description` - The description the Trophy will receive\n        `url` - The URL the Trophy will receive\n\n        *Note that this differs from Award codenames, because it may be\n        desirable to allow users to have multiple copies of the same Award,\n        but restrict another aspect of the Trophy. For example, users\n        are allowed to have multiple Translator awards, but should only get\n        one for each language, so the `unique_award_id`s for those would be\n        of the form \"i18n_%(language)s\"\n\n        '''\n        return super(AwardClaimToken, cls)._new(\n            awardfullname=award._fullname,\n            description=description or \"\",\n            url=url or \"\",\n            uid=uid,\n        )\n\n    def post_url(self):\n        # Relative URL; should be used on an on-site form\n        return \"/awards/claim/%s\" % self._id\n\n    def confirm_url(self):\n        # Full URL; for emailing, PM'ing, etc.\n        base = g.https_endpoint or g.origin\n        return \"%s/awards/confirm/%s\" % (base, self._id)\n"
  },
  {
    "path": "r2/r2/models/traffic.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\"\"\"\nThese models represent the traffic statistics stored for subreddits and\npromoted links.  They are written to by Pig-based MapReduce jobs and read from\nvarious places in the UI.\n\nAll traffic statistics are divided up into three \"intervals\" of granularity,\nhourly, daily, and monthly.  Individual hits are tracked as pageviews /\nimpressions, and can be safely summed.  Unique hits are tracked as well, but\ncannot be summed safely because there's no way to know overlap at this point in\nthe data pipeline.\n\n\"\"\"\n\nimport datetime\n\nfrom pylons import app_globals as g\nfrom sqlalchemy.ext.declarative import declarative_base\nfrom sqlalchemy.orm import scoped_session, sessionmaker\nfrom sqlalchemy.orm.exc import NoResultFound\nfrom sqlalchemy.schema import Column\nfrom sqlalchemy.sql.expression import desc, distinct\nfrom sqlalchemy.sql.functions import sum as sa_sum\nfrom sqlalchemy.types import (\n    BigInteger,\n    DateTime,\n    Integer,\n    String,\n    TypeDecorator,\n)\n\nfrom r2.lib.memoize import memoize\nfrom r2.lib.utils import timedelta_by_name, tup\nfrom r2.models.link import Link\n\n\nengine = g.dbm.get_engine(\"traffic\")\nSession = scoped_session(sessionmaker(bind=engine, autocommit=True))\nBase = declarative_base(bind=engine)\n\n\ndef memoize_traffic(**memoize_kwargs):\n    \"\"\"Wrap the memoize decorator and automatically determine memoize key.\n\n    The memoize key is based off the full name (including class name) of the\n    method being memoized.\n\n    \"\"\"\n    def memoize_traffic_decorator(fn):\n        def memoize_traffic_wrapper(cls, *args, **kwargs):\n            method = \".\".join((cls.__name__, fn.__name__))\n            actual_memoize_decorator = memoize(method, **memoize_kwargs)\n            actual_memoize_wrapper = actual_memoize_decorator(fn)\n            return actual_memoize_wrapper(cls, *args, **kwargs)\n        return memoize_traffic_wrapper\n    return memoize_traffic_decorator\n\n\nclass PeekableIterator(object):\n    \"\"\"Iterator that supports peeking at the next item in the iterable.\"\"\"\n\n    def __init__(self, iterable):\n        self.iterator = iter(iterable)\n        self.item = None\n\n    def peek(self):\n        \"\"\"Get the next item in the iterable without advancing our position.\"\"\"\n        if not self.item:\n            try:\n                self.item = self.iterator.next()\n            except StopIteration:\n                return None\n        return self.item\n\n    def next(self):\n        \"\"\"Get the next item in the iterable and advance our position.\"\"\"\n        item = self.peek()\n        self.item = None\n        return item\n\n\ndef zip_timeseries(*series, **kwargs):\n    \"\"\"Zip timeseries data while gracefully handling gaps in the data.\n\n    Timeseries data is expected to be a sequence of two-tuples (date, values).\n    Values is expected itself to be a tuple. The width of the values tuples\n    should be the same across all elements in a timeseries sequence. The result\n    will be a single sequence in timeseries format.\n\n    Gaps in sequences are filled with an appropriate number of zeros based on\n    the size of the first value-tuple of that sequence.\n\n    \"\"\"\n\n    next_slice = (max if kwargs.get(\"order\", \"descending\") == \"descending\"\n                  else min)\n    iterators = [PeekableIterator(s) for s in series]\n    widths = []\n    for w in iterators:\n        r = w.peek()\n        if r:\n            date, values = r\n            widths.append(len(values))\n        else:\n            widths.append(0)\n\n    while True:\n        items = [it.peek() for it in iterators]\n        if not any(items):\n            return\n\n        current_slice = next_slice(item[0] for item in items if item)\n\n        data = []\n        for i, item in enumerate(items):\n            # each item is (date, data)\n            if item and item[0] == current_slice:\n                data.extend(item[1])\n                iterators[i].next()\n            else:\n                data.extend([0] * widths[i])\n\n        yield current_slice, tuple(data)\n\n\ndef decrement_month(date):\n    \"\"\"Given a truncated datetime, return a new one one month in the past.\"\"\"\n\n    if date.day != 1:\n        raise ValueError(\"Input must be truncated to the 1st of the month.\")\n\n    date -= datetime.timedelta(days=1)\n    return date.replace(day=1)\n\n\ndef fill_gaps_generator(time_points, query, *columns):\n    \"\"\"Generate a timeseries sequence with a value for every sample expected.\n\n    Iterate over specified time points and pull the columns listed out of\n    query. If the query doesn't have data for a time point, fill the gap with\n    an appropriate number of zeroes.\n\n    \"\"\"\n\n    iterator = PeekableIterator(query)\n    for t in time_points:\n        row = iterator.peek()\n\n        if row and row.date == t:\n            yield t, tuple(getattr(row, c) for c in columns)\n            iterator.next()\n        else:\n            yield t, tuple(0 for c in columns)\n\n\ndef fill_gaps(*args, **kwargs):\n    \"\"\"Listify the generator returned by fill_gaps_generator for `memoize`.\"\"\"\n    generator = fill_gaps_generator(*args, **kwargs)\n    return list(generator)\n\n\ntime_range_by_interval = dict(hour=datetime.timedelta(days=4),\n                              day=datetime.timedelta(weeks=8),\n                              month=datetime.timedelta(weeks=52))\n\n\ndef get_time_points(interval, start_time=None, stop_time=None):\n    \"\"\"Return time points for given interval type.\n\n    Time points are in reverse chronological order to match the sort of\n    queries this will be used with. If start_time and stop_time are not\n    specified they will be picked based on the interval.\n\n    \"\"\"\n\n    def truncate_datetime(dt):\n        dt = dt.replace(minute=0, second=0, microsecond=0)\n        if interval in (\"day\", \"month\"):\n            dt = dt.replace(hour=0)\n        if interval == \"month\":\n            dt = dt.replace(day=1)\n        return dt\n\n    if start_time and stop_time:\n        start_time, stop_time = sorted([start_time, stop_time])\n        # truncate stop_time to an actual traffic time point\n        stop_time = truncate_datetime(stop_time)\n    else:\n        # the stop time is the most recent slice-time; get this by truncating\n        # the appropriate amount from the current time\n        stop_time = datetime.datetime.utcnow()\n        stop_time = truncate_datetime(stop_time)\n\n        # then the start time is easy to work out\n        range = time_range_by_interval[interval]\n        start_time = stop_time - range\n\n    step = timedelta_by_name(interval)\n    current_time = stop_time\n    time_points = []\n\n    while current_time >= start_time:\n        time_points.append(current_time)\n        if interval != 'month':\n            current_time -= step\n        else:\n            current_time = decrement_month(current_time)\n    return time_points\n\n\ndef points_for_interval(interval):\n    \"\"\"Calculate the number of data points to render for a given interval.\"\"\"\n    range = time_range_by_interval[interval]\n    interval = timedelta_by_name(interval)\n    return range.total_seconds() / interval.total_seconds()\n\n\ndef make_history_query(cls, interval):\n    \"\"\"Build a generic query showing the history of a given aggregate.\"\"\"\n\n    time_points = get_time_points(interval)\n    q = (Session.query(cls)\n                .filter(cls.date.in_(time_points)))\n\n    # subscription stats doesn't have an interval (it's only daily)\n    if hasattr(cls, \"interval\"):\n        q = q.filter(cls.interval == interval)\n\n    q = q.order_by(desc(cls.date))\n\n    return time_points, q\n\n\ndef top_last_month(cls, key, ids=None, num=None):\n    \"\"\"Aggregate a listing of the top items (by pageviews) last month.\n\n    We use the last month because it's guaranteed to be fully computed and\n    therefore will be more meaningful.\n\n    \"\"\"\n\n    cur_month = datetime.date.today().replace(day=1)\n    last_month = decrement_month(cur_month)\n\n    q = (Session.query(cls)\n                .filter(cls.date == last_month)\n                .filter(cls.interval == \"month\")\n                .order_by(desc(cls.date), desc(cls.pageview_count)))\n\n    if ids:\n        q = q.filter(getattr(cls, key).in_(ids))\n    else:\n        num = num or 55\n        q = q.limit(num)\n\n    return [(getattr(r, key), (r.unique_count, r.pageview_count))\n            for r in q.all()]\n\n\nclass CoerceToLong(TypeDecorator):\n    # source:\n    # https://groups.google.com/forum/?fromgroups=#!topic/sqlalchemy/3fipkThttQA\n\n    impl = BigInteger\n\n    def process_result_value(self, value, dialect):\n        if value is not None:\n            value = long(value)\n        return value\n\n\ndef sum(column):\n    \"\"\"Wrapper around sqlalchemy.sql.functions.sum to handle BigInteger.\n\n    sqlalchemy returns a Decimal for sum over BigInteger values. Detect the\n    column type and coerce to long if it's a BigInteger.\n\n    \"\"\"\n\n    if isinstance(column.property.columns[0].type, BigInteger):\n        return sa_sum(column, type_=CoerceToLong)\n    else:\n        return sa_sum(column)\n\n\ndef totals(cls, interval):\n    \"\"\"Aggregate sitewide totals for self-serve promotion traffic.\n\n    We only aggregate codenames that start with a link type prefix which\n    effectively filters out all DART / 300x100 etc. traffic numbers.\n\n    \"\"\"\n\n    time_points = get_time_points(interval)\n\n    q = (Session.query(cls.date, sum(cls.pageview_count).label(\"sum\"))\n                .filter(cls.interval == interval)\n                .filter(cls.date.in_(time_points))\n                .filter(cls.codename.startswith(Link._type_prefix))\n                .group_by(cls.date)\n                .order_by(desc(cls.date)))\n    return fill_gaps(time_points, q, \"sum\")\n\n\ndef total_by_codename(cls, codenames):\n    \"\"\"Return total lifetime pageviews (or clicks) for given codename(s).\"\"\"\n    codenames = tup(codenames)\n    # uses hour totals to get the most up-to-date count\n    q = (Session.query(cls.codename, sum(cls.pageview_count))\n                       .filter(cls.interval == \"hour\")\n                       .filter(cls.codename.in_(codenames))\n                       .group_by(cls.codename))\n    return list(q)\n\n\ndef promotion_history(cls, count_column, codename, start, stop):\n    \"\"\"Get hourly traffic for a self-serve promotion.\n\n    Traffic stats are summed over all targets for classes that include a target.\n\n    count_column should be cls.pageview_count or cls.unique_count.\n\n    NOTE: when retrieving uniques the counts for ALL targets are summed, which\n    isn't strictly correct but is the best we can do for now.\n\n    \"\"\"\n\n    time_points = get_time_points('hour', start, stop)\n    q = (Session.query(cls.date, sum(count_column))\n                .filter(cls.interval == \"hour\")\n                .filter(cls.codename == codename)\n                .filter(cls.date.in_(time_points))\n                .group_by(cls.date)\n                .order_by(cls.date))\n    return [(r[0], (r[1],)) for r in q.all()]\n\n\ndef campaign_history(cls, codenames, start, stop):\n    \"\"\"Get hourly traffic for given campaigns.\"\"\"\n    time_points = get_time_points('hour', start, stop)\n    q = (Session.query(cls)\n                .filter(cls.interval == \"hour\")\n                .filter(cls.codename.in_(codenames))\n                .filter(cls.date.in_(time_points))\n                .order_by(cls.date))\n    return [(r.date, r.codename, r.subreddit, (r.unique_count,\n                                               r.pageview_count))\n            for r in q.all()]\n\n\n@memoize(\"traffic_last_modified\", time=60 * 10)\ndef get_traffic_last_modified():\n    \"\"\"Guess how far behind the traffic processing system is.\"\"\"\n    try:\n        return (Session.query(SitewidePageviews.date)\n                   .order_by(desc(SitewidePageviews.date))\n                   .limit(1)\n                   .one()).date\n    except NoResultFound:\n        return datetime.datetime.min\n\n\n@memoize(\"missing_traffic\", time=60 * 10)\ndef get_missing_traffic(start, end):\n    \"\"\"Check for missing hourly traffic between start and end.\"\"\"\n\n    # NOTE: start, end must be UTC time without tzinfo\n    time_points = get_time_points('hour', start, end)\n    q = (Session.query(SitewidePageviews.date)\n                .filter(SitewidePageviews.interval == \"hour\")\n                .filter(SitewidePageviews.date.in_(time_points)))\n    found = [t for (t,) in q]\n    return [t for t in time_points if t not in found]\n\n\nclass SitewidePageviews(Base):\n    \"\"\"Pageviews across all areas of the site.\"\"\"\n\n    __tablename__ = \"traffic_aggregate\"\n\n    date = Column(DateTime(), nullable=False, primary_key=True)\n    interval = Column(String(), nullable=False, primary_key=True)\n    unique_count = Column(\"unique\", Integer())\n    pageview_count = Column(\"total\", BigInteger())\n\n    @classmethod\n    @memoize_traffic(time=3600)\n    def history(cls, interval):\n        time_points, q = make_history_query(cls, interval)\n        return fill_gaps(time_points, q, \"unique_count\", \"pageview_count\")\n\n\nclass PageviewsBySubreddit(Base):\n    \"\"\"Pageviews within a subreddit (i.e. /r/something/...).\"\"\"\n\n    __tablename__ = \"traffic_subreddits\"\n\n    subreddit = Column(String(), nullable=False, primary_key=True)\n    date = Column(DateTime(), nullable=False, primary_key=True)\n    interval = Column(String(), nullable=False, primary_key=True)\n    unique_count = Column(\"unique\", Integer())\n    pageview_count = Column(\"total\", Integer())\n\n    @classmethod\n    @memoize_traffic(time=3600)\n    def history(cls, interval, subreddit):\n        time_points, q = make_history_query(cls, interval)\n        q = q.filter(cls.subreddit == subreddit)\n        return fill_gaps(time_points, q, \"unique_count\", \"pageview_count\")\n\n    @classmethod\n    @memoize_traffic(time=3600 * 6)\n    def top_last_month(cls, num=None):\n        return top_last_month(cls, \"subreddit\", num=num)\n\n    @classmethod\n    @memoize_traffic(time=3600 * 6)\n    def last_month(cls, srs):\n        ids = [sr.name for sr in srs]\n        return top_last_month(cls, \"subreddit\", ids=ids)\n\n\nclass PageviewsBySubredditAndPath(Base):\n    \"\"\"Pageviews within a subreddit with action included.\n\n    `srpath` is the subreddit name, a dash, then the controller method called\n    to render the page the user viewed. e.g. reddit.com-GET_listing. This is\n    useful to determine how many pageviews in a subreddit are on listing pages,\n    comment pages, or elsewhere.\n\n    \"\"\"\n\n    __tablename__ = \"traffic_srpaths\"\n\n    srpath = Column(String(), nullable=False, primary_key=True)\n    date = Column(DateTime(), nullable=False, primary_key=True)\n    interval = Column(String(), nullable=False, primary_key=True)\n    unique_count = Column(\"unique\", Integer())\n    pageview_count = Column(\"total\", Integer())\n\n\nclass PageviewsByLanguage(Base):\n    \"\"\"Sitewide pageviews correlated by user's interface language.\"\"\"\n\n    __tablename__ = \"traffic_lang\"\n\n    lang = Column(String(), nullable=False, primary_key=True)\n    date = Column(DateTime(), nullable=False, primary_key=True)\n    interval = Column(String(), nullable=False, primary_key=True)\n    unique_count = Column(\"unique\", Integer())\n    pageview_count = Column(\"total\", BigInteger())\n\n    @classmethod\n    @memoize_traffic(time=3600)\n    def history(cls, interval, lang):\n        time_points, q = make_history_query(cls, interval)\n        q = q.filter(cls.lang == lang)\n        return fill_gaps(time_points, q, \"unique_count\", \"pageview_count\")\n\n    @classmethod\n    @memoize_traffic(time=3600 * 6)\n    def top_last_month(cls):\n        return top_last_month(cls, \"lang\")\n\n\nclass ClickthroughsByCodename(Base):\n    \"\"\"Clickthrough counts for ads.\"\"\"\n\n    __tablename__ = \"traffic_click\"\n\n    codename = Column(\"fullname\", String(), nullable=False, primary_key=True)\n    date = Column(DateTime(), nullable=False, primary_key=True)\n    interval = Column(String(), nullable=False, primary_key=True)\n    unique_count = Column(\"unique\", Integer())\n    pageview_count = Column(\"total\", Integer())\n\n    @classmethod\n    @memoize_traffic(time=3600)\n    def history(cls, interval, codename):\n        time_points, q = make_history_query(cls, interval)\n        q = q.filter(cls.codename == codename)\n        return fill_gaps(time_points, q, \"unique_count\", \"pageview_count\")\n\n    @classmethod\n    @memoize_traffic(time=3600)\n    def promotion_history(cls, codename, start, stop):\n        return promotion_history(cls, cls.unique_count, codename, start, stop)\n\n    @classmethod\n    @memoize_traffic(time=3600)\n    def historical_totals(cls, interval):\n        return totals(cls, interval)\n\n    @classmethod\n    @memoize_traffic(time=3600)\n    def total_by_codename(cls, codenames):\n        return total_by_codename(cls, codenames)\n\n\nclass TargetedClickthroughsByCodename(Base):\n    \"\"\"Clickthroughs for ads, correlated by ad campaign.\"\"\"\n\n    __tablename__ = \"traffic_clicktarget\"\n\n    codename = Column(\"fullname\", String(), nullable=False, primary_key=True)\n    subreddit = Column(String(), nullable=False, primary_key=True)\n    date = Column(DateTime(), nullable=False, primary_key=True)\n    interval = Column(String(), nullable=False, primary_key=True)\n    unique_count = Column(\"unique\", Integer())\n    pageview_count = Column(\"total\", Integer())\n\n    @classmethod\n    @memoize_traffic(time=3600)\n    def promotion_history(cls, codename, start, stop):\n        return promotion_history(cls, cls.unique_count, codename, start, stop)\n\n    @classmethod\n    @memoize_traffic(time=3600)\n    def total_by_codename(cls, codenames):\n        return total_by_codename(cls, codenames)\n\n    @classmethod\n    def campaign_history(cls, codenames, start, stop):\n        return campaign_history(cls, codenames, start, stop)\n\n\nclass AdImpressionsByCodename(Base):\n    \"\"\"Impressions for ads.\"\"\"\n\n    __tablename__ = \"traffic_thing\"\n\n    codename = Column(\"fullname\", String(), nullable=False, primary_key=True)\n    date = Column(DateTime(), nullable=False, primary_key=True)\n    interval = Column(String(), nullable=False, primary_key=True)\n    unique_count = Column(\"unique\", Integer())\n    pageview_count = Column(\"total\", BigInteger())\n\n    @classmethod\n    @memoize_traffic(time=3600)\n    def history(cls, interval, codename):\n        time_points, q = make_history_query(cls, interval)\n        q = q.filter(cls.codename == codename)\n        return fill_gaps(time_points, q, \"unique_count\", \"pageview_count\")\n\n    @classmethod\n    @memoize_traffic(time=3600)\n    def promotion_history(cls, codename, start, stop):\n        return promotion_history(cls, cls.pageview_count, codename, start, stop)\n\n    @classmethod\n    @memoize_traffic(time=3600)\n    def historical_totals(cls, interval):\n        return totals(cls, interval)\n\n    @classmethod\n    @memoize_traffic(time=3600)\n    def top_last_month(cls):\n        return top_last_month(cls, \"codename\")\n\n    @classmethod\n    @memoize_traffic(time=3600)\n    def recent_codenames(cls, fullname):\n        \"\"\"Get a list of recent codenames used for 300x100 ads.\n\n        The 300x100 ads get a codename that looks like \"fullname_campaign\".\n        This function gets a list of recent campaigns.\n\n        \"\"\"\n        time_points = get_time_points('day')\n        query = (Session.query(distinct(cls.codename).label(\"codename\"))\n                        .filter(cls.date.in_(time_points))\n                        .filter(cls.codename.startswith(fullname)))\n        return [row.codename for row in query]\n\n    @classmethod\n    @memoize_traffic(time=3600)\n    def total_by_codename(cls, codename):\n        return total_by_codename(cls, codename)\n\n\nclass TargetedImpressionsByCodename(Base):\n    \"\"\"Impressions for ads, correlated by ad campaign.\"\"\"\n\n    __tablename__ = \"traffic_thingtarget\"\n\n    codename = Column(\"fullname\", String(), nullable=False, primary_key=True)\n    subreddit = Column(String(), nullable=False, primary_key=True)\n    date = Column(DateTime(), nullable=False, primary_key=True)\n    interval = Column(String(), nullable=False, primary_key=True)\n    unique_count = Column(\"unique\", Integer())\n    pageview_count = Column(\"total\", Integer())\n\n    @classmethod\n    @memoize_traffic(time=3600)\n    def promotion_history(cls, codename, start, stop):\n        return promotion_history(cls, cls.pageview_count, codename, start, stop)\n\n    @classmethod\n    @memoize_traffic(time=3600)\n    def total_by_codename(cls, codenames):\n        return total_by_codename(cls, codenames)\n\n    @classmethod\n    def campaign_history(cls, codenames, start, stop):\n        return campaign_history(cls, codenames, start, stop)\n\n\nclass SubscriptionsBySubreddit(Base):\n    \"\"\"Subscription statistics for subreddits.\n\n    This table is different from the rest of the traffic ones.  It only\n    contains data at a daily interval (hence no `interval` column) and is\n    updated separately in the subscribers cron job (see\n    reddit-job-subscribers).\n\n    \"\"\"\n\n    __tablename__ = \"traffic_subscriptions\"\n\n    subreddit = Column(String(), nullable=False, primary_key=True)\n    date = Column(DateTime(), nullable=False, primary_key=True)\n    subscriber_count = Column(\"unique\", Integer())\n\n    @classmethod\n    @memoize_traffic(time=3600)\n    def history(cls, interval, subreddit):\n        time_points, q = make_history_query(cls, interval)\n        q = q.filter(cls.subreddit == subreddit)\n        return fill_gaps(time_points, q, \"subscriber_count\")\n\n\n# create the tables if they don't exist\nif g.db_create_tables:\n    Base.metadata.create_all()\n"
  },
  {
    "path": "r2/r2/models/trylater.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\"\"\"A delayed execution system.\n\nThe ``trylater`` module provides tools for performing an action at a set time\nin the future.  To use it, you must do two things.\n\nFirst, make a scheduling call::\n\n    from datetime import timedelta\n\n    from r2.models.trylater import TryLater\n\n    def make_breakfast(spam):\n        breakfast = cook(spam)\n        later = timedelta(minutes=45)\n        # The storage layer only likes strings.\n        data = json.dumps(breakfast)\n        TryLater.schedule('wash_dishes', data, later)\n\nThen, write the delayed code and decorate it with a hook, using the same\nidentifier as you used when you scheduled it::\n\n    from r2.lib import hooks\n    trylater_hooks = hooks.HookRegistrar()\n\n    @trylater_hooks.on('trylater.wash_dishes')\n    def on_dish_washing(data):\n        # data is an ordered dictionary of timeuuid -> data pairs.\n        for datum in data.values():\n            meal = json.loads(datum)\n            for dish in meal.dishes:\n                dish.wash()\n\nNote: once you've scheduled a ``TryLater`` task, there's no stopping it!  If\nyou might need to cancel your jobs later, use ``TryLaterBySubject``, which uses\nalmost the exact same semantics, but has a useful ``unschedule`` method.\n\"\"\"\n\nfrom collections import OrderedDict\nfrom datetime import datetime, timedelta\nimport uuid\n\nfrom pycassa.system_manager import TIME_UUID_TYPE, UTF8_TYPE\nfrom pycassa.util import convert_uuid_to_time\nfrom pylons import app_globals as g\n\nfrom r2.lib.db import tdb_cassandra\nfrom r2.lib.utils import tup\n\n\nclass TryLater(tdb_cassandra.View):\n    _use_db = True\n    _read_consistency_level = tdb_cassandra.CL.QUORUM\n    _write_consistency_level = tdb_cassandra.CL.QUORUM\n    _compare_with = TIME_UUID_TYPE\n\n    @classmethod\n    def process_ready_items(cls, rowkey, ready_fn):\n        cutoff = datetime.now(g.tz)\n\n        columns = cls._cf.xget(rowkey, include_timestamp=True)\n        ready_items = OrderedDict()\n        ready_timestamps = []\n        unripe_timestamps = []\n\n        for ready_time_uuid, (data, timestamp) in columns:\n            ready_time = convert_uuid_to_time(ready_time_uuid)\n            ready_datetime = datetime.fromtimestamp(ready_time, tz=g.tz)\n            if ready_datetime <= cutoff:\n                ready_items[ready_time_uuid] = data\n                ready_timestamps.append(timestamp)\n            else:\n                unripe_timestamps.append(timestamp)\n\n        g.stats.simple_event(\n            \"trylater.{system}.ready\".format(system=rowkey),\n            delta=len(ready_items),\n        )\n        g.stats.simple_event(\n            \"trylater.{system}.pending\".format(system=rowkey),\n            delta=len(unripe_timestamps),\n        )\n\n        if not ready_items:\n            return\n\n        try:\n            ready_fn(ready_items)\n        except:\n            g.stats.simple_event(\n                \"trylater.{system}.failed\".format(system=rowkey),\n            )\n\n        cls.cleanup(rowkey, ready_items, ready_timestamps, unripe_timestamps)\n\n    @classmethod\n    def cleanup(cls, rowkey, ready_items, ready_timestamps, unripe_timestamps):\n        \"\"\"Remove ALL ready items from the C* row\"\"\"\n        if (not unripe_timestamps or\n                min(unripe_timestamps) > max(ready_timestamps)):\n            # do a row/timestamp delete to avoid generating column\n            # tombstones\n            cls._cf.remove(rowkey, timestamp=max(ready_timestamps))\n            g.stats.simple_event(\n                \"trylater.{system}.row_delete\".format(system=rowkey),\n                delta=len(ready_items),\n            )\n        else:\n            # the columns weren't created with a fixed delay and there are some\n            # unripe items with older (lower) timestamps than the items we want\n            # to delete. fallback to deleting specific columns.\n            cls._cf.remove(rowkey, ready_items.keys())\n            g.stats.simple_event(\n                \"trylater.{system}.column_delete\".format(system=rowkey),\n                delta=len(ready_items),\n            )\n\n    @classmethod\n    def run(cls):\n        \"\"\"Run all ready items through their processing hook.\"\"\"\n        from r2.lib import amqp\n        from r2.lib.hooks import all_hooks\n\n        for hook_name, hook in all_hooks().items():\n            if hook_name.startswith(\"trylater.\"):\n                rowkey = hook_name[len(\"trylater.\"):]\n\n                def ready_fn(ready_items):\n                    return hook.call(data=ready_items)\n\n                g.log.info(\"Trying %s\", rowkey)\n                cls.process_ready_items(rowkey, ready_fn)\n\n        amqp.worker.join()\n        g.stats.flush()\n\n    @classmethod\n    def search(cls, rowkey, when):\n        if isinstance(when, uuid.UUID):\n            when = convert_uuid_to_time(when)\n        try:\n            return cls._cf.get(rowkey, column_start=when, column_finish=when)\n        except tdb_cassandra.NotFoundException:\n            return {}\n\n    @classmethod\n    def schedule(cls, system, data, delay=None):\n        \"\"\"Schedule code for later execution.\n\n        system:  an string identifying the hook to be executed\n        data:    passed to the hook as an argument\n        delay:   (optional) a datetime.timedelta indicating the desired\n                 execution time\n        \"\"\"\n        if delay is None:\n            delay = timedelta(minutes=60)\n        key = datetime.now(g.tz) + delay\n        scheduled = {key: data}\n        cls._set_values(system, scheduled)\n        return scheduled\n\n    @classmethod\n    def unschedule(cls, rowkey, column_keys):\n        column_keys = tup(column_keys)\n        return cls._cf.remove(rowkey, column_keys)\n\n\nclass TryLaterBySubject(tdb_cassandra.View):\n    _use_db = True\n    _read_consistency_level = tdb_cassandra.CL.QUORUM\n    _write_consistency_level = tdb_cassandra.CL.QUORUM\n    _compare_with = UTF8_TYPE\n    _extra_schema_creation_args = {\n        \"key_validation_class\": UTF8_TYPE,\n        \"default_validation_class\": TIME_UUID_TYPE,\n    }\n    _value_type = 'date'\n\n    @classmethod\n    def schedule(cls, system, subject, data, delay, trylater_rowkey=None):\n        if trylater_rowkey is None:\n            trylater_rowkey = system\n        scheduled = TryLater.schedule(trylater_rowkey, data, delay)\n        when = scheduled.keys()[0]\n\n        # TTL 10 minutes after the TryLater runs just in case TryLater\n        # is running late.\n        ttl = (delay + timedelta(minutes=10)).total_seconds()\n        coldict = {subject: when}\n        cls._set_values(system, coldict, ttl=ttl)\n        return scheduled\n\n    @classmethod\n    def search(cls, rowkey, subjects=None):\n        try:\n            if subjects:\n                subjects = tup(subjects)\n                return cls._cf.get(rowkey, subjects)\n            else:\n                return cls._cf.get(rowkey)\n        except tdb_cassandra.NotFoundException:\n            return {}\n\n    @classmethod\n    def unschedule(cls, rowkey, colkey, schedule_rowkey):\n        colkey = tup(colkey)\n        victims = cls.search(rowkey, colkey)\n        for uu in victims.itervalues():\n            keys = TryLater.search(schedule_rowkey, uu).keys()\n            TryLater.unschedule(schedule_rowkey, keys)\n        cls._cf.remove(rowkey, colkey)\n"
  },
  {
    "path": "r2/r2/models/vote.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport collections\nfrom datetime import datetime, timedelta\nimport json\nfrom uuid import uuid1\n\nfrom pycassa.types import CompositeType, AsciiType\nfrom pycassa.system_manager import TIME_UUID_TYPE\nfrom pylons import app_globals as g\nimport pytz\n\nfrom r2.lib import hooks\nfrom r2.lib.db import tdb_cassandra\nfrom r2.lib.db.tdb_cassandra import (\n    ASCII_TYPE,\n    UTF8_TYPE,\n)\nfrom r2.lib.utils import Enum, epoch_timestamp\n\nfrom r2.models import Account\n\n\nclass Vote(object):\n    DIRECTIONS = Enum(\"up\", \"down\", \"unvote\")\n    SERIALIZED_DIRECTIONS = {\n        DIRECTIONS.up: 1,\n        DIRECTIONS.down: -1,\n        DIRECTIONS.unvote: 0,\n    }\n    DESERIALIZED_DIRECTIONS = {\n        v: k for k, v in SERIALIZED_DIRECTIONS.iteritems()}\n\n    def __init__(self, user, thing, direction, date, data=None, effects=None,\n            get_previous_vote=True, event_data=None):\n        if not thing.is_votable:\n            raise TypeError(\"Can't create vote on unvotable thing %s\" % thing)\n\n        if direction not in self.DIRECTIONS:\n            raise ValueError(\"Invalid vote direction: %s\" % direction)\n\n        self.user = user\n        self.thing = thing\n        self.direction = direction\n        self.date = date.replace(tzinfo=g.tz)\n        self.data = data\n        self.event_data = event_data\n\n        # see if the user has voted on this thing before\n        if get_previous_vote:\n            self.previous_vote = VoteDetailsByThing.get_vote(user, thing)\n            if self.previous_vote:\n                # XXX: why do we keep the old date?\n                self.date = self.previous_vote.date.replace(tzinfo=g.tz)\n        else:\n            self.previous_vote = None\n\n        self.effects = VoteEffects(self, effects)\n\n    def __eq__(self, other):\n        return (self.user == other.user and\n            self.thing == other.thing and\n            self.direction == other.direction)\n\n    def __ne__(self, other):\n        return not self == other\n\n    @classmethod\n    def serialize_direction(cls, direction):\n        \"\"\"Convert the DIRECTIONS enum to values used when storing.\"\"\"\n        if direction not in cls.DIRECTIONS:\n            raise ValueError(\"Invalid vote direction: %s\" % direction)\n\n        return cls.SERIALIZED_DIRECTIONS[direction]\n\n    @classmethod\n    def deserialize_direction(cls, direction):\n        \"\"\"Convert stored vote direction value back to DIRECTIONS enum.\"\"\"\n        direction = int(direction)\n\n        if direction not in cls.DESERIALIZED_DIRECTIONS:\n            raise ValueError(\"Invalid vote direction: %s\" % direction)\n\n        return cls.DESERIALIZED_DIRECTIONS[direction]\n\n    @property\n    def _id(self):\n        return \"%s_%s\" % (self.user._id36, self.thing._id36)\n\n    @property\n    def affected_thing_attr(self):\n        \"\"\"The attr on the thing this vote will increment.\"\"\"\n        if not self.effects.affects_score:\n            return None\n\n        if self.is_upvote:\n            return \"_ups\"\n        elif self.is_downvote:\n            return \"_downs\"\n\n    @property\n    def is_upvote(self):\n        return self.direction == self.DIRECTIONS.up\n\n    @property\n    def is_downvote(self):\n        return self.direction == self.DIRECTIONS.down\n\n    @property\n    def is_self_vote(self):\n        \"\"\"Whether the voter is also the author of the thing voted on.\"\"\"\n        return self.user._id == self.thing.author_id\n\n    @property\n    def is_automatic_initial_vote(self):\n        \"\"\"Whether this is the automatic vote cast on things when posted.\"\"\"\n        return self.is_self_vote and not self.previous_vote\n\n    @property\n    def delay(self):\n        \"\"\"How long after the thing was posted that the vote was cast.\"\"\"\n        if self.is_automatic_initial_vote:\n            return timedelta(0)\n        \n        return self.date - self.thing._date\n\n    def apply_effects(self):\n        \"\"\"Apply the effects of the vote to the thing that was voted on.\"\"\"\n        # remove the old vote\n        if self.previous_vote and self.previous_vote.affected_thing_attr:\n            self.thing._incr(self.previous_vote.affected_thing_attr, -1)\n\n        # add the new vote\n        if self.affected_thing_attr:\n            self.thing._incr(self.affected_thing_attr, 1)\n\n        if self.effects.affects_karma:\n            change = self.effects.karma_change\n            if self.previous_vote:\n                change -= self.previous_vote.effects.karma_change\n\n            if change:\n                self.thing.author_slow.incr_karma(\n                    kind=self.thing.affects_karma_type,\n                    sr=self.thing.subreddit_slow,\n                    amt=change,\n                )\n\n        hooks.get_hook(\"vote.apply_effects\").call(vote=self)\n\n    def commit(self):\n        \"\"\"Apply the vote's effects and persist it.\"\"\"\n        if self.previous_vote and self == self.previous_vote:\n            return\n\n        self.apply_effects()\n        VotesByAccount.write_vote(self)\n\n        # Always update the search index if the thing has fewer than 20 votes.\n        # When the thing has more votes queue an update less often.\n        if self.thing.num_votes < 20 or self.thing.num_votes % 10 == 0:\n            self.thing.update_search_index(boost_only=True)\n\n        if self.event_data:\n            g.events.vote_event(self)\n\n        g.stats.simple_event('vote.total')\n\n\nclass VoteEffects(object):\n    \"\"\"Contains details about how a vote affects the thing voted on.\"\"\"\n    def __init__(self, vote, effects=None):\n        \"\"\"Initialize a new set of vote effects.\n\n        If a dict of previously-determined effects are passed in as `effects`,\n        those will be used instead of calculating the effects.\n        \"\"\"\n        self.note_codes = {}\n        self.validator = None\n\n        if effects:\n            self.affects_score = effects.pop(\"affects_score\")\n            self.affects_karma = effects.pop(\"affects_karma\")\n            self.other_effects = effects\n        else:\n            hook = hooks.get_hook(\"vote.get_validator\")\n            self.validator = hook.call_until_return(vote=vote, effects=self)\n\n            self.affects_score = self.determine_affects_score(vote)\n            self.affects_karma = self.determine_affects_karma(vote)\n            self.other_effects = self.determine_other_effects(vote)\n\n        self.karma_change = 0\n        if self.affects_karma:\n            if vote.is_upvote:\n                self.karma_change = 1\n            elif vote.is_downvote:\n                self.karma_change = -1\n\n    def add_note(self, code, message=None):\n        self.note_codes[code] = message\n\n    @property\n    def notes(self):\n        notes = []\n\n        for code, message in self.note_codes.iteritems():\n            note = code\n            if message:\n                note += \" (%s)\" % message\n            notes.append(note)\n\n        return notes\n\n    def determine_affects_score(self, vote):\n        \"\"\"Determine whether the vote should affect the thing's score.\"\"\"\n        # If it's the automatic upvote on the user's own post, it won't affect\n        # the score because we create it with a score of 1 already.\n        if vote.is_automatic_initial_vote:\n            self.add_note(\"AUTOMATIC_INITIAL_VOTE\")\n            return False\n\n        if vote.previous_vote:\n            if not vote.previous_vote.effects.affects_score:\n                self.add_note(\"PREVIOUS_VOTE_NO_EFFECT\")\n                return False\n\n        if self.validator:\n            affects_score = self.validator.determine_affects_score()\n            if affects_score is not None:\n                return affects_score\n\n        return True\n\n    def determine_affects_karma(self, vote):\n        \"\"\"Determine whether the vote should affect the author's karma.\"\"\"\n        from r2.models import Comment\n\n        if not self.affects_score:\n            return False\n\n        if vote.previous_vote:\n            if not vote.previous_vote.effects.affects_karma:\n                self.add_note(\"PREVIOUS_VOTE_NO_KARMA\")\n                return False\n\n        if not bool(vote.thing.affects_karma_type):\n            self.add_note(\"KARMALESS_THING\")\n            return False\n\n        # never give karma on stickied comments. Only check distinguished\n        # comments to avoid fetching the link on most votes, for performance.\n        if isinstance(vote.thing, Comment) and vote.thing.is_distinguished:\n            link = vote.thing.link_slow\n            if vote.thing._id == link.sticky_comment_id:\n                self.add_note(\"COMMENT_STICKIED\")\n                return False\n\n        if self.validator:\n            affects_karma = self.validator.determine_affects_karma()\n            if affects_karma is not None:\n                return affects_karma\n\n        return True\n\n    def determine_other_effects(self, vote):\n        \"\"\"Determine any other effects of the vote.\"\"\"\n        other_effects = {}\n\n        if self.validator:\n            other_effects.update(self.validator.other_effects)\n\n        return other_effects\n\n    @property\n    def serializable_data(self):\n        \"\"\"Return the effects data in a format suitable for storing.\"\"\"\n        data = {\n            \"affects_score\": self.affects_score,\n            \"affects_karma\": self.affects_karma,\n        }\n\n        for key, value in self.other_effects.iteritems():\n            data[key] = value\n\n        if self.notes:\n            data[\"notes\"] = \", \".join(self.notes)\n\n        return data\n\n\nclass VotesByAccount(tdb_cassandra.DenormalizedRelation):\n    _use_db = False\n    _read_consistency_level = tdb_cassandra.CL.ONE\n\n    @classmethod\n    def rel(cls, thing_cls):\n        from r2.models import Comment, Link\n        if thing_cls == Link:\n            return LinkVotesByAccount\n        elif thing_cls == Comment:\n            return CommentVotesByAccount\n\n        raise TypeError(\"Can't find %r class for %r\" % (cls, thing_cls))\n\n    @classmethod\n    def write_vote(cls, vote):\n        rel = cls.rel(vote.thing.__class__)\n        rel.create(vote.user, vote.thing, vote=vote)\n\n    @classmethod\n    def value_for(cls, thing1, thing2, vote):\n        return str(Vote.serialize_direction(vote.direction))\n\n\nclass LinkVotesByAccount(VotesByAccount):\n    _use_db = True\n    _views = []\n    _last_modified_name = \"LinkVote\"\n    # this is taken care of in r2.lib.voting:cast_vote\n    _write_last_modified = False\n\n\nclass CommentVotesByAccount(VotesByAccount):\n    _use_db = True\n    _views = []\n    _last_modified_name = \"CommentVote\"\n    # this is taken care of in r2.lib.voting:cast_vote\n    _write_last_modified = False\n\n\nclass VoteDetailsByThing(tdb_cassandra.View):\n    _use_db = False\n    _fetch_all_columns = True\n    _extra_schema_creation_args = dict(key_validation_class=ASCII_TYPE,\n                                       default_validation_class=UTF8_TYPE)\n\n    @classmethod\n    def create(cls, user, thing, vote):\n        # we don't use the user or thing args, but they need to be there for\n        # calling this automatically when updating views of a DenormalizedRel\n        vote_data = vote.data.copy()\n\n        # pull the IP out of the data to store it separately with a TTL\n        ip = vote_data.pop(\"ip\")\n\n        effects_data = vote.effects.serializable_data\n        # split the notes out to store separately\n        notes = effects_data.pop(\"notes\", None)\n\n        data = json.dumps({\n            \"direction\": Vote.serialize_direction(vote.direction),\n            \"date\": int(epoch_timestamp(vote.date)),\n            \"data\": vote_data,\n            \"effects\": effects_data,\n        })\n\n        cls._set_values(vote.thing._id36, {vote.user._id36: data})\n\n        # write the IP data and notes separately so they can be TTLed\n        if ip:\n            VoterIPByThing.create(vote, ip)\n\n        if notes:\n            VoteNote.set(vote, notes)\n\n    @classmethod\n    def get_vote(cls, user, thing):\n        details = cls.get_details(thing, [user])\n        if details:\n            return details[0]\n\n        return None\n\n    @staticmethod\n    def convert_old_details(old_data):\n        if \"valid_thing\" not in old_data:\n            return old_data\n\n        converted_data = {}\n        converted_data[\"direction\"] = int(old_data.pop(\"direction\"))\n        converted_data[\"date\"] = int(old_data.pop(\"date\"))\n\n        valid_thing = old_data.pop(\"valid_thing\", True)\n        valid_user = old_data.pop(\"valid_user\", True)\n        converted_data[\"effects\"] = {\n            \"affects_score\": valid_thing,\n            \"affects_karma\": valid_user,\n        }\n\n        if old_data:\n            converted_data[\"data\"] = old_data\n\n        return converted_data\n\n    @classmethod\n    def get_details(cls, thing, voters=None):\n        from r2.models import Comment, Link\n        if isinstance(thing, Link):\n            details_cls = VoteDetailsByLink\n        elif isinstance(thing, Comment):\n            details_cls = VoteDetailsByComment\n        else:\n            raise ValueError\n\n        voter_id36s = None\n        if voters:\n            voter_id36s = [voter._id36 for voter in voters]\n\n        try:\n            row = details_cls._byID(thing._id36, properties=voter_id36s)\n            raw_details = row._values()\n        except tdb_cassandra.NotFound:\n            return []\n\n        try:\n            row = VoterIPByThing._byID(thing._fullname, properties=voter_id36s)\n            ips = row._values()\n        except tdb_cassandra.NotFound:\n            ips = {}\n\n        details = []\n        for voter_id36, json_data in raw_details.iteritems():\n            data = json.loads(json_data)\n            data = cls.convert_old_details(data)\n\n            user = Account._byID36(voter_id36, data=True)\n            direction = Vote.deserialize_direction(data.pop(\"direction\"))\n            date = datetime.utcfromtimestamp(data.pop(\"date\"))\n            effects = data.pop(\"effects\")\n            data[\"ip\"] = ips.get(voter_id36)\n\n            vote = Vote(user, thing, direction, date, data, effects,\n                get_previous_vote=False)\n            details.append(vote)\n        details.sort(key=lambda d: d.date)\n\n        return details\n\n\n@tdb_cassandra.view_of(LinkVotesByAccount)\nclass VoteDetailsByLink(VoteDetailsByThing):\n    _use_db = True\n\n\n@tdb_cassandra.view_of(CommentVotesByAccount)\nclass VoteDetailsByComment(VoteDetailsByThing):\n    _use_db = True\n\n\nclass VoterIPByThing(tdb_cassandra.View):\n    _use_db = True\n    _ttl = timedelta(days=100)\n    _fetch_all_columns = True\n    _extra_schema_creation_args = dict(key_validation_class=ASCII_TYPE,\n                                       default_validation_class=UTF8_TYPE)\n\n    @classmethod\n    def create(cls, vote, ip):\n        cls._set_values(vote.thing._fullname, {vote.user._id36: ip})\n\n\nclass VoteNote(tdb_cassandra.View):\n    _use_db = True\n    _connection_pool = 'main'\n    _compare_with = TIME_UUID_TYPE\n    _ttl = timedelta(days=100)\n\n    @classmethod\n    def _rowkey(cls, vote):\n        return '%s_%s' % (vote.user._fullname, vote.thing._fullname)\n\n    @classmethod\n    def set(cls, vote, note):\n        rowkey = cls._rowkey(vote)\n        column = {uuid1(): note}\n        cls._set_values(rowkey, column)\n\n    @classmethod\n    def get(cls, vote):\n        rowkey = cls._rowkey(vote)\n        try:\n            all_notes = cls._byID(rowkey)\n        except tdb_cassandra.NotFound:\n            return None\n\n        return \", \".join(all_notes._values().values())\n"
  },
  {
    "path": "r2/r2/models/wiki.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom ConfigParser import SafeConfigParser\nfrom datetime import datetime, timedelta\nfrom r2.lib.db import tdb_cassandra\nfrom r2.lib.db.thing import NotFound\nfrom r2.lib.merge import *\nfrom r2.models.last_modified import LastModified\nfrom pycassa.system_manager import TIME_UUID_TYPE\nfrom pylons import tmpl_context as c\nfrom pylons import app_globals as g\nfrom pylons.controllers.util import abort\nfrom r2.lib.db.tdb_cassandra import NotFound\nfrom r2.models.printable import Printable\nfrom r2.models.account import Account\nfrom collections import OrderedDict\nfrom StringIO import StringIO\n\nimport pycassa.types\n\n# Used for the key/id for pages,\nPAGE_ID_SEP = '\\t'\n\n# Number of days to keep recent revisions for\nWIKI_RECENT_DAYS = g.wiki_keep_recent_days\n\n# Max length of a single page in bytes\nMAX_PAGE_LENGTH_BYTES = g.wiki_max_page_length_bytes\n\n# Page names which should never be\nimpossible_namespaces = ('edit/', 'revisions/', 'settings/', 'discussions/', \n                         'revisions/', 'pages/', 'create/')\n\n# Namespaces in which access is denied to do anything but view\nrestricted_namespaces = ('reddit/', 'config/', 'special/')\n\n# Pages which may only be edited by mods, must be within restricted namespaces\nspecial_pages = {\n    'config/automoderator',\n    'config/description',\n    'config/sidebar',\n    'config/stylesheet',\n    'config/submit_text',\n}\n\nspecial_page_view_permlevels = {\n    \"config/automoderator\": 2,\n}\n\n# Pages that get created automatically from the subreddit settings page\nautomatically_created_pages = {\n    'config/description',\n    'config/sidebar',\n    'config/stylesheet',\n    'config/submit_text',\n}\n\n# Pages which have a special length restrictions (In bytes)\nspecial_length_restrictions_bytes = {\n    'config/stylesheet': 128*1024,\n    'config/submit_text': 1024,\n    'config/sidebar': 5120,\n    'config/description': 500,\n    'usernotes': 1024*1024,\n}\n\nmodactions = {\n    \"config/automoderator\": \"Updated AutoModerator configuration\",\n    \"config/description\": \"Updated subreddit description\",\n    \"config/sidebar\": \"Updated subreddit sidebar\",\n    \"config/submit_text\": \"Updated submission text\",\n}\n\n# Page \"index\" in the subreddit \"reddit.com\" and a seperator of \"\\t\" becomes:\n#   \"reddit.com\\tindex\"\ndef wiki_id(sr, page):\n    return ('%s%s%s' % (sr, PAGE_ID_SEP, page)).lower()\n\nclass ContentLengthError(Exception):\n    def __init__(self, max_length):\n        Exception.__init__(self)\n        self.max_length = max_length\n\nclass WikiPageExists(Exception):\n    pass\n\nclass WikiBadRevision(Exception):\n    pass\n\nclass WikiPageEditors(tdb_cassandra.View):\n    _use_db = True\n    _value_type = 'str'\n    _connection_pool = 'main'\n\nclass WikiRevision(tdb_cassandra.UuidThing, Printable):\n    \"\"\" Contains content (markdown), author of the edit, page the edit belongs to, and datetime of the edit \"\"\"\n    \n    _use_db = True\n    _connection_pool = 'main'\n    \n    _str_props = ('pageid', 'content', 'author', 'reason')\n    _bool_props = ('hidden', 'admin_deleted')\n    _defaults = {'admin_deleted': False}\n\n    cache_ignore = set(list(_str_props)).union(Printable.cache_ignore).union(['wikipage'])\n    \n    def get_author(self):\n        author = self._get('author')\n        return Account._byID36(author, data=True) if author else None\n    \n    @classmethod\n    def get_authors(cls, revisions):\n        authors = [r._get('author') for r in revisions]\n        authors = filter(None, authors)\n        return Account._byID36(authors, data=True)\n    \n    @classmethod\n    def get_printable_authors(cls, revisions):\n        from r2.lib.pages import WrappedUser\n        authors = cls.get_authors(revisions)\n        return dict([(id36, WrappedUser(v))\n                     for id36, v in authors.iteritems() if v])\n    \n    @classmethod\n    def add_props(cls, user, wrapped):\n        authors = cls.get_printable_authors(wrapped)\n        pages = {r.page: None for r in wrapped}\n        pages = WikiPage.get_multiple((c.site, page) for page in pages)\n        for item in wrapped:\n            item._hidden = item.is_hidden\n            item._spam = False\n            item.wikipage = pages[item.pageid]\n            author = item._get('author')\n            item.printable_author = authors.get(author, '[unknown]')\n            item.reported = False\n    \n    @classmethod\n    def get(cls, revid, pageid):\n        wr = cls._byID(revid)\n        if wr.pageid != pageid:\n            raise WikiBadRevision('Revision is not for the expected page')\n        return wr\n    \n    def toggle_hide(self):\n        self.hidden = not self.is_hidden\n        self._commit()\n        return self.hidden\n\n    @classmethod\n    def create(cls, pageid, content, author=None, reason=None):\n        kw = dict(pageid=pageid, content=content)\n        if author:\n            kw['author'] = author\n        if reason:\n            kw['reason'] = reason\n        wr = cls(**kw)\n        wr._commit()\n        WikiRevisionHistoryByPage.add_object(wr)\n        WikiRevisionsRecentBySR.add_object(wr)\n        return wr\n\n    def _on_commit(self):\n        WikiRevisionHistoryByPage.add_object(self)\n        WikiRevisionsRecentBySR.add_object(self)\n\n    @classmethod\n    def get_recent(cls, sr, count=100):\n        return WikiRevisionsRecentBySR.query([sr._id36], count=count)\n    \n    @property\n    def is_hidden(self):\n        return bool(getattr(self, 'hidden', False))\n    \n    @property\n    def info(self, sep=PAGE_ID_SEP):\n        info = self.pageid.split(sep, 1)\n        try:\n            return {'sr': info[0], 'page': info[1]}\n        except IndexError:\n            g.log.error('Broken wiki page ID \"%s\" did PAGE_ID_SEP change?', self.pageid)\n            return {'sr': 'broken', 'page': 'broken'}\n    \n    @property\n    def page(self):\n        return self.info['page']\n    \n    @property\n    def sr(self):\n        return self.info['sr']\n\n\nclass WikiPage(tdb_cassandra.Thing):\n    \"\"\" Contains permissions, current content (markdown), subreddit, and current revision (ID)\n        Key is subreddit-pagename \"\"\"\n    \n    _use_db = True\n    _connection_pool = 'main'\n    \n    _read_consistency_level = tdb_cassandra.CL.QUORUM\n    _write_consistency_level = tdb_cassandra.CL.QUORUM\n    \n    _date_props = ('last_edit_date')\n    _str_props = ('revision', 'name', 'last_edit_by', 'content', 'sr')\n    _int_props = ('permlevel')\n    _bool_props = ('listed')\n    _defaults = {'listed': True}\n\n    def get_author(self):\n        if self._get('last_edit_by'):\n            return Account._byID36(self.last_edit_by, data=True)\n        return None\n    \n    @classmethod\n    def id_for(cls, sr, name):\n        id = getattr(sr, '_id36', None)\n        if not id:\n            raise tdb_cassandra.NotFound\n        return wiki_id(id, name)\n    \n    @classmethod\n    def get_multiple(cls, pages):\n        \"\"\"Get multiple wiki pages.\n        \n        Arguments:\n        pages -- list of tuples in the form of [(sr, names),..]\n        \"\"\"\n        return cls._byID([cls.id_for(sr, name) for sr, name in pages])\n    \n    @classmethod\n    def get(cls, sr, name):\n        return cls._byID(cls.id_for(sr, name))\n\n    @classmethod\n    def create(cls, sr, name):\n        if not name or not sr:\n            raise ValueError\n\n        name = name.lower()\n        _id = wiki_id(sr._id36, name)\n        lock_key = \"wiki_create_%s:%s\" % (sr._id36, name)\n        with g.make_lock(\"wiki\", lock_key):\n            try:\n                cls._byID(_id)\n            except tdb_cassandra.NotFound:\n                pass\n            else:\n                raise WikiPageExists\n\n            page = cls(_id=_id, sr=sr._id36, name=name, permlevel=0, content='')\n            page._commit()\n            return page\n\n    @property\n    def restricted(self):\n        return WikiPage.is_restricted(self.name)\n\n    @classmethod\n    def is_impossible(cls, page):\n        return (\"%s/\" % page) in impossible_namespaces or page.startswith(impossible_namespaces)\n    \n    @classmethod\n    def is_restricted(cls, page):\n        return (\"%s/\" % page) in restricted_namespaces or page.startswith(restricted_namespaces)\n    \n    @classmethod\n    def is_special(cls, page):\n        return page in special_pages\n\n    @classmethod\n    def get_special_view_permlevel(cls, page):\n        return special_page_view_permlevels.get(page, 0)\n\n    @classmethod\n    def is_automatically_created(cls, page):\n        return page in automatically_created_pages\n    \n    @property\n    def special(self):\n        return WikiPage.is_special(self.name)\n    \n    def add_to_listing(self):\n        WikiPagesBySR.add_object(self)\n    \n    def _on_create(self):\n        self.add_to_listing()\n    \n    def _on_commit(self):\n         self.add_to_listing()\n    \n    def remove_editor(self, user):\n        WikiPageEditors._remove(self._id, [user])\n    \n    def add_editor(self, user):\n        WikiPageEditors._set_values(self._id, {user: ''})\n    \n    @classmethod\n    def get_pages(cls, sr, after=None, filter_check=None):\n        NUM_AT_A_TIME = num = 1000\n        pages = []\n        while num >= NUM_AT_A_TIME:\n            wikipages = WikiPagesBySR.query([sr._id36],\n                                            after=after,\n                                            count=NUM_AT_A_TIME)\n            wikipages = list(wikipages)\n            num = len(wikipages)\n            pages += wikipages\n            after = wikipages[-1] if num else None\n        return filter(filter_check, pages)\n    \n    @classmethod\n    def get_listing(cls, sr, filter_check=None):\n        \"\"\"\n            Create a tree of pages from their path.\n        \"\"\"\n        page_tree = OrderedDict()\n        pages = cls.get_pages(sr, filter_check=filter_check)\n        pages = sorted(pages, key=lambda page: page.name)\n        for page in pages:\n            p = page.name.split('/')\n            cur_node = page_tree\n            # Loop through all elements of the path except the page name portion\n            for name in p[:-1]:\n                next_node = cur_node.get(name)\n                # If the element did not already exist in the tree, create it\n                if not next_node:\n                    new_node = OrderedDict()\n                    cur_node[name] = [None, new_node]\n                else:\n                    # Otherwise, continue through\n                    new_node = next_node[1]\n                cur_node = new_node\n            # Get the actual page name portion of the path\n            pagename = p[-1]\n            node = cur_node.get(pagename)\n            # The node may already exist as a path name in the tree\n            if node:\n                node[0] = page\n            else:\n                cur_node[pagename] = [page, OrderedDict()]\n\n        return page_tree, pages\n    \n    def get_editor_accounts(self):\n        editors = self.get_editors()\n        accounts = [Account._byID36(editor, data=True)\n                    for editor in self.get_editors()]\n        accounts = [account for account in accounts\n                    if not account._deleted]\n        return accounts\n    \n    def get_editors(self, properties=None):\n        try:\n            return WikiPageEditors._byID(self._id, properties=properties)._values().keys() or []\n        except tdb_cassandra.NotFoundException:\n            return []\n    \n    def has_editor(self, editor):\n        return bool(self.get_editors(properties=[editor]))\n    \n    def revise(self, content, previous = None, author=None, force=False, reason=None):\n        if content is None:\n            content = \"\"\n        if self.content == content:\n            return\n        force = True if previous is None else force\n        max_length = special_length_restrictions_bytes.get(self.name, MAX_PAGE_LENGTH_BYTES)\n        if len(content) > max_length:\n            raise ContentLengthError(max_length)\n        \n        revision = getattr(self, 'revision', None)\n        \n        if not force and (revision and previous != revision):\n            if previous:\n                origcontent = WikiRevision.get(previous, pageid=self._id).content\n            else:\n                origcontent = ''\n            try:\n                content = threewaymerge(origcontent, content, self.content)\n            except ConflictException as e:\n                e.new_id = revision\n                raise e\n        \n        wr = WikiRevision.create(self._id, content, author, reason)\n        self.content = content\n        self.last_edit_by = author\n        self.last_edit_date = wr.date\n        self.revision = str(wr._id)\n        self._commit()\n\n        LastModified.touch(self._fullname, \"Edit\")\n\n        return wr\n    \n    def change_permlevel(self, permlevel, force=False):\n        NUM_PERMLEVELS = 3\n        if permlevel == self.permlevel:\n            return\n        if not force and int(permlevel) not in range(NUM_PERMLEVELS):\n            raise ValueError('Permlevel not valid')\n        self.permlevel = permlevel\n        self._commit()\n\n    def get_revisions(self, after=None, count=100):\n        return WikiRevisionHistoryByPage.query(\n            rowkeys=[self._id], after=after, count=count)\n\n\nclass WikiRevisionHistoryByPage(tdb_cassandra.View):\n    \"\"\"Create a time ordered index of revisions for a wiki page\"\"\"\n    _use_db = True\n    _connection_pool = 'main'\n    _view_of = WikiRevision\n    _compare_with = TIME_UUID_TYPE\n\n    @classmethod\n    def _rowkey(cls, wikirevision):\n        return wikirevision.pageid\n\n    @classmethod\n    def _obj_to_column(cls, wikirevision):\n        return {wikirevision._id: ''}\n\n\nclass WikiPagesBySR(tdb_cassandra.DenormalizedView):\n    \"\"\" Associate revisions with subreddits, store only recent \"\"\"\n    _use_db = True\n    _connection_pool = 'main'\n    _view_of = WikiPage\n    \n    @classmethod\n    def _rowkey(cls, wp):\n        return wp.sr\n\nclass WikiRevisionsRecentBySR(tdb_cassandra.DenormalizedView):\n    \"\"\" Associate revisions with subreddits, store only recent \"\"\"\n    _use_db = True\n    _connection_pool = 'main'\n    _view_of = WikiRevision\n    _compare_with = TIME_UUID_TYPE\n    _ttl = timedelta(days=WIKI_RECENT_DAYS)\n    \n    @classmethod\n    def _rowkey(cls, wr):\n        return wr.sr\n\n\nclass ImagesByWikiPage(tdb_cassandra.View):\n    _use_db = True\n    _read_consistency_level = tdb_cassandra.CL.QUORUM\n    _write_consistency_level = tdb_cassandra.CL.QUORUM\n    _extra_schema_creation_args = {\n        \"key_validation_class\": pycassa.types.AsciiType(),\n        \"column_name_class\": pycassa.types.UTF8Type(),\n        \"default_validation_class\": pycassa.types.UTF8Type(),\n    }\n\n    @classmethod\n    def add_image(cls, sr, page_name, image_name, url):\n        rowkey = WikiPage.id_for(sr, page_name)\n        cls._set_values(rowkey, {image_name: url})\n\n    @classmethod\n    def get_images(cls, sr, page_name):\n        try:\n            rowkey = WikiPage.id_for(sr, page_name)\n            return cls._byID(rowkey)._values()\n        except tdb_cassandra.NotFound:\n            return {}\n\n    @classmethod\n    def get_image_count(cls, sr, page_name):\n        rowkey = WikiPage.id_for(sr, page_name)\n        return cls._cf.get_count(rowkey,\n            read_consistency_level=cls._read_consistency_level)\n\n    @classmethod\n    def delete_image(cls, sr, page_name, image_name):\n        rowkey = WikiPage.id_for(sr, page_name)\n        cls._remove(rowkey, [image_name])\n\n\nclass WikiPageIniItem(object):\n    _bool_values = (\"is_enabled\", \"is_new\")\n\n    @classmethod\n    def get_all(cls, return_dict=False):\n        items = OrderedDict()\n        try:\n            wp = WikiPage.get(*cls._get_wiki_config())\n        except NotFound:\n            return items if return_dict else items.values()\n        wp_content = StringIO(wp.content)\n        cfg = SafeConfigParser(allow_no_value=True)\n        cfg.readfp(wp_content)\n\n        for section in cfg.sections():\n            def_values = {'id': section}\n            for name, value in cfg.items(section):\n                # coerce boolean variables\n                if name in cls._bool_values:\n                    def_values[name] = cfg.getboolean(section, name)\n                else:\n                    def_values[name] = value\n\n            try:\n                item = cls(**def_values)\n            except TypeError:\n                # a required variable wasn't set for this item, skip\n                continue\n\n            if item.is_enabled:\n                items[section] = item\n        \n        return items if return_dict else items.values()\n"
  },
  {
    "path": "r2/r2/public/static/button/button-embed.js",
    "content": "var buttonEmbed = (function() {\n  var baseUrl = \"//www.reddit.com\"\n  var apiUrl = \"//buttons.reddit.com\"\n  var logo = $q('a.logo')\n  var up = $q('a.up')\n  var down = $q('a.down')\n  var submission = $q('a.submission-details')\n  var query = getQueryParams()\n\n  function $q(s) {\n    return document.querySelector(s)\n  }\n\n  function getQueryParams() {\n    var params = {}\n    var segments = window.location.search.substring(1).split('&')\n\n    for (var i=0; i < segments.length; i++) {\n      var pair = segments[i].split('=')\n      params[pair[0]] = decodeURIComponent(pair[1])\n    }\n\n    return params\n  }\n\n  function pointLabel(x) {\n    x = parseInt(x, 10)\n    return x + \" <span class='points-label'>point\" + (x !== 1 ? \"s\" : \"\") + \"</span>\"\n  }\n\n  function submitUrl() {\n    var url = baseUrl\n\n    if (query.sr) {\n      url += '/r/' + encodeURIComponent(query.sr)\n    }\n\n    url += '/submit?url=' + encodeURIComponent(query.url)\n\n    if (query.title) {\n      url += '&title=' + encodeURIComponent(query.title)\n    }\n\n    return url\n  }\n\n  function parseSubmission(response) {\n    if (response.data && response.data.children.length > 0) {\n      var child = response.data.children[0];\n\n      submission.href = baseUrl + child.data.permalink;\n      submission.innerHTML = pointLabel(child.data.score);\n      submission.className += ' has-points';\n      logo.href = up.href = down.href = submission.href;\n    } else {\n      submission.innerHTML = 'submit';\n    }\n  }\n\n  function loadSubmission() {\n    var script = document.createElement('script');\n    script.type = 'text/javascript';\n    script.src = apiUrl + '/button_info.json?jsonp=buttonEmbed.parseSubmission&url=' + encodeURIComponent(query.url);\n    document.body.appendChild(script);\n  }\n\n  function safeColor(colorString) {\n    var match = colorString.match(/([A-F0-9]{6}|[A-F0-9]{3})/i)\n    if (match) {\n      return '#' + match[0]\n    }\n    return null\n  }\n\n  function applyParams() {\n    if (query.bgcolor) {\n      document.body.style.backgroundColor = safeColor(query.bgcolor)\n    }\n\n    if (query.bordercolor) {\n      $q('.wrap').style.borderColor = safeColor(query.bordercolor)\n    }\n\n    var links = document.getElementsByTagName('a')\n    for (var i=0; i < links.length; i++) {\n      links[i].target = query.newwindow ? \"_blank\" : \"_top\"\n    }\n  }\n\n  function init() {\n    submission.href = logo.href = up.href = down.href = submitUrl()\n    applyParams()\n    loadSubmission()\n  }\n\n  return {\n    init: init,\n    parseSubmission: parseSubmission\n  }\n}())\n\nbuttonEmbed.init()\n"
  },
  {
    "path": "r2/r2/public/static/button/button1.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <title></title>\n    <style type=\"text/css\">\n    html, body, div, span {\n      margin: 0;\n      padding: 0;\n    }\n\n    body {\n      height: 22px;\n      width: 120px;\n      white-space: nowrap;\n      font-size: 10px;\n      font-family: verdana, arial, sans-serif;\n    }\n\n    .wrap {\n      height: 18px;\n      border-radius: 2px;\n      border: 1px solid #C7DEF7;\n    }\n\n    .logo {\n      background: #C7DEF7 url(data:image/svg+xml;charset=utf-8;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+DQo8c3ZnIHdpZHRoPSIyOXB4IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyOSAyNSIgdmVyc2lvbj0iMS4xIiBoZWlnaHQ9IjI1cHgiPg0KIDxnIGZpbGwtcnVsZT0iZXZlbm9kZCIgZmlsbD0ibm9uZSI+DQogIDxnIHN0cm9rZS13aWR0aD0iMS4xIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtNDYpIj4NCiAgIDxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKDQ3IDEpIj4NCiAgICA8Y2lyY2xlIHN0cm9rZT0iIzAwMCIgY3g9IjIuNzAyIiByPSIyLjcwMiIgY3k9IjEwLjcwMiIgZmlsbD0iI2ZmZiIvPg0KICAgIDxjaXJjbGUgc3Ryb2tlPSIjMDAwIiBjeD0iMjQuNzAyIiByPSIyLjcwMiIgY3k9IjEwLjcwMiIgZmlsbD0iI2ZmZiIvPg0KICAgIDxwYXRoIHN0cm9rZS1saW5lam9pbj0icm91bmQiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBkPSJtMjEuMjI2IDEuMzUybC01LjQwNC0xLjI3Mi0xLjk4NiA3LjI3OCIvPg0KICAgIDxjaXJjbGUgc3Ryb2tlPSIjMDAwIiBjeD0iMjMuMTMiIHI9IjIuMTMiIGN5PSIyLjEzIiBmaWxsPSIjZmZmIi8+DQogICAgPGVsbGlwc2UgY3k9IjE0Ljk4NyIgcng9IjEyLjIzOCIgcnk9IjcuOTg3IiBzdHJva2U9IiMwMDAiIGN4PSIxMy4yMzgiIGZpbGw9IiNmZmYiLz4NCiAgICA8ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSg4IDEyKSI+DQogICAgIDxjaXJjbGUgc3Ryb2tlPSIjRkY0NTAwIiBjeD0iMSIgcj0iMS40MyIgY3k9IjEuNDMiIGZpbGw9IiNGRjQ1MDAiLz4NCiAgICAgPGNpcmNsZSBzdHJva2U9IiNGRjQ1MDAiIGN4PSIxMCIgcj0iMS40MzEiIGN5PSIxLjQzMSIgZmlsbD0iI0ZGNDUwMCIvPg0KICAgICA8cGF0aCBzdHJva2U9IiMwMDAiIGQ9Im0xLjUwNyA2LjIyOWMxLjA2NiAxLjA2NiAyLjc4NiAxLjI3MSA0LjIxMiAxLjI3MW00LjI0NC0xLjI3MWMtMS4wNjcgMS4wNjYtMi43ODYgMS4yNzEtNC4yMTMgMS4yNzEiLz4NCiAgICA8L2c+DQogICA8L2c+DQogIDwvZz4NCiA8L2c+DQo8L3N2Zz4=) center left no-repeat;\n      background-size: 16px;\n      width: 18px;\n      height: 18px;\n      display: block;\n      text-indent: -9999px;\n      float: left;\n    }\n\n    .votebox {\n      height: 18px;\n      background-color: white;\n      margin-left: 2px;\n    }\n\n    .vote {\n      text-indent: -9999px;\n      float: left;\n      margin: 0 2px;\n      width: 0;\n      height: 0;\n      border: 8px solid transparent;\n      position: relative;\n    }\n\n    .vote.up {\n      top: -7px;\n      border-bottom-color: #c6c6c6;\n    }\n\n    .vote.down {\n      bottom: -7px;\n      border-top-color: #c6c6c6;\n    }\n\n    .vote:before {\n      width: 6px;\n      height: 8px;\n      right: -3px;\n      content: \"\";\n      position: absolute;\n      display: block;\n    }\n\n    .vote.up:before {\n      top: 7px;\n      background-color: #c6c6c6;\n      background: linear-gradient(to bottom, #c6c6c6 0%, rgba(198, 198, 198, .9), rgba(198, 198, 198, 0) 100%);\n      background: -webkit-linear-gradient(to bottom, #c6c6c6 0%, rgba(198, 198, 198, .9), rgba(198, 198, 198, 0) 100%);\n      background: -o-linear-gradient(to bottom, #c6c6c6 0%, rgba(198, 198, 198, .9), rgba(198, 198, 198, 0) 100%);\n    }\n\n    .vote.down:before {\n      top: -14px;\n      background-color: #c6c6c6;\n      background: linear-gradient(to top, #c6c6c6 0%, rgba(198, 198, 198, .9), rgba(198, 198, 198, 0) 100%);\n      background: -webkit-linear-gradient(to top, #c6c6c6 0%, rgba(198, 198, 198, .9), rgba(198, 198, 198, 0) 100%);\n      background: -o-linear-gradient(to top, #c6c6c6 0%, rgba(198, 198, 198, .9), rgba(198, 198, 198, 0) 100%);\n    }\n\n    .vote.up.active {\n     border-bottom-color: #ff8b60;\n    }\n\n    .vote.down.active {\n     border-top-color: #9494ff;\n    }\n\n    .vote.down.active:before {\n      background-color: #9494ff;\n      background: linear-gradient(to top, #9494ff 0%, rgba(148, 148, 255, .9), rgba(148, 148, 255, 0) 100%);\n      background: -webkit-linear-gradient(to top, #9494ff 0%, rgba(148, 148, 255, .9), rgba(148, 148, 255, 0) 100%);\n      background: -o-linear-gradient(to top, #9494ff 0%, rgba(148, 148, 255, .9), rgba(148, 148, 255, 0) 100%);\n    }\n\n    .vote.up.active:before {\n      background-color: #ff8b60;\n      background: linear-gradient(to bottom, #ff8b60 0%, rgba(255, 139, 96, .9), rgba(255, 139, 96, 0) 100%);\n      background: -webkit-linear-gradient(to bottom, #ff8b60 0%, rgba(255, 139, 96, .9), rgba(255, 139, 96, 0) 100%);\n      background: -o-linear-gradient(to bottom, #ff8b60 0%, rgba(255, 139, 96, .9), rgba(255, 139, 96, 0) 100%);\n    }\n\n    a {\n      color: #369;\n      text-decoration: none;\n    }\n\n    a:hover {\n      text-decoration: underline;\n    }\n\n    .submission-details {\n      display: block;\n      height: 18px;\n      line-height: 18px;\n      margin-left: 46px;\n      text-align: center;\n      width: 65px;\n      color: #888;\n    }\n\n    </style>\n  </head>\n  <body><div class=\"wrap\"><a class=\"logo\" href=\"#\">reddit</a><div class=\"votebox\"><a class=\"vote up\" href=\"#\">Upvote</a><a class=\"vote down\" href=\"#\">Downvote</a><a class=\"submission-details\" href=\"#\"></a></div></div>\n  <script type=\"text/javascript\" src=\"button-embed.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "r2/r2/public/static/button/button1.js",
    "content": "(function() {\n  var write_string=\"<iframe src=\\\"//www.redditstatic.com/button/button1.html?url=\";\n\n  if (window.reddit_url)  { \n      write_string += encodeURIComponent(reddit_url); \n  }\n  else { \n      write_string += encodeURIComponent(window.location.href);\n  }\n  if (window.reddit_title) {\n       write_string += '&title=' + encodeURIComponent(window.reddit_title);\n  }\n  if (window.reddit_target) {\n       write_string += '&sr=' + encodeURIComponent(window.reddit_target);\n  }\n  if (window.reddit_css) {\n      write_string += '&css=' + encodeURIComponent(window.reddit_css);\n  }\n  if (window.reddit_bgcolor) {\n      write_string += '&bgcolor=' + encodeURIComponent(window.reddit_bgcolor); \n  }\n  if (window.reddit_bordercolor) {\n      write_string += '&bordercolor=' + encodeURIComponent(window.reddit_bordercolor); \n  }\n  if (window.reddit_newwindow) { \n      write_string += '&newwindow=' + encodeURIComponent(window.reddit_newwindow);}\n  write_string += \"\\\" height=\\\"22\\\" width=\\\"120\\\" scrolling='no' frameborder='0'></iframe>\";\n  document.write(write_string);\n})()\n"
  },
  {
    "path": "r2/r2/public/static/button/button2.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <title></title>\n    <style type=\"text/css\">\n    html, body, div, span {\n      margin: 0;\n      padding: 0;\n    }\n\n    body {\n      height: 69px;\n      width: 51px;\n      white-space: nowrap;\n      font-size: 10px;\n      font-family: verdana, arial, sans-serif;\n      overflow: hidden;\n      position: absolute;\n    }\n\n    .wrap {\n      position: absolute;\n      width: 49px;\n      height: 67px;\n      border-radius: 2px;\n      border: 1px solid #C7DEF7;\n      background-color: white;\n    }\n\n    .logo {\n      display: block;\n      position: absolute;\n      bottom: 0;\n      width: 100%;\n      text-align: center;\n      background-color: #c7def7;\n      font-size: 13px;\n    }\n\n    .votebox {\n      height: 57px;\n      background-color: white;\n      text-align: center;\n    }\n\n    .vote {\n      text-indent: -9999px;\n      display: block;\n      width: 0;\n      height: 0;\n      border: 8px solid transparent;\n      position: absolute;\n      left: 17px;\n    }\n\n    .vote.up {\n      top: -6px;\n      border-bottom-color: #c6c6c6;\n    }\n\n    .vote.down {\n      top: 40px;\n      border-top-color: #c6c6c6;\n    }\n\n    .vote:before {\n      width: 6px;\n      height: 8px;\n      right: -3px;\n      content: \"\";\n      position: absolute;\n      display: block;\n    }\n\n    .vote.up:before {\n      top: 7px;\n      background-color: #c6c6c6;\n      background: linear-gradient(to bottom, #c6c6c6 0%, rgba(198, 198, 198, .9), rgba(198, 198, 198, 0) 100%);\n      background: -webkit-linear-gradient(to bottom, #c6c6c6 0%, rgba(198, 198, 198, .9), rgba(198, 198, 198, 0) 100%);\n      background: -o-linear-gradient(to bottom, #c6c6c6 0%, rgba(198, 198, 198, .9), rgba(198, 198, 198, 0) 100%);\n    }\n\n    .vote.down:before {\n      top: -14px;\n      background-color: #c6c6c6;\n      background: linear-gradient(to top, #c6c6c6 0%, rgba(198, 198, 198, .9), rgba(198, 198, 198, 0) 100%);\n      background: -webkit-linear-gradient(to top, #c6c6c6 0%, rgba(198, 198, 198, .9), rgba(198, 198, 198, 0) 100%);\n      background: -o-linear-gradient(to top, #c6c6c6 0%, rgba(198, 198, 198, .9), rgba(198, 198, 198, 0) 100%);\n    }\n\n    .vote.up.active {\n     border-bottom-color: #ff8b60;\n    }\n\n    .vote.down.active {\n     border-top-color: #9494ff;\n    }\n\n    .vote.down.active:before {\n      background-color: #9494ff;\n      background: linear-gradient(to top, #9494ff 0%, rgba(148, 148, 255, .9), rgba(148, 148, 255, 0) 100%);\n      background: -webkit-linear-gradient(to top, #9494ff 0%, rgba(148, 148, 255, .9), rgba(148, 148, 255, 0) 100%);\n      background: -o-linear-gradient(to top, #9494ff 0%, rgba(148, 148, 255, .9), rgba(148, 148, 255, 0) 100%);\n    }\n\n    .vote.up.active:before {\n      background-color: #ff8b60;\n      background: linear-gradient(to bottom, #ff8b60 0%, rgba(255, 139, 96, .9), rgba(255, 139, 96, 0) 100%);\n      background: -webkit-linear-gradient(to bottom, #ff8b60 0%, rgba(255, 139, 96, .9), rgba(255, 139, 96, 0) 100%);\n      background: -o-linear-gradient(to bottom, #ff8b60 0%, rgba(255, 139, 96, .9), rgba(255, 139, 96, 0) 100%);\n    }\n\n    a {\n      color: #369;\n      text-decoration: none;\n    }\n\n    a:hover {\n      text-decoration: underline;\n    }\n\n    .submission-details {\n      position: absolute;\n      top: 17px;\n      left: 0;\n      width: 49px;\n      height: 18px;\n      line-height: 18px;\n      text-align: center;\n    }\n\n    .submission-details.has-points {\n      color: #888;\n      font-size: 13px;\n    }\n\n    .points-label {\n      display: none;\n    }\n\n    </style>\n  </head>\n  <body><div class=\"wrap\"><a class=\"logo\" href=\"#\">reddit</a><div class=\"votebox\"><a class=\"vote up\" href=\"#\">Upvote</a><a class=\"vote down\" href=\"#\">Downvote</a><a class=\"submission-details\" href=\"#\"></a></div></div>\n  <script type=\"text/javascript\" src=\"button-embed.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "r2/r2/public/static/button/button2.js",
    "content": "(function() {\n  var write_string=\"<iframe src=\\\"//www.redditstatic.com/button/button2.html?url=\";\n\n  if (window.reddit_url)  { \n      write_string += encodeURIComponent(reddit_url); \n  }\n  else { \n      write_string += encodeURIComponent(window.location.href);\n  }\n  if (window.reddit_title) {\n       write_string += '&title=' + encodeURIComponent(window.reddit_title);\n  }\n  if (window.reddit_target) {\n       write_string += '&sr=' + encodeURIComponent(window.reddit_target);\n  }\n  if (window.reddit_css) {\n      write_string += '&css=' + encodeURIComponent(window.reddit_css);\n  }\n  if (window.reddit_bgcolor) {\n      write_string += '&bgcolor=' + encodeURIComponent(window.reddit_bgcolor); \n  }\n  if (window.reddit_bordercolor) {\n      write_string += '&bordercolor=' + encodeURIComponent(window.reddit_bordercolor); \n  }\n  if (window.reddit_newwindow) { \n      write_string += '&newwindow=' + encodeURIComponent(window.reddit_newwindow);}\n  write_string += \"\\\" height=\\\"69\\\" width=\\\"51\\\" scrolling='no' frameborder='0'></iframe>\";\n  document.write(write_string);\n})()\n"
  },
  {
    "path": "r2/r2/public/static/button/button3.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <title></title>\n    <style type=\"text/css\">\n    html, body, div, span {\n      margin: 0;\n      padding: 0;\n    }\n\n    body {\n      height: 52px;\n      width: 69px;\n      white-space: nowrap;\n      font-size: 10px;\n      font-family: verdana, arial, sans-serif;\n      overflow: hidden;\n      position: absolute;\n    }\n\n    .wrap {\n      position: absolute;\n      width: 69px;\n      height: 52px;\n      border-radius: 2px;\n    }\n\n    .logo {\n      background: transparent url(data:image/svg+xml;charset=utf-8;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyOXB4IiBoZWlnaHQ9IjQwcHgiIHZpZXdCb3g9IjAgMCAyOSA0MCIgdmVyc2lvbj0iMS4xIj48ZyBzdHJva2U9Im5vbmUiIHN0cm9rZS13aWR0aD0iMSIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIj48ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSgxLCAxKSI+PGVsbGlwc2Ugc3Ryb2tlPSIjMDAwMDAwIiBzdHJva2Utd2lkdGg9IjEuMSIgZmlsbD0iI0ZGRkZGRiIgY3g9IjE4LjQxMSIgY3k9IjI3LjMyNCIgcng9IjQuNDExIiByeT0iNS4zMjQiLz48ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSg1LCAzNSkiPjxwYXRoIGQ9Ik02LjMwMyAzLjI4NUM2LjM2NiAzLjA4OCA2LjQxMSAyLjg4NSA2LjQxMSAyLjY3IDYuNDExIDEuMzExIDUuMDUgMC4yMDcgMy4zNzEgMC4yMDcgMS42OTIgMC4yMDcgMC4zMzIgMS4zMTEgMC4zMzIgMi42NyAwLjMzMiAyLjg4NSAwLjM3NiAzLjA4OCAwLjQ0IDMuMjg1TDYuMzAzIDMuMjg1IDYuMzAzIDMuMjg1WiIgZmlsbD0iI0ZGRkZGRiIvPjxwYXRoIGQ9Ik02LjMwMyAzLjI4NUM2LjM2NiAzLjA4OCA2LjQxMSAyLjg4NSA2LjQxMSAyLjY3IDYuNDExIDEuMzExIDUuMDUgMC4yMDcgMy4zNzEgMC4yMDcgMS42OTIgMC4yMDcgMC4zMzIgMS4zMTEgMC4zMzIgMi42NyAwLjMzMiAyLjg4NSAwLjM3NiAzLjA4OCAwLjQ0IDMuMjg1TDYuMzAzIDMuMjg1IDYuMzAzIDMuMjg1WiIgc3Ryb2tlPSIjMDAwMDAwIiBzdHJva2Utd2lkdGg9IjEuMSIvPjwvZz48ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSgxNiwgMzUpIj48cGF0aCBkPSJNNS45NDkgMy4yODVDNi4wMTEgMy4wODggNi4wNTYgMi44ODUgNi4wNTYgMi42NyA2LjA1NiAxLjMxMSA0LjY5NSAwLjIwNyAzLjAxNSAwLjIwNyAxLjMzNiAwLjIwNy0wLjAyNCAxLjMxMS0wLjAyNCAyLjY3IC0wLjAyNCAyLjg4NSAwLjAxOSAzLjA4OCAwLjA4MyAzLjI4NUw1Ljk0OSAzLjI4NSA1Ljk0OSAzLjI4NVoiIGZpbGw9IiNGRkZGRkYiLz48cGF0aCBkPSJNNS45NDkgMy4yODVDNi4wMTEgMy4wODggNi4wNTYgMi44ODUgNi4wNTYgMi42NyA2LjA1NiAxLjMxMSA0LjY5NSAwLjIwNyAzLjAxNSAwLjIwNyAxLjMzNiAwLjIwNy0wLjAyNCAxLjMxMS0wLjAyNCAyLjY3IC0wLjAyNCAyLjg4NSAwLjAxOSAzLjA4OCAwLjA4MyAzLjI4NUw1Ljk0OSAzLjI4NSA1Ljk0OSAzLjI4NVoiIHN0cm9rZT0iIzAwMDAwMCIgc3Ryb2tlLXdpZHRoPSIxLjEiLz48L2c+PGNpcmNsZSBzdHJva2U9IiMwMDAwMDAiIHN0cm9rZS13aWR0aD0iMS4xIiBmaWxsPSIjRkZGRkZGIiBjeD0iMi43MDIiIGN5PSIxMC43MDIiIHI9IjIuNzAyIi8+PGNpcmNsZSBzdHJva2U9IiMwMDAwMDAiIHN0cm9rZS13aWR0aD0iMS4xIiBmaWxsPSIjRkZGRkZGIiBjeD0iMjQuNzAyIiBjeT0iMTAuNzAyIiByPSIyLjcwMiIvPjxwYXRoIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgZD0iTTIxLjIyNiAxLjM1MkMyMS4yMjYgMS4zNTIgMjEuMjI2IDEuMzUyIDE1LjgyMiAwLjA4TDEzLjgzNiA2LjM1OCIgc3Ryb2tlPSIjMDAwMDAwIiBzdHJva2Utd2lkdGg9IjEuMSIvPjxlbGxpcHNlIHN0cm9rZT0iIzAwMDAwMCIgc3Ryb2tlLXdpZHRoPSIxLjEiIGZpbGw9IiNGRkZGRkYiIGN4PSI5LjQxMSIgY3k9IjI3LjMyNCIgcng9IjQuNDExIiByeT0iNS4zMjQiLz48Y2lyY2xlIHN0cm9rZT0iIzAwMDAwMCIgc3Ryb2tlLXdpZHRoPSIxLjEiIGZpbGw9IiNGRkZGRkYiIGN4PSIyMy4xMyIgY3k9IjIuMTMiIHI9IjIuMTMiLz48ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSg4LCAxMykiPjxwYXRoIGQ9Ik00LjE4MSAyNS4yODlMNy4xNzEgMjUuMjg5QzkuODI2IDIzLjkwNiAxMS43OTUgMTguOTQ5IDExLjc5NSAxMy4wMjcgMTEuNzk1IDYuMDM3IDkuMDU1IDAuMzcyIDUuNjc2IDAuMzcyIDIuMjk2IDAuMzcyLTAuNDQzIDYuMDM4LTAuNDQzIDEzLjAyNyAtMC40NDMgMTguOTQ5IDEuNTI1IDIzLjkwNiA0LjE4MSAyNS4yODlMNC4xODEgMjUuMjg5WiIgZmlsbD0iI0ZGRkZGRiIvPjxwYXRoIGQ9Ik00LjE4MSAyNS4yODlMNy4xNzEgMjUuMjg5QzkuODI2IDIzLjkwNiAxMS43OTUgMTguOTQ5IDExLjc5NSAxMy4wMjcgMTEuNzk1IDYuMDM3IDkuMDU1IDAuMzcyIDUuNjc2IDAuMzcyIDIuMjk2IDAuMzcyLTAuNDQzIDYuMDM4LTAuNDQzIDEzLjAyNyAtMC40NDMgMTguOTQ5IDEuNTI1IDIzLjkwNiA0LjE4MSAyNS4yODlMNC4xODEgMjUuMjg5WiIgc3Ryb2tlPSIjMDAwMDAwIiBzdHJva2Utd2lkdGg9IjEuMSIvPjwvZz48ZWxsaXBzZSBzdHJva2U9IiMwMDAwMDAiIHN0cm9rZS13aWR0aD0iMS4xIiBmaWxsPSIjRkZGRkZGIiBjeD0iMTMuMjM4IiBjeT0iMTQuOTg3IiByeD0iMTIuMjM4IiByeT0iNy45ODciLz48cGF0aCBkPSJNMTAuNDU3IDM4LjI4NUwxNy4wNTMgMzguMjg1IiBzdHJva2U9IiMwMDAwMDAiIHN0cm9rZS13aWR0aD0iMS4xIi8+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoOCwgMTIpIiBzdHJva2Utd2lkdGg9IjEuMSI+PGNpcmNsZSBzdHJva2U9IiNGRjQ1MDAiIGZpbGw9IiNGRjQ1MDAiIGN4PSIxLjQzIiBjeT0iMS40MyIgcj0iMS40MyIvPjxjaXJjbGUgc3Ryb2tlPSIjRkY0NTAwIiBmaWxsPSIjRkY0NTAwIiBjeD0iMTAuNDMxIiBjeT0iMS40MzEiIHI9IjEuNDMxIi8+PHBhdGggZD0iTTEuNTA3IDYuMjI5QzIuNTczIDcuMjk1IDQuMjkzIDcuNSA1LjcxOSA3LjUiIHN0cm9rZT0iIzAwMDAwMCIvPjxwYXRoIGQ9Ik05Ljk2MyA2LjIyOUM4Ljg5NiA3LjI5NSA3LjE3NyA3LjUgNS43NSA3LjUiIHN0cm9rZT0iIzAwMDAwMCIvPjwvZz48L2c+PC9nPjwvc3ZnPg==) center right no-repeat;\n      background-size: 26px;\n      width: 32px;\n      height: 43px;\n      display: block;\n      text-indent: -9999px;\n      position: absolute;\n      right: 3px;\n      top: 3px;\n    }\n\n    .votebox {\n      height: 52px;\n      text-align: center;\n    }\n\n    .vote {\n      text-indent: -9999px;\n      display: block;\n      width: 0;\n      height: 0;\n      border: 8px solid transparent;\n      position: absolute;\n      left: 10px;\n    }\n\n    .vote.up {\n      top: -6px;\n      border-bottom-color: #c6c6c6;\n    }\n\n    .vote.down {\n      top: 40px;\n      border-top-color: #c6c6c6;\n    }\n\n    .vote:before {\n      width: 6px;\n      height: 8px;\n      right: -3px;\n      content: \"\";\n      position: absolute;\n      display: block;\n    }\n\n    .vote.up:before {\n      top: 7px;\n      background-color: #c6c6c6;\n      background: linear-gradient(to bottom, #c6c6c6 0%, rgba(198, 198, 198, .9), rgba(198, 198, 198, 0) 100%);\n      background: -webkit-linear-gradient(to bottom, #c6c6c6 0%, rgba(198, 198, 198, .9), rgba(198, 198, 198, 0) 100%);\n      background: -o-linear-gradient(to bottom, #c6c6c6 0%, rgba(198, 198, 198, .9), rgba(198, 198, 198, 0) 100%);\n    }\n\n    .vote.down:before {\n      top: -14px;\n      background-color: #c6c6c6;\n      background: linear-gradient(to top, #c6c6c6 0%, rgba(198, 198, 198, .9), rgba(198, 198, 198, 0) 100%);\n      background: -webkit-linear-gradient(to top, #c6c6c6 0%, rgba(198, 198, 198, .9), rgba(198, 198, 198, 0) 100%);\n      background: -o-linear-gradient(to top, #c6c6c6 0%, rgba(198, 198, 198, .9), rgba(198, 198, 198, 0) 100%);\n    }\n\n    .vote.up.active {\n     border-bottom-color: #ff8b60;\n    }\n\n    .vote.down.active {\n     border-top-color: #9494ff;\n    }\n\n    .vote.down.active:before {\n      background-color: #9494ff;\n      background: linear-gradient(to top, #9494ff 0%, rgba(148, 148, 255, .9), rgba(148, 148, 255, 0) 100%);\n      background: -webkit-linear-gradient(to top, #9494ff 0%, rgba(148, 148, 255, .9), rgba(148, 148, 255, 0) 100%);\n      background: -o-linear-gradient(to top, #9494ff 0%, rgba(148, 148, 255, .9), rgba(148, 148, 255, 0) 100%);\n    }\n\n    .vote.up.active:before {\n      background-color: #ff8b60;\n      background: linear-gradient(to bottom, #ff8b60 0%, rgba(255, 139, 96, .9), rgba(255, 139, 96, 0) 100%);\n      background: -webkit-linear-gradient(to bottom, #ff8b60 0%, rgba(255, 139, 96, .9), rgba(255, 139, 96, 0) 100%);\n      background: -o-linear-gradient(to bottom, #ff8b60 0%, rgba(255, 139, 96, .9), rgba(255, 139, 96, 0) 100%);\n    }\n\n    a {\n      color: #369;\n      text-decoration: none;\n    }\n\n    a:hover {\n      text-decoration: underline;\n    }\n\n    .submission-details {\n      position: absolute;\n      top: 17px;\n      left: 0;\n      width: 37px;\n      height: 18px;\n      line-height: 18px;\n      text-align: center;\n    }\n\n    .submission-details.has-points {\n      color: #888;\n      font-size: 13px;\n    }\n\n    .points-label {\n      display: none;\n    }\n\n    </style>\n  </head>\n  <body><div class=\"wrap\"><a class=\"logo\" href=\"#\">reddit</a><div class=\"votebox\"><a class=\"vote up\" href=\"#\">Upvote</a><a class=\"vote down\" href=\"#\">Downvote</a><a class=\"submission-details\" href=\"#\"></a></div></div>\n  <script type=\"text/javascript\" src=\"button-embed.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "r2/r2/public/static/button/button3.js",
    "content": "(function() {\n  var write_string=\"<iframe src=\\\"//www.redditstatic.com/button/button3.html?url=\";\n\n  if (window.reddit_url)  { \n      write_string += encodeURIComponent(reddit_url); \n  }\n  else { \n      write_string += encodeURIComponent(window.location.href);\n  }\n  if (window.reddit_title) {\n       write_string += '&title=' + encodeURIComponent(window.reddit_title);\n  }\n  if (window.reddit_target) {\n       write_string += '&sr=' + encodeURIComponent(window.reddit_target);\n  }\n  if (window.reddit_css) {\n      write_string += '&css=' + encodeURIComponent(window.reddit_css);\n  }\n  if (window.reddit_bgcolor) {\n      write_string += '&bgcolor=' + encodeURIComponent(window.reddit_bgcolor); \n  }\n  if (window.reddit_bordercolor) {\n      write_string += '&bordercolor=' + encodeURIComponent(window.reddit_bordercolor); \n  }\n  if (window.reddit_newwindow) { \n      write_string += '&newwindow=' + encodeURIComponent(window.reddit_newwindow);}\n  write_string += \"\\\" height=\\\"52\\\" width=\\\"69\\\" scrolling='no' frameborder='0'></iframe>\";\n  document.write(write_string);\n})()\n"
  },
  {
    "path": "r2/r2/public/static/css/adminbar.less",
    "content": ".transition (@property, @duration, @function: ease, @delay: 0s) {\n    -webkit-transition: @arguments;\n    -moz-transition: @arguments;\n    -o-transition: @arguments;\n    -ms-transition: @arguments;\n    transition: @arguments;\n}\n\n.no-select {\n    -webkit-user-select: none;\n    -moz-user-select: none;\n    -o-user-select: none;\n    -ms-user-select: none;\n    user-select: none;\n}\n\n@debug-color: #ee0;\n@debug-dark-color: #990;\n@admin-color: #e00;\n@admin-dark-color: #900;\n#admin-bar {\n    position: fixed;\n    top: 0;\n    left: 0;\n    right: 0;\n    z-index: 100;\n\n    .show-button {\n        display: none;\n    }\n    &.hidden {\n        .status-bar, .timings-bar {\n            display: none !important;\n        }\n\n        .show-button {\n            display: block;\n            &:before, &:after {\n                content: '';\n                border: 0 solid transparent;\n                position: absolute;\n                display: block;\n                top: 0;\n            }\n\n            &:before {\n                @size: 12px;\n                right: -@size;\n                border-width: @size;\n                z-index: 101;\n            }\n\n            &:after {\n                @size: 18px;\n                right: -@size;\n                border-width: @size;\n                border-top-color: gray;\n                z-index: 100;\n            }\n        }\n\n        &.debug {\n            .show-button:before {\n                border-top-color: @debug-color;\n            }\n            .show-button:after {\n                border-top-color: @debug-dark-color;\n            }\n        }\n\n        &.admin {\n            .show-button:before {\n                border-top-color: @admin-color;\n            }\n            .show-button:after {\n                border-top-color: @admin-dark-color;\n            }\n        }\n    }\n\n    .status-bar {\n        font-size: 12px;\n        height: 14px;\n        line-height: 14px;\n        padding: 2px 5px;\n        border-bottom: 1px solid gray;\n        background: white;\n        z-index: 9999;\n        white-space: nowrap;\n        overflow: hidden;\n        box-shadow: 0 -5px 15px #cee3f8 inset;\n        .no-select;\n\n        .caption {\n            font-weight: bold;\n            color: gray;\n        }\n\n        .indicator {\n            margin-left: 14px;\n        }\n\n        .indicator .icon {\n            display: inline-block;\n            width: 12px;\n            height: 12px;\n            border-radius: 2px;\n            vertical-align: bottom;\n            margin-right: 5px;\n            border: 1px solid black;\n        }\n\n        .indicator.debug .icon {\n            background: @debug-color;\n            border-color: @debug-dark-color;\n        }\n\n        .indicator.admin .icon {\n            background: @admin-color;\n            border-color: @admin-dark-color;\n        }\n\n        .indicator.secure .icon {\n            background: #8e0;\n            border-color: #890;\n        }\n\n        .indicator.dev-statics .icon {\n            background: #9ff;\n            border-color: #089;\n            border-top-width: 5px;\n            border-bottom-width: 5px;\n            height: 4px;\n        }\n\n        .indicator.prod-statics .icon {\n            background: #0ee;\n            border-color: #089;\n        }\n\n        .indicator.disabled {\n            opacity: .5;\n        }\n\n        .indicator.disabled .icon {\n            display: none;\n        }\n\n        .admin-off {\n            margin-left: 8px;\n            padding: 0 4px;\n            background: fade(@admin-dark-color, 5%);\n            border: 1px solid fade(@admin-dark-color, 40%);\n            border-radius: 3px;\n            font-size: 11px;\n            color: @admin-dark-color;\n            cursor: pointer;\n            vertical-align: top;\n        }\n\n        .controls {\n            position: absolute;\n            right: 0;\n\n            & > span {\n                border-left: 1px solid #ccc;\n                padding: 0 7px;\n                color: #369;\n                cursor: default;\n\n                .state {\n                    display: inline-block;\n                    width: 1em;\n                }\n            }\n\n            .dropdown.lightdrop .selected {\n                font-weight: normal;\n                text-decoration: none;\n                color: #369;\n            }\n        }\n    }\n    &.admin .status-bar {\n        box-shadow: 0 -5px 15px #ffcfc7 inset;\n    }\n\n    .timings-bar {\n        position: relative;\n        display: block;\n        background: #eee;\n        border-bottom: 1px solid gray;\n        box-shadow: 0 -5px 15px #ddd inset;\n\n        .expand-button {\n            display: inline-block;\n            width: 11px;\n            line-height: 11px;\n            margin: 1px 4px;\n            background: #f4f4f4;\n            border: 1px solid #bbb;\n            border-bottom-width: 2px;\n            border-radius: 2px;\n            font-size: 11px;\n            text-align: center;\n            cursor: pointer;\n            .no-select;\n        }\n\n        &.mini-timings {\n            height: 16px;\n            line-height: 16px;\n            .timeline-browser {\n                display: none;\n            }\n        }\n        &.full-timings {\n            height: 25px;\n            line-height: 25px;\n        }\n\n        .timelines {\n            position: absolute;\n            left: 20px;\n            right: 0;\n            top: 0;\n            bottom: 0;\n            cursor: -webkit-zoom-in;\n            cursor: -moz-zoom-in;\n            cursor: zoom-in;\n\n            &.zoomed {\n                cursor: -webkit-zoom-out;\n                cursor: -moz-zoom-out;\n                cursor: zoom-out;\n            }\n\n        }\n\n        .timeline {\n            position: relative;\n            margin: 1px;\n\n            .elapsed {\n                position: absolute;\n                top: 0;\n                right: 2px;\n            }\n\n            .events {\n                position: relative;\n                margin-right: 40px;\n            }\n\n            .event {\n                position: absolute;\n                top: 0;\n                bottom: 0;\n                border: 1px solid rgba(0, 0, 0, .5);\n                z-index: 2;\n\n                opacity: .65;\n                .transition(opacity, .25s);\n                &:hover {\n                    opacity: 1;\n                }\n            }\n\n            .event.out-of-bounds {\n                left: 0;\n                right: 0;\n                border: 1px solid #888;\n                background: #aaa;\n            }\n        }\n\n        .event-color(@color) {\n            background: @color;\n            border-color: darken(saturate(@color, 30%), 23%);\n            box-shadow: 0 -2px 0px darken(@color, 4%) inset;\n        }\n\n        .timeline-browser {\n            @height: 8px;\n            height: @height;\n            .events {\n                height: @height;\n            }\n\n            .elapsed {\n                font-size: 8px;\n                line-height: @height;\n            }\n\n            .event.start {\n                .event-color(rgb(20, 200, 60));\n            }\n\n            .event.redirect {\n                .event-color(rgb(20, 200, 120));\n            }\n\n            .event.https {\n                .event-color(rgb(120, 100, 120));\n            }\n\n            .event.request {\n                .event-color(rgb(20, 150, 150));\n            }\n\n            .event.response {\n                .event-color(rgb(20, 90, 250));\n            }\n\n            .event.domLoading {\n                .event-color(rgb(50, 50, 150));\n            }\n\n            .event.domInteractive {\n                .event-color(rgb(80, 50, 150));\n            }\n\n            .event.domContentLoaded {\n                .event-color(rgb(120, 50, 150));\n            }\n        }\n\n        .timeline-server {\n            @height: 14px;\n            height: @height;\n            .events {\n                height: @height;\n            }\n\n            .elapsed {\n                font-size: 10px;\n                line-height: @height;\n            }\n\n            .event.web {\n                z-index: 1;\n                .event-color(rgb(50, 150, 20));\n            }\n\n            .event.render {\n                &.nocache {\n                    .event-color(rgb(240, 200, 17));\n                }\n                &.cached {\n                    .event-color(rgb(200, 170, 50));\n                }\n            }\n\n            .event.cassandra {\n                .event-color(rgb(150, 50, 20));\n            }\n\n            .event.pg {\n                .event-color(rgb(20, 50, 150));\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "r2/r2/public/static/css/compact.css",
    "content": "/* mixins */\n/* meat */\nbody { background: #c5ccd3; font-family: Helvetica, \"Helvetica Neue\", Arial, sans-serif; margin: 0; width: 100%; height: 100%; -webkit-font-smoothing: antialiased; }\n\np { margin: 0; padding: 0; }\n\na { color: #517191; }\n\na:visited { color: #4F565B; }\n\ntextarea { font-family: inherit; }\n\n/*Preloading*/\n#preload { position: absolute; top: -1000px; left: -1000px; }\n\n/*UI stuff*/\n.newbutton { -moz-appearance: none; -webkit-appearance: none; border: 8px solid transparent; -moz-border-image: url(\"../compact/border-button.png\") 8 fill; -o-border-image: url(\"../compact/border-button.png\") 8 fill; -webkit-border-image: url(\"../compact/border-button.png\") 8 fill; border-image: url(\"../compact/border-button.png\") 8 fill; color: white; font-family: inherit; font-size: 12px; font-weight: bold; text-decoration: none; text-shadow: 0px 1px 1px rgba(255, 255, 255, 0.1), 0px -1px 1px rgba(0, 0, 0, 0.4); background: none; }\n.newbutton:active, .newbutton:hover, .newbutton[selected], .newbutton.expanded, .newbutton.active { -moz-border-image: url(\"../compact/border-button-active.png\") 8 fill; -o-border-image: url(\"../compact/border-button-active.png\") 8 fill; -webkit-border-image: url(\"../compact/border-button-active.png\") 8 fill; border-image: url(\"../compact/border-button-active.png\") 8 fill; color: white; }\n\n.button, .button:visited { -moz-border-radius: 6px; -webkit-border-radius: 6px; border-radius: 6px; background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuNSIgeTE9IjAuMCIgeDI9IjAuNSIgeTI9IjEuMCI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2JmZDBlMCIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iIzgwYTJjNCIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #bfd0e0), color-stop(100%, #80a2c4)); background: -moz-linear-gradient(top, #bfd0e0, #80a2c4); background: -webkit-linear-gradient(top, #bfd0e0, #80a2c4); background: linear-gradient(to bottom, #bfd0e0, #80a2c4); background-color: #9fb9d2; height: 30px; line-height: 30px; color: white; font-family: inherit; font-size: 12px; font-weight: bold; margin: 0px; padding: 5px; text-decoration: none; text-overflow: ellipsis; white-space: nowrap; width: auto; text-shadow: 0px 1px 1px rgba(255, 255, 255, 0.1), 0px -1px 1px rgba(0, 0, 0, 0.4); border: 1px solid #517191; -moz-box-shadow: inset 0px 1px 0px rgba(255, 255, 255, 0.75), 0px 1px 1px rgba(255, 255, 255, 0.6), 0px -1px 1px rgba(0, 0, 0, 0.1); -webkit-box-shadow: inset 0px 1px 0px rgba(255, 255, 255, 0.75), 0px 1px 1px rgba(255, 255, 255, 0.6), 0px -1px 1px rgba(0, 0, 0, 0.1); box-shadow: inset 0px 1px 0px rgba(255, 255, 255, 0.75), 0px 1px 1px rgba(255, 255, 255, 0.6), 0px -1px 1px rgba(0, 0, 0, 0.1); }\n\n.button:active, .button[selected], .button.active, .button.upmod, .button.downmod { background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuNSIgeTE9IjAuMCIgeDI9IjAuNSIgeTI9IjEuMCI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iIzdlODk5NCIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iIzgwYTJjNCIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #7e8994), color-stop(100%, #80a2c4)); background: -moz-linear-gradient(top, #7e8994, #80a2c4); background: -webkit-linear-gradient(top, #7e8994, #80a2c4); background: linear-gradient(to bottom, #7e8994, #80a2c4); background-color: #7f95ac; }\n\nbutton.button { padding: 0 5px; }\n\n.secondary_button { background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuNSIgeTE9IjAuMCIgeDI9IjAuNSIgeTI9IjEuMCI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2FiYmJjOSIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iIzgzOTNhMyIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #abbbc9), color-stop(100%, #8393a3)); background: -moz-linear-gradient(top, #abbbc9, #8393a3); background: -webkit-linear-gradient(top, #abbbc9, #8393a3); background: linear-gradient(to bottom, #abbbc9, #8393a3); background-color: #97a7b6; border: 1px solid #626D78; }\n\n.secondary_button:active, .second_button[selected], .second_button.active { background-color: #ABBBC9; background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuNSIgeTE9IjAuMCIgeDI9IjAuNSIgeTI9IjEuMCI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iIzgzOTNhMyIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iI2FiYmJjOSIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #8393a3), color-stop(100%, #abbbc9)); background: -moz-linear-gradient(top, #8393a3, #abbbc9); background: -webkit-linear-gradient(top, #8393a3, #abbbc9); background: linear-gradient(to bottom, #8393a3, #abbbc9); background-color: #97a7b6; }\n\n.small_button, .small_button:visited { -moz-border-radius: 6px; -webkit-border-radius: 6px; border-radius: 6px; background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuNSIgeTE9IjAuMCIgeDI9IjAuNSIgeTI9IjEuMCI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2JmZDBlMCIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iIzgwYTJjNCIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #bfd0e0), color-stop(100%, #80a2c4)); background: -moz-linear-gradient(top, #bfd0e0, #80a2c4); background: -webkit-linear-gradient(top, #bfd0e0, #80a2c4); background: linear-gradient(to bottom, #bfd0e0, #80a2c4); background-color: #9fb9d2; line-height: 20px; color: white; font-family: inherit; font-size: 12px; font-weight: bold; margin: 0px; padding: 1px; text-decoration: none; text-overflow: ellipsis; white-space: nowrap; width: auto; text-shadow: 0px 1px 1px rgba(255, 255, 255, 0.1), 0px -1px 1px rgba(0, 0, 0, 0.4); border: 1px solid #517191; -moz-box-shadow: \"0px 1px 1px rgba(255,255,255,.6), 0px -1px 1px rgba(0,0,0,.1) \"; -webkit-box-shadow: \"0px 1px 1px rgba(255,255,255,.6), 0px -1px 1px rgba(0,0,0,.1) \"; box-shadow: \"0px 1px 1px rgba(255,255,255,.6), 0px -1px 1px rgba(0,0,0,.1) \"; }\n\n.small_button:active, .small_button[selected], .small_button.active { background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuNSIgeTE9IjAuMCIgeDI9IjAuNSIgeTI9IjEuMCI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iIzdlODk5NCIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iIzgwYTJjNCIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #7e8994), color-stop(100%, #80a2c4)); background: -moz-linear-gradient(top, #7e8994, #80a2c4); background: -webkit-linear-gradient(top, #7e8994, #80a2c4); background: linear-gradient(to bottom, #7e8994, #80a2c4); background-color: #7f95ac; }\n\n.group_button { -moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0; border-left: 1px solid #a6bed9; border-right: 1px solid #445d79; }\n\n.group_button:first-child { -moz-border-radius-topleft: 6px; -moz-border-radius-bottomleft: 6px; border-top-left-radius: 6px; border-bottom-left-radius: 6px; -webkit-border-top-left-radius: 6px; -webkit-border-bottom-left-radius: 6px; border-left: 1px solid #517090; }\n\n.group_button:last-child { -webkit-border-top-left-radius: 0px; -moz-border-radius-topleft: 0px; -webkit-border-bottom-left-radius: 0px; -moz-border-radius-bottomleft: 0px; -webkit-border-top-right-radius: 6px; -moz-border-radius-topright: 6px; -webkit-border-bottom-right-radius: 6px; -moz-border-radius-bottomright: 6px; border-right: 1px solid #517090; }\n\n/*Options popups*/\n.options_link { font-size: x-small; clear: left; margin: 2px 0px 0px 10px; display: inline-block; width: 30px; height: 30px; position: absolute; top: 35px; right: 10px; background-image: url(\"../compact/options.png\"); /*SPRITE*/ }\n.options_link.active { background-image: url(\"../compact/options-active.png\"); /*SPRITE*/ }\n\n.comment .options_link { top: 10px; }\n\n/*Options expando*/\n.link .options_expando, .comment .options_expando, .message .options_expando { background: #213345; margin: 35px -5px -1px; border-top: 1px solid #111922; display: none; -moz-box-shadow: inset 0px 3px 8px rgba(0, 0, 0, 0.8); -webkit-box-shadow: inset 0px 3px 8px rgba(0, 0, 0, 0.8); box-shadow: inset 0px 3px 8px rgba(0, 0, 0, 0.8); text-shadow: 0px -1px 0px rgba(0, 0, 0, 0.8); text-align: center; height: 60px; overflow: hidden; }\n.link .options_expando a, .comment .options_expando a, .message .options_expando a { display: inline-block; color: white; text-decoration: none; font-size: 11px; padding: 10px; width: 50px; height: 40px; text-align: center; border-right: 1px solid #111922; border-left: 1px solid #324c67; -moz-transition: all 100ms ease-in; -o-transition: all 100ms ease-in; -webkit-transition: all 100ms ease-in; transition: all 100ms ease-in; }\n.link .options_expando a:active, .comment .options_expando a:active, .message .options_expando a:active { background-color: #324c67; border-left: 1px solid #42668a; }\n.link .options_expando a:hover, .comment .options_expando a:hover, .message .options_expando a:hover { background-color: #263340; -moz-box-shadow: inset 0px 3px 8px rgba(0, 0, 0, 0.8); -webkit-box-shadow: inset 0px 3px 8px rgba(0, 0, 0, 0.8); box-shadow: inset 0px 3px 8px rgba(0, 0, 0, 0.8); border-left: 1px solid #42668a; }\n.link .options_expando a:first-child, .comment .options_expando a:first-child, .message .options_expando a:first-child { border-left: none; }\n.link .options_expando a:last-child, .comment .options_expando a:last-child, .message .options_expando a:last-child { border-right: none; }\n.link .options_expando.expanded, .comment .options_expando.expanded, .message .options_expando.expanded { display: block; }\n\n.comment .entry, .message .entry { margin-right: 50px; }\n.comment .child .options_link, .message .child .options_link { top: 8px; }\n.comment .options_expando, .message .options_expando { margin: 10px -50px 10px 0px; }\n\n.message .options_expando { margin: 25px -55px 10px -5px; }\n\n.options_icons, .email-icon, .report-icon, .save-icon, .unsave-icon, .domain-icon, .edit-icon, .reply-icon, .permalink-icon, .collapse-icon, .context-icon, .parent-icon, .unread-icon, .hide-icon, .unhide-icon { display: block; width: 24px; height: 24px; margin-left: auto; margin-right: auto; margin-bottom: 5px; }\n\n.email-icon { background-image: url(\"../compact/email.png\"); /*SPRITE*/ }\n\n.report-icon { background-image: url(\"../compact/report.png\"); /*SPRITE*/ }\n\n.save-icon { background-image: url(\"../compact/save.png\"); /*SPRITE*/ }\n\n.unsave-icon { background-image: url(\"../compact/unsave.png\"); /*SPRITE*/ }\n\n.domain-icon { background-image: url(\"../compact/domain.png\"); /*SPRITE*/ }\n\n.edit-icon { background-image: url(\"../compact/edit.png\"); /*SPRITE*/ }\n\n.reply-icon { background-image: url(\"../compact/reply.png\"); /*SPRITE*/ }\n\n.permalink-icon { background-image: url(\"../compact/permalink.png\"); /*SPRITE*/ }\n\n.collapse-icon { background-image: url(\"../compact/collapse.png\"); /*SPRITE*/ }\n\n.context-icon { background-image: url(\"../compact/context.png\"); /*SPRITE*/ }\n\n.parent-icon { background-image: url(\"../compact/context.png\"); /*SPRITE*/ }\n\n.unread-icon { background-image: url(\"../compact/unread.png\"); /*SPRITE*/ }\n\n.hide-icon { background-image: url(\"../compact/hide.png\"); /*SPRITE*/ }\n\n.unhide-icon { background-image: url(\"../compact/unhide.png\"); /*SPRITE*/ }\n\n/*Toolbar*/\n#topbar { background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuNSIgeTE9IjAuMCIgeDI9IjAuNSIgeTI9IjEuMCI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2NlZTNmOCIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iI2E4YzRlMCIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #cee3f8), color-stop(100%, #a8c4e0)); background: -moz-linear-gradient(top, #cee3f8, #a8c4e0); background: -webkit-linear-gradient(top, #cee3f8, #a8c4e0); background: linear-gradient(to bottom, #cee3f8, #a8c4e0); background-color: #bbd3ec; border-bottom: 1px solid #7599BD; border-top: 1px solid #DCEAF7; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; padding: 0px 10px; height: 40px; position: relative; }\n#topbar #header-img { height: 32px; width: auto; }\n#topbar .left { position: absolute; left: 0; bottom: 3px; overflow: hidden; max-height: 40px; z-index: 1; }\n#topbar .right { position: absolute; right: 10px; bottom: 1px; z-index: 3; }\n#topbar > h1 { color: #444; font-size: 18px; font-weight: bold; text-align: center; text-overflow: ellipsis; white-space: nowrap; overflow: hidden; margin: 0 10px; padding: 0; padding-top: 16px; text-shadow: rgba(255, 255, 255, 0.5) 0px 1px 0px, rgba(0, 0, 0, 0.1) 0px -1px 0px; -webkit-box-flex: 1; -moz-box-flex: 1; -ms-box-flex: 1; box-flex: 1; }\n\n#topbar > h1 a { position: relative; color: inherit; text-decoration: inherit; z-index: 2; }\n\nbody[orient=\"landscape\"] > #topbar > h1 { margin-left: -125px; width: 250px; }\n\n#topbar > .right > .button { padding-top: 8px; padding-bottom: 3px; }\n\n#topbar > .right > .button:first-child { margin-right: 5px; }\n\n#topbar > .right > #mail { width: 30px; height: 30px; display: inline-block; }\n#topbar > .right > #mail.nohavemail { background-image: url(\"../compact/nomail.png\"); /*SPRITE*/ }\n#topbar > .right > #mail.nohavemail:active, #topbar > .right > #mail.nohavemail:hover { background-image: url(\"../compact/nomail-active.png\"); /*SPRITE*/ }\n#topbar > .right > #mail.havemail { background-image: url(\"../compact/havemail.png\"); /*SPRITE*/ }\n#topbar > .right > #mail.havemail:active, #topbar > .right > #mail.havemail:hover { background-image: url(\"../compact/havemail-active.png\"); /*SPRITE*/ }\n\n#topbar > .right > #modmail { width: 30px; height: 30px; display: inline-block; }\n#topbar > .right > #modmail.nohavemail { background-image: url(\"../compact/modmail.png\"); /*SPRITE*/ }\n#topbar > .right > #modmail.nohavemail:active, #topbar > .right > #modmail.nohavemail:hover { background-image: url(\"../compact/modmail-active.png\"); /*SPRITE*/ }\n#topbar > .right > #modmail.havemail { background-image: url(\"../compact/newmodmail.png\"); /*SPRITE*/ }\n#topbar > .right > #modmail.havemail:active, #topbar > .right > #modmail.havemail:hover { background-image: url(\"../compact/newmodmail-active.png\"); /*SPRITE*/ }\n\n.topbar-options { width: 30px; height: 30px; display: inline-block; background-image: url(\"../compact/menu-options.png\"); /*SPRITE*/ }\n.topbar-options.active, .topbar-options:hover, .topbar-options:active { background-image: url(\"../compact/menu-options-active.png\"); /*SPRITE*/ }\n\n#top_menu { position: absolute; right: 5px; top: 44px; background-color: white; border: 1px solid rgba(27, 47, 94, 0.4); border-top: 0px; -webkit-border-bottom-left-radius: 10px; -moz-border-radius-bottomleft: 10px; -webkit-border-bottom-right-radius: 10px; -moz-border-radius-bottomright: 10px; border-bottom-left-radius: 10px; border-bottom-right-radius: 10px; -moz-box-shadow: 0px 0px 8px rgba(0, 0, 0, 0.3); -webkit-box-shadow: 0px 0px 8px rgba(0, 0, 0, 0.3); box-shadow: 0px 0px 8px rgba(0, 0, 0, 0.3); z-index: 5; display: none; }\n\n#top_menu > .menuitem { padding: 5px; }\n\n#top_menu > .menuitem.bottm-bar { border-bottom: 1px solid rgba(27, 47, 94, 0.4); }\n\n#top_menu > .menuitem a { text-decoration: none; color: #222; font-weight: bold; }\n\n.status { color: red; margin-left: 20px; }\n\n/*Subtoolbar (eg hot)*/\n.subtoolbar { -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; height: 32px; background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuNSIgeTE9IjAuMCIgeDI9IjAuNSIgeTI9IjEuMCI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2ZmZmZmZiIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iI2NjY2NjYyIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #ffffff), color-stop(100%, #cccccc)); background: -moz-linear-gradient(top, #ffffff, #cccccc); background: -webkit-linear-gradient(top, #ffffff, #cccccc); background: linear-gradient(to bottom, #ffffff, #cccccc); background-color: #e5e5e5; border-bottom: 1px solid #bbb; padding: 6px; text-overflow: ellipsis; overflow: hidden; }\n\n.subtoolbar > ul { list-style-type: none; margin: 0; padding: 0; }\n\n.subtoolbar > ul > li { display: inline-block; text-overflow: ellipsis; overflow: hidden; }\n\n.subtoolbar > ul > li a { color: #4c566c; font-weight: bold; text-decoration: none; font-size: 12px; line-height: 20px; margin: 0; padding: 3px 10px; text-overflow: ellipsis; overflow: hidden; }\n\n.subtoolbar > ul > li.selected a { background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuNSIgeTE9IjAuMCIgeDI9IjAuNSIgeTI9IjEuMCI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2RkZGRkZCIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iI2FhYWFhYSIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #dddddd), color-stop(100%, #aaaaaa)); background: -moz-linear-gradient(top, #dddddd, #aaaaaa); background: -webkit-linear-gradient(top, #dddddd, #aaaaaa); background: linear-gradient(to bottom, #dddddd, #aaaaaa); background-color: #c3c3c3; -moz-border-radius: 8px; -webkit-border-radius: 8px; border-radius: 8px; border: 1px solid #aaa; padding-top: 2px; padding-bottom: 1px; -moz-box-shadow: 0px 1px 1px rgba(255, 255, 255, 0.8); -webkit-box-shadow: 0px 1px 1px rgba(255, 255, 255, 0.8); box-shadow: 0px 1px 1px rgba(255, 255, 255, 0.8); }\n\n/*Things*/\n/*Arrows*/\n.link .arrow, .comment .arrow, .message .arrow { width: 28px; height: 28px; cursor: pointer; display: block; margin: 1px auto 0px; outline: none; }\n.link .arrow.up, .comment .arrow.up, .message .arrow.up { background-image: url(\"../compact/upvote.png\"); /*SPRITE*/ }\n.link .arrow.down, .comment .arrow.down, .message .arrow.down { background-image: url(\"../compact/downvote.png\"); /*SPRITE*/ }\n.link .arrow.upmod, .comment .arrow.upmod, .message .arrow.upmod { background-image: url(\"../compact/upvote-active.png\"); /*SPRITE*/ }\n.link .arrow.downmod, .comment .arrow.downmod, .message .arrow.downmod { background-image: url(\"../compact/downvote-active.png\"); /*SPRITE*/ }\n\n/*Links*/\n.link { min-height: 70px; border-bottom: 1px solid #999; border-top: 1px solid #ddd; padding: 5px 5px; padding-bottom: 0px; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; background: rgba(255, 255, 255, 0.6); position: relative; overflow: hidden; }\n\n.link:first-child { border-top: none; }\n\n.link:nth-child(odd) { background: rgba(206, 227, 248, 0.5); }\n\n/* Voting stuff */\n.link > .rank { float: left; margin-top: 17px; font-size: 12px; color: #aaa; }\n\n.link > .midcol { float: left; width: 25px; margin: 0 10px 1px 0px; padding-bottom: 5px; position: relative; }\n\n.link > .entry .score, .link > .entry.likes .score.unvoted, .link > .entry.dislikes .score.unvoted { display: none; }\n\n.link > .entry .score.unvoted, .link > .entry.likes .score.likes, .link > .entry.dislikes .score.dislikes { display: inline; font-weight: bold; }\n\n.link > .entry.likes .score.likes { color: #E07A7A; }\n\n.link > .entry.dislikes .score.dislikes { color: #7272D1; }\n\n/* experimental */\n.link .rank { display: none; }\n\n.link .modcol { float: left; }\n\n.comment { position: relative; }\n\n.comment > .entry > .tagline .score { display: none; }\n\n.comment > .entry.unvoted > .tagline .score.unvoted, .comment > .entry.likes > .tagline .score.likes, .comment > .entry.dislikes > .tagline .score.dislikes { display: inline; }\n\n/** Vote up **/\n.link > .midcol.likes > .score { color: #E07A7A; }\n\n/** Vote down **/\n.link > .midcol.dislikes > .score { color: #7272D1; }\n\n/*Image*/\n.link .thumbnail { float: right; margin: 0 0 5px 5px; overflow: hidden; max-height: 50px; }\n\n.link .thumbnail img { max-width: 50px; max-height: 50px; }\n\n/* Entry*/\n.link .entry { margin: 0px 50px 3px 0px; }\n\n.link a { text-decoration: none; color: #517191; color: #369; }\n\n.link p.title { margin: 0; padding: 0; text-overflow: ellipsis; word-wrap: break-word; font-size: .8em; font-weight: bold; }\n\n.link p.title > a { text-overflow: ellipsis; overflow: hidden; color: #25A; }\n\n.link.stickied p.title > a { color: #228822; }\n\n.link .domain { color: #737373; font-size: 9px; margin-left: 5px; }\n.link .domain a, .link .domain a:hover { color: inherit; }\n\n.link .tagline { margin: 2px 0 5px; padding: 0; padding-top: 2px; font-size: 10px; color: #333; }\n\n.link .tagline > span { margin-right: 2px; }\n\n.link .tagline a { font-weight: bold; }\n\n.link .tagline .stickied-tagline { color: #228822; }\n\n/*Expando*/\n.link .expando-button { float: left; display: block; height: auto; line-height: inherit; margin: 3px 10px 2px 0; width: 30px; height: 30px; background-image: url(\"../compact/selftext.png\"); /*SPRITE*/ }\n.link .expando-button.expanded { background-image: url(\"../compact/selftext-active.png\"); /*SPRITE*/ }\n\n.link > .expando { clear: both; margin: 5px 0; margin-bottom: 30px; border: 1px solid #999; background: #ddd; padding: 5px; -moz-border-radius: 8px; -webkit-border-radius: 8px; border-radius: 8px; font-size: 11px; }\n\n.link > .thing_options { font-size: x-small; margin: none; display: block; float: left; clear: left; margin: 2px 0px 0px 10px; }\n\n.nsfw-warning, .quarantine-warning { -moz-border-radius: 3px; -webkit-border-radius: 3px; border-radius: 3px; color: #ac3939; text-decoration: none; font-weight: normal; font-size: 9px; margin-left: 5px; padding: 0 2px; border: 1px solid #d27979 !important; }\n\n/* Comment count */\n.commentcount { float: right; margin: 5px; width: 45px; text-align: right; }\n\n.commentcount > .comments { border: 8px solid transparent; -moz-border-image: url(\"../compact/border-button.png\") 8 fill; -o-border-image: url(\"../compact/border-button.png\") 8 fill; -webkit-border-image: url(\"../compact/border-button.png\") 8 fill; border-image: url(\"../compact/border-button.png\") 8 fill; color: white; font-family: inherit; font-size: 12px; font-weight: bold; text-decoration: none; text-shadow: 0px 1px 1px rgba(255, 255, 255, 0.1), 0px -1px 1px rgba(0, 0, 0, 0.4); }\n.commentcount > .comments:active, .commentcount > .comments:hover, .commentcount > .comments[selected], .commentcount > .comments.preloaded { -moz-border-image: url(\"../compact/border-button-active.png\") 8 fill; -o-border-image: url(\"../compact/border-button-active.png\") 8 fill; -webkit-border-image: url(\"../compact/border-button-active.png\") 8 fill; border-image: url(\"../compact/border-button-active.png\") 8 fill; }\n\n/* Comment styles */\n.commentarea > h1 { color: #4c566c; font-size: 17px; margin: 10px 10px 5px; border-bottom: 1px solid rgba(0, 0, 0, 0.2); -moz-box-shadow: 0px 1px 1px rgba(255, 255, 255, 0.4); -webkit-box-shadow: 0px 1px 1px rgba(255, 255, 255, 0.4); box-shadow: 0px 1px 1px rgba(255, 255, 255, 0.4); }\n\n.commentarea > .menuarea { display: none; /*TODO: Make dropdown menu*/ }\n\n.commentarea > .main-form-title { color: #4c566c; font-size: 17px; font-weight: bold; margin: 0 10px; }\n\n.commentarea > .usertext { background: white; margin: 0 10px 5px; border: 1px solid #d9d9d9; -moz-border-radius: 8px; -webkit-border-radius: 8px; border-radius: 8px; }\n\n.commentarea > .usertext textarea { margin: 0; padding: 5px; width: 100%; height: 100px; border: none; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; -moz-border-radius: 8px; -webkit-border-radius: 8px; border-radius: 8px; border-bottom: 1px solid #d9d9d9; }\n\n.cancel, .save { float: right; padding: 0 5px !important; }\n\n.save { margin-left: 5px; }\n\n/* Errors */\n.error { color: red; }\n\n.content > .error { color: rgba(255, 255, 255, 0.9); font-size: 25px; margin: 10px; text-align: center; text-shadow: rgba(0, 0, 0, 0.15) 0px -1px 0px; }\n\n.help-toggle { float: left; margin-top: 3px; }\n\n.bottom-area { padding: 5px; }\n\n.markhelp-parent { display: none; }\n\n.markhelp { width: 100%; border-collapse: collapse; }\n\n.markhelp th { background: #d9d9d9; }\n\n.markhelp th:first-child { -webkit-border-top-left-radius: 8px; -moz-border-radius-topleft: 8px; border-top-left-radius: 8px; }\n\n.markhelp th:last-child { -webkit-border-top-right-radius: 8px; -moz-border-radius-topright: 8px; border-top-right-radius: 8px; }\n\n.markhelp tr:nth-child(odd) td { background: rgba(0, 0, 100, 0.1); }\n\n.markhelp td { border: 1px solid #d9d9d9; padding: 5px; }\n\n.markhelp tr:last-child td:first-child { -webkit-border-bottom-left-radius: 8px; -moz-border-radius-bottomleft: 8px; border-bottom-left-radius: 8px; }\n\n.markhelp tr:last-child td:last-child { -webkit-border-bottom-right-radius: 8px; -moz-border-radius-bottomright: 8px; border-bottom-right-radius: 8px; }\n\n/*Cloned comment reply */\n.usertext textarea { margin: 0; padding: 5px; border: 1px solid #d9d9d9; width: 100%; min-height: 100px; -moz-border-radius: 5px; -webkit-border-radius: 5px; border-radius: 5px; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; }\n\n.child form.usertext.cloneable { margin: 5px; }\n\n/**Actual comments*/\n.comment { background: white; border: 1px solid #d9d9d9; margin: 10px; -moz-border-radius: 8px; -webkit-border-radius: 8px; border-radius: 8px; }\n\n.comment > .midcol { float: left; margin: 7px; overflow: hidden; }\n\n.comment > .entry > .tagline { font-size: 11px; padding-bottom: 2px; }\n\n.child .comment { margin: 4px; margin-top: 0px; -webkit-border-top-right-radius: 0px; -moz-border-radius-topright: 0px; }\n\n.comment.collapsed .child, .comment.collapsed .usertext, .comment.collapsed .midcol, .comment.collapsed .button, .comment.collapsed .options_link, .comment.collapsed .options_expando { display: none; }\n\n.comment.collapsed { font-style: italcs; }\n\n.comment.collapsed .tagline { margin-left: 20px; font-style: italcs; color: #AAA; }\n\n/** gilding */\n.gilded-icon { position: relative; display: inline-block; margin: 0 0 -15px 8px; top: -8px; color: #99895F; font-size: .9em; vertical-align: middle; }\n\n.gilded-icon:before { display: inline-block; content: ''; background-image: url(../gold-coin.png); /* SPRITE */ background-repeat: no-repeat; height: 14px; width: 13px; margin-right: 2px; vertical-align: -3px; }\n\n.user-gilded > .entry .gilded-icon:before { width: 23px; }\n\nbody.post-under-6h-old .gilded-icon { opacity: .55; }\n\n/** messages and inbox */\n.message { background: white; position: relative; border: 1px solid #d9d9d9; margin: 10px; -moz-border-radius: 8px; -webkit-border-radius: 8px; border-radius: 8px; padding: 5px; }\n\n.message > .midcol { float: left; margin: 10px; overflow: hidden; }\n\n.message.unread { background-color: #FFFFAA; }\n\n.message .correspondent { background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuNSIgeTE9IjAuMCIgeDI9IjAuNSIgeTI9IjEuMCI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2NlZTNmOCIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iIzllYmVkYyIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #cee3f8), color-stop(100%, #9ebedc)); background: -moz-linear-gradient(top, #cee3f8, #9ebedc); background: -webkit-linear-gradient(top, #cee3f8, #9ebedc); background: linear-gradient(to bottom, #cee3f8, #9ebedc); background-color: #b6d0ea; /* TODO */ margin-right: 10px; padding: 2px 5px; -moz-border-radius: 15px; -webkit-border-radius: 15px; border-radius: 15px; }\n\n.message .correspondent a { text-decoration: none; }\n\n.message .message .subject { display: none; }\n\n.message > .entry > .tagline { font-size: 11px; padding-bottom: 2px; margin-bottom: 2px; }\n\n.message > .entry .usertext-body, .message > .entry .md { font-size: 11px; word-wrap: break-word; }\n\n.message > .metabuttons { float: right; margin: 10px; }\n\n.message .subject { font-weight: bold; font-size: 13px; border-bottom: 1px solid #d9d9d9; padding: 5px; overflow: hidden; }\n\n.message .subject a { margin-left: 5px; }\n\n.message .subject .correspondent a { margin-left: 0; }\n\n/* subreddit */\n.link .subreddit { background-color: transparent; margin: 0px; }\n\n.subreddit { background-color: white; -moz-border-radius: 5px; -webkit-border-radius: 5px; border-radius: 5px; margin: 5px; }\n\n.subreddit p.title { display: block; margin-left: 35px; margin-right: 30px; }\n\n.subreddit a.title { display: block; margin: 0; padding: 0; text-overflow: ellipsis; word-wrap: break-word; font-size: small; font-weight: bold; text-overflow: ellipsis; overflow: hidden; color: #25A; text-decoration: none; }\n\n.subreddit .title a.domain { font-size: x-small; color: #AAA; font-style: italic; display: block; }\n\n.subreddit .tagline { font-size: x-small; color: #666; }\n\n.subreddit .button.active { background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuNSIgeTE9IjAuMCIgeDI9IjAuNSIgeTI9IjEuMCI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2JmZDBlMCIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iIzgwYTJjNCIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #bfd0e0), color-stop(100%, #80a2c4)); background: -moz-linear-gradient(top, #bfd0e0, #80a2c4); background: -webkit-linear-gradient(top, #bfd0e0, #80a2c4); background: linear-gradient(to bottom, #bfd0e0, #80a2c4); background-color: #9fb9d2; }\n\n.subreddit > .entry .score, .subreddit > .entry.likes .score.unvoted, .subreddit > .entry.dislikes .score.unvoted { display: none; }\n\n.subreddit > .entry .score.unvoted, .subreddit > .entry.likes .score.likes, .subreddit > .entry.dislikes .score.dislikes { display: inline; }\n\n.subreddit .midcol .button.add, .subreddit .midcol .button.remove { font-family: courier; font-size: small; }\n\n.subreddit .midcol { float: left; }\n\n.subreddit .midcol .button { display: none; margin: 4px; }\n\n.subreddit .midcol .button.active { display: block; width: auto; height: auto; padding: 0px 9px; }\n\n.subreddit .expando-button { float: right; height: 100%; }\n\n.subreddit .description { border-top: 1px solid #AAA; margin-top: 2px; padding-top: 2px; margin-left: 0px; padding-left: 10px; }\n\n/* Compose */\n#compose-message { background: white; border: 1px solid #d9d9d9; border-top: 0px; margin: 10px; margin-top: 0; padding: 10px; -webkit-border-bottom-left-radius: 8px; -webkit-border-bottom-right-radius: 8px; -moz-border-radius-bottomleft: 8px; -moz-border-radius-bottomright: 8px; }\n\n#compose-message label { display: block; font-size: 17px; font-weight: bold; }\n\n#compose-message input[type=\"text\"] { -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; border: 1px solid #757575; -moz-border-radius: 5px; -webkit-border-radius: 5px; border-radius: 5px; margin-bottom: 5px; padding: 5px; width: 100%; }\n\n#compose-message textarea { border-color: #757575; height: 200px; }\n\n.comment > .entry .usertext-body { font-size: 11px; word-wrap: break-word; }\n\n.comment > .entry .usertext-edit { margin-left: 42px; }\n\n.comment > .metabuttons { float: right; margin: 10px; }\n\n/*Child comment specific styles*/\n/*Reduce the bottom margin on the last child comment in a thread, to make viewing easier.*/\n.child .comment { margin-right: -1px; }\n\n.child .comment:last-child { margin-bottom: 2px; }\n\n.comment > .morecomments { margin: 5px; text-align: right; }\n\n/*Link colors*/\n.tagline .submitter { color: blue; }\n\n.tagline .friend { color: orange; /*Why not orangered? Because orangered can look very red on a mobile*/ }\n\n.tagline .moderator { color: #282; }\n\n.tagline .admin { color: #F01; }\n\n.tagline .userattrs .cakeday { display: inline-block; text-indent: -9999px; width: 11px; height: 8px; background-image: url(../cake.png); /* SPRITE */ vertical-align: middle; }\n\n/*Loading spinner, yay CSS animation*/\n@-webkit-keyframes rotateThis { from { -webkit-transform: scale(0.75) rotate(0deg); }\n  to { -webkit-transform: scale(0.75) rotate(360deg); } }\n.loading { width: 100%; background-color: white; text-align: center; }\n\n.loading img { -webkit-animation-name: rotateThis; -webkit-animation-duration: .5s; -webkit-animation-iteration-count: infinite; -webkit-animation-timing-function: linear; }\n\n.throbber { display: none; margin: 0 2px; background: url(\"../compact/throbber.gif\") no-repeat; width: 18px; height: 18px; }\n\n.working .throbber { display: inline-block; }\n\n/* Login and Register */\n#login_login, #login_reg { background: white; border: 1px solid #d9d9d9; margin: 10px; -webkit-border-bottom-left-radius: 8px; -webkit-border-bottom-right-radius: 8px; -moz-border-radius-bottomleft: 8px; -moz-border-radius-bottomright: 8px; max-width: 350px; margin-left: auto; margin-right: auto; }\n\n#login_login > div, #login_reg > div { padding: 10px; }\n\n#login_login > div > ul, #login_reg > div > ul { list-style-type: none; padding: 0; margin: 0 0 10px; }\n\n#login_login > div > ul li label, #login_reg > div > ul li label { display: block; font-size: 17px; font-weight: bold; }\n\n#login_login input[type=\"text\"], #login_login input[type=\"password\"], #login_reg input[type=\"text\"], #login_reg input[type=\"password\"] { width: 100%; margin: 0 0 5px; -moz-border-radius: 5px; -webkit-border-radius: 5px; border-radius: 5px; border: 1px solid #757575; /*It was the coins fault!*/ font-size: 17px; padding: 5px; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; }\n\n#login_login > div > ul li input[type=\"checkbox\"] + label, #login_reg > div > ul li input[type=\"checkbox\"] + label { display: inline; }\n\n.user-form .submit * { vertical-align: middle; }\n\n/* takdown page (sigh) */\n.infobar.red img { float: left; }\n\n.infobar.red { border: 1px solid red; padding: 10px; margin: 5px; background-color: #FFA177; }\n\n.clear { clear: both; }\n\n.clearleft { clear: left; }\n\n.cover { position: fixed; left: 0px; top: 0px; width: 100%; height: 100%; background-color: gray; opacity: .3; z-index: 1000; }\n\n.popup { position: absolute; top: 75px; left: 0; -moz-border-radius: 30px; -webkit-border-radius: 30px; border-radius: 30px; background-color: white; text-align: left; z-index: 1001; padding: 10px; border-color: #B2B2B2 black black #B2B2B2; border-style: solid; border-width: 1px; margin-left: auto; margin-right: auto; max-width: 350px; }\n\n.popup h1 { text-align: center; font-size: large; font-weight: normal; color: orangered; }\n\n/* Submit links */\n#newlink { background: white; border: 1px solid #d9d9d9; margin: 10px; -webkit-border-bottom-left-radius: 8px; -webkit-border-bottom-right-radius: 8px; -moz-border-radius-bottomleft: 8px; -moz-border-radius-bottomright: 8px; }\n\n#newlink .save { margin: 8px; }\n\n/** Tab switcher **/\n#newlink .tabmenu { display: -webkit-box; display: -moz-box; -webkit-box-orient: horizontal; -moz-box-orient: horizontal; margin: 10px; padding: 0; }\n\n#newlink .tabmenu li { display: block; webkit-box-flex: 1; background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuNSIgeTE9IjAuMCIgeDI9IjAuNSIgeTI9IjEuMCI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2Q5ZDlkOSIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iI2IzYjNiMyIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #d9d9d9), color-stop(100%, #b3b3b3)); background: -moz-linear-gradient(top, #d9d9d9, #b3b3b3); background: -webkit-linear-gradient(top, #d9d9d9, #b3b3b3); background: linear-gradient(to bottom, #d9d9d9, #b3b3b3); background-color: #c6c6c6; border: 1px solid #999999; position: relative; }\n\n#newlink .tabmenu li a { width: 100%; height: 100%; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; display: block; padding: 5px; color: #4d4d4d; text-shadow: rgba(255, 255, 255, 0.4) 0px 1px 1px; text-decoration: none; font-weight: bold; }\n\n#newlink .tabmenu li:first-child { -webkit-border-bottom-left-radius: 5px; -webkit-border-top-left-radius: 5px; -moz-border-radius-bottomleft: 5px; -moz-border-radius-topleft: 5px; }\n\n#newlink .tabmenu li:last-child { -webkit-border-bottom-right-radius: 5px; -webkit-border-top-right-radius: 5px; -moz-border-radius-bottomright: 5px; -moz-border-radius-topright: 5px; border-left-color: #cccccc; }\n\n#newlink li.selected { background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuNSIgeTE9IjAuMCIgeDI9IjAuNSIgeTI9IjEuMCI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iIzgwODA4MCIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iI2IzYjNiMyIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #808080), color-stop(100%, #b3b3b3)); background: -moz-linear-gradient(top, #808080, #b3b3b3); background: -webkit-linear-gradient(top, #808080, #b3b3b3); background: linear-gradient(to bottom, #808080, #b3b3b3); background-color: #999999; }\n\n#newlink li.selected a { text-shadow: rgba(0, 0, 0, 0.4) 0px -1px 1px; color: #f2f2f2; }\n\n#newlink .spacer { margin-bottom: 5px; }\n\n#newlink .infobar { margin: 5px; }\n\n/* Fields */\n#newlink textarea, #newlink input[type=\"text\"], #newlink input[type=\"url\"] { border: 1px solid #999999; }\n\n#newlink .roundfield { position: relative; padding: 0px 5px; }\n\n#newlink .roundfield-content textarea { -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; width: 100%; height: 5em; -moz-border-radius: 5px; -webkit-border-radius: 5px; border-radius: 5px; }\n\n#newlink .roundfield-content input[type=\"text\"], #newlink .roundfield-content input[type=\"url\"] { -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; width: 100%; height: 2em; -moz-border-radius: 5px; -webkit-border-radius: 5px; border-radius: 5px; }\n\n#newlink .title { font-weight: bold; }\n\n/* Individual sections */\n#url-field .button { float: right; margin-top: 5px; }\n\n#url-field .title-status { background: #e6e6e6; border: 1px solid gray; padding: 2px 4px; margin-top: 5px; display: inline-block; }\n\n#suggested-reddits ul { background: #e6e6e6; border: 1px solid gray; padding: 8px; -moz-border-radius: 8px; -webkit-border-radius: 8px; border-radius: 8px; }\n\n#suggested-reddits ul li { display: inline; }\n\n#suggested-reddits ul li a { background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuNSIgeTE9IjAuMCIgeDI9IjAuNSIgeTI9IjEuMCI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2JlY2ZlMCIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iIzgxYTNjNSIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #becfe0), color-stop(100%, #81a3c5)); background: -moz-linear-gradient(top, #becfe0, #81a3c5); background: -webkit-linear-gradient(top, #becfe0, #81a3c5); background: linear-gradient(to bottom, #becfe0, #81a3c5); background-color: #9fb9d2; -moz-border-radius: 10px; -webkit-border-radius: 10px; border-radius: 10px; display: inline-block; margin: 5px; padding: 3px 7px; text-decoration: none; border: 1px solid #507faf; color: #304c69; }\n\n/* Autocomplete */\n#sr-autocomplete-area { position: relative; z-index: 50; }\n\n#sr-drop-down { position: absolute; background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuNSIgeTE9IjAuMCIgeDI9IjAuNSIgeTI9IjEuMCI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2U2ZTZlNiIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iI2JmYmZiZiIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #e6e6e6), color-stop(100%, #bfbfbf)); background: -moz-linear-gradient(top, #e6e6e6, #bfbfbf); background: -webkit-linear-gradient(top, #e6e6e6, #bfbfbf); background: linear-gradient(to bottom, #e6e6e6, #bfbfbf); background-color: #d2d2d2; border: 1px solid gray; -webkit-border-bottom-left-radius: 5px; -webkit-border-bottom-right-radius: 5px; -moz-border-radius-bottomleft: 5px; -moz-border-radius-bottomright: 5px; border-top: 0px; display: none; left: 5px; margin: 0px; padding: 0px; position: absolute; font-weight: bold; color: #333333; }\n\n#sr-drop-down li { display: block; padding: 2px 5px; }\n\n#sr-drop-down li:hover, #sr-drop-down li:active { background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuNSIgeTE9IjAuMCIgeDI9IjAuNSIgeTI9IjEuMCI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2JlY2ZlMCIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iIzgxYTNjNSIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #becfe0), color-stop(100%, #81a3c5)); background: -moz-linear-gradient(top, #becfe0, #81a3c5); background: -webkit-linear-gradient(top, #becfe0, #81a3c5); background: linear-gradient(to bottom, #becfe0, #81a3c5); background-color: #9fb9d2; color: white; text-shadow: rgba(255, 255, 255, 0.09766) 0px 1px 1px, rgba(0, 0, 0, 0.39844) 0px -1px 1px; -webkit-text-stroke: 1px solid #517090; }\n\n#sr-drop-down li:last-child { -webkit-border-bottom-left-radius: 5px; -webkit-border-bottom-right-radius: 5px; -moz-border-radius-bottomleft: 5px; -moz-border-radius-bottomright: 5px; }\n\n/* markdown */\n.md { overflow: auto; font-size: small; }\n\n.md p, .md h1 { margin: 5px 0; }\n\n.md h1 { font-weight: bold; font-size: 100%; }\n\n.md h2 { font-weight: bold; font-size: 100%; }\n\n.md > * { margin-bottom: 0px; }\n\n.md strong { font-weight: bold; }\n\n.md em { font-style: italic; }\n\n.md strong em { font-style: italic; font-weight: bold; }\n\n.md img { display: none; }\n\n.md ol, .md ul { margin: 10px 2em; }\n\n.md ul { list-style: disc outside; }\n\n.md ol { list-style: decimal outside; }\n\n.md pre { margin: 10px; }\n\n.md blockquote, .help blockquote { border-left: 2px solid #369; padding-left: 4px; margin: 5px; margin-right: 15px; }\n\n.md td, .md th { border: 1px solid #EEE; padding: 1px 3px; }\n\n.md th { font-weight: bold; }\n\n.md table { margin: 5px 10px; }\n\n.md center { text-align: left; }\n\n.tryme { width: 100%; max-width: 280px; padding: 10px; background-color: white; -moz-border-radius: 10px; -webkit-border-radius: 10px; border-radius: 10px; margin: 10px auto; }\n\n.tryme p { margin: 10px; font-size: small; }\n\n.tryme .choices .button { width: 260px; display: block; text-align: center; margin: 10px; }\n\n.deepthread { margin-left: 40px; }\n\n.morecomments a, .deepthread a { text-decoration: none; color: white; }\n\n.morechildren { margin: 5px 10px; }\n\n.morechildren a { display: block; text-align: center; max-width: 350px; color: white !important; }\n\na.author { margin-right: 0.5em; }\n\n.flair, .linkflair { margin-top: 2px; margin-right: 0.5em; padding: 0px 2px; display: inline-block; background: whiteSmoke; color: #545454; border: 1px solid #dedede; font-size: 9px; -moz-border-radius: 2px; -webkit-border-radius: 2px; border-radius: 2px; -moz-box-shadow: inset 0px 1px 0px rgba(255, 255, 255, 0.9); -webkit-box-shadow: inset 0px 1px 0px rgba(255, 255, 255, 0.9); box-shadow: inset 0px 1px 0px rgba(255, 255, 255, 0.9); }\n\n.linkflair { font-weight: normal; max-width: 10em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n\n.mobile-web-redirect-bar { background: white; box-sizing: border-box; font-family: sans-serif; font-size: 14px; padding: 20px; width: 100%; z-index: 1000; }\n.mobile-web-redirect-bar a { text-decoration: none; }\n.mobile-web-redirect-bar .mobile-web-redirect-header { font-size: 18px; line-height: 25px; margin-bottom: 20px; }\n.mobile-web-redirect-bar .mobile-web-redirect-optin { background-color: #4a7fc5; border-radius: 3px; box-shadow: inset 0 -3px 0 0 #3e6ab7; color: white; display: block; font-family: \"Verdana\", sans-serif; font-weight: bold; line-height: 20px; margin-bottom: 20px; padding: 10px 0; text-align: center; text-transform: uppercase; }\n.mobile-web-redirect-bar .mobile-web-redirect-optout { color: #7f7f7f; }\n\n.commentspacer { clear: both; }\n"
  },
  {
    "path": "r2/r2/public/static/css/compact.scss",
    "content": "// Requires Compass and Sass to compile properly\n// [$]> gem install haml compass\n// [$]> cd r2/public/static/css\n// [$]> scss --compass --sourcemap=none --no-cache --style compact --watch compact.scss:compact.css\n\n/* mixins */\n\n@import \"compass/css3\";\n// TODO: Replace with non-deprecated\n// http://compass-style.org/reference/compass/css3/flexbox/ .\n@import \"compass/css3/box\";\n\n@mixin vertical_gradient($from, $to) {\n    @include background(linear-gradient(top, $from, $to));\n    background-color: mix($from, $to); //Takes 2 colors, and gives their combination. Looks good on gradual gradients, not on \"exciting\" gradients\n}\n@mixin sprite($url) {\n    background-image: url($url); /*SPRITE*/\n}\n//Directory for compact static files\n$static: \"../compact/\";\n\n/* meat */\n\n\nbody {\n    background: rgb(197,204,211);\n    font-family: Helvetica, \"Helvetica Neue\", Arial, sans-serif;\n    margin: 0;\n    width: 100%; height: 100%;\n    -webkit-font-smoothing: antialiased;\n}\np {\n    margin: 0; padding: 0;\n}\na {\n    color: #517191; \n}\na:visited {\n    color: #4F565B;\n}\ntextarea {\n    font-family: inherit;\n}\n/*Preloading*/\n#preload {\n    position: absolute;\n    top: -1000px;\n    left: -1000px;\n}\n/*UI stuff*/\n.newbutton {\n    @include appearance(none);\n    border: 8px solid transparent;\n    @include border-image(url($static + 'border-button.png') 8 fill);\n    color: white;\n    font: {\n        family: inherit;\n        size: 12px;\n        weight: bold;\n    }\n    text-decoration: none;\n    text-shadow: 0px 1px 1px rgba(255,255,255,.1), 0px -1px 1px rgba(0,0,0,.4);\n    background: none;\n\n    &:active, &:hover, &[selected], &.expanded, &.active {\n        @include border-image(url($static + 'border-button-active.png') 8 fill);\n        color: white;\n    }\n}\n.button, .button:visited {\n    @include border-radius(6px);\n    @include vertical_gradient(#BFD0E0, #80A2C4);\n\n    height: 30px; line-height: 30px;\n    color: white;\n    font-family: inherit; font-size: 12px;\n    font-weight: bold;\n    margin: 0px;\n    padding: 5px;\n    text-decoration: none;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n    width: auto;\n    text-shadow: 0px 1px 1px rgba(255,255,255,.1), 0px -1px 1px rgba(0,0,0,.4);\n    border: 1px solid #517191;\n    @include box-shadow(inset 0px 1px 0px hsla(0,0%,100%,.75), 0px 1px 1px rgba(255,255,255,.6), 0px -1px 1px rgba(0,0,0,.1) );\n}\n.button:active, .button[selected], .button.active,\n/*Vote toolbar styles*/.button.upmod, .button.downmod\n {\n    @include vertical_gradient(#7E8994, #80A2C4);\n}\nbutton.button {\n    padding: 0 5px;\n}\n.secondary_button {\n    @include vertical_gradient(#ABBBC9, #8393A3);\n    border: 1px solid #626D78;\n}\n.secondary_button:active, .second_button[selected], .second_button.active {\n    background-color: #ABBBC9;\n    @include vertical_gradient(#8393A3, #ABBBC9);\n}\n\n.small_button, .small_button:visited {\n    @include border-radius(6px);\n    @include vertical_gradient(#BFD0E0, #80A2C4);\n\n   line-height: 20px;\n   color: white;\n   font-family: inherit; font-size: 12px;\n   font-weight: bold;\n   margin: 0px;\n   padding: 1px;\n   text-decoration: none;\n   text-overflow: ellipsis;\n   white-space: nowrap;\n   width: auto;\n   text-shadow: 0px 1px 1px rgba(255,255,255,.1), 0px -1px 1px rgba(0,0,0,.4);\n   border: 1px solid #517191;\n   @include box-shadow( \"0px 1px 1px rgba(255,255,255,.6), 0px -1px 1px rgba(0,0,0,.1) \");\n}\n.small_button:active, .small_button[selected], .small_button.active {\n    @include vertical_gradient(#7E8994, #80A2C4);\n}\n\n\n.group_button {\n    @include border-radius(0);\n    border-left: 1px solid hsl(211, 40%, 75%);\n    border-right: 1px solid hsl(211, 28%, 37%);\n}\n.group_button:first-child {\n    -moz-border-radius-topleft: 6px;\n    -moz-border-radius-bottomleft: 6px;\n\n    border-top-left-radius: 6px;\n    border-bottom-left-radius: 6px;\n\n    -webkit-border-top-left-radius: 6px;\n    -webkit-border-bottom-left-radius: 6px;\n    border-left: 1px solid hsl(210, 28%, 44%);\n}\n.group_button:last-child {\n    -webkit-border-top-left-radius: 0px; -moz-border-radius-topleft: 0px;\n    -webkit-border-bottom-left-radius: 0px; -moz-border-radius-bottomleft: 0px;\n    -webkit-border-top-right-radius: 6px; -moz-border-radius-topright: 6px;\n    -webkit-border-bottom-right-radius: 6px; -moz-border-radius-bottomright: 6px;\n    border-right: 1px solid hsl(210, 28%, 44%);\n}\n\n/*Options popups*/\n.options_link {\n    font-size: x-small;\n    clear: left;\n    margin: 2px 0px 0px 10px;\n    display: inline-block;\n    width: 30px; height: 30px;\n    position: absolute;\n    top: 35px;\n    right: 10px;\n    \n    @include sprite($static + 'options.png');\n\n    &.active {\n        @include sprite($static + 'options-active.png');\n    }\n}\n\n.comment .options_link {\n    top: 10px;\n}\n\n/*Options expando*/\n\n.link, .comment, .message {\n    .options_expando {\n        background: hsl(210,35%,20%);\n        margin:  35px -5px -1px;\n        border-top:  1px solid hsl(210,35%,10%);\n        display: none;\n        @include box-shadow(inset 0px 3px 8px hsla(0,0%,0%,.8));\n        text-shadow: 0px -1px 0px hsla(0,0%,0%,.8);\n        text-align: center;\n        height: 60px;\n        overflow: hidden;\n\n        a {\n            display: inline-block;\n            color: white;\n            text-decoration: none;\n            font-size: 11px;\n            padding: 10px;\n            width: 50px; height: 40px;\n            text-align: center;\n            border-right:  1px solid hsl(210,35%,10%);\n            border-left:  1px solid hsl(210,35%,30%);\n            @include transition(all 100ms ease-in);\n\n            &:active {\n                background-color: hsl(210,35%,30%);\n                border-left: 1px solid hsl(210,35%,40%);\n            }\n\n            &:hover {\n                background-color: hsl(210,25%,20%);\n                @include box-shadow(inset 0px 3px 8px hsla(0,0%,0%,.8));\n                border-left: 1px solid hsl(210,35%,40%);\n            }\n\n            &:first-child {\n                border-left: none;\n            }\n            &:last-child {\n                border-right: none;\n            }\n        }\n\n        &.expanded {\n            display: block;\n        }\n    }\n}\n.comment, .message { //For the funky styles on comments/messages\n    .entry {\n        margin-right: 50px;\n    }\n    .child {\n        .options_link {\n            top: 8px; //Instead of 35px, for normal styles, we want 8px for these, as they are typically much smaller\n        }\n    }\n    .options_expando {\n        margin: 10px -50px 10px 0px; //Overwrite the link styles\n    }\n}\n.message {\n\n    .options_expando {\n        margin: 25px -55px 10px -5px;\n    }\n}\n\n.options_icons {\n    display: block;\n    width: 24px;\n    height: 24px;\n    margin: {\n        left: auto;\n        right: auto;\n        bottom: 5px;\n    }\n}\n.email-icon {\n    @extend .options_icons;\n    @include sprite($static + 'email.png');\n}\n.report-icon {\n    @extend .options_icons;\n    @include sprite($static + 'report.png');\n}\n.save-icon {\n    @extend .options_icons;\n    @include sprite($static + 'save.png');\n}\n.unsave-icon {\n    @extend .options_icons;\n    @include sprite($static + 'unsave.png');\n}\n.domain-icon {\n    @extend .options_icons;\n    @include sprite($static + 'domain.png');\n}\n.edit-icon {\n    @extend .options_icons;\n    @include sprite($static + 'edit.png');\n}\n.reply-icon {\n    @extend .options_icons;\n    @include sprite($static + 'reply.png');\n}\n.permalink-icon {\n     @extend .options_icons;\n    @include sprite($static + 'permalink.png');\n}\n.collapse-icon {\n    @extend .options_icons;\n    @include sprite($static + 'collapse.png');\n}\n.context-icon {\n    @extend .options_icons;\n    @include sprite($static + 'context.png');\n}\n.parent-icon {\n    @extend .options_icons;\n    @include sprite($static + 'context.png');\n}\n.unread-icon {\n    @extend .options_icons;\n    @include sprite($static + 'unread.png');\n}\n.hide-icon {\n    @extend .options_icons;\n    @include sprite($static + 'hide.png');\n}\n.unhide-icon {\n    @extend .options_icons;\n    @include sprite($static + 'unhide.png');\n}\n\n/*Toolbar*/\n#topbar {\n    @include vertical_gradient(#CEE3F8, #A8C4E0);\n    border-bottom: 1px solid #7599BD; border-top: 1px solid #DCEAF7;\n    @include box-sizing(border-box);\n    padding: 0px 10px; //Left and right padding\n    height: 40px;\n    position: relative;\n\n    #header-img {\n        height: 32px;\n        width: auto;\n    }\n\n    .left {\n        position: absolute;\n        left:  0;\n        bottom: 3px;\n        overflow: hidden;\n        max-height: 40px;\n        z-index: 1;\n    }\n    .right {\n        position: absolute;\n        right: 10px;\n        bottom: 1px;\n        z-index: 3;\n    }\n\n    & > h1 {\n        color: #444;\n        font-size: 18px;\n        font-weight: bold;\n        text-align: center;\n        text-overflow: ellipsis;\n        white-space: nowrap;\n        overflow: hidden;\n        margin: 0 10px;\n        padding: 0;\n        padding-top: 16px;\n        text-shadow: hsla(0, 100%, 100%, 0.5) 0px 1px 0px, hsla(0, 0%, 0%, 0.1) 0px -1px 0px;\n        @include box-flex(1); //This box will flex to fill avalible space\n    }\n}\n\n#topbar > h1 a {\n    position: relative;\n    color: inherit;\n    text-decoration: inherit;\n    z-index: 2;\n}\nbody[orient=\"landscape\"] > #topbar > h1 {\n    margin-left: -125px;\n    width: 250px;\n}\n\n#topbar > .right > .button {\n    padding-top: 8px; padding-bottom: 3px;\n}\n#topbar > .right > .button:first-child {\n\tmargin-right: 5px;\n}\n#topbar > .right > #mail {\n    width: 30px;\n    height: 30px;\n    display: inline-block;\n\n    &.nohavemail {\n        @include sprite($static + 'nomail.png');\n\n        &:active, &:hover {\n            @include sprite($static + 'nomail-active.png');\n        }\n    }\n    &.havemail {\n        @include sprite($static + 'havemail.png');\n\n        &:active, &:hover {\n            @include sprite($static + 'havemail-active.png');\n        }\n    }\n}\n#topbar > .right > #modmail {\n    width: 30px;\n    height: 30px;\n    display: inline-block;\n\n    &.nohavemail {\n        @include sprite($static + 'modmail.png');\n\n        &:active, &:hover {\n            @include sprite($static + 'modmail-active.png');\n        }\n    }\n    &.havemail {\n        @include sprite($static + 'newmodmail.png');\n\n        &:active, &:hover {\n            @include sprite($static + 'newmodmail-active.png');\n        }\n    }\n}\n\n.topbar-options {\n    width: 30px;\n    height: 30px;\n    display: inline-block;\n    @include sprite($static + 'menu-options.png');\n\n    &.active, &:hover, &:active {\n        @include sprite($static + 'menu-options-active.png');\n    }\n}\n#top_menu {\n    position: absolute;\n    right: 5px; top: 44px;\n    background-color: white;\n    border: 1px solid rgba(27,47,94,.4); border-top: 0px;\n    -webkit-border-bottom-left-radius: 10px; -moz-border-radius-bottomleft: 10px;\n    -webkit-border-bottom-right-radius: 10px; -moz-border-radius-bottomright: 10px;\n    border-bottom-left-radius: 10px; \n    border-bottom-right-radius: 10px;\n    @include box-shadow(0px 0px 8px rgba(0,0,0,.3) );\n    z-index: 5;\n    display: none;\n}\n#top_menu > .menuitem {\n    padding: 5px;\n}\n#top_menu > .menuitem.bottm-bar {\n    border-bottom: 1px solid rgba(27,47,94,.4);\n}\n#top_menu > .menuitem a {\n    text-decoration: none;\n    color: #222;\n    font-weight: bold;\n}\n\n.status { \n    color: red; \n    margin-left: 20px; \n}\n\n/*Subtoolbar (eg hot)*/\n.subtoolbar {\n    @include box-sizing(border-box);\n    height: 32px;\n    @include vertical_gradient(white, #ccc);\n    border-bottom: 1px solid #bbb;\n    padding: 6px;\n    text-overflow: ellipsis;\n    overflow: hidden;\n}\n.subtoolbar > ul {\n    list-style-type: none;\n    margin: 0;  padding: 0;\n}\n.subtoolbar > ul > li {\n    display: inline-block;\n    text-overflow: ellipsis;\n    overflow: hidden;\n}\n.subtoolbar > ul > li a {\n    color: rgb(76, 86, 108);\n    font-weight: bold;\n    text-decoration: none;\n    font-size: 12px; line-height: 20px;\n    margin: 0;\n    padding: 3px 10px;\n    text-overflow: ellipsis;\n    overflow: hidden;\n}\n.subtoolbar > ul > li.selected a {\n    @include vertical_gradient(#ddd, #aaa);\n    @include border-radius(8px);\n    border: 1px solid #aaa;\n    padding-top: 2px; \n    padding-bottom: 1px; \n    @include box-shadow(0px 1px 1px rgba(255,255,255,.8));\n}\n/*Things*/\n/*Arrows*/\n.link , .comment, .message {\n    .arrow {\n        width: 28px; height: 28px;\n        cursor: pointer;\n        display: block;\n        margin: 1px auto 0px;\n        outline: none;\n\n        //Unvoted\n        &.up {\n            @include sprite($static + 'upvote.png');\n        }\n        &.down {\n            @include sprite($static + 'downvote.png');\n        }\n        //Voted\n        &.upmod {\n            @include sprite($static + 'upvote-active.png');\n        }\n        &.downmod {\n            @include sprite($static + 'downvote-active.png');\n        }\n    }\n}\n\n/*Links*/\n.link {\n    min-height: 70px;\n    border-bottom: 1px solid #999; \n    border-top: 1px solid #ddd;\n    padding: 5px 5px;\n    padding-bottom: 0px; \n    @include box-sizing(border-box);\n    background: rgba(255,255,255,.6);\n    position: relative;\n    overflow: hidden;\n}\n.link:first-child {\n    border-top: none;\n}\n.link:nth-child(odd) {\n    background: rgba(206, 227, 248, .5);\n}\n/* Voting stuff */\n.link > .rank {\n    float: left;\n    margin-top: 17px;\n    font-size: 12px;\n    color: #aaa;\n}\n.link > .midcol {\n    float: left;\n    width: 25px;\n    margin: 0 10px 1px 0px;\n    padding-bottom: 5px; \n    position: relative;\n}\n\n.link > .entry .score,\n.link > .entry.likes  .score.unvoted,\n.link > .entry.dislikes  .score.unvoted  {\n    display:none;\n}\n\n.link > .entry  .score.unvoted,\n.link > .entry.likes  .score.likes,\n.link > .entry.dislikes  .score.dislikes\n{\n    display:inline;\n    font-weight: bold;\n}\n.link > .entry.likes  .score.likes {\n    color: #E07A7A;\n}\n.link > .entry.dislikes  .score.dislikes {\n    color: #7272D1;\n}\n\n/* experimental */\n.link .rank { display: none; }\n.link .modcol { float: left; }\n\n.comment { \n    position: relative; \n}\n.comment > .entry > .tagline .score {\n    display:none;\n}\n.comment > .entry.unvoted > .tagline .score.unvoted,\n.comment > .entry.likes > .tagline .score.likes,\n.comment > .entry.dislikes > .tagline .score.dislikes\n{\n    display:inline;\n}\n\n\n/** Vote up **/\n.link > .midcol.likes > .score {\n    color: #E07A7A;\n}\n\n/** Vote down **/\n.link > .midcol.dislikes > .score {\n    color: #7272D1;\n}\n/*Image*/\n.link .thumbnail {\n    float: right; \n    margin: 0 0 5px 5px;\n    overflow: hidden;\n    max-height: 50px; \n}\n\n.link .thumbnail img {\n    max-width: 50px;\n    max-height: 50px;\n}\n\n/* Entry*/\n\n.link .entry { \n    margin: 0px 50px 3px 0px; \n}\n\n.link a { \n    text-decoration: none;\n    color: #517191;\n    color: #369;\n}\n\n.link  p.title {\n    margin: 0; padding: 0;\n    text-overflow: ellipsis; \n    word-wrap: break-word;\n    font-size: .8em;\n    font-weight: bold;\n}\n.link  p.title > a {\n    text-overflow: ellipsis; overflow: hidden;\n    color: #25A; \n}\n.link.stickied p.title > a {\n    color: #228822;\n}\n\n.link .domain {\n    color: hsl(0,0%,45%);\n    font-size: 9px;\n    margin: {\n        left: 5px;\n    }\n\n    a, a:hover {\n        color: inherit;\n\n    }\n}\n\n.link .tagline {\n    margin: 2px 0 5px;\n    padding: 0;\n    padding-top: 2px; \n    font-size: 10px;\n    color: #333;\n}\n\n.link .tagline > span { \n    margin-right: 2px;\n}\n\n\n.link .tagline a {\n    font-weight: bold; \n}\n\n.link .tagline .stickied-tagline {\n    color: #228822;\n}\n\n/*Expando*/\n.link  .expando-button {\n    float: left;\n    display: block;\n    height: auto; line-height: inherit;\n    margin: 3px 10px 2px 0;\n    width: 30px;\n    height: 30px;\n    @include sprite($static + 'selftext.png');\n\n    &.expanded {\n        @include sprite($static + 'selftext-active.png');\n    }\n}\n\n.link > .expando {\n    clear: both;\n    margin: 5px 0; \n    margin-bottom: 30px;\n    border: 1px solid #999;\n    background: #ddd;\n    padding: 5px;\n    @include border-radius(8px);\n    font-size: 11px;\n}\n\n.link > .thing_options { \n    font-size: x-small; \n    margin: none;\n\t display: block;\n\t float: left;\n\t clear: left;\n\t margin: 2px 0px 0px 10px;\n}\n.link > .thing_options a{ \n}\n\n.nsfw-warning {\n    @include border-radius(3px);\n    color: #ac3939;\n    text-decoration: none;\n    font-weight: normal;\n    font-size: 9px;\n    margin-left: 5px;\n    padding: 0 2px;\n    border: 1px solid #d27979 !important;\n}\n\n/* Comment count */\n.commentcount {\n    float: right;\n    margin: 5px;\n    width: 45px;\n    text-align: right;\n}\n.commentcount > .comments {\n    border: 8px solid transparent;\n    @include border-image(url($static + 'border-button.png') 8 fill);\n\n    color: white; font-family: inherit; font-size: 12px;\n    font-weight: bold;\n    text-decoration: none;\n    text-shadow: 0px 1px 1px rgba(255,255,255,.1), 0px -1px 1px rgba(0,0,0,.4);\n\n    &:active, &:hover, &[selected], &.preloaded {\n        @include border-image(url($static + 'border-button-active.png') 8 fill);\n    }\n}\n/* Comment styles */\n.commentarea > h1 {\n    color: rgb(76,86,108);\n    font-size: 17px;\n    margin: 10px 10px 5px;\n    border-bottom: 1px solid rgba(0,0,0,.2);\n    @include box-shadow( 0px 1px 1px rgba(255,255,255,.4));\n}\n.commentarea > .menuarea {\n\tdisplay: none; /*TODO: Make dropdown menu*/\n}\n.commentarea > .main-form-title {\n    color: rgb(76,86,108);\n    font-size: 17px;\n    font-weight: bold;\n    margin: 0 10px;\n}\n.commentarea > .usertext {\n    background: white;\n    margin:  0 10px 5px;\n    border: 1px solid rgb(217,217,217);\n    @include border-radius(8px)\n}\n.commentarea > .usertext textarea {\n    margin: 0; padding: 5px;\n    width: 100%;\n    height: 100px;\n    border: none;\n    @include box-sizing(border-box);\n    @include border-radius(8px);\n    border-bottom: 1px solid rgb(217,217,217);\n\n}\n.cancel,\n.save {\n    float: right;\n    padding: 0 5px !important;\n}\n.save {\n\tmargin-left: 5px;\n}\n/* Errors */\n.error {\n\tcolor: red;\n}\n.content > .error {\n\tcolor: hsla(0, 100%, 100%, 0.9);\n\tfont-size: 25px;\n\tmargin: 10px;\n\ttext-align: center;\n\ttext-shadow: hsla(0, 0%, 0%, 0.15) 0px -1px 0px;\n}\n\n.help-toggle {\n    float: left;\n    margin: {\n        top: 3px;\n    }\n}\n.bottom-area {\npadding: 5px;\n}\n.markhelp-parent {\n    display: none;\n}\n.markhelp {\n   width: 100%;\n   border-collapse: collapse;\n}\n.markhelp tbody {\n}\n.markhelp  th {\n    background: rgb(217,217,217);\n}\n.markhelp th:first-child {\n    -webkit-border-top-left-radius: 8px; \n    -moz-border-radius-topleft: 8px;\n    border-top-left-radius: 8px;\n}\n.markhelp th:last-child {\n    -webkit-border-top-right-radius: 8px; \n    -moz-border-radius-topright: 8px;\n    border-top-right-radius: 8px;\n}\n.markhelp tr:nth-child(odd) td {\n    background: rgba(0,0,100,.1);\n}\n.markhelp td {\n    border: 1px solid rgb(217,217,217);\n    padding: 5px;\n}\n.markhelp tr:last-child td:first-child {\n    -webkit-border-bottom-left-radius: 8px; \n    -moz-border-radius-bottomleft: 8px;\n    border-bottom-left-radius: 8px;\n}\n.markhelp tr:last-child td:last-child {\n    -webkit-border-bottom-right-radius: 8px; \n    -moz-border-radius-bottomright: 8px;\n    border-bottom-right-radius: 8px;\n}\n/*Cloned comment reply */\n.usertext textarea {\n    margin: 0; padding: 5px;\n    border: 1px solid rgb(217,217,217);\n    width: 100%;\n    min-height: 100px;\n    @include border-radius(5px);\n    @include box-sizing(border-box);\n}\n.child form.usertext.cloneable {\n\tmargin: 5px;\n}\n\n/**Actual comments*/\n.comment {\n    background: white;\n    border: 1px solid rgb(217,217,217);\n    margin: 10px;\n    @include border-radius(8px)\n}\n.comment > .midcol {\n    float: left;\n    margin: 7px;\n    overflow: hidden;\n}\n.comment > .entry > .tagline {\n    font-size: 11px;\n    padding-bottom: 2px; \n}\n.comment > .admin_takedown {\n    background-color: #F7F7F7;\n    color: #888888;\n    \n    a:link {\n        color: #326699;\n    }\n}\n\n.child .comment { \n    margin: 4px; \n    margin-top: 0px;\n    -webkit-border-top-right-radius: 0px;\n    -moz-border-radius-topright: 0px;\n}\n\n\n.comment.collapsed .child,\n.comment.collapsed .usertext,\n.comment.collapsed .midcol,\n.comment.collapsed .button,\n.comment.collapsed .options_link,\n.comment.collapsed .options_expando\n{\n    display: none;\n}\n\n.comment.collapsed { \n    font-style: italcs;\n}\n\n.comment.collapsed .tagline { \n    margin-left: 20px; \n    font-style: italcs;\n    color: #AAA; \n}\n\n\n/** gilding */\n.gilded-icon {\n    position: relative;\n    display: inline-block;\n    margin: 0 0 -15px 8px;\n    top: -8px;\n    color: #99895F;\n    font-size: .9em;\n    vertical-align: middle;\n}\n\n.gilded-icon:before {\n    display: inline-block;\n    content: '';\n    background-image: url(../gold-coin.png);  /* SPRITE */\n    background-repeat: no-repeat;\n    height: 14px;\n    width: 13px;\n    margin-right: 2px;\n    vertical-align: -3px;\n}\n\n.user-gilded > .entry .gilded-icon:before {\n    width: 23px;\n}\n\nbody.post-under-6h-old .gilded-icon {\n    opacity: .55;\n}\n\n\n/** messages and inbox */\n.message {\n    background: white;\n    position: relative; \n    border: 1px solid rgb(217,217,217);\n    margin: 10px;\n    @include border-radius(8px);\n    padding: 5px;\n}\n.message > .midcol {\n    float: left;\n    margin: 10px;\n    overflow: hidden;\n}\n\n.message.unread {\n    background-color: #FFFFAA;\n}\n\n.message .correspondent {\n    @include vertical_gradient(hsl(210, 75%, 89%), hsl(209, 47%, 74%));\n    /* TODO */\n    margin-right: 10px;\n    padding: 2px 5px;\n    @include border-radius(15px);\n}\n.message .correspondent a {\n    text-decoration: none;\n}\n\n.message .message .subject { \n    display: none; \n}\n\n\n.message > .entry > .tagline {\n    font-size: 11px;\n    padding-bottom: 2px; margin-bottom: 2px;\n}\n.message > .entry .usertext-body, .message > .entry .md {\n    font-size: 11px;\n    word-wrap: break-word;\n}\n.message > .metabuttons {\n    float: right;\n    margin: 10px;\n}\n\n.message .subject {\n    font-weight: bold;\n\t font-size: 13px;\n\t border-bottom: 1px solid rgb(217,217,217);\n\t padding: 5px; overflow: hidden; \n}\n\n.message .subject a {\n    margin-left: 5px;\n}\n.message .subject .correspondent a {\n    margin-left: 0;\n}\n\n/* subreddit */\n\n.link .subreddit { \n    background-color: transparent;\n    margin: 0px; \n}\n\n.subreddit { \n    background-color: white;\n    @include border-radius(5px);\n    margin: 5px; \n}\n\n.subreddit p.title { \n    display: block;\n    margin-left: 35px;\n    margin-right: 30px; \n}\n\n.subreddit  a.title {\n    display: block;\n    margin: 0; padding: 0;\n    text-overflow: ellipsis; \n    word-wrap: break-word;\n    font-size: small;\n    font-weight: bold;\n    text-overflow: ellipsis; overflow: hidden;\n    color: #25A; \n    text-decoration: none;\n}\n.subreddit .title a.domain { \n    font-size: x-small;\n    color: #AAA; \n    font-style: italic; \n    display: block; \n}\n\n.subreddit .tagline { \n    font-size: x-small;\n    color: #666;\n}\n\n.subreddit .button.active { \n    @include vertical_gradient(#BFD0E0, #80A2C4);\n}\n\n.subreddit > .entry .score,\n.subreddit > .entry.likes  .score.unvoted,\n.subreddit > .entry.dislikes  .score.unvoted  {\n    display:none;\n}\n\n.subreddit > .entry  .score.unvoted,\n.subreddit > .entry.likes  .score.likes,\n.subreddit > .entry.dislikes  .score.dislikes\n{\n    display:inline;\n}\n\n.subreddit .midcol .button.add,\n.subreddit .midcol .button.remove { \n    font-family: courier;\n    font-size: small; \n}\n\n.subreddit .midcol { \n    float: left; \n}\n\n.subreddit .midcol .button { \n    display: none;\n    margin: 4px; \n}\n.subreddit .midcol .button.active { \n    display: block;\n    width: auto; \n    height: auto;\n    padding: 0px 9px; \n}\n.subreddit .expando-button { \n    float: right; \n    height: 100%; \n}\n\n.subreddit .description { \n    border-top: 1px solid #AAA;\n    margin-top: 2px;\n    padding-top: 2px; \n    margin-left: 0px;\n    padding-left: 10px; \n}\n\n/* Compose */\n#compose-message {\n\tbackground: white;\n\tborder: 1px solid rgb(217,217,217); border-top: 0px;\n\tmargin: 10px; margin-top: 0;\n\tpadding: 10px;\n\t-webkit-border-bottom-left-radius: 8px; -webkit-border-bottom-right-radius: 8px;\n    -moz-border-radius-bottomleft: 8px; -moz-border-radius-bottomright: 8px;\n}\n#compose-message label {\n\tdisplay: block;\n\tfont-size: 17px;\n\tfont-weight: bold;\n}\n#compose-message input[type=\"text\"] {\n    @include box-sizing(border-box);\n    border: 1px solid rgb(117,117,117);\n    @include border-radius(5px);\n    margin-bottom: 5px;\n    padding: 5px;\n    width: 100%;\n}\n#compose-message textarea {\n\tborder-color: rgb(117,117,117);\n\theight: 200px;\n}\n\n.comment > .entry .usertext-body {\n    font-size: 11px;\n\t word-wrap: break-word;\n}\n.comment > .entry .usertext-edit {\n    margin-left: 42px;\n}\n.comment > .metabuttons {\n    float: right;\n    margin: 10px;\n}\n/*Child comment specific styles*/\n/*Reduce the bottom margin on the last child comment in a thread, to make viewing easier.*/\n.child .comment {\n    margin-right: -1px; \n}\n.child .comment:last-child {\n    margin-bottom: 2px;\n}\n\n.comment > .morecomments {\n    margin: 5px;\n    text-align: right;\n}\n.comment > .morecomments > a {\n\n}\n/*Link colors*/\n.tagline .submitter {\n    color: blue;\n}\n.tagline .friend {\n    color: orange; /*Why not orangered? Because orangered can look very red on a mobile*/\n}\n.tagline .moderator {\n    color: #282;\n}\n.tagline .admin {\n    color: #F01;\n}\n\n.tagline .userattrs .cakeday {\n    display: inline-block;\n    text-indent: -9999px;\n    width: 11px;\n    height: 8px;\n    background-image: url(../cake.png); /* SPRITE */\n    vertical-align: middle;\n}\n\n/*Loading spinner, yay CSS animation*/\n@-webkit-keyframes rotateThis {\n  from {-webkit-transform:scale(0.75) rotate(0deg);}\n  to {-webkit-transform:scale(0.75) rotate(360deg);}\n} \n.loading { width: 100%; background-color: white; text-align: center; }\n.loading img {\n\t-webkit-animation-name: rotateThis;\n  -webkit-animation-duration: .5s;\n  -webkit-animation-iteration-count:infinite;\n  -webkit-animation-timing-function:linear;\n}\n\n.throbber {\n    display: none;\n    margin: 0 2px;\n    background: url($static + 'throbber.gif') no-repeat;\n    width: 18px;\n    height: 18px;\n}\n.working .throbber { display: inline-block; }\n\n/* Login and Register */\n#login_login, #login_reg {\n    background: white;\n    border: 1px solid rgb(217,217,217);\n    margin: 10px;\n    -webkit-border-bottom-left-radius: 8px; -webkit-border-bottom-right-radius: 8px;\n    -moz-border-radius-bottomleft: 8px; -moz-border-radius-bottomright: 8px;\n    max-width: 350px;\n    margin-left: auto;\n    margin-right: auto; \n}\n#login_login > div, #login_reg > div {\n\tpadding: 10px;\n}\n#login_login > div > ul, #login_reg > div > ul {\n\tlist-style-type: none;\n\tpadding: 0;\n\tmargin: 0 0 10px;\n}\n#login_login > div > ul li label, #login_reg > div > ul li label {\n\tdisplay: block;\n\tfont-size: 17px;\n\tfont-weight: bold;\n}\n#login_login input[type=\"text\"], #login_login input[type=\"password\"], #login_reg input[type=\"text\"], #login_reg input[type=\"password\"] {\n    width: 100%;\n    margin: 0 0 5px;\n    @include border-radius(5px);\n    border: 1px solid rgb(117,117,117); /*It was the coins fault!*/\n    font-size:17px;\n    padding: 5px;\n    @include box-sizing(border-box);\n}\n#login_login > div > ul li input[type=\"checkbox\"] + label, #login_reg > div > ul li input[type=\"checkbox\"] + label {\n\tdisplay: inline;\n}\n\n.user-form .submit * {\n    vertical-align: middle;\n}\n\n/* takdown page (sigh) */\n.infobar.red img { \n    float: left;\n}\n\n.infobar.red { \n    border: 1px solid red;\n    padding: 10px;\n    margin: 5px;\n    background-color: #FFA177;\n}\n\n.clear { \n    clear: both; \n}\n.clearleft { \n    clear: left; \n}\n\n.cover {\n    position: fixed;\n    left: 0px;\n    top: 0px; \n    width: 100%;\n    height: 100%;\n    background-color: gray;\n    opacity: .3;\n    z-index: 1000;\n}\n\n.popup {\n    position: absolute;\n    top: 75px;\n    left: 0;\n    @include border-radius(30px);\n    background-color: white;\n    text-align: left;\n    z-index: 1001;\n    padding: 10px;\n    border-color: #B2B2B2 black black #B2B2B2;\n    border-style: solid;\n    border-width: 1px;\n    margin-left: auto;\n    margin-right: auto;\n    max-width: 350px; \n}\n\n.popup h1 {\n    text-align: center;\n    font-size: large; \n    font-weight: normal;\n    color: orangered;\n}\n\n/* Submit links */\n#newlink {\n\tbackground: white;\n\tborder: 1px solid hsl(0, 0%, 85%);\n\tmargin: 10px;\n\t-webkit-border-bottom-left-radius: 8px;\t-webkit-border-bottom-right-radius: 8px;\n    -moz-border-radius-bottomleft: 8px; -moz-border-radius-bottomright: 8px;\n}\n#newlink .save {\n\tmargin: 8px;\n}\n/** Tab switcher **/\n#newlink .tabmenu {\n\tdisplay: -webkit-box;\n    display: -moz-box;\n\t-webkit-box-orient: horizontal;\n    -moz-box-orient: horizontal;\n\tmargin: 10px; padding: 0;\n\t\n}\n#newlink .tabmenu li {\n    display: block;\n    webkit-box-flex: 1;\n    @include vertical_gradient(hsl(0, 0%, 85%), hsl(0, 0%, 70%));\n    border: 1px solid hsl(0, 0%, 60%);\n    position: relative;\n}\n#newlink .tabmenu li a {\n\twidth: 100%; height: 100%;\n    @include box-sizing(border-box);\n\tdisplay: block;\n\tpadding: 5px;\n\tcolor: hsl(0, 0%, 30%);\n\ttext-shadow: hsla(0, 100%, 100%, 0.40) 0px 1px 1px;\n\ttext-decoration: none;\n\tfont-weight: bold;\n}\n#newlink .tabmenu li:first-child {\n\t-webkit-border-bottom-left-radius: 5px;\t-webkit-border-top-left-radius: 5px;\n    -moz-border-radius-bottomleft: 5px; -moz-border-radius-topleft: 5px;\n}\n#newlink .tabmenu li:last-child {\n\t-webkit-border-bottom-right-radius: 5px; -webkit-border-top-right-radius: 5px;\n    -moz-border-radius-bottomright: 5px; -moz-border-radius-topright: 5px;\n\tborder-left-color: hsl(0, 0%, 80%);\n}\n#newlink li.selected {\n    @include vertical_gradient(hsl(0, 0%, 50%), hsl(0, 0%, 70%));\n}\n#newlink li.selected a {\n    text-shadow: hsla(0, 0%, 0%, 0.40) 0px -1px 1px;\n    color: hsl(0, 0%, 95%);\n}\n#newlink .spacer {\n    margin-bottom: 5px;\n}\n#newlink .infobar {\n    margin: 5px;\n}\n/* Fields */\n#newlink textarea, #newlink input[type=\"text\"], #newlink input[type=\"url\"] {\n    border: 1px solid hsl(0, 0%, 60%);\n}\n#newlink .roundfield {\n    position: relative;\n    padding: 0px 5px;\n}\n#newlink .roundfield-content textarea {\n    @include box-sizing(border-box);\n    width: 100%;\n    height: 5em;\n    @include border-radius(5px);\n}\n#newlink .roundfield-content input[type=\"text\"], #newlink .roundfield-content input[type=\"url\"] {\n    @include box-sizing(border-box);\n\twidth: 100%;\n\theight: 2em;\n    @include border-radius(5px);\n}\n#newlink .title {\n\tfont-weight: bold;\n}\n/* Individual sections */\n#url-field .button {\n\tfloat: right;\n\tmargin-top: 5px;\n}\n#url-field .title-status {\n\tbackground: hsl(0, 0%, 90%);\n\tborder: 1px solid hsl(0, 0%, 50%);\n\tpadding: 2px 4px;\n\tmargin-top: 5px;\n\tdisplay: inline-block;\n}\n\n#suggested-reddits ul {\n    background: hsl(0, 0%, 90%);\n    border: 1px solid hsl(0, 0%, 50%);\n    padding: 8px;\n    @include border-radius(8px);\n}\n#suggested-reddits ul li {\n\tdisplay: inline;\n}\n#suggested-reddits ul li a {\n    @include vertical_gradient(hsl(210, 35%, 81%), hsl(210, 37%, 64%));\n    @include border-radius(10px);\n    display: inline-block;\n    margin: 5px;\n    padding: 3px 7px;\n    text-decoration: none;\n    border: 1px solid hsl(210, 37%, 50%);\n    color: hsl(210, 37%, 30%);\n}\n\n/* Autocomplete */\n#sr-autocomplete-area {\n\tposition: relative;\n\tz-index: 50;\n\t\n}\n#sr-drop-down {\n\tposition: absolute;\n    @include vertical_gradient(hsl(0, 0%, 90%), hsl(0, 0%, 75%));\n\tborder: 1px solid hsl(0,0%,50%);\n\t-webkit-border-bottom-left-radius: 5px;\t-webkit-border-bottom-right-radius: 5px;\n    -moz-border-radius-bottomleft: 5px; -moz-border-radius-bottomright: 5px;\n\tborder-top: 0px;\n\tdisplay: none;\n\tleft: 5px;\n\tmargin: 0px;\n\tpadding: 0px;\n\tposition: absolute;\n\tfont-weight: bold; color: hsl(0,0%,20%);\n}\n#sr-drop-down li {\n\tdisplay: block;\n\tpadding: 2px 5px;\n}\n#sr-drop-down li:hover, #sr-drop-down li:active {\n    @include vertical_gradient(hsl(209, 35%, 81%), hsl(210, 37%, 64%));\n    color: white;\n    text-shadow: hsla(0, 100%, 100%, 0.0976563) 0px 1px 1px, hsla(0, 0%, 0%, 0.398438) 0px -1px 1px;\n\t-webkit-text-stroke: 1px solid hsl(210, 28%, 44%);\n}\n#sr-drop-down li:last-child {\n\t-webkit-border-bottom-left-radius: 5px;\t-webkit-border-bottom-right-radius: 5px;\n    -moz-border-radius-bottomleft: 5px; -moz-border-radius-bottomright: 5px;\t\n}\n\n/* markdown */\n.md { overflow: auto; font-size: small; }\n.md p, .md h1 { margin: 5px 0}\n.md h1 { font-weight: bold; font-size: 100%; }\n.md h2 { font-weight: bold; font-size: 100%; }\n.md > * { margin-bottom: 0px }\n.md strong { font-weight: bold; }\n.md em { font-style: italic; }\n.md strong em { font-style: italic; font-weight: bold }\n.md img { display: none }\n.md ol, .md ul { margin: 10px 2em; }\n.md ul { list-style: disc outside }\n.md ol { list-style: decimal outside }\n.md pre { margin: 10px; }\n.md blockquote, .help blockquote {\n    border-left: 2px solid #369;\n    padding-left: 4px; \n    margin: 5px;\n    margin-right: 15px;\n}\n.md td, .md th { border: 1px solid #EEE; padding: 1px 3px; }\n.md th { font-weight: bold;  }\n.md table { margin: 5px 10px;  }\n.md center { text-align: left;  }\n\n.tryme { \n    width: 100%;\n    max-width: 280px;\n    padding: 10px;\n    background-color: white;\n    @include border-radius(10px);\n    margin: 10px auto; \n}\n\n.tryme p { \n    margin: 10px; \n    font-size: small;\n}\n\n.tryme .choices .button { \n    width: 260px;\n    display: block;\n    text-align: center;\n    margin: 10px; \n}\n\n.deepthread {\n    margin-left: 40px;\n}\n\n.morecomments a,\n.deepthread a {\n    text-decoration: none;\n    color: white\n}\n\n.morechildren { \n    margin: 5px 10px;\n}\n\n.morechildren a { \n    display: block;\n    text-align: center;\n    max-width: 350px;\n    @warn \"Evntually this needs to be swapped out for a more future-proof solution to solve the red text on 'load more->loading; for the morechildren button\";\n    color: white !important;\n}\n\na.author { margin-right: 0.5em; }\n \n.flair, .linkflair {\n    margin: {\n        top: 2px;\n        right: 0.5em;\n    }\n    padding: 0px 2px;\n    display: inline-block;\n    background: whiteSmoke;\n    color: hsl(0, 0%, 33%);\n    border: 1px solid hsl(0, 0%, 87%);\n    font-size: 9px;\n    \n    @include border-radius(2px);\n    @include box-shadow(inset 0px 1px 0px hsla(0,0%,100%,.9));\n}\n\n.linkflair {\n    font-weight: normal;\n    max-width: 10em;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n}\n\n.mobile-web-redirect-bar {\n    background: white;\n    box-sizing: border-box;\n    font-family: sans-serif;\n    font-size: 14px;\n    padding: 20px;\n    width: 100%;\n    z-index: 1000;\n    \n    a {\n        text-decoration: none;\n    }\n\n    .mobile-web-redirect-header {\n        font-size: 18px;\n        line-height: 25px;\n        margin-bottom: 20px;\n    }\n\n    .mobile-web-redirect-optin {\n        background-color: #4a7fc5;\n        border-radius: 3px;\n        box-shadow: inset 0 -3px 0 0 #3e6ab7;\n        color: white;\n        display: block;\n        font-family: \"Verdana\", sans-serif;\n        font-weight: bold;\n        line-height: 20px;\n        margin-bottom: 20px;\n        padding: 10px 0;\n        text-align: center;\n        text-transform: uppercase;\n    }\n\n    .mobile-web-redirect-optout {\n        color: #7f7f7f;\n    }\n}\n\n.commentspacer {\n    clear: both;\n}\n"
  },
  {
    "path": "r2/r2/public/static/css/components/alerts.less",
    "content": ".c-alert {\n    padding: 15px;\n    margin: 8px 0 19px;\n    border: 1px solid transparent;\n    border-radius: 2px;\n}\n\n.c-alert-danger {\n    background-color: @alert-danger-bg;\n    border-color: @alert-danger-border;\n    color: @alert-danger-color !important;\n}\n"
  },
  {
    "path": "r2/r2/public/static/css/components/animations.less",
    "content": "@keyframes spin {\n    to {\n        transform: rotate(360deg)\n    }\n}\n\n@-webkit-keyframes spin {\n    to {\n        -webkit-transform: rotate(360deg)\n    }\n}"
  },
  {
    "path": "r2/r2/public/static/css/components/buttons.less",
    "content": ".c-btn {\n    .box-sizing(border-box);\n    display: inline-block;\n    margin-bottom: 0;\n    text-align: center;\n    text-transform: uppercase;\n    font-weight: bold;\n    vertical-align: middle;\n    cursor: pointer;\n    background-image: none;\n    border: 1px solid transparent;\n    white-space: nowrap;\n    .button-size(4px; @padding-base-horizontal; 12px; 20px; 3px);\n    .no-select();\n\n    &:hover,\n    &:focus {\n        color: #fff;\n        text-decoration: none;\n    }\n\n    &:active,\n    &.active {\n        outline: 0;\n        background-image: none;\n    }\n\n    &.disabled,\n    &[disabled],\n    fieldset[disabled] & {\n        cursor: not-allowed;\n        pointer-events: none;\n        .opacity(.65);\n        box-shadow: none;\n    }\n\n}\n\n.c-btn-primary {\n    .button-variant(@text-color: #fff; @bg-color: #4f86b5; @bevel-color: #4270a2);\n}\n\n.c-btn-secondary {\n    .button-variant(@text-color: #fff, @bg-color: #c6c6c6, @bevel-color: #b8b8b8);\n}\n\n.c-btn-highlight {\n    .button-variant(@text-color: #fff; @bg-color: #DC6431; @bevel-color: #C9532B);\n}\n"
  },
  {
    "path": "r2/r2/public/static/css/components/close.less",
    "content": ".c-close {\n  display: block;\n  width: 12px;\n  height: 12px;\n  .hdpi-bg-image(@1x: data-uri('../close_x.png'); @2x: data-uri('../close_x_2x.png'));\n  background-size: 12px;\n  background-position: center;\n\n  &:hover {\n    .hdpi-bg-image(@1x: data-uri('../close_x_hover.png'); @2x: data-uri('../close_x_hover_2x.png'));\n  }\n}\n"
  },
  {
    "path": "r2/r2/public/static/css/components/colors.less",
    "content": "/* \n  do not use these colors directly! create local aliases with descriptive names\n  i.e.\n   \n  @my-background-color: @color-eggshell;\n */\n\n// Colors\n@color-warning-red: #EA0027;\n@color-orangered: #FF4500;\n@color-orange: #FF8717;\n@color-yellow: #FFD635;\n@color-highlight: #FFF03E;\n@color-gold: #D8C161;\n@color-lime: #C7E223;\n@color-green: #7CD344;\n@color-teal: #25B79F;\n@color-dark-teal: #008985;\n@color-light-blue: #24A0ED;\n@color-link-blue: #333df5;\n@color-link-purple: #551a8b;\n@color-periwinkle: #9494FF;\n@color-white: #FFFFFF;\n@color-off-white: #FCFCF7;\n@color-eggshell: #F2F1E9;\n@color-pale-grey: #E5E3DA;\n@color-grey: #B5B3AC;\n@color-semi-black: #222222;\n\n// Product-specific colors\n@color-alien-blue: #0079D3;\n@color-ads-blue: #6093BF;\n@color-ads-grey: #F8F8F8;\n\n// non-styleguide colors:\n@color-ui-blue: #336699;\n@color-text-grey: #4F4F4F;\n@color-dark-grey: #808080;\n\n// ban colors\n@color-spam: #ff8b60;\n@color-temp-ban: #ff0000;\n@color-perma-ban: #cc0000;\n@color-deleted: #c6c6c6;\n"
  },
  {
    "path": "r2/r2/public/static/css/components/components.less",
    "content": "@import \"animations.less\";\n@import \"variables.less\";\n@import \"mixins.less\";\n@import \"utils.less\";\n@import \"forms.less\";\n@import \"buttons.less\";\n@import \"tooltip.less\";\n@import \"form-states.less\";\n@import \"alerts.less\";\n@import \"strength-meter.less\";\n@import \"modal.less\";\n@import \"close.less\";\n@import \"toggles.less\";\n@import \"read-next.less\";\n@import \"infobar.less\";\n@import \"progress.less\";\n@import \"image-upload.less\";\n"
  },
  {
    "path": "r2/r2/public/static/css/components/form-states.less",
    "content": ".c-has-feedback {\n    position: relative;\n}\n\n.c-has-error {\n\n    .c-tooltip.right .tooltip-arrow {\n        border-right-color: @tooltip-danger-arrow-color;\n    }\n\n    .c-tooltip.left .tooltip-arrow {\n        border-left-color: @tooltip-danger-arrow-color;\n    }\n\n    .c-tooltip.top-right .tooltip-arrow,\n    .c-tooltip.top .tooltip-arrow {\n        border-top-color: @tooltip-danger-arrow-color;\n    }\n\n    .c-tooltip.bottom .tooltip-arrow {\n        border-bottom-color: @tooltip-danger-arrow-color;\n    }\n\n    .tooltip-inner {\n        background-color: @tooltip-danger-bg;\n    }\n\n}\n\n.c-has-success {\n\n    .c-tooltip.right .tooltip-arrow {\n        border-right-color: @tooltip-success-arrow-color;\n    }\n\n    .c-tooltip.left .tooltip-arrow {\n        border-left-color: @tooltip-success-arrow-color;\n    }\n\n    .c-tooltip.top-right .tooltip-arrow,\n    .c-tooltip.top .tooltip-arrow {\n        border-top-color: @tooltip-success-arrow-color;\n    }\n\n    .c-tooltip.bottom .tooltip-arrow {\n        border-bottom-color: @tooltip-success-arrow-color;\n    }\n\n    .tooltip-inner {\n        background-color: @tooltip-success-bg;\n    }\n\n}\n\n.c-form-control-feedback-wrapper {\n    width: calc(100% + 25px);\n    width: 110%;\n    position: absolute;\n    left: 100%;\n    top: 0;\n\n    &.inside-input {\n        margin-left: -30px;\n    }\n}\n\n.c-form-control-feedback {\n    @vertical-space: ((@input-height-base - 20px) / 2);\n    display: none;\n    position: absolute;\n    left: 5px;\n    top: @vertical-space;\n    height: 20px;\n    width: 20px;\n    vertical-align: middle;\n}\n\n.c-form-control-feedback-throbber,\n.c-form-throbber:after {\n    .hdpi-bg-image(@1x: data-uri('../throbber_v2.gif'); @2x: data-uri('../throbber_v2_2x.gif'));\n\n    .cssanimations & {\n        background: none;\n        .box-sizing(border-box);\n        .animation(spin .75s linear infinite);\n        border: 2px solid @throbber-color;\n        border-bottom-color: transparent;\n        border-left-color: transparent;\n        border-radius: 100%;\n    }\n}\n\n.c-form-control-feedback-success {\n    .hdpi-bg-image(@1x: data-uri('../check.png'); @2x: data-uri('../check_2x.png'));\n}\n\n.c-form-control-feedback-error {\n    .hdpi-bg-image(@1x: data-uri('../alert.png'); @2x: data-uri('../alert_2x.png'));\n\n    &:hover {\n        .hdpi-bg-image(@1x: data-uri('../alert_mouseover.png'); @2x: data-uri('../alert_mouseover_2x.png'));\n\n        + .c-tooltip {\n            .tooltip-inner {\n                background-color: @tooltip-danger-hover-bg;\n            }\n\n            &.right .tooltip-arrow {\n                border-right-color: @tooltip-danger-hover-bg\n            }\n\n            &.left .tooltip-arrow {\n                border-left-color: @tooltip-danger-hover-bg\n            }\n\n            &.top-right .tooltip-arrow,\n            &.top .tooltip-arrow {\n                border-top-color: @tooltip-danger-hover-bg\n            }\n\n            &.bottom .tooltip-arrow {\n                border-bottom-color: @tooltip-danger-hover-bg\n            }\n        }\n    }\n}\n\n.c-has-throbber .c-form-control-feedback-throbber,\n.c-has-success .c-form-control-feedback-success,\n.c-has-error .c-form-control-feedback-error {\n    display: block;\n}\n\n.c-form-throbber {\n    display: none;\n    float: right;\n    padding: 5px 0;\n    margin-right: -25px;\n\n    &:after {\n        content: '';\n        height: 20px;\n        width: 20px;\n        display: block;\n    }\n\n    .working & {\n        display: block;\n    }\n}\n"
  },
  {
    "path": "r2/r2/public/static/css/components/forms.less",
    "content": ".c-form-control {\n    .box-sizing(border-box);\n    display: block;\n    width: 100%;\n    font-size: 14px;\n    height: @input-height-base;\n    line-height: (@input-height-base - (@padding-base-vertical * 2 + 2));\n    padding: @padding-base-vertical @padding-base-horizontal;\n    background-image: none;\n    border: 1px solid #ccc;\n    border-radius: 4px;\n    box-shadow: inset 1px 1px 2px -1px rgba(0,0,0,0.15);\n    .transition-shorthand(~\"border-color ease-in-out .15s, box-shadow ease-in-out .15s\");\n    &:focus {\n        border-color: @input-focus-color;\n        outline: 0;\n        box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 4px @input-focus-color;\n    }\n\n    .input-placeholder();\n\n    textarea& {\n        height: auto;\n        resize: vertical;\n    }\n}\n\n.c-form-group {\n    margin-bottom: 10px;\n    position: relative;\n}\n\n.c-form-inline {\n    .c-form-group {\n        display: inline-block;\n        vertical-align: top;\n        margin-bottom: 0;\n        vertical-align: middle;\n    }\n\n    .c-form-control {\n        display: inline-block;\n        vertical-align: middle;\n    }\n}\n\n.c-submit-group {\n    margin-top: @input-height-base;\n}\n\n.c-radio,\n.c-checkbox {\n    position: relative;\n    display: block;\n    font-size: 12px;\n    margin-top: 10px;\n    margin-bottom: 10px;\n\n    label {\n        font-weight: normal;\n        cursor: pointer;\n    }\n\n    &.c-input-height {\n        min-height: @input-height-base;\n    }\n}\n\n.c-radio input[type=\"radio\"],\n.c-checkbox input[type=\"checkbox\"] {\n    margin-right: .5em;\n    line-height: normal;\n\n    &:focus {\n        outline: thin dotted;\n        outline: 5px auto -webkit-focus-ring-color;\n        outline-offset: -2px;\n    }\n}\n\n.c-help-block {\n    display: block;\n\n    .c-checkbox & {\n        margin-top: 5px;\n    }\n\n    &.c-help-block-toggle {\n        display: none;\n    }\n}\n"
  },
  {
    "path": "r2/r2/public/static/css/components/image-upload.less",
    "content": ".c-image-upload-input {\n  position: absolute;\n  top: 0;\n  bottom: 0;\n  left: 0;\n  right: 0;\n  display: block;\n  margin: 0;\n  opacity: 0;\n  padding: 0;\n  width: 100%;\n\n  &:hover {\n    cursor: pointer;\n  }\n}\n\n.c-image-upload-preview-container {\n  border-width: 2px;\n  border-style: dashed;\n  border-color: lightgray;\n  display: inline-block;\n  margin: 5px 0;\n  overflow: hidden;\n  padding: 5px;\n  position: relative;\n}\n\n.c-image-upload-preview {\n  display: block;\n  margin: 0 !important;\n}\n\n.c-image-upload-btn {\n  display: block;\n  padding: 2px 8px !important;\n  font-size: 10px !important;;\n}\n"
  },
  {
    "path": "r2/r2/public/static/css/components/infobar.less",
    "content": "@infobar-legacy-color: #f6e69f;\n@infobar-legacy-color-border: orange;\n@infobar-color: #fff7d7;\n@infobar-color-border: darken(@infobar-color, 32%);\n@infobar-color-text: @color-semi-black;\n@infobar-padding: unit(@margin-small, px);\n@infobar-min-text-height: unit(@line-x-small, px) + unit(2 * @margin-x-small, px);\n@infobar-icon-size: 2 * @infobar-padding + @infobar-min-text-height;\n\n.infobar {\n  background-color: @infobar-legacy-color;\n  border-color: @infobar-legacy-color-border;\n  border-style: solid;\n  border-width: 1px;\n  font-size: small;\n  margin: 5px 305px 5px 0px;\n  padding: 5px 10px;\n\n  img {\n    display: inline;\n    vertical-align: middle;\n  }\n\n  strong {\n    font-weight: bold;\n  }\n}\n\n.reddit-infobar {\n  .box-sizing(border-box);\n  background-color: @infobar-color;\n  border-color: @infobar-color-border;\n  border-style: solid;\n  border-width: 1px;\n  margin-bottom: @infobar-padding;\n  margin-left: 0;\n  margin-top: unit(@margin-x-small, px);\n  overflow: auto;\n  padding: @infobar-padding;\n  position: relative;\n\n  .md {\n    color: @infobar-color-text;\n  }\n\n  &.with-icon {\n    min-height: @infobar-icon-size;\n    padding-left: @infobar-padding + @infobar-icon-size;\n\n    &:before {\n      content: '';\n      display: block;\n      width: @infobar-icon-size;\n      height: 100%;\n      background-color: @infobar-color-border;\n      position: absolute;\n      left: 0;\n      top: 0;\n      background-position: center;\n    }\n  }\n}\n\n.locked-infobar:before {\n  @1x: url('../infobar-icon-lock.png');\n  @2x: url('../infobar-icon-lock_2x.png');\n  @2x-bg-size: 20px 25px;\n  .hdpi-bg-image(@1x: @1x, @2x: @2x, @2x-bg-size);\n}\n\n.archived-infobar {\n  @archived-infobar-color: #FCFCFB;\n  @archived-infobar-color-border: @color-grey;\n  background-color: @archived-infobar-color;\n  border-color: @archived-infobar-color-border;\n\n  &.with-icon:before {\n    background-color: @archived-infobar-color-border;\n    @1x: url('../infobar-icon-archived.png');\n    @2x: url('../infobar-icon-archived_2x.png');\n    @2x-bg-size: 25px 24px;\n    .hdpi-bg-image(@1x: @1x, @2x: @2x, @2x-bg-size);\n  }\n}\n\n.timeout-infobar {\n  @timeout-infobar-color: #fff0f0;\n  @timeout-infobar-color-border: #e70028;\n  background-color: @timeout-infobar-color;\n  border-color: @timeout-infobar-color-border;\n\n  &.with-icon:before {\n    @1x: url('../infobar-icon-banhammer.png');\n    @2x: url('../infobar-icon-banhammer_2x.png');\n    @2x-bg-size: 20px 27px;\n    .hdpi-bg-image(@1x: @1x, @2x: @2x, @2x-bg-size);   \n    background-color: @timeout-infobar-color-border;\n  }\n}\n"
  },
  {
    "path": "r2/r2/public/static/css/components/mixins.less",
    "content": ".animation (@animation) {\n    -webkit-animation: @animation;\n    -moz-animation: @animation;\n    -ms-animation: @animation;\n    -o-animation: @animation;\n    animation: @animation;\n}\n\n.transition (@property, @duration, @function: ease, @delay: 0s) {\n    -webkit-transition: @arguments;\n    -moz-transition: @arguments;\n    -o-transition: @arguments;\n    -ms-transition: @arguments;\n    transition: @arguments;\n}\n\n.translate(@x; @y) {\n    -webkit-transform: translate(@x, @y);\n    -ms-transform: translate(@x, @y);\n    -o-transform: translate(@x, @y);\n    transform: translate(@x, @y);\n}\n\n.transition-transform(@transition) {\n    -webkit-transition: -webkit-transform @transition;\n    -moz-transition: -moz-transform @transition;\n    -o-transition: -o-transform @transition;\n    transition: transform @transition;\n}\n\n.transition-shorthand(@transition) {\n    -webkit-transition: @transition;\n    -moz-transition: @transition;\n    -o-transition: @transition;\n    -ms-transition: @transition;\n    transition: @transition;\n}\n\n.linear-gradient(@color1, @color2) {\n    background: @color1;\n    background: -moz-linear-gradient(top,  @color1 0%, @color2 100%);\n    background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,@color1), color-stop(100%, @color2));\n    background: -webkit-linear-gradient(top,  @color1 0%, @color2 100%);\n    background: -o-linear-gradient(top,  @color1 0%, @color2 100%);\n    background: -ms-linear-gradient(top,  @color1 0%, @color2 100%);\n    background: linear-gradient(to bottom,  @color1 0%, @color2 100%);\n}\n\n.box-shadow(@shadow) {\n    -webkit-box-shadow: @shadow; // iOS <4.3 & Android <4.1\n    box-shadow: @shadow;\n}\n\n.box-sizing(@sizing) {\n    box-sizing: @sizing;\n    -webkit-box-sizing: @sizing;\n    -moz-box-sizing: @sizing;\n}\n\n.transform(@transformation) {\n    transform: @transformation;\n    -webkit-transform: @transformation;\n    -moz-transform: @transformation;\n    -o-transform: @transformation;\n    -ms-transform: @transformation;\n}\n\n.opacity(@opacity) {\n    opacity: @opacity;\n    @opacity-ie: (@opacity * 100);\n    filter: ~\"alpha(opacity=@{opacity-ie})\";\n}\n\n.flex(@props) {\n  -webkit-flex: @props;\n  flex: @props;\n}\n\n.flex-wrap(@props) {\n  -webkit-flex-wrap: @props;\n  flex-wrap: @props;\n}\n\n.flex-direction(@props) {\n  -webkit-flex-direction: @props;\n  flex-direction: @props;\n}\n\n.display-flex() {\n  display: -webkit-flex;\n  display: flex;\n}\n\n.align-items(@props) {\n    -webkit-align-items: @props;\n    align-items: @props;\n}\n\n.justify-content(@props) {\n    -webkit-justify-content: @props;\n    justify-content: @props;\n}\n\n.clearfix() {\n    &:before,\n    &:after {\n        content: \" \";\n        display: table;\n    }\n    &:after {\n        clear: both;\n    }\n}\n\n.button-variant(@text-color; @bg-color; @bevel-color;) {\n    background-color: @bg-color;\n    border-bottom: 2px solid @bevel-color;\n    color: @text-color;\n\n    &:hover,\n    &:focus,\n    &:active,\n    &.disabled,\n    &[disabled] {\n        background-color: darken(@bg-color, 2.5%);\n        color: @text-color;\n    }\n\n    &:focus {\n        outline-offset: 1px;\n    }\n\n    &:active,\n    &.active {\n        border-bottom-width: 1px;\n        margin-top: 1px;\n    }\n}\n\n.button-size(@padding-vertical; @padding-horizontal; @font-size; @line-height; @border-radius) {\n    padding: @padding-vertical @padding-horizontal (@padding-vertical - 1px);\n    font-size: @font-size;\n    line-height: @line-height;\n    border-radius: @border-radius;\n}\n\n#gradient {\n    .horizontal(@start-color: #555, @end-color: #333) {\n        background-color: @end-color;\n        background-image: -moz-linear-gradient(left, @start-color, @end-color);\n        background-image: -webkit-gradient(linear, 0 0, 100% 0, from(@start-color), to(@end-color));\n        background-image: -webkit-linear-gradient(left, @start-color, @end-color);\n        background-image: -o-linear-gradient(left, @start-color, @end-color);\n        background-image: linear-gradient(to right, @start-color, @end-color);\n        background-repeat: repeat-x;\n        filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(start-colorstr='%d', end-colorstr='%d', GradientType=1)\", argb(@start-color), argb(@end-color))); // IE9 and down\n    }\n\n    .vertical(@start-color: #555, @end-color: #333) {\n        background-color: mix(@start-color, @end-color, 60%);\n        background-image: -moz-linear-gradient(top, @start-color, @end-color);\n        background-image: -webkit-gradient(linear, 0 0, 0 100%, from(@start-color), to(@end-color));\n        background-image: -webkit-linear-gradient(top, @start-color, @end-color);\n        background-image: -o-linear-gradient(top, @start-color, @end-color);\n        background-image: linear-gradient(to bottom, @start-color, @end-color);\n        background-repeat: repeat-x;\n        filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(start-colorstr='%d', end-colorstr='%d', GradientType=0)\", argb(@start-color), argb(@end-color))); // IE9 and down\n    }\n}\n\n.hdpi-bg-image(@1x; @2x; @2x-bg-size: 100%; @bg-repeat: no-repeat) {\n    background-image: @1x;\n    background-repeat: @bg-repeat;\n\n    @media\n    only screen and (min-resolution: 2dppx),\n    only screen and (-webkit-min-device-pixel-ratio: 2) {\n        background-image: @2x;\n        background-size: @2x-bg-size;\n    }\n}\n\n.input-placeholder(@color: @input-placeholder-color;) {\n    &::-moz-placeholder {\n        color: @color;\n        opacity: 1;\n    }\n\n    &:-ms-input-placeholder { color: @color; }\n    &::-webkit-input-placeholder  { color: @color; }\n}\n\n.no-select() {\n    -webkit-user-select: none;\n    -moz-user-select: none;\n    -ms-user-select: none;\n    user-select: none;\n}\n"
  },
  {
    "path": "r2/r2/public/static/css/components/modal.less",
    "content": "//\n// Modals\n// --------------------------------------------------\n\n// .modal-open      - body class for killing the scroll\n// .modal           - container to scroll within\n// .modal-dialog    - positioning shell for the actual modal\n// .modal-content   - actual modal w/ bg and corners and shit\n\n// Kill the scroll on the body\n.modal-open {\n    overflow: hidden;\n}\n\n// Container that the modal scrolls within\n.modal {\n    display: none;\n    overflow: hidden;\n    position: fixed;\n    top: 0;\n    right: 0;\n    bottom: 0;\n    left: 0;\n    z-index: @zindex-modal;\n    -webkit-overflow-scrolling: touch;\n\n    // Prevent Chrome on Windows from adding a focus outline. For details, see\n    // https://github.com/twbs/bootstrap/pull/10951.\n    outline: 0;\n\n    // When fading in the modal, animate it to slide down\n    &.fade .modal-dialog {\n        .translate(0, -25%);\n        .transition-transform(~\"0.3s ease-out\");\n    }\n    &.in .modal-dialog { .translate(0, 0) }\n}\n.modal-open .modal {\n    overflow-x: hidden;\n    overflow-y: auto;\n}\n\n// Shell div to position the modal with bottom padding\n.modal-dialog {\n    position: relative;\n    width: auto;\n    margin: 10px;\n\n    @media (min-width: @screen-sm-min) {\n        width: @modal-md;\n        margin: 30px auto;\n    }\n}\n\n// Actual modal\n.modal-content {\n    position: relative;\n    background-color: @modal-content-bg;\n    border-radius: @modal-border-radius;\n    border: 1px solid @modal-content-border-color;\n    .box-shadow(3px 3px 13px rgba(0,0,0,0.35));\n    background-clip: padding-box;\n    // Remove focus outline from opened modal\n    outline: 0;\n\n    @media (min-width: @screen-sm-min) {\n        .box-shadow(0 5px 15px rgba(0,0,0,.5));\n    }\n}\n\n// Modal background\n.modal-backdrop {\n    position: fixed;\n    top: 0;\n    right: 0;\n    bottom: 0;\n    left: 0;\n    background-color: @modal-backdrop-bg;\n    z-index: (@zindex-modal - 1);\n    // Fade for backdrop\n    &.fade { .opacity(0); }\n    &.in { .opacity(@modal-backdrop-opacity); }\n}\n\n// Modal header\n// Top section of the modal w/ title and dismiss\n.modal-header {\n    position: absolute;\n    top: 12px;\n    right: 12px;\n}\n\n// Title text\n.modal-title {\n    color: @modal-title-color;\n    display: block;\n\n    h1&,\n    h2&,\n    h3& {\n        font-size: 18px;\n        font-weight: normal;\n        margin: 12px 0 18px;\n    }\n\n    h4&,\n    h5&,\n    h6& {\n        font-size: 12px;\n        font-weight: bold;\n        margin: 4px 0 6px;\n        text-transform: uppercase;\n    }\n}\n\n// Modal body\n// Where all modal content resides (sibling of .modal-header and .modal-footer)\n.modal-body {\n    padding: @modal-padding;\n}\n\n// Footer (for actions)\n.modal-footer {\n    background: @modal-footer-bg;\n    border-bottom-left-radius: @modal-border-radius;\n    border-bottom-right-radius: @modal-border-radius;\n    border-top: 1px solid @modal-footer-border-color;\n}\n\n// Measure scrollbar width for padding body during modal show/hide\n.modal-scrollbar-measure {\n    position: absolute;\n    top: -9999px;\n    width: 50px;\n    height: 50px;\n    overflow: scroll;\n}\n"
  },
  {
    "path": "r2/r2/public/static/css/components/progress.less",
    "content": ".c-progress {\n    display: none;\n    height: 2px;\n    overflow: hidden;\n    background-color: #f5f5f5;\n    .box-shadow(~\"inset 0 1px 2px rgba(0,0,0,.1)\");\n    position: relative;\n    margin-top: -2px;\n}\n\n.c-progress-bar {\n    float: left;\n    width: 0;\n    height: 100%;\n    line-height: 2px;\n    background-color: #337ab7;\n    .box-shadow(~\"inset 0 -1px 0 rgba(0,0,0,.15)\");\n    .transition-shorthand(width .6s ease);\n}\n"
  },
  {
    "path": "r2/r2/public/static/css/components/read-next.less",
    "content": "@read-next-color-background: @color-white;\n@read-next-color-header: #EFF7FF;\n@read-next-color-button-background: lighten(@color-ui-blue, 5%);\n@read-next-color-text: @color-semi-black;\n@read-next-color-text-light: @color-dark-grey;\n@read-next-height: 100px;\n@read-next-width: 300px;\n@read-next-link-height: 60px;\n@read-next-thumbnail-size: 45px;\n@zindex-read-next: @zindex-modal - 1;\n\n@read-next-font-base: @font-x-small;\n@read-next-font-small: @font-xx-small;\n@read-next-line-base: @line-x-small;\n@read-next-line-large: @line-small;\n\n.read-next-font-size(@font:@read-next-font-base, @line:@read-next-line-base) {\n  .md-font-size(@read-next-font-base, @font, @line);\n}\n\n.read-next-container {\n  font-size: @base-font-keyword;\n}\n\n.read-next {\n  .md-base-font-size(@read-next-font-base);\n  .no-select();\n  background: @read-next-color-background;\n  border: 1px solid darken(@read-next-color-background, 15%);\n  box-sizing: border-box;\n  color: @read-next-color-text;\n  display: none;\n  height: @read-next-height;\n  position: relative;\n  width: @read-next-width;\n  z-index: @zindex-read-next;\n\n  &.active {\n    display: block;\n  }\n\n  &.fixed {\n    border-bottom-width: 0;\n    bottom: 0;\n    position: fixed;\n  }\n\n  .read-next-header {\n    background-color: @read-next-color-header;\n    border-bottom: 1px solid darken(@read-next-color-header, 5%);\n    color: @read-next-color-text-light;\n    padding-left: @margin-small * 1px;\n    padding-right: @margin-small * 1px;\n    padding-top: @margin-x-small * 1px;\n  }\n\n  .read-next-header-title {\n    .read-next-font-size(@read-next-font-base, @read-next-line-large);\n    margin-left: (@read-next-line-base + @margin-x-small) * 2px;\n    position: relative;\n    // magic number, but it looks better ;_;\n    top: -2px;\n  }\n\n  .read-next-title {\n    .read-next-font-size();\n    display: block;\n    max-height: @read-next-line-base * 3px;\n    overflow: hidden;\n    text-overflow: ellipsis;\n  }\n\n  .read-next-nav {\n    .read-next-font-size(@read-next-font-small);\n    position: absolute;\n    top: @margin-x-small * 1px;\n  }\n\n  .read-next-nav-right {\n    right: @margin-x-small * 1px;\n\n    > * {\n      margin-left: @margin-x-small * 1px;\n    }\n  }\n\n  .read-next-nav-left {\n    left: @margin-x-small * 1px;\n     \n    > * {\n      margin-right: @margin-x-small * 1px;\n    }\n  }\n\n  .read-next-dismiss,\n  .read-next-button {\n    .transform(scale(1, 1) translateY(0px));\n    .transition(all, 0.2s);\n    cursor: pointer;\n    display: inline-block;\n    height: @read-next-line-base * 1px;\n    position: relative;\n    text-align: center;\n    width: @read-next-line-base * 1px;\n\n    &:active {\n      .transform(scale(1.01, 1.01) translateY(1px));\n    }\n  }\n\n  .read-next-button {\n    background-color: @read-next-color-button-background;\n    border-radius: 50%;\n    color: @read-next-color-background;\n\n    &:active {\n      background-color: darken(@read-next-color-button-background, 5%);\n    }\n  }\n\n  .read-next-list {\n    padding: @margin-small * 1px;\n    padding-top: @margin-x-small * 1px;\n  }\n\n  .read-next-link {\n    display: none;\n    float: left;\n    height: @read-next-link-height;\n    overflow: hidden;\n    width: 100%;\n\n    &.active {\n      display: block;\n    }\n\n    .read-next-thumbnail {\n      display: block;\n      float: left;\n      height: @read-next-thumbnail-size;\n      margin-right: @margin-x-small * 1px;\n      // magic number, but it just looks better\n      margin-top: 3px; \n      width: @read-next-thumbnail-size;\n\n      img {\n        height: auto;\n        width: 100%;\n      }\n    }\n  }\n\n  .read-next-meta {\n    .read-next-font-size(@read-next-font-small);\n    color: @read-next-color-text-light;\n  }\n}\n"
  },
  {
    "path": "r2/r2/public/static/css/components/strength-meter.less",
    "content": ".strength-meter {\n    background-color: @strength-meter-bg;\n    border-radius: 5px;\n    display: none;\n    position: absolute;\n    right: 5px;\n    top: 50%;\n    margin-top: -4px;\n    width: 50px;\n    height: 8px;\n    overflow: hidden;\n    pointer-events: none;\n\n    .c-has-feedback & {\n        display: block;\n    }\n}\n\n.strength-meter-fill {\n    height: 100%;\n    width: 0;\n\n    .c-has-success & {\n        background-color: @strength-meter-fill-success;\n    }\n\n    .c-has-error & {\n        background-color: @strength-meter-fill-error;\n    }\n\n    .c-has-throbber & {\n        background-color: @strength-meter-fill-loading;\n    }\n}\n"
  },
  {
    "path": "r2/r2/public/static/css/components/toggles.less",
    "content": ".c-toggle {\n\n    &:after {\n        content: ' [+]';\n        font-family: monospace;\n        font-size: 10px;\n        vertical-align: 1px;\n    }\n\n    &.c-toggle-toggled:after {\n        content: ' [-]';\n    }\n}\n\n.c-toggle-content {\n    display: none;\n\n    &.c-toggle-content-toggled {\n        display: block;\n    }\n}\n"
  },
  {
    "path": "r2/r2/public/static/css/components/tooltip.less",
    "content": ".c-tooltip {\n    position: absolute;\n    z-index: 1002;\n    display: block;\n    visibility: visible;\n    font-size: 11px;\n    line-height: 1.4;\n    .opacity(0);\n\n    .tooltip-inner {\n        padding: 2px 8px 3px;\n        color: @tooltip-color;\n        text-decoration: none;\n        background-color: @tooltip-bg;\n        border-radius: 2px;\n    }\n\n    .tooltip-arrow {\n        position: absolute;\n        width: 0;\n        height: 0;\n        border-color: transparent;\n        border-style: solid;\n        border-width: @tooltip-arrow-width;\n    }\n\n    &.in {\n      .opacity(@tooltip-opacity);\n    }\n\n    &.top {\n        padding: @tooltip-arrow-width 0;\n    }\n\n    &.right {\n        padding: 0 @tooltip-arrow-width;\n    }\n\n    &.bottom {\n        padding: @tooltip-arrow-width 0;\n    }\n\n    &.left {\n        padding: 0 @tooltip-arrow-width;\n    }\n\n    &.top .tooltip-arrow {\n        bottom: 2px;\n        left: 50%;\n        margin-left: -@tooltip-arrow-width;\n        border-bottom-width: 0;\n        border-top-color: @tooltip-arrow-color;\n    }\n\n    &.top-right {\n        .tooltip-arrow {\n            bottom: -@tooltip-arrow-width;\n            left: auto;\n            right: 2px;\n            border-bottom-width: 0;\n            border-top-color: @tooltip-arrow-color;\n        }\n    }\n\n    &.right .tooltip-arrow {\n        top: 50%;\n        left: 2px;\n        margin-top: -@tooltip-arrow-width;\n        border-left-width: 0;\n        border-right-color: @tooltip-arrow-color;\n    }\n\n    &.left .tooltip-arrow {\n        top: 50%;\n        right: 2px;\n        margin-top: -@tooltip-arrow-width;\n        border-right-width: 0;\n        border-left-color: @tooltip-arrow-color;\n    }\n\n    &.bottom .tooltip-arrow {\n        top: 2px;\n        left: 50%;\n        margin-left: -@tooltip-arrow-width;\n        border-top-width: 0;\n        border-bottom-color: @tooltip-arrow-color;\n    }\n\n}\n"
  },
  {
    "path": "r2/r2/public/static/css/components/utils.less",
    "content": ".c-hidden {\n  display: none;\n}\n\n.c-clearfix {\n    .clearfix();\n}\n\n.c-pull-right {\n    float: right;\n}\n\n.c-pull-left {\n    float: left;\n}\n\n.c-hide-text {\n    text-indent: 100%;\n    white-space: nowrap;\n    overflow: hidden;\n}\n\n.c-hidden {\n    display: none;\n}\n\n.fade {\n    opacity: 0;\n    .transition-shorthand(opacity .15s linear);\n\n    &.in {\n        opacity: 1;\n    }\n}\n"
  },
  {
    "path": "r2/r2/public/static/css/components/variables.less",
    "content": "@import \"colors.less\";\n\n@reddit-sidebar-width: 300px;\n\n// States\n\n@brand-success: #6ec02a;\n@brand-danger: #d4473f;\n\n// Shared\n\n@padding-base-vertical: 6px;\n@padding-base-horizontal: 12px;\n\n// Forms\n\n@input-height-base: 34px;\n@input-focus-color: #3c80d3;\n@input-placeholder-color: #a6a6a6;\n\n// Tooltips\n\n@tooltip-max-width: 200px;\n@tooltip-color: #fff;\n@tooltip-bg: #000;\n@tooltip-success-bg: @brand-success;\n@tooltip-success-hover-bg: @tooltip-success-bg;\n@tooltip-danger-bg: @brand-danger;\n@tooltip-danger-hover-bg: rgb(191, 64, 57);\n@tooltip-opacity: 1;\n@tooltip-arrow-width: 8px;\n@tooltip-arrow-color: @tooltip-bg;\n@tooltip-success-arrow-color: @tooltip-success-bg;\n@tooltip-danger-arrow-color: @tooltip-danger-bg;\n\n// Alerts\n\n@alert-danger-bg: lighten(@brand-danger, 40%);\n@alert-danger-color: darken(@brand-danger, 10%);\n@alert-danger-border: lighten(@brand-danger, 38%);\n\n// Screen Sizes\n\n@screen-xs-min:              480px;\n@screen-sm-min:              768px;\n@screen-md-min:              992px;\n@screen-lg-min:              1200px;\n@screen-xs-max:              (@screen-sm-min - 1);\n@screen-sm-max:              (@screen-md-min - 1);\n@screen-md-max:              (@screen-lg-min - 1);\n\n// Throbber\n@throbber-color: #606060;\n\n// Strength meter\n@strength-meter-bg: #efefef;\n@strength-meter-fill-loading: @throbber-color;\n@strength-meter-fill-success: @brand-success;\n@strength-meter-fill-error: @brand-danger;\n\n// Modal\n@zindex-modal: 9999;\n@modal-border-radius: 5px;\n@modal-padding: 60px;\n@modal-title-color: #4270a2;\n@modal-title-line-height: 1.618;\n@modal-content-bg: #f8fbfd;\n@modal-content-border-color: rgba(0,0,0,.2);\n@modal-backdrop-bg: #000;\n@modal-backdrop-opacity: .5;\n@modal-footer-border-color: #e9eef1;\n@modal-footer-bg: #e9f0f7;\n@modal-lg: 758px;\n@modal-md: 728px;\n@modal-sm: 300px;\n\n// Font size and vertical grid\n@base-font-keyword: small;\n@base-font-keyword-size: 13; // small == 13px;\n\n@font-xx-small: 10;\n@font-x-small: 12;\n@font-small: 14;\n@font-medium: 16;\n@font-large: 18;\n@font-x-large: 20;\n@font-xx-large: 24;\n@font-xxx-large: 32;\n\n@line-x-small: 15;\n@line-small: 20;\n@line-medium: 20;\n@line-large: 25;\n@line-x-large: 25;\n@line-xx-large: 30;\n@line-xxx-large: 40;\n\n@margin-x-small: 5;\n@margin-small: 10;\n@margin-medium: 15;\n@margin-large: 20;\n@margin-x-large: 25;\n@margin-xx-large: 30;\n@margin-xxx-large: 40;\n"
  },
  {
    "path": "r2/r2/public/static/css/config.rb",
    "content": "# Require any additional compass plugins here.\n\n# Set this to the root of your project when deployed:\nhttp_path = \"/\"\ncss_dir = \".\"\nsass_dir = \".\"\nimages_dir = \"images\"\njavascripts_dir = \"javascripts\"\n\n# You can select your preferred output style here (can be overridden via the command line):\n# output_style = :expanded or :nested or :compact or :compressed\noutput_style = :compact\n# To enable relative paths to assets via compass helper functions. Uncomment:\n# relative_assets = true\n\n# To disable debugging comments that display the original location of your selectors. Uncomment:\n line_comments = false\n\n\n# If you prefer the indented syntax, you might want to regenerate this\n# project again passing --syntax sass, or you can uncomment this:\n# preferred_syntax = :sass\n# and then run:\n# sass-convert -R --from scss --to sass sass scss && rm -rf sass && mv scss sass\n"
  },
  {
    "path": "r2/r2/public/static/css/expando.less",
    "content": ".thing .entry .expando-button {\n    background-color: transparent;\n    cursor: pointer;\n    height: 20px;\n    margin: 4px 8px 4px 0;\n    width: 20px;\n\n    &.expanded {\n        background-image: url('../icon-contract.png'); /* SPRITE */\n    }\n\n    &.expanded:hover {\n        background-image: url('../icon-contract-hover.png'); /* SPRITE */\n    }\n\n    &.collapsed {\n        background-image: url('../icon-expand.png'); /* SPRITE */\n    }\n\n    &.collapsed:hover {\n        background-image: url('../icon-expand-hover.png'); /* SPRITE */\n    }\n\n    @media\n    only screen and (min-resolution: 2dppx),\n    only screen and (-webkit-min-device-pixel-ratio: 2) {\n        background-size: 100%;\n\n        &.expanded {\n            background-image: url(../icon-contract_2x.png); /* SPRITE pixel-ratio=2 */\n        }\n\n        &.expanded:hover {\n            background-image: url(../icon-contract-hover_2x.png); /* SPRITE pixel-ratio=2 */\n        }\n\n        &.collapsed {\n            background-image: url(../icon-expand_2x.png); /* SPRITE pixel-ratio=2 */\n        }\n\n        &.collapsed:hover {\n            background-image: url(../icon-expand-hover_2x.png); /* SPRITE pixel-ratio=2 */\n        }\n    }\n}\n"
  },
  {
    "path": "r2/r2/public/static/css/highlight.css",
    "content": "/* subreddit stylesheet source viewer */\n.subreddit-stylesheet-source {\n    padding: 0.5em;\n    overflow-x: auto;\n    margin: 10px 7px;\n    font-size: medium;\n    font-family: \"Bitstream Vera Sans Mono\", Consolas, monospace;\n}\n\n/* github.com style (c) Vasily Polovnyov <vast@whiteants.net> - from the highlight.js source */\npre {\n    color: #333;\n    background-color: #f8f8ff;\n}\n\npre .comment,\npre .template_comment,\npre .diff .header,\npre .javadoc {\n  color: #998;\n  font-style: italic\n}\n\npre .keyword,\npre .css .rule .keyword,\npre .winutils,\npre .javascript .title,\npre .nginx .title,\npre .subst,\npre .request,\npre .status {\n  color: #333;\n  font-weight: bold\n}\n\npre .number,\npre .hexcolor,\npre .ruby .constant {\n  color: #099;\n}\n\npre .string,\npre .tag .value,\npre .phpdoc,\npre .tex .formula {\n  color: #d14\n}\n\npre .title,\npre .id {\n  color: #900;\n  font-weight: bold\n}\n\npre .javascript .title,\npre .lisp .title,\npre .clojure .title,\npre .subst {\n  font-weight: normal\n}\n\npre .class .title,\npre .haskell .type,\npre .vhdl .literal,\npre .tex .command {\n  color: #458;\n  font-weight: bold\n}\n\npre .tag,\npre .tag .title,\npre .rules .property,\npre .django .tag .keyword {\n  color: #000080;\n  font-weight: normal\n}\n\npre .attribute,\npre .variable,\npre .lisp .body {\n  color: #008080\n}\n\npre .regexp {\n  color: #009926\n}\n\npre .class {\n  color: #458;\n  font-weight: bold\n}\n\npre .symbol,\npre .ruby .symbol .string,\npre .lisp .keyword,\npre .tex .special,\npre .input_number {\n  color: #990073\n}\n\npre .built_in,\npre .lisp .title,\npre .clojure .built_in {\n  color: #0086b3\n}\n\npre .preprocessor,\npre .pi,\npre .doctype,\npre .shebang,\npre .cdata {\n  color: #999;\n  font-weight: bold\n}\n\npre .deletion {\n  background: #fdd\n}\n\npre .addition {\n  background: #dfd\n}\n\npre .diff .change {\n  background: #0086b3\n}\n\npre .chunk {\n  color: #aaa\n}\n\n/* Monokai style - ported by Luigi Maselli - http://grigio.org - from the highlight.js source */\n.res-nightmode pre {\n  background: #272822;\n  color: #ddd;\n}\n\n.res-nightmode pre .tag,\n.res-nightmode pre .tag .title,\n.res-nightmode pre .keyword,\n.res-nightmode pre .literal,\n.res-nightmode pre .change,\n.res-nightmode pre .winutils,\n.res-nightmode pre .flow,\n.res-nightmode pre .lisp .title,\n.res-nightmode pre .clojure .built_in,\n.res-nightmode pre .nginx .title,\n.res-nightmode pre .tex .special {\n  color: #F92672;\n}\n\n.res-nightmode pre code .constant {\n  color: #66D9EF;\n}\n\n.res-nightmode pre .class .title {\n  color: white;\n}\n\n.res-nightmode pre .attribute,\n.res-nightmode pre .symbol,\n.res-nightmode pre .symbol .string,\n.res-nightmode pre .value,\n.res-nightmode pre .regexp {\n  color: #BF79DB;\n}\n\n.res-nightmode pre .tag .value,\n.res-nightmode pre .string,\n.res-nightmode pre .subst,\n.res-nightmode pre .title,\n.res-nightmode pre .haskell .type,\n.res-nightmode pre ..res-nightmode preprocessor,\n.res-nightmode pre .ruby .class .parent,\n.res-nightmode pre .built_in,\n.res-nightmode pre .sql .aggregate,\n.res-nightmode pre .django .template_tag,\n.res-nightmode pre .django .variable,\n.res-nightmode pre .smalltalk .class,\n.res-nightmode pre .javadoc,\n.res-nightmode pre .django .filter .argument,\n.res-nightmode pre .smalltalk .localvars,\n.res-nightmode pre .smalltalk .array,\n.res-nightmode pre .attr_selector,\n.res-nightmode pre .pseudo,\n.res-nightmode pre .addition,\n.res-nightmode pre .stream,\n.res-nightmode pre .envvar,\n.res-nightmode pre .apache .tag,\n.res-nightmode pre .apache .cbracket,\n.res-nightmode pre .tex .command,\n.res-nightmode pre .input_number {\n  color: #A6E22E;\n}\n\n.res-nightmode pre .comment,\n.res-nightmode pre .java .annotation,\n.res-nightmode pre .python .decorator,\n.res-nightmode pre .template_comment,\n.res-nightmode pre .pi,\n.res-nightmode pre .doctype,\n.res-nightmode pre .deletion,\n.res-nightmode pre .shebang,\n.res-nightmode pre .apache .sqbracket,\n.res-nightmode pre .tex .formula {\n  color: #75715E;\n}\n\n.res-nightmode pre .keyword,\n.res-nightmode pre .literal,\n.res-nightmode pre .css .id,\n.res-nightmode pre .phpdoc,\n.res-nightmode pre .title,\n.res-nightmode pre .haskell .type,\n.res-nightmode pre .vbscript .built_in,\n.res-nightmode pre .sql .aggregate,\n.res-nightmode pre .rsl .built_in,\n.res-nightmode pre .smalltalk .class,\n.res-nightmode pre .diff .header,\n.res-nightmode pre .chunk,\n.res-nightmode pre .winutils,\n.res-nightmode pre .bash .variable,\n.res-nightmode pre .apache .tag,\n.res-nightmode pre .tex .special,\n.res-nightmode pre .request,\n.res-nightmode pre .status {\n  font-weight: bold;\n}\n\n.res-nightmode pre .coffeescript .javascript,\n.res-nightmode pre .javascript .xml,\n.res-nightmode pre .tex .formula,\n.res-nightmode pre .xml .javascript,\n.res-nightmode pre .xml .vbscript,\n.res-nightmode pre .xml .css,\n.res-nightmode pre .xml .cdata {\n  opacity: 0.5;\n}\n"
  },
  {
    "path": "r2/r2/public/static/css/interstitial.less",
    "content": "@margin-interstitial-top: 60;\n\n.interstitial {\n  font-family: \"Helvetica Neue\", \"Helvetica\", \"Arial\", sans-serif;\n  margin: auto;\n  text-align: center;\n  width: 650px;\n}\n\n.modal .interstitial {\n  width: auto;\n\n  .interstitial-image {\n    margin-top: @margin-large * 1px;\n  }\n}\n\n.interstitial-image {\n  margin-bottom: @margin-large * 1px;\n  margin-top: @margin-interstitial-top * 1px;\n}\n\n.interstitial-message .note {\n  color: #a5a4a4;\n  font-size: @font-small;\n}\n\n.interstitial .md {\n  h3 {\n    .md-font-size(@font-small, @font-x-large, @line-x-large);\n    .md-margins(@font-x-large, @margin-large, @margin-large);\n  }\n\n  h5,\n  p {\n    .md-font-size(@font-small, @font-large, @line-large);\n  }\n\n  p { \n    font-weight: 300;\n  }\n}\n\n.interstitial-subreddit-description {\n  background-color: lighten(@color-pale-grey, 5%);\n  margin-bottom: @margin-large * 1px;\n  padding: (@margin-small * 1px) (@margin-large * 1px);\n\n  h5 {\n    margin-bottom: @margin-large * 1px;\n    margin-top: 0;\n  }\n\n  p {\n    margin: 0;\n    text-align: left;\n  }\n}\n\n.interstitial .buttons {\n  .display-flex();\n  .justify-content(center);\n  margin-left: auto;\n  margin-right: auto;\n  margin-top: @margin-xx-large * 1px;\n  width: 400px;\n\n  .c-btn {\n    .flex(0 1 75%);\n    font-weight: 500;\n    margin-left: @margin-x-small * 1px;\n    margin-right: @margin-x-small * 1px;\n    padding-bottom: 8px;\n    padding-top: 8px;\n  }\n}\n"
  },
  {
    "path": "r2/r2/public/static/css/markdown.less",
    "content": "// markdown colors\n@markdown-color-link: @color-alien-blue;\n@markdown-color-code: @color-off-white;\n@markdown-color-code-alt: @color-white;\n@markdown-color-border-light: @color-pale-grey;\n@markdown-color-border-heavy: darken(@color-pale-grey, 15%);\n@markdown-color-font: @color-semi-black;\n@markdown-color-font-light: @color-text-grey;\n\n// wiki page styles\n@markdown-color-wiki-header: @color-text-grey;\n@markdown-color-wiki-header-alt: @color-ui-blue;\n\n\n.md-font-size(@base, @font, @line) {\n  font-size: (@font / @base) * 1em;\n  line-height: (@line / @font) * 1em;\n}\n\n.md-margins(@font, @top, @bottom) {\n  margin-top: (@top / @font) * 1em;\n  margin-bottom: (@bottom / @font) * 1em;\n}\n\n.md-base-font-size(@font-size) {\n  font-size: (@font-size / @base-font-keyword-size) * 1em;\n}\n\n.md-container-small,\n.md-container {\n  font-size: @base-font-keyword;\n}\n\n.md {\n  h1, h2, h3, h4, h5, h6 {\n    &:extend(.md .-headers all);\n  }\n\n  th, td {\n    &:extend(.md .-cells all);\n  }\n\n  ul, ol {\n    &:extend(.md .-lists all);\n  }\n\n  .-lists, pre, blockquote, table, p {\n    &:extend(.md .-blocks all);\n  }\n\n  p, pre > code, th, td, li {\n    &:extend(.md .-text all);\n  }\n\n  color: @markdown-color-font;\n  max-width: 60em;\n  overflow-wrap: break-word;\n  word-wrap: break-word;\n\n  .-headers {\n    border: 0;\n    color: inherit;\n    -webkit-font-smoothing: antialiased;\n\n    code {\n      font-size: inherit;\n    }\n  }\n\n  blockquote,\n  del {\n    color: @markdown-color-font-light;\n  }\n\n  a {\n    color: @markdown-color-link;\n    text-decoration: none;\n\n    del {\n      color: inherit;\n    }\n  }\n\n  h6 {\n    text-decoration: underline;\n  }\n\n  em {\n    font-style: italic;\n    font-weight: inherit;\n  }\n\n  th,\n  strong,\n  .-headers {\n    font-weight: 600;\n    font-style: inherit;\n  }\n\n  h2,\n  h4 {\n    font-weight: 500;\n  }\n\n  &,\n  h6 {\n    font-weight: 400;\n  }\n\n  * {\n    margin-left: 0;\n    margin-right: 0;\n  }\n\n  tr,\n  code,\n  .-cells,\n  .-lists,\n  .-blocks,\n  .-headers {\n    margin: 0;\n    padding: 0;\n  }\n\n  hr {\n    border: 0;\n    color: transparent;\n    background: @markdown-color-border-heavy;\n    height: 2px;\n    padding: 0;\n  }\n\n  blockquote {\n    border-left: 2px solid @markdown-color-border-heavy;\n  }\n\n  code,\n  pre {\n    border: 1px solid darken(@markdown-color-code, 10%);\n    background-color: @markdown-color-code;\n    border-radius: 2px;\n  }\n\n  code {\n    margin: 0 2px;\n    white-space: nowrap;\n    word-break: normal;\n  }\n\n  p {\n    code {\n      line-height: 1em;\n    }\n  }\n\n  pre {\n    overflow: auto;\n\n    code {\n      white-space: pre;\n      background-color: transparent;\n      border: 0;\n      display: block;\n      padding: 0 !important;\n    }\n  }\n\n  // can't rely on &:extend here, doesn't apply the align rules\n  td, th {\n    border: 1px solid @markdown-color-border-light;\n    text-align: left;\n\n    &[align=center] {\n      text-align: center;\n    }\n\n    &[align=right] {\n      text-align: right;\n    }\n  }\n\n  img {\n    max-width: 100%;\n  }\n\n  ul {\n    list-style-type: disc;\n  }\n\n  ol {\n    list-style-type: decimal;\n  }\n\n  blockquote {\n    // -2 to compensate for border\n    padding: 0 ((@margin-small - 2) * 1px);\n    margin-left: (@margin-x-small * 1px);\n  }\n\n  code {\n    padding: 0 ((@margin-x-small - 1) * 1px);\n  }\n\n  pre,\n  .-cells {\n    padding: ((@margin-x-small - 1) * 1px) ((@margin-small - 1) * 1px);\n  }\n\n  .-lists {\n    padding-left: (@margin-xxx-large * 1px);\n  }\n\n  sup {\n    font-size: 0.86em;\n    line-height: 0;\n  }\n\n  li li,\n  li p {\n    font-size: 1em !important;\n  }\n}\n\n\n.link .usertext .md {\n  padding: (@margin-x-small * 1px) (@margin-small * 1px);\n}\n\n.new-comment .md,\n.link .md,\n.usertext.border .md {\n  :not(pre) > code,\n  pre {\n    background-color: @markdown-color-code-alt;\n  }\n}\n\n.linklisting .md,\n.commentarea .md {\n  @vertical-margin: (@margin-x-small * 1px);\n  margin-top: @vertical-margin;\n  margin-bottom: @vertical-margin;\n}\n\ntextarea {\n  // prevent from using system default colors, since this can cause\n  // problems on systems with dark color schemes\n  background-color: white;\n  color: black;\n}\n\ncode {\n  // this hack fixes browsers automatically shrinking monospace fonts.  weird.\n  // http://code.stephenmorley.org/html-and-css/fixing-browsers-broken-monospace-font-handling/\n  font-family:monospace,monospace;\n}\n\n\n.md {\n  @font-base: @font-small;\n  @line-base: @line-small;\n\n  .md-base-font-size(@font-base);\n\n  h1,\n  h2 {\n    .md-font-size(@font-base, @font-large, @line-large);\n    .md-margins(@font-large, @margin-medium, @margin-medium);\n  }\n\n  h3,\n  h4 {\n    .md-font-size(@font-base, @font-medium, @line-medium);\n    .md-margins(@font-medium, @margin-small, @margin-small);\n  }\n\n  h5,\n  h6 {\n    .md-font-size(@font-base, @font-base, @line-base);\n    .md-margins(@font-base, @margin-small, @margin-x-small);\n  }\n\n  .-blocks {\n    .md-margins(@font-base, @margin-x-small, @margin-x-small);\n  }\n\n  textarea,\n  .-text {\n    .md-font-size(@font-base, @font-base, @line-base);\n  }\n}\n\n.md-container-small .md,\n.side .md {\n  @font-base: @font-x-small;\n  @line-base: @line-x-small;\n\n  .md-base-font-size(@font-base);\n\n  h1,\n  h2 {\n    .md-font-size(@font-base, @font-large, @line-large);\n    .md-margins(@font-large, @margin-small, @margin-small);\n  }\n\n  h3,\n  h4 {\n    .md-font-size(@font-base, @font-medium, @line-medium);\n    .md-margins(@font-medium, @margin-small, @margin-small);\n  }\n\n  h5,\n  h6 {\n    .md-font-size(@font-base, @font-small, @line-small);\n    .md-margins(@font-small, @margin-small, @margin-x-small);\n  }\n\n  .-blocks {\n    .md-margins(@font-base, @margin-x-small, @margin-x-small);\n  }\n\n  .-text {\n    .md-font-size(@font-base, @font-base, @line-base);\n  }\n}\n\n.wiki-page-content .md {\n  @font-base: @font-small;\n  @line-base: @line-small;\n\n  h1 {\n    .md-font-size(@font-base, @font-xxx-large, @line-xxx-large);\n    .md-margins(@font-xxx-large, @margin-xxx-large, @margin-x-large);\n  }\n\n  h2 {\n    .md-font-size(@font-base, @font-xx-large, @line-xx-large);\n    .md-margins(@font-xx-large, @margin-xx-large, @margin-medium);\n  }\n\n  h3 {\n    .md-font-size(@font-base, @font-x-large, @line-x-large);\n    .md-margins(@font-x-large, @margin-large, @margin-small);\n  }\n\n  .-blocks {\n    .md-margins(@font-base, @margin-x-small, @margin-small);\n  }\n\n  // Style changes for wiki pages\n  h1,\n  h6 {\n    color: @markdown-color-wiki-header;\n    font-weight: 300;\n  }\n\n  h2 {\n    color: @markdown-color-wiki-header-alt;\n  }\n\n  h2,\n  h3 {\n    font-weight: 600;\n  }\n\n  h4 {\n    font-style: italic;\n  }\n\n  h5 {\n    text-decoration: underline;\n  }\n\n  h4,\n  h5 {\n    font-weight: 400;\n  }\n\n  h6 {\n    .md-font-size(@font-base, @font-base, @line-base);\n    .md-margins(@font-base, @margin-medium, @margin-x-small);\n\n    text-decoration: none;\n    text-transform: uppercase;\n    letter-spacing: 1px;\n  }\n}\n\n\n.md {\n  &,\n  .-cells,\n  .-lists,\n  .-blocks,\n  .-headers {\n    > :first-child {\n      margin-top: 0;\n    }\n\n    > :last-child {\n      margin-bottom: 0;\n    }\n  }\n\n  li {\n    > :first-child {\n      margin-top: 0;\n      \n      &:last-child {\n        margin-bottom: 0;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "r2/r2/public/static/css/mobile.css",
    "content": "body {\n     font-family: verdana,arial,helvetica,sans-serif;\n     margin: 0;\n     padding: 0;\n     color: #888;\n}\n\na {\n  text-decoration: none;\n  color: #369;\n  }\n\np { \n  margin: 0px;\n  padding: 0px;\n  }\n\nul {\n   margin: 5px;\n   padding: 0px;\n   list-style: none;\n   }\n\n.link {\n      padding: 5px 5px 5px 5px;\n      /*padding-top: 5px;\n      padding-bottom: 5px;\n      margin-left: 2px;\n      display: inline;*/\n      }\n\n.title {\n       color: black;\n       }\n\n.byline {\n\tmargin: 0px 0px .5em 2px;\n        display: inline;\n        font-size: small;\n        }\n\n.description {\n             margin-bottom: .5em;\n             }\n\n.domain {\n        color: #369;\n        }\n\n.comment .md, .subreddit .md {\n    color: black;\n}\n\n.link .md {\n    color: black;\n    margin: 5px 5px 10px 5px;\n}\n\n.comment .child {\n    border-left: 1px dotted #DDF;\n}\n\n.child {\n       padding-left: 1em;\n       margin-left: 1em;\n       border-left: 1px dotted #DDF;\n       }\n\n.headerbar {\n           background-color: lightgrey;\n\t   margin: 5px 0px 5px 2px;\n}\n\n.headerbar span {\n           background-color: white;\n           color: gray;\n           font-weight: bold;\n           margin-left: 15px;\n           padding: 0px 3px;\n}\n\n.score {\n       margin: 0px .5em 0px .5em;\n}\n\n.error { \n       color: red; \n       margin: 5px;\n       }\n\n.nextprev { \n          margin: 10px;\n          }\n\n.tabmenu {\n    list-style-type: none;\n    display: inline;\n}\n\n.tabmenu li {\n            display: inline;\n\t    margin-right: .25em;\n\t    padding-left: .25em;\n            border-left: thin solid #000;\n            }\n\n.redditname {\n            font-weight: bold;\n            margin: 0px 3px 0px 3px;\n            }\n\n.selected {\n          font-weight: bold;\n          }\n\n.pagename {\n          font-weight: bold;\n          margin-right: 1ex;\n          color: black;\n          }\n\n.or {\n    border-left:thin solid #000000;\n    border-right:thin solid #000000;\n    padding: 0px .25em 0px .25em;\n    }\n \n#header {\n        background-color: #CEE3F8;\n        }\n\n#header .redditname a{\n                     color: black;\n                     }\n\n#header img {\n            margin-right: 3px;\n            }\n\n/* markdown */\n.md { max-width: 60em; overflow: auto; }\n.md p, .md h1 { margin-bottom: .5em;}\n.md h1 { font-weight: normal; font-size: 100%; }\n.md > * { margin-bottom: 0px }\n.md strong { font-weight: bold; }\n.md em { font-style: italic; }\n.md img { display: none }\n.md ol, .md ul { margin: 10px 2em; }\n.md pre { margin: 10px; }\n.flair, .linkflair { color: #545454; background-color: #F5F5F5; border: 1px solid #DEDEDE; }\n.linkflair { display: inline-block; font-size: small; max-width: 10em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\na.author, .flair, .linkflair { margin-right: 0.5em }\n.author.submitter { color: white; background-color: #5F99CF; padding: 0 4px; }"
  },
  {
    "path": "r2/r2/public/static/css/mod-action-icons.less",
    "content": "@mod-action-icon-size: 16px;\n\n.mod-action-icon {\n    display: inline-block;\n    width: @mod-action-icon-size;\n    height: @mod-action-icon-size;\n    background-position: center;\n}\n\n.mod-action-icon-delete {\n    .hdpi-bg-image(@1x: url(../mod-action-icon-delete.png),\n                   @2x: url(../mod-action-icon-delete_2x.png),\n                   @2x-bg-size: @mod-action-icon-size);\n    \n    &:hover,\n    .mod-action-deleting & {\n        .hdpi-bg-image(@1x: url(../mod-action-icon-delete-active.png),\n                       @2x: url(../mod-action-icon-delete-active_2x.png),\n                       @2x-bg-size: @mod-action-icon-size);\n    }\n}\n\n.mod-action-icon-edit {\n    .hdpi-bg-image(@1x: url(../mod-action-icon-edit.png),\n                   @2x: url(../mod-action-icon-edit_2x.png),\n                   @2x-bg-size: @mod-action-icon-size);\n\n    &:hover {\n        .hdpi-bg-image(@1x: url(../mod-action-icon-edit-active.png),\n                       @2x: url(../mod-action-icon-edit-active_2x.png),\n                       @2x-bg-size: @mod-action-icon-size);\n    }\n}\n\n.mod-action-icon-add {\n    .hdpi-bg-image(@1x: url(../mod-action-icon-add.png),\n                   @2x: url(../mod-action-icon-add_2x.png),\n                   @2x-bg-size: @mod-action-icon-size);\n}\n\n.mod-action-icon-cancel {\n    .hdpi-bg-image(@1x: url(../mod-action-icon-cancel.png),\n                   @2x: url(../mod-action-icon-cancel_2x.png),\n                   @2x-bg-size: @mod-action-icon-size);\n}\n\n.mod-action-icon-confirm {\n    .hdpi-bg-image(@1x: url(../mod-action-icon-confirm.png),\n                   @2x: url(../mod-action-icon-confirm_2x.png),\n                   @2x-bg-size: @mod-action-icon-size);\n}\n"
  },
  {
    "path": "r2/r2/public/static/css/modtools.less",
    "content": "@import \"components/variables.less\";\n@import \"components/mixins.less\";\n@import \"markdown.less\";\n\n@import \"mod-action-icons.less\";\n@import \"subreddit-rules.less\";\n\n#this_is_a_hack__please_ignore {\n  // lets me put this file in the SPRITED_STYLESHEETS section of the Makefile\n  background-image: url(../reddit.com.header.png); /* SPRITE */\n}\n\n.modtools-page {\n    & > div.content {\n      font-family: \"Helvetica Neue\", \"Helvetica\", \"Arial\", sans-serif;\n      margin: 0;\n      margin-right: @reddit-sidebar-width;\n      padding: 15px 20px;\n    }\n}\n"
  },
  {
    "path": "r2/r2/public/static/css/policies.less",
    "content": ".transition (@property, @duration, @function: ease, @delay: 0s) {\n    -webkit-transition: @arguments;\n    -moz-transition: @arguments;\n    -o-transition: @arguments;\n    -ms-transition: @arguments;\n    transition: @arguments;\n}\n\n.policy-page {\n    position: relative;\n    margin: 0 auto;\n    padding-left: 195px;\n    width: 43em;\n    font-size: 11pt;\n\n    .md {\n        line-height: 1.75em;\n        overflow: visible;\n        margin: 0;\n\n        a:hover {\n            text-decoration: underline;\n        }\n\n        h1 {\n            font-size: 1.75em;\n            font-weight: bold;\n            margin: 1.5em 0;\n\n            a {\n                color: black;\n                padding-bottom: 2px;\n\n                &:hover {\n                    // the padding added to the em causes a break in the text-decoration underline\n                    text-decoration: none;\n                    border-bottom: 1px solid #777;\n                }\n            }\n\n            strong {\n                margin-right: .5em;\n            }\n\n            em {\n                font-size: .60em;\n                font-style: normal;\n                color: #777;\n                white-space: nowrap;\n            }\n        }\n\n        h2 {\n            font-size: 1.4em;\n            margin: 1em 0 .75em;\n            padding-top: 1em;\n            color: #336699;\n        }\n\n        h3 {\n            color: #333;\n            font-size: 1.1em;\n            margin-top: 1.5em;\n\n            a {\n                color: black;\n            }\n        }\n\n        h1, h2, h3 {\n            text-transform: lowercase;\n        }\n\n        p {\n            margin-bottom: 1em;\n            color: #222;\n        }\n\n        p em {\n            line-height: 1.5em;\n            font-style: normal;\n            display: block;\n            background: #fcfbd7;\n            padding: .5em .75em;\n            margin: 0 -.75em;\n            margin-top: 1.5em;\n            position: relative;\n            top: -.5em;\n            border-radius: 5px;\n            box-shadow: 0 2px 5px #d1d0bb;\n        }\n\n        a.p-anchor {\n            float: left;\n            font-size: 10pt;\n            font-weight: normal;\n            color: #ddd;\n            margin-left: -3.5em;\n            width: 1.5em;\n            text-align: right;\n            .transition(color, 1.5s);\n        }\n\n        h1:hover > a.p-anchor,\n        h2:hover > a.p-anchor,\n        h3:hover > a.p-anchor,\n        p:hover > a.p-anchor {\n            color: #555;\n            .transition(color, 0.25s, ease, 0.5s);\n\n            &:hover {\n                .transition(color, 0s);\n            }\n        }\n\n        blockquote {\n            margin: .5em 0;\n            padding: 0;\n            border: none;\n            font-size: 1.7em;\n\n            p {\n                line-height: 1.5em;\n                color: #555;\n            }\n        }\n    }\n\n    .doc-info, .md blockquote {\n        top: 2.5em;\n        margin-top: 1.5em;\n        h4 {\n            margin-bottom: .75em;\n        }\n    }\n\n    .doc-info {\n        position: absolute;\n        margin-left: -13em;\n        width: 10em;\n\n        h4 {\n            color: #555;\n            font-size: .9em;\n        }\n\n        .revisions li {\n            font-size: .85em;\n        }\n\n        .toc ul ul li {\n            font-size: .85em;\n        }\n\n        li {\n            margin: .75em 0;\n\n            &.selected {\n                font-weight: bold;\n            }\n        }\n\n        .revisions li {\n            white-space: nowrap;\n        }\n\n        .toc {\n            position: relative;\n            text-transform: lowercase;\n\n            & > ul {\n                margin-bottom: 1.5em;\n\n                // only display h2 sections in toc\n\n                & > li {\n                    display: none;\n                }\n\n                & > li.toc_child {\n                    display: block;\n                }\n\n                ul ul {\n                    display: none;\n                }\n            }\n\n            .location {\n                position: absolute;\n                left: -11px;\n                margin-top: 2px;\n                border: 5px solid transparent;\n                border-left-color: orangered;\n            }\n        }\n    }\n}\n\n@media (max-width: 835px) {\n    .policy-page {\n        padding-left: 0;\n\n        .doc-info.scroll-fixed-standin {\n            display: none;\n        }\n\n        .doc-info {\n            position: static;\n            margin: 2em 0;\n            width: auto;\n\n            &.scroll-fixed {\n                position: static !important;\n            }\n\n            .toc {\n                display: none;\n            }\n\n            .revisions li {\n                display: inline-block;\n                margin: 0 .5em;\n            }\n        }\n    }\n}\n\n@media (max-width: 635px) {\n    .policy-page {\n        margin: 0 1em;\n        width: auto;\n    }\n}\n"
  },
  {
    "path": "r2/r2/public/static/css/post-sharing.less",
    "content": "@post-sharing-icon-size: 26px;\n@post-sharing-max-width: 550px;\n@post-sharing-shareplane-width: 50px;\n@post-sharing-shareplane-height: 32px;\n\n@post-sharing-shareplane-image: '../share-paper-airplane.png';\n@post-sharing-facebook-image: '../share-to-facebook.png';\n@post-sharing-twitter-image: '../share-to-twitter.png';\n@post-sharing-tumblr-image: '../share-to-tumblr.png';\n@post-sharing-email-image: '../share-to-email.png';\n@post-sharing-reddit-pm-image: '../share-to-reddit-pm.png';\n\n@post-sharing-color-success: @color-green;\n@post-sharing-color-button-focus: @color-white;\n@post-sharing-color-background: #fbfbfb;\n@post-sharing-color-border: darken(@post-sharing-color-background, 10%);\n@post-sharing-color-tooltip: @color-white;\n@post-sharing-color-tooltip-border: darken(@color-white, 10%);\n@post-sharing-color-tooltip-text: @color-semi-black;\n\n@post-sharing-font-base: @font-small;\n@post-sharing-font-small: @font-x-small;\n@post-sharing-line-base: @line-small;\n\n.post-sharing-font-size(@font:@post-sharing-font-base, @line:@post-sharing-line-base) {\n  .md-font-size(@post-sharing-font-base, @font, @line);\n}\n\n.post-sharing {\n  background: @post-sharing-color-background;\n  border: 1px solid @post-sharing-color-border;\n  box-sizing: border-box;\n  display: none;\n  font-size: @base-font-keyword;\n  max-width: @post-sharing-max-width;\n  padding: (@margin-large * 1px);\n  position: relative;\n\n  .c-close {\n    position: absolute;\n    top: 0;\n    right: 0;\n    padding: @margin-x-small * 1px;\n  }\n\n  .post-sharing-form {\n    .md-base-font-size(@post-sharing-font-base);\n    display: none;\n  }\n\n  .post-sharing-main {\n    display: block;\n\n    .c-form-group {\n      .align-items(center);\n      .display-flex();\n\n      > * {\n        .flex(1 1 100%);\n      }\n\n      > .post-sharing-label {\n        .flex(0 0 83px);\n        padding-right: @margin-small * 1px;\n        text-align: right;\n      }\n\n      &:last-child {\n        margin-bottom: 0px;\n      }\n    }\n  }\n\n  .post-sharing-email-form {\n    .c-form-group {\n      margin-bottom: @margin-small * 1px;\n      margin-top: @margin-small * 1px;\n    }\n\n    &.shared {\n      .post-sharing-shareplane {\n        display: block;\n\n        &:before {\n          .animation(post-sharing-shareplane .7s forwards);\n        }\n      }\n\n      .post-sharing-buttons {\n        .c-btn {\n          opacity: 0;\n        }\n      }\n    }\n  }\n\n  .post-sharing-label {\n    .post-sharing-font-size();\n    color: @color-dark-grey;\n  }\n\n  .post-sharing-option {\n    background-size: @post-sharing-icon-size @post-sharing-icon-size;\n    cursor: pointer;\n    display: inline-block;\n    height: @post-sharing-icon-size;\n    margin-right: @margin-x-small * 1px;\n    margin-top: @margin-x-small * 1px;\n    position: relative;\n    width: @post-sharing-icon-size;\n\n    &:hover .c-tooltip {\n      bottom: 100%;\n      opacity: 1;\n    }\n\n    .c-tooltip {\n      .transform(translate(-50%, @tooltip-arrow-width * -1));\n      .transition(all, 0.15s);\n      bottom: 50%;\n      left: 50%;\n      pointer-events: none;\n\n      .tooltip-inner {\n        .box-shadow(0 1px 3px rgba(0, 0, 0, 0.08));\n        background: @post-sharing-color-tooltip;\n        border: 1px solid @post-sharing-color-tooltip-border;\n        color: @post-sharing-color-tooltip-text;\n        padding: @margin-small * 1px;\n        white-space: nowrap;\n      }\n\n      .tooltip-arrow {\n        .transform(translate(-50%, -1px));\n        border-top-color: @post-sharing-color-tooltip;\n        left: 50%;\n        top: 100%;\n      }\n    }\n  }\n\n  .post-sharing-option-facebook {\n    background-image: data-uri(@post-sharing-facebook-image);\n  }\n\n  .post-sharing-option-twitter {\n    background-image: data-uri(@post-sharing-twitter-image);\n  }\n\n  .post-sharing-option-tumblr {\n    background-image: data-uri(@post-sharing-tumblr-image);\n  }\n\n  .post-sharing-option-email {\n    background-image: data-uri(@post-sharing-email-image);\n  }\n\n  .post-sharing-option-reddit-pm {\n    background-image: data-uri(@post-sharing-reddit-pm-image);\n  }\n\n  .post-sharing-shareplane {\n    .post-sharing-font-size(@post-sharing-font-small);\n    bottom: 0;\n    color: @post-sharing-color-success;\n    display: none;\n    font-weight: bold;\n    padding: (@margin-x-small* 1px) (@margin-medium * 1px);\n    position: absolute;\n    right: 0;\n    text-transform: uppercase;\n\n    &:before {\n      background-image: data-uri(@post-sharing-shareplane-image);\n      background-size: @post-sharing-shareplane-width @post-sharing-shareplane-height;\n      content: '';\n      height: @post-sharing-shareplane-height;\n      position: absolute;\n      transform: translate(0, 0);\n      width: @post-sharing-shareplane-width;\n    }\n  }\n\n  .post-sharing-buttons {\n    overflow: auto;\n\n    .c-btn {\n      .no-select();\n      margin-left: @margin-x-small * 1px;\n      margin-right: 0;\n      padding: (@margin-x-small * 1px) (@margin-medium * 1px);\n\n      &:focus {\n        box-shadow: 0 0 0 1px @post-sharing-color-button-focus inset;\n        outline: none;\n      }\n    }\n  }\n}\n\n.post-sharing-shareplane-animation() {\n  @keyframe-0-translate-x: 0px;\n  @keyframe-0-translate-y: 0px;\n  @keyframe-0-opacity: 0;\n\n  @keyframe-1-translate-x: 20px;\n  @keyframe-1-translate-y: -10px;\n  @keyframe-1-opacity: 1;\n\n  @keyframe-2-translate-x: -50px;\n  @keyframe-2-translate-y: -60px;\n  @keyframe-2-opacity: 0;\n\n  0% {\n    opacity: @keyframe-0-opacity;\n    transform: translate(@keyframe-0-translate-x, @keyframe-0-translate-y);\n  }\n\n  50% {\n    opacity: @keyframe-1-opacity;\n    transform: translate(@keyframe-1-translate-x, @keyframe-1-translate-y);\n  }\n\n  60% {\n    opacity: @keyframe-1-opacity;\n    transform: translate(@keyframe-1-translate-x, @keyframe-1-translate-y);\n  }\n\n  100% {\n    opacity: @keyframe-2-opacity;\n    transform: translate(@keyframe-2-translate-x, @keyframe-2-translate-y);\n  }\n}\n\n@keyframes post-sharing-shareplane {\n  .post-sharing-shareplane-animation()\n}\n\n@-o-keyframes post-sharing-shareplane {\n  .post-sharing-shareplane-animation()\n}\n\n@-webkit-keyframes post-sharing-shareplane {\n  .post-sharing-shareplane-animation()\n}\n"
  },
  {
    "path": "r2/r2/public/static/css/reddit-embed.less",
    "content": "@import \"components/variables.less\";\n@import \"components/mixins.less\";\n@import \"markdown.less\";\n\n\n* {\n    .box-sizing(border-box)\n}\n\nbody {\n    font: normal 14px verdana, arial, helvetica, sans-serif;\n    margin: 0;\n}\n\na {\n    color: #369;\n    text-decoration: none;\n}\n\na:hover {\n    text-decoration: underline;\n}\n\np {\n    margin: 0 0 .5em;\n}\n\n.reddit-embed {\n    background: #fff;\n}\n\n.reddit-embed-content {\n    border-bottom: 1px solid #f6f6f6;\n    padding: 20px;\n}\n\n.reddit-embed-footer {\n    @snoo-height: 32px;\n    @snoo-width: 90px;\n    @snoo-padding: 12px;\n\n    background: #fafbfd;\n    display: table;\n    font-size: 12px;\n    height: (@snoo-height + (@snoo-padding * 2));\n    padding: 20px;\n    padding-right: (@snoo-width + (@snoo-padding * 2));\n    position: relative;\n    width: 100%;\n\n    p {\n        display: table-cell; \n        margin: 0;\n        padding: 0;\n        vertical-align: middle; \n    }\n\n    .reddit-embed-footer-img {\n        display: block;\n        position: absolute;\n        right: @snoo-padding;\n        bottom: @snoo-padding;\n        width: @snoo-width;\n        height: @snoo-height;\n        .hdpi-bg-image(@1x: url(../reddit-embed-logo.png); @2x: url(../reddit-embed-logo_2x.png););\n        text-indent: 100%;\n        white-space: nowrap;\n        overflow: hidden;\n    }\n}\n\n.reddit-embed-list {\n    margin: 0;\n    padding: 0;\n\n    .reddit-embed-list {\n        margin-left: 20px;\n    }\n}\n\n.reddit-embed-list-item {\n    list-style: none;\n    margin-top: 24px;\n\n    .reddit-embed-content > .reddit-embed-list > &:first-child {\n        margin-top: 0;\n    }\n}\n\n.reddit-embed-author {\n    color: #6f6f6f;\n    font-weight: bold;\n}\n\n// Comment embeds\n.reddit-embed-comment-header {\n    position: relative;\n}\n\n.reddit-embed-comment-meta {\n    font-size: 12px;\n    position: absolute;\n    right: 0;\n    bottom: 0;\n}\n\n.reddit-embed-comment-meta-item {\n    color: #6f6f6f;\n\n    &:after {\n        content: '•';\n        display: inline-block;\n        margin: 0 5px;\n    }\n\n    &:last-child:after {\n        display: none;\n    }\n}\n\n.reddit-embed-comment-body {\n    margin: 10px 0 0;\n}\n\n.reddit-embed-comment-edited,\n.reddit-embed-comment-deleted {\n    background: #f7f7f7;\n    padding: 48px 18px;\n    text-align: center;\n}\n\n.reddit-embed-comment-more {\n    display: none;\n}\n\n.reddit-embed-comment-fade {\n    .reddit-embed-comment-body {\n        position: relative;\n        overflow: hidden;\n        margin-bottom: 8px;\n\n        &:before {\n            content:'';\n            position: absolute;\n            left: 0;\n            bottom: 0;\n            width: 100%;\n            height: 40px;\n\n            .linear-gradient(rgba(255, 255, 255, 0), rgba(255, 255, 255, 1));\n        }\n    }\n\n    .reddit-embed-comment-more {\n        position: relative;\n        top: -20px;\n        display: block;\n        padding-top: 20px;\n    }\n}\n"
  },
  {
    "path": "r2/r2/public/static/css/reddit.less",
    "content": "@import \"components/variables.less\";\n@import \"components/components.less\";\n@import \"markdown.less\";\n@import \"post-sharing.less\";\n@import \"search.less\";\n@import \"interstitial.less\";\n\n.no-select {\n    -webkit-user-select: none;\n    -moz-user-select: none;\n    -o-user-select: none;\n    -ms-user-select: none;\n    user-select: none;\n}\n\n.screenreader-only {\n    position: absolute;\n    width: 1px;\n    height: 1px;\n    padding: 0;\n    margin: -1px;\n    overflow: hidden;\n    clip: rect(0,0,0,0);\n    border: 0;\n}\n\n.basic-button {\n    border-width: 1px;\n    border-style: solid;\n    border-radius: 5px;\n\n    &:focus {\n        outline: 0;\n        box-shadow: inset 0 0 0 1px fadeout(white, 65%);\n    }\n\n    &:active {\n        box-shadow: inset 0 1px 1px 1px fadeout(black, 90%);\n    }\n\n    &:disabled, &.disabled {\n        .linear-gradient(#e9edf1, #dce3ea) !important;\n        color: #999999 !important;\n        text-shadow: 0 1px 0 lighten(#dce3ea, 15%) !important;\n        border-color: darken(#dce3ea, 5%) !important;\n        box-shadow: none !important;\n    }\n}\n\n.button-style(@top-color, @bottom-color, @border-color) {\n    &:extend(.basic-button all);\n    .linear-gradient(@top-color, @bottom-color);\n    border-color: @border-color;\n    &:hover {\n        .linear-gradient(lighten(@top-color, 5%), @bottom-color);\n    }\n}\n\n@gold-fonts: Palatino, georgia, garamond, FreeSerif, serif;\n@gold-button-bg: #938870;\n@gold-button-hover: #B8AB90;\n@gold-button-active: #C3B598;\n@gold-button-border: 1px solid #5E5137;\n\nhtml {\n    height: 100%; /* Needed for toolbar's comments panel's pinstripe */\n}\n\nbody,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,form,fieldset,input,p,blockquote,th,td,iframe {\n  margin:0;\n  padding:0;\n}\n\ntable { border-collapse:collapse; }\n\nfieldset,img { border:0; }\n\naddress,caption,cite,code,dfn,em,strong,th,var { font-style:normal; font-weight:normal; }\nol,ul { list-style:none; }\ncaption,th { text-align:left; }\nh1,h2,h3,h4,h5,h6 { font-size:100%; }\nq:before,q:after { content:''; }\n\nbody {\n    font: normal x-small verdana, arial, helvetica, sans-serif;\n    background-color: white;\n    z-index: 1;\n    min-height: 100%;\n}\n\ntextarea { font:  normal small verdana, arial, helvetica, sans-serif; }\n\n/* [1] fixes a bug in old versions of WebKit, as used in Android 4.0. See\n * https://github.com/necolas/normalize.css/commit/79b3d21b697e ,\n * https://github.com/h5bp/mobile-boilerplate/issues/121 */\nbutton,\nhtml input[type=\"button\"], /* [1] */\ninput[type=\"reset\"],\ninput[type=\"submit\"] {\n    -webkit-appearance: button;\n    cursor: pointer;\n    /* Since we reset the padding on some of these things above, make it\n     * something reasonable (and consistent). */\n    padding: 2px 6px 3px;\n}\n\nbutton[disabled],\nhtml input[disabled] {\n    cursor: default;\n}\n\n/*html,body { height: 100%; }*/\n\n/* IE dumbness patch. hidden input in a hidden block that is\n * subsequently shown leads to the input to \"show\" and generate undesired\n * )padding.  This makes it go away. */ \ninput[type=hidden] { position: absolute; }\n\n/* html element defaults */\n\nh1 { font-size: 18px; font-weight: normal; margin: 10px 0 }\n\nh2 { color: #369; font-size: 13px; }\nh2 a { text-decoration: none }\nh2 a:visited { color: #369 }\nh2 a:hover { text-decoration: underline }\nh3 { font-size:110%; /*text-transform:uppercase;*/ }\n\na img { border: 0 none; }\na { text-decoration: none; color: #369; }\n\n/* Polyfill for HTML5 hidden attribute: http://caniuse.com/#feat=hidden */\n[hidden] { display: none; }\n\n/*\na:active { border: 0 none;}\na:focus { -moz-outline-style: none; }\n*/\n\n.rounded {\n    border-radius: 7px;\n}\n\n.rounded .morelink {\n    border-top-right-radius: 6px;\n}\n\n\ndiv.autosize { display: table; width: 1px}\ndiv.autosize > div { display: table-cell; }\n\ninput.txt {\n    background-color:#f7f7f7;\n    border: 1px solid #369; \n}\n\ninput[type=checkbox], input[type=radio] { margin-top: .4em; }\n\n/* forms */\n\nlabel.disabled { color: gray; }\n.wrong {color: red; font-weight: normal} \n.attention {\n    font-weight: bold;\n    border: solid 1px #ff6600;\n    padding: 3px;\n    border-radius: 7px;\n}\n\n.subform input.text { width: 25em }\n.subform textarea.text { width: 25em }\n.subform label { margin: 0 5px 0 5px }\n.subform td { padding: 0px 5px 5px 0}\n.subform td.nopadding { padding: 0px}\n\n.nowrap { white-space: nowrap; }\n.leftpad { padding-left: 1em }\n.nomargin { margin: 0px }\n.nopadding { padding: 0px }\n\n/* Fancy buttons */\n\n\n.fancybutton {\n    padding: 5px 10px;\n    background: -webkit-gradient(linear, 0% 0%, 0% 100%, from( hsl(210, 54%, 89%)), to( hsl(210, 54%, 79%)));\n    background: -moz-linear-gradient(top, hsl(210, 54%, 89%), hsl(210, 54%, 79%));\n    background-color: #ADC9E6;\n    border: 1px solid #5E96CF;\n    border-radius: 7px;\n    -webkit-background-clip: padding-box;\n    -moz-background-clip: padding-box;\n    color: #2E6399;\n    text-shadow: 0px 1px 0px hsla(0, 0%, 100%, .7);\n    -webkit-box-shadow: inset 0px 1px 0px hsla(0, 0%, 100%, .8);\n    -moz-box-shadow: inset 0px 1px 0px hsla(0,0%, 100%, .8);\n    box-shadow: inset 0px 1px 0px hsla(0,0%,100%,.8);\n    text-decoration: none;\n    font-weight: bold;\n}\n.fancybutton:hover {\n    background: -webkit-gradient(linear, 0% 0%, 0% 100%, from( hsl(210, 54%, 93%)), to( hsl(210, 54%, 89%)));\n    background:-moz-linear-gradient(top, hsl(210, 54%, 93%), hsl(210, 54%, 89%));\n    background-color: #D4E3F2;\n}\n.fancybutton:focus, .fancybutton:active {\n    background: -webkit-gradient(linear, 0% 0%, 0% 100%, to( hsl(210, 54%, 89%)), from( hsl(210, 54%, 79%)));\n    background: -moz-linear-gradient(top, hsl(210, 54%, 79%), hsl(210, 54%, 89%));\n    background-color: #D4E3F2;\n    -webkit-box-shadow: inset 0px -1px 0px hsla(0,0%,100%,.7);\n    -moz-box-shadow: inset 0px -1px 0px hsla(0,0%,100%, .7);\n    box-shadow: inset 0px -1px 0px hsla(0,0%, 100%, .7);\n}\n\n.fancybutton.disabled, \n.fancybutton.disabled:active {\n    background: -webkit-gradient(linear, 0% 0%, 0% 100%, from( hsl(210, 24%, 93%)), to( hsl(210, 24%, 89%)));\n    background:-moz-linear-gradient(top, hsl(210, 24%, 93%), hsl(210, 24%, 89%));\n    background-color: #D4E3F2;\n    border-color: #999;\n    color: #999;\n}\n\n/* header / menus */\n\n.hover a:hover { text-decoration: underline }\n\n.selected, .choice.primary { font-weight: bold; }\n\n.flat-list {list-style-type: none; display: inline;}\n.flat-list li, .flat-list form {display: inline; white-space: nowrap; }\n\n.flat-list li a.gold { color: #9a7d2e; font-weight: bold; }\n.flat-list li.selected a { color: orangered; }\n\n.link .flat-list { display: block; padding: 1px 0; }\n.link.compressed .flat-list { display: inline-block; padding: 0 0 1px 0; }\n\nul.flat-vert {text-align: left;}\n.flat-vert .separator { margin: 0 }\n\n.flat-vert.title {\n    font-family:arial,verdana,helvetica,sans-serif;\n    color: #777;\n    font-size: 18px;\n    font-weight: normal;\n    margin-bottom: 5px;\n }\n\n.separator { color: gray; margin: 0px .7ex 0px .7ex; cursor: default; }\n\n.pref-lang { font-weight: bold; }\n.pref { font-weight: bold; }\n\n#jumpToContent { position: absolute; left: 135px; top: 25px; font-weight: bold; margin-left: -1000px;}\n#jumpToContent:focus { margin-left: 0 !important; }\n\n#header {\n\n    border-bottom: 1px solid #5f99cf;\n\n    position: relative;\n    background-color: #cee3f8;\n    z-index: 99;\n}\n\n#header-img {margin-top: 2px; margin-right: 5px;}\n\n#header-img.default-header {\n    text-indent: -9999px;\n    background-image: url(../reddit.com.header.png); /* SPRITE */\n    height: 40px;\n    width: 120px;\n    display: inline-block;\n    vertical-align: bottom;\n    margin-bottom: 3px;\n}\n\n#header-top {\n    position: absolute;\n    right: 5px;\n}\n\n#header-bottom-left {\n    font-size: larger;\n}\n\n#header-bottom-right {\n    position: absolute;\n    right: 0px;\n    bottom: 0px;\n    background-color: #EFF7FF;\n    padding: 4px;\n    line-height: 12px;\n    border-top-left-radius: 7px;\n }\n\n#mail {\n    position: relative;\n    top: 2px;\n    display: inline-block;\n    text-indent: -9999px;\n    overflow: hidden;\n    width: 15px;\n    height: 10px;\n}\n#mail.havemail {\n    background-image: url(../mail.png);  /* SPRITE */\n}\n#mail.nohavemail {\n    background-image: url(../mailgray.png);  /* SPRITE */\n}\n.message-count {\n  background-color: #FF7500;\n  color: #FFF;\n  font-size: 8px;\n  font-weight: bold;\n  padding: 0px 3px;\n  margin-left: 3px;\n  border-radius: 2px;\n  display: inline-block;\n}\n#modmail {\n    position: relative;\n    top: -2px;\n    display: inline-block;\n    text-indent: -9999px;\n    overflow: hidden;\n    width: 16px;\n    height: 16px;\n    margin-bottom: -6px;\n}\n#modmail.havemail {\n    background-image: url(../modmail.png);  /* SPRITE */\n}\n#modmail.nohavemail {\n    background-image: url(../modmailgray.png);  /* SPRITE */\n}\n.user {color: gray;}\n.user .userkarma {\n    font-weight: bold;\n}\n.beta-hint {\n    position: absolute;\n    cursor: inherit;\n    height: 24px;\n\n    opacity: 0.8;\n    &:hover {\n        opacity: 1;\n    }\n\n    a {\n        position: absolute;\n        text-indent: 24px;\n        white-space: nowrap;\n        overflow: hidden;\n        margin-left: -24px;\n        display: inline-block;\n        width: 20px;\n        height: 13px;\n        background: transparent data-uri('image/svg+xml;charset=UTF-8', '../flask.svg') center left no-repeat;\n        background-size: contain;\n    }\n}\n\n.pagename {\n    font-weight: bold;\n    margin-right: 1ex;\n    font-variant: small-caps;\n    font-size: 1.2em;\n    vertical-align: bottom;\n}\n.pagename a {color: black; }\n.redditname { }\n\n.newpagelink {\n    padding: 3px 5px;\n    background-color: #ff9;\n}\n\n.dropdown {\n    cursor: default;\n    display: inline;\n    position: relative;\n}\n\n.drop-choices.inuse { display: block; }\n\n.drop-choices {\n    position: absolute;\n    left: 0px; /* top gets set in js */\n    border: 1px solid gray;\n    z-index: 100;\n    background-color: white;\n    white-space: nowrap;\n    line-height: normal;\n    margin-top: 1px;\n    display: none;\n}\n\n\n.drop-choices a.choice {\n    cursor: pointer;\n    padding: 2px 3px 1px 3px;\n    display: block;\n }\n\n.drop-choices a.choice:hover {\n    background-color: #c7def7;\n}\n\n.drop-choices a.choice.selected {\n    display: none;\n}\n\n.dropdown.lightdrop .selected {\n    position: relative;\n    background: none no-repeat scroll center right;\n    background-image: url(../droparrowgray.gif);\n    padding-right: 21px;\n    text-decoration: underline;\n    color: gray; \n }\n\n.drop-choices.lightdrop {\n    margin-top: 2px;\n }\n\n/*tab drop*/\n.dropdown.tabdrop .selected {\n    position: relative;\n    background: white none no-repeat scroll center right;\n    background-image: url(../droparrowgray.gif);\n    padding: 2px 21px 1px 5px;\n    margin-left: 3px;\n    border: 1px solid #5f99cf;\n    border-bottom: none;\n    color: orangered;\n}\n\n.dropdown.tabdrop .selected.title {\n    background-color: #eff7ff;\n    color: #369;\n    padding-bottom: 0;\n    border:none;\n}\n.drop-choices.tabdrop {margin-top: 2px;}\n.dropdown-title.tabdrop { display: none }\n\n.drop-choices .choice.hidden {\n    display: none;\n}\n\n.tabmenu {\n    list-style-type: none;\n    white-space: nowrap;\n    display: inline-block;\n    margin-top: 5px;\n    vertical-align: bottom;\n}\n\n.tabmenu li {\n    display: inline;\n    font-weight: bold;\n    margin: 0px 3px;\n}\n\n.tabmenu li a {\n    padding: 2px 6px 0 6px;\n    background-color: #eff7ff;\n }\n\n.tabmenu li.selected a{\n    color: orangered;\n    background-color: white;\n    border: 1px solid #5f99cf;\n    border-bottom: 1px solid white;\n    z-index: 100;\n}\n\n.tabpane-content { border: 1px solid #5f99cf; padding: 4px 4px 4px 4px; }\n\n.content {\n    z-index: 1;\n    margin: 7px 5px 0px 5px;\n}\n\n.content .spacer { margin-bottom: 5px }\n\n.state-button { display:inline }\n\n/* side box menus */\n\n.side {\n    float: right; \n    background-color: white; \n    margin: 0px 5px 0 5px;\n    width: 300px;\n}\n\n.side .spacer {\n    margin: 7px 0 12px 0;\n }\n\n.side .side-message {\n    background: lighten(@infobar-legacy-color, 10%) no-repeat 10px 10px;\n    border: 1px solid darken(@infobar-legacy-color, 20%);\n    border-radius: 2px;\n    padding: 10px;\n    line-height: 1.75em;\n\n    &:before {\n        content: '';\n        display: inline-block;\n        float: left;\n        background-image: url(../icon-info.png);  /* SPRITE */\n        width: 16px;\n        height: 16px;\n        margin-right: 7px;\n    }\n\n    p {\n        font-size: .9em;\n        margin: 0;\n\n        strong {\n            display: block;\n            font-weight: normal;\n            font-size: 1.25em;\n        }\n    }\n\n    p + p {\n        margin-top: .25em;\n    }\n\n    &.gold {\n        font-family: serif;\n        border: 1px solid lighten(#c4b487, 10%);\n        box-shadow: 0 0 10px lighten(#dad0b3, 10%) inset;\n        border-radius: 0;\n\n        &:before {\n            background-image: url(../gold-coin.png);  /* SPRITE */\n            width: 13px;\n            height: 14px;\n            margin-top: 1px;\n        }\n    }\n}\n\n.morelink {\n    display:block;\n    text-align: center;\n    position: relative;\n\n    border: 1px solid #c4dbf1;\n\n    background: white none repeat-x scroll center left;\n    background-image: url(../gradient-button.png);  /* SPRITE stretch-x */\n\n    font-size:150%;\n    font-weight:bold;\n\n    letter-spacing:-1px;\n    line-height: 29px;\n    height: 29px;\n}\n\n.morelink:hover, .mlh {\n    border-color: #879eb4;\n    background-image: url(../gradient-button-hover.png);  /* SPRITE stretch-x */\n}\n\n.morelink a {\n    display: block;\n    width: 100%;\n    height: 100%;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    color:#369;\n}\n\n.morelink:hover a {\n     color:white;\n}\n\n.morelink .nub {\n    position: absolute;\n    top: -1px;\n    right: -1px;\n    height: 31px;\n    width: 24px;\n    background: white none no-repeat scroll center left;\n    background-image: url(../gradient-nub.png);  /* SPRITE */\n}\n\n.morelink:hover .nub, .mlhn {\n    background-image: url(../gradient-nub-hover.png);  /* SPRITE */\n}\n\n.disabled .morelink, .disabled .morelink:hover {\n    background-image: url(../gradient-button-gray.png);  /* SPRITE stretch-x */\n    border-color: #dadada;\n}\n\n.disabled .morelink a {\n    cursor: default;\n    color: #aaa;\n}\n\n.disabled .morelink .nub, .disabled .morelink:hover .nub {\n    background-image: url(../gradient-nub-gray.png);  /* SPRITE */\n}\n\n/* raised box */\n\n.raisedbox {\n    padding: 5px;\n    background: #E0E0E0;\n    border: 1px solid gray;\n}\n\n.raisedbox h4 { margin-bottom: 3px }\n.raisedbox li {margin-bottom: 2px;}\n\n.sidebox .spacer {\n    position: relative;\n    margin-top: 10px;\n    padding: 5px 0 0 44px;\n    min-height: 41px;\n    background: white none no-repeat scroll top left;\n}\n\n.sidebox .spacer.no-icon {\n    padding: 0;\n    min-height: 0;\n}\n\n.sidebox .spacer a {\n    position: absolute;\n    top: 0; left: 0px;\n    display: block;\n    height: 40px;\n    width: 40px;\n}\n\n.sidebox.create .spacer a {\n    background-image: url(../create-a-reddit.png);  /* SPRITE */\n    background-repeat:no-repeat;\n}\n\n.sidebox.gold .spacer a {\n    background-image: url(../reddit_gold-40.png);  /* SPRITE */\n    background-repeat:no-repeat;\n}\n\n.sidebox.gold .morelink {\n    border:none;\n    background-color:transparent;\n    background-image: url(../goldmorelink.png);  \n    background-position: 0 0;\n    background-repeat:no-repeat;\n    height:31px;\n}\n\n.sidebox.gold .morelink a, .sidebox.gold .morelink a:visited {\n    color:#9a7d2e;\n}\n\n.sidebox.gold .morelink:hover {\n    background-position: 0 -31px;\n}\n\n.sidebox.gold .morelink:hover a {\n    color:#ffffff;\n    margin-top:1px;\n}\n\n.sidebox.gold .morelink .nub {\n    display: none;\n}\n\n.submit.mod-override .morelink {\n    a:after {\n        background-image: url(\"../shield.png\");\n        content: \" \";\n        position: absolute;\n        height: 16px;\n        width: 16px;\n        margin: 7px;\n    }\n\n    &:hover a:after {\n        opacity: 0.5;\n    }\n}\n\n.sidebox .subtitle {\n    margin-left: 10px;\n    color: dimgray;\n    font-size: 110%;\n}\n\n.account-activity-box {\n    text-align: center;\n}\n\n#account-activity table {\n    margin: 2em 0 0 2em;\n    width: 45em;\n    font-size: larger;\n}\n\n#account-activity th {\n    font-weight: bold;\n}\n\n#account-activity td {\n    padding: .5em 0;\n}\n\n.infotable { margin-top: 5px; margin-bottom: 10px; }\n.infotable .small { font-size: smaller; }\n.infotable td { padding-right: 1em; }\n.infotable a:hover { text-decoration: underline }\n.infotable .state-button a {  background-color: #F0F0F0; color: gray; }\n.infotable .bold { font-weight: bold; }\n.infotable .invalid-user { background-color: pink}\n.infotable .organic-vote { border: 1px solid green; }\n\n\n/* used on profile pages */\n\n.profile-attr {}\n.profile-attr .label {font-weight: bold; }\n.profile-attr .value {color: #404040; \n                       margin-right: 5px; }\n\n.profile-attr .md {\n    margin-left: 10px; \n    margin-top: 5px; \n    border-color: #B2B2B2 #D0D0D0 #D0D0D0 #B2B2B2;\n    border-style: solid;\n    border-width: 1px;\n    padding: 10px; }\n\n.profile-attr .md ul {\n    float: none; \n    list-style-type: disc;\n    margin-left: 15px; \n}\n\n.profile-attr .md p { margin-top: 0px; }\n\n.question { color: red; }\n.question .yes { margin-left: 5px; margin-right: 3px; }\n.question .no  { margin: 0px 3px 0px 3px; }\n\n/* thing rendering */\n\n.arrow {\n    margin: 2px 0px 0px 0px;\n    width: 100%;\n    height: 14px;\n    display: block;\n    cursor: pointer;\n    background-position: center center;\n    background-repeat: no-repeat; \n    width: 15px;\n    margin-left: auto;\n    margin-right: auto; \n    outline: none;\n}\n\n.arrow.upmod { \n    background-image: url(../aupmod.gif);  /* SPRITE */\n}\n.arrow.downmod { \n    background-image: url(../adownmod.gif);  /* SPRITE */\n}\n.arrow.up { \n    background-image: url(../aupgray.gif);  /* SPRITE */\n}\n.arrow.down { \n    background-image: url(../adowngray.gif);  /* SPRITE */\n}\n\n.midcol {\n    float: left; \n    margin-right: 7px;\n    margin-left: 7px; \n    background: transparent; \n    overflow: hidden;\n}\n\nbody > .content .link.compressed .midcol {\n    width: 15px;\n    margin-right: 5px;\n}\n\n.entry {\n    overflow: hidden; \n    margin-left: 3px;\n    opacity: 1;\n}\n.domain { color: #888; font-size:x-small; white-space: nowrap; }\n.domain a {\n    color: #888;\n    display: inline-block;\n    overflow: hidden;\n    white-space: nowrap;\n    text-overflow: ellipsis;\n    vertical-align: middle;\n    max-width: 19em;\n}\n.domain a:hover { text-decoration: underline; max-width: none; }\n.spam .domain,\n.spam .domain a {\n    color: black;\n}\n\n.link-note {\n    background-color: white;\n    color: #ff4444;\n    font-size:x-small;\n}\n\n@moderator-color: #228822;\n@admin-color: #ff0011;\n\n.user-distinction {\n  color: #888;\n  font-size:x-small;\n  margin: 5px 5px 0px 5px;\n}\n\n.user-distinction .admin {\n  color: @admin-color;\n  text-decoration: none;\n  font-weight: bold;\n}\n\n.tagline { color:#888; font-size:x-small; }\n.tagline a {color: #369; text-decoration: none; }\n.tagline .friend    { color: orangered }\n.tagline .submitter { color: #0055df }\n.tagline .moderator, .green { color: @moderator-color }\n.tagline .admin { color: @admin-color; }\n.tagline .alum { color: #BE1337; }\n.tagline a.author.admin { font-weight: bold }\n.tagline a:hover { text-decoration: underline }\n.tagline .edited-timestamp{ cursor: default }\n.tagline .stickied-tagline { color: @moderator-color; }\n.comment .tagline .stickied-tagline:before {\n  content: \"- \";\n}\n\n.tagline .userattrs .cakeday {\n    display: inline-block;\n    text-indent: -9999px;\n    width: 11px;\n    height: 8px;\n    background-image: url(../cake.png); /* SPRITE */\n    vertical-align: middle;\n}\n\na.author { margin-right: 0.5em; }\n.tagline .subreddit {\n    .userattrs { margin-left: 0.5em; }\n    .admin-distinguish { color: @admin-color; }\n    .moderator-distinguish { color: @moderator-color; }\n}\na.banned-user { color: red; }\n\n.thing .parent {\n    .stamp,\n    .author {\n        margin-right: 0.5em;\n    }\n}\n\n\n.flair, .linkflairlabel {\n    display: inline-block;\n    margin-right: .5em;\n    padding: 0 2px;\n    background: #f5f5f5;\n    color: #555;\n    border: 1px solid #ddd;\n    border-radius: 2px;\n}\n\n.collapsed .flair { display: none; }\n\n.flair input {\n    font-size: xx-small;\n}\n\n.linkflairlabel {\n    font-size: x-small;\n    max-width: 10em;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n}\n\n.link .flair {\n    font-size: x-small;\n    margin-top: -1px;\n}\n\n.flair-settings { margin-bottom: 16px; }\n\n.flairlist .flair-jump {\n    margin-bottom: 1em;\n}\n\n.flairlist .flair-jump input[type=\"text\"] { width:430px; }\n.flair-jump button { font-size:100%; }\n\n.flairlist.pretty-form { font-size:inherit; }\n\n.flairlisthome, .flairlist .nextprev {\n    display: inline-block;\n    margin-top: 10px;\n}\n\n.flairlisthome { font-size: smaller; }\n\n.flaircell, .flairlist .header {\n    display: inline-block;\n    text-align: center;\n    width: 30ex;\n    margin-right: 4ex;\n}\n\n.flair-entry { display: inline-block; }\n\n.flaircell.narrow, .flairlist .header.narrow { width: 14ex; }\n\n.flairsample-left { text-align: right !important; }\n.flairsample-right { text-align: left !important; }\n\n.flairrow .tagline {\n    display: inline-block;\n    margin-bottom: 8px;\n    margin-left: 6px;\n    text-align: left;\n    width: 36ex;\n}\n.flairlist .flaircell input[type=\"text\"] { width: 28ex; }\n.flairrow > form button { display: none; }\n.flairrow .edited button { display: inline-block; }\n\n.flairrow .flairdeletebtn { display: inline; }\n.flairrow:hover .flairdeletebtn { opacity: 1.0; }\n\n.reportform {\n    position: relative;\n    display: none;\n    max-width: 450px;\n}\n\n.reportform.active {\n    display: block;\n}\n\n.flairselector {\n    box-shadow: 4px 4px 4px #ccc;\n    font-size: x-small;\n    position: absolute;\n    width: 400px;\n}\n\n.flairselector img { margin: none; }\n\n.flairselector h2 {\n    background: #cee3f8;\n    padding-bottom: 2px;\n    margin-bottom: 4px;\n    text-align: center;\n}\n\n.flairselector.drop-choices.active {\n    border: 1px solid gray;\n    display: block;\n}\n\n.flairselector .error { text-align: center; }\n.flairselector ul {\n    display: inline-block;\n    max-width: 200px;\n    overflow: hidden;\n    vertical-align: top;\n}\n\n.flairselector .selected, .flairselector.active li {\n    display: block;\n    font-weight: normal;\n    text-decoration: none !important;\n}\n\n.flairselector li {\n    border: 1px solid white;\n    cursor: pointer;\n    display: block !important;\n    padding-left: 4px;\n}\n\n.flairselector li a {\n    color: #369 !important;\n    font-weight: normal !important;\n}\n\n.flairselector li:hover { background-color: #bbb;  border: 1px solid #bbb; }\n.flairselector li a:hover { text-decoration: none; }\n.flairselector li.selected { border: dashed 1px black; }\n.flairselector .title { font-size: x-small !important; }\n\n.flairselector form {\n    border-top: solid 1px gray;\n    clear: both;\n    display: block;\n    padding-top: 4px;\n    text-align: center;\n\n    > div {\n        margin: 2px 0;\n    }\n\n    button {\n        margin-left: 5px;\n    }\n}\n\n.flairoptionpane {\n    margin-bottom: 4px;\n    max-height: 200px;\n    overflow: auto;\n    text-align: center;\n}\n\n.flairselector .customizer { display: inline-block; }\n.flairselector .customizer input { display: none; }\n.flairselector .customizer button { display: inline !important; }\n\n.flairselector .flairremove { display: none; }\n\n.media-button .option { color: red; }\n.media-button .option.active {\n    background: none no-repeat scroll right center;\n    background-image: url(../reddit-button-play.png);   /* SPRITE */\n    padding-right: 15px;\n    color: #336699;\n}\n\n.embededmedia { margin-top: 5px; margin-left: 60px; }\n\n.thing .title {\n    color: blue;\n    outline: none;\n    margin-right: .4em;\n    padding: 0px;\n    overflow: hidden;\n}\n\n.thing .title:visited, .thing.visited .title { color: #551a8b }\n\n.thing.stickied.link a.title {\n  font-weight: bold;\n  color: @moderator-color;\n}\n\nbody.with-listing-chooser.explore-page #header .pagename {\n    position: static;\n}\n\n.explore-header {\n    font-weight: bold; \n    margin-bottom: 7px;\n    padding: 5px 0;\n    \n    #explore-settings {\n        input {\n            margin-left: 5px;\n        }\n        button {\n            color: #333;\n            font-weight: bold;\n            line-height: 10px;\n            margin-left: 8px;\n        }\n    }\n    \n    .explore-title {\n        font-size: 1.3em;\n    }\n}\n\n.explore-item {\n    margin-bottom: 1em;\n\n    .explore-label {\n        border-radius: 2px;\n        display: inline-block;\n        margin: 0 5px 1px 0;\n        padding: 1px 2px 2px;\n    }\n\n    .explore-label-type, .explore-label-link {\n        padding: 0 5px;\n    }\n\n    .explore-sr-details {\n        color: #777;\n        display: inline-block;\n        font-size: x-small;\n        font-weight: normal;\n        margin-left: 3px;\n    }\n\n    .explore-feedback {\n        display: inline-block;\n        .fancy-toggle-button .add, .fancy-toggle-button .remove {\n            background-color: transparent;\n            background-image: none;\n            border: none;\n            color: #aaa;\n            border: 1px solid #ccc;\n            border-radius: 2px;\n            margin-left: 10px;\n            padding-top: 0;\n\n            .option {\n                line-height: 7px;\n            }\n\n            &:hover {\n                color: white;\n                border: 1px solid #444;\n            }\n        }\n        .fancy-toggle-button .add {\n            &:hover {\n                background-image: url(../bg-button-add.png); /* SPRITE stretch-x */\n            }\n        }\n        .fancy-toggle-button .remove {\n            &:hover {\n                background-image: url(../bg-button-remove.png); /* SPRITE stretch-x */\n            }\n        }\n        .subscribe-button {\n            display: inline-block;\n            margin: 0 4px 0 0;\n        }\n    }\n\n    .explore-feedback-dismiss {\n        cursor: pointer;\n        display: inline-block;\n        text-indent: -9999px;\n        width: 9px;\n        height: 9px;\n        background-image: url(../close-small.png);  /* SPRITE */\n        background-repeat: no-repeat;\n        opacity: .3;\n        margin-left: 4px;\n        vertical-align: middle;\n        border: 3px solid transparent;\n        &:hover {\n            opacity: 1;\n        }\n    }\n\n    .explore-sr {\n        display: inline-block;\n        font-size: 1.1em;\n        font-weight: bold;\n        margin-bottom: 3px;\n        padding: 2px 4px;\n        line-height: 13px;\n        height: 18px;\n    }\n\n    .midcol {\n        display: none;\n    }\n\n    .rank {\n        display: none;\n    }\n}\n\n.explore-comment {\n    .explore-label {\n        background-color: #cee3f8;\n        border: solid thin #5f99cf;\n    }\n    .tagline, .buttons, .thumbnail, .expando-button {\n        display: none;\n    }\n    .comment {\n        border-left: solid 2px #eee;\n        color: #888;\n        margin: -3px 0 3px 5px;\n        max-height: 100px;\n        overflow-x: hidden;\n        overflow-y: hidden;\n        position: relative;\n        .md {\n            font-size: x-small;\n            padding-bottom: 2px;\n            p {\n                margin: 5px;\n            }\n        }\n    }\n    /* make long comment boxes fade to white instead of cutting off mid-line */\n    .comment-fade {\n        background: -moz-linear-gradient(bottom, rgba(255,255,255,1) 0%, rgba(255,255,255,0) 100%);\n        background: -webkit-gradient(linear, left bottom, left top, color-stop(0%,rgba(255,255,255,1)), color-stop(100%,rgba(255,255,255,0)));\n        bottom: 0;\n        border: none;\n        height: 10px;\n        position: absolute;\n        width: 100%;\n    }\n    .comment-link {\n        color: #888;\n        display: inline-block;\n        font-weight: bold;\n        padding: 0 0 8px 5px;\n    }\n}\n\n.explore-page.res-nightmode .comment-fade {\n    display: none;\n}\n\n.explore-hot .explore-label {\n    background-color: #fff088;\n    border: solid thin #c4b487;\n}\n\n.explore-rising .explore-label {\n    background-color: #d6fbcb;\n    border: solid thin #485;\n}\n\n.explore-discovery .explore-label {\n    background-color: #dedede;\n    border: solid thin #aaa;\n}\n\n.explore-subscribe-bubble {\n    margin-left: 22px;\n}\n\n.sitetable { list-style-type: none; }\n.ajaxhook { position: absolute; top: -1000px; left: 0px; }\n\n.nextprev, .next-suggestions {\n    color: gray;\n    font-size: larger;\n    margin-top: 10px;\n\n    a {\n        padding: 1px 4px;\n        background: #eee;\n        border: 1px solid #ddd;\n        border-radius: 3px;\n        font-weight: bold;\n    }\n\n    a:hover {\n        background: #f0f0f0;\n        border: 1px solid #82A6C9;\n    }\n\n    a:active {\n        background: #e4e4e4;\n    }\n\n    .separator {\n        margin: 0;\n        margin-left: .5em;\n        padding-left: .5em;\n        border-left: 1px solid #ccc;\n    }\n\n}\n\n.next-suggestions {\n    margin-left: 0.75em;\n\n    a {\n        background: none;\n        font-weight: normal;\n        margin-left: .5em;\n    }\n}\n\n.next-suggestions .mark-all-read-container .throbber {\n  position: absolute;\n  margin-left: 5px;\n  margin-top: -2px;\n  padding-left: 22px;\n  min-width: 18px;\n  width: auto;\n  font-size: 10px;\n  line-height: 16px;\n}\n\n/* corner help */\n.help a.help {\n    color: #808080;\n    text-decoration: underline;\n}\n\n.help.help-cover {\n    position: relative;\n    background-color: #F8F8F8;\n    border: 1px solid gray;\n    display:none;\n    padding: 5px 10px 10px 10px;\n    overflow:hidden;\n }\n\n.help p, .help form { margin: 5px; font-size:110%; }\n.help form { display: inline; }\n\n.help-hoverable {\n    cursor: help;\n}\n\n.hover-bubble {\n    display: none;\n    position: absolute;\n    background: white;\n    color: #333;\n    border: 1px solid gray;\n    padding: 3px;\n    box-shadow: 0 2px 10px rgba(0,0,0,.25);\n    z-index: 100;\n\n    &:before, &:after {\n        position: absolute;\n        display: block;\n        content: '';\n    }\n\n    &.anchor-top {\n        &:before, &:after {\n            right: 8px;\n            border: 9px solid transparent;\n        }\n\n        &:before {\n            top: -19px;\n            border-bottom-color: gray;\n        }\n\n        &:after {\n            top: -18px;\n            border-bottom-color: white;\n        }\n    }\n\n    &.anchor-top-left {\n        &:before, &:after {\n            left: 8px;\n            border: 9px solid transparent;\n        }\n\n        &:before {\n            top: -19px;\n            border-bottom-color: gray;\n        }\n\n        &:after {\n            top: -18px;\n            border-bottom-color: white;\n        }\n    }\n\n    &.anchor-top-centered {\n        &:before, &:after {\n            left: 50%;\n            margin-left: -9px;\n            border: 9px solid transparent;\n        }\n\n        &:before {\n            top: -19px;\n            border-bottom-color: gray;\n        }\n\n        &:after {\n            top: -18px;\n            border-bottom-color: white;\n        }\n    }\n\n    &.anchor-right, &.anchor-left {\n        &:before, &:after {\n            top: 8px;\n            border: 9px solid transparent;\n        }\n    }\n    &.anchor-right {\n        &:before {\n            right: -19px;\n            border-left-color: gray;\n        }\n\n        &:after {\n            right: -18px;\n            border-left-color: white;\n        }\n    }\n\n    &.anchor-left {\n        &:before {\n            left: -19px;\n            border-right-color: gray;\n        }\n\n        &:after {\n            left: -18px;\n            border-right-color: white;\n        }\n    }\n}\n\n.help-bubble {\n    width: 35em;\n\n    p, form {\n        margin: .5em;\n    }\n\n    a {\n      font-weight: bold;\n    }\n\n    a:hover {\n        text-decoration: underline\n    }\n}\n\n.hover-bubble.multi-selector {\n    @arrow-offset: 40px;\n    margin-top: -7px - @arrow-offset;\n    min-width: 130px;\n    min-height: @arrow-offset;\n    padding: 8px 0;\n    .no-select;\n\n    &:before, &:after {\n        top: 8px + @arrow-offset;\n    }\n\n    strong, a.sr {\n        display: block;\n        margin: 3px 0;\n        text-align: center;\n    }\n\n    strong {\n        font-size: 1.05em;\n        font-weight: bold;\n        color: #333;\n    }\n\n    .throbber {\n        position: absolute;\n        top: 10px;\n        right: 8px;\n    }\n\n    .multi-list {\n        margin-top: 5px;\n    }\n\n    label {\n        font-size: 1.25em;\n        display: block;\n        padding: 5px 12px;\n\n        &:hover {\n            background: #eee;\n        }\n\n        input[type=\"checkbox\"] {\n            margin-top: 0;\n            margin-right: 5px;\n            vertical-align: middle;\n        }\n\n        a {\n            float: right;\n            margin-left: 7px;\n            width: 12px;\n            height: 12px;\n            line-height: 12px;\n            background: white;\n            border: 1px solid lighten(#369, 20%);\n            border-radius: 2px;\n            text-align: center;\n            opacity: .65;\n\n            &:hover {\n                opacity: 1;\n            }\n        }\n    }\n\n    .create-multi {\n        input[type=\"text\"] {\n            .light-text-input;\n        }\n    }\n}\n\n.infotext { \n    border: 1px solid #369;\n    background-color: #EFF7FF;\n    -webkit-box-shadow: inset 0px 1px 0px hsla(0,0%,100%,.8), 0px 1px 0px hsla(0,0%,100%,.6);\n    -moz-box-shadow: inset 0px 1px 0px hsla(0,0%,100%,.8), 0px 1px 0px hsla(0,0%,100%,.6);\n    box-shadow: inset 0px 1px 0px hsla(0,0%,100%,.8), 0px 1px 0px hsla(0,0%,100%,.6);\n}\n\n.infotext p { \n    font-size: small;\n    margin: 5px;\n}\n\n.wikiaction-revisions::before {\n    background-image: url(../report.png); /* SPRITE */\n}\n\n.wikiaction-pages::before {\n    background-image: url(../page_white_copy.png); /* SPRITE */\n}\n\n/* organic listing */\n@min-uncompressed-height: 82px;\n@min-compressed-height: 51px;\n\n.organic-listing {\n    border: solid 1px gray;\n    padding: 0;\n    overflow: hidden; \n    position: relative;\n    margin-bottom: 7px;\n\n    &.loading {\n        display: none;\n    }\n\n    .link {\n        background-color: #F8F8F8;\n        padding-top: 5px;\n        padding-bottom: 5px;\n        // subtract padding from min heights\n        min-height: @min-uncompressed-height - 10px;\n    }\n\n    body.compressed-display & .link {\n        padding-top: 7px;\n        padding-bottom: 7px;\n        min-height: @min-compressed-height - 14px;\n    }\n}\n\n.organic-listing.show-placeholder.loading {\n    display: block;\n    height: @min-uncompressed-height;\n    body.compressed-display & {\n        height: @min-compressed-height;\n    }\n    opacity: .5;\n\n    & .help, & .throbber {\n        display: none;\n    }\n}\n\n.organic-listing .link,\n.organic-listing .link.compressed,\n.organic-listing .link.promotedlink {\n    padding-right: 7em;\n    padding-left: 2px;\n    margin-bottom: 0px;  \n}\n\n.organic-listing .nextprev {\n    margin: 0px;\n    position: absolute;\n    right: 0px;\n    top: 0px;\n    vertical-align: top;\n    z-index: 1;\n}\n\n.organic-listing .nextprev .arrow, .organic-listing .nextprev .throbber {\n    width: 21px;\n    height: 21px;\n    margin: 5px 5px 2px 0px;\n}\n\n.organic-listing .nextprev .throbber {\n    vertical-align: top;\n    background-position: center center;\n}\n\n.organic-listing .nextprev .arrow {\n    border: solid 1px #B3B3B3;\n    display: inline-block;\n    position: relative;\n    /* This is a really weird value, but it needs to be low to hide text without affecting layout in IE. */\n    text-indent: 50px;\n}\n.organic-listing .nextprev .arrow.prev {\n    background-image: url(../prev_organic.png); /* SPRITE */\n}\n.organic-listing .nextprev .arrow.next {\n    background-image: url(../next_organic.png); /* SPRITE */\n}\n.organic-listing .nextprev .arrow:hover {\n    cursor: pointer;\n    border: solid 1px #336699;\n}\n.organic-listing .nextprev .arrow:active {\n    top: 1px;\n}\n\n.organic-listing .help {\n    color: #336699;\n    margin: 0px 5px 5px 0;\n    position: absolute;\n    right: -1px;\n    bottom: 0px;\n    z-index: 1;\n}\n\n.link.promotedlink {\n    /*background-color: lightgreen; */\n    border: 1px solid gray;\n    padding: 5px 0 5px 3px;\n    overflow: hidden;\n    position: relative;\n}\n.link.promotedlink.unpaid   { background-color: #FFC; }\n.link.promotedlink.unseen   { background-color: #FFC; }\n.link.promotedlink.accepted { background-color: #9F9; }\n.link.promotedlink.rejected { background-color: #FF9A9A; }\n.link.promotedlink.accepted { background-color: #9F9; }\n.link.promotedlink.pending  { background-color: #BFC; }\n.link.promotedlink.promoted { background-color: #EFF7FF; }\n.link.promotedlink.finished { background-color: #DDD; }\n.link.promotedlink.edited_live { background-color: #FFD59A; }\n#promo-form + form #img-preview-container { display: none; }\n\n.profile-page .link.promotedlink.saved {\n    background-color: white;\n    border: none;\n\n    .sponsored-tagline {\n        display: none;\n    }\n}\n\n.rejection-form textarea { \n    width: 40em;\n    height: 10em; \n}\n\n.promoted-list { font-size: larger; }\n.promoted-list .unpromote-button { display: inline }\n.promoted-list .unpromote-button a { color: gray; }\n\n\n.help-cover.promoted {\n    background-color: #EFF7FF;\n}\n\n.organic-listing .promoted {\n    background-color: #EFF7FF;\n    border: none;\n}\n\n.organic-listing .sponsored-tagline {\n    right: 6.8em;\n}\n\n.sponsored-tagline {\n    color: #808080;\n    bottom: 0;\n    margin: 0 5px 5px 0;\n    position: absolute;\n    font-weight: bold;\n    right: 0;\n}\n\n.geotarget-notice {\n    margin: 5px 10px;\n\n    .md p {\n        font-size: smaller;\n        margin: 1px 0 0;\n    }\n\n    div:before {\n        content: \"\";\n        float: left;\n        height: 16px;\n        width: 20px;\n        background-repeat: no-repeat;\n    }\n\n    &.city div:before {\n        background-image: url(../map.png); /* SPRITE */\n    }\n\n    &.country div:before {\n        background-image: url(../world.png); /* SPRITE */\n    }\n}\n\n.promote-pixel {\n    position: absolute;\n    top: -1000px;\n    right: -1000px;\n}\n\n.organic-help-button { padding: 0 .5ex; }\n\n.menuarea {\n    border-bottom: 1px dotted gray;\n    padding: 5px 10px;\n    margin: 5px;\n    overflow: hidden; \n    font-size: larger;\n}\n\n.menuarea .spacer {display: inline; margin-right: 15px}\n\n.panestack-title {\n    margin: 10px 310px 0px 10px;\n    padding-bottom: 3px;\n    border-bottom: 1px dotted gray;\n}\n\n.panestack-title .title {\n    font-size: 16px;\n    font-weight: normal;\n    margin: 10px 0;\n}\n\n.panestack-title a.title-button {\n    font-size: 12px;\n    margin-left: 8px;\n}\n\n.panestack-title a.title-button.gold {\n    background-color: #fff088;\n    color: #6a4d00;\n    border: 1px solid #9a7d2e;\n    padding: 1px 5px;\n    border-radius: 3px;\n}\n\n.commentarea .menuarea {\n    border: none;\n    margin: 0 310px 10px 10px;\n    padding: 0;\n    color: gray;\n\n    form.toggle { margin-left: 8px; }\n}\n\n.commentarea .menuarea .toggle {\n    display: inline-block;\n}\n\n.commentarea .menuarea .toggle a {\n    color: gray;\n    font-weight: bold;\n    font-size: x-small;\n}\n\n.commentarea > .usertext {\n    margin: 0 0 10px 10px;\n    overflow: auto;\n}\n\n.infobar.red { \n    padding: 5px;\n    background-color: #FFAEAE;\n    border-color: red;\n}\n\n.infobar.red img {\n    float: left;\n    margin-right: 5px;\n}\n\n\n.infobar.mellow {\n    background-color: #eff8ff;\n    border: 1px solid #93abc2;\n}\n\n.infobar.gold {\n    background-color: #fffdcc;\n    border: 1px solid #e1b000;\n    color: #9a7d2e;\n}\n\n.content .infobar.gold:before {\n    margin-top: 5px;\n    margin-right: 7px;\n}\n\n.infobar.welcome {\n    display: none;\n    background: url(../welcome-lines.png) top center;\n    border: 1px solid #ff8b60;\n    padding: 0;\n    height: 80px;\n    overflow: hidden;\n    margin-right: 0;  /* work around safari 5 width issue */\n    white-space: nowrap;\n}\n\n.infobar.welcome h1, .infobar.welcome h2 {\n    display: inline-block;\n    font-weight: normal;\n    margin: 0;\n}\n\n.infobar.welcome h1 {\n    margin-top: 14px;\n    margin-left: 2%;\n    padding: 7px 16px;\n    font-size: 16px;\n    background: white;\n    border-bottom: 2px solid #5f99cf;\n}\n\n.infobar.welcome .button-row {\n    position: relative;\n    top: -8px;\n    margin-left: 10%;\n}\n\n.infobar.welcome h2 {\n    padding: 4px 14px;\n    padding-left: 38px;\n    background: white url(../welcome-upvote.png) 12px center no-repeat;\n    font-size: 13px;\n    color: #222;\n    border-bottom: 2px solid #ff4500;\n}\n\n.infobar.welcome a {\n    margin-left: 2%;\n    background: #e75018;\n    font-size: 11px;\n    font-weight: bold;\n    color: white;\n    padding: 5px 10px;\n    border-radius: 4px;\n    border-bottom: 2px solid #a73a11;\n}\n\n.infobar.welcome a:hover {\n    background: #f0571e;\n    border-bottom-color: #c74514;\n}\n\n.infobar.welcome a:active {\n    position: relative;\n    top: 1px;\n    background: #df531f;\n    border-bottom: 1px solid #a73a11;\n}\n\n.infobar.newsletterbar {\n    .box-sizing(border-box);\n    position: relative;\n    overflow: hidden;\n    min-height: 80px;\n    padding: 15px 20px 20px 25px;\n    border: none;\n    border-radius: 2px;\n    background-color: #30659B;\n\n    header {\n        float: left;\n        height: 45px;\n        width: 325px;\n    }\n\n    a.newsletter-close {\n        position: absolute;\n        right: 3px;\n        top: 0;\n        font-size: 11px;\n        color: #CCC;\n    }\n\n    form {\n        margin: 2px 150px 0 340px;\n        max-width: 400px;\n        min-width: 150px;\n        line-height: 45px;\n        white-space: nowrap;\n    }\n\n    .subscribe-thanks {\n      display: none;\n    }\n\n    &.success {\n        header {\n            padding-left: 65px;\n\n            &:before {\n                content: \"✓\";\n                color: #80d654;\n                font-weight: bold;\n                font-size: 60px;\n                position: absolute;\n                top: 0;\n                left: 15px;\n            }\n        }\n\n        .subscribe-callout {\n          display: none;\n        }\n\n        .subscribe-thanks {\n          display: block;\n        }\n    }\n\n    h1 {\n        margin: 0;\n\n        a:hover {\n            border-bottom: 1px dotted #999;\n        }\n    }\n\n    h2 {\n        color: white;\n        font-weight: normal;\n        font-size: 14px;\n        margin-top: 5px;\n    }\n\n    .c-form-group {\n        width: 100%;\n    }\n\n    .c-form-control-feedback-wrapper {\n        top: 5px;\n    }\n\n    button {\n        .button-size(@padding-base-vertical; @padding-base-horizontal; 12px; 20px; 3px);\n        margin-left: 10px;\n    }\n\n    @media screen and (max-width: @screen-md-min) {\n        header {\n            float: none;\n        }\n\n        form {\n            margin: 10px 0 0;\n        }\n\n        .c-form-group {\n            max-width: 50%;\n        }\n    }\n}\n\n.locationbar {\n    margin: 5px;\n\n    .md, .md p, .options {\n        color: #888888;\n        font-weight: bold;\n        font-size: 11px;\n        display: inline;\n    }\n\n    .options {\n        margin-left: 15px;\n    }\n}\n\n/*top link*/\na.star { text-decoration: none; color: #ff8b60 }\n\n.odd  { }\n.even { }\n\n/* buttons on main link style */\n.entry .buttons li {\n    display: inline-block;\n    border: none;\n    padding-right: 4px;\n    line-height: 1.6em;\n}\n.entry .buttons li + li {\n    padding-left: 4px;\n}\n\n.entry .buttons li.stamp + li.stamp {\n    margin-left: 4px;\n}\n\n.entry .buttons li a {\n    color: #888;\n    font-weight: bold;\n    padding: 0 1px;\n}\n\n.entry .buttons li a.nonbutton {\n    color: #369;\n    font-weight: normal;\n}\n\n.entry .buttons a:hover {text-decoration: underline}\n\n.entry .buttons .status-msg {\n    display: none;\n    margin-right: .5em;\n}\n\n/* links */\n\n.toggle .error { font-size: x-small; }\n.toggle .option { display: none; }\n.toggle .option.active { display: inline; }\n\n.thing .stub { display: none; }\n.link.last-clicked { border: 1px dashed gray; overflow: hidden; }\n\n.link { margin: 0; margin-bottom: 8px; padding-left: 3px; }\n.link .score {text-align: center; color: #c6c6c6;}\n.link .title {font-size:medium; font-weight: normal; margin-bottom: 1px;}\n\n.link .child h3 {\n    margin: 15px; \n    text-transform: none; \n    font-size: medium; \n}\n\n.rank { overflow: hidden }\n\n.profile-page .link .rank, .single-page .link .rank { display: none; }\n\n.link .midcol {font-weight: bold; font-size: small;}\n\n.link .score.likes   { color: #FF8B60; }\n.link .score.dislikes { color: #9494FF; }\n.link .rank {\n    float:left;\n    margin-top: 15px;\n    color: #c6c6c6;\n    font-family: arial;\n    font-size: medium;\n    text-align: right;\n}\n\n.rank-spacer {\n    font-size: medium;\n}\n\n.midcol-spacer {\n    font-size: small;\n}\n\n.link.compressed { margin-bottom: 5px; }\n.link.compressed .rank { margin-top: 10px; }\n.link.compressed .title { margin: -2px 0 3px }\n.link.compressed .score { color: #888888 }\n.link.compressed .score-placeholder { height: 3px }\n.link.compressed .subreddit { font-weight: bold }\n.link.compressed .tagline { display: inline; margin-right: 12px }\n.link.compressed .expando-button { display: none; }\n\n/* display the right score based on whether they've voted */\n.score.likes, .score.dislikes {display: none;}\n.likes .score, .dislikes .score {display: none;}\n.likes .score.likes {display: inline;}\n.dislikes .score.dislikes {display: inline;}\n.likes div.score.likes {display: block;}\n.dislikes div.score.dislikes {display: block;}\n\n.warm-entry .rank { color: #EDA179; }\n.hot-entry .rank { color: #E47234; }\n.cool-entry .rank { color: #A5ABFB; }\n.cold-entry .rank { color: #4959F7; }\n\n/* recently viewed links */\n\n.gadget {\n  font-size: x-small;\n\n  .midcol {\n    width: 15px;\n    margin: 0;\n  }\n\n  .reddit-link-end {\n    clear: left;\n    padding-top: 10px;\n  }\n\n  .click-gadget {\n    font-size: small;\n  }\n\n  small {\n    color: gray;\n  }\n\n  .reddit-entry {\n    margin-left: 20px;\n  }\n\n  .right {\n    text-align: right;\n  }\n\n  .stamp:first-child {\n    margin-left: 0;\n  }\n\n  .score {\n    margin-left: 0.5em;\n  }\n}\n\n.quarantine-tool.noncollapsed {\n    .quarantine-info {\n        display: block;\n    }\n}\n\n.quarantine-tool.collapsed {\n    .quarantine-info {\n        display: none;\n    }\n}\n\n/* comments */\n\n.comment, .content .details { margin-left: 10px; }\n\n.comment.noncollapsed {\n    .numchildren {\n        display: none;\n    }\n\n    .usertext, .child, .buttons {\n        display: block;\n    }\n\n    .midcol {\n        visibility: visible;\n    }\n}\n\nbody.show-controversial .comment.controversial > .entry .score:after {\n  content: '†';\n  position: relative;\n  top: -2px;\n}\n\n.comment.collapsed {\n    padding-bottom: 10px;\n    line-height: 14px;\n\n    .numchildren {\n        display: inline;\n    }\n\n    .usertext, .child, .buttons {\n        display: none;\n    }\n\n    .midcol {\n        visibility: hidden;\n        height: 1px;\n    }\n\n    .tagline, .tagline a {\n        color: gray;\n\n        :not(.expand) {\n            font-style: italic;\n        }\n    }\n\n    &.collapsed-for-reason {\n        .collapsed-reason {\n            display: inline;\n        }\n\n        .score, .live-timestamp {\n            display: none;\n        }\n    }\n}\n\n.admin_takedown {\n    background-color: #F7F7F7;\n    color: #888888;\n    padding: 3px;\n    \n    a:link {\n        color: #326699;\n    }\n}\n\n.comment {\n    .midcol {\n        margin-left: 0px;\n        width: 15px;\n    }\n\n    .title {\n        font-size: small;\n        margin-top: 10px;\n    }\n\n    .author {\n        font-weight: bold;\n    }\n\n    .expand {\n        margin-right: 3px;\n        padding: 1px;\n    }\n\n    .child, .showreplies {\n        margin-top: 10px; \n        margin-left: 15px; \n        border-left: 1px dotted #DDF;\n    }\n\n    &.collapsed-for-reason {\n        .collapsed-reason {\n            display: none;\n        }\n    }\n}\n\n.comment.deleted > .midcol {\n    visibility: hidden;\n}\n\n.comment .showreplies {\n    display: block;\n    margin-top: 7px;\n    margin-bottom: 15px;\n    padding: 5px;\n}\n\ntextarea.gray { color: gray; }\n\n.deepthread:after {\n    background-image: url(../continue-thread.png);  /* SPRITE */\n    content: \" \";\n    display: inline-block;\n    width: 25px;\n    height: 9px;\n    margin: 5px 0 0 5px;\n}\n\n.deepthread a { font-size: larger; color: #336699 }\n.deepthread a:hover { text-decoration: underline}\n\n.morecomments {font-size: larger}\n.morecomments a { color: #336699 }\n.morecomments a:hover { text-decoration: underline}\n.morecomments .gray {font-weight: normal; color: gray}\n\n.expand-btn { \n    font-size: smaller;\n    margin: 0px 5px;  \n    margin-top: 4px; \n    display: inline-block; \n}\n\n.message.noncollapsed {\n    .numchildren {\n        display: none;\n    }\n\n    .child, .buttons, .md {\n        display: block;\n    }\n\n    .midcol {\n        visibility: visible;\n    }\n}\n\n.message.collapsed {\n    > .entry {\n        .buttons, .md {\n            display: none;\n        }\n    }\n\n    &.threaded {\n        .tagline, .tagline a {\n            color: gray;\n\n            :not(.expand) {\n                font-style: italic;\n            }\n        }\n\n        .child {\n            display: none;\n        }\n    }\n\n    .midcol {\n        visibility: hidden;\n        height: 20px;\n    }\n}\n\n.message {\n    margin: 10px 10px 20px 5px; \n    padding-left: 5px; \n    margin:10px 10px 20px 5px;\n    padding:7px;\n}\n\n.message .collapsed .head { \n    color: #888888;\n    font-style: italic;\n}\n\n\n.message .tagline {\n    color: #485;\n}\n\n.message.message-parent > .entry,\n.message.message-reply  > .entry {\n    color: #485;\n    \n    .md,\n    blockquote,\n    del {\n      color: inherit;\n    }\n}\n\n.message.recipient > .entry {\n    color: black;\n}\n\n.message.message-reply.recipient > .entry .head,\n.message.message-parent.recipient > .entry .head { \n    color: black; \n    font-weight: bold;\n}\n\n.message.color-bar {\n    border-left: 5px solid transparent;\n}\n\n.message {\n    .recipient a.author, .sender a.author, .subreddit {\n        font-weight: bold;\n    }\n}\n\n.message.new > .entry .head {\n  color:orangered; font-weight: bold;  \n}\n.message.new > .entry{ \n    background-color:#F7F7F7;\n    border:1px solid #E9E9E9;\n    padding: 6px;\n}\n\n.message.new .unread { \n    display: none; \n}\n\n\n.message.threaded .child {\n    margin-left: 20px;\n    border-left: 2px dashed #E7E7E7;\n}\n\n// message-reply and message-parent classes are only present in tree view\n.message.message-reply, .message.message-parent {\n    &:not(.threaded) .entry {\n        margin-left: 10px;\n        border-left: 2px dashed #E7E7E7;\n    }\n}\n\n.message .child .message,\n.message .child .usertext { \n    margin-top: 10px; \n    margin-left: 12px; \n}\n\n.message.was-comment .child .message,\n.message.was-comment .child .usertext { \n    margin-top: 0px;\n    margin-left: 0px; \n}\n\n.message .expand { \n    margin-right: 3px;\n    display: none;\n}\n\n.message .entry { \n    margin-left: 0px; \n}\n\n.message.message-parent .expand { \n    display: inline; \n}\n\n/* threaded message styles: remove padding */\n.message.message-parent .child .message,\n.message.message-reply  .child .message\n { \n    margin: 0;\n    padding: 0; \n}\n\n.message.message-parent .subject { \n    margin-bottom: 10px; \n}\n\n.message.message-parent .message .subject { \n    display: none; \n}\n\n.message.message-reply .subject { \n    display:none;\n}\n\n.message.message-reply .entry, \n.message.message-parent .entry { \n    padding-left: 10px;\n    padding-bottom: 10px; \n}\n\n\n.message .buttons,\n.message .md { margin-left: 15px; }\n.message .entry .parent { \n    border: 1px solid #336699; \n    max-width: 60em;\n    margin: 3px 10px; \n}\n\n.message .subject .correspondent {\n    background-color:#EFF7FF;\n    border:1px solid #336699;\n    color:#336699;\n    display:inline-block;\n    margin-right:10px;\n    padding:2px 5px; \n}\n\n.message .subject .reddit .marker-dot {\n    border-radius: 50%; \n    width: 12px; \n    height: 12px; \n    float: left; \n    margin-top: 2px; \n    margin-right: 5px;\n}\n\n.message .subject .title { \n    font-weight: normal;\n    font-style: italic;\n    margin-left: 10px; \n}\n.message .parent-link { \n    margin-left: 12px; \n    padding: 0 2px;\n    font-weight: bold; \n}\n\n.message.was-comment .midcol { margin-left: 0px;  }\n\n.message.was-comment .buttons,\n.message.was-comment .parent-link { \n    margin-left: 0px; }\n\n.message.was-comment .md { \n    margin-left: 2px; \n }\n\n\n.message .subject { font-weight: bold; font-size: larger; }\n\n.message.gold {\n    font-family: \"Bitstream Charter\", \"Hoefler Text\", \"Palatino Linotype\",\n                 \"Book Antiqua\", Palatino, georgia, garamond, FreeSerif, serif;\n    background: url(../gold/tikkit-bg.png);\n    max-width: 80em;\n    text-align: center;\n    padding: 20px;\n    border-radius: 4px;\n    border: 1px solid #555;\n\n    .insignia {\n        float: left;\n        margin: 6em 20px 0 20px;\n    }\n\n    .subject {\n        font-size: 2.6em;\n        line-height: 1.5em;\n        text-shadow: -1px -1px 0px rgba(255, 255, 255, 0.8);\n    }\n\n    .tagline, .correspondent, .expand-btn, .unread-button, .block-button, .report-button, ul.buttons li.first {\n        display: none;\n    }\n\n    .entry {\n        margin: 0;\n        border: 0;\n    }\n\n    .md {\n        margin: 0;\n        margin-bottom: 10px;\n        padding: 15px;\n        max-width: 100%;\n        text-shadow: 0 0 2px #fff;\n        border: 0 dashed #000;\n        border-width: 1px 0;\n    }\n\n    .md blockquote {\n        border: 0;\n        font-size: 0.7em;\n        font-style: italic;\n    }\n\n    .md p {\n        font-size: 1.2em;\n        line-height: 1.4em;\n    }\n\n    .usertext-edit {\n        margin: 0 auto;\n    }\n\n    .usertext-buttons {\n        text-align: left;\n    }\n\n    ul.buttons li a {\n        font-size: 2em;\n        text-shadow: 0 0 3px #fff;\n        color: #7a5d0e;\n    }\n\n    ul.buttons, ul.buttons li {\n        margin: 0;\n        padding: 0;\n    }\n\n    &.new > .entry {\n        background-color: transparent;\n        border: 0;\n        padding: 0;\n    }\n}\n\n.message.gold-auto {\n    blockquote {\n        background-color: #fafafa;\n        border: 0;\n        padding: 4px;\n        margin-left: 0;\n        margin-top: 1em;\n        font-style: italic;\n        font-size: 0.8em;\n        color: #808080;\n\n        p { margin: 2px; }\n        strong { font-style: inherit; }\n    }\n}\n\n.clippy img {\n    float: left;\n}\n\n.clippy-bubble {\n    background-color:#fffdd7;\n    border: solid black 1px;\n    width: 350px;\n    border-radius: 5px;\n    margin-left: 5px;\n    margin-bottom: 15px;\n    padding: 7px;\n    float: left;\n}\n\n.clippy-headline {\n    font-weight:bold;\n    margin-bottom: 0.5em;\n}\n\n.clippy-bubble ul {\n    list-style-type: disc;\n    list-style-image: url(../clippy-bullet.png);\n    padding-left: 15px;\n}\n\n.clippy-bubble li {\n    margin-top: 0.5em;\n}\n\n.subreddit { margin-bottom: 10px; }\n.subreddit p { margin-top: 0px; margin-bottom: 1px; }\n.subreddit .description {font-size: small; max-width: 60em;}\n.subreddit .key {display: block;}\n.subreddit .title { font-size: medium; margin-right: 5px; }\n.subreddit .midcol { margin-right: 5px; margin-top: 5px; text-align: right; width: 12em !important; }\n\n.fancy-toggle-button {\n    display: block;\n    margin-bottom: 5px;\n}\n.fancy-toggle-button .active {\n    border: 1px solid #444;\n    padding: 1px 6px;\n    background: white none repeat-x scroll center left;\n\n    color: white;\n    font-size: 10px;\n    font-weight: bold;\n\n    line-height: 20px;\n    border-radius: 3px;\n}\n\n.fancy-toggle-button .remove { \n    background-image: url(../bg-button-remove.png); /* SPRITE stretch-x */\n}\n.fancy-toggle-button .add { \n    background-image: url(../bg-button-add.png); /* SPRITE stretch-x */\n}\n.fancy-toggle-button .banned {\n    background-color: #666;\n    padding: 1px 1.9em;\n}\n\n\n\n.commentbody.border { background-color: #ffc; padding-left: 5px}\n.commentbody.grayed {\n    color: gray;\n    background-color: #E0E0E0;\n    padding-left: 5px;\n}\n\n.fixedwidth { float: left; width: 100px; height: 0px; }\n.clearleft { clear: left; height: 0px; }\n.clear { clear: both; }\n\n.sharetable.preftable {margin-left: 20px; }\n.sharetable.preftable th { padding-bottom: 5px; padding-top: 5px;  }\n.sharetable.preftable button { margin-top: 10px }\n\n.preftable.widget-preview { font-size:smaller; }\n.preftable.widget-preview input[type=\"text\"] { width: 150px; }\n.preftable #css-options input[type=\"text\"] { margin-left: 0px; width: 6em; }\n\n.share-summary { width: 95%; margin-top: 10px; }\n.share-summary .head td { width: 50%; font-size: large; text-align: center }\n.share-summary td { vertical-align: top;}\n.share-summary > tbody > tr > td  {\n    padding-left: 10px; \n    padding-bottom: 10px; \n}\n.share-summary th { padding: 5px; border-bottom: 1px solid #000; }\n\n.sponsored .entry  { margin-right: 20px;}\n\n.sponsored .titlerow { background: #fcfcfc;\n    padding: 10px; \n    border-top: #BCBCBC solid 1px;\n    border-left: #BCBCBC solid 1px;\n    border-bottom: #E0E0E0 solid 1px;\n    border-right: #E0E0E0 solid 1px;\n}\n\n/* footer */\n.footer-parent {\n    font-size: larger;\n    padding-top: 40px; \n    clear: both;\n    text-align: center;\n}\n\n.footer {\n    color: gray;\n    padding: 5px;\n    margin: 15px auto;\n    border:1px solid #F0F0F0;\n    display: flex;\n    display: -webkit-flex;\n    max-width: 600px;\n}\n\n.footer .col {\n    display: inline-block;\n    vertical-align: top;\n    -webkit-flex: 0 0 25%;\n    flex: 0 0 25%;\n    margin: 10px 0;\n    padding: 0 15px;\n    border-left: 1px solid #E0E0E0;\n    box-sizing: border-box;\n }\n\n.footer .col:first-child {border: none;}\n\n.notes-button {\n    margin-top: 3px;\n}\n\n.notes-status {\n    font-size: larger;\n}\n\n.load0 { background-color: #FFFFFF; } /* white */\n.load1 { background-color: #f0f5FF; } /* pale blue */\n.load2 { background-color: #E2ECFF; } /* blue */\n.load3 { background-color: #d6f5cb; } /* pale green */\n.load4 { background-color: #CAFF98; } /* green */\n.load5 { background-color: #e4f484; } /* yellowgreen */\n.load6 { background-color: #FFEA71; } /* orange */\n.load7 { background-color: #ffdb81; } /* orangerose */\n.load8 { background-color: #FF9191; } /* pink */\n.load9 { background-color: #FF0000; color: #FFFFFF } /* red */\n\n/* login form */\n\n.orangered { color: orangered; }\n\n.logout { display: inline; }\n.login-form-side {\n    border: 1px solid gray;\n}\n\n.login-form-side input[type=text],\n.login-form-side input[type=password] {\n    font-family: verdana; /* Override Chrome's defaults. */\n    font-size: 11px;\n    .box-sizing(border-box);\n    border: 1px solid #999;\n    width: 141px;\n    margin: 5px 0px 0px 5px;\n    top: 5px;\n    padding: 6px;\n }\n\n.login-form-side input[type=password] {\n    width: 142px;\n}\n\n.login-form-side #remember-me,\n.login-form-side .submit {\n    margin: 4px;\n}\n\n.login-form-side .submit input[type=button] {\n    margin:1px;\n}\n\n.login-form-side #remember-me {\n    float: left;\n    line-height: 24px;\n    margin-left: 5px;\n}\n\n.login-form-side #remember-me * {\n    vertical-align:middle;\n}\n\n/*the checkbox*/\n#rem-login-main {\n    position: static;\n    height: auto;\n    width: auto;\n    border: none;\n    margin-right: 5px;\n    margin-top: 0;\n}\n\n.login-form-side label {\n    padding: 2px 0 2px 0;\n    margin-right: 5px;\n    white-space: nowrap;\n}\n\n.login-form-side .recover-password {\n    margin-left: 1em;\n}\n\n.login-form-side .status { display:none; }\n\n.login-form-side .submit {\n    float: right;\n}\n\n.login-form-side .submit *, .user-form .submit * {\n    vertical-align: middle;\n}\n\n.throbber {\n    display: none;\n    margin: 0 2px;\n    background: url(../throbber.gif) no-repeat;\n    width: 18px;\n    height: 18px;\n}\n.working .throbber { display: inline-block; }\n\n.working {\n    [type=\"submit\"] {\n        cursor: not-allowed;\n        .opacity(.65);\n        pointer-events: none;\n    }\n}\n\n.sr_style_toggle .throbber {\n   position: absolute;\n   margin-top: -2px;\n   margin-left: 4px;\n}\n\n.status { margin: 5px 0 0 5px; font-size: small;}\n.error { color: red; font-size: small; }\n.red { color:red }\n.buygold { color: #9A7D2E; font-weight: bold; }\n.line-through { text-decoration: line-through }\n\n#noresults { margin-right: 310px;  }\n\n#ad-frame, #ad_main {\n    border: 0px;\n    overflow: hidden;\n    width: 300px;\n    height: 280px;\n}\n\n#ad_sponsorship {\n    border: 0px;\n    overflow: hidden;\n    width: 300px;\n    height: 100px;\n}\n\n/* newsletter standalone page */\n\nbody.newsletter {\n    background: #EEF7FF;\n    font-size: 12px;\n}\n\n.newsletter-box {\n    .box-shadow(0 3px 10px 4px rgba(0,0,0,0.1));\n    margin: 10% auto;\n    background-color: white;\n    width: 90%;\n    max-width: 600px;\n    border-radius: 4px;\n    padding: 40px;\n\n    h1 {\n        margin: 0;\n        min-height: 50px;\n        font-size: 15px;\n    }\n\n    .upvoted-weekly-logo {\n        display: block;\n        margin-top: 15px;\n        min-height: 53px;\n        background: transparent url(../upvoted-weekly-logo.svg) 0 0 no-repeat;\n        background-size: contain;\n    }\n\n    .subscribe-thanks {\n      display: none;\n    }\n\n    &.success {\n        &:before {\n            content: \"✓\";\n            display: block;\n            text-align: center;\n            color: #80d654;\n            font-weight: bold;\n            font-size: 60px;\n            line-height: 1;\n        }\n\n        .result-message {\n            display: block;\n            margin: 0 auto;\n            text-align: center;\n        }\n\n        .subscribe-callout {\n          display: none;\n        }\n\n        .subscribe-thanks {\n          display: block;\n          text-align: center;\n          margin-top: 25px;\n        }\n\n        form {\n            display: none;\n        }\n    }\n\n    .result-message {\n        margin-top: 21px;\n        line-height: 1.5;\n        font-size: 14px;\n        max-width: 400px;\n        color: @color-text-grey;\n        font-weight: normal;\n    }\n\n    form {\n        margin-top: 40px;\n        text-align: right;\n    }\n\n    .c-form-group {\n        width: 50%;\n    }\n\n    button {\n        .button-size(@padding-base-vertical; @padding-base-horizontal; 12px; 20px; 3px);\n        margin-left: 10px;\n    }\n\n    .faq-toggle {\n        position: absolute;\n        margin-top: -13px;\n        min-width: 100px;\n        font-size: 11px;\n        font-weight: bold;\n        color: #79a6d2;\n\n        &:after {\n            content: \"▾\";\n            display: inline-block;\n            height: 15px;\n            width: 15px;\n            text-align: center;\n            position: absolute;\n        }\n\n        &.active:after {\n            .transform(rotate(180deg));\n        }\n    }\n\n    .faq {\n        display: none;\n\n        h3 {\n            margin-top: 1.5em;\n        }\n    }\n}\n\n.upvoted-gradient {\n    position: fixed;\n    bottom: 0;\n    width: 100%;\n    height: 25%;\n    background: transparent url(../upvoted-arrow-bg.png);\n    z-index: -1;\n\n    &:after {\n        content: \"\";\n        position: absolute;\n        width: 100%;\n        height: 100%;\n        top: 0;\n        left: 0;\n        .linear-gradient(#EEF7FF, rgba(255,255,255,0));\n    }\n}\n\n@media screen and (max-width: @screen-md-min) {\n    .newsletter-box {\n        position: static;\n        .transform(none);\n        margin: 10px auto;\n        padding: 15px;\n        max-width: 85%;\n\n        h1, p {\n            font-size: 13px;\n        }\n\n        .faq-toggle {\n            position: static;\n            display: block;\n            margin-top: 20px;\n            font-size: 13px;\n        }\n    }\n\n    .upvoted-gradient {\n        display: none;\n    }\n}\n\n/* search */\n\n#searchmenu { margin: 10px 0 0px 0; padding: 2px 0 0 0; \n    border-bottom: 2px solid #369; \n    background-color: whitesmoke}\n\n#searchmenu .searchlabel { background-color: white; \n    padding: 2px 15px 0px 0px; \n    font-weight: bold; color: #369 }\n\n#searchmenu .searchtime {\n    font-weight: bold; \n    display: inline; \n    width: 305px;\n}\n\n#searchexpando {\n    display: none;\n    margin: 5px 0 0 0;\n    padding-top: 10px;\n    border-radius: 3px;\n}\n\n#searchexpando input, #searchexpando p {\n    margin-bottom: 10px;\n}\n\n#searchexpando dl {\n    margin: 10px 0;\n}\n\n#searchexpando dt {\n    margin: 0;\n}\n\n#previoussearch p {\n    margin: 5px 0;\n}\n\n#previoussearch label {\n    display: block;\n    margin: 5px 0;\n}\n\n#moresearchinfo {\n    display: none;\n    padding-top: 5px;\n    max-width: 300px;\n    border: 0 solid orange;\n    margin-top: -5px;\n}\n\nlabel + #moresearchinfo {\n    border-width: 1px 0 0 0;\n    margin-top: 0px;\n}\n\n#previoussearch #moresearchinfo {\n    border-color: gray;\n    margin: 5px 0;\n}\n\n#search_hidemore {\n    float: right;\n    margin-left: 5px;\n}\n\n.searchparams { margin: 5px 20px 5px 20px\n}\n.searchparams .labels {text-align: right;\n    margin-left: 10px; }\n\n\n.searchpane {\n    margin: 5px 305px 5px 0px;\n    padding-left: 96px;\n    background: #E0E0E0 url(../search-large.png) 26px center no-repeat;\n} \n\n.search-summary {\n    float: right;\n    text-align: right;\n    margin: 6px 8px 0 0;\n}\n\n.search-summary .result-count {\n    font-weight: bold;\n}\n\n.searchfail {\n    color: #c00000;\n    font-size: larger;\n    line-height: 2em;\n}\n\n.searchfail a {\n    color: red;\n    text-decoration: underline;\n}\n\n#search {\n    /* Allow the search icon to be on the same line as the search box. */\n    white-space: nowrap;\n}\n\n#searchexpando, #moresearchinfo {\n    white-space: normal;\n}\n\n#search input[type=text] {\n    border: 1px solid gray;\n    font-size: 13px;\n    font-family: verdana; /* Override Chrome's defaults. */\n    width: 300px;\n    .box-sizing(border-box);\n    padding: 6px;\n    padding-right: 25px; /* 13px image + 6px right margin + 6px left margin */\n    padding-left: 9px;\n    vertical-align: middle;\n}\n\n#search input[type=submit] {\n    background-color: transparent;\n    background-image: url(../search.png); /* SPRITE */\n    background-repeat: no-repeat;\n    height: 13px;\n    width: 13px;\n    .box-sizing(border-box);\n    border: none;\n    margin: 0;\n    margin-left: -22px; /* 13px image + 6px margin + 3px visual adjustment */\n    vertical-align: middle;\n}\n\n#search input[type=submit]:hover {\n    background-image: url(../search-mouseover.png); /* SPRITE */\n}\n\n@media\n(-webkit-min-device-pixel-ratio: 2),\n(min-resolution: 192dpi) {\n    #search input[type=submit] {\n        background-image: data-uri('../search-x2.png');\n        background-size: 13px 13px;\n        background-position: 0 0;\n    }\n\n    #search input[type=submit]:hover {\n        background-image: data-uri('../search-mouseover-x2.png');\n        background-position: 0 0;\n    }\n}\n\n/* login, register */\n\n\n\n.legal {color: #808080; font-family: serif; font-size: small; margin-top: 20px; }\n.legal a {text-decoration: underline}\n\n.divide { border-right: 2px solid #D3D3D3; margin-right: -2px; }\n\n.login-form-section {\n    position: relative;\n    float: left;\n    overflow: hidden;\n    padding-left: 2%;\n    padding-right: 2%;\n\n    &.register {\n        width: 56%;\n    }\n\n    &.login {\n        width: 36%;\n    }\n}\n\n.login-form-section > h3 {\n    margin-bottom: 0;\n    margin-top: 10px;\n    font-size: large;\n    font-weight: bold; \n    font-variant: small-caps;\n    color: #404040;\n}\n.login-form-section p {\n    text-align: left;\n    margin-bottom: 10px; \n    color: #606060;\n    margin-bottom: 20px; \n}\n\n.login-form-section.register .registration-info {\n    position: absolute;\n    left: 53%;\n    width: 40%;\n    min-width: 20em;\n    margin-top: 1.25em;\n    color: #777;\n\n    .md {\n        font-size: 1.1em;\n\n        li {\n            list-style-type: disc;\n            margin-bottom: .5em;\n        }\n    }\n}\n\n.user-form label {\n    display: block; \n    font-weight: bold;\n    color: #606060; \n}\n\n.user-form label.note {\n    font-weight: normal;\n}\n\n.user-form .error {\n    display: inline-block;\n    margin-top: 2px;\n    line-height: 16px;\n    color: inherit;\n    font-size: inherit;\n}\n\n/* Form-level errors. */\n.user-form .error.field-ratelimit,\n.user-form .error.field-vdelay {\n    display: block;\n}\n\n.user-form .remember {\n    display: inline;\n    margin-left: 2px;\n    text-transform: lowercase;\n}\n\n.user-form input[type=checkbox] {\n    vertical-align: bottom;\n}\n\n.user-form ul { margin: 7px; }\n.user-form li { margin-top: 5px; }\n.user-form p .btn { margin-top: 5px }\n.user-form input.logtxt { width: 125px; }\n\n.user-form input[type=text],\n.user-form input[type=password],\n.user-form input[type=email] {\n    width: 125px;\n    border: 1px solid #A0A0A0;\n    margin-top: 2px; \n    margin-bottom: 2px; \n    margin-right: 10px;\n    padding: 1px;\n}\n\n.user-form #captcha {\n    width: 250px;\n }\n\n.user-form .submit {\n    margin-top: 10px;\n}\n\n#passform h1 {margin-bottom: 0px}\n#passform p {margin-bottom: 5px; font-size: small}\n\n.register-form .name-entry * {\n    vertical-align: middle;\n}\n\n.notice-taken, .notice-available {\n    display: none;\n    line-height: 16px;\n}\n\n.register-form.name-taken .notice-taken, .register-form.name-available .notice-available {\n    display: inline-block;\n    margin-top: 2px;\n}\n\n.register-form .name-entry .throbber {\n    display: none;\n    margin-left: 5px;\n}\n\n.register-form.name-checking {\n    .name-entry .throbber {\n        display: inline-block;\n        margin-left: -1px;\n        margin-top: 2px;\n    }\n}\n\n.login-page {\n    #login {\n        margin-right: 300px;\n    }\n\n    @media (max-width: @screen-sm-min) {\n        #login {\n            margin-right: 0;\n        }\n\n        .side {\n            display: none;\n        }\n    }\n}\n\n#cover-msg {\n    line-height: normal;\n    margin: 0 0 50px;\n}\n\n#login {\n    .modal-title {\n        margin: 0 0 25px;\n    }\n\n    .c-alert {\n        display: none;\n        font-size: 11px;\n    }\n\n    @media (max-width: @screen-xs-min) {\n\n        .c-btn {\n            display: block;\n            width: 100%;\n        }\n\n    }\n}\n\n.login-disclaimer {\n    color: #6a6a6a;\n}\n\n.split-panel {\n    .clearfix();\n    margin-bottom: 49px;\n\n    .split-panel-section-responsive(@panel-break-point: @screen-sm-min;) {\n        float: none;\n        width: 100%;\n\n        &:first-child {\n            padding-right: 0;\n        }\n\n        &:last-child {\n            padding-left: 0;\n        }\n\n        &.split-panel-divider {\n            &:first-child {\n                border: 0;\n                border-bottom: 1px solid #e0e0e0;\n                padding-bottom: 60px;\n                margin-bottom: 60px;\n            }\n\n            &:last-child {\n                border: 0;\n                border-top: 1px solid #e0e0e0;\n                padding-top: 60px;\n                margin-top: 60px;\n            }\n        }\n\n        @media (min-width: @panel-break-point) {\n            float: left;\n            width: 50%;\n\n            &:first-child {\n                padding-right: 60px;\n            }\n\n            &:last-child {\n                padding-left: 60px;\n            }\n\n            &.split-panel-divider {\n                &:first-child {\n                    border: 0;\n                    border-right: 1px solid #e0e0e0;\n                    margin-bottom: 0;\n                    padding-bottom: 0;\n                }\n\n                &:last-child {\n                    border: 0;\n                    border-left: 1px solid #e0e0e0;\n                    margin-top: 0;\n                    padding-top: 0;\n                }\n            }\n        }\n    }\n\n    .split-panel-section {\n        .box-sizing(border-box);\n        .split-panel-section-responsive();\n\n        .login-page & {\n            .split-panel-section-responsive(@screen-md-min);\n        }\n    }\n}\n\n.content > #login {\n    > .split-panel {\n        padding-left: 60px;\n        padding-right: 60px;\n        padding-top: 60px;\n    }\n\n    > p {\n        margin-left: 60px;\n        margin-right: 60px;\n    }\n}\n\n.popup h1 {\n    font-size: large;\n    font-weight: normal;\n    margin-left: 1em;\n}\n\n.popup h2 {\n    text-align: center;\n    font-size: small;\n    margin-top: 0px;\n    color: black;\n    font-weight: normal;\n}\n\n.usertable { margin-left: 10px;} \n.usertable { font-size: larger }\n.usertable td, .usertable th { padding: 0 .7em }\n.usertable { white-space: nowrap }\n\n.usertable > .toggle {\n    display: inline-block;\n    margin: 1em 0 .5em;\n    padding: 11px 15px;\n    border: 1px solid #bbb;\n    border-radius: 2px;\n    background: #fdffe8;\n}\n\n.usertable > .toggle .option.main:before {\n    margin-right: 7px;\n}\n\n.usertable > .toggle .option {\n    display: inline;\n}\n\n.usertable > .toggle .togglebutton, .usertable > .toggle .error {\n    display: none;\n    font-size: inherit;\n    border-left: 1px solid #bbb;\n    padding: 4px 15px;\n    padding-right: 0;\n    margin-left: 10px;\n}\n\n.usertable > .toggle .active .togglebutton {\n    display: inline;\n}\n\n.usertable > .toggle .error.active {\n    display: inline;\n}\n\n.usertable tr:hover {\n    background-color: #e5efff;\n}\n\n.usertable tr.banned-user,\n.usertable tr.banned-user a,\n.usertable tr.banned-user .user {\n    color: red;\n}\n\n.aboutpage {  margin-right: 320px; }\n.aboutpage p { margin: 5px; }\n.aboutpage h1, .aboutpage h2 { margin: 10px;}\n\n.aboutpage .usertable { width: 45%; }\n\n.little a { font-size: x-small;  }\n\n.oldbylink a {  background-color: #F0F0F0; margin: 2px; color: gray}\n\n.error-log {\n    clear: both;\n}\n\n.error-log a:hover { text-decoration: underline }\n\n.error-log .rest {\n    display: none;\n}\n\n.error-log:first-child .rest {\n    display: block;\n}\n\n.error-log, .error-log .exception {\n    border: solid #aaa 1px;\n    padding: 3px 5px;\n    margin-bottom: 10px;\n}\n\n.error-log .exception {\n    background-color: #f0f0f8;\n}\n\n.error-log .exception.new {\n    border: dashed #ff6600 2px;\n}\n\n.error-log .exception.severe {\n    border: solid #ff0000 2px;\n    background-color: #ffdfdf;\n}\n\n.error-log .exception.interesting {\n    border: dotted black 2px;\n    background-color: #e0e0e8;\n}\n\n.error-log .exception.fixed {\n    border: solid #008800 1px;\n    background-color: #e8f6e8;\n}\n\n.error-log .exception span {\n    font-weight: bold;\n    margin-right: 5px;\n}\n\n.error-log .exception span.normal {\n    margin-right: 0;\n    display: none;\n}\n\n.error-log .exception span.new, .error-log .edit-area label.new {\n    color: #ff6600;\n}\n\n.error-log .exception span.severe, .error-log .edit-area label.severe {\n    color: #ff0000;\n}\n\n.error-log .exception span.interesting, .error-log .edit-area label.interesting {\n    font-weight: normal;\n    font-style: italic;\n}\n\n.error-log .exception span.fixed, .error-log .edit-area label.fixed {\n    color: #008800;\n}\n\n.error-log .exception-name {\n    margin-right: 5px;\n    display: inline-block;\n    max-height: 50px;\n    overflow: hidden;\n}\n\n.error-log .nickname {\n    color: black;\n    font-weight: bold;\n    font-size: larger;\n}\n\n.error-log .exception.fixed .nickname {\n    text-decoration: line-through;\n}\n\n.error-log a:focus {\n    -moz-outline-style: none;\n}\n\n.error-log .edit-area {\n    border: solid black 1px;\n    background-color: #eee;\n}\n\n.error-log .edit-area label {\n    margin-right: 25px;\n}\n\n.error-log .edit-area input[type=radio] {\n    margin-right: 4px;\n}\n\n.error-log .edit-area input[type=text] {\n    width: 800px;\n}\n\n.error-log .edit-area table td, .error-log .edit-area table th {\n    padding: 5px 0 0 5px;\n}\n\n.error-log .save-button {\n    margin: 0 5px 5px 0;\n    font-size: small;\n    padding: 0;\n}\n\n.error-log .date {\n    font-size: 150%;\n    font-weight: bold;\n}\n\n.error-log .hexkey {\n    color: #997700;\n}\n\n.error-log .exception-name {\n    font-size: larger;\n    color: #000077;\n}\n\n.error-log .frequency {\n    font-size: larger;\n    float: right;\n    color: #886666;\n}\n\n.error-log .occurrences {\n    border: solid #003300 1px;\n    margin: 5px 0 2px;\n    padding: 2px;\n}\n\n.error-log .occurrence {\n    color: #003300;\n    font-family: monospace;\n    margin-right: 3em;\n    white-space: nowrap;\n}\n\n.error-log table.stacktrace th, .error-log table.stacktrace td {\n    border: solid 1px #aaa;\n}\n\n.error-log table.stacktrace td {\n    font-family: monospace;\n}\n\n.error-log table.stacktrace td.col-1 {\n    text-align: right;\n    padding-right: 10px;\n}\n\n.error-log .logtext.error {\n    color: black;\n    margin: 0 0 10px 0;\n}\n\n.error-log .logtext {\n    margin-bottom: 10px;\n    border: solid #555 2px;\n    background-color: #eeece6;\n    padding: 5px;\n    font-size: small;\n}\n\n.error-log .logtext * {\n    color: black;\n}\n\n.error-log .logtext.error .loglevel {\n    color: white;\n    background-color: red;\n}\n\n.error-log .logtext.warning .loglevel {\n    background-color: #ff6600;\n}\n\n.error-log .logtext.info .loglevel {\n    background-color: #00bbff;\n}\n\n.error-log .logtext.debug .loglevel {\n    background-color: #00ee00;\n}\n\n.error-log .logtext .loglevel {\n    padding: 0 5px;\n    margin-right: 5px;\n    border: solid black 1px;\n}\n.error-log .logtext table {\n    margin: 8px 5px 2px 0;\n    font-family: monospace;\n}\n\n.error-log .logtext table,\n.error-log .logtext table th,\n.error-log .logtext table td {\n  border: solid #aaa 1px;\n}\n.error-log .logtext table th, .error-log .logtext table td {\n    border: solid #aaa 1px;\n}\n\n.error-log .logtext table .occ {\n    text-align: right;\n}\n\n.error-log .logtext table .dotdotdot {\n    padding: 0;\n}\n.error-log .logtext table .dotdotdot a {\n    margin: 0;\n    display: block;\n    width: 100%;\n    height: 100%;\n    background-color: #e0e0e0;\n}\n.error-log .logtext table .dotdotdot a:hover {\n    background-color: #bbb;\n    text-decoration: none;\n}\n\n.error-log .logtext .classification {\n    font-size: larger;\n    font-weight: bold;\n}\n.error-log .logtext .actual-text {\n    max-width: 600px;\n    overflow: hidden;\n}\n.error-log .logtext .occ {\n}\n\n.details {\n    font-size: x-small; \n    margin-bottom: 10px; \n}\n\n.details span { margin: 0 5px 0 5px; }\n.details th {\n    text-align: right; \n    padding-right: 5px;\n    font-weight: bold;\n}\n.details td {\n    vertical-align: top;\n}\n\n.ring {\n    font-weight:bold;\n    background-color:red;\n    color:white;\n    text-align:center;\n    padding-left: 3px;\n    padding-right: 4px !important;\n    cursor: pointer;\n}\n\n.vote-note {\n    padding-left: 3px;\n    max-width: 150px;\n}\n.vote-a-notes {\n    color: red;\n}\n.vote-up {\n    color: orangered;\n}\n.vote-down {\n    color: #336699;\n}\n.vote-invalid {\n    color: #888888 !important;\n    font-style: italic;\n}\n\n.unvotable-message {\n    border: solid 1px #ff6600;\n    margin-top: 4px;\n    padding: 1px 3px;\n\n    border-radius: 3px;\n\n    display: none;\n}\n\n.bottommenu { color: gray; font-size: smaller; clear: both}\n.bottommenu a { color: gray; text-decoration: underline;   }\n.bottommenu .updated {\n    color: green;\n}\n\n.debuginfo {\n    text-align: right;\n    padding: 5px;\n    color: gray;\n    font-size: smaller;\n    clear: both;\n}\n.debuginfo .icon { color:#a0a0a0; font:1.5em serif; padding:0 2px; }\n.debuginfo .content { display:none; }\n.debuginfo:hover .content { display:inline; }\n\n\n/* Buttons specific */\n\n\n.button {\n    border-collapse: collapse;\n    color: gray;\n    text-align: center;\n    margin: 1px;\n    color: #369;\n}\n\nbutton.button[disabled] {\n    color: gray;\n}\n\n.button #cover {\n    position: relative; \n }\n\n.button .cover {  \n    background: white; \n}\n\n.button #popup {\n    position: absolute; \n    width: 80%; \n    z-index: 1001; \n    background: white; \n    padding: 1px; \n    left: 0px; \n    top: 0px; \n    margin: 0px; \n    border-color: #B2B2B2 black black #B2B2B2;\n    border-style: solid;\n    border-width: 1px;\n}\n\n\n.button .arrow { width: 15px; }\n\n.num { font-weight: bold; font-size: larger }\n\n.button.thing { \n    margin:0px;\n    padding:0px;\n}\n\n.button-body { \n    background-color: transparent;\n}\n.button .blog {\n    border: 1px solid #c7def7;\n    color: gray;\n    text-align: center;\n    margin: 0px;\n    border-radius: 4px;\n    background-color:white;\n}\n\n.button .blog .r { color: gray;  }\n.button .blog .score { white-space: nowrap; }\n\n.button a:hover { text-decoration: underline }\n\n.button .blog1 { font-size: x-small; }\n.button .blog1 .arrow { float:left; margin-left: 2px;  margin-right: 2px;  }\n\n.button .blog1 .headimgcell {\n    background-color: #c7def7;\n    width: 18px;\n    float: left; \n}\n.button .blog1 .headimgcell a {\n    display: inline-block;\n}\n.button .blog1 .score {\n    float: center;\n    margin-top: 2px;\n    margin-right: 5px; \n}\n\n.button .blog2 { font-size: small; }\n.button .blog2 .arrow { width: 15px; margin-left: auto; margin-right: auto;  }\n.button .blog2 .bottomreddit { color: black; background-color: #c7def7; font-size: small; }\n.button .blog2 .score .submit { \n    display: block;\n    font-size: x-small; \n    line-height: 17px;\n}\n\n.button .blog.blog3 { \n    font-size: small; \n    border: none; \n    background-color: transparent;\n}\n.button .blog3 .left { float: left; width: 50%; }\n.button .blog2 .arrow { width: 15px; margin-left: auto; margin-right: auto;  }\n.button .blog3 .right { float: right; margin-top: 5px; }\n.button .blog3 .score .submit { \n    display: block;\n    font-size: x-small; \n    line-height: 17px;\n}\n.button .blog3 .snoo {\n    margin-top: -1px;\n}\n\n\n.blog5 .right { float: right; }\n.blog5 .left  { float: left; display:block; margin-top: 10px; }\n.blog5 .clearleft { clear: left; }\n.button .blog.blog5 { border: none; text-align: left; font-size: small; }\n.blog5 a.bling { float:left; }\n.blog5 .container { margin-left: 35px; margin-top: 2px; height: 50px;}\n.blog5 ul { display: inline; }\n.blog5 ul a { color: #515481; font-weight: bold; text-decoration: underline;  }\n.blog5 li { display: inline; padding: 1px 10px 1px 10px; }\n.blog5 li.selected {  \n    background-color: #F8F8F1; \n    color: #000; \n    border-color: #CCC; \n    border-style: solid solid none solid;\n    border-width: 1px;\n\n}\n.blog5 .votes {\n    height: 25px; \n    background-color: #F8F8F1; \n    border: 1px solid #CCC;\n    padding-top: 5px; \n}\n.blog5 .arrow {\n    margin-right: 15px; \n    margin-left: 5px; \n    color: black; \n    cursor: pointer;\n    display: inline; \n    background-position: left center;\n    background-repeat: no-repeat; \n    padding-left: 20px; \n}\n.blog5 .votes.disabled .arrow { color: #888; }\n.blog5 .arrow:hover { text-decoration: none; }\n.blog5 .arrow b { font-size: larger; }\n.blog5 .arrow.upmod b   { color: #FF8B60; }\n.blog5 .arrow.downmod b { color: #9494FF; }\n\n.blog5 .right { margin-right: 5px; font-size: medium; font-style: italic;  }\n\n.optional {color: green}\n.instructions { font-size: larger;  }\n.instructions h1, .instructions h2, .instructions h3 { margin-top: 20px; margin-bottom: 20px;  }\n.instructions p { margin: 10px; max-width: 60em}\n.instructions pre { margin: 5px;  margin-right: 10px; }\n.instructions iframe { margin: 5px 10px 5px 0px; }\n.instructions input, .instructions select { margin: 0 .5em }\n.instructions a:focus { -moz-outline-style: none; }\n.instructions strong { font-weight: bold; }\n.instructions .buttons { margin-left: 1em; max-width: 50em; }\n.instructions .buttons li { margin-top: 1em; \n                            border-bottom: 1px solid #e0e0e0;  \n                            padding-bottom: 1em;}      \n.instructions code {\n    display: block; \n    font-family: monospace; \n    font-size: small;\n    margin: 5px; \n    background-color: #FF9; \n    padding: 10px; \n    max-width: 50em;}\n\n.self-service.instructions { \n    margin-bottom: 50px; \n }\n\n.self-service.instructions p { \n    margin: 10px 0;\n}\n\n.self-service.instructions ul { \n    list-style-type: circle; \n    margin-left: 60px; \n}\n\n.self-service.instructions li + li { \n    padding-top: 10px; \n}\n\n.self-service {\n    .ad-launch-buttons {\n        text-align: center; \n\n        .button {\n            font-size: 22px;\n            padding: 10px 20px;\n            margin-bottom: 5px;\n        }\n    }\n\n    .col-bottom-box {\n        margin-right: 20px;\n    }\n}\n\nbody.contact-us-page {\n    overflow-y: scroll;\n}\n\n.contact-us-page .content {\n    width: 600px;\n    margin: 0px auto;\n}\n\n.contact-us-page h1 {\n    font-size: xx-large;\n    text-align: center;\n    margin: 20px 0px;\n}\n\n.contact-us-page .info {\n    font-size: larger;\n    text-align: center;\n    margin-bottom: 20px;\n}\n\n.contact-us-page h2.button {\n    background-color: #cee2f5;\n    font-size: x-large;\n    font-weight: bold;\n    color: #369;\n    text-align: center;\n    border-radius: 7px;\n    border: 2px solid #369;\n    line-height: 1.5em;\n    margin: 0px 10px 10px 10px;\n}\n\n.contact-us-page h2.button:hover {\n    background-color: #daeaf8; \n    cursor: pointer;   \n}\n\n.contact-us-page .details{\n    margin: 0;\n    display: none;\n}\n\n.contact-us-page li:target .details {\n    display: block;\n}\n\n.contact-us-page .details li {\n    background-color: #fafafa;\n    font-size: small;\n    border: 1px solid #ccc;\n    margin: 0px 40px 10px 40px;\n    padding: 10px;\n    width: 500px;\n}\n\n.contact-us-page img.space-snoo {\n    display: block;\n    margin: 50px auto 0 auto;\n}\n\n.button-demo a.view-code,\n.button-demo a.hide-code { float: right; margin-bottom: 1em; }\n.button-demo a.hide-code { display: none; }\n.instructions .button-demo code { display: none; }\n\n.button-demo.show-demo a.view-code { display: none; }\n.button-demo.show-demo a.hide-code { display: inline; }\n.button-demo.show-demo code { display: block; }\n\n#preview { float: right; width: 30em; margin: 10px; }\n#preview span { color: lightgray;  }\n#preview #previewbox {\n  border-width: .2em;\n  border-style: dashed;\n  border-color: lightgray;\n  padding: 1em;\n  font-size: larger;\n}\n\n.bookmarklet {\n    border: solid #888888 1px;\n    padding: 0px 2px;\n}\n\n/* default form styles */\n\nform .blurb {\n    margin-bottom: 5px;\n}\n\nform .spacer + .spacer {\n    margin: 15px 0;\n}\n\nform input[type=checkbox],\nform input[type=radio] {margin: 2px .5em 0 0; }\n\n.pretty-form {\n    font-size: larger;\n    vertical-align: top;\n}\n\n.pretty-form p {margin: 3px ;}\n.pretty-form input[type=checkbox],\n.pretty-form input[type=radio] {margin: 2px .5em 0 0; }\n.pretty-form img { margin: 3px .5em}\n.pretty-form input[type=text],\n.pretty-form textarea,\n.pretty-form input[type=password],\n.pretty-form input[type=number]{\n    border: 1px solid gray;\n    width: 300px; \n    padding: 2px;\n    \n    -webkit-box-shadow: inset 0px 1px 1px hsla(0,0%,0%,.3), 0px 1px 0px hsla(0,0%,100%,.6);\n    -moz-box-shadow: inset 0px 1px 1px hsla(0,0%,0%,.3), 0px 1px 0px hsla(0,0%,100%,.6);\n    box-shadow: inset 0px 1px 1px hsla(0,0%,0%,.3), 0px 1px 0px hsla(0,0%,100%,.6);\n}\n\n.pretty-form.short-text input[type=text].number {\n    margin: 0 0.5em;\n}\n\n.pretty-form.short-text input[type=text].text {\n    margin: 0 0.5em 0 0;\n    width: 10em;\n}\n\n.pretty-form .infobar {\n    width: 285px; \n    margin: 5px;\n }\n\n.pretty-form input[type=text],\n.pretty-form input[type=file],\n.pretty-form input[type=password],\n.pretty-form input[type=number],\n.pretty-form select,\n.pretty-form b,\n.pretty-form textarea,\n.pretty-form button  { margin: 3px 5px; }\n.pretty-form th { text-align: right }\n.pretty-form input[type=number]  {width: 75px; }\n\n\n\n/* delete page */\n.white-field, .delete-field {\n    background-color: white;\n    padding: 10px;\n}\n\n.delete-field td {\n    vertical-align: top;\n}\n\n.pretty-form .delete-field {\n    background: transparent;\n}\n\n.pretty-form .delete-field td label + label {\n    margin-left: 2em;\n}\n\n#pref-deactivate textarea#deactivate-message {\n    font-size: smaller;\n    height: 5em;\n}\n\n#pref-deactivate .md ul {\n    margin-top: 0;\n    margin-bottom: 0;\n}\n\n#pref-deactivate .md ul li {\n    margin: .5em 0;\n}\n\n#pref-deactivate .credentials input {\n    margin: .2em 0;\n}\n\n#pref-deactivate .credentials .error,\n#pref-deactivate .error.RATELIMIT {\n    margin-left: 5px;\n}\n\n/*pref page boxes*/\n.pretty-form.short-text input[type=text],\n.pretty-form.short-text textarea,\n.pretty-form.short-text input[type=password] {width: 2em }\n\n/*submit*/\n\n#url-field #suggest-title { text-align: right; }\n#url-field button {margin: 10px 0 0 5px;}\n#url-field .title-status { color: red; font-size: small}\n\n.content.submit .info-notice {\n    background-color: #E4F2FB;\n    border: 1px solid #5F99CF;\n    padding: 9px;\n    margin-bottom: 12px;\n    font-size: larger;\n}\n\n.content.submit .info-notice a {\n    font-weight: bold;\n    text-decoration: underline;\n}\n\n/*opt-out/in form*/\n.opt-form { font-size: larger }\n.opt-form form { display: inline; }\n\n/* pref table - used for preferences and edit subreddit pages */\n.preftable th {\n    padding: 10px; \n    font-weight: bold; \n    vertical-align: top;\n    text-align: right;\n    white-space: nowrap;\n}\n.preftable th label { display: block; }\n.sharetable.preftable th label { display: inline; }\n.preftable th span { display: block; }\n\n.preftable td.prefright {\n    padding: 10px 0;\n\n    h6 {\n        font-weight: normal;\n        font-style: italic;\n        text-transform: capitalize;\n    }\n}\n\n.preferences-media {\n    label {\n        display: inline-block;\n    }\n\n    label:first-letter {\n        text-transform: uppercase;\n    }\n}\n\n.preftable select { margin: 0 .5em 0 .5em; }\n\n.preftable .spacer { margin-bottom: 5px; }\n.preftable .note { width: 100%; vertical-align: top; padding-top: 10px; }\n\n.preftable .details { \n    font-size: smaller ;\n    color: gray;\n    margin: 0;\n\n    &.reddit-gold{\n        color: #9A7D2E;\n    }\n}\n\n/* Stylesheets everywhere prefs */\n.preftable {\n\n    @selected-theme-color: #a8c8ea;\n\n    .reddit-themes-description {\n        max-width: 800px;\n        margin-bottom: 10px;\n    }\n\n    .container.reddit-themes{\n        max-width: 800px;\n        margin: 5px 0;\n        display: flex;\n        display: -webkit-flex;\n        flex-wrap: wrap;\n        -webkit-flex-wrap: wrap;\n        flex-direction: row;\n        -webkit-flex-direction: row;\n        justify-content: flex-start;\n        -webkit-justify-content: flex-start;\n\n        .theme {\n            -webkit-flex: 1 0 250px;\n            flex: 1 0 250px;\n            padding: 7px 0 11px 0;\n            position: relative;\n            max-width: 270px;\n\n            &.selected {\n                background-color: @selected-theme-color;\n                font-weight: normal;\n            }\n\n            &.select-custom-theme {\n                -webkit-flex: 1 0 100%;\n                flex: 1 0 100%;\n                max-width: 100%;\n                width: 100%;\n                margin: 7px;\n                padding: 7px;\n\n                input {\n                    margin-left: 2px;\n                }\n            }\n\n            img {\n                margin: 0;\n            }\n\n            .theme-thumbnail {\n                display: block;\n                margin: 5px auto;\n            }\n\n            .theme-container {\n                max-width: 240px;\n                margin: 0 auto;\n\n                p {\n                    display: inline;\n                }\n            }\n\n            .theme-thumbnail {\n                margin: 5px auto;\n                \n            }\n\n            .theme-thumbnail:hover .theme-preview {\n                visibility: visible;\n                opacity: 1;\n            }\n\n            .theme-preview {\n                position: fixed;\n                top: 50%;\n                left: 50%;\n                .transform(translate(-50%, -50%));\n                visibility: hidden;\n                opacity: 0;\n                z-index: 100;\n                .transition(opacity, 0.2s, ease, 0.3s);\n                .box-shadow(0 0 5px #000);\n            }\n\n            .theme-preview img {\n                margin: 0;\n                display: block;\n            }\n        }\n    }\n}\n\n.over18 button { margin: 0 10px 0 10px; padding: 5px}\n\n@color-stamp-text-dark: @color-semi-black;\n@color-stamp-text-light: @color-white;\n@color-nsfw-stamp: darken(@color-warning-red, 5%);\n@color-quarantine: @color-yellow;\n@color-private-stamp: darken(@color-orange, 10%);\n@color-restricted-stamp: darken(@color-orange, 10%);\n@color-archived-stamp: lighten(@color-text-grey, 15%);\n\n.stamp {\n    border-radius: 3px;\n    border: 1px solid;\n    display: inline-block;\n    font-size: 10px;\n    line-height: 14px;\n    padding: 0 4px;\n}\n\n.nsfw-stamp {\n    color: @color-nsfw-stamp;\n\n    acronym {\n        border: none;\n        text-decoration: none;\n    }\n}\n\n.private-stamp {\n    color: @color-private-stamp;\n}\n\n.restricted-stamp {\n    color: @color-restricted-stamp;\n}\n\n.archived-stamp {\n    color: @color-archived-stamp;\n}\n\n.quarantine-stamp {\n    background-color: @color-quarantine;\n    border-color: @color-quarantine;\n    color: @color-stamp-text-dark;\n}\n\n.quarantine-notice {\n    .box-sizing(border-box);\n    background: @color-quarantine;\n    padding: @margin-small * 1px;\n    padding-top: @margin-x-small * 1px;\n    margin-bottom: @margin-small * 1px;\n    .md p {\n        margin-top: 0;\n    }\n}\n\n.btn-quarantine {\n    font-weight: normal;\n    background-color: lighten(@color-quarantine, 13%);\n    text-transform: none;\n    width: 100%;\n    border: 1px solid darken(@color-quarantine, 15%);\n\n    &:hover {\n        background-color: lighten(@color-quarantine, 17%);\n        color: @color-semi-black;\n    }\n\n    &:active {\n        background-color: darken(@color-quarantine, 10%);\n    }\n\n    &:focus {\n        color: @color-semi-black;\n    }\n}\n\n.entry .buttons li.reported-stamp {\n    border: 1px solid black !important;\n    padding: 0 4px;\n    background-color: #f6e69f;\n}\n\n.suspicious { background-color: #f6e69f }\n.thing.spam { background-color: #FA8072 }\n\n.comment.spam > .child, .message.spam > .child {\n    background-color: white;\n}\n.comment.spam > .child {\n    margin-left: 0;\n    padding-left: 15px;\n}\n.message.spam > .child {\n    /* There's a thin pink \"border\" due to the parent's padding:7px,\n       which we could try to fix here some day. */\n}\n\n.thing.banned-user {\n    overflow: hidden;\n    background-color: rgba(250, 128, 114, 0.5);\n\n    .title {\n        text-decoration: line-through;\n    }\n}\n\n.approval-checkmark {\n    cursor: pointer;\n    height: 0.8em;\n    vertical-align: baseline;\n    margin-left: 3px;\n}\n\n.tagline .approval-checkmark {\n    height: 1em;\n}\n\n.little { font-size: smaller  }\n.gray { color: gray }\n\n/* stats page */\n\n.stats { float: left; margin-right: 2em; border-collapse: collapse; font-size: larger; }\n.stats td.space {width: 20px}\n.stats td.sec { padding-bottom: 7px; font-size : 18px; font-weight: normal }\n.stats a {color: #369}\n.stats a:hover {text-decoration: underline;}\n.stats td.k { color: gray }\n.stats th { text-align: left; background-color: whitesmoke; \n            color: #369; font-weight: bold;}\n.stats td.ri { padding-left: 20px; text-align: right}\n\n.thumbnail {\n    float: left; \n    font-size: 0;\n    margin: 0;\n    margin-right: 5px;\n    margin-bottom: 2px;\n    overflow: hidden;\n    width: 70px; \n}\n\n.thumbnail.nsfw {\n    height: 70px;\n    background-image: url(../nsfw2.png); /* SPRITE */\n}\n\n.thumbnail.self {\n    height: 50px;\n    background-image: url(../self_default2.png); /* SPRITE */\n}\n\n.thumbnail.default {\n    height: 50px;\n    background-image: url(../noimage.png); /* SPRITE */\n}\n\n\n/* CSS customisation page */\n\n.stylesheet-customize-container { }\n.stylesheet-customize-container textarea { font-family: \"Bitstream Vera Sans Mono\", Consolas, monospace; margin: 0; padding: 0px; }\n.stylesheet-customize-container h2 { margin-top: 15px; margin-bottom: 10px;  }\n\n.image-upload .new-image { margin-left: 20px }\n.image-upload span  { padding-left: 5px; }\n\n\nul#image-preview-list {\n    margin: 20px 320px 20px 20px;\n    font-size:larger;\n}\nul#image-preview-list li {\n    padding-bottom: 10px; \n    margin-bottom: 20px; \n    vertical-align: top; \n    width: 45%; \n    height: 100px; \n    float: left;\n    position: relative; \n}\n\nul#image-preview-list .preview {\n    width: 100px;                                     \n    float: left;\n    display: block;\n    text-align: center;\n    max-height: 100px;\n    overflow: hidden;\n}\nul#image-preview-list .preview img {\n    max-width: 100px; \n    padding: auto;\n}\nul#image-preview-list .description {\n    vertical-align: top;\n    margin-left: 105px; \n}\nul#image-preview-list .description pre {\n    display: inline;\n    padding: 5px;\n    color: #000;\n    background-color: transparent;\n}\n\n\n.sheets { margin-right: 315px; }\n.sheets .col { width: 100%; }\n.sheets .col > div { margin: 0 5px; }\n.sheets .col textarea { width: 100% }\n.sheets .buttons { margin-left: 5px }\n.sheets .btn { margin-left: 0px; margin-right: 5px; }\n.sheets .btn.right { float: right; margin-right: 3px;}\n\n\n#validation-errors {\n    margin-left: 40px; \n    margin-top: 10px; \n    list-style-type: disc;\n}\n\n#validation-errors a,\n#validation-errors li,\n.errors h2 { color: red }\n\n#validation-errors a:hover { text-decoration: underline; }\n#validation-errors pre { padding: 10px; color: black; }\n\n#preview-table {\n    padding-right: 15px;  \n}\n#preview-table > table {  \n    border-width: .2em;\n    border-style: dashed;\n    border-color: lightgray;\n    padding: 5px; \n    margin: 5px; \n    width: 900px;\n}\n\n#preview-table > table > tbody > tr { padding-bottom: 10px;  }\n#preview-table > table > tbody > tr > td { padding: 5px; padding-right: 15px;}\n#preview-table > table > tbody > tr > th {\n    padding: 5px; padding-right: 15px;\n    font-weight: bold; \n    vertical-align: top;\n    font-size: larger;\n    text-align: right;\n}\n\n#img-preview-container {\n    border-width: .2em;\n    border-style: dashed;\n    border-color: lightgray;\n    padding: 5px; \n    margin: 5px; \n    float: left;\n}\n\n#image-upload #img-preview-container img {\n  max-width: 160px;\n}\n#icon-upload #img-preview-container img {\n  width: 64px;\n  height: 64px;\n  margin: 0;\n}\n#banner-upload #img-preview-container img {\n  width: 160px;\n  height: 48px;\n  margin: 0;\n}\n\n.linefield.mobile {\n  width: 512px;\n  background-color: #EFF7FF;\n  border: 1px solid #CEE3F8;\n}\n\n.private-feeds.instructions .prefright { \n    line-height: 2em;\n}\n\n.private-feeds.instructions .feedlink {\n    padding: 2px 5px;\n    font-weight: bold;\n    margin-right: 5px; \n    border: 1px solid #0000FF;\n    color: white; \n    padding-left: 22px; \n    background: #336699 none no-repeat scroll top left;\n}\n\n.private-feeds.instructions .feedlink.rss-link {\n    background-image: url(../rss.png);\n}\n\n.private-feeds.instructions .feedlink.json-link {\n    background-color: #DDDDDD;\n    background-image: url(../json.png);\n    color: black; \n}\n\n#sr-header-area {\n    background-color: #f0f0f0;\n    white-space: nowrap;\n    text-transform: uppercase;\n    border-bottom: 1px solid gray;\n    font-size: 90%;\n    height: 18px;\n    line-height: 18px;\n }\n\n#sr-header-area .width-clip {\n    position: absolute;\n    left: 0;\n    right: 0;\n}\n\n#sr-header-area .selected a {\n    color: orangered;\n}\n\n#sr-header-area .sr-list {\n    overflow: hidden;\n}\n\n#sr-header-area .dropdown.srdrop {\n    float: left;\n    padding-left: 5px;\n}\n\n#sr-header-area .drop-choices.srdrop {\n    margin-top: 0;\n    margin-left: 5px;\n}\n\n.dropdown.srdrop .selected {\n    background: none no-repeat scroll center right;\n    background-image: url(../droparrowgray.gif); \n    display: inline-block;\n    vertical-align: bottom;\n    padding-right: 21px;\n    padding-left: 5px; /* have to use padding instead of margin cause of ie */\n    color: black;\n    font-weight: normal;\n    margin-left: -5px; /* There's 5px margin intended for the SR list... */\n    margin-right: 10px;/* ...so we move it to the right side plus 5 more */\n    cursor: pointer;\n }\n\n.srdrop .choice {padding-top: 3px;}\n\n.srdrop .choice.top-option {\n    font-style: italic;\n    border-bottom: 1px dotted #369;\n}\n\n\n\n.srdrop .choice.bottom-option {\n    font-style: italic;\n    border-top: 1px dotted #369;\n}\n\n.sr-bar .separator {color: gray; }\n.sr-bar a {color: black;} \n.sr-bar a.gold { color: #9a7d2e; font-weight: bold; }\n\n#sr-more-link {\n    color: black;\n    background-color: #f0f0f0;\n    position: absolute;\n    right: 0;\n    top: 0;\n    padding: 0 5px 0 15px;\n    font-weight: bold;\n    margin: 0;\n}\n\n#sr-more-link:hover {text-decoration: underline;}\n\n.subscription-box li {\n    clear: left;\n    margin-bottom: 10px;\n}\n\n.subscription-box .fancy-toggle-button {\n    margin-right: 5px;\n    float: left;\n}\n\n.subscription-box .title {\n    font-size: medium;\n    color: blue;\n    margin-right: 5px;\n}\n\n.subscription-box .title.banned {\n    color: dimgray;\n    text-decoration: line-through;\n}\n\n.subscription-box .column {\n    width: 50%;\n    float: left;\n}\n\n.subscription-box .box-top {\n    position: relative;\n    height: 20px;\n}\n\n.subscription-box .box-separator {\n    border-style: none none dotted none;\n    border-width: 1px;\n    margin-bottom: 5px;\n}\n\n.subscription-box h1{ text-align: center; }\n \n.toggle.deltranslator-button { display: inline; }\n\n/****************/\n\n#sr { margin-left: 0px }\n\n#sr-list-wrapper {\n    width: 454px;\n    height: 200px;\n    border: 1px solid gray;\n    border-top: none;\n    margin: 0 5px;\n    font-size: smaller;\n    position: relative;\n}\n\n#sr-list-cover {\n    position: absolute;\n    background: gray none no-repeat scroll center center;\n    background-color: url(../throbber.gif); \n    height: 100%;\n    width: 100%;\n    opacity: .7;\n    filter:alpha(opacity=70); /* IE patch */\n    z-index: 1000;\n    display: none;\n}\n\n#sr-list {\n    overflow: auto;\n    position: absolute;\n    height: 100%;\n    width: 100%;\n}\n\n\n#sr-searchfield { margin: 0 5px; }\n\n#sr-name-box {\n    display: inline-block;\n    span {\n        display: block;\n        unicode-bidi: isolate;\n    }\n    .tooltip {\n        border-bottom: 1px dotted;\n        margin-bottom: 2px;\n    }\n}\n\n.sr-name {\n    font-size: small;\n    vertical-align: top;\n    padding: 3px 3px 3px 0;\n}\n\n.sr-description {\n    padding: 3px\n }\n\n.sr-row {\n    cursor: default;\n }\n\n.sr-row.sr-selected {\n    background: #EFF7FF none no-repeat scroll 0px 5px;\n    background-image: url(../rightarrow.png);    /* SPRITE */\n}\n\n.sr-arrow {\n    width: 10px;\n    height: 12px;\n }\n\n#sr-autocomplete-area {\n    position: relative;\n    z-index: 100;\n }\n\n#sr-drop-down {\n    position: absolute;\n    width: 100%;\n    margin: 0;\n    border: 1px solid gray;\n    background: white;\n    display: none;\n    left: 0;\n}\n\n#sr-drop-down  table {\n    width: 100%;\n}\n\n.sr-name-row {\n    cursor: default;\n }\n\n.sr-name-row.sr-selected {\n    background-color: #369;\n    color: white;\n}\n\n.submit-header {\n    font-size: larger;\n    font-weight: bold;\n}\n\n#suggested-reddits {\n    margin-top: 5px;\n    font-size: small;\n}\n\n#suggested-reddits h3 {\n    font-size: 1em;\n    font-weight: normal;\n    margin-top: .5em;\n}\n\n#suggested-reddits li {\n    display: inline;\n    padding-right: 5px;\n}\n\n\n/*** new menu shit ***/\n\n.formtabs-content {\n    width: 520px;\n    border-top: 4px solid #5f99cf;\n    padding-top: 10px;\n }\n\n.formtabs-content .infobar {\n    margin: 0;\n    padding: 5px;\n }\n\nul.tabmenu.formtab {\n    display: block;\n    padding-left: 10px;\n    font-size: larger;\n}\n\n.tabmenu.formtab li {\n    margin: 0;\n }\n\n.tabmenu.formtab a {\n    font-weight: normal;\n    outline: none;\n    padding: 0px 12px;\n    vertical-align: bottom;\n\n    border: 1px solid #c1c1c1;\n    border-bottom: none;\n}\n\n.tabmenu.formtab .selected a {\n    color:white;\n    font-size: 130%;\n    background-color: #5f99cf;\n    border: none;\n}\n\n\n/******* embed stuff ******/\n.expando {\n    clear: left;\n    margin: 5px 0 5px 0;\n    position: relative;\n\n    .form-bar {\n        float: left;\n    }\n}\n\n.link.over18 .expando-uninitialized {\n    .media-embed,\n    .media-preview {\n        visibility: hidden;\n    }\n}\n\n.expando-content {\n    display: none;\n}\n\n.expando-with-nsfw-interstitial {\n    .media-embed {\n        display: none;\n    }\n\n    .media-preview {\n        img.preview {\n            display: none;\n        }\n\n        img.censored-preview {\n            display: inline;\n        }\n    }\n}\n\n.expando-nsfw-gate {\n    @expando-nsfw-gate-color: #545452;\n    @expando-nsfw-font-size: 12px;\n\n    align-items: center;\n    color: @color-white;\n    font-size: @expando-nsfw-font-size;\n    .display-flex();\n    .justify-content(center);\n\n    &.expando-nsfw-interstitial {\n        background: @expando-nsfw-gate-color;\n    }\n\n    &.expando-nsfw-overlay {\n        height: 100%;\n        left: 0;\n        position: absolute;\n        top: 0;\n        width: 100%;\n    }\n\n    &.expando-nsfw-normal {\n        .expando-nsfw-gate-text {\n            cursor: pointer;\n        }\n    }\n\n    .expando-nsfw-gate-controls {\n        margin: auto;\n        text-align: center;\n    }\n\n    .expando-nsfw-gate-show-once {\n        background: none;\n        border: 1px solid @color-white;\n        color: inherit;\n        margin: unit(@margin-large, px);\n        padding: unit(@margin-small, px);\n        text-transform: uppercase;\n    }\n}\n\n.expando-nsfw-flow-complete {\n    @icon-size: 16px;\n\n    .form-bar {\n        padding-left: @icon-size + unit(@margin-small * 2, px);\n    }\n\n    .form-bar:before {\n        width: @icon-size;\n        height: @icon-size;\n        content: \"\";\n        display: block;\n        position: absolute;\n        left: unit(@margin-small, px);\n        top: 50%;\n        .transform(translateY(-50%));\n        .hdpi-bg-image(@1x: url(../mod-action-icon-confirm.png),\n                       @2x: url(../mod-action-icon-confirm_2x.png),\n                       @2x-bg-size: @icon-size);\n    }\n}\n\n.form-bar {\n    background: lighten(@color-pale-grey, 10%);\n    border: 1px solid @color-pale-grey;\n    clear: left;\n    margin-top: unit(@margin-small, px);\n    padding: unit(@margin-x-small, px) unit(@margin-small, px);\n    position: relative;\n\n    .md {\n        float: left;\n    }\n\n    button {\n        float: right;\n        margin-right: 0;\n    }\n}\n\n.media-preview {\n    overflow: auto;\n    position: relative;\n\n    .media-preview-content {\n        float: left;\n    }\n\n    img {\n        width: 100%;\n        height: auto;\n    }\n\n    img.censored-preview {\n        display: none;\n    }\n}\n\n.expando-button {\n    float: left;\n    height: 23px;\n    width: 23px;\n    margin: 2px 5px 2px 0;\n    background: white none no-repeat scroll center center;\n}\n\n.expando-button.selftext.collapsed {\n    background-image: url(../blog-collapsed.png); /* SPRITE */\n}\n.expando-button.selftext.collapsed:hover, .eb-sch {\n    background-image: url(../blog-collapsed-hover.png); /* SPRITE */\n}\n.expando-button.selftext.expanded, .eb-se {\n    margin-bottom: 5px;\n    background-image: url(../blog-expanded.png); /* SPRITE */\n}\n.expando-button.selftext.expanded:hover, .eb-seh {\n    background-image: url(../blog-expanded-hover.png); /* SPRITE */\n}\n\n.expando-button.video.collapsed {\n    background-image: url(../vid-collapsed.png); /* SPRITE */\n}\n\n.expando-button.video.collapsed:hover, .eb-vch {\n    background-image: url(../vid-collapsed-hover.png); /* SPRITE */\n}\n\n.expando-button.video.expanded, .eb-ve {\n    background-image: url(../vid-expanded.png); /* SPRITE */\n}\n.expando-button.video.expanded:hover, .eb-veh {\n    background-image: url(../vid-expanded-hover.png); /* SPRITE */\n}\n\n.expando .psuedo-selftext {\n    border-radius: 7px;\n    border: 1px solid #369;\n    overflow: hidden;\n    max-width: 710px;\n\n    /* webkit won't properly mask the overflow of iframe corners without this hack */\n    -webkit-mask-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAA5JREFUeNpiYGBgAAgwAAAEAAGbA+oJAAAAAElFTkSuQmCC');\n}\n\n.expando .psuedo-selftext iframe {\n    padding: 0;\n    margin: 0;\n    width: 100%;\n    border: 0;\n}\n\n\n/******** self text stuff ****/\n.subreddit .usertext .md {\n    padding: 2px 5px;\n    background-color: #fafafa;\n    border: 1px solid #CCC;\n    border-radius: 7px;\n}\n\n.link .usertext-body .md {\n    background-color: #fafafa;\n    border: 1px solid #369;\n    border-radius: 7px;\n}\n\n\n.usertext {\n    font-size: small;\n}\n\n.usertext-edit {\n    margin-top: 5px;\n    padding: 0 1px; /* so the border of help/textbox don't get chopped off */\n    width: 500px;\n}\n\n.usertext-edit textarea {\n    width: 500px;\n    height: 100px;\n}\n\n/*permalinks*/\n.usertext.border .usertext-body {\n    background-color: #ffc; padding-left: 5px;\n}\n\n.usertext.grayed .usertext-body {\n    color: gray;\n    background-color: #f0f0f0;\n    padding-left: 5px;\n    padding-right: 5px;\n    display: inline-block;\n}\n\n.usertext button {\n    margin: 5px 5px 10px 0;\n}\n\n.usertext .help-toggle, .usertext a.reddiquette {\n    font-size: smaller;\n    float:right;\n    margin-top: 5px; \n    margin-left: 10px;\n}\n\n.usertext .bottom-area {\n    /* this restricts children floats to the container */\n    overflow: hidden;\n    width: 100%;\n}\n\n.usertext .markhelp {\n    padding: 4px;\n    margin: 5px 0px;\n    border-top: 1px dotted #c0c0c0;\n\n    table {\n        width: 100%;\n        margin: 5px 0px;\n    }\n\n    tr, td {\n        width: 50%;\n        border: 1px solid #c0c0c0;\n    }\n}\n\n.usertext .markhelp .spaces {background-color: #c0c0c0}\n\n/*** roundfield stuff *******/\n.roundfield {\n    width: 500px;\n    background-color: #cee3f8;\n    border-radius: 4px;\n    padding: 5px 10px 10px 10px;\n    font-size: large;\n}\n\n.roundfield-actions {\n    width: 520px;\n}\n\n.roundfield .title {\n}\n\n.roundfield .roundfield-content {\n    margin-top: 5px;\n    border: none;\n    vertical-align: top;\n}\n\n.roundfield .usertext-edit {\n    width: 500px;\n}\n\n.roundfield textarea,\n.roundfield input[type=text],\n.roundfield input[type=url],\n.roundfield input[type=password],\n.roundfield input[type=number],\n#compose-message .roundfield select {\n    font-size: 100%;\n    width: 492px;\n    padding: 3px;\n    margin: 0;\n    border: 1px solid gray;\n}\n\n.roundfield.captcha .capimage {\n    margin-bottom: 10px;\n}\n.roundfield label { font-size: smaller; padding-right: 2px;  }\n\n\n\n/*** linefield stuff *****/\n.linefield {\n    width: 514px;\n    padding: 7px 5px;\n    font-size: large;\n    background-color: #CEE3F8;\n    margin-bottom: 10px;\n}\n\n.linefield .title {\n    /*background-color:#CEE3F8;\n    /*background-color:#EFF7FF;*!/\n    color: #336699;*/\n    color: blue;\n    font-weight:bold;\n    padding:1px 10px;\n}\n.linefield .title + .gray {\n    font-size: x-small;\n}\n.linefield .small-field, .linefield .delete-field {\n    padding: 0; \n    font-size: smaller; \n}\n\n.linefield span + span { \n    margin-left: 10px;\n}\n\n.linefield .linefield-description {\n    display: block;\n}\n\nul.colors {\n  overflow: auto;\n\n  li {\n    float: left;\n    width: 180px;\n    padding: 5px 10px;\n  }\n\n  label {\n    display: block;\n  }\n\n  .swatch {\n    display: inline-block;\n    width: 20px;\n    height: 20px;\n    margin-right: 0.5em;\n    vertical-align: middle;\n  }\n\n  li.custom-color {\n    clear: left;\n    width: auto;\n\n    p {\n      margin: 0 -10px 5px;\n    }\n\n    .swatch {\n      border: 1px solid #CEE3F8;\n    }\n\n    input[type=color] {\n      width: 100%;\n      height: 100%;\n      overflow: hidden;\n      cursor: default;\n      opacity: 0;\n    }\n\n    input[type=text] {\n      width: 60px;\n      vertical-align: middle;\n    }\n  }\n}\n\n.campaign .linefield span + span {\n    margin-left: 0;\n}\n\n\n.linefield .info { \n    font-style: italic;\n    color: red;\n    font-size: small;\n}\n\n.linefield .linefield-content {\n    /*border-color:#CEE3F8;\n    /*border-color:#EFF7FE;*!/\n    border-style:solid none none;\n    border-width:4px medium medium;*/\n    padding:2px 7px 5px;\n    vertical-align:top;\n}\n\n.linefield.usertext .usertext-edit {\n    font-size: small;\n}\n\n.linefield.usertext .edit-usertext {\n    font-size: x-small;\n    float: right;\n}\n\n.linefield .upload {\n    font-size: small;\n}\n.linefield .upload label {\n    font-size: small;\n}\n.linefield .upload > li {\n    margin-top: 10px;\n\n    &:first-child {\n      margin-top: 0;\n    }\n}\n\n.linefield.usertext .infobar {\n    width: 100%;\n}\n\n.linefield.usertext .usertext-buttons {\n    display: none;\n}\n\n.linefield textarea,\n.linefield input[type=text],\n.linefield input[type=password] {\n    font-size: 100%;\n    width: 492px;\n    padding: 3px;\n    margin: 0;\n    border: 1px solid gray;\n}\n\n.linefield select { margin: 0; }\n\n.linefield.captcha .capimage {\n    margin-bottom: 10px;\n}\n.linefield label { font-size: smaller; margin-right: 0.5em; }\n.linefield span{ font-size: smaller; }\n\n.linefield input[type=\"text\"].small-text { \n    font-size: smaller;\n    width: 100%; \n}\n\n.linefield .markhelp table {\n    background: #FFFFFF;\n}\n\n#kind-selector label {\n    padding-right: 20px;\n}\n\n.campaign .status { \n    font-size: x-small;\n}\n\n.campaign-detail {\n    .existing-campaigns {\n        > table {\n            width: auto;\n            background-color: white;\n            border: 1px solid #cc9;\n        }\n\n        .campaign-spent, .campaign-buttons, button {\n            display: none;\n        }\n    }\n\n    .hidden {\n        display: none;\n    }\n}\n\n.existing-campaigns > table { \n    font-size: x-small;\n    width: 100%;\n    margin: 0px 0; \n    border: none;\n}\n\n.existing-campaigns {\n    td.campaign-total-budget span {\n        display: inline-block;\n        margin-right: 5px;\n        line-height: 20px;\n        margin-top: 3px;\n        vertical-align: top;\n    }\n\n    td.campaign-total-budget.paid {\n        background: url(../green-check.png) no-repeat scroll center right;\n    }\n}\n\n.existing-campaigns tr.refund {\n    color: red;\n    font-weight: bold;\n}\n\n.frequency-cap-inputs {\n    margin-left: 20px;\n    .label {\n        width: 200px;\n    }\n}\n\n.frequency-cap-message {\n    font-size: 12px;\n    margin-left: 20px;\n}\n\n.frequency-cap-message.example {\n    margin-top: -15px;\n    font-style: italic;\n}\n\n.frequency-cap-dur-unit {\n    vertical-align: sub;\n    margin-left: 5px;\n}\n\n.minimum-spend, .daily-budget-minimum {\n    margin-left: 10px;\n    font-size: 13px;\n}\n\n.minimum-spend.error {\n    font-weight: bold;\n    color: red;\n    font-size: 15px;\n}\n\n.existing-campaigns > table {\n    margin: 10px 0;\n}\n\n.existing-campaigns > table > tbody > tr > td {\n    border-bottom: 1px solid darken(white, 15%);\n    max-width: 120px;\n}\n\n.existing-campaigns > table > tbody > tr#edit-campaign-tr > td { \n    text-align: left;\n}\n\n.existing-campaigns > table > tbody > tr#edit-campaign-tr > td {\n    padding: 0;\n}\n\n#edit-campaign-tr .campaign-editor > .linefield {\n    border-radius: 0;\n    margin: 0;\n}\n\n.existing-campaigns > table > thead > tr > th {\n    @color: lighten(black, 45%);\n    font-weight: bold;\n    color: @color;\n    border-bottom: 1px solid lighten(@color, 20%);\n}\n\n.existing-campaigns {\n    .campaign-target,\n    .campaign-location {\n         overflow: hidden;\n         text-overflow: ellipsis;\n         max-width: 100px;\n     }\n\n     .campaign-bid {\n        width: 60px;\n     }\n\n    .campaign-row,\n    .campaign-header-row {\n        > td,\n        > th {\n            padding: 8px 2px 5px 8px;\n        }\n    }\n\n    .campaign-start-date {\n        text-align: right;\n        padding-right: 7px;\n        padding-left: 8px;\n    }\n\n    .campaign-end-date,\n    .campaign-buttons,\n    .campaign-total-budget,\n    .campaign-duration,\n    .campaign-spent {\n        text-align: right;\n        padding-right: 8px;\n        padding-left: 2px;\n    }\n\n    > table > thead {\n        .campaign-start-date,\n        .campaign-end-date,\n        .campaign-spent {\n            width: 10%;\n        }\n        .campaign-spent,\n        .campaign-priority,\n        .campaign-duration {\n            width: 8%;\n        }\n        .campaign-total-budget {\n            width: 12%;\n        }\n        .campaign-buttons {\n            width: 15%;\n        }\n    }\n}\n\n.campaign .bid-info { font-size: x-small; }\n.campaign .bid-info.error { color: red; }\n.campaign td.prefright { \n    padding: 8px 4px 4px;\n}\n.campaign #bid, .campaign #impressions,\n.campaign #cap, .campaign #duration,\n.campaign #daily_budget {\n    text-align: right;\n    width: auto;\n    margin-bottom: 5px;\n}\n\n.campaign .subreddit-targeting input{\n    width: 95%;\n    border-radius: 7px;\n}\n\n.campaign #suggested-reddits ul {\n    margin:  0 20px 10px 0;\n}\n.campaign th { \n    font-size: small;\n    padding: 4px; \n    padding-top: 8px;\n    width: 90px;\n}\n\n.linefield-content .infotext {\n    margin-top: 5px;\n}\n    .linefield-content .infotext p {\n        margin:  5px;\n    }\n\n#campaign label,\n#campaign li {\n    font-size: small;\n}\n\n#campaign .geotarget-select {\n    display: block;\n    margin-top: 2px;\n}\n\n.geotargeting-disabled {\n    font-size: 13px;\n    color: grey;\n}\n\n.collection-selector {\n    position: relative;\n    width: 100%;\n    height: 50px;\n    z-index: 100;\n\n    .widget-container {\n        width: 100%;\n        background: #fff;\n        border: 1px solid #CECECE;\n        border-radius: 5px;\n        overflow: hidden;\n        position: absolute;\n        top: 0;\n        left: 0;\n    }\n    \n    .form-group-list {\n        position: relative;\n        .transition(all, 0.2s);\n        \n        .form-group {\n            display: block;\n            padding: 0;\n            margin: 0;\n\n            > input[type=radio] {\n                display: none;\n            }\n\n            > .label-group {\n                height: 50px;\n                width: 100%;\n                padding: 5px 10px;\n                .box-sizing(border-box);\n                cursor: pointer;\n                color: #404040;\n\n                &:hover {\n                    background: #E4EDF7;\n                }\n\n                .label {\n                    font-size: 15px;\n                }\n                .description {\n                    font-size: 10px;\n                }\n            }\n\n            > input[type=radio]:checked + .label-group {\n                background: #FFFFFF;\n                display: block;\n            }\n        }\n    }\n\n\n    .selected-style() {\n        background-color: #4A90E2;\n        color: #FFF;\n        box-shadow: none;\n    }\n    \n    &.expanded {\n        .form-group {\n            > input[type=radio]:checked + .label-group {\n                .selected-style();\n            }\n\n        }\n    }\n    &.collapsed {\n        input[type=radio]:checked + .label-group {\n            box-shadow: inset 0 -1px 0 1px #f2f2f2\n        }\n        .widget-container:before {\n            display: block;\n            content: \"\";\n            width: 0;\n            height: 0;\n            border-width: 15px 10px 0;\n            border-style: solid;\n            border-color: #CCC transparent;\n            position: absolute;\n            top: 18px;\n            right: 10px;\n            z-index: 100;\n            pointer-events: none;\n        }\n    }\n\n    &.uninitialized {\n        .form-group {\n            > .label-group {\n                display: none;\n            }\n\n            > input[type=radio]:checked + .label-group {\n                display: block;\n            }\n        }\n        &:hover .form-group {\n            > .label-group {\n                display: block;\n            }\n            \n            > input[type=radio]:checked + .label-group {\n                .selected-style();\n            }\n        }\n    }\n}\n\n.collection-subreddit-list {\n    font-size: 13px;\n    .label {\n        margin: 5px 0;\n        color: #404040;\n        font-size: 10px;\n    }\n    ul {\n        li {\n            display: inline-block;\n            margin-right: 5px;\n        }\n    }\n}\n\n/***traffic stuff***/\n.traffic-table,\n.traffic-tables-side fieldset {\n    margin: 1.5em 2em;\n    font-size: small;\n    border: 0;\n}\n\n.traffic-table caption,\n.traffic-tables-side fieldset legend{\n    font-weight: bold;\n    text-align: left;\n    font-size: medium;\n    font-variant: small-caps;\n}\n\n.traffic-table caption .normal {\n    font-weight: normal;\n    font-size: small;\n    font-variant: normal;\n    margin-left: .5em;\n}\n\n.traffic-form {\n    float: left;\n    margin-right: 10em;\n\n    p {\n        font-size: small;\n        margin-bottom: 1em;\n        max-width: 20em;\n    }\n\n    textarea {\n        display: block;\n    }\n}\n\n.traffic-table a:hover { text-decoration: underline; }\n.traffic-table thead th { font-weight: bold; text-align: center; padding-left: 2em;}\n.traffic-table thead th:first-child { text-align: left; padding-left: 0; }\n.traffic-table tbody th,\n.traffic-table tfoot th { text-align: left;}\n.traffic-table td { padding: 0 5px; }\n.traffic-table td { text-align: right; }\n\n.traffic-table tfoot tr { border-top: 1px solid black; }\n.traffic-table tfoot th,\n.traffic-table tfoot td { font-style: italic; }\n\n.traffic-table tr.max  { border-width: 2px; border-style: solid; }\n.traffic-table tr.min  { border: 2px solid #336699; }\n.traffic-table tbody tr:nth-child(even) { background-color: #E0E0E0; }\n.traffic-table tr.mean { font-style: italic; border-top: 1px solid; }\n\n/* this injects a blank space between weeks so that weekly patterns\n * are more visible. since the borders are collapsed, we need to use\n * cell padding to achieve the effect. however, the zebra-striping of\n * the table makes for ugly extra-wide stripes when a stripe and week\n * gap coincide. the nth-child rules put the padding on the top of\n * the next row instead of the bottom of the current one if the current\n * one is a zebra stripe. */\n.traffic-table .dow-6 th,\n.traffic-table .dow-6 td { padding-bottom: 1em; }\n.traffic-table tr:nth-child(odd).dow-5 th,\n.traffic-table tr:nth-child(odd).dow-5 td { padding-top: 1em; }\n.traffic-table tr:nth-child(even).dow-6 th,\n.traffic-table tr:nth-child(even).dow-6 td { padding-bottom: 0; }\n\n.traffic-tables-side {\n    float: left;\n    min-height: 50em;\n}\n\n#promote-graph-table,\n#traffic-hour {\n    display: none;\n}\n\ndiv.timeseries {\n    padding: 10px;\n    border: 1px solid #B0B0B0;\n    margin: 10px 10px;\n    display: inline-block;\n    text-align: center;\n}\n.timeseries-placeholder {\n    width: 350px;\n    height: 200px;\n    font-family: verdana;\n    font-size: small;\n}\ndiv.timeseries span.title {\n    font-weight: bold;\n    font-size: medium;\n    font-variant: small-caps;\n}\n\n#timeseries-unprocessed {\n    font-size: small;\n    font-weight: bold;\n    margin: 1em 0;\n    max-width: 60em;\n\n    &.slow {\n        color: #900;\n    }\n}\n\n.timeseries-tablebar {\n    height: 5px;\n    margin: 1px 0;\n}\n\n.promoted-traffic .usertable {  margin-left: 0px;  }\n\n.promoted-traffic h1 a {\n    font-size: small;\n    margin-left: 10px;\n}\n\n.promoted-traffic tfoot th,\n.promoted-traffic tfoot td {\n    font-style: normal;\n    font-weight: bold;\n    text-transform: uppercase;\n    padding-top: .3em;\n}\n\n.promocampaign-table td {\n    white-space: nowrap;\n}\n\n.traffic-table.promocampaign-table {\n    margin: 10px;\n\n    thead th {\n        text-align: right;\n        padding: 0 5px;\n    }\n\n    tr.total {\n        border-top: 1px solid black;\n    }\n\n    tr.active {\n        background-color: pink;\n        font-weight: bold;\n        border: 2px dotted red;\n    }\n}\n\n.promo-traffic .content .tabmenu li {\n    font-size: 1.3em;\n}\n\n.promo-traffic #helptext {\n    font-size: 1.2em;\n    padding: 3px 10px 6px;\n}\n\n#promo-traffic-no-campaigns {\n    padding: 20px;\n}\n\n.promo-traffic .tabpane-content {\n    margin-right: 305px;\n    min-width: 800px;\n    position: relative;\n}\n\n.promo-traffic #timeseries-unprocessed {\n    font-size: 1.2em;\n    margin: 0 .1em;\n    padding: 0px;\n    position: absolute;\n    right: 1em;\n    top: -1.3em;\n}\n\n.promo-traffic-csv-link {\n    font-size: 1.1em;\n    font-weight: bold;\n    position: absolute;\n    right: 15px;\n    top: 11px;\n}\n\n.promo-traffic-help {\n    font-size: 1.2em;\n    margin: 20px;\n}\n\n.promo-traffic-help p {\n    padding: 5px;\n}\n\n#promo-traffic-lifetime-stats {\n    font-size: 1.1em;\n    font-weight: bold;\n    margin: 2px 10px;\n    padding-top: 5px;\n}\n\n.promo-traffic-live {\n    background-color: #EFF7FF;\n\n    td {\n        max-width: 150px;\n        overflow: hidden;\n        text-overflow: ellipsis;\n    }\n}\n\n.promo-traffic-settings-instructions {\n    font-size: small;\n    margin: 0px 15px 10px;\n}\n\n.promo-traffic-settings {\n    padding: 20px;\n}\n\np.totals-are-preliminary {\n    margin-left: 10px;\n}\n\n.award-square-container {\n    max-width: 1000px;\n    overflow: hidden;\n}\n\n.award-square {\n    float: left;\n    padding: 10px 0px 30px 40px;\n    white-space: nowrap;\n    width: 300px;\n}\n\n.award-square.mini {\n    width: 100px;\n    white-space: normal;\n    text-align: center;\n}\n\n.award-square img {\n    float: left;\n    margin: 0 10px;\n    width: 70px;\n    height: 70px;\n}\n\n.award-square.mini img {\n    float: none;\n    margin-bottom: 7px;\n}\n\n.award-square .award-name {\n    color: black;\n    font-size: 22px;\n    font-family: verdana, arial, helvetica, sans-serif;\n    font-weight: bold;\n    line-height: 1em;\n}\n\n.award-square.mini .award-name {\n    font-size: 18px;\n    min-height: 36px;\n    display: block;\n}\n\n.award-square .winner-info {\n    line-height: 15px;\n    margin-top: 15px;\n    color: gray;\n}\n\n.award-square .winner-name {\n    font-size: 18px;\n    color: #336699;\n}\n\n.lined-table {\n    margin: 5px;\n}\n\ntable.lined-table {\n    margin: 5px 3px;\n}\n\n.lined-table th, .lined-table td {\n    border: solid #cdcdcd 1px;\n    padding: 3px;\n}\n\n.lined-table th {\n    text-align: center;\n    font-weight: bold;\n}\n\n.sponsorshipbox { \n    max-width: 300px;\n}\n\n.sponsorshipbox span { \n    color: gray;\n}\n\n.sponsorshipbox div { \n    border: 1px solid #D0D0D0; \n    width: 300px; \n    font-size: 0;\n}\n\n/* otherwise the pixel will cause a horizontal scrollbar in firefox */\n.sponsorshipbox .promote-pixel {\n  right:0;\n}\n\n.sidecontentbox a.helplink {\n    float: right;\n    margin-top: 4px;\n}\n\n.confirm-award-claim .md {\n    max-width: none;\n    font-size: 18px;\n}\n\n.trophy-table {\n    width: 100%;\n}\n\n.trophy-area .content {\n    background-color: #f5f5f5;\n}\n\n.trophy-info {\n    text-align: center;\n    vertical-align: top;\n}\n\n.trophy-info div {\n    margin-left: auto;\n    margin-right: auto;\n    width: 130px;\n    vertical-align: top;\n    padding: 15px 0 15px;\n}\n\n.trophy-icon {\n    margin-bottom: 2px;\n    width: 40px;\n    height: 40px;\n}\n\n.trophy-info.left {\n    margin-right: 10px;\n}\n\n.trophy-info.right {\n}\n\n.trophy-name {\n    color: black !important;\n}\n\n.trophy-description {\n    color: #555555;\n    font-size: x-small;\n}\n\n.dust {\n    text-align: center;\n    margin: 45px auto;\n    color: #d0d0d0;\n}\n\n.removecup-button {\n    display: inline;\n}\n\n.cup-info-box {\n    border: dashed #eeaa33 2px;\n    padding: 5px;\n}\n\n.cup-info-box tt {\n    background-color: #f5f5aa;\n}\n\n/* Datepicker\n----------------------------------*/\n.datepicker {\n    z-index: 1000; \n    display: none;\n    border-radius: 6px;\n    -webkit-box-shadow: 0px 4px 6px 3px hsla(0, 0%, 0%, .2), inset 0px 1px 0px 0px hsla(0, 0%, 100%, .9);\n    -moz-box-shadow: 0px 4px 6px 3px hsla(0, 0%, 0%, .2), inset 0px 1px 0px 0px hsla(0, 0%, 100%, .9);\n    box-shadow: 0px 4px 6px 3px hsla(0, 0%, 0%, .2), inset 0px 1px 0px 0px hsla(0, 0%, 100%, .9);\n    text-shadow:  0px 1px 0px hsla(0,0%,100%,.8);\n }\n /* @group CSS triangles*/\n .datepicker::before {\n     content: ' ';\n     display: block;\n     width: 0;\n     height: 0;\n     border: 10px solid;\n     position: absolute;\n     top: -20px;\n     left: 17px;\n     border-color: transparent transparent #369;\n }\n .datepicker::after {\n     content: ' ';\n     display: block;\n     width: 0;\n     height: 0;\n     border: 10px solid;\n     position: absolute;\n     top: -18px;\n     left: 17px;\n     border-color: transparent transparent #E5F2FF;\n }\n /* @end CSS Triangles*/\n \n.datepicker.inuse { display: block; }\n\n.ui-datepicker-inline {\n    font-size: x-small; \n    padding: 5px; \n}\n.ui-corner-all {\n    border-radius: 6px;\n}\n.ui-datepicker-header {\n    background: -webkit-gradient(linear, 0% 0%, 0% 100%, from( hsl(210, 54%, 89%)), to( hsl(210, 54%, 79%)));\n    background: -moz-linear-gradient(top, hsl(210, 54%, 89%), hsl(210, 54%, 79%));\n    background-color: #ADC9E6;\n    border:  1px solid #5E96CF;\n    color: #2E6399;\n    font-weight: bold;\n    font-size:  1.3em;\n    text-shadow: 0px 1px 0px hsla(0,0%,100%,.7);\n    -webkit-box-shadow: inset 0px 1px 0px hsla(0,100%,100%,.8);\n    -moz-box-shadow: inset 0px 1px 0px hsla(0,100%,100%,.8);\n    box-shadow: inset 0px 1px 0px hsla(0,100%,100%,.8);\n}\n.ui-datepicker-inline .ui-datepicker-prev {float: left; }\n.ui-datepicker-inline .ui-datepicker-next {float: right; }\n\n.ui-datepicker-inline .ui-datepicker-prev span, \n.ui-datepicker-inline .ui-datepicker-next span {\n    display: block;\n    text-align: center;\n    margin-right: 1px;\n    margin-bottom: 1px;\n    font-size: 1.5em;\n}\n.ui-datepicker-inline .ui-datepicker-prev:active, \n.ui-datepicker-inline .ui-datepicker-next:active {\n    color: white;\n}\n\n.ui-datepicker-inline .ui-datepicker-prev.ui-state-disabled, \n.ui-datepicker-inline .ui-datepicker-next.ui-state-disabled {\n    display: none; \n}\n\n.ui-datepicker-inline .ui-datepicker-prev, \n.ui-datepicker-inline .ui-datepicker-next {\n    display: block;\n    cursor: pointer;\n    padding: 0px 5px;\n}\n\n.ui-datepicker-year {\n    margin-left:  none !important; //Undoes linefield span\n}\n\n.ui-datepicker-inline .ui-datepicker-title {text-align: center; padding: 5px; margin: 0em 2em;}\n.ui-datepicker-inline table {\n    clear: right;\n    margin-top:  5px;\n    border: 1px solid hsl(210, 54%, 59%);\n}\n\n.ui-datepicker-inline .ui-datepicker-calendar th,\n.ui-datepicker-inline .ui-datepicker-calendar td {\n    padding: 0px;\n    border: 1px solid #5E96CF;\n    -webkit-box-shadow: inset 0px 1px 0px hsla(0,0%,100%,.7);\n    -moz-box-shadow: inset 0px 1px 0px hsla(0,0%,100%,.7);\n    box-shadow: inset 0px 1px 0px hsla(0,0%,100%,.7);\n    }\n.ui-datepicker-calendar th {\n    font-size: 1.1em;\n}\n\n.ui-datepicker-inline .ui-datepicker-calendar th span,\n.ui-datepicker-inline .ui-datepicker-calendar td span,\n.ui-datepicker-inline .ui-datepicker-calendar td a { \n    border:  0px;\n    margin: auto;\n    padding: 3px; \n    display: block;\n    width: 30px; \n    height: 2em;\n    text-align: center;\n    vertical-align: middle; \n    color: black;\n    background-color: #C8DBEF;\n    font-size: 1.5em;\n    font-weight: bold;\n    -webkit-box-shadow: inset 0px 1px 0px hsla(0,0%,100%,.7);\n    -moz-box-shadow: inset 0px 1px 0px hsla(0,0%,100%,.7);\n    box-shadow: inset 0px 1px 0px hsla(0,0%,100%,.7);\n}\n\n.ui-datepicker-inline .ui-datepicker-calendar th span {\n    text-align: center; \n    border: none; \n}\n\n.ui-datepicker-inline .ui-datepicker-calendar td.ui-datepicker-today a,\n.ui-datepicker-inline .ui-datepicker-calendar td.ui-datepicker-today span,\n.ui-datepicker-inline .ui-datepicker-calendar td a.ui-state-active\n {\n    color: white;\n    background: -webkit-gradient(linear, 0% 0%, 0% 100%, from( hsl(210, 54%, 65%)), to( hsl(210, 54%, 45%)));\n    background: -moz-linear-gradient(top, hsl(210, 54%, 65%), hsl(210, 54%, 45%));\n    background-color: #4F8AC9;\n    -webkit-box-shadow: inset 0px 2px 3px hsla(0,0%,0%,.6);\n    -moz-box-shadow: inset 0px 2px 3px hsla(0,0%,0%,.6);\n    box-shadow: inset 0px 2px 3px hsla(0,0%,0%,.6);\n    text-shadow: 0px -1px 0px hsla(0,0%,0%,.8);\n}\n\n.ui-datepicker-inline .ui-datepicker-calendar td span {\n    color: #888;\n}\n\n.ui-datepicker-inline .ui-datepicker-calendar td a.ui-state-hover  {\n    background: #6BB3FF; \n    color: white;\n    text-shadow: 0px -1px 0px hsla(0,0%,0%,.6);\n}\n\n.ui-datepicker-inline .ui-datepicker-calendar td a.ui-state-active {\n    background: -webkit-gradient(linear, 0% 0%, 0% 100%, from( hsl(0, 54%, 75%)), to( hsl(0, 54%, 55%)));\n    background: -moz-linear-gradient(top, hsl(0,54%,75%), hsl(0,54%,55%));\n    background-color: #E19D9D;\n}\n\n.date-input {\n    display: inline;\n    position: relative; \n}\n.date-input input {\n    border: 1px solid #888; \n    padding: 2px;\n    text-align: center; \n    margin: 0 2px; \n}\n.date-input .drop-choices {  \n    position: absolute;\n    border: 1px solid #369;\n    background-color: #E5F2FF;\n    margin: 10px 3px;\n}\n\n\n.payment-setup input[name=bid] { width: 6em; text-align: right; }\n.payment-setup form { margin: 20px;  }\n.payment-setup p { margin-bottom: 10px; }\n\n.pay-form textarea[disabled] {\n    font-size:smaller;\n    padding: 0; \n}\n.pay-form *[disabled],\n.pay-form input[disabled]\n {\n    border: none;\n    color: black;\n    font-weight: bold;\n    background-color: white;\n}\n\n.bid-table { margin: 5px 10px;  }\n.bid-table td, \n.bid-table th \n{\n    padding: 2px 5px; \n    text-align: right; \n}\n\n.bid-table th \n{\n    text-align: center; \n    font-weight: bold; \n}\n\ndiv #campaign-field {\n  width: auto;\n}\n\n#promo-form {\n    .linefield {\n        width: auto;\n    }\n\n    .usertext-edit {\n        width: auto;\n    }\n}\n\n.form-group {\n    display: block;\n\n    > label,\n    > .label,\n    > .label-group {\n        display: inline-block;\n     }\n \n    > .label-group > .label {\n        display: block;\n    }\n}\n\n.input-group {\n    display: inline-block;\n    vertical-align: top;\n}\n\n.checkbox-group,\n.radio-group {\n    font-size: 14px;\n    line-height: 20px;\n\n    .form-group {\n        margin: 5px 0;\n    }\n\n    input[type=radio],\n    input[type=checkbox] {\n        font-size: 20px;\n        line-height: 20px;\n        margin-right: 10px;\n        margin-left: 1px;\n    }\n}\n\n.radio-group {\n    .label-group,\n    .label,\n    input[type=radio] {\n        vertical-align: middle;\n    }\n}\n\n.dashboard {\n    header {\n        position: relative;\n        margin-bottom: 10px;\n    }\n}\n\n.mobile-targeting-group {\n    display: block;\n}\n\n.mobile-os-group,\n.os-device-group {\n    display: inline-block;\n    vertical-align: top;\n}\n\n.mobile-os-group {\n    width: 150px;\n}\n\n.sponsored-page {\n    .checkbox-group,\n    .radio-group,\n    .form-group,\n    .select-group,\n    .collection-targeting,\n    .subreddit-targeting {\n        margin-bottom: 10px;\n        > .label:first-child {\n            width: 110px;\n            margin-right: 10px;\n            text-transform: uppercase;\n            font-size: 12px;\n            color: lighten(black, 40%);\n            letter-spacing: 0.6px;\n        }\n        > .label.cost-basis-label {\n            width: 150px;\n        }\n    }\n \n    .radio-group .form-group {\n        margin-bottom: 5px;\n    }\n\n    .inline-dollar {\n        position: absolute;\n        margin-left: 10px;\n        margin-top: 5px;\n    }\n\n    .targeting-field .radio-group {\n        .label-group,\n        input[type=radio] {\n            vertical-align: middle;\n        }\n\n        .label-group {\n            font-size: 15px;\n        }\n\n        .label-group small {\n            font-size: 0.67em;\n        }\n    }\n\n    .targeting-field .linefield-content {\n        > .radio-group,\n        > .target-group {\n            width: 50%;\n            display: inline-block;\n            vertical-align: top;\n        }\n    }\n\n    .lookup-user-field,\n    .budget-field,\n    .pricing-field,\n    .timing-field,\n    .frequency-cap-field {\n        .group {\n            display: flex;\n            margin-bottom: 10px;\n        }\n\n        .form-group {\n            flex-basis: 0;\n            flex-grow: 1;\n            label, .label {\n                display: block;\n            }\n            .cost-basis-label {\n                width: 300px;\n            }\n        }\n    }\n\n    .display-text:before {\n        content: \"\\00a0\";\n    }\n    \n    .display-text.daily-max-spend:before {\n        content: \"\\00a0\\0024\";\n    }\n\n    .display-text {\n        font-weight: bold;\n    }\n\n    .budget-field {\n        .group {\n            margin-bottom: 10px;\n        }\n    }\n\n    .pricing-message,\n    .budget-message {\n        font-size: 13px;\n    }\n\n    .pricing-message {\n        margin-top: 10px;\n        margin-bottom: 10px;\n    }\n\n    .campaign-set {\n        padding: 10px;\n        background: #fff;\n        border-radius: 2px;\n        margin-bottom: 5px;\n\n        .info-text {\n            display: block;\n            border-bottom: 1px solid #e6e6e6;\n        }\n\n        .campaign-button {\n            padding: 5px 10px;\n        }\n\n        .button-group {\n            white-space: nowrap;\n\n            button:first-child {\n                border-top-right-radius: 0;\n                border-bottom-right-radius: 0;\n                border-right: 0;\n                padding-right: 5px;\n                margin-right: 0;\n            }\n\n            button + button {\n                border-top-left-radius: 0;\n                border-bottom-left-radius: 0;\n                border-left: 0;\n                padding-left: 5px;\n                margin-left: 0;\n            }\n        }\n    }\n\n    .campaign-set + .campaign-set,\n    .campaign-set + .info-text {\n        margin-top: 20px;\n    }\n\n    .campaign-option-table {\n        width: 100%;\n\n        .date {\n            text-align: left;\n            width: 18%;\n        }\n\n        .total-budget {\n            width: 20%;\n        }\n\n        .total-budget + td {\n            width: 35%;\n        }\n\n        td {\n            width: 18%;\n            line-height: 27px;\n            font-size: 14px;\n            font-weight: bold;\n            vertical-align: bottom;\n            text-align: right;\n            padding: 0 4px;\n        }\n    }\n\n    .lookup-user {\n        input[name=name] {\n            width: 10em;\n        }\n\n        input[name=email] {\n            width: 20em;\n        }\n\n    }\n}\n\n.os-device-group {\n    .radio-group + .device-version-group + .device-version-group {\n        border-top: 1px solid #e6e6e6;\n        padding-top: 10px;\n    }\n    .device-version-group {\n        padding-left: 24px;\n    }\n}\n\n.device-version-group {\n    .checkbox-group {\n        width: 140px;\n    }\n    .checkbox-group,\n    .select-group {\n        display: inline-block;\n        vertical-align: top;\n    }\n}\n\n.version-select {\n    .form-group {\n        margin: 5px 0 20px 0;\n        select {\n            width: 100px;\n        }\n        > .label:first-child {\n            width: 30px;\n            display: inline-block;\n        }\n    }\n}\n\n.inventory-dashboard {\n    .geotargeting-group {\n        display: none;\n    }\n}\n\n.sponsored-page {\n    @bg-color: lighten(#cee3f8, 5%);\n    width: 800px;\n    margin: auto;\n    color: lighten(black, 5%);\n\n    textarea,\n    input[type=text] {\n        width: 100%;\n        .box-sizing(border-box);\n    }\n\n    .hidden {\n        display: none;\n    }\n\n    .help p {\n        margin: 0;\n    }\n\n    .help a.help {\n        font-weight: bold;\n        text-decoration: none;\n        float: right;\n        color: orangered;\n    }\n\n    h2 {\n        margin: 20px 0 5px;\n        font-size: 200%;\n        color: lighten(black, 40%);\n        font-weight: normal;\n    }\n\n    .infobar {\n        margin: 5px 0;\n        padding: 0;\n        background: transparent;\n        border: 0;\n        color: lighten(black, 25%);\n\n        a {\n            white-space: nowrap;\n        }\n    }\n\n    .post-type-field,\n    .targeting-feild {\n        .radio-group {\n            .label-group {\n                font-size: 15px;\n            }\n            .label-group small {\n                font-size: 0.67em;\n            }\n        }\n    }\n\n    .post-type-field {\n        .radio-group {\n            overflow: auto;\n        }\n        .form-group {\n            float: left;\n            width: 33%;\n        }\n    }\n\n    > header {\n        margin: 10px;\n    }\n\n    .rules {\n        float: left;\n        font-size: 13px;\n        line-height: 30px;\n    }\n\n    .primary-button {\n        @color2: lighten(#FF4500, 15%);\n        @color1: lighten(@color2, 7%);\n        .button-style(@color1, @color2, darken(@color2, 10%));\n        \n        color: white;\n        text-shadow: 0 1px 0 darken(@color2, 10%);\n    }\n\n    button {\n        @color: desaturate(darken(#CEE3F8, 5%), 15%);\n        .button-style(@color, darken(@color, 5%), darken(@color, 15%));\n        \n        margin: 0;\n        padding: 5px 20px;\n        font-size: 12px;\n        font-weight: bold;\n        color: lighten(#369, 5%);\n        text-shadow: 0 1px 0 lighten(#369, 45%);\n        vertical-align: text-top;\n    }\n\n    .new-campaign {\n        position: absolute;\n        right: 0;\n        top: 10px;\n        padding: 10px 15px;\n        font-size: 13px;\n    }\n\n    .campaign-buttons,\n    .campaign-total-budget {\n        button {\n            font-size: 10px;\n            font-weight: normal;\n            border-top-width: 1px;\n            box-shadow: none;\n            padding: 2px 4px;\n            margin: 2px 2px 0;\n            &:active {\n                box-shadow: inset 0 1px 0 1px fadeout(black, 90%);\n            }\n            &:hover, &:active {\n                margin-bottom: 0;\n            }\n        }\n    }\n\n    .editor-group {\n        position: relative;\n        @background-color: darken(#FFFFFF, 1%);\n        background: @bg-color;\n        margin: 0 0 10px;\n        border-radius: 2px;\n        font-size: 15px;\n        line-height: 20px;\n\n        footer {\n            margin: 0 20px;\n            padding: 10px 0;\n        }\n    }\n\n    .promotelink-editor {\n        .collapsed-display {\n            .linefield {\n                border-bottom: 0;\n                padding-bottom: 0;\n            }\n        }\n        .thing {\n            border-color: darken(@bg-color, 10%);\n            font-size: 10px;\n\n            .title {\n                padding: 0;\n                font-weight: normal;\n            }\n\n            .rank {\n                display: none;\n            }\n\n            .flat-list.buttons {\n                text-align: left;\n            }\n\n            a.thumbnail {\n                line-height: 0;\n            }\n        }\n    }\n\n    .dashboard {\n        padding: 20px;\n        border-bottom: 2px solid #ccc;\n\n        .help p {\n            margin-bottom: 10px;\n        }\n    }\n\n    .campaign-editor {\n        @color: #f8f8f8;\n\n        .editor-group {\n            background: transparent;\n            background: @color;\n            border: none;\n            box-shadow: none;\n            font-size: 13px;\n\n            >.linefield > .title:before {\n                background: darken(#f7f7f7, 10%);\n                margin-right: 0;\n            }\n\n            >.linefield > .title {\n                color: lighten(black, 40%);\n            }\n\n            .linefield {\n                border-bottom-color: darken(#FFFFFF, 10%);\n            }\n        }\n\n    }\n\n    .campaign-list-editor {\n        > .editor-group > .linefield {\n            border-bottom: 0;\n            padding-bottom: 10px;\n\n            .help {\n               font-size: 12px;\n               margin-right: 120px;\n            }\n        }\n    }\n\n    .campaign-list {\n        padding-bottom: 5px;\n    }\n\n    .editor {\n        @background-color: #CEE3F8;\n        .image-field {\n            position: relative;\n            overflow: auto;\n\n            input[type=file] {\n                margin-top: 10px;\n                margin-left: 0;\n            }\n            button.submit-img {\n                position: absolute;\n                bottom: 10px;\n                right: 20px;\n            }\n            img {\n                max-width: 600px;\n            }\n        }\n        .image-field.has-image {\n            .linefield-content {\n                margin-left: 110px;\n            }\n            .img-preview-container {\n                position: absolute;\n                left: 0;\n                top: 45px;\n                margin: 0;\n                padding: 5px;\n                img {\n                    display: block;\n                    width: 70px;\n                    height: auto;\n                }\n                br {\n                    display: none;\n                }\n            }\n        }\n\n        textarea,\n        input[type=text],\n        .date-input input,\n        .linefield {\n            border-radius: 2px;\n            font-size: 15px;\n            line-height: 20px;\n        }\n\n        textarea,\n        input[type=text],\n        .date-input input {\n            border: 1px solid;\n            border-color: darken(white, 25%) darken(white, 20%) darken(white, 15%);\n            background: darken(white, 1%);\n            padding: 5px 10px;\n\n            &:focus {\n                background: white;\n                border-color: lighten(#369, 15%);\n                outline: none;\n                box-shadow: 0 0 3px white;\n            }\n\n            &:disabled {\n                box-shadow: none;\n                color: darken(white, 40%);\n                border-color: darken(white, 20%);\n                background: darken(white, 2%);\n            }\n        }\n\n        input[type=text],\n        .date-input input {\n            padding: 5px;\n            box-shadow: inset 0 1px 0 1px darken(white, 5%);\n            font-size: 15px;\n        }\n\n        input[type=text].total_budget_pennies {\n            padding-left: 20px;\n        }\n\n        textarea {\n            resize: vertical;\n            padding: 10px;\n            box-shadow: inset 0 2px 0 1px darken(white, 5%);\n        }\n\n        .date-input .datepicker {\n            @color: lighten(#369, 30%);\n            text-shadow: none;\n            border-color: @color;\n\n            .ui-corner-all,\n            & {\n                border-radius: 2px;\n            }\n\n            .ui-datepicker-header,\n            .ui-datepicker-calendar,\n            td,\n            th,\n            th span {\n                box-shadow: none;\n                border-color: @color;\n            }\n\n            table,\n            th {\n                border: 0;\n            }\n\n            th span {\n                font-size: 12px;\n                font-weight: normal;\n                background: transparent;\n                height: auto;\n            }\n\n            td a,\n            td span {\n                box-shadow: none;\n                font-size: 15px;\n                width: 100%;\n                .box-sizing(border-box);\n                text-align: right;\n            }\n\n            .ui-datepicker-today span {\n                background: lighten(@color, 5%);\n                box-shadow: none;\n                text-shadow: none;\n            }\n\n            a {\n                text-shadow: none;\n            }\n\n            a.ui-state-active {\n                background: lighten(orangered, 50%);\n                color: orangered;\n                text-shadow: none;\n            }\n\n            a.ui-state-hover {\n                text-shadow: none;\n                border-color: darken(@color, 5%);\n            }\n\n        }\n\n        .linefield {\n            width: auto;\n            padding: 20px 0;\n            margin: 0 20px;\n            border-radius: 0;\n            border-bottom: 1px solid darken(@bg-color, 10%);\n            background: transparent;\n            font-size: 15px;\n\n            span {\n                font-size: inherit;\n            }\n\n            &:last-child {\n                border-bottom: none;\n            }\n            > .linefield-content {\n                padding: 0;\n            }\n\n            > .title {\n                padding: 0;\n                display: block;\n                margin: 5px 0;\n                color: #369;\n                font-size: 15px;\n\n                &:before {\n                    position: absolute;\n                    width: 20px;\n                    height: 20px;\n                    right: 100%;\n                    border-radius: 10px;\n                    background: darken(@bg-color, 5%);\n                    margin-right: -10px;\n                }\n            }\n        }\n\n        > .linefield {\n            padding: 0;\n            background: transparent;\n            border: 0;\n        }\n\n        > .linefield > .title {\n            font-weight: normal;\n            font-size: 18px;\n            line-height: 20px;\n            margin: 10px;\n            color: saturate(darken(#369, 10%), 100%);\n        }\n\n        .buttons {\n            text-align: right;\n            button {\n                margin-left: 5px;\n            }\n        }\n\n        .infotext {\n            border: 0;\n            background: transparent;\n            font-style: italic;\n            color: grey;\n            padding: 0;\n            font-size: 13px;\n            line-height: 20px;\n            box-shadow: none;\n        }\n    }\n\n    .text-field {\n        textarea {\n            font-size: 13px;\n        }\n    }\n\n    .existing-campaigns {\n        .campaign-editor .editor-group {\n            margin-bottom: 10px;\n            border-radius: 0;\n        }\n\n        .campaign-editor footer {\n            margin-bottom: 10px;\n        }\n\n        .campaign {\n            border: 0;\n        }\n    }\n\n    .help {\n        font-size: 13px;\n    }\n\n    .campaign-dashboard header {\n        .error, .help {\n            margin-right:  150px;\n        }\n    }\n}\n\n.geotargeting-disabled {\n    font-size: 12px;\n    color: grey;\n}\n\n.fancy-settings h1 { font-size: 200%; color: #999; margin:10px 5px; }\n.fancy-settings h2 { font-size: 200%; font-weight:normal; color: #999; margin:10px 5px; }\n.fancy-settings h1 strong { font-weight:bold; color: #666;  }\n\n.bidding-history { \n    padding-top: 5px; \n    margin-right: 5px;\n}\n.bidding-history .linefield {\n    width: auto; \n    overflow: hidden; \n    padding-left: 10px; \n    border-left: 1px #DDD dashed;  }\n.bidding-history .linefield .bid-table,\n.bidding-history .linefield .notes { font-size: x-small; }\n.bidding-history .linefield .notes { margin-top: 10px; }\n.bidding-history .linefield .notes p {\n    text-indent: -20px;  \n    padding-left: 20px; \n    margin-bottom: 2px; \n    font-family: courier;\n}\n\n.pay-form tr.input-error th {\n    color: red;\n    font-weight: bold;\n    font-style: italic; \n}\n.pay-form th { padding: 0px }\n\n.pay-form tr.input-error input,\n.pay-form tr.input-error textarea,\n.pay-form tr.input-error select { border: 1px solid red; }\n\n.pay-form input[name=expirationDate],\n.pay-form input[name=cardCode] { width: 10ex; }\n\n.pay-form .optional { font-size: smaller; }\n.pay-form .disabled .optional { display: none; }\n.pay-form p.info { color: red; }\n.pay-link { font-size: smaller; margin-left: 10px;  }\n\ndt { margin-left: 10px; font-weight: bold;  }\ndd { margin-left: 20px;  }\n\n.borderless td {\n  border: none;\n}\n\n.promote-report-form {\n  margin: 1.5em 2em;\n}\n\n.promote-report-csv {\n  font-size: small;\n}\n\n.promote-report-table {\n  border: 0 none;\n  font-size: small;\n  margin: 1.5em 2em;\n\n  thead th {\n    font-weight: bold;\n    text-align: center;\n    padding: 0 1em;\n    border: 1px solid white;\n    background-color: #CEE3F8;\n  }\n\n  thead th.blank {\n    background: none;\n  }\n\n  thead th[colspan] {\n    text-align: center;\n  }\n\n  td {\n    text-align: right;\n    padding: 0 2em;\n  }\n\n  td.text {\n    text-align: left;\n    padding: 0 2em 0 0;\n  }\n  tr.total {\n    background-color: #FFC;\n    border-top: 1px solid #000000;\n    font-weight: bold;\n  }\n}\n\n.inventory-table {\n    font-size: smaller;\n    text-align: right;\n    margin-top: 20px;\n    width: 100%;\n\n    th, td {\n        padding: 3px;\n\n        &:last-child {\n            font-weight: bold;\n        }\n    }\n\n    th {\n        border-bottom: 1px solid #000000;\n        text-align: right;\n\n        &:first-child {\n            text-align: left;\n        }\n    }\n\n    td.title {\n        width: 120px;\n        text-align: left;\n    }\n\n    td:not(.title) {\n        border-left: 1px dashed #DDD;\n    }\n\n    tr:nth-child(even) {\n        background-color: #EFF7FF;\n    }\n\n    tr.total {\n        background-color: #FFC;\n        border-top: 1px solid #000000;\n        font-weight: bold;\n    }\n}\n\n/* title box */\n.titlebox {\n    font-size: larger;\n}\n\n.titlebox h1 {\n    font-family:arial,verdana,helvetica,sans-serif;\n    margin: 0px;\n    margin-bottom: 5px;\n    font-weight: bold;\n    font-size: 19px;\n}\n\n.titlebox h1 a {\n    color: black;\n}\n\n.titlebox .karma {\n    font-size: 18px;\n    font-weight: bold;\n}\n\n.titlebox .fancy-toggle-button {\n    display: inline-block;\n    margin-right: 5px;\n}\n\n.titlebox .bottom {\n    border-top: 1px solid gray;\n    padding-top: 2px;\n    font-size: 80%;\n    color: gray;\n}\n\n.titlebox .age {float: right;}\n\n#side-mod-list {\n    line-height: 1.5;\n\n    .sr-banned {\n        a {\n            color: @color-perma-ban;\n        }\n    }\n}\n\n#side-multi-list {\n    li {\n        @columns: 3;\n        @column-spacing: 3px;\n        display: inline-block;\n        width: 288px / @columns - @column-spacing;\n        margin-right: @column-spacing;\n        text-overflow: ellipsis;\n        overflow: hidden;\n    }\n\n    & + .expand-summary {\n        padding: 0 4px;\n        margin: 0;\n        margin-top: 3px;\n        font-size: x-small;\n    }\n}\n\n.confirm-button .confirmation {\n    color: red;\n    white-space: nowrap;\n\n    .prompt {\n        margin-right: .5em;\n    }\n}\n\n.gray-buttons {\n    button, a {\n        padding: 0;\n        margin: 0;\n        border: none;\n        background: none;\n        color: #888;\n        font-weight: bold;\n\n        &:hover {\n            text-decoration: underline;\n        }\n    }\n}\n\n.multi-details, .filtered-details {\n    h1 {\n        margin-bottom: 0;\n    }\n\n    h1 a, .throbber {\n        vertical-align: middle;\n    }\n    .throbber {\n        margin-left: 5px;\n    }\n\n    h2 {\n        margin-top: 0;\n        margin-bottom: 3px;\n    }\n\n    .settings {\n        margin-bottom: 5px;\n\n        input[type=\"radio\"] {\n            margin: 0;\n            margin-right: 3px;\n            vertical-align: middle;\n        }\n\n        button {\n            cursor: pointer;\n        }\n\n        label, & > button {\n            margin-right: 12px;\n        }\n\n        .visibility-group {\n            margin-right: 8px;\n            margin-bottom: 8px;\n        }\n    }\n\n    h3 {\n        font-weight: normal;\n        color: #777;\n        margin-bottom: 3px;\n    }\n\n    form.copy-multi, form.rename-multi {\n        display: none;\n        margin-bottom: 10px;\n\n        .multi-name {\n            border: 1px solid #ccc;\n            padding: 3px;\n        }\n\n        button {\n            .light-button;\n            padding: 3px 4px;\n        }\n\n        .throbber {\n            height: 22px;\n            display: none;\n        }\n\n        &.working .throbber {\n            display: inline-block;\n        }\n    }\n\n    form.copy-multi button {\n        background: #eeffdd;\n    }\n\n    form.rename-multi {\n        button {\n            background: #ffffdd;\n        }\n\n        .warning {\n            margin-top: .5em;\n            font-weight: bold;\n            color: #c2461f;\n        }\n    }\n\n    .description {\n        margin: .75em 0;\n\n        .usertext-edit, textarea {\n            // use !important to override legacy inline style width set by\n            // show_edit_usertext()\n            width: 294px !important;\n        }\n    }\n\n    ul, form.add-sr {\n        margin-left: 12px;\n    }\n\n    button.remove-sr, button.add {\n        .box-sizing(content-box);\n        text-indent: -9999px;\n        margin-left: 3px;\n        background: none no-repeat;\n        border: 3px solid transparent;\n        padding: 0;\n        opacity: .3;\n\n        &:hover {\n            opacity: 1;\n        }\n\n        &:active {\n            position: relative;\n            top: 1px;\n        }\n\n        &.remove-sr {\n            width: 9px;\n            height: 9px;\n            background-image: url(../close-small.png);  /* SPRITE */\n        }\n\n        &.add {\n            width: 15px;\n            height: 15px;\n            background-image: url(../add.png);  /* SPRITE */\n        }\n    }\n\n    &.readonly {\n        button.remove-sr {\n            display: none;\n        }\n    }\n\n    .share-in-sr {\n        display: none;\n    }\n    &.public {\n        .share-in-sr {\n            display: inline;\n        }\n    }\n\n    form.add-sr {\n        .sr-name, button.add {\n            vertical-align: middle;\n        }\n\n        .sr-name {\n            border: 1px solid #ccc;\n            padding: 3px;\n        }\n\n        button.add {\n            border: 5px solid transparent;\n        }\n    }\n\n    li {\n        font-size: 1.15em;\n        line-height: 1.5em;\n\n        a, button {\n            vertical-align: middle;\n        }\n    }\n\n    .bottom {\n        margin-top: 1em;\n    }\n}\n\n.modsr-link {\n    display: block;\n    margin-top: 5px;\n    margin-left: 15px;\n    font-weight: bold;\n}\n\n.filtered-details {\n    .unfilter {\n        font-weight: bold;\n        display: inline-block;\n        margin-top: 1em;\n        &:hover {\n            text-decoration: underline;\n        }\n    }\n\n    .add-sr {\n        margin-top: 5px;\n    }\n}\n\n.side .recommend-box {\n    margin: 15px 5px 30px 0px;\n    opacity: 0;\n    .transition(all, .1s, ease-in-out);\n\n    h1 {\n        display: inline-block;\n        font-size: 1.35em;\n        font-weight: bold;\n        white-space: nowrap;\n    }\n\n    ul {\n        margin: 4px 0;\n    }\n\n    .rec-item {\n        background-color: rgb(247, 247, 247);\n        border: solid thin silver;\n        display: inline-block;\n        font-size: 1.0em;\n        margin: 2px;\n        padding: 0 0 1px 5px;\n        position: relative;\n        width: 136px;\n        white-space: nowrap;\n\n        a {\n            display: inline-block;\n            height: 100%;\n            overflow: hidden;\n            line-height: 1.8em;\n            padding-left: 2px;\n            text-overflow: ellipsis;\n            vertical-align: middle;\n            width: 111px;\n        }\n\n        button.add {\n            background-color: rgb(247, 247, 247);\n            background-image: none;\n            border: none;\n            cursor: pointer;\n            height: 100%;\n            opacity: 0.3;\n\n            &:after {\n                background-image: url(../add.png);  /* SPRITE */\n                content: \"\";\n                display: block;\n                height: 15px;\n                width: 15px;\n            }\n\n            &:hover {\n                opacity: 1.0;\n            }\n        }\n    }\n    \n    .more {\n        color: #369;\n        cursor: pointer;\n        display: inline-block;\n        font-weight: bold;\n        vertical-align: top;\n    }\n\n    .endoflist {\n        background-color: #f7f7f7;\n        padding: 15px 25px;\n\n        h1 {\n            margin-bottom: 10px;\n        }\n\n        .heading {\n            color: #555;\n            font-weight: bold;\n        }\n\n        ul {\n            font-size: x-small;\n            list-style-type: disc;\n            margin: 4px 0 0 20px;\n        }\n\n        .reset {\n            cursor: pointer;\n        }\n    }\n}\n\n.readonly .recommend-box li > button {\n    display: none;\n}\n\n.hover-bubble.multi-add-notice {\n    @bg-color: lighten(orange, 42%);\n    @border-color: lighten(orangered, 30%);\n    padding: 10px 15px;\n    margin-top: -5px;\n    margin-right: 10px;\n    background: @bg-color;\n    border-color: @border-color;\n    border-radius: 4px;\n\n    &:before {\n        border-left-color: @border-color;\n    }\n\n    &:after {\n        border-left-color: @bg-color;\n    }\n\n    h3 {\n        font-size: 2em;\n    }\n\n    p {\n        font-size: 1.5em;\n        color: gray;\n    }\n}\n\n.sidecontentbox {\n    font-size: normal;\n}\n\n.sidecontentbox .content {\n    margin: 0;\n    padding: 5px;\n    border: 1px solid gray;\n    font-size: larger;\n    list-style: none;\n}\n\n.sidecontentbox .more { \n    margin-top: 5px; \n    text-align: right; \n    font-size: smaller;\n}\n\n.sidecontentbox .more a { \n    color: gray; \n}\n\n.sidecontentbox .title h1 {\n    display: inline;\n    text-transform:uppercase;\n    margin: 0;\n    color: gray;\n    font-size: 130%;\n}\n\n.sidecontentbox.collapsible .title {\n    cursor: pointer;\n}\n\n.sidecontentbox .collapse-button {\n    display: inline-block;\n    width: 10px;\n    height: 10px;\n    line-height: 10px;\n    text-align: center;\n    font-size: 10px;\n    background: #eee;\n    color: #333;\n    border: 1px solid #999;\n    border-radius: 2px;\n    margin: 1px 8px;\n    vertical-align: bottom;\n}\n\n.sidecontentbox .stamp {\n    margin-left: 3px;\n}\n\n.titlebox form.toggle, .leavemoderator {\n    margin: 0;\n    padding: 5px 0px;\n    font-size: smaller;\n    color: gray;\n    background: white none no-repeat scroll center left;\n}\n\n.usertable .moderator.toggle .main:before,\n.titlebox .leavemoderator:before,\n.titlebox form.leavecontributor-button:before,\n.icon-menu .reddit-edit:before,\n.icon-menu .reddit-traffic:before,\n.icon-menu .reddit-reported:before,\n.icon-menu .reddit-spam:before,\n.icon-menu .reddit-edited:before,\n.icon-menu .reddit-automod:before,\n.icon-menu .wikiaction-pages:before,\n.icon-menu .wikiaction-revisions:before,\n.icon-menu .reddit-ban:before,\n.icon-menu .reddit-mute:before,\n.icon-menu .reddit-flair:before,\n.icon-menu .reddit-moderationlog:before,\n.icon-menu .reddit-unmoderated:before,\n.icon-menu .reddit-moderators:before,\n.icon-menu .moderator-mail:before,\n.icon-menu .edit-stylesheet:before,\n.icon-menu .community-rules:before,\n.icon-menu .reddit-contributors:before,\n.icon-menu .reddit-modqueue:before,\n.giftgold a:before,\n.gilded-link a:before,\n.infobar.gold:before,\n.users-online:before,\n.notice-taken:before,\n.notice-available:before,\n.user-form .error:before {\n    height: 16px;\n    width: 16px;\n    display: block;\n    content: \" \";\n    float: left;\n    margin-right: 5px;\n}\n\n.titlebox .leavemoderator:before, .moderator.toggle .main:before {\n    background-image: url(../shield.png); /* SPRITE */\n}\n\n.moderator.accept-invite .main:before {\n    background-image: url(../addmoderator.png); /* SPRITE */\n}\n\n.titlebox form.leavecontributor-button:before {\n    background-image: url(../pencil.png); /* SPRITE */\n}\n\n.titlebox form.flairtoggle {\n    padding: 0;\n}\n\n.titlebox .tagline {\n    margin: 5px 0 5px 20px;\n}\n\n.icon-menu a {\n    background: white none no-repeat scroll center left;\n} \n.icon-menu li {margin: 5px 0;}\n\n.icon-menu .reddit-edit:before {\n    background-image: url(../reddit_edit.png); /* SPRITE */\n}\n.icon-menu .edit-stylesheet:before {\n    background-image: url(../css.png); /* SPRITE */\n}\n.icon-menu .community-rules:before {\n    background-image: url(../reddit_rules.png); /* SPRITE */\n}\n.icon-menu .reddit-traffic:before {\n    background-image: url(../reddit_traffic.png); /* SPRITE */\n}\n.icon-menu .reddit-reported:before {\n    background-image: url(../reddit_reported.png); /* SPRITE */\n}\n.icon-menu .reddit-spam:before {\n    background-image: url(../reddit_spam.png); /* SPRITE */\n}\n.icon-menu .reddit-ban:before {\n    background-image: url(../reddit_ban.png); /* SPRITE */\n}\n.icon-menu .reddit-mute:before {\n    background-image: url(../reddit_mute.png); /* SPRITE */\n}\n.icon-menu .reddit-flair:before {\n    background-image: url(../reddit_flair.png); /* SPRITE */\n    /* Work around a centering difference between this icon and reddit_ban.png */\n    margin-left: 1px;\n}\n.icon-menu .reddit-moderationlog:before {\n    background-image: url(../reddit_moderationlog.png); /* SPRITE */\n    margin-left: 1px;\n}\n.icon-menu .reddit-unmoderated:before {\n    background-image: url(../reddit_unmoderated.png); /* SPRITE */\n    margin-left: 1px;\n}\n.icon-menu .reddit-edited:before {\n    background-image: url(../reddit_edited.png); /* SPRITE */\n    margin-left: 1px;\n}\n.icon-menu .reddit-automod:before {\n    background-image: url(../reddit_automod.png); /* SPRITE */\n}\n.icon-menu .reddit-moderators:before {\n    background-image: url(../shield.png); /* SPRITE */\n}\n.icon-menu .moderator-mail:before {\n    background-image: url(../mailgray.png); /* SPRITE */\n    width: 15px;\n    height: 10px;\n    margin-top: 4px;\n    margin-left: 1px;\n}\n.icon-menu .reddit-contributors:before {\n    background-image: url(../pencil.png); /* SPRITE */\n}\n.icon-menu .reddit-modqueue:before {\n    background-image: url(../reddit_modqueue.png); /* SPRITE */\n}\n\n.users-online:before {\n    background-image: url(../online.png);  /* SPRITE */\n}\n\n.notice-taken:before, .notice-available:before {\n    margin-right: 3px;\n}\n\n.notice-taken:before,\n.user-form .error:before {\n    background-image: url(../icon-circle-exclamation.png);  /* SPRITE */\n}\n\n.notice-available:before {\n    background-image: url(../icon-circle-check.png);  /* SPRITE */\n}\n\n.linkinfo {\n    padding: 5px;\n    border: 1px solid #5f99cf;\n    background-color: #EFF7FF;\n    font-family:arial,helvetica,sans-serif;\n    font-size: larger;\n    border-radius:3px;\n}\n\n.linkinfo .score .number {\n    font-size: 22px;\n    font-weight: bold;\n }\n\n.linkinfo .score .word {\n    font-size: 15px;\n    font-weight: bold;\n }\n\n\n.linkinfo .shortlink {font-size: 80%; margin-top: 3px; }\n\n.linkinfo .shortlink input {\n    border: 1px solid gray;\n    font-family: monospace;\n    font-size: 140%;\n    padding: 3px 2px;\n    width: 175px;\n}\n\n.linkinfo .shortlink input:hover {\n    cursor: text;\n}\n\n.linkinfo table {margin-top: 5px;}\n\n.linkinfo td, .linkinfo th {\n    padding: 2px;\n    font-size: smaller;\n    border: 1px solid gray;\n}\n\na.adminbox {\n    border: solid 1px #eeeeee;\n    color: #cdcdcd;\n    font-family: monospace;\n    text-align: center;\n    padding-right: 1px;\n}\n\na.adminbox:hover {\n    text-decoration: none;\n    color: orangered;\n    border: solid 1px orangered;\n}\n\n.email {\n    font-family: monospace;\n    font-size: larger;\n}\n\n.lined-table, .lined-table th, .lined-table td {\n  border: solid #cdcdcd 1px;\n  border-collapse: collapse;\n  padding: 2px;\n  margin-bottom: 10px;\n}\n\n.lined-table th {\n  font-weight: bold;\n}\n\n.wide {\n    width: 100%;\n}\n\n.centered {\n    text-align: center;\n    vertical-align: middle;\n}\n\n.sr-ad-table .inherited {\n    background-color: #ddeeff;\n}\n.sr-ad-table .overridden {\n    background-color: #ffeedd;\n}\n.sr-ad-table .unused {\n    background-color: #eee;\n}\n.sr-ad-table .inherited .whence {\n    font-style: italic;\n}\n.sr-ad-table .overridden .whence {\n    font-weight: bold;\n}\n.sr-ad-table .details {\n    font-size: 150%;\n    padding: 10px;\n    vertical-align: top;\n}\n.sr-ad-table .details div {\n}\n.sr-ad-table .details .codename {\n    font-size: 150%;\n    margin-bottom: 20px;\n}\n.sr-ad-table .weight {\n    width: 4em;\n}\n\n.ad-assign-table .warning {\n    font-weight: bold;\n    color: red;\n}\n\na.pretty-button:hover { text-decoration: none !important }\n\n.pretty-button {\n    display: inline-block;\n    margin-left: 5px;\n    margin-bottom: 5px;\n\n    border: 1px solid #666;\n    padding: 1px 6px;\n    background: white none repeat-x scroll center left;\n\n    color: #111;\n    font-size: 10px;\n    font-weight: normal;\n\n    border-radius: 3px;\n    outline-style: none;\n}\n\n.pretty-button {\n    color: black;\n}\n\n.pretty-button.pressed {\n    color: white;\n}\n\n.pretty-button.negative {\n    background-image: url(../bg-button-negative-unpressed.png); /* SPRITE stretch-x */\n}\n\n.pretty-button.negative.pressed {\n    background-image: url(../bg-button-negative-pressed.png); /* SPRITE stretch-x */\n}\n\n.pretty-button.neutral {\n    background-image: url(../bg-button-neutral-unpressed.png); /* SPRITE stretch-x */\n}\n\n.pretty-button.neutral.pressed {\n    background-image: url(../bg-button-neutral-pressed.png); /* SPRITE stretch-x */\n}\n\n.pretty-button.positive {\n    background-image: url(../bg-button-positive-unpressed.png); /* SPRITE stretch-x */\n}\n\n.pretty-button.positive.pressed {\n    background-image: url(../bg-button-positive-pressed.png); /* SPRITE stretch-x */\n}\n\n.oatmeal img { \n    display: block;\n    margin: 5px auto; \n}\n\n.gold-thanks.gold-accent {\n    font-size: small;\n    margin: 35px auto 0;\n    padding: 20px;\n    width: 600px;\n    position: relative;\n    display: block;\n    border-radius: 3px;\n}\n\n.gold-thanks p { margin: 15px 0; text-align: center; }\n.gold-thanks .lounge-msg p { font-size: medium; }\n.gold-thanks .fancy-snoo img {\n    margin: 10px auto;\n    position: relative;\n    display: block;\n}\n\n.gold-accent {\n    margin-top: 10px;\n    padding: 0 10px 5px;\n    background-color: #fffdcc;\n    border: solid 1px #e1b000;\n    color: #9a7d2e;\n    display: inline-block;\n}\n\ntr.gold-accent { \n    display: table-row; \n    border-radius: 3px;\n}\n\ntr.gold-accent + tr > td { \n    padding-top: 10px; \n}\n\n.gold-accent.titlebox {\n    margin-top: 0;\n    padding-top: .5em;\n}\n\n.allminus-link {\n    margin-top: 1em;\n}\n\nbody:not(.gold) .allminus-link {\n    opacity: .75;\n}\n\n.allminus-link a {\n    font-size: 1.15em;\n}\n\n.gilded-link {\n    margin-top: 1em;\n}\n\n.gilded-link a {\n    color: #9a7d2e;\n    font-weight: bold;\n    font-size: 1.15em;\n}\n\n.gilded-link a:before {\n    height: 14px;\n    width: 14px;\n    margin: 0 6px 0 1px;\n    background-image: url(../gold-coin.png); /* SPRITE */\n}\n\n#per-sr-karma {\n    width: 300px;\n    margin: .6em auto 0 auto;\n    table-layout: fixed;\n}\n\n#per-sr-karma thead th,\n#per-sr-karma td {\n    text-align: right;\n}\n\n#per-sr-karma tbody th {\n    text-align: left;\n}\n\n#per-sr-karma #sr-karma-header {\n    width: 150px;\n    text-align: left;\n}\n\n#per-sr-karma thead th {\n    font-weight: bold;\n    padding-bottom: 2px;\n}\n\n#per-sr-karma tbody th {\n    white-space: nowrap;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    -o-text-overflow: ellipsis;\n}\n\n#per-sr-karma tbody td {\n    font-weight: bold;\n    color: #666;\n}\n\n#per-sr-karma th.helpful span {\n    border-bottom: 1px dotted #666;\n    cursor: help;\n    display: inline-block;\n}\n\n.more-karmas {\n    display: none;\n}\n\n.karma-breakdown {\n    margin-top: .6em;\n    margin-bottom: 5px;\n}\n\n.karma-breakdown a {\n    font-weight: bold;\n}\n\n.rel-note button[type=submit] { \n    visibility: hidden;\n    font-size: x-small;\n    padding-top: 1px;\n    padding-bottom: 1px;\n    margin: 0px;\n    margin-left: 5px;\n}\n.rel-note.edited button[type=submit] { visibility: visible; }\n.rel-note.edited input[name=note] { width: 65%; }\n\n.rel-note input[name=note] {\n    font-size: xx-small;\n    max-width: 200px;\n    width: 100%;\n}\n\nform#banned textarea {\n    display: block;\n    margin-left: 0;\n}\n\n.gold-accent h1,\n.gold-accent th {\n    color: #6a4d00;\n    font-family: \"Hoefler Text\",\"Palatino Linotype\",\"Book Antiqua\",\n                  Palatino,georgia,garamond,FreeSerif,serif;\n    font-variant: small-caps;\n}\n\n.gold-accent .pretty-form input[type=text] {\n    margin-top: 1px;\n    margin-bottom: 2px;\n    margin-left: 0;  /* or else friends with benefits'\n                        \"note\" td and th don't line up */\n}\n\n.gold-accent .pretty-form input[type=text]#name {\n    border-radius: 3px;\n}\n\n.gold-accent .pretty-form button {\n    background-color: #fff088;\n    color: #6a4d00;\n    border: 1px solid #9a7d2e;\n    border-radius: 3px;\n}\n\n.gold-accent.snoovatar-link {\n    color: #9A7D2E; \n    display: block;\n    padding: 0;\n    margin: 3px 0;\n    border-radius: 5px;\n    text-align: center;\n    position: relative;\n    line-height: 30px;\n\n    &:before {\n        background-image: url(../gold/snoo-head.png);\n        background-repeat: no-repeat;\n        content: '';\n        display: inline-block;\n        background-size: 27px;\n        width: 27px;\n        height: 27px;\n        vertical-align: top;\n        background-position: center left;\n        margin-top: 1px;\n        margin-left: -13px;\n        margin-right: 7px;\n    }\n}\n\n.gold-expiration-info {\n    display: block;\n    margin: 3px 0;\n    text-align: center;\n}\n\n.gold-expiration-info div:last-of-type {\n    margin-top: -10px;\n}\n\n.gold-expiration-info .karma {\n    color: #583800;\n}\n\n.giftgold {\n    margin-bottom: 5px;\n}\n.giftgold a {\n    color: #9a7d2e;\n    font-weight: bold;\n}\n.giftgold a:before,\n.infobar.gold:before\n{\n    background-image: url(../giftgold.png); /* SPRITE */\n}\n\n.gold-accent.comment-visits-box {\n    color: #583800;\n    margin: 0px 5px 15px;\n    padding: 7px 10px 7px 7px;\n    max-width: 550px;\n}\n\n.gold-accent.roundfield {\n    margin-top: 0px;\n}\n\n.gold-form .note {\n    font-size: 13px;\n    line-height: 16px;\n    font-style: italic;\n    color: #222;\n    margin-top: 25px;\n}\n\n.gold-form p.goldtype-note {\n    .gold-form .note;\n    margin-top: 0;\n    margin-left: 22px;\n    padding-bottom: 7px;\n    border-bottom: 1px solid #e1b000;\n}\n\n.gold-form label:last-of-type {\n    padding-right: 0;\n\n    p.goldtype-note {\n        border-bottom: 0;\n        padding: 0;\n    }\n}\n\n.gold-form .spacer {\n    margin-top: 20px !important;\n}\n\n.gold-subsection {\n    display: none;\n    position: absolute;\n}\n\n.gold-wrap.inline-gold {\n    .fancy {\n      width: 620px;\n      margin: 0;\n    }\n\n    .fancy-content {\n        margin: 10px auto;\n    }\n\n    .sidelines {\n        display: none;\n    }\n}\n\n.gold-wrap.cloneable-link, .gold-wrap.cloneable-comment {\n    display: none;\n}\n\n.gold-form {\n    .credit-card-input {\n        display: inline;\n    }\n\n    .stripe-submit {\n        display: block;\n        margin-top: 10px;\n    }\n}\n\n.gold-wrap form {\n    display: inline;\n}\n\n.gold-logo {\n    float: left;\n    margin: 5px 0;\n}\n\n.thing .gold-form {\n    margin: 10px 0 10px 4px;\n    min-height: 0;\n}\n\n.gold-payment .roundfield-content {\n    margin-left: 80px;\n}\n\n.gold-payment .close-button {\n    position: absolute;\n    top: 0;\n    right: 0;\n    width: 13px;\n    height: 13px;\n    margin: 6px 4px 6px 8px;\n    border: none;\n    background: url(../close.png) no-repeat;  /* SPRITE */\n    text-indent: -9999px;\n    opacity: .25;\n}\n\n.gold-payment .close-button:hover {\n    opacity: .55;\n}\n\n.giftmessage {\n    background-color: white;\n    border-radius: 3px;\n    border: solid #888 1px;\n    color: black;\n    margin-bottom: 15px;\n    margin-left: 20px;\n    padding: 0 10px;\n    max-width: 300px;\n    width: 90%;\n}\n\n.gold-button {\n  margin-left: 5px;\n  padding: 10px 20px;\n  color: #fff;\n  font-size: 1.1em;\n  font-family: @gold-fonts;\n  text-align: center;\n  background-color: @gold-button-bg;\n  border-radius: 4px;\n  border: 1px solid transparent;\n  .transition(background-color, .2s);\n  .box-sizing(content-box);\n  box-shadow: inset 0 -2px 0 rgba(0,0,0,0.27);\n}\n\n.gold-button:hover {\n  background-color: @gold-button-hover;\n}\n\n.gold-button:active {\n  background-color: @gold-button-hover;\n  border: @gold-button-border;\n  box-shadow: inset 0px 0px 7px rgba(0,0,0,0.27);\n}\n\n.gold-button.disabled, .gold-button:disabled  {\n    color: #999;\n    background-color: #ccc;\n    border-color: #aaa;\n    text-shadow: none;\n}\n\n.creddits-gold {\n  .remaining {\n    font-size: 10px;\n    text-align: center;\n    width: 125px;\n    position: absolute;\n    display: block;\n    line-height: 1.5;\n  }\n\n  .gold-button {\n    padding-left: 35px;\n    position: relative;\n\n    .snoo-head {\n      background-image: url(../gold/snoo-head.png);\n      background-repeat: no-repeat;\n      position: absolute;\n      top: 7px;\n      left: 7px;\n      right: 0;\n      bottom: 0;\n      background-size: 23px;\n      width: 23px;\n      opacity: 0.5;\n      .transition(opacity, 0.2s);\n    }\n  }\n\n  .gold-button:hover{\n    .snoo-head {\n      opacity: 1;\n    }\n  }\n}\n\n.gold-dropdown, .goldvertisement {\n    font-family: \"Bitstream Charter\", \"Hoefler Text\", \"Palatino Linotype\",\n                 \"Book Antiqua\", Palatino, georgia, garamond, FreeSerif, serif;\n}\n\n.gold-dropdown {\n    color: #482800;\n    background-color: #fff088;\n    font-size: 16px;\n}\n\n.gold-expiration-info, .server-seconds {\n    padding: 6px;\n    border-radius: 5px;\n}\n\n.server-seconds {\n    background-color: #EFF7FF;\n    border: 1px solid #5F99CF;\n    text-align: center;\n\n    p {\n        padding: 5px 0;\n    }\n\n    em {\n        font-weight: bold;\n        color: #393939;\n    }\n}\n\n.server-seconds-public {\n    &.bottom {\n        border-top-style: dashed;\n        border-top-color: #ccc;\n        margin-top: 6px;\n        padding-top: 10px;\n        text-align: left;\n    }\n\n    * {\n        vertical-align: middle;\n    }\n\n    input[type=radio] {\n        margin-top: 0;\n    }\n\n    label {\n        margin: 0 5px;\n        position: relative;\n        top: -2px;\n    }\n\n    .title {\n        float: left;\n        padding-left: 20px;\n        margin-right: 10px;\n    }\n\n    .note {\n        font-style: italic;\n        margin-top: 10px;\n        text-align: center;\n    }\n}\n\n.comment-visits-box .title {\n    font-weight: bold;\n    font-size: 12px;\n}\n\n.new-comment .usertext-body {\n    background-color: #e5efff;\n    border: solid 1px #cddaf3;\n    margin: -1px 0;\n    padding: 0 4px;\n}\n\n.role {\n    width: 800px;\n}\n/* Style that can override other styles. Applied as a class per-field */\n.styled-input {\n    border: 1px solid gray;\n    padding: 2px;\n    \n    -webkit-box-shadow: inset 0px 1px 1px hsla(0,0%,0%,.3), 0px 1px 0px hsla(0,0%,100%,.6);\n    -moz-box-shadow: inset 0px 1px 1px hsla(0,0%,0%,.3), 0px 1px 0px hsla(0,0%,100%,.6);\n    box-shadow: inset 0px 1px 1px hsla(0,0%,0%,.3), 0px 1px 0px hsla(0,0%,100%,.6);\n}\n\n.infobar.client-info {\n    position: relative;\n    margin: 10px 2%;\n    width: 94%;\n    height: 48px;\n}\n\n.infobar.client-info .icon img {\n    position: absolute;\n    left: 10px;\n    width: 48px;\n    height: 48px;\n}\n\n.infobar.client-info div {\n    line-height: 48px;\n    margin-left: 56px;\n}\n\n.infobar.client-info div p {\n    white-space: nowrap;\n}\n\n.oauth2-authorize {\n    position: relative;\n    background: url(../snoo-tray.png) no-repeat;\n    width: 542px;\n    height: 235px;\n    margin: 40px auto 0;\n    padding-left: 268px;\n    padding-top: 18px;\n}\n\n.oauth2-authorize h1 {\n    margin-left: 10px;\n}\n\n.oauth2-authorize h1 a {\n    font-weight: bold;\n    letter-spacing: -.04em;\n}\n\n.oauth2-authorize .icon {\n    position: absolute;\n    left: 160px;\n    top: 69px;\n    width: 72px;\n    height: 72px;\n    line-height: 72px;\n    text-align: center;\n    white-space: nowrap;\n}\n\n.oauth2-authorize .icon img {\n    vertical-align: middle;\n}\n\n.oauth2-authorize .access,\n.infobar.client-info {\n    background: #f7f7f7;\n    border: 1px solid #b3b3b3;\n}\n\n.oauth2-authorize .access {\n    position: relative;\n    float: right;\n    width: 510px;\n    padding: 10px 15px;\n    font-size: 1.5em;\n    line-height: 1.5em;\n}\n\n.oauth2-authorize .access:before {\n    position: absolute;\n    display: block;\n    content: '';\n    border-width: 9px;\n    border-style: solid solid outset; /* mitigates firefox drawing a thicker arrow */\n    border-color: transparent #b3b3b3 transparent transparent;\n    left: -19px;\n    top: 13px;\n}\n\n.oauth2-authorize .access:after {\n    position: absolute;\n    display: block;\n    content: '';\n    border: 9px solid;\n    border-color: transparent #f7f7f7 transparent transparent;\n    left: -18px;\n    top: 13px;\n}\n\n.oauth2-authorize .access .notice { line-height: normal; }\n\n.oauth2-authorize h2 {\n    font-size: 1em;\n    font-weight: normal;\n    color: black;\n}\n\n.oauth2-authorize ul {\n    list-style-type: disc;\n    padding-left: 25px;\n}\n\n.oauth2-authorize .notice {\n    color: #333;\n    font-size: .85em;\n    margin: .5em 0;\n}\n\n.oauth2-authorize .fancybutton {\n    margin: 0;\n    margin-right: 1em;\n    cursor: pointer;\n}\n\n.oauth2-authorize .fancybutton.allow {\n    color: white;\n    background: #ff4500;\n    border-color: #541700;\n    box-shadow: inset 0px 1px 0px rgba(255, 255, 255, .25);\n    text-shadow: 0px 1px 0px rgba(0, 0, 0, .7);\n}\n\n.oauth2-authorize .fancybutton.allow:hover {\n    background: #ff571a;\n}\n\n.oauth2-authorize .fancybutton.allow:active {\n    background: #eb3f00;\n    box-shadow: inset 0px -1px 0px rgba(255, 255, 255, .25);\n}\n\n.oauth2-authorize .fancybutton.decline {\n    color: black;\n    background: #eee;\n    border-color: #555;\n    box-shadow: inset 0px 1px 0px rgba(255, 255, 255, .5);\n    text-shadow: 0px 1px 0px rgba(255, 255, 255, .7);\n}\n\n.oauth2-authorize .fancybutton.decline:hover {\n    background: #f7f7f7;\n}\n\n.oauth2-authorize .fancybutton.decline:active {\n    background: #e4e4e4;\n    box-shadow: inset 0px -1px 0px white;\n}\n\n.modactionlisting table {\n    margin: 0 5px;\n}\n\n.modactionlisting td.timestamp, .modactionlisting td.subreddit {\n    white-space: nowrap;\n}\n\n.modactionlisting td.button {\n    padding-right: 0;\n    padding-left: 1.5em;\n}\n\n.modactionlisting td.description em {\n    font-style: italic;\n}\n\n.modactions td {\n    font-size: small;\n    text-align: left;\n    padding: 2px 10px;\n}\n.modactions.banuser,\n.modactions.unbanuser,\n.modactions.muteuser,\n.modactions.unmuteuser,\n.modactions.removelink,\n.modactions.approvelink,\n.modactions.removecomment,\n.modactions.approvecomment,\n.modactions.addmoderator,\n.modactions.removemoderator,\n.modactions.invitemoderator,\n.modactions.uninvitemoderator,\n.modactions.acceptmoderatorinvite,\n.modactions.addcontributor,\n.modactions.removecontributor,\n.modactions.editsettings,\n.modactions.editflair,\n.modactions.distinguish,\n.modactions.marknsfw,\n.modactions.wikirevise,\n.modactions.wikipermlevel,\n.modactions.wikibanned,\n.modactions.wikiunbanned,\n.modactions.wikicontributor,\n.modactions.wikipagelisted,\n.modactions.removewikicontributor,\n.modactions.ignorereports,\n.modactions.unignorereports,\n.modactions.setpermissions,\n.modactions.setsuggestedsort,\n.modactions.sticky,\n.modactions.unsticky,\n.modactions.setcontestmode,\n.modactions.unsetcontestmode,\n.modactions.lock,\n.modactions.unlock,\n.modactions.createrule,\n.modactions.editrule,\n.modactions.deleterule, {\n    height: 16px;\n    width: 16px;\n    display: block;\n    content: \" \";\n    float: left;\n    margin-right: 5px;\n}\n.modactions.banuser {\n    background-image: url(../modactions_banuser.png); /* SPRITE */\n}\n.modactions.unbanuser {\n    background-image: url(../modactions_unbanuser.png); /* SPRITE */\n}\n.modactions.muteuser {\n    background-image: url(../modactions_mute.png); /* SPRITE */\n}\n.modactions.unmuteuser {\n    background-image: url(../modactions_unmute.png); /* SPRITE */\n}\n.modactions.removelink {\n    background-image: url(../modactions_removelink.png); /* SPRITE */\n}\n.modactions.approvelink {\n    background-image: url(../modactions_approvelink.png); /* SPRITE */\n}\n.modactions.removecomment {\n    background-image: url(../modactions_removecomment.png); /* SPRITE */\n}\n.modactions.approvecomment {\n    background-image: url(../modactions_approvecomment.png); /* SPRITE */\n}\n.modactions.addmoderator,\n.modactions.invitemoderator,\n.modactions.acceptmoderatorinvite {\n    background-image: url(../modactions_addmoderator.png); /* SPRITE */\n}\n.modactions.removemoderator,\n.modactions.uninvitemoderator {\n    background-image: url(../modactions_removemoderator.png); /* SPRITE */\n}\n.modactions.addcontributor {\n    background-image: url(../modactions_addcontributor.png); /* SPRITE */\n}\n.modactions.removecontributor {\n    background-image: url(../modactions_removecontributor.png); /* SPRITE */\n}\n.modactions.wikipagelisted,\n.modactions.editsettings {\n    background-image: url(../modactions_editsettings.png); /* SPRITE */\n}\n.modactions.editflair {\n    background-image: url(../modactions_editflair.png); /* SPRITE */\n}\n.modactions.distinguish {\n    background-image: url(../modactions_distinguish.png); /* SPRITE */\n}\n.modactions.marknsfw {\n    background-image: url(../modactions_marknsfw.png); /* SPRITE */\n}\n.modactions.wikirevise {\n    background-image: url(../modactions_wikirevise.png); /* SPRITE */\n}\n.modactions.wikipermlevel {\n    background-image: url(../modactions_wikipermlevel.png); /* SPRITE */\n}\n.modactions.wikibanned {\n    background-image: url(../modactions_banuser.png); /* SPRITE */\n}\n.modactions.wikiunbanned {\n    background-image: url(../modactions_unbanuser.png); /* SPRITE */\n}\n.modactions.wikicontributor {\n    background-image: url(../modactions_addcontributor.png); /* SPRITE */\n}\n.modactions.removewikicontributor {\n    background-image: url(../modactions_removecontributor.png); /* SPRITE */\n}\n.modactions.ignorereports {\n    background-image: url(../modactions_mute.png); /* SPRITE */\n}\n.modactions.unignorereports {\n    background-image: url(../modactions_unmute.png); /* SPRITE */\n}\n.modactions.setpermissions {\n    background-image: url(../modactions_setpermissions.png); /* SPRITE */\n}\n.modactions.setsuggestedsort {\n    background-image: url(../modactions_setsuggestedsort.png); /* SPRITE */\n}\n.modactions.sticky {\n    background-image: url(../modactions_sticky.png); /* SPRITE */\n}\n.modactions.unsticky {\n    background-image: url(../modactions_unsticky.png); /* SPRITE */\n}\n.modactions.setcontestmode {\n    background-image: url(../modactions_setcontestmode.png); /* SPRITE */\n}\n.modactions.unsetcontestmode {\n    background-image: url(../modactions_unsetcontestmode.png); /* SPRITE */\n}\n.modactions.lock {\n    background-image: url(../modactions_lock.png); /* SPRITE */\n}\n.modactions.unlock {\n    background-image: url(../modactions_unlock.png); /* SPRITE */\n}\n.modactions.createrule {\n    background-image: url(../modactions_createrule.png); /* SPRITE */\n}\n.modactions.editrule {\n    background-image: url(../modactions_editrule.png); /* SPRITE */\n}\n.modactions.deleterule {\n    background-image: url(../modactions_deleterule.png); /* SPRITE */\n}\n\n.adminpasswordform {\n    display: block;\n    margin: .5em auto 0 auto;\n}\n\n.adminpasswordform label {\n    display: block;\n    padding: .5em;\n}\n\n.content.api-help {\n    font-size: 1.25em;\n    margin: 0 auto;\n    max-width: 950px;\n}\n\n.api-help .contents {\n    padding: 0 20px;\n    margin-left: 24em;\n    margin-top: 20px;\n}\n\n.api-help .contents .section {\n    margin-bottom: 2em;\n}\n\n.api-help .sidebar {\n    float: left;\n    margin-left: 10px;\n}\n\n.api-help .sidebar .head {\n    position: relative;\n    background: url(../xray-snoo-head.png) top center no-repeat;\n    height: 188px;\n    margin-bottom: -78px;\n    z-index: 2;\n}\n\n.api-help .sidebar .feet {\n    position: relative;\n    background: url(../xray-snoo-feet.png) top center no-repeat;\n    height: 75px;\n    margin-top: -42px;\n    z-index: 2;\n}\n\n.api-help .toc {\n    background: #181818 url(../xray-snoo-body.png) center repeat-y;\n    border: 5px solid #959595;\n    border-radius: 8px;\n    padding: 15px 2em 0 2em;\n    width: 18em;\n}\n\n.api-help .contents .introduction {\n    position: relative;\n    border: 2px solid #ccc;\n    border-radius: 12px;\n    margin-bottom: -1em;\n}\n\n.api-help .contents .introduction p {\n    margin: 1em 14px;\n}\n\n.api-help .contents .introduction strong {\n    color: #222;\n    font-weight: bold;\n}\n\n.api-help .contents .overview {\n    h3 {\n        margin-top: 1.5em;\n    }\n\n    p {\n        margin: .8em 0;\n    }\n\n    code {\n        background-color: #f0f0f0;\n        padding: 0 .5em;\n        border-radius: 3px;\n    }\n}\n\n.api-help .toc ul {\n    position: relative;\n    margin-top: .5em;\n    margin-bottom: 1.5em;\n    z-index: 10;\n}\n\n.api-help .toc > ul > li > strong {\n    color: #aaa;\n}\n\n.api-help .toc a.section {\n    color: #888;\n    font-weight: bold;\n}\n\n.api-help .toc a {\n    display: block;\n    color: #8EB0D2;\n}\n\n.api-help .toc a:hover, .api-help .endpoint a:hover {\n    text-decoration: underline;\n}\n\n.api-help .toc .mode-selector {\n    display: inline-block;\n    font-size: x-small;\n    border-radius: 5px;\n    border: 1px solid #888;\n    margin-top: 6px;\n    vertical-align: middle;\n}\n\n.api-help .toc .mode-selector .mode {\n    display: inline-block;\n    margin: 2px;\n    padding-top: 2px;\n    padding-bottom: 3px;\n    border-radius: 3px;\n    text-align: center;\n    width: 107px;\n    color: #ddd;\n}\n\n.api-help .toc .mode-selector .mode:hover {\n    background-color: #ccc;\n    color: black;\n    text-decoration: none;\n}\n\n.api-help .toc .mode-selector .mode-current {\n    color: black;\n    background-color: #eee;\n}\n\n.api-help .toc .mode-selector .mode-current:hover {\n    background-color: #ddd;\n}\n\n.api-help em.placeholder {\n    font-style: italic;\n    font-weight: normal;\n}\n\n.api-help .toc em.placeholder {\n    color: #8EB0D2;\n}\n\n.api-help .toc li.supports-oauth a { background: none; }\n.api-help .toc li.supports-oauth a:after {\n    content: 'oauth';\n    display: inline-block;\n    position: absolute;\n    right: 0;\n    font-size: .75em; \n    background: #29440e;\n    color: #ddc;\n    padding: 0 2px;\n    margin-left: 2px;\n    border-radius: 2px;\n}\n\n.api-help .endpoint em.placeholder {\n    color: #369;\n}\n\n.api-help .endpoint, .api-help .section .description {\n    margin-bottom: 1.5em;\n}\n\n.api-help .oauth-scope-list { display: inline; margin-left: 1em; }\n\n.api-help .api-badge {\n    display: inline-block;\n    margin-left: 0.5em;\n    font-size: .75em;\n    font-weight: normal;\n    vertical-align: bottom;\n    color: #fbfbf9;\n    padding: 2px 6px;\n    border-radius: 2px;\n}\n\n.api-badge.oauth-scope {\n    background: #577439;\n}\n\n.api-badge.rss-support {\n    background: #f38f35;\n}\n\n.api-help .overview h2,\n.api-help .methods h2 {\n    color: black;\n    font-size: 1.45em;\n    text-align: middle;\n    margin-top: 1.5em;\n    margin-bottom: 1em;\n    border-bottom: 1px solid #aaa;\n}\n\n.api-help .methods h2 .scope-id {\n    margin-left: 1em;\n    font-size: small;\n    font-weight: normal;\n    font-style: italic;\n}\n\n.api-help .endpoint .info {\n    padding-left: 1em;\n    border-left: 1px solid #ddd;\n}\n\n.api-help .endpoint h3, .api-help .endpoint .uri-variants {\n    color: #369;\n    margin-bottom: .5em;\n}\n\n.api-help .endpoint .uri-variants {\n    opacity: .85;\n    font-weight: bold;\n    margin-top: -.5em;\n    margin-left: 3em;\n}\n\n.api-help .endpoint .method, .api-help .endpoint .extensions {\n    font-weight: normal;\n    color: gray;\n}\n\n.api-help .endpoint .extensions {\n    margin-left: .5em;\n}\n\n.api-help .endpoint .links {\n    float: right;\n}\n\n.api-help .endpoint .links a {\n    margin-left: .85em;\n    opacity: .45;\n}\n\n.api-help .endpoint:hover .links a {\n    opacity: 1;\n}\n\n.api-help .parameters {\n    background: #f0f0f0;\n    border-collapse: separate;\n    border-radius: 3px;\n    padding: 5px 10px;\n    border-spacing: 0;\n    width: 100%;\n}\n\n.api-help caption {\n    font-weight: bold;\n    margin: 1em 0 .5em .5em;\n}\n\n.api-help .parameters th,\n.api-help .parameters td {\n    vertical-align: top;\n    border-bottom: 1px dotted #ccc;\n    padding: 5px 0;\n    margin: 0;\n}\n\n.api-help .parameters tr:last-child th,\n.api-help .parameters tr:last-child td {\n    border: none;\n}\n\n.api-help .parameters th {\n    font-family: 'Courier New', monospace;\n    line-height: 1.6;\n    width: 30%;\n    padding-right: 10px;\n}\n\n.api-help .parameters td pre {\n    margin: .5em 0;\n}\n\n.api-help .parameters code {\n    white-space: pre-wrap;\n}\n\n#classy-error {\n    text-align: center;\n}\n\n.errorpage-message {\n    margin: 1em auto;\n    width: 500px;\n    font-size: small;\n}\n\n.errorpage-message.sr-description {\n    border-top: 1px solid black;\n    margin-top: 2em;\n    padding-top: 2em;\n}\n\n.errorpage-message.sr-description h2 {\n    color: black;\n    font-weight: bold;\n    font-size: 125%;\n    margin-bottom: .7em;\n}\n\n.sr-description p {\n    margin: .75em 0;\n}\n\n#private-subreddit-message-link {\n  border-top: 1px solid black;\n  margin-top: 1em;\n  padding-top: 2em;\n  font-size: 1.4em;\n}\n\n/** one-time password stuff **/\n#pref-otp .roundfield {\n    margin: 1em 0;\n}\n\n#pref-otp-qr {\n    display: none;\n}\n\n#otp-secret-info {\n    margin: 2em;\n    width: 512px;\n    font-size: small;\n}\n\n#otp-secret-info div {\n    margin: 1em 0;\n}\n\n#otp-secret-info .secret {\n    font-weight: bold;\n}\n\n.users-online {\n    margin-bottom: .25em;\n}\n\n.users-online .word, .users-online .number:after {\n    cursor: help;\n}\n\n.sr-interest-bar {\n    position: relative;\n    background: #cee3f8 url(../snoo-upside-down.png) 15px top no-repeat;\n    padding: 5px;\n    overflow: hidden;\n    border: 1px solid #336699;\n    margin-bottom: 10px;\n}\n\n.organic-listing .sr-interest-bar {\n    border: none;\n    margin: 0;\n}\n\n.sr-interest-bar .bubble {\n    position: relative;\n    margin-left: 85px;\n    margin-right: 68px;\n    max-width: 700px;\n    font-size: 13px;\n    background: white;\n    padding: 6px;\n    border-radius: 8px;\n}\n\n.sr-interest-bar .bubble:after {\n    position: absolute;\n    display: block;\n    content: '';\n    border: 10px solid;\n    border-style: solid solid outset;\n    border-color: transparent;\n    border-right-color: white;\n    left: -20px;\n    top: 15px;\n}\n\n.sr-interest-bar .bubble p {\n    margin: 6px 3px;\n    margin-top: 0;\n}\n\n.sr-interest-bar .subscribe {\n    background-image: url(../bg-button-add.png); /* SPRITE stretch-x */\n    border: 1px solid #444;\n    border-radius: 3px;\n    padding: 0 6px;\n    color: white;\n    font-weight: bold;\n}\n\n.sr-interest-bar .query-box {\n    position: relative;\n    padding: 2px 4px;\n    border: 2px solid #979797;\n    border-radius: 5px;\n}\n\n.sr-interest-bar.focus .query-box {\n    border-color: #5f99cf;\n}\n\n.sr-interest-bar.error .query-box {\n    border-color: #cf5e5e;\n}\n\n.sr-interest-bar .error-caption, .sr-interest-bar.error .caption {\n    display: none;\n}\n\n.sr-interest-bar.error .error-caption {\n    display: block;\n}\n\n.sr-interest-bar .query {\n    width: 100%;\n    font-size: 20px;\n    margin: 0;\n    padding: 0;\n    border: none;\n    outline: none;\n}\n\n.sr-interest-bar .throbber {\n    position: absolute;\n    right: 3px;\n    top: 5px;\n}\n\n.sr-interest-bar ul.results {\n    margin: 0;\n    margin-top: 6px;\n    padding-top: 2px;\n    border-top: 1px dotted #bbb;\n    display: none;\n}\n\n.sr-interest-bar li {\n    display: inline-block;\n    margin: 6px 3px;\n}\n\n.sr-interest-bar a {\n    padding: 1px 2px;\n}\n\n.sr-interest-bar a:hover {\n    text-decoration: underline;\n}\n\n.sr-interest-bar .results .random {\n    color: gray;\n    font-weight: bold;\n}\n\n.ajax-upload-form iframe { display: none; }\n\n.developed-app, .authorized-app {\n    border: solid 1px black;\n    margin-left: 20px;\n    margin-bottom: 0.5em;\n    padding: 7px;\n    position: relative;\n    width: 880px;\n    font-size: x-small;\n}\n\n.developed-app.collapsed, .authorized-app { min-height: 100px; }\n.developed-app .collapsed { display: none; }\n\n.developed-app .ajax-upload-form {\n    display: none;\n}\n\n.app-details {\n    display: inline-block;\n    width: 200px;\n    min-height: 72px;\n    margin-left: 1em;\n    vertical-align: top;\n}\n\n.app-details h2 { font-size: medium; margin: 0px; }\n.app-details h3 { font-size: x-small; margin: 0px; }\n\n.app-icon {\n    display: inline-block;\n    width: 72px;\n    height: 72px;\n    line-height: 72px;\n    text-align: center;\n    white-space: nowrap;\n}\n\n.app-icon img {\n    vertical-align: middle;\n}\n\n.app-permissions li { position: relative; }\n.app-permissions-details { margin-top: 1em; }\n\n.app-scope {\n    display: none;\n    position: absolute;\n    top: 1ex;\n    left: 3ex;\n    border: 1px solid black;\n    background: #fffdcc;\n    z-index: 1;\n}\n\n.app-description {\n    display: inline-block;\n    font-size: small;\n    width: 597px;\n    height: 80px;\n    overflow-y: auto;\n    vertical-align: top;\n}\n\n.app-developers {\n    position: absolute;\n    left: 289px;\n    bottom: 1ex;\n    width: 600px;\n}\n\n.edit-app-button, .revoke-app-button {\n    position: absolute;\n    bottom: 1ex;\n    left: 12px;\n    width: 200px;\n}\n\n.edit-app.collapsed, .edit-app-icon, .developed-app .collapsed {\n    display: none;\n}\n\n.edit-app-icon-button { display: block; text-align: center; width: 72px; }\n.edit-app-form, .edit-app-form form { display: inline-block; }\n.edit-app-form th, .edit-app-icon th { width: 12ex; }\n.edit-app-form input.text { margin: 0px; width: 50ex; }\n.edit-app-form input[name=\"name\"] { width: 20ex !important; }\n.edit-app-form input[type=\"file\"] { width: auto !important; }\n.edit-app-form input[type=\"submit\"] {\n    margin-left: 10px;\n    width: auto !important;\n}\n\n.delete-app-button {\n    position: absolute;\n    bottom: 7px;\n    left: 100px;\n}\n\n#create-app { display: none; }\n\ntable.diff {font-size: small;}\n.diff_header {background-color:lightgrey}\n.diff_next {background-color:lightgrey}\n.diff_add {background-color:lightgreen}\n.diff_chg {background-color:yellow}\n.diff_sub {background-color:lightcoral}\n\n.gilded-icon {\n    position: relative;\n    display: inline-block;\n    margin: 0 0 -15px 8px;\n    top: -8px;\n    color: #99895F;\n    font-size: .9em;\n    vertical-align: middle;\n}\n\n.gilded-icon:before {\n    display: inline-block;\n    content: '';\n    background-image: url(../gold-coin.png);  /* SPRITE */\n    background-repeat: no-repeat;\n    height: 14px;\n    width: 13px;\n    margin-right: 2px;\n    vertical-align: -3px;\n}\n\n.user-gilded > .entry .gilded-icon:before {\n    width: 23px;\n}\n\nbody.post-under-6h-old .gilded-icon {\n    opacity: .55;\n}\n\n.gold-progress (@bar-height: 17px) {\n    padding: (@bar-height / 2 - 1) 0;\n\n    .bar {\n        border: 1px solid #dad0b3;\n        height: @bar-height;\n        overflow: auto;\n\n        border-radius: 10px;\n\n        span {\n            display: block;\n            height: 100%;\n            background-color: #f3e287;\n            background-image: -webkit-linear-gradient(top, #fff8ba, #eccf90);\n            background-image: linear-gradient(to bottom, #fff8ba 0%, #eccf90 100%);\n            border-radius: (@bar-height/2);\n        }\n    }\n\n    p {\n        float: right;\n        font-weight: bold;\n        font-size: @bar-height - 2;\n        color: #5a3f1a;\n        line-height: @bar-height + 2; // +2 to match border on bar\n        margin-left: 6px;\n        margin-top: 0;\n    }\n}\n\n.goldvertisement {\n    @border-color: #c4b487;\n    @background-shadow: #dad0b3;\n\n    border: 1px solid @border-color;\n    text-align: center;\n    line-height: 1.3em;\n    box-shadow: 0 0 10px @background-shadow inset;\n    color: darken(@border-color, 40%);\n\n    .inner {\n        margin: 1px;\n        border: 1px solid #dbd1b5;\n        padding: 6px 8px;\n    }\n\n    li {\n        display: inline-block;\n        margin-right: 2em;\n    }\n\n    h2 {\n        margin: 0;\n        font-weight: normal;\n        color: inherit;\n    }\n\n    .progress {\n        .gold-progress\n    }\n\n    a {\n        display: inline-block;\n        margin: 0;\n        padding: 2px 4px;\n        border-radius: 3px;\n        background: lighten(@background-shadow, 20%);\n        border: 1px solid lighten(@border-color, 10%);\n        border-bottom-width: 2px;\n        color: darken(@border-color, 40%);\n\n        &:hover {\n            background: #fdf4c5;\n        }\n\n        &:active {\n            margin-top: 1px;\n            border-bottom-width: 1px;\n        }\n    }\n}\n\n.gold-bubble {\n    width: 290px;\n    border-radius: 4px;\n    font-size: 125%;\n    line-height: 1.13;\n    font-family: \"Hoefler Text\",\"Palatino Linotype\",\"Book Antiqua\",\n                  Palatino,georgia,garamond,FreeSerif,serif;\n    border-color: #907c47;\n    padding: 4px;\n\n    &.anchor-top-centered:before {\n        border-bottom-color: #907c47;\n    }\n\n    p + p {\n        margin-top: 1em;\n    }\n\n    span.gold-branding {\n        display: inline-block;\n        vertical-align: bottom;\n        text-indent: -9999px;\n        background: transparent url(../gold/goldvertisement-logo.png) top left no-repeat;\n        width: 79px;\n        height: 18px;\n        margin-right: 1px;\n    }\n\n    p.buy-gold {\n        background: transparent url(../gold/goldvertisement-gold.png) top left no-repeat;\n        margin-left: 13px;\n        padding-left: 67px;\n        min-height: 45px;\n\n        a {\n            color: #825b25;\n        }\n    }\n\n    p.give-gold {\n        background: transparent url(../gold/goldvertisement-gild.png) top left no-repeat;\n        margin-left: 23px;\n        padding-left: 57px;\n        min-height: 39px;\n    }\n\n    p.aside {\n        color: #777;\n        font-style: italic;\n\n        a {\n            color: inherit;\n        }\n    }\n\n    div.history {\n        margin: 5px 0;\n        padding-top: 2px;\n        border-top: 1px solid #e2ddcf;\n        \n        p {\n            margin-bottom: 0;\n        }\n\n        .progress {\n            .gold-progress(12px);\n            margin: 0 7px;\n            opacity: 0.8;\n\n            p {\n                margin-right: 0;\n                font-weight: normal;\n            }\n        }\n    }\n}\n\n#stripe-payment th {\n    padding:5px; \n    vertical-align:top;\n    text-align:right;\n    white-space:nowrap;\n    font-size:smaller;\n}\n#stripe-payment {\n  padding-top: 15px;\n\n    .credit-card-amount, .credit-card-interval {\n        text-align: left;\n    }\n}\n#stripe-payment th label { display:inline; }\n#stripe-payment td input {\n    font-size:small;\n    width:200px;\n}\n\n#stripe-payment input.card-cvc { width:9ex; }\n#stripe-payment input.card-address_zip { width:13ex; }\n\n.stripe-note a.icon {\n    position: relative;\n    float: left;\n    text-indent: -9999px;\n    margin-right: 10px;\n    width: 119px;\n    height: 33px;\n    background-image: url(../stripe.png);\n}\n\n.stripe-note div {\n    float: left;\n    width: 250px;\n    font-size: small;\n}\n\n.gold-subscription {\n    font-size: small;\n    padding: 2px;\n\n    div.buttons {\n        padding: 10px 0;\n    }\n\n    .cancel-button, .edit-button {\n        margin: 5px;\n        display: inline;\n    }\n\n    .status, .error {\n        font-size: small;\n        margin: 0;\n    }\n\n    .roundfield {\n        background-color: #fffdd7;\n        width: 400px;\n    }\n\n    #stripe-cancel {\n        display: inline;\n    }\n}\n\n.permissions {\n    display: inline-block;\n    font-size: small;\n    text-align: right;\n    width: 36ex;\n}\n#moderator_invite .permissions { width: 30ex; }\n.permissions > form { display: none; }\n\n.permission-summary {\n    display: inline-block;\n    font-size: small;\n    border: 1px solid white;\n}\n.permission-summary.edited { border: dashed 1px black; }\n.permission-bit.added { font-weight: bold; }\n.permission-bit.removed { text-decoration: line-through; }\n.permission-bit.none { font-style: italic; }\n\n.permissions-edit { font-size: x-small; }\n\n.permission-selector {\n    border: 1px solid black;\n    background-color: white;\n    position: absolute;\n    width: 24ex;\n}\n.permission-selector.active { display: block; }\n.permission-selector label {\n    display: block; text-align: left;\n    padding: 0px 2px 1px 2px;\n}\n.permission-selector label:first-child { border-bottom: 1px solid black; }\n.permission-selector label:hover { background-color: #bbb; }\n.permission-selector label.disabled { background-color: #ddd; }\n.permission-selector form { text-align: right; }\n.permission-selector .status, .permission-selector .error {\n    text-align: left;\n    white-space: normal;\n}\n\n.light-button {\n    background: none;\n    border: 1px solid #777;\n    border-radius: 3px;\n    box-shadow: 0 1px 1px rgba(0, 0, 0, .25);\n    opacity: .75;\n\n    &:active {\n        position: relative;\n        top: 1px;\n        box-shadow: none;\n    }\n}\n\n.light-text-input {\n    background: white;\n    border: 1px solid #ccc;\n    padding: 2px 5px;\n}\n\n@lc-width: 130px;\n@lc-shadow-width: 6px;\n@lc-grippy-width: 8px;\nbody.with-listing-chooser {\n    @grippy-image-width: 6px;\n    @grippy-image-height: 65px;\n    @grippy-fudge: 6px;  // extra width for easy targeting\n    position: relative;\n\n    & #header .tabmenu {\n        margin-left: @lc-grippy-width;\n        li:first-child.selected {\n            margin-left: 2px;\n        }\n    }\n\n    & #header .pagename {\n        position: absolute;\n        bottom: 20px;\n        margin-left: @lc-grippy-width + 2px;\n    }\n\n    & > .content, & .footer-parent {\n        margin-left: 140px + @lc-grippy-width;\n    }\n\n    .listing-chooser {\n        position: absolute;\n        top: 65px;\n        left: 0;\n        bottom: 0;\n        width: @lc-width;\n        padding-right: @lc-grippy-width + @grippy-fudge;\n        background: #f7f7f7;\n        overflow: hidden;\n\n        &.initialized {\n            .transition(width, .25s);\n\n            .grippy, .grippy:before, .grippy:after {\n                // the 0.03s delay here is intended to prevent flashes when the\n                // mouse is passing over en route to a tab.\n                .transition(all, 0.1s, ease, 0.03s);\n            }\n        }\n\n        .grippy {\n            position: absolute;\n            right: 0;\n            width: @lc-grippy-width + @grippy-fudge;\n            height: 100%;\n            background: white;\n            border-left: 1px solid #ccc;\n            box-shadow: 0 0 @lc-shadow-width rgba(0, 0, 0, .2);\n            z-index: 25;\n            cursor: pointer;\n\n            &:before {\n                content: '';\n                display: block;\n                position: absolute;\n                width: @lc-grippy-width;\n                height: 100%;\n                background: url(../sidebar-grippy-hide.png) fixed no-repeat;\n                background-position: (@lc-width + (@lc-grippy-width - @grippy-image-width) / 2) center;\n                margin-left: (@lc-grippy-width - @grippy-image-width) / 2;\n                opacity: .5;\n            }\n\n            &:after {\n                content: '';\n                display: block;\n                position: absolute;\n                height: 100%;\n                right: @grippy-fudge - 1;\n                width: @lc-grippy-width;\n                border-right: 1px dotted #e5e5e5;\n                z-index: -1;\n            }\n\n            &:hover {\n                &:before {\n                    opacity: 1;\n                }\n\n                &:after {\n                    background: #f4f4f4;\n                }\n            }\n        }\n\n        &:hover {\n            .grippy:before {\n                opacity: .8;\n            }\n        }\n    }\n\n    &.listing-chooser-collapsed {\n        @lc-grippy-width: 9px;\n\n        & #header .tabmenu {\n            margin-left: 0;\n        }\n\n        & #header .pagename {\n            margin-left: 2px;\n        }\n\n        & > .content, & .footer-parent {\n            margin-left: @lc-grippy-width + 6px;\n        }\n\n        .listing-chooser {\n            width: 0;\n            padding-right: @lc-grippy-width + @grippy-fudge;\n            z-index: -1;\n\n            .grippy {\n                z-index: 40;\n                width: @lc-grippy-width + @grippy-fudge;\n\n                &:before {\n                    background-image: url(../sidebar-grippy-show.png);\n                    background-position: ((@lc-grippy-width - @grippy-image-width) / 2 + 1) center;\n                    margin-left: (@lc-grippy-width - @grippy-image-width) / 2;\n                    width: @lc-grippy-width;\n                }\n\n                &:after {\n                    right: @grippy-fudge - 1;\n                    width: @lc-grippy-width;\n                    border-right: 1px solid #ccc;\n                }\n            }\n        }\n    }\n}\n\n.listing-chooser {\n    h3 {\n        color: #777;\n        text-align: right;\n        padding: 4px;\n    }\n\n    .intro {\n        background: lighten(#f6e69f, 5%);\n        border: 1px solid lighten(orange, 5%);\n        border-left: none;\n        border-right: none;\n        margin-bottom: 10px;\n\n        // set a static width so text doesn't wrap upon bar collapse\n        width: @lc-width;\n\n        p {\n            font-size: 1.15em;\n            margin: 4px;\n            margin-left: 8px;\n        }\n\n        ul.multis {\n            margin: 6px 0;\n        }\n    }\n\n    ul.global, ul.other {\n        padding: 8px 0;\n        li {\n            margin-left: 4px;\n\n            a {\n                font-size: 1.3em;\n                padding: 1em 5px;\n                padding-left: 12px;\n            }\n        }\n    }\n\n    ul.other {\n        margin-top: 10px;\n    }\n\n    ul.multis {\n        li {\n            margin-left: 12px;\n            .transition(all, 0.15s);\n\n            &:hover {\n                margin-left: 9px;\n            }\n\n            a {\n                font-size: 1.2em;\n                padding: .8em 5px;\n                padding-left: 10px;\n            }\n        }\n    }\n\n    li {\n        text-align: left;\n        margin-bottom: 3px;\n        background: #fff;\n        border: 1px solid #ccc;\n        border-bottom-width: 2px;\n        border-right: none;\n        border-top-left-radius: 5px;\n        border-bottom-left-radius: 5px;\n\n        a {\n            display: block;\n            position: relative;\n            overflow: hidden;\n            text-overflow: ellipsis;\n            margin-right: 5px;\n\n            .description {\n                color: gray;\n                font-size: .8em;\n                font-weight: normal;\n                white-space: nowrap;\n            }\n        }\n\n        &:last-child a {\n            border-bottom: none;\n        }\n\n        &.selected {\n            position: relative;\n            background: lighten(#cee3f8, 6%);\n            border-color: lighten(#369, 40%);\n            margin-right: -@lc-grippy-width;\n            padding-right: @lc-grippy-width;\n            box-shadow: -30px 0 30px -15px rgba(255, 255, 255, .5) inset,\n                        0 2px @lc-shadow-width -1px rgba(0, 0, 0, .2);\n            z-index: 35;\n\n            a {\n                font-weight: bold;\n            }\n\n            &:before {\n                @size: 5px;\n                position: absolute;\n                top: 50%;\n                right: 0;\n                margin-top: -@size;\n                display: block;\n                content: '';\n                border: @size solid ~\"transparent\";\n                border-style: solid solid outset; // mitigates firefox drawing a thicker arrow\n                border-left-color: lighten(#369, 25%);\n            }\n        }\n\n        &.gold-perks {\n            background: #fdfbf2;\n            a {\n                color: #9a7d2e;\n            }\n\n            &.selected {\n                border-color: lighten(#c4b487, 6%);\n\n                &:before {\n                    border-left-color: lighten(#9a7d2e, 15%);\n                }\n            }\n        }\n    }\n\n\n    .create {\n        padding: 5px;\n\n        input[type=\"text\"] {\n            .light-text-input;\n            width: 95px;\n            margin-bottom: 3px;\n            display: none;\n        }\n\n        .error {\n            margin: 4px 0;\n            width: 100px;\n        }\n\n        button {\n            display: inline;\n            text-align: center;\n            padding: 1px 4px;\n            margin: 0;\n\n            background: none;\n            border: 1px solid #777;\n            border-radius: 3px;\n            opacity: .5;\n\n            &:hover {\n                opacity: .90;\n            }\n\n            &:active {\n                background: #e9e9e9;\n            }\n        }\n\n        button, .throbber {\n            vertical-align: middle;\n        }\n\n        .throbber {\n            float: right;\n        }\n\n        &.expanded {\n            input[type=\"text\"] {\n                display: block;\n            }\n\n            button {\n                .light-button;\n            }\n        }\n    }\n}\n\n.user-jumped-to {\n    border-radius: 5px;\n    -moz-border-radius: 5px;\n    border: 1px solid #DDF;\n    display: inline-block;\n    margin-top: 10px;\n    padding: 10px 15px;\n    background-color: #EEF;\n}\n\n.submit_text {\n    display: none;\n    max-height: 250px;\n    overflow: auto;\n\n    ol, ul {\n        margin: 0;\n        margin-left: 2em;\n    }\n\n    &.working .content:before {\n        content: \"\";\n        width: 16px;\n        height: 16px;\n        display: block;\n        background-image: url(../throbber.gif); \n    }\n\n    h1 {\n        color: rgb(51, 102, 153);\n        display: block;\n        font-size: 16px;\n        font-weight: bold;\n    }\n\n    .content {\n        margin: 0;\n\n        p {\n            word-wrap: break-word;\n            clear: both;\n        }\n    }\n\n    &.enabled {\n        display: inline-block;\n    }\n}\n\n.hover-bubble.save-selector  {\n    label {\n        display: block;\n        font-weight: bold;\n        margin-left: 5px;\n        font-size: 10px;\n    }\n    .savedcategory {\n        border: 1px solid #ccc;\n        padding: 1px 3px;\n        margin: 0 2px;\n    }\n    display: none;\n}\n\n.save-category {\n    &.hidden {\n        display: none;\n    }\n    margin-left: 2px;\n    background-color: #DDF;\n    padding: 2px 5px;\n    border-radius: 5px;\n    -moz-border-radius: 5px;\n}\n\n#adminnotes-form {\n    textarea {\n        width: 285px;\n    }\n\n    .notes-button {\n        margin: 3px 0px;\n        display: block;\n    }\n}\n\n#past-notes {\n    overflow-y: auto;\n    max-height: 150px;\n\n    li.adminnote {\n        border-top: 1px solid black;\n        overflow-x: auto;\n    }\n\n    .adminnote-info {\n        text-align: right;\n        font-size: small;\n        font-style: italic;\n    }\n}\n\n.trending-subreddits {\n    margin-top: -2px;\n    margin-bottom: 8px;\n    line-height: 1.75em;\n\n    // the combined padding/margins of .link and .midcol\n    margin-left: 17px;\n    body.compressed-display & {\n        margin-left: 15px;\n\n        .midcol-spacer {\n            width: 15px;\n        }\n    }\n\n    // hax to align trending subreddits list with links/thumbnails\n    // this uses the overflow:hidden trick to create a block formatting context\n    // to shift the left edge of the trending subreddits list over\n    .rank-spacer, .midcol-spacer {\n        float: left;\n        height: 1px;\n    }\n\n    .trending-subreddits-content {\n        overflow: hidden;\n    }\n\n    strong {\n        color: #29541c;\n\n        &:before {\n            height: 14px;\n            width: 14px;\n            display: inline-block;\n            content: \" \";\n            margin-right: 5px;\n            background-image: url(../trending.png); /* SPRITE */\n            vertical-align: middle;\n        }\n    }\n\n    ul {\n        display: inline;\n    }\n\n    li {\n        display: inline-block;\n        margin-left: 0.5em;\n    }\n\n    li:first-child {\n        margin-left: 0;\n    }\n\n    ul, .comments {\n        margin-left: 1em;\n    }\n\n    .comments {\n        color: #888;\n        font-weight: bold;\n        white-space: nowrap;\n\n        &:hover {\n            text-decoration: underline;\n        }\n    }\n}\n\n/* Responsive gold paper-style fancy borders with endcaps */\n.fancy {\n  @endcap-height: 27px;\n  @endcap-overhang-width: 4px;\n  @endcap-width: 27px;\n  @fancy-bg-color: #FFFCFC;\n  @inner-padding-width: 12px;\n\n  background-color: @fancy-bg-color;\n  border: 2px solid #D4D3CF;\n  margin: 10px auto;\n  max-width: 974px;\n  padding: 0;\n  position: relative;\n  width: 100%;\n\n  .fancy-inner {\n    background-image: url(../gold/gold-laurel-bg.png);\n    background-position: top center;\n    background-repeat: no-repeat;\n    border: 1px solid #e3e2df;\n    margin: @inner-padding-width;\n    padding: 0;\n    position: relative;\n  }\n\n  // end caps\n  &:before,\n  &:after,\n  .fancy-inner:before,\n  .fancy-inner:after {\n    background-image: url(../gold/endcap.png);\n    background-repeat: no-repeat;\n    background-size: 27px 27px;\n    content: '';\n    display: block;\n    height: @endcap-height;\n    position: absolute;\n    width: 100%;\n  }\n\n  // top right\n  &:before {\n    .transform(scaleX(-1));\n    background-position: top left; // mirrored x\n    margin-left: @endcap-overhang-width - 1px;\n    margin-top: -@endcap-overhang-width + 1px;\n  }\n\n  // bottom left\n  &:after {\n    .transform(scaleY(-1));\n    background-position: top left; // mirrored y\n    margin-left: -@endcap-overhang-width + 1px;\n    margin-top: @endcap-overhang-width - @endcap-height - 1px;\n  }\n\n  // top left\n  .fancy-inner:before {\n    background-position: top left;\n    margin-left: -@endcap-overhang-width - @inner-padding-width;\n    margin-top: -@endcap-overhang-width - @inner-padding-width;\n  }\n\n  // bottom right\n  .fancy-inner:after {\n    .transform(scaleY(-1) scaleX(-1));\n    background-position: top left; // mirrored xy\n    margin-left: @endcap-overhang-width + @inner-padding-width;\n    margin-top: @inner-padding-width + @endcap-overhang-width - @endcap-height;\n  }\n\n  .fancy-content {\n    margin: 30px auto;\n    max-width: 600px;\n    padding: 3px;\n  }\n}\n\n// Show nice little fancy lines on the side of a header.\n// Adapted from http://css-tricks.com/line-on-sides-headers/\n.sidelines {\n  overflow: hidden;\n  text-align: center;\n  font-size: 1.75em;\n  color: #444;\n  font-weight: bold;\n  line-height: 1.6;\n\n  span {\n    display: inline-block;\n    position: relative;\n\n    &:before,\n    &:after {\n      content: '';\n      position: absolute;\n      border: 0 solid #ccc;\n      height: 1px;\n      border-top-width: 1px;\n      top: 50%;\n      width: 600px;\n    }\n\n    &:before {\n      right: 100%;\n      margin-right: 15px;\n    }\n\n    &:after {\n      left: 100%;\n      margin-left: 15px;\n    }\n  }\n}\n\n.gold-page,\n.gilding {\n    overflow-y: scroll;\n}\n\n.gold-wrap {\n  @dark: #686868;\n  @light: #cccccb;\n  @off-white: #fffdfd;\n\n  font-family: @gold-fonts;\n  color: @dark;\n  font-size: 1.5em;\n  line-height: 1.6em;\n\n  &.inline-gold {\n    margin: 10px 0;\n\n    h1 {\n      display: none;\n    }\n\n    .fancy-inner {\n      background-position: bottom center;\n    }\n\n    .gold-snoo {\n      display: none;\n    }\n\n    .gold-form {\n      margin: 0 20px 5px;\n      font-size: 1em;\n\n      .gold-button {\n        font-size: 1em;\n      }\n\n      .container {\n        padding: 0;\n        border: none;\n        background-color: transparent;\n      }\n\n      .transaction-summary {\n        padding-bottom: 0;\n      }\n    }\n  }\n\n  .gold-banner {\n    background: transparent url(../gold/reddit-golds.png) center center no-repeat;\n    background-size: contain;\n    height: 80px;\n    margin: 30px auto 20px;\n    text-indent: -9999px;\n    text-align: center;\n    max-width: 500px;\n\n    a {\n      display: block;\n      height: 100%;\n      width: 100%;\n    }\n  }\n  \n  .container {\n    padding: 10px 30px;\n    border: 1px solid @light;\n    background-color: @off-white;\n\n    a {\n      color: @dark;\n      text-decoration: underline;\n    }\n  }\n\n  .tab {\n    display: none;\n\n    &.active {\n      display: block;\n    }\n  }\n\n  .error {\n    background: transparent center left no-repeat;\n    background-image: url(../icon-circle-exclamation.png);  /* SPRITE */\n    padding-left: 20px;\n    white-space: nowrap;\n    line-height: 1;\n  }\n\n  #form-options section {\n    padding: 10px 0;\n  }\n\n  .tab-chooser {\n    margin-bottom: 10px;\n    width: 100%;\n    display: inline-block;\n\n    h3 {\n      text-align:  center;\n      font-weight: normal;\n      font-size: 1em;\n      font-style: italic;\n      margin: 0;\n      line-height: 3em;\n    }\n  }\n\n  a.tab-toggle {\n    .box-sizing(border-box);\n    background-color: @gold-button-bg;\n    border-radius: 0px;\n    border-top: @gold-button-border;\n    border-bottom: @gold-button-border;\n    border-right: @gold-button-border;\n    box-shadow: inset 0 -3px 0 rgba(0, 0, 0, 0.27);\n    color: @off-white;\n    display: inline-block;\n    float: left;\n    font-family: @gold-fonts;\n    font-size: 1.1em;\n    height: 66px;\n    line-height: 66px;\n    text-align: center;\n    text-decoration: none;\n    vertical-align: middle;\n    width: 33%;\n\n    &.active {\n      background-color: @gold-button-active;\n      box-shadow: inset 0px 0px 14px rgba(0,0,0,0.27);\n    }\n\n    &:not(.active):hover {\n      background-color: @gold-button-hover;\n      box-shadow: inset 0 -3px 0 rgba(0, 0, 0, 0.27);\n    }\n\n    &:first-of-type {\n      border-radius: 3px 0 0 3px;\n      border-left: @gold-button-border;\n    }\n\n    &:last-of-type {\n        border-right-width: 1px;\n        border-radius: 0 3px 3px 0;\n    }\n  }\n\n  .buttons {\n    margin-top: 10px;\n  }\n\n  h2 {\n    color: @dark;\n  }\n\n  h3 {\n    font-size: 1.1em;\n    color: @dark;\n    margin-bottom: 10px;\n  }\n\n  h3.toggle, dt.toggle {\n    cursor: pointer;\n    margin-bottom: 0;\n  }\n\n  dt.toggle:before {\n    content: \"[+] \";\n  }\n\n  dt.toggle.toggled:before {\n    content: \"[–] \";\n  }\n\n  section#redeem-a-code {\n    margin-top: -10px;\n\n    .sidelines {\n      font-size: 1em;\n      font-weight: normal;\n      padding: 0.7em;\n      color: #686868;\n    }\n  }\n\n  .gold-payment {\n    .gift-message {\n      background-color: #fff;\n      margin: 10px 30px;\n      padding: 0 10px;\n      border: 1px solid #cccccb;\n    }\n\n    .transaction-summary {\n      padding-bottom: 10px;\n\n      p {\n        padding: 5px 0;\n\n        strong {\n            font-weight: bold;\n        }\n      }\n\n      blockquote {\n        font-size: 0.8em;\n        font-style: italic;\n      }\n    }\n\n    .divider-text {\n      font-weight: bold;\n      font-size: 1.75em;\n      color: #444;\n    }\n\n    .status {\n        margin: 5px 0 0 0;\n    }\n  }\n\n  span.gold-snoo {\n    background: transparent url(../gold/gold-snoo.png) center center no-repeat;\n    background-size: 100px;\n    position: absolute;\n    right: 160px;\n    margin-top: -85px;\n    width: 100px;\n    height: 171px;\n    text-indent: -9999px;\n  }\n\n  .login-note {\n    text-align: center;\n    font-size: 13px;\n    font-style: italic;\n    line-height: 1;\n    margin-bottom: 20px;\n\n    a {\n      text-decoration: underline;\n    }\n  }\n\n  section.gold-question {\n    margin-top: 20px;\n\n    h3.toggle {\n      font-weight: normal;\n      font-size: 0.9em;\n\n      &:before {\n        content: \"[+] \";\n      }\n\n      &.toggled:before {\n        content: \"[–] \";\n      }\n    }\n  }\n\n  section#give-as-gift {\n    padding-top: 10px;\n  }\n\n  input[type=checkbox], input[type=radio] {\n    margin: 0 0.5em 0 0;\n  }\n\n  input[type=text].inline, input[type=email].inline {\n    font-size: 0.9em;\n    margin: 2px 5px 5px 5px;\n  }\n\n  input[type=text], input[type=email], textarea {\n    .box-sizing(border-box);\n    border: 1px solid @light;\n    color: @dark;\n    background-color: #fff;\n    font-family: @gold-fonts;\n    font-style: italic;\n    padding: 5px;\n    font-size: 1em;\n  }\n\n  input[name=\"code\"] {\n    width: 100%;\n    padding: 10px;\n  }\n\n  input:focus::-webkit-input-placeholder,\n  textarea:focus::-webkit-input-placeholder,\n  input:focus:-moz-placeholder,\n  textarea:focus:-moz-placeholder,\n  input:focus:-ms-input-placeholder,\n  textarea:focus:-ms-input-placeholder {  \n    opacity: 0.3;\n  }\n\n  .hidden {\n    display: none;\n  }\n\n  .gift-details {\n      margin-left: 1em;\n      overflow: hidden;\n      .transition(max-height, 0.75s);\n  }\n\n  .gift-details.hidden {\n      display: block;\n      max-height: 0;\n  }\n\n  .gift-details:not(.hidden) {\n      max-height: 400px;\n  }\n\n  //explicit width for jquery slideToggle\n  .details {\n    font-size: 0.9em;\n    margin: 10px 0 0 0;\n    width: 600px;\n  }\n\n  .gilding-info {\n    .details {\n      width: 538px;\n    }\n\n    .examples {\n      margin-top: 10px;\n \n      img {\n        display: block;\n        margin: 0 auto;\n      }\n\n      p {\n        text-align: center;\n        font-size: 0.7em;\n      }\n    }\n  }\n\n  .gold-dropdown {\n    color: @dark;\n    background-color: @off-white;\n    font-size: 16px;\n  }\n\n  .indent {\n    margin-left: 20px;\n  }\n\n  .loggedout-gold-form {\n    .loggedout-email {\n      display: block;\n      margin: 10px 0;\n    }\n\n    .hint {\n      font-size: 12px;\n      font-style: italic;\n    }\n  }\n\n  @media screen and (max-width: 1024px) {\n    .buttons {\n      text-align: center;\n    }\n\n    span.gold-snoo {\n      display: block;\n      position: static;\n      width: 100%;\n      text-align: center;\n      margin-top: 25px;\n    }\n  }\n}\n\n/* Page level logic for gold pages, outside of gold-wrap so we can use body classes. */\n.gold-page.creddits-purchase,\n.gold-page.creddits-payment {\n\n  .gold-snoo {\n    background-image: url(../gold/creddits-snoo.png);\n  }\n}\n\n.gold-page.gilding {\n  .gold-banner {\n    background-image: url(../gold/reddit-gilding.png);\n  }\n\n  dt {\n    margin: 0.9em 0 0.5em;\n    font-weight: bold;\n    padding-top: 1em;\n    border-top: 1px solid #CCC;\n    font-size: 1.2em;\n  }\n\n  dt:first-of-type {\n    padding-top: 0;\n    border-top-width: 0;\n  }\n\n  dd {\n    margin-left: 0;\n    line-height: 1.8em;\n  }\n\n  .example {\n    margin: 1em 0;\n\n    figure {\n      margin: 0 auto;\n      padding: 0;\n      width: 339px;\n      border: 1px solid #cccccb;\n\n      &.userpage-gild {\n        height: 227px;\n        background: url('../gold/userpage-gild.png') no-repeat center center;\n      }\n\n      &.comment-gild {\n        height: 160px;\n        background: url('../gold/comment-gild.png') no-repeat top left;\n      }\n\n      &.using-creddits {\n        height: 90px;\n        background: url('../gold/using-creddits.png') no-repeat top left;\n      }\n    }\n  }\n\n  .gold-button {\n    display: block;\n    .box-sizing(border-box);\n    margin: 0;\n    font-size: 1.3em;\n  }\n}\n\n@media \n(-webkit-min-device-pixel-ratio: 2), \n(min-resolution: 192dpi) { \n  .gold-page.gilding .example figure {\n\n    &.userpage-gild {\n        background: url('../gold/userpage-gild-x2.png') no-repeat center center;\n        background-size: 339px 227px;\n      }\n\n    &.comment-gild {\n        background: url('../gold/comment-gild-x2.png') no-repeat top left;\n        background-size: 339px 160px;\n      }\n\n    &.using-creddits {\n        background: url('../gold/using-creddits-x2.png') no-repeat top left;\n        background-size: 339px 90px;\n      }\n  }\n}\n\n/* Gold only subreddits */\n\n.gold-only {\n  @gold: #9a7d2e;\n  @light-gold: #faf1b3;\n\n  #header {\n    border-bottom-color: @gold;\n    .linear-gradient(#d7cc7e, #e2ce3e);\n\n    #header-bottom-right {\n      background-color: @light-gold;\n\n      a { \n        color: @gold; \n      }\n\n      .message-count { \n        background-color: #e2ce3e;\n      }\n\n      #mail.havemail {\n        background-image: url(../gold/gold-only-mail-havemail.png); /* SPRITE */\n      }\n\n      #modmail.havemail {\n        background-image: url(../gold/gold-only-modmail-havemail.png); /* SPRITE */\n      }\n    }\n\n    .tabmenu li a {\n      background-color: @light-gold;\n      color: @gold;\n    }\n\n    .tabmenu li.selected a {\n      background-color: #ffffff;\n      border-color: @gold;\n      border-bottom-color: #ffffff;\n    }\n\n    #sr-header-area {\n      background-color: transparent;\n      border: none;\n      opacity: 0.5;\n\n      &:hover {\n        opacity: 1;\n      }\n    }\n  }\n\n  .arrow.upmod { \n    background-image: url(../gold/gold-only-upvote-mod.png); /* SPRITE */\n  }\n\n  .arrow.downmod { \n    background-image: url(../gold/gold-only-downvote-mod.png); /* SPRITE */\n  }\n\n  .link .score.dislikes {\n    color: #a98d79;\n  }\n\n  .link .score.likes {\n    color: #dec145;\n  }\n}\n\n/* Quarantined subreddits */\n\n.quarantine {\n  #header-img.default-header {\n    background-image: url(../quarantine-header.png); /* SPRITE */\n    width: 40px;\n    height: 40px;\n    margin-left: 3px;\n  }\n\n  .sidebox.create .spacer {\n    display: none;\n  }\n}\n\n.fraud-reason {\n    display: none;\n\n    &:not(:empty) {\n        display: block;\n        padding-bottom: 5px;\n        margin-bottom: 5px;\n        border-bottom: 1px solid #d8bb3c;\n    }\n\n    &:before {\n        content: 'reason(s): ';\n        display: inline;\n    }\n}\n\n.report-action-form {\n    max-width: 300px;\n}\n\n.subreddit-report-form,\n.action-form {\n    display: none;\n    background-color: #f6e69f;\n    border: thin solid #d8bb3c;\n    padding: 5px;\n    margin: 5px 0;\n    font-size: larger;\n\n    input {\n\n        &[type=\"radio\"] {\n            margin: 2px 0.5em 0 0;\n        }\n\n        &[type=\"text\"] {\n            margin-top: 5px;\n            width: 95%;\n        }\n\n        &:disabled {\n            background: #dddddd;\n        }\n    }\n\n    ol {\n        margin-bottom: 5px;\n    }\n}\n\n.subreddit-report-form {\n    @bg-color: #FBFBFB;\n    @text-color: @color-semi-black;\n\n    font-family: \"Helvetica Neue\", \"Helvetica\", \"Arial\", sans-serif;\n    background-color: @bg-color;\n    border: 1px solid darken(@bg-color, 10%);\n    border-radius: 0;\n    color: @text-color;\n    padding: 10px 15px;\n    font-size: 14px;\n    line-height: 20px;\n    position: relative;\n    \n    .report-header {\n        margin-bottom: 10px;\n        font-weight: 500;\n    }\n\n    .report-reason-list {\n        margin-top: 10px;\n        margin-bottom: 10px;\n    }\n\n    .report-reason-item {\n        margin-top: 5px;\n        margin-bottom: 5px;\n\n        label {\n            cursor: pointer;\n        }\n\n        input[type=radio] {\n            margin-right: 15px;\n            float: left;\n        }\n\n        select {\n            max-width: 100%;\n        }\n\n        .report-reason-display {\n            vertical-align: top;\n            overflow: auto;\n        }\n    }\n\n    .report-reason-other {\n        input[type=text] {\n            width: 100%;\n        }\n    }\n\n    .c-submit-group {\n        text-align: right;\n        margin-top: 10px;\n    }\n\n    .action-icon {\n        width: 16px;\n        height: 16px;\n        display: inline-block;\n    }\n\n    .action-icon-info {\n        .hdpi-bg-image(\n            @1x: url(../action-icon-info-color.png),\n            @2x: url(../action-icon-info-color_2x.png)\n        );\n    }\n\n    .action-icon {\n        position: absolute;\n        top: 12px;\n        right: 15px;\n    }\n}\n\n.reported-stamp.has-reasons {\n    cursor: pointer;\n}\n\nul.report-reasons {\n    width: 80%;\n    background-color: #f6e69f;\n    border: thin solid black;\n    display: none;\n\n    li {\n        &.report-reason {\n            padding: 1px 10px;\n            display: block;\n            overflow: hidden;\n            text-overflow: ellipsis;\n        }\n\n        &.report-reason-title {\n            padding: 1px 10px;\n            font-weight: bold;\n        }\n    }\n}\n\n#header {\n    @spam-bg: @color-spam;\n    @temp-ban-bg: @color-temp-ban;\n    @perma-ban-bg: @color-perma-ban;\n    @deleted-bg: @color-deleted;\n    @striped-bg-image: repeating-linear-gradient(45deg, transparent,\n                                                        transparent 30px,\n                                                        rgba(255,255,255,0.5) 30px,\n                                                        rgba(255,255,255,0.5) 60px);\n\n    body.deleted &,\n    body.user-deleted & {\n        background-color: @color-deleted;\n        background-image: @striped-bg-image;\n    }\n\n    body.banned &,\n    body.user-banned &,\n    body.user-in-timeout-perma & {\n        background-color: @color-perma-ban;\n    }\n\n    body.user-in-timeout-temp & {\n        background-color: @color-temp-ban;\n    }\n\n    body.user-spam & {\n        background-color: @color-spam;\n    }\n}\n\n.author {\n    &.user-banned {\n        color: @color-perma-ban;\n        font-weight: bold;\n    }\n\n    &.user-in-timeout-temp {\n        color: @color-temp-ban;\n        font-weight: bold;\n    }\n\n    &.user-in-timeout-perma {\n        color: @color-perma-ban;\n        font-weight: bold;\n    }\n\n    &.user-spam {\n        color: @color-spam;\n        font-weight: bold;\n    }\n}\n\n#compose-message select {\n    font-size: 100%;\n}\n\n.embed-modal {\n  .modal-body,\n  .modal-footer {\n    padding: 40px;\n  }\n\n  .modal-body {\n    padding-bottom: 10px;\n\n    .c-checkbox {\n      margin: 10px 0;\n    }\n  }\n\n  .modal-footer {\n    padding-top: 20px;\n\n    .c-form-control {\n      margin-top: 10px;\n    }\n  }\n\n  .modal-title {\n    margin: 0;\n  }\n\n  #embed-preview {\n    overflow-y: hidden;\n  }\n}\n\n#related-srs {\n  margin: 3px;\n  font-size: smaller;\n}\n\n#add-related-sr {\n  margin-left: 3px;\n  font-size: smaller;\n\n  #sr-autocomplete-area,\n  div.error {\n    display: inline-block;\n  }\n\n  #sr-autocomplete {\n    width: 200px;\n  }\n\n  #sr-drop-down {\n    width: 206px;\n  }\n}\n\n.more-actions {\n    .title {\n        color: #888;\n\n        &:hover {\n            cursor: pointer;\n        }\n    }\n}\n\n.full-context-info {\n    .md {\n        padding: 10px 5px 5px;\n        border: 1px solid @color-warning-red;\n        border-radius: 5px;\n        background: @color-pale-grey;\n    }\n\n    .parent {\n        padding: 0 (@margin-small - 1) * 1px;\n    }\n\n    td {\n        color: @color-text-grey;\n    }\n\n    .arrow {\n        display: inline-block;\n        margin-right: 100px;\n\n        &:after {\n            display: inline-block;\n            margin-left: 20px;\n            width: 100px;\n        }\n    }\n\n    .arrow.unvoted {\n        &:after {\n            content: \"did not vote\";\n            margin-left: 0;\n        }\n    }\n\n    .arrow.vote-changed {\n        background: data-uri('../close.png') no-repeat;\n        background-size: 13px 13px;\n\n        &:after {\n            content: \"changed vote\";\n        }\n    }\n\n    .arrow.upmod:after {\n        content: \"upvoted\";\n    }\n\n    .arrow.downmod:after {\n        content: \"downvoted\";\n    }\n}\n\n.mobile-web-redirect {\n    background-color: #4a7fc5;\n    box-sizing: border-box;\n    color: #FFF;\n    font-size: 40px;\n    font-weight: bold;\n    padding: 30px 0;\n    text-align: center;\n    text-decoration: none;\n    text-transform: uppercase;\n    width: 100%;\n    display: block;\n    position: relative;\n    z-index: 50;\n}\n\nbody:not(.loggedin) .comment-save-button,\nbody:not(.loggedin) .give-gold-button,\nbody:not(.loggedin) .reply-button,\nbody:not(.loggedin) .report-button {\n    display: none;\n}\n\n.sr-type-icon {\n    display: inline-block;\n    width: 16px;\n    height: 16px;\n\n    &.sr-type-icon-banned {\n        .hdpi-bg-image(@1x: url(../sr-type-icon-banned.png), @2x: url(../sr-type-icon-banned_2x.png));\n    }\n\n    &.sr-type-icon-moderator {\n        .hdpi-bg-image(@1x: url(../sr-type-icon-moderator.png), @2x: url(../sr-type-icon-moderator_2x.png));\n    }\n\n    &.sr-type-icon-approved {\n        .hdpi-bg-image(@1x: url(../sr-type-icon-approved.png), @2x: url(../sr-type-icon-approved_2x.png));\n    }\n\n    &.sr-type-icon-restricted {\n        .hdpi-bg-image(@1x: url(../sr-type-icon-restricted.png), @2x: url(../sr-type-icon-restricted_2x.png));\n    }\n\n    &.sr-type-icon-private {\n        .hdpi-bg-image(@1x: url(../sr-type-icon-private.png), @2x: url(../sr-type-icon-private_2x.png));\n    }\n\n    &.sr-type-icon-quarantined {\n        .hdpi-bg-image(@1x: url(../sr-type-icon-quarantined.png), @2x: url(../sr-type-icon-quarantined_2x.png));\n    }\n\n    &.sr-type-icon-nsfw {\n        .hdpi-bg-image(@1x: url(../sr-type-icon-nsfw.png), @2x: url(../sr-type-icon-nsfw_2x.png));\n    }\n}\n\n.subscription-box .sr-type-icon {\n    margin-right: 3px;\n}\n\n.subreddit .midcol .sr-type-icon {\n    margin-left: 3px;\n}\n\n#auction-announcement-container {\n    #auction-announcement {\n        width: 100%;\n        margin: -5px 5px 0 5px;\n        background-color: #336699;\n        height: 86px;\n        text-align: center;\n        color: #FFF;\n        h1 {\n            font-size: 20px;\n            padding-top: 18px;\n            font-weight: bold;\n        }\n        p {\n            font-size: 12px;\n            font-weight: bold;\n            a {\n                color: #B2D6EE;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "r2/r2/public/static/css/search.less",
    "content": "@color-body: @color-text-grey;\n@color-meta: @color-dark-grey;\n@color-link: darken(@color-alien-blue, 5%);\n@color-link-visited: @color-link-purple;\n\n@color-unsubscribe: darken(@color-warning-red, 10%);\n@color-subscribe: darken(@color-green, 10%);\n@color-subscribe-button-focus: @color-white;\n@color-group-header-border: @color-pale-grey;\n\n@color-subreddit: @color-meta;\n\n@max-width: 750px;\n@min-width: 600px;\n\n@search-font-large: @font-medium;\n@search-font-medium: @font-small;\n@search-font-base: @font-x-small;\n@search-font-small: @font-xx-small;\n@search-line-base: @line-medium;\n@search-line-small: @line-x-small;\n\n.search-font-size(@font:@search-font-base, @line:@search-line-base, @base:@search-font-base) {\n  .md-font-size(@base, @font, @line);\n}\n\n.combined-search-page {\n  > .content {\n    font-size: @base-font-keyword;\n  }\n\n  // needs the extra selector to override the default styles :/\n  .search-subscribe-button {\n    display: inline-block;\n    margin: 0;\n    margin-right: @margin-x-small * 1px;\n\n    .remove {\n      @color: @color-unsubscribe;\n      @color2: darken(@color, 10%);\n\n      background: @color;\n      border-color: @color2;\n\n      &:active {\n        background: @color2;\n      }\n    }\n\n    .add {\n      @color: @color-subscribe;\n      @color2: darken(@color, 10%);\n\n      background: @color;\n      border-color: @color2;\n\n      &:active {\n        background: @color2;\n      }\n    }\n\n    .add, .remove {\n      color: @color-white;\n      text-align: center;\n      transform: scale(1, 1);\n      transition: all 0.15s;\n      width: 90px;\n\n      &:focus {\n        outline: none;\n        box-shadow: 0 0 0 1px @color-subscribe-button-focus inset;\n      }\n\n      &.active {\n        display: inline-block;\n        line-height: 13px;\n      }\n    }\n  }\n\n  .searchfacets {\n    .search-font-size();\n    border: 1px solid @color-meta;\n    margin: 0;\n    margin-left: @margin-xx-large * 1px;\n    margin-top: @margin-large * 1px;\n    max-width: @max-width;\n    min-width: @min-width;\n    overflow: auto;\n    padding: @margin-medium * 1px;\n    white-space: pre-wrap;\n\n    > h4.title {\n      color: @color-dark-grey;\n      margin-bottom: @margin-x-small * 1px;\n    }\n\n    .facet:hover {\n      text-decoration: underline;\n    }\n  }\n\n  li.searchfacet {\n    display: inline-block;\n    line-height: 20px;\n    min-width: 32%;\n  }\n\n  .facet.count {\n    color: @color-meta;\n\n    &:hover {\n      text-decoration: none;\n    }\n  }\n\n  .searchpane {\n    background: none;\n    border: none;\n    padding: @margin-small * 1px;\n    padding-left: @margin-xx-large * 1px;\n  }\n\n  #search {\n    padding-right: 120px;\n  }\n\n  #search input[type=text] {\n    .search-font-size(@search-font-medium);\n    border-color: @color-dark-grey;\n    border-radius: 2px;\n    box-sizing: border-box;\n    max-width: @max-width;\n    min-width: @max-width / 2;\n    padding: @margin-x-small * 1px;\n    padding-left: @margin-small * 1px;\n    padding-right: @margin-x-large * 1px;\n    vertical-align: middle;\n    width: 100%;\n  }\n\n  .search-submit-button {\n    margin-left: @margin-small * 1px;\n    .search-font-size(@search-font-base);\n    padding: (@margin-x-small * 1px) (@margin-medium * 1px);\n    vertical-align: middle;\n  }\n\n  .menuarea {\n    font-size: 1em;\n    margin-left: @margin-large * 1px;\n  }\n\n  .linkflairlabel {\n    line-height: 17px;\n  }\n\n  // undo old styles, remove when unflagged\n  .searchfacets {\n    background: transparent;\n    box-shadow: none;\n\n    .list {\n      margin: 0;\n    }\n  }\n\n  .facet.count {\n    font-weight: normal;\n  }\n\n  .search-icon {\n    background-image: data-uri('../search-button-icon.png');\n    background-size: 20px 20px;\n    display: inline-block;\n    height: 20px;\n    transform: translateY(-1px);\n    vertical-align: middle;\n    width: 20px;\n  }\n}\n\n.search-result-listing {\n  .md-base-font-size(@search-font-base);\n}\n\n.search-result.visited {\n  .search-title,\n  .search-link {\n    &, > mark {\n      color: @color-link-visited;\n    }\n  }\n}\n\n.search-result {\n  margin-bottom: @margin-x-large * 1px;\n  margin-top: @margin-small * 1px;\n\n  :link {\n    &, > mark {\n      color: @color-link;\n    }\n  }\n\n  :visited {\n    &, > mark {\n      color: @color-link-visited;\n    }\n  }\n\n  &.has-thumbnail {\n    .display-flex();\n\n    > * {\n      .flex(1 1);\n    }\n\n    > .thumbnail {\n      .flex(0 0 70px);\n      margin-right: @margin-small * 1px;\n      width: 70px;\n\n      img {\n        display: block;\n        height: auto;\n        width: 100%;\n      }\n    }\n  }\n\n\n  mark {\n    background-color: transparent;\n    color: inherit;\n    font-weight: bold;\n    line-height: 1em;\n  }\n}\n\n.search-result-meta,\n.search-result-footer {\n  .search-font-size();\n  vertical-align: baseline;\n\n  > * {\n    // prevent unstyled children (e.g. spans) from adding to line-height\n    line-height: 1em;\n  }\n}\n\n.search-result-header {\n  .search-font-size();\n\n  > * {\n    vertical-align: top;\n  }\n\n}\n\n.search-title {\n  font-size: @search-font-large * 1px;\n  margin-right: @margin-x-small * 1px;\n}\n\n.search-result-meta {\n  // let this element inherit the same styles as the 'tagline' class\n  // we don't want to add the extra class to the markup because it will cause\n  // the line to potentially inherit a bunch of unwanted styles from subreddit\n  // stylesheets\n  &:extend(.tagline all);\n\n  .search-font-size(@search-font-small, @search-line-base);\n  color: @color-meta;\n\n  .search-result-icon {\n    vertical-align: text-bottom;\n  }\n}\n\n.search-score {\n  .search-font-size(@search-font-base, @search-font-base, @search-font-small);\n\n  &:after {\n    content: ' •';\n  }\n}\n\n.search-comments {\n  font-weight: bold;\n  color: @color-meta;\n}\n\n.search-result-body {\n  .search-font-size(@search-font-base, @search-line-small);\n  color: @color-body;\n  padding-right: @margin-xxx-large * 1px;\n}\n\n.search-expando {\n  overflow: hidden;\n\n  &.collapsed {\n    max-height: 45px;\n    position: relative;\n\n    &:before {\n      bottom: 0;\n      content:'';\n      height: 15px;\n      left: 0;\n      position: absolute;\n      width: 100%;\n\n      .linear-gradient(rgba(255, 255, 255, 0), rgba(255, 255, 255, 1));\n    }\n  }\n}\n\n.search-expando-button {\n  color: @color-link;\n\n  &:hover {\n    cursor: pointer;\n    text-decoration: underline;\n  }\n\n  > span {\n    display: none;\n  }\n\n  &.expanded .search-expando-button-label-expanded,\n  &.collapsed .search-expando-button-label-collapsed {\n    display: inline;\n  }\n}\n\n.search-result-footer {\n  .search-font-size();\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n\n  .search-link {\n    margin-left: @margin-x-small * 1px;\n  }\n}\n\n.search-result-group {\n  max-width: @max-width;\n  min-width: @min-width;\n  padding-left: @margin-xx-large * 1px;\n  padding-right: @margin-large * 1px;\n\n\n  footer .nav-buttons {\n    .search-font-size(@search-font-base);\n    margin-top: @margin-small * 1px;\n    margin-bottom: @margin-xx-large * 1px;\n\n    * { \n      font-size: inherit;\n    }\n  }\n\n  footer .info {\n    color: @color-meta;\n  }\n}\n\n.search-result-group-header {\n  border-bottom: 2px solid @color-group-header-border;\n  color: @color-dark-grey;\n  margin-bottom: @margin-large * 1px;\n  margin-top: @margin-xx-large * 1px;\n}\n\n.search-header-label {\n  .search-font-size(@search-font-medium);\n  font-weight: bold;\n}\n\n.search-header-menus {\n  float: right;\n}\n\n.search-menu {\n  .search-font-size(@search-font-base);\n  display: inline-block;\n  margin-left: @margin-large * 1px;\n}\n\n.search-result-icon {\n  background-position: center;\n  background-repeat: no-repeat;\n  display: inline-block;\n  height: 15px;\n  vertical-align: middle;\n  width: 16px;\n}\n\n.search-result-icon-score {\n  background-image: data-uri('../ambivote-icon.png');\n  background-size: 10px 14px;\n}\n\n.search-result-icon-external {\n  background-image: data-uri('../external-link-icon.png');\n  background-size: 15px 7px;\n}\n\n.search-result-icon-filter {\n  background-image: data-uri('../search-icon.png');\n  background-size: 11px 12px;\n}\n\n.search-result-icon-internal {\n  background-image: data-uri('../internal-link-icon.png');\n  background-size: 11px 10px;\n}\n\n\n// old styles, remove when unflagged\n.searchfacets {\n  overflow: auto;\n  font-size: small;\n  white-space: pre-wrap;\n  border: 1px solid gray;\n  padding: 10px;\n  margin-top: -6px;\n  box-shadow: 0 4px 6px -1px #ccc inset;\n  background: #fefefe;\n}\n\n.searchfacets .title {\n  margin: 5px;\n}\n\n.searchfacets .facet:hover {\n  text-decoration: underline;\n}\n\n.searchfacets .list {\n  margin: 0px 0px 0px 10px;\n}\n\nli.searchfacet {\n  display: inline-block;\n  width: 15em;\n}\n\n.facet.count {\n  color: #888888;\n  font-weight: bold;\n}\n"
  },
  {
    "path": "r2/r2/public/static/css/subreddit-rules.less",
    "content": ".subreddit-rule-item {\n    margin-bottom: 10px;\n    margin-left: -10px;\n    margin-top: 10px;\n    padding: 10px;\n\n    &:hover {\n        background: #F7F7F7;\n    }\n\n    .subreddit-rule-delete-confirmation {\n        text-align: right;\n    }\n}\n\n.subreddit-rule-item .editable .subreddit-rule-contents,\n.subreddit-rule-form {\n    .display-flex();\n}\n\n.subreddit-rule-item .editable .subreddit-rule-contents-display,\n.subreddit-rule-form .form-inputs {\n    .flex(1 1);\n}\n\n.subreddit-rule-item .editable .subreddit-rule-buttons,\n.subreddit-rule-form .form-buttons {\n    .flex(0 0 41px);\n\n    > button {\n        margin-bottom: 5px;\n        margin-left: 5px;\n\n        .mod-action-icon {\n            padding: 5px 10px;\n        }\n    }\n}\n\n.subreddit-rule-form .form-buttons {\n    .display-flex();\n    .flex-direction(column);\n    .justify-content(flex-end);\n}\n\n.subreddit-rule-form {\n    .c-form-group {\n        position: relative;\n        margin-bottom: unit(@margin-medium, px);\n\n        .label {\n            color: @color-grey;\n        }\n\n        .label.required:after {\n            color: @color-warning-red;\n            content: ' *';\n            display: inline;\n        }\n\n        .text-counter {\n            color: @color-grey;\n            position: absolute;\n            right: 0;\n            top: 0;\n\n            &.has-error {\n                color: @color-warning-red;\n            }\n        }\n    }\n\n    .form-group-kind label {\n        margin-right: unit(@margin-large, px);\n    }\n}\n\n.subreddit-rule-add-form-buttons .subreddit-rule-edit-button {\n    color: @color-light-blue;\n    font-size: 12px;\n    font-weight: bold;\n    line-height: 15px;\n    text-transform: uppercase;\n\n    &[disabled] {\n        color: @color-grey;\n    }\n\n    .mod-action-icon {\n        margin-right: 5px;\n        vertical-align: bottom;\n    }\n}\n\n.error-fields {\n    color: @color-warning-red;\n    margin-top: unit(@margin-x-small, px);\n    text-align: right;\n\n    > .error {\n        display: block;\n    }\n}\n\n.subreddit-rule-too-many-notice {\n    color: @color-dark-teal;\n    margin-top: unit(@margin-x-small, px);\n}\n\n.subreddit-rule-delete-confirmation {\n    color: @color-warning-red;\n\n    button {\n        background: transparent;\n        border: none;\n        padding: 0 3px;\n        color: @color-light-blue;\n        font-size: 1em;\n    }\n\n    .separator {\n        color: @color-grey;\n    }\n}\n\n.subreddit-rules-page {\n    @icon-size: 52px;\n\n    max-width: 700px;\n    padding-left: @icon-size + 20px;\n    padding-right: 20px;\n    padding-top: 10px;\n    position: relative;\n    \n    &:before {\n        .hdpi-bg-image(@1x: url(../modtools-page-icon-rules.png),\n                       @2x: url(../modtools-page-icon-rules_2x.png));\n\n        content: '';\n        display: block;\n        height: @icon-size;\n        left: 0;\n        position: absolute;\n        top: 0;\n        width: @icon-size;\n    }\n\n    .subreddit-rule-buttons,\n    .subreddit-rule-add-form-buttons,\n    .form-buttons {\n        button {\n            background: transparent;\n            border: none;\n            padding: 0;\n        }\n    }\n\n    > header {\n      margin-bottom: @margin-xx-large * 1px;\n\n      p {\n        font-weight: 500;\n      }\n    }\n\n    > footer {\n        margin-top: unit(@margin-xx-large, px);\n    }\n\n    .md {\n        p:first-child {\n            margin-top: 0;\n        }\n\n        p:last-child {\n            margin-bottom: 0;\n        }\n    }\n\n    .md-container .md {\n        @font-base: @font-small;\n        @line-base: @line-small;\n\n        h2 {\n            .md-font-size(@font-base, @font-xx-large, @line-xx-large);\n            .md-margins(@font-xx-large, 0, 0);\n            font-weight: 300;\n        }\n\n        .subreddit-rule-title {\n            .md-font-size(@font-base, @font-medium, @line-medium);\n            margin-top: 0;\n            margin-bottom: unit(@margin-x-small, px);\n            font-weight: 500;\n        }\n\n        .subreddit-rule-kind {\n            .md-font-size(@font-base, @font-x-small, @line-medium);\n            margin-bottom: unit(@margin-x-small, px);\n            font-weight: 500;\n            color: @color-dark-grey;\n        }\n\n        .subreddit-rule-description {\n            color: @color-text-grey;\n        }\n    }\n}\n\n"
  },
  {
    "path": "r2/r2/public/static/css/wiki.less",
    "content": ".transition (@prop: all, @time: 1s, @ease: linear) {\n    -webkit-transition: @prop @time @ease;\n    -moz-transition: @prop @time @ease;\n    -o-transition: @prop @time @ease;\n    -ms-transition: @prop @time @ease;\n    transition: @prop @time @ease;\n}\n\n.wiki-page {\n    // Main content portion of the wiki pages\n    .wiki-page-content {\n        margin: 15px;\n        margin-right: 325px;\n\n        // Wiki page listing \n        .pagelisting {\n            font-size: 1.2em;\n            font-weight: bold;\n            color: black;\n            padding-left: 25px;\n            ul {\n                list-style: disc;\n                padding: 2px;\n                padding-left: 10px;\n            }\n        }\n\n        // Page action subtitle \n        .description {\n            padding-bottom: 5px;\n            h2 {\n                color: #222;\n            }\n        }\n\n        // Revision listing \n        .wikirevisionlisting {\n            .generic-table {\n                width: 100%;\n            }\n            table tr td {\n                padding-right: 15px;\n            }\n            .revision {\n                .transition(opacity, 500ms);\n            }\n            .revision.deleted {\n                opacity: .5;\n                text-decoration: line-through;\n            }\n            .revision.hidden {\n                opacity: .5;\n            }\n            // Opera does not inherit the opacity to the td\n            .revision.hidden td {\n                opacity: inherit;\n            }\n        }\n\n        // Wiki markdown \n        .wiki.md {\n            max-width: none;\n        }\n\n        // Wiki table of contents \n        .wiki > .toc > ul {\n            float: right;\n            padding: 11px 22px;\n            margin: 0 0 11px 22px;\n            border: 1px solid #8D9CAA;\n            list-style: none;\n            max-width: 300px;\n        }\n        .wiki > .toc > ul ul {\n            margin: 4px 0;\n            padding-left: 22px;\n            border-left: 1px dotted #cce;\n            list-style: none;\n        }\n        .wiki > .toc > ul li {\n            margin: 0;\n        }\n\n        // Page settings \n        .fancy-settings .toggle {\n            display: inline-block;\n            padding-right: 15px;\n        }\n\n        // Reason for revision field \n        #wiki_revision_reason {\n            padding: 2px;\n            margin-left: 0;\n            width: 100%;\n        }\n        \n        // Save page button, save settings button, etc\n        .wiki_button {\n            padding: 2px;\n        }\n\n        .throbber {\n            margin-bottom: -5px;\n        }\n\n        // Submit Discussion button \n        .discussionlink {\n            display: inline-block;\n            margin-left: 15px;\n            padding-right: 50px;\n            margin-top: 5px;\n            a {\n                padding-left: 15px;\n            }\n        }\n\n        .markhelp {\n            max-width: 500px;\n            font-size: 1.2em;\n            padding: 4px;\n            margin: 5px 0;\n        }\n\n    }\n\n    // Page title \n    .wikititle {\n        margin-left: 15px;\n        color: #666;\n        display: inline;\n        vertical-align: middle;\n        strong {\n            font-weight: bold;\n        }\n    }\n\n    // Wiki tabs \n    .pageactions {\n        display: inline-block;\n        font-size: larger;\n        margin-left: 25px;\n        border-radius: 5px;\n        border: 1px solid #8D9CAA;\n        vertical-align: middle;\n        .wikiaction {\n            display: inline-block;\n            margin: 2px;\n            padding-top: 2px;\n            padding-bottom: 3px;\n            border-radius: 3px;\n            padding-right: 10px;\n            padding-left: 10px;\n        }\n        .wikiaction:hover {\n            background-color: #CEE3F8;\n        }\n        .wikiaction-current:hover {\n            background-color: #5F99CF;\n        }\n        .wikiaction-current {\n            color: white;\n            background-color: #5F99CF;\n        }\n    }\n    .source {\n        width: 100%;\n        display: none;\n    }\n    .toggle-source {\n        float: right;\n    }\n}\n\n// Specific wiki page styles\n.wiki-page-config-automoderator {\n  #editform textarea {\n    font-family: \"Bitstream Vera Sans Mono\", Consolas, monospace;\n  }\n\n  .wiki-page-content pre {\n    word-wrap: break-word;\n    white-space: pre-wrap;\n  }\n}\n"
  },
  {
    "path": "r2/r2/public/static/inbound-email-policy.html",
    "content": "<html>\r\n<head>\r\n<title>reddit email policy</title>\r\n</head>\r\n<body>\r\n\r\n<div style=\"width: 80ex\">\r\n<p>\r\nSection 17538.45 of the California Business and Professional Code allows \r\nElectronic Mail Service Providers to restrict or prohibit use of their \r\nequipment for Unsolicited Electronic Mail Advertising (UEMA), when the \r\nequipment is located in the state of California. It further provides \r\nfor liquidated damages of $50 per message received. reddit.com uses \r\nequipment located in California, and the reddit.com policy is that \r\nUnsolicited Electronic Mail Advertising is prohibited. \r\n</p>\r\n<hr>\r\n<p>Text of section 17538.45 of the Business and Professional Code of the \r\nState of California, copied on July 17th, 2008.  Check\r\nwith the <a HREF=\"http://www.leginfo.ca.gov/calaw.html\">California \r\nLaw</a> website for the current text (search for section 17538 or \"unsolicited\" \r\nin the Business and Professions Code). \r\n</p> \r\n</div>\r\n<pre>\r\n17538.45.  (a) For purposes of this section, the following words\r\nhave the following meanings:\r\n   (1) \"Electronic mail advertisement\" means any electronic mail\r\nmessage, the principal purpose of which is to promote, directly or\r\nindirectly, the sale or other distribution of goods or services to\r\nthe recipient.\r\n   (2) \"Unsolicited electronic mail advertisement\" means any\r\nelectronic mail advertisement that meets both of the following\r\nrequirements:\r\n   (A) It is addressed to a recipient with whom the initiator does\r\nnot have an existing business or personal relationship.\r\n   (B) It is not sent at the request of or with the express consent\r\nof the recipient.\r\n   (3) \"Electronic mail service provider\" means any business or\r\norganization qualified to do business in California that provides\r\nregistered users the ability to send or receive electronic mail\r\nthrough equipment located in this state and that is an intermediary\r\nin sending or receiving electronic mail.\r\n   (4) \"Initiation\" of an unsolicited electronic mail advertisement\r\nrefers to the action by the initial sender of the electronic mail\r\nadvertisement.  It does not refer to the actions of any intervening\r\nelectronic mail service provider that may handle or retransmit the\r\nelectronic message.\r\n   (5) \"Registered user\" means any individual, corporation, or other\r\nentity that maintains an electronic mail address with an electronic\r\nmail service provider.\r\n   (b) No registered user of an electronic mail service provider\r\nshall use or cause to be used that electronic mail service provider's\r\nequipment located in this state in violation of that electronic mail\r\nservice provider's policy prohibiting or restricting the use of its\r\nservice or equipment for the initiation of unsolicited electronic\r\nmail advertisements.\r\n   (c) No individual, corporation, or other entity shall use or cause\r\nto be used, by initiating an unsolicited electronic mail\r\nadvertisement, an electronic mail service provider's equipment\r\nlocated in this state in violation of that electronic mail service\r\nprovider's policy prohibiting or restricting the use of its equipment\r\nto deliver unsolicited electronic mail advertisements to its\r\nregistered users.\r\n   (d) An electronic mail service provider shall not be required to\r\ncreate a policy prohibiting or restricting the use of its equipment\r\nfor the initiation or delivery of unsolicited electronic mail\r\nadvertisements.\r\n   (e) Nothing in this section shall be construed to limit or\r\nrestrict the rights of an electronic mail service provider under\r\nSection 230(c)(1) of Title 47 of the United States Code, any decision\r\nof an electronic mail service provider to permit or to restrict\r\naccess to or use of its system, or any exercise of its editorial\r\nfunction.\r\n   (f) (1) In addition to any other action available under law, any\r\nelectronic mail service provider whose policy on unsolicited\r\nelectronic mail advertisements is violated as provided in this\r\nsection may bring a civil action to recover the actual monetary loss\r\nsuffered by that provider by reason of that violation, or liquidated\r\ndamages of fifty dollars ($50) for each electronic mail message\r\ninitiated or delivered in violation of this section, up to a maximum\r\nof twenty-five thousand dollars ($25,000) per day, whichever amount\r\nis greater.\r\n   (2) In any action brought pursuant to paragraph (1), the court may\r\naward reasonable attorney's fees to a prevailing party.\r\n   (3) (A) In any action brought pursuant to paragraph (1), the\r\nelectronic mail service provider shall be required to establish as an\r\nelement of its cause of action that prior to the alleged violation,\r\nthe defendant had actual notice of both of the following:\r\n   (i) The electronic mail service provider's policy on unsolicited\r\nelectronic mail advertising.\r\n   (ii) The fact that the defendant's unsolicited electronic mail\r\nadvertisements would use or cause to be used the electronic mail\r\nservice provider's equipment located in this state.\r\n   (B) In this regard, the Legislature finds that with rapid advances\r\nin Internet technology, and electronic mail technology in\r\nparticular, Internet service providers are already experimenting with\r\nembedding policy statements directly into the software running on\r\nthe computers used to provide electronic mail services in a manner\r\nthat displays the policy statements every time an electronic mail\r\ndelivery is requested.  While the state of the technology does not\r\nsupport this finding at present, the Legislature believes that, in a\r\ngiven case at some future date, a showing that notice was supplied\r\nvia electronic means between the sending and receiving computers\r\ncould be held to constitute actual notice to the sender for purposes\r\nof this paragraph.\r\n   (4) (A) An electronic mail service provider who has brought an\r\naction against a party for a violation under Section 17529.8 shall\r\nnot bring an action against that party under this section for the\r\nsame unsolicited commercial electronic mail advertisement.\r\n   (B) An electronic mail service provider who has brought an action\r\nagainst a party for a violation of this section shall not bring an\r\naction against that party under Section 17529.8 for the same\r\nunsolicited commercial electronic mail advertisement.\r\n</pre>\r\n</body>\r\n</html>\r\n\r\n"
  },
  {
    "path": "r2/r2/public/static/js/access.js",
    "content": "!function(r) {\n  r.access = {};\n\n  var initialized = false\n  var initHookQueue = [];\n\n  _.extend(r.access, {\n    init: function() {\n      initialized = true;\n\n      initHookQueue.forEach(function(fn) {\n        fn();\n      });\n\n      initHookQueue.length = 0;\n    },\n\n    isLinkRestricted: function(el) {\n      return false;\n    },\n\n    initHook: function(fn) {\n      if (initialized) {\n        fn();\n      } else {\n        initHookQueue.push(fn);\n      }\n    },\n  });\n}(r);\n"
  },
  {
    "path": "r2/r2/public/static/js/action-forms.js",
    "content": "r.actionForm = {\n  init: function() {\n    $('div.content').on(\n      'click',\n      '.action-thing, .cancel-action-thing',\n      this.toggleActionForm.bind(this)\n    );\n\n    $('div.content').on(\n      'submit',\n      '.action-form',\n      this.submitAction.bind(this)\n    );\n  },\n\n  toggleActionForm: function(e) {\n    var el = e.target;\n    var $el = $(el);\n\n    if (r.access.isLinkRestricted(el)) {\n      return;\n    }\n\n    var $thing = $el.thing();\n    var $thingForm = $thing.find('> .entry .action-form');\n    var formSelector = $el.data('action-form');\n\n    e.stopPropagation();\n    e.preventDefault();\n\n    if ($thingForm.length > 0) {\n      if ($el.parents('.drop-choices').length) {\n        $thingForm.show();\n      } else {\n        $thingForm.toggle();\n      }\n    } else {\n      var $form = $(formSelector);\n      var $clonedForm = $form.clone();\n      var $insertionPoint = $thing.find('> .entry .buttons');\n      var thingFullname = $thing.thing_id();\n\n      $clonedForm.attr('id', 'action-thing-' + thingFullname);\n      $clonedForm.find('input[name=\"thing_id\"]').val(thingFullname);\n      $clonedForm.insertAfter($insertionPoint);\n      $clonedForm.show();\n    }\n  },\n\n  submitAction: function(e) {\n    var $actionForm = $(e.target).thing().find('.action-form');\n    var action = $actionForm.data('form-action');\n\n    return post_pseudo_form($actionForm, action);\n  }\n\n};\n\nr.fraud = {\n\n  init: function() {\n    $('div.content').on(\n      'click',\n      '.action-thing',\n      this.showReason.bind(this)\n    );\n\n    $('div.content').on(\n      'change',\n      '.fraud-action-form input',\n      this.validate.bind(this)\n    );\n  },\n\n  validate: function(e) {\n    var $el = $(e.target);\n    var $form = $el.parents('form');\n    var $submit = $form.find('[type=\"submit\"]');\n    var $refund = $form.find('input[name=refund]');\n    var fraud = $form.find('input[name=fraud]:checked').val();\n    var allowRefund = fraud === 'True';\n\n    if (allowRefund) {\n      $refund.removeAttr('disabled').focus();\n    } else {\n      $refund.prop('checked', false).attr('disabled', 'disabled');\n    }\n\n    if (!!fraud) {\n      $submit.removeAttr('disabled');\n    } else {\n      $submit.attr('disabled', 'disabled');\n    }\n  },\n\n  showReason: function(e) {\n    var $el = $(e.target);\n    var $thing = $el.thing();\n    var $form = $thing.find('> .entry .action-form');\n    var $reason = $form.find('.fraud-reason');\n    var reason = $el.attr('title');\n\n    $reason.text(reason);\n  },\n\n};\n\nr.report = {\n\n  init: function() {\n    $('div.content').on(\n      'change',\n      '.report-action-form input',\n      this.validate.bind(this)\n    );\n\n    $('div.content').on(\n      'click',\n      '.reported-stamp.has-reasons',\n      this.toggleReasons.bind(this)\n    );\n  },\n\n  toggleReasons: function(e) {\n    if (r.access.isLinkRestricted(e.target)) {\n      return;\n    }\n\n    $(e.target).parent().find('.report-reasons').toggle();\n  },\n\n  validate: function(e) {\n    var $thing = $(e.target).thing();\n    var $form = $thing.find('> .entry .report-action-form');\n    var $submit = $form.find('[type=\"submit\"]');\n    var $reason = $form.find('[name=reason]:checked');\n    var $other = $form.find('[name=\"other_reason\"]');\n    var isOther = $reason.val() === 'other';\n\n    $submit.removeAttr('disabled');\n\n    if (isOther) {\n      $other.removeAttr('disabled').focus();\n    } else {\n      $other.attr('disabled', 'disabled');\n    }\n  }\n\n};\n\n$(function () {\n  r.actionForm.init();\n  r.fraud.init();\n  r.report.init();\n});\n"
  },
  {
    "path": "r2/r2/public/static/js/actions.js",
    "content": "!function(r) {\n  r.actions = {\n    trigger: function(actionName, payload) {\n      payload = payload || {};\n      payload.action = actionName;\n      var eventName = 'action:' + actionName;\n      var $e = $.Event(eventName, payload);\n      \n      $(document.body).trigger($e);\n\n      if ($e.isDefaultPrevented()) {\n        $(document.body).trigger($.Event(eventName + ':failure', payload));\n      } else {\n        $(document.body).trigger($.Event(eventName + ':success', payload));\n      }\n      \n      $(document.body).trigger($.Event(eventName + ':complete', payload));\n    },\n\n    on: function(actionName, fn) {\n      $(document.body).on('action:' + actionName, fn);\n    },\n\n    off: function(actionName, fn) {\n      $(document.body).off('action:' + actionName, fn);\n    },\n  };\n}(r);\n"
  },
  {
    "path": "r2/r2/public/static/js/adminbar.js",
    "content": "!function(r, Backbone, store){\n  'use strict'\n\n  var AdminBar = Backbone.View.extend({\n    events: {\n      'click .show-button': 'toggleVisibility',\n      'click .hide-button': 'toggleVisibility',\n      'click .timings-button': 'toggleTimings',\n      'click .expand-button': 'toggleFullTimings',\n      'click .timelines': 'toggleZoom',\n      'click .admin-off': 'adminOff',\n    },\n\n    initialize: function(options) {\n      this.hidden = store.safeGet('adminbar.hidden') === true\n      this.showTimings = store.safeGet('adminbar.timings.show') === true\n      this.showFullTimings = store.safeGet('adminbar.timings.full') === true\n      this.zoomTimings = store.safeGet('adminbar.timings.zoom') !== false\n      this.timingScale = store.safeGet('adminbar.timings.scale') || 8.0\n\n      this.timings = options.timings\n      this.browserTimings = options.browserTimings\n\n      this.serverTimingGraph = new TimingBarGraph({\n        collection: this.timings,\n        el: this.$('.timeline-server'),\n      })\n\n      this.browserTimingGraph = new TimingBarGraph({\n        collection: this.browserTimings,\n        el: this.$('.timeline-browser'),\n      })\n\n      this.timings.on('reset', this.render, this)\n      this.browserTimings.on('reset', this.render, this)\n    },\n\n    adminOff: function() {\n      window.location = '/adminoff'\n    },\n\n    render: function() {\n      this.$el.toggleClass('hidden', this.hidden)\n\n      this.$('.timings-bar')\n        .toggle(this.showTimings)\n        .toggleClass('mini-timings', !this.showFullTimings)\n        .toggleClass('full-timings', this.showFullTimings)\n\n      this.$('.status-bar .timings-button .state')\n        .text(this.showTimings ? '-' : '+')\n\n      this.$('.timings-bar .expand-button')\n        .text(this.showFullTimings ? '-' : '+')\n\n      this.$('.timelines').toggleClass('zoomed', this.zoomTimings)\n\n      $('body').css({\n        'margin-top': this.$el.outerHeight(),\n        'position': 'relative',\n      })\n\n      if (this.timings.isEmpty()) {\n        return\n      }\n\n      var bt = this.browserTimings\n      var browserEndBound = bt.endTime\n\n      if (!this.zoomTimings && (bt.endTime - bt.startTime) < this.timingScale) {\n        browserEndBound = bt.startTime + this.timingScale\n      }\n\n      this.browserTimingGraph.setBounds(bt.startTime, browserEndBound)\n\n      if (this.showFullTimings && !bt.isEmpty()) {\n        this.serverTimingGraph.setBounds(bt.startTime, browserEndBound)\n      } else {\n        var scaleStart = this.timings.startTime\n        var scaleEnd = this.timings.endTime\n\n        if (!this.zoomTimings && (scaleEnd - scaleStart) < this.timingScale) {\n          scaleEnd = scaleStart + this.timingScale\n        }\n\n        this.serverTimingGraph.setBounds(scaleStart, scaleEnd)\n      }\n\n      // if showing full times, avoid rendering until both timelines loaded\n      // to avoid a flicker when the server timing graph rescales.\n      if (!this.showFullTimings || !bt.isEmpty()) {\n        this.serverTimingGraph.render()\n        this.browserTimingGraph.render()\n      }\n    },\n\n    toggleVisibility: function() {\n      this.hidden = !this.hidden\n      store.safeSet('adminbar.hidden', this.hidden)\n      this.render()\n    },\n\n    toggleTimings: function() {\n      this.showTimings = !this.showTimings\n      store.safeSet('adminbar.timings.show', this.showTimings)\n      this.render()\n    },\n\n    toggleFullTimings: function(value) {\n      this.showFullTimings = !this.showFullTimings\n      store.safeSet('adminbar.timings.full', this.showFullTimings)\n      this.render()\n    },\n\n    toggleZoom: function(value) {\n      this.zoomTimings = !this.zoomTimings\n      store.safeSet('adminbar.timings.zoom', this.zoomTimings)\n      this.render()\n    }\n  })\n\n  var TimingBarGraph = Backbone.View.extend({\n    setBounds: function(start, end) {\n      this.options.startBound = start\n      this.options.endBound = end\n    },\n\n    render: function() {\n      var startBound = this.options.startBound || this.collection.startTime\n      var endBound = this.options.endBound || this.collection.endTime\n      var boundDuration = endBound - startBound\n\n      var pos = function(time) {\n        var frac = time / boundDuration\n        return (frac * 100).toFixed(2)\n      }\n\n      if (this.collection.endTime < this.options.startBound) {\n        this.$el.append($('<div class=\"event out-of-bounds\">'))\n        return\n      }\n\n      this.$el.empty()\n\n      var eventsEl = $('<ol class=\"events\">')\n\n      this.collection.each(function(timing) {\n        var key = timing.get('key'),\n          keyParts = key.split('.')\n\n        if (keyParts[keyParts.length-1] === 'total') {\n          return\n        }\n\n        var eventDuration = (timing.get('end') - timing.get('start')).toFixed(2)\n\n        eventsEl.append($('<li class=\"event\">')\n          .addClass(keyParts[0])\n          .addClass(keyParts[1])\n          .addClass(keyParts[2])\n          .attr('title', key + ': ' + eventDuration + 's')\n          .css({\n            left: pos(timing.get('start') - startBound) + '%',\n            right: pos(endBound - timing.get('end')) + '%',\n            zIndex: 1000 - Math.min(800, Math.floor(timing.duration() * 100)),\n          })\n        )\n      }, this)\n\n      this.$el.append(eventsEl)\n\n      var elapsed = this.collection.endTime - this.collection.startTime\n\n      if (elapsed) {\n        this.$el.append($('<span class=\"elapsed\">')\n          .text(elapsed.toFixed(2) + 's'))\n      }\n\n      return this\n    }\n  })\n\n  var timings = new r.Timings()\n  var browserTimings = new r.NavigationTimings()\n\n  var bar = new AdminBar({\n    el: $('#admin-bar'),\n    timings: timings,\n    browserTimings: browserTimings,\n  }).render()\n\n  r.adminbar = {\n    AdminBar: AdminBar,\n    TimingBarGraph: TimingBarGraph,\n    bar: bar,\n  }\n\n  $(function() {\n    if (!r.timings) { return }\n    timings.reset(r.timings)\n\n    setTimeout(function() {\n      browserTimings.fetch()\n    }, 0)\n  })\n}(r, Backbone, store)\n"
  },
  {
    "path": "r2/r2/public/static/js/ajax.js",
    "content": "!function(r, undefined) {\n    r.ajax = function(request) {\n        var url = request.url\n\n        if (request.type == 'GET' && _.isEmpty(request.data)) {\n            var preloaded = r.preload.read(url)\n            if (preloaded != null) {\n                if (request.dataFilter) {\n                    preloaded = request.dataFilter(preloaded, 'json')\n                }\n\n                request.success(preloaded)\n\n                var deferred = new jQuery.Deferred\n                deferred.resolve(preloaded)\n                return deferred\n            }\n        }\n\n        var isLocal = url && (url[0] == '/' || url.lastIndexOf(r.config.currentOrigin, 0) == 0)\n        if (isLocal) {\n            if (!request.headers) {\n                request.headers = {}\n            }\n            request.headers['X-Modhash'] = r.config.modhash\n        }\n\n        return $.ajax(request)\n    };\n}(r);\n"
  },
  {
    "path": "r2/r2/public/static/js/analytics.js",
    "content": "r.analytics = {\n  init: function() {\n    // these guys are relying on the custom 'onshow' from jquery.reddit.js\n    $(document).delegate(\n      '.organic-listing .promotedlink.promoted',\n      'onshow',\n      _.bind(function(ev) {\n        this.fireTrackingPixel(ev.target);\n      }, this)\n    );\n\n    $('.promotedlink.promoted:visible').trigger('onshow');\n\n    // dont track sponsor's activity\n    r.analytics.addEventPredicate('ads', function() {\n      return !r.config.is_sponsor;\n    });\n\n    // virtual page tracking for ads funnel\n    if (r.config.ads_virtual_page) {\n      r.analytics.fireFunnelEvent('ads', r.config.ads_virtual_page);\n    }\n\n    r.analytics.contextData = {\n      dnt: window.DO_NOT_TRACK,\n      language: document.getElementsByTagName('html')[0].getAttribute('lang'),\n      link_id: r.config.cur_link ? r.utils.fullnameToId(r.config.cur_link) : null,\n      loid: null,\n      loid_created: null,\n      referrer_url: document.referrer || '',\n      referrer_domain: null,\n      sr_id: r.config.cur_site ? r.utils.fullnameToId(r.config.cur_site) : null,\n      sr_name: r.config.post_site || null,\n      user_id: null,\n      user_name: null,\n      user_in_beta: r.config.pref_beta,\n    };\n\n    if (r.config.user_id) {\n      r.analytics.contextData.user_id = r.config.user_id;\n      r.analytics.contextData.user_name = r.config.logged;\n    } else {\n      var tracker = new redditlib.Tracker();\n      var loggedOutData = tracker.getTrackingData();\n      if (loggedOutData && loggedOutData.loid) {\n        r.analytics.contextData.loid = loggedOutData.loid;\n        if (loggedOutData.loidcreated) {\n          r.analytics.contextData.loid_created = decodeURIComponent(loggedOutData.loidcreated);\n        }\n      }\n    }\n\n    if (document.referrer) {\n      var referrerDomain = document.referrer.match(/\\/\\/([^\\/]+)/);\n      if (referrerDomain && referrerDomain.length > 1) {\n        r.analytics.contextData.referrer_domain = referrerDomain[1];\n      }\n    }\n\n    if ($('body').hasClass('comments-page')) {\n      r.analytics.contextData.page_type = 'comments';\n    } else if ($('body').hasClass('listing-page')) {\n      r.analytics.contextData.page_type = 'listing';\n\n      if (r.config.cur_listing) {\n        r.analytics.contextData.listing_name = r.config.cur_listing;\n      }\n    }\n\n    if (r.config.feature_screenview_events) {\n      r.analytics.screenviewEvent();\n    }\n\n    if (r.config.expando_preference) {\n      r.analytics.contextData.expando_preference = r.config.expando_preference;\n    }\n\n    if (r.config.pref_no_profanity) {\n      r.analytics.contextData.media_preference_hide_nsfw = r.config.pref_no_profanity\n    }\n\n    r.analytics.firePageTrackingPixel(r.analytics.stripAnalyticsParams);\n\n    r.hooks.get('analytics').call();\n  },\n\n  _eventPredicates: {},\n\n  addEventPredicate: function(category, predicate) {\n    var predicates = this._eventPredicates[category] || [];\n\n    predicates.push(predicate);\n\n    this._eventPredicates[category] = predicates;\n  },\n\n  shouldFireEvent: function(category/*, arguments*/) {\n    var args = _.rest(arguments);\n\n    return !this._eventPredicates[category] ||\n        this._eventPredicates[category].every(function(fn) {\n          return fn.apply(this, args);\n        });\n  },\n\n  _isGALoaded: false,\n\n  isGALoaded: function() {\n    // We've already passed this test, just return `true`\n    if (this._isGALoaded) {\n      return true;\n    }\n\n    // GA hasn't tried to load yet, so we can't know if it\n    // will succeed.\n    if (_.isArray(_gaq)) {\n      return undefined;\n    }\n\n    var test = false;\n\n    _gaq.push(function() {\n      test = true;\n    });\n\n    // Remember the result, so we only have to run this test once\n    // if it passes.\n    this._isGALoaded = test;\n\n    return test;\n  },\n\n  _wrapCallback: function(callback) {\n    var original = callback;\n\n    original.called = false;\n    callback = function() {\n      if (!original.called) {\n        original();\n        original.called = true;\n      }\n    };\n\n    // GA may timeout.  ensure the callback is called.\n    setTimeout(callback, 500);\n\n    return callback;\n  },\n\n  fireFunnelEvent: function(category, action, options, callback) {\n    options = options || {};\n    callback = callback || window.Function.prototype;\n\n    var page = '/' + _.compact([category, action, options.label]).join('-');\n\n    // if it's for Gold tracking and we have new _ga available\n    // then use it to track the event; otherwise, fallback to old version\n    if (options.tracker &&\n        '_ga' in window &&\n        window._ga.getByName &&\n        window._ga.getByName(options.tracker)) {\n      window._ga(options.tracker + '.send', 'pageview', {\n        'page': page,\n        'hitCallback': callback\n      });\n\n      if (options.value) {\n        window._ga(options.tracker + '.send', 'event', category, action, options.label, options.value);\n      }\n\n      return;\n    }\n\n    if (!window._gaq || !this.shouldFireEvent.apply(this, arguments)) {\n      callback();\n      return;\n    }\n\n    var isGALoaded = this.isGALoaded();\n\n    if (!isGALoaded) {\n      callback = this._wrapCallback(callback);\n    }\n\n    // Virtual page views are needed for a funnel to work with GA.\n    // see: http://gatipoftheday.com/you-can-use-events-for-goals-but-not-for-funnels/\n    _gaq.push(['_trackPageview', page]);\n\n    // The goal can have a conversion value in GA.\n    if (options.value) {\n      _gaq.push(['_trackEvent', category, action, options.label, options.value]);\n    }\n\n    _gaq.push(callback);\n  },\n\n  fireGAEvent: function(category, action, opt_label, opt_value, opt_noninteraction, callback) {\n    opt_label = opt_label || '';\n    opt_value = opt_value || 0;\n    opt_noninteraction = !!opt_noninteraction;\n    callback = callback || function() {};\n\n    if (!window._gaq || !this.shouldFireEvent.apply(this, arguments)) {\n      callback();\n      return;\n    }\n\n    var isGALoaded = this.isGALoaded();\n\n    if (!isGALoaded) {\n      callback = this._wrapCallback(callback);\n    }\n\n    _gaq.push(['_trackEvent', category, action, opt_label, opt_value, opt_noninteraction]);\n    _gaq.push(callback);\n  },\n\n  fireTrackingPixel: function(el) {\n    var $el = $(el);\n    var onCommentsPage = $('body').hasClass('comments-page');\n\n    if ($el.data('trackerFired') || onCommentsPage) {\n      return;\n    }\n\n    var pixel = new Image();\n    var impPixel = $el.data('impPixel');\n\n    if (impPixel) {\n      pixel.src = impPixel;\n    }\n\n    if (!adBlockIsEnabled) {\n      var thirdPartyTrackingUrl = $el.data('thirdPartyTrackingUrl');\n      if (thirdPartyTrackingUrl) {\n        var thirdPartyTrackingImage = new Image();\n        thirdPartyTrackingImage.src = thirdPartyTrackingUrl;\n      }\n\n      var thirdPartyTrackingUrl2 = $el.data('thirdPartyTrackingTwoUrl');\n      if (thirdPartyTrackingUrl2) {\n        var thirdPartyTrackingImage2 = new Image();\n        thirdPartyTrackingImage2.src = thirdPartyTrackingUrl2;\n      }\n    }\n\n    var adServerPixel = new Image();\n    var adServerImpPixel = $el.data('adserverImpPixel');\n    var adServerClickUrl = $el.data('adserverClickUrl');\n\n    if (adServerImpPixel) {\n      adServerPixel.src = adServerImpPixel;\n    }\n\n    $el.data('trackerFired', true);\n  },\n\n  fireUITrackingPixel: function(action, srname, extraParams) {\n    var pixel = new Image();\n    pixel.src = r.config.uitracker_url + '?' + $.param(\n      _.extend(\n        {\n          act: action,\n          sr: srname,\n          r: Math.round(Math.random() * 2147483647), // cachebuster\n        },\n        r.analytics.breadcrumbs.toParams(),\n        extraParams\n      )\n    );\n  },\n\n  firePageTrackingPixel: function(callback) {\n    var url = r.config.tracker_url;\n    if (!url) {\n      return;\n    }\n    var params = {\n      dnt: this.contextData.dnt,\n    };\n\n    if (this.contextData.loid) {\n      params.loid = this.contextData.loid;\n    }\n    if (this.contextData.loid_created) {\n      params.loidcreated = decodeURIComponent(this.contextData.loid_created);\n    }\n\n    var querystring = [\n      'r=' + Math.random(),\n    ];\n\n    if (this.contextData.referrer_domain) {\n      querystring.push(\n        'referrer_domain=' + encodeURIComponent(this.contextData.referrer_domain)\n      );\n    }\n\n    for(var p in params) {\n      if (params.hasOwnProperty(p)) {\n        querystring.push(\n          encodeURIComponent(p) + '=' + encodeURIComponent(params[p])\n        );\n      }\n    }\n\n    var pixel = new Image();\n    pixel.onload = pixel.onerror = callback;\n    pixel.src = url + '&' + querystring.join('&');\n  },\n\n  // If we passed along referring tags to this page, after it's loaded, remove them from the URL so that \n  // the user can have a clean, copy-pastable URL. This will also help avoid erroneous analytics if they paste the URL\n  // in an email or something.\n  stripAnalyticsParams: function() {\n    var hasReplaceState = !!(window.history && window.history.replaceState);\n    var params = $.url().param();\n    var stripParams = ['ref', 'ref_source', 'ref_campaign'];\n    // strip utm tags as well\n    _.keys(params).forEach(function(paramKey){\n      if (paramKey.indexOf('utm_') === 0){\n        stripParams.push(paramKey);\n      }\n    });\n\n    var strippedParams = _.omit(params, stripParams);\n\n    if (hasReplaceState && !_.isEqual(params, strippedParams)) {\n      var a = document.createElement('a');\n      a.href = window.location.href;\n      a.search = $.param(strippedParams);\n      if (!a.search) {\n        // Safari leaves a trailing ? when search is empty\n        a.href = a.href.replace(/\\?(#.+)?$/, a.hash);\n      }\n\n      window.history.replaceState({}, document.title, a.href);\n    }\n  },\n\n  addContextData: function(properties, payload) {\n    /* jshint sub: true */\n    payload = payload || {};\n\n    if (this.contextData.user_id) {\n      payload['user_id'] = this.contextData.user_id;\n      payload['user_name'] = this.contextData.user_name;\n    } else {\n      payload['loid'] = this.contextData.loid;\n      payload['loid_created'] = decodeURIComponent(this.contextData.loid_created);\n    }\n\n    properties.forEach(function(contextProperty) {\n      /* jshint eqnull: true */\n      if (this.contextData[contextProperty] != null) {\n        payload[contextProperty] = this.contextData[contextProperty];\n      }\n    }.bind(this));\n\n    return payload;\n  },\n\n  screenviewEvent: function() {\n    var eventTopic = 'screenview_events';\n    var eventType = 'cs.screenview';\n    var payload = this.addContextData([\n      'sr_name',\n      'sr_id',\n      'listing_name',\n      'language',\n      'dnt',\n      'referrer_domain',\n      'referrer_url',\n      'user_in_beta',\n    ]);\n\n    if (r.config.event_target) {\n      for (var key in r.config.event_target) {\n        var value = r.config.event_target[key];\n        if (value !== null) {\n          payload[key] = value;\n        }\n      }\n    }\n\n    var rank_by_link = {};\n    $('.linklisting .thing.link').each(function() {\n        var $thing = $(this);\n        var fullname = $thing.data('fullname');\n        var rank = parseInt($thing.data('rank')) || '';\n        rank_by_link[fullname] = rank;\n    });\n    if (!_.isEmpty(rank_by_link)) {\n        payload['rank_by_link'] = rank_by_link;\n    }\n\n    // event collector\n    r.events.track(eventTopic, eventType, payload).send();\n  },\n\n  loginRequiredEvent: function(actionName, actionDetail, targetType, targetFullname) {\n    var eventTopic = 'login_events';\n    var eventType = 'cs.loggedout_' + actionName;\n    var payload = this.addContextData([\n      'sr_name',\n      'sr_id',\n      'listing_name',\n      'referrer_domain',\n      'referrer_url',\n    ]);\n\n    payload['process_notes'] = 'LOGIN_REQUIRED';\n\n    if (actionDetail) {\n      payload['details_text'] = actionDetail;\n    }\n\n    if (targetType) {\n      payload['target_type'] = targetType;\n    }\n\n    if (targetFullname) {\n      payload['target_fullname'] = targetFullname;\n      payload['target_id'] = r.utils.fullnameToId(targetFullname);\n    }\n\n    // event collector\n    r.events.track(eventTopic, eventType, payload).send();\n  },\n\n  timeoutForbiddenEvent: function(actionName, actionDetail, targetType, targetFullname) {\n    var eventTopic = 'forbidden_actions';\n    var eventType = 'cs.forbidden_' + actionName;\n    var payload = this.addContextData([\n      'sr_name',\n      'sr_id',\n    ]);\n\n    payload['process_notes'] = 'IN_TIMEOUT';\n\n    if (actionDetail) {\n      payload['details_text'] = actionDetail;\n    }\n\n    if (targetType) {\n      payload['target_type'] = targetType;\n    }\n\n    if (targetFullname) {\n      payload['target_fullname'] = targetFullname;\n      payload['target_id'] = r.utils.fullnameToId(targetFullname);\n    }\n\n    // event collector\n    r.events.track(eventTopic, eventType, payload).send();\n  },\n\n  expandoEvent: function(actionName, targetData) {\n    var eventTopic = 'expando_events';\n    var eventType = 'cs.' + actionName;\n    var payload = this.addContextData([\n      'page_type',\n      'listing_name',\n      'referrer_domain',\n      'referrer_url',\n      'expando_preference',\n      'media_preference_hide_nsfw',\n    ]);\n\n    if ('linkIsNSFW' in targetData) {\n      payload['nsfw'] = targetData.linkIsNSFW;\n    }\n\n    if ('linkType' in targetData) {\n      payload['target_type'] = targetData.linkType;\n      \n    }\n\n    if ('provider' in targetData) {\n      payload['provider'] = targetData.provider;\n    }\n\n    if ('linkFullname' in targetData) {\n      payload['target_fullname'] = targetData.linkFullname;\n      payload['target_id'] = r.utils.fullnameToId(targetData.linkFullname);\n    }\n\n    if ('linkCreated' in targetData) {\n      payload['target_create_ts'] = targetData.linkCreated;\n    }\n\n    if ('linkURL' in targetData) {\n      payload['target_url'] = targetData.linkURL;\n    }\n\n    if ('linkDomain' in targetData) {\n      payload['target_url_domain'] = targetData.linkDomain;\n    }\n\n    if ('authorFullname' in targetData) {\n      payload['target_author_id'] = r.utils.fullnameToId(targetData.authorFullname);\n    }\n      \n    if ('subredditName' in targetData) {\n      payload['sr_name'] = targetData.subredditName;\n    }\n\n    if ('subredditFullname' in targetData) {\n      payload['sr_id'] = r.utils.fullnameToId(targetData.subredditFullname);\n    }\n\n    r.events.track(eventTopic, eventType, payload).send();\n  },\n\n  sendEvent: function(eventTopic, actionName, defaultFields, customFields, done) {\n    var eventType = 'cs.' + actionName;\n    var payload = this.addContextData(defaultFields, customFields);\n    r.events.track(eventTopic, eventType, payload).send(done);\n  },\n};\n\nr.analytics.breadcrumbs = {\n  selector: '.thing, .side, .sr-list, .srdrop, .tagline, .md, .organic-listing, .gadget, .sr-interest-bar, .trending-subreddits, a, button, input',\n  maxLength: 3,\n  sendLength: 2,\n\n  init: function() {\n    this.hasSessionStorage = this._checkSessionStorage();\n    this.data = this._load();\n\n    var refreshed = this.data[0] && this.data[0].url == window.location;\n    if (!refreshed) {\n      this._storeBreadcrumb();\n    }\n\n    $(document).delegate('a, button', 'click', $.proxy(function(ev) {\n      this.storeLastClick($(ev.target));\n    }, this));\n  },\n\n  _checkSessionStorage: function() {\n    // Via modernizr.com's sessionStorage check.\n    try {\n      sessionStorage.setItem('__test__', 'test');\n      sessionStorage.removeItem('__test__');\n      return true;\n    } catch(e) {\n      return false;\n    }\n  },\n\n  _load: function() {\n    if (!this.hasSessionStorage) {\n      return [{stored: false}];\n    }\n\n    var data;\n\n    try {\n      data = JSON.parse(sessionStorage.breadcrumbs);\n    } catch (e) {\n      data = [];\n    }\n\n    if (!_.isArray(data)) {\n      data = [];\n    }\n\n    return data;\n  },\n\n  store: function() {\n    if (this.hasSessionStorage) {\n      sessionStorage.breadcrumbs = JSON.stringify(this.data);\n    }\n  },\n\n  _storeBreadcrumb: function() {\n    var cur = {\n      url: location.toString(),\n    };\n\n    if ('referrer' in document) {\n      var referrerExternal = !document.referrer.match('^' + r.config.currentOrigin);\n      var referrerUnexpected = this.data[0] && document.referrer != this.data[0].url;\n\n      if (referrerExternal || referrerUnexpected) {\n        cur.ref = document.referrer;\n      }\n    }\n\n    this.data.unshift(cur);\n    this.data = this.data.slice(0, this.maxLength);\n    this.store();\n  },\n\n  storeLastClick: function(el) {\n    try {\n      this.data[0].click =\n        r.utils.querySelectorFromEl(el, this.selector);\n      this.store();\n    } catch (e) {\n      // Band-aid for Firefox NS_ERROR_DOM_SECURITY_ERR until fixed.\n    }\n  },\n\n  lastClickFullname: function() {\n    var lastClick = _.find(this.data, function(crumb) {\n      return crumb.click;\n    });\n\n    if (lastClick) {\n      var match = lastClick.click.match(/.*data-fullname=\"(\\w+)\"/);\n      return match && match[1];\n    }\n  },\n\n  toParams: function() {\n    params = [];\n    for (var i = 0; i < this.sendLength; i++) {\n      _.each(this.data[i], function(v, k) {\n        params['c' + i + '_' + k] = v;\n      });\n    }\n    return params;\n  },\n\n};\n\n\nr.hooks.get('setup').register(function() {\n  r.analytics.breadcrumbs.init();\n});\n\n"
  },
  {
    "path": "r2/r2/public/static/js/apps.js",
    "content": "r.apps = {\n  init: function() {\n    $('.authorized-app')\n        .delegate('.app-permissions li', 'mouseover mouseout',\n                  function(e) {\n                    if (e.type == 'mouseover') {\n                      $(this).find('.app-scope').show()\n                    } else {\n                      $(this).find('.app-scope').hide()\n                    }\n                  })\n\n    $('#developed-apps')\n        .delegate('.edit-app-button', 'click',\n                  function() {\n                    $(this).toggleClass('collapsed').closest('.developed-app')\n                        .removeClass('collapsed')\n                        .find('.app-developers').remove().end()\n                        .find('.edit-app')\n                          .slideToggle().removeClass('collapsed').end()\n                  })\n        .delegate('.edit-app-icon-button', 'click',\n                  function() {\n                    $(this).toggleClass('collapsed')\n                        .closest('.developed-app')\n                            .find('.ajax-upload-form').show()\n                  })\n\n    $('#create-app-button').click(\n        function() {\n            $(this).hide()\n            $('#create-app').fadeIn()\n        })\n  },\n\n  revoked: function (elem, op) {\n      $(elem).closest('.authorized-app').fadeOut()\n  },\n\n  deleted: function (elem, op) {\n      $(elem).closest('.developed-app').fadeOut()\n  }\n}\n"
  },
  {
    "path": "r2/r2/public/static/js/archived.js",
    "content": "/*\n  If a link is too old, don't allow commenting or voting.\n */\n!function(r) {\n  r.archived = {\n    init: function() {\n      this._popup = r.ui.createGatePopup({\n        templateId: 'archived-popup',\n        className: 'archived-error-modal',\n      });\n\n      $('body').on('click', '.archived', this._handleClick.bind(this));\n    },\n\n    _handleClick: function(e) {\n      this._popup.show();\n      return false;\n    },\n  };\n\n  r.hooks.get('reddit').register(function() {\n    r.archived.init();\n  });\n}(r);\n"
  },
  {
    "path": "r2/r2/public/static/js/backbone-init.js",
    "content": "!function(r, undefined) {\n  r.sync = function(method, model, options) {\n    var wrappedDataFilter = options.dataFilter\n    options.dataFilter = function(data, type) {\n      var filteredData\n\n      if (type === 'json') {\n        filteredData = r.utils.unescapeJson(data)\n      } else {\n        filteredData = data\n      }\n\n      if (wrappedDataFilter) {\n        return wrappedDataFilter(filteredData)\n      } else {\n        return filteredData\n      }\n    }\n    return r.backboneSync.call(Backbone, method, model, options)\n  }\n\n  r.setupBackbone = function() {\n      Backbone.emulateJSON = true\n      Backbone.ajax = r.ajax\n\n      if (!r.backboneSync) {\n          r.backboneSync = Backbone.sync\n          Backbone.sync = r.sync\n      }\n  }\n}(r);\n"
  },
  {
    "path": "r2/r2/public/static/js/base.js",
    "content": "r = window.r || {}\n"
  },
  {
    "path": "r2/r2/public/static/js/bootstrap.tooltip.extension.js",
    "content": "(function ($) {\n\n  var base = $.fn.tooltip.Constructor.prototype;\n  var _getCalculatedOffset = base.getCalculatedOffset;\n\n  base.getCalculatedOffset = function(placement, pos, actualWidth, actualHeight) {\n    var offset;\n\n    if (placement === 'top-right') {\n        offset = {top: pos.top - actualHeight - 10, left: pos.left + pos.width - actualWidth};\n        if (this.$element.outerWidth() <= 18) {\n          offset.left += 4;\n        }\n    } else {\n        return _getCalculatedOffset.apply(this, arguments);\n    }\n\n    return offset;\n  };\n\n  base.replaceArrow = function(delta, dimension, isHorizontal) {\n    var offsetMultiplier = (1 - delta / dimension);\n\n    if (offsetMultiplier !== 1) {\n      this.arrow()\n        .css(isHorizontal ? 'left' : 'top', 50 * offsetMultiplier + '%');\n    }\n\n    this.arrow()\n      .css(isHorizontal ? 'top' : 'left', '');\n  };\n\n})(window.jQuery);\n"
  },
  {
    "path": "r2/r2/public/static/js/cache-poisoning-detection.js",
    "content": "!function(global, $, r) {\n  'use strict'\n\n  var CANARY_COOKIE = 'pc';\n\n  /***\n   * Generate a random canary to use to detect cache poisoning\n   *\n   * Stuffing the username into a cookie and comparing against `r.config.logged`\n   * would work, but cleanup would me messier. There are also extensions\n   * that swap out user credentials themselves that might not take the canary\n   * cookie into account. For that reason, we use a persistent, random id as\n   * the canary.\n   *\n   * There are 36**2 (1296) possible canaries, large enough to make\n   * false-negatives unlikely, but small enough at reddit's scale\n   * to make them not uniquely identifying. Essentially, we eat the\n   * possibility of missing 1 in every 1296 instances of poisoned caches to\n   * protect privacy.\n   */\n  function generateCanary() {\n    return randString(2);\n  }\n\n  function randString(stringLen) {\n    var id = '';\n    var chars = 'abcdefghijklmnopqrstuvwxyz0123456789';\n\n    for (var i = 0; i < stringLen; i++) {\n      id += chars.charAt(Math.floor(Math.random() * chars.length));\n    }\n\n    return id;\n  }\n\n  r.cachePoisoning = {};\n\n  /***\n   * Create or refresh the poisoning canary cookie\n   */\n  r.cachePoisoning.updateCanaryCookie = function() {\n    var canary = $.cookie(CANARY_COOKIE);\n    if (!canary) {\n      canary = generateCanary();\n    }\n\n    // Clear out busted path-relative cookies\n    $.cookie(CANARY_COOKIE, null, {\n        secure: r.config.https_forced,\n        domain: r.config.cur_domain,\n    });\n\n    // Create the cookie, or extend the lifetime of the existing one\n    $.cookie(CANARY_COOKIE, canary, {\n        secure: r.config.https_forced,\n        domain: r.config.cur_domain,\n        path: '/',\n        expires: 365,\n    });\n  };\n\n  /***\n   * Check if this is likely a response from a poisoned cache\n   */\n  r.cachePoisoning.checkPoisoned = function() {\n    var clientCanary = $.cookie(CANARY_COOKIE);\n    if (clientCanary && r.config.poisoning_canary) {\n      if(clientCanary !== r.config.poisoning_canary) {\n        return true;\n      }\n    }\n    return false;\n  };\n\n  /***\n   * Parse the header list from a string to a `{name -> [value, ...]` map\n   */\n  r.cachePoisoning._parseHeaders = function(headersString) {\n    // based on jQuery's ajax.js\n    var rheaders = /^(.*?):[ \\t]*([^\\r\\n]*)$/mg;\n    var headersHash = {};\n    var match;\n    while ( (match = rheaders.exec( headersString )) ) {\n      var headerName = match[1].toLowerCase();\n      if (headersHash[headerName] === undefined) {\n        headersHash[headerName] = [];\n      }\n      headersHash[headerName].push(match[2]);\n    }\n    return headersHash;\n  };\n\n  r.cachePoisoning.logPoisoning = function() {\n    var poisonDetails = {\n      // A MAC tied to the poisoner and their canary\n      report_mac: r.config.poisoning_report_mac,\n      poisoner_name: r.config.logged,\n      poisoner_id: r.config.user_id,\n      // So we can test if one cache policy is more poisonous than another\n      cache_policy: r.config.cache_policy,\n      poisoner_canary: r.config.poisoning_canary,\n      victim_canary: $.cookie(CANARY_COOKIE),\n      // So we know just how stale the cached response was\n      render_time: r.config.server_time,\n      route_name: r.config.pageInfo.actionName,\n      source: 'web',\n      url: window.location.href || '',\n      resp_headers: {},\n    };\n\n    // re-request the page via XHR so we can get the value of\n    // the various caching headers (`CF-Cache-Status`, etc.)\n    // Note that this will be missing `Set-Cookie` headers as well as a few\n    // others, we need to use Flash sockets if we want them.\n    // this would be fairly straightforward, but a lot of work.\n    $.ajax({\n      url: window.location.href,\n      // Stop jQuery from adding the `X-Requested-With` header in case caches\n      // key on it.\n      xhr: function() {\n          // Get new xhr object using default factory\n          var xhr = jQuery.ajaxSettings.xhr();\n          // Copy the browser's native setRequestHeader method\n          var setRequestHeader = xhr.setRequestHeader;\n          // Replace with a wrapper\n          xhr.setRequestHeader = function(name, value) {\n              if (name === 'X-Requested-With') return;\n              setRequestHeader.call(this, name, value);\n          }\n          // pass it on to jQuery\n          return xhr;\n      },\n      complete: function(xhr) {\n        // modhashes change with every response, if the modhash in the response\n        // has changed, the response was different and we can't use it to guess\n        // the headers sent with _this page's_ response. This could use any\n        // identifier that's unique to a single response, though.\n        if ((xhr.responseText || \"\").indexOf(r.config.modhash) !== -1) {\n          // jQuery only gives us the string-serialized response headers, parse\n          // them back to a hash.\n          var hdrs = xhr.getAllResponseHeaders();\n          poisonDetails.resp_headers = r.cachePoisoning._parseHeaders(hdrs);\n        }\n\n        poisonDetails.resp_headers = JSON.stringify(poisonDetails.resp_headers);\n        r.ajax({\n          type: 'POST',\n          url: '/web/poisoning.json',\n          data: poisonDetails,\n          headers: {\n            'X-Loggit': true,\n          },\n          success: function() {\n            r.log('Sent cache poisoning report to server');\n          },\n          error: function(xhr, err, status) {\n            r.warn('Error sending cache poisoning report to server');\n          }\n        });\n      }\n    });\n  }\n\n  r.cachePoisoning.init = function() {\n    if (r.config.logged) {\n      if (r.cachePoisoning.checkPoisoned()) {\n        r.cachePoisoning.logPoisoning();\n      }\n    }\n    r.cachePoisoning.updateCanaryCookie();\n  };\n\n}(window, jQuery, r);\n\n"
  },
  {
    "path": "r2/r2/public/static/js/client-error-logger.js",
    "content": "!function(global, $, r, _) {\n  'use strict'\n\n  var oldOnError = global.onerror\n\n  global.onerror = function(message, file, line, character, errorType) {\n    // Don't log errors from userscripts and plugins.\n    var badFileNameRegex = /^(chrome:\\/\\/|file:\\/\\/)/i\n\n    // These are special messages fired by some firefox plugins and bad\n    // browsers.\n    var badMessageRegex = /((^Script error\\.$)|(atomicFindClose))/i\n\n    if (badFileNameRegex.test(file) || badMessageRegex.test(message)) {\n      return\n    }\n\n    var exception = {\n      message: message,\n      file: file,\n      line: line,\n      character: character,\n      errorType: errorType,\n    }\n\n    r.logging.sendException(exception)\n\n    if (oldOnError) {\n      oldOnError.apply(global, arguments)\n    }\n  }\n}(this, jQuery, r, _)\n\n"
  },
  {
    "path": "r2/r2/public/static/js/compact.js",
    "content": "/*This hides the url bar on mobile*/\n(function($) {\n    $.fn.show_toolbar = function() {\n        var tb = this;\n        $(this).show();\n    };\n    $.unsafe_orig = $.unsafe;\n    $.unsafe = function(text) {\n        /* inverts websafe filtering of reddit app. */\n        text = $.unsafe_orig(text);\n        if (typeof(text) == \"string\") {\n            /* space compress the result */\n            text = r.utils.spaceCompress(text);\n        }\n        return (text || \"\");\n    }\n})(jQuery);\n\n$(function() {\n    if ($(window).scrollTop() == 0) {\n        $(window).scrollTop(1);\n    }\n    ;\n    /* Top menu dropdown*/\n    $('#topmenu_toggle').click(function() {\n        $(this).toggleClass(\"active\");\n        $('#top_menu').toggle();\n        return false;\n    });\n    //Self text expando\n    $(document).on('click', '.expando-button', function() {\n        $(this).toggleClass(\"expanded\");\n        $(this).thing().find(\".expando\").toggle();\n        return false;\n    });\n    //Help expando\n    $(document).on('click', '.help-toggle', function() {\n        $(this).toggleClass(\"expanded\");\n        $(this).parent().parent().siblings(\".markhelp-parent\").toggle();\n        return false;\n    });\n\n    //Options expando\n    $(document).on('click', '.options_link', function(evt) {\n\n        if (! $(this).siblings(\".options_expando\").hasClass('expanded')) {\n            $('.options_expando.expanded').each(function() { //Collapse any other open ones\n                $(this).removeClass('expanded');\n            });\n            $('.options_link.active').each(function() {\n               $(this).removeClass('active');\n            });\n            $(this).siblings(\".options_expando\").addClass('expanded'); //Expand this one\n            $(this).addClass('active');\n        } else {\n             $(this).siblings(\".options_expando\").removeClass('expanded'); //Just collapse this one\n             $(this).removeClass('active');\n        }\n        return false;\n    });\n    //Save button state transition\n    $(document).on(\"click\", \".save-button\", function() {\n        $(this).toggle();\n        $(this).siblings(\".unsave-button\").toggle();\n    });\n    $(document).on(\"click\", \".unsave-button\", function() {\n        $(this).toggle();\n        $(this).siblings(\".save-button\").toggle();\n    });\n    //Hide options when we collapse\n    $(document).on(\"click\", '.options_expando .collapse-button', function() {\n        $(this).parent().removeClass('expanded');\n        $(this).parent().parent().parent().addClass(\"collapsed\");\n        $(this).parent().siblings('.options_link').removeClass(\"active\");\n    });\n    //Collapse when we click reply, or edit\n    $(document).on(\"click\", '.reply-button, .edit-button', function() {\n        $(this).parent().siblings('.options-link').click();\n    });\n\n    $(document).on(\"click\", \".link\", function(evt) {\n        if (evt && evt.target && $(evt.target).hasClass(\"thing\")) {\n            $(this).find(\".options_link\").click();\n            return false;\n        }\n    });\n    //Comment options\n    $(document).on(\"click\", \".comment.collapsed\", function(e) {\n        $(this).removeClass(\"collapsed\");\n    });\n    $(document).on(\"click\", \".message.unread\", function(e) {\n        var thing = $(this)\n        read_thing(thing);\n        return false;\n    });\n    /*Finally*/\n    $(document).on('click', 'a[href=#]', function() {\n        return false;\n    });\n});\n\n$(function() {\n    var eut = edit_usertext;\n    edit_user_text = function(what) {\n        $(what).parent().parent().toggleClass('hidden');\n        return eut(what);\n    };\n\n});\n\nfunction show_edit_usertext(form) {\n    var edit = form.find(\".usertext-edit\");\n    var body = form.find(\".usertext-body\");\n    var textarea = edit.find('div > textarea');\n    //we need to show the textbox first so it has dimensions\n    body.hide();\n    edit.show();\n\n    form\n            .find(\".cancel, .save\").show().end()\n            .find(\".help-toggle\").show().end();\n\n    textarea.focus();\n}\n\nfunction fetch_more() {\n    $(\"#siteTable\").after($(\"<div class='loading'><img src='\" + r.utils.staticURL('reddit_loading.png') + \"'/></div>\"));\n\n\n    var o = document.location;\n    var path = o.pathname.split(\".\");\n    if (path[path.length - 1].indexOf('/') == -1) {\n        path = path.slice(0, -1).join('.');\n    } else {\n        path = o.pathname;\n    }\n    var apath = o.protocol + \"//\" + o.host + path + \".json-compact\" + o.search;\n    var last = $(\"#siteTable\").find(\".thing:last\");\n    apath += ((document.location.search) ? \"&\" : \"?\") +\n            \"after=\" + last.thing_id();\n\n    if (last.find(\".rank\").length)\n        \"&count=\" + parseInt(last.find(\".rank\").html())\n\n    $.getJSON(apath, function(res) {\n        $.insert_things(res.data, true);\n        $(\".thing\").click(function() {\n        });\n        /* remove the loading image */\n        $(\"#siteTable\").next(\".loading\").remove();\n        if (res && res.data.length == 0) {\n            $(window).unbind(\"scroll\");\n        }\n    });\n}\n\n$(function() {\n    if (!!store.safeGet('mobile-web-redirect-opted')) {\n        return;\n    }\n\n    var $bar = $('.mobile-web-redirect-bar');\n\n    $bar.find('.mobile-web-redirect-optin').on('click', function() {\n        store.safeSet('mobile-web-redirect-opted', true);\n    });\n\n    $bar.find('.mobile-web-redirect-optout').on('click', function(e) {\n        e.preventDefault();\n        store.safeSet('mobile-web-redirect-opted', true);\n        $bar.fadeOut();\n    });\n\n    $bar.show();\n});\n"
  },
  {
    "path": "r2/r2/public/static/js/custom-event.js",
    "content": ";(function(window, undefined) {\n  if (window.CustomEvent) {\n    return;\n  }\n\n  function CustomEvent(eventName, options) {\n    options = options || {bubbles: false, cancelable: false, detail: undefined};\n\n    var e = document.createEvent('CustomEvent');\n\n    e.initCustomEvent(eventName, options.bubbles, options.cancelable, options.detail);\n\n    return e;\n  }\n\n  CustomEvent.prototype = window.Event.prototype;\n\n  window.CustomEvent = CustomEvent;\n})(this);\n"
  },
  {
    "path": "r2/r2/public/static/js/do-not-track.js",
    "content": "!(function(global, undefined) {\n  var RE_IE_VERSION = /(?:\\b(?:MS)?IE\\s+|\\bTrident\\/7\\.0;.*\\s+rv:)(\\d+(?:\\.?\\d+)?)/i;\n  var uaMatches = navigator.userAgent.match(RE_IE_VERSION);\n  var ieVersion = uaMatches && uaMatches[1] && parseFloat(uaMatches[1]);\n  var doNotTrack = navigator.doNotTrack ||\n    global.doNotTrack ||\n    navigator.msDoNotTrack;\n\n  global.DO_NOT_TRACK = /^(yes|1)$/i.test(doNotTrack) && ieVersion !== 10;\n\n})(this);\n"
  },
  {
    "path": "r2/r2/public/static/js/edit-subreddit-rules.js",
    "content": "/*\nrequires Backbone\nrequires r.errors\nrequires r.models.SubredditRule\nrequires r.models.SubredditRuleCollection\nrequires r.ui.TextCounter\n */\n\n!function(r, Backbone, undefined) {\n  var SubredditRuleBaseView = Backbone.View.extend({\n    countersInitialized: false,\n    state: '',\n\n    DEFAULT_STATE: '',\n    EDITING_STATE: 'editing',\n\n    events: {\n      'click .subreddit-rule-delete-button': function onDelete(e) {\n        e.preventDefault();\n        this.delete();\n      },\n\n      'click .subreddit-rule-edit-button': function onEdit(e) {\n        e.preventDefault();\n        this.edit();\n      },\n\n      'click .subreddit-rule-cancel-button': function onCancel(e) {\n        e.preventDefault();\n        this.cancel();\n      },\n\n      'submit form': function onSubmit(e) {\n        e.preventDefault();\n        this.submit();\n      },\n    },\n\n    initialize: function(options) {\n      this.formTemplate = options.formTemplate;\n    },\n\n    delegateEvents: function() {\n      SubredditRuleBaseView.__super__.delegateEvents.apply(this, arguments);\n\n      this.listenTo(this.model, 'request', this.disableForm);\n      this.listenTo(this.model, 'invalid', function(model, error) {\n        this.model.revert();\n        this.showErrors([error]);\n      });\n      this.listenTo(this.model, 'error', function(model, errors) {\n        this.model.revert();\n        this.showErrors(errors);\n      });\n    },\n\n    setState: function(state) {\n      if (this.state !== state) {\n        this.state = state;\n        this.render();\n      }\n    },\n\n    initCounters: function() {\n      if (this.countersInitialized) {\n        return;\n      }\n\n      var shortNameCounter = this.$el.find('.form-group-short_name')[0];\n      var descriptionCounter = this.$el.find('.form-group-description')[0];\n      \n      if (!shortNameCounter) {\n        return;\n      }\n\n      this.shortNameCounter = new r.ui.TextCounter({\n        el: shortNameCounter,\n        maxLength: this.model.SHORT_NAME_MAX_LENGTH,\n        initialText: this.model.get('short_name'),\n      });\n      this.descriptionCounter = new r.ui.TextCounter({\n        el: descriptionCounter,\n        maxLength: this.model.DESCRIPTION_MAX_LENGTH,\n        initialText: this.model.get('description'),\n      });\n      this.countersInitialized = true;\n    },\n\n    removeCounters: function() {\n      if (!this.countersInitialized) {\n        return;\n      }\n\n      this.shortNameCounter.remove();\n      this.descriptionCounter.remove();\n      this.shortNameCounter = null;\n      this.shortNameCounter = null;\n      this.countersInitialized = false;\n    },  \n  \n    delete: function() {\n      this.model.destroy();\n    },\n\n    submit: function() {\n      var $form = this.$el.find('form');\n      var formData = get_form_fields($form);\n      this.model.save(formData);\n    },\n\n    disableForm: function() {\n      r.errors.clearAPIErrors(this.$el);\n      this.$el.find('input, button')\n              .attr('disabled', true);\n    },\n\n    enableForm: function() {\n      this.$el.find('input, button')\n              .removeAttr('disabled');\n    },\n\n    showErrors: function(errors) {\n      r.errors.clearAPIErrors(this.$el);\n      r.errors.showAPIErrors(this.$el, errors);\n      this.enableForm();\n    },\n\n    render: function() {\n      this.removeCounters();\n      this.renderTemplate();\n      this.initCounters();\n    },\n\n    focus: function() {\n      this.$el.find('input, textarea').get(0).focus();\n    },\n  });\n\n\n  var SubredditRuleView = SubredditRuleBaseView.extend({\n    DELETING_STATE: 'deleting',\n\n    initialize: function(options) {\n      SubredditRuleView.__super__.initialize.apply(this, arguments);\n      this.ruleTemplate = options.ruleTemplate;\n    },\n\n    delegateEvents: function() {\n      SubredditRuleView.__super__.delegateEvents.apply(this, arguments);\n      this.listenTo(this.model, 'sync:update', this.cancel);\n      this.listenTo(this.model, 'sync:delete', this.remove);\n    },\n\n    delete: function() {\n      if (this.state === this.DELETING_STATE) {\n        this.model.destroy();\n      } else if (this.state === this.DEFAULT_STATE) {\n        this.setState(this.DELETING_STATE);\n      }\n    },\n\n    edit: function() {\n      if (this.state === this.DEFAULT_STATE) {\n        this.setState(this.EDITING_STATE);\n        this.focus();\n      }\n    },\n\n    cancel: function() {\n      if (this.state === this.EDITING_STATE ||\n          this.state === this.DELETING_STATE) {\n        this.$el.removeClass('mod-action-deleting');\n        this.setState(this.DEFAULT_STATE);\n      }\n    },\n\n    render: function() {\n      SubredditRuleView.__super__.render.call(this);\n\n      if (this.state === this.DELETING_STATE) {\n        this.$el.addClass('mod-action-deleting');\n        this.$el.find('.subreddit-rule-delete-confirmation').removeAttr('hidden');\n        this.$el.find('.subreddit-rule-buttons button').attr('disabled', true);\n      }\n      if (this.state === this.EDITING_STATE) {\n        this.$el.find('.form-group-kind input[value=' + this.model.get('kind') + ']').prop('checked', true);\n      }\n    },\n\n    renderTemplate: function() {\n      var modelData = this.model.toJSON();\n      var kind = modelData.kind;\n\n      modelData.kind = r.config.kind_labels[kind];\n\n      if (this.state === this.EDITING_STATE) {\n        this.$el.html(this.formTemplate(modelData));\n      } else {\n        this.$el.html(this.ruleTemplate(modelData));\n      }\n    },\n  });\n\n\n  var AddSubredditRuleView = SubredditRuleBaseView.extend({\n    DISABLED_STATE: 'disabled',\n\n    initialize: function(options) {\n      AddSubredditRuleView.__super__.initialize.apply(this, arguments);\n      this.collection = options.collection;\n      this.initializeNewModel();\n      this.$collapsedDisplay = this.$el.find('.subreddit-rule-add-form-buttons');\n      this.$maxRulesNotice = this.$collapsedDisplay.find('.subreddit-rule-too-many-notice');\n\n      this.$el.removeAttr('hidden');\n\n      if (this.collection._disabled) {\n        this.setState(this.DISABLED_STATE);\n      }\n    },\n\n    delegateEvents: function() {\n      AddSubredditRuleView.__super__.delegateEvents.apply(this, arguments);\n      this.listenTo(this.collection, 'enabled', function() {\n        if (this.state === this.DISABLED_STATE) {\n          this.setState(this.DEFAULT_STATE);\n        }\n      })\n      this.listenTo(this.collection, 'disabled', function() {\n        this.setState(this.DISABLED_STATE);\n      });\n      this.listenTo(this.model, 'sync:create', this._handleRuleCreated);\n    },\n\n    initializeNewModel: function() {\n      var Model = this.collection.model;\n      this.model = new Model(undefined, { collection: this.collection });\n    },\n\n    _handleRuleCreated: function(model) {\n      this.undelegateEvents();\n      this.initializeNewModel();\n      this.delegateEvents();\n      this.cancel();\n      this.trigger('success', model);\n    },\n\n    edit: function() {\n      if (this.state === this.DEFAULT_STATE) {\n        this.setState(this.EDITING_STATE);\n      }\n    },\n\n    cancel: function() {\n      if (this.state === this.EDITING_STATE) {\n        this.setState(this.DEFAULT_STATE);\n      }\n    },\n\n    render: function() {\n      this.$collapsedDisplay.detach();\n      AddSubredditRuleView.__super__.render.call(this);\n\n      if (this.state === this.DISABLED_STATE) {\n        this.$maxRulesNotice.removeAttr('hidden');\n        this.$el.append(this.$collapsedDisplay);\n        this.disableForm();\n      } else if (this.state === this.DEFAULT_STATE) {\n        this.$maxRulesNotice.attr('hidden', true);\n        this.$el.append(this.$collapsedDisplay);\n        this.enableForm();\n      } else if (this.state === this.EDITING_STATE) {\n        this.focus();\n      }\n    },\n\n    renderTemplate: function() {\n      if (this.state !== this.EDITING_STATE) {\n        this.$el.empty();\n      } else {\n        var modelData = this.model.toJSON();\n        this.$el.html(this.formTemplate(modelData));\n      }\n    },\n  });\n\n  \n  var SubredditRulesPage = Backbone.View.extend({\n    initialize: function(options) {\n      this.ruleTemplate = options.ruleTemplate;\n      this.formTemplate = options.formTemplate;\n      var collectionOptions = {\n        subredditName: r.config.post_site,\n        subredditFullname: r.config.cur_site,\n      };\n      this.collection = new r.models.SubredditRuleCollection(null, collectionOptions);\n\n      this.newRuleForm = new AddSubredditRuleView({\n        el: options.addForm,\n        collection: this.collection,\n        formTemplate: this.formTemplate,\n      });\n\n      // initialize views for the rules prerendered on the page\n      var ruleItems = this.$el.find('.subreddit-rule-item').toArray();\n      ruleItems.forEach(function(el) {\n        var model = this.createSubredditRuleModel(el);\n        this.createSubredditRuleView(el, model);\n      }, this);\n\n      if (!this.collection.length) {\n        this.newRuleForm.edit();\n      }\n\n      r.hooks.get('new-report-form').register(function() {\n        this._updateRuleCache();\n      }.bind(this));\n    },\n\n    delegateEvents: function() {\n      SubredditRulesPage.__super__.delegateEvents.apply(this, arguments);\n\n      this.listenTo(this.newRuleForm, 'success', function(model) {\n        var props = model.toJSON();\n        var newModel = new r.models.SubredditRule(props);\n        this.addNewRule(newModel);\n      });\n\n      this.listenTo(this.collection, 'sync', function() {\n        this._updateRuleCache();\n      });\n    },\n\n    createSubredditRuleModel: function(el) {\n      var $el = $(el);\n\n      return new r.models.SubredditRule({\n        priority: parseInt($el.data('priority'), 10),\n        short_name: $el.find('.subreddit-rule-title').text(),\n        description: $el.data('description'),\n        description_html: $el.find('.subreddit-rule-description').html(),\n        kind: $el.data('kind'),\n      });\n    },\n\n    createSubredditRuleView: function(el, model) {\n      this.collection.add(model);\n\n      return new SubredditRuleView({\n        el: el,\n        model: model,\n        ruleTemplate: this.ruleTemplate,\n        formTemplate: this.formTemplate,\n      });\n    },\n\n    addNewRule: function(model) {\n      var el = $.parseHTML('<div class=\"subreddit-rule-item\"></div>')[0];\n      var view = this.createSubredditRuleView(el, model);\n      view.render();\n      this.$el.append(el);\n    },\n\n    _updateRuleCache: function() {\n      try {\n        var newRules = this.collection.toApiJSON();\n        var storageKey = r.rulesSessionStorageKey;\n        var rulesCache = window.sessionStorage.getItem(storageKey);\n        rulesCache = rulesCache ? JSON.parse(rulesCache) : {};\n        rulesCache[this.collection.subredditFullname] = newRules;\n        rulesCache = JSON.stringify(rulesCache);\n        window.sessionStorage.setItem(storageKey, rulesCache);\n      } catch (err) {\n      }\n    },\n  });\n\n\n  $(function() {\n    var $page = $('.subreddit-rules-page');\n\n    if (!$page.hasClass('editable')) {\n      return;\n    }\n\n    var ruleTemplate = document.getElementById('subreddit-rule-template');\n    var formTemplate = document.getElementById('subreddit-rule-form-template');\n    var addForm = document.getElementById('subreddit-rule-add-form');\n    var ruleList = document.getElementById('subreddit-rule-list');\n\n    if (!ruleTemplate || !formTemplate) {\n      throw 'Subreddit rule templates not found!';\n    }\n\n    new SubredditRulesPage({\n      el: ruleList,\n      addForm: addForm,\n      ruleTemplate: _.template(ruleTemplate.innerHTML),\n      formTemplate: _.template(formTemplate.innerHTML),\n    });\n  });\n}(r, Backbone);\n"
  },
  {
    "path": "r2/r2/public/static/js/embed/comment-embed.js",
    "content": ";(function(App, window, undefined) {\n\n  var RE_ABS = /^https?:\\/\\//i;\n  var RE_COMMENT = /\\/?r\\/[\\w_]+\\/comments\\/(?:\\S+\\/){2,}[\\w_]+\\/?/i;\n  var PROTOCOL = location.protocol === 'file:' ? 'https:' : '';\n\n  function isComment(anchor) {\n    return RE_ABS.test(anchor.href) && RE_COMMENT.test(anchor.pathname);\n  }\n\n  function getCommentPathname(anchor) {\n    return isComment(anchor) && anchor.pathname.replace(/^\\//, '');\n  }\n\n  function getCommentUrl(links, host) {\n    var pathname;\n\n    for (var i = 0, l = links.length; i < l; i++) {\n      if ((pathname = getCommentPathname(links[i]))) {\n        break;\n      }\n    }\n\n    return '//' + host + '/' + pathname;\n  }\n\n  function getEmbedUrl(commentUrl, el) {\n    var context = 0;\n    var showedits = el.getAttribute('data-embed-live');\n\n    if (el.getAttribute('data-embed-parent') === 'true') {\n      context++;\n    }\n\n    var query = 'embed=true' +\n                '&context=' + context +\n                '&depth=' + (++context) +\n                '&showedits=' + (showedits === 'true') +\n                '&created=' + el.getAttribute('data-embed-created') +\n                '&showmore=false';\n\n    return PROTOCOL + (commentUrl.replace(/\\/$/,'')) + '?' + query;\n  }\n\n  App.init = function(options, callback) {\n    options = options || {};\n    callback = callback || function() {};\n\n    var embeds = document.querySelectorAll('.reddit-embed');\n\n    [].forEach.call(embeds, function(embed) {\n      if (embed.getAttribute('data-initialized')) {\n        return;\n      }\n\n      embed.setAttribute('data-initialized', true);\n\n      var iframe = document.createElement('iframe');\n      var anchors = embed.getElementsByTagName('a');\n      var commentUrl = getCommentUrl(anchors, embed.getAttribute('data-embed-media'));\n\n      if (!commentUrl) {\n        return;\n      }\n\n      r.frames.addPostMessageOrigin(embed.getAttribute('data-embed-media'));\n      r.frames.listen('embed');\n\n      iframe.height = iframe.style.height = 0;\n      iframe.width = iframe.style.width = '100%';\n      iframe.scrolling = 'no';\n      iframe.frameBorder = 0;\n      iframe.allowTransparency = true;\n      iframe.style.display = 'none';\n      iframe.style.maxWidth = '800px';\n      iframe.style.minWidth = '220px';\n      iframe.style.margin = '10px 0';\n      iframe.style.borderRadius = '5px';\n      iframe.style.boxShadow = '0 0 5px 0.5px rgba(0, 0, 0, 0.05)';\n      iframe.style.borderColor = 'rgba(199,199,199, 0.55)';\n      iframe.style.borderWidth = '1px';\n      iframe.style.borderStyle = 'solid';\n      iframe.style.boxSizing = 'border-box';\n      iframe.src = getEmbedUrl(commentUrl, embed);\n\n      r.frames.receiveMessageOnce(iframe, 'ping.embed', function(e) {\n        embed.parentNode.removeChild(embed);\n        iframe.style.display = 'block';\n\n        callback(e);\n        r.frames.postMessage(iframe.contentWindow, 'pong.embed', {\n          type: embed.getAttribute('data-embed-parent') === 'true' ?\n            'comment_and_parent' : 'comment',\n          location: location,\n          options: options,\n        });\n      });\n\n      var resizer = r.frames.receiveMessage(iframe, 'resize.embed', function(e) {\n        if (!iframe.parentNode) {\n          resizer.off();\n\n          return;\n        }\n\n        iframe.height = iframe.style.height = (e.detail + 'px');\n      });\n\n      embed.parentNode.insertBefore(iframe, embed);\n    });\n  };\n\n  if (App.preview) {\n    return;\n  }\n\n  App.init();\n\n})((window.rembeddit = window.rembeddit || {}), window.r, this);\n"
  },
  {
    "path": "r2/r2/public/static/js/embed/embed.js",
    "content": ";(function(App, r, window, undefined) {\n  App.VERSION = '0.1';\n\n  var RE_HOST = /^https?:\\/\\/([^\\/|?]+).*/;\n  var config = window.REDDIT_EMBED_CONFIG;\n  var thing = config.thing;\n\n  if (document.referrer && document.referrer.match(RE_HOST)) {\n    r.frames.addPostMessageOrigin(RegExp.$1);\n  }\n\n  r.frames.listen('embed');\n\n  function checkHeight() {\n    var height = document.body.clientHeight;\n\n    if (height && App.height !== height) {\n      App.height = height;\n\n      r.frames.postMessage(window.parent, 'resize.embed', height, { targetOrigin: '*' });\n    }\n  }\n\n  function clipComments() {\n    var height = config.comment_max_height;\n    var flex = 30;\n\n    if (!height) {\n      return;\n    }\n\n    function expandComment(e) {\n      var el = this;\n\n      el.previousSibling.style.maxHeight = '';\n      el.parentNode.className =\n        el.parentNode.className.replace(' reddit-embed-comment-fade', '');\n    }\n\n    var blockquotes = document.getElementsByTagName('blockquote');\n\n    for (var i = 0, l = blockquotes.length; i < l; i++) {\n      if (blockquotes[i].clientHeight > height + flex) {\n        blockquotes[i].style.maxHeight = height + 'px';\n        blockquotes[i].parentNode.className += ' reddit-embed-comment-fade';\n        blockquotes[i].nextSibling.addEventListener('click', expandComment, false);\n      }\n    }\n  }\n\n  function createPayloadFactory(location) {\n    return function payloadFactory(type, action, payload) {\n      var now = new Date();\n      var data = {\n        'event_topic': 'embed',\n        'event_name': 'embed_' + action,\n        'event_ts': now.getTime(),\n        'event_ts_utc_offset': now.getTimezoneOffset() / -60,\n        'user_agent': navigator.userAgent,\n        'sr_id': thing.sr_id,\n        'sr_name': thing.sr_name,\n        'embed_id': thing.id,\n        'embed_version': App.VERSION,\n        'embed_type': type,\n        'embed_control': config.showedits,\n        'embed_host_url': location.href,\n        'comment_edited': thing.edited,\n        'comment_deleted': thing.deleted,\n        'uuid': App.utils.uuid(),\n      };\n\n      // If the creation field doesn't exist (due to manual modification, bad\n      // oEmbed plugin, etc.), don't send the field at all to avoid messing up\n      // the data pipeline.\n      if (config.created !== \"null\") {\n        data['embed_ts_created'] = config.created;\n      }\n  \n      for (var name in payload) {\n        data[name] = payload[name];\n      }\n  \n      return data;\n    };\n  }\n\n  setInterval(checkHeight, 100);\n\n  r.frames.receiveMessage(window.parent, 'pong.embed', function(e) {\n    var type = e.detail.type;\n    var options = e.detail.options;\n    var location = e.detail.location;\n    var createPayload = createPayloadFactory(location);\n\n    clipComments();\n\n    if (options.track === false) {\n      return;\n    }\n\n    var tracker = new App.PixelTracker({\n      url: config.eventtracker_url,\n      anonymousUrl: config.anon_eventtracker_url,\n    });\n\n    tracker.send(createPayload(type, 'view'), {anonymous: true});\n  \n    function trackLink(e) {\n      var el = this;\n      var base = document.getElementsByTagName('base');\n      var target = el.target || (base && base[0] && base[0].target);\n      var newTab = target === '_blank';\n      var payload = createPayload(type, 'click', {\n        'redirect_url': el.href,\n        'redirect_type': el.getAttribute('data-redirect-type'),\n        'redirect_dest': el.host,\n        'redirect_thing_id': el.getAttribute('data-redirect-thing'),\n      });\n      var redirectParams = {\n        \"data\": JSON.stringify(payload),\n        \"url\": el.href,\n      };\n\n      if (el.href.indexOf(config.event_clicktracker_url) === -1) {\n        // Use a DOM object for easier query manipulation\n        var tmpLink = document.createElement('a');\n        tmpLink.href = config.event_clicktracker_url;\n        tmpLink.search = '?' + App.utils.serialize(redirectParams);\n\n        // Rewrite our URL to our event-driven URL\n        el.href = tmpLink.href;\n      }\n\n      if (!newTab) {\n        window.top.location.href = el.href;\n      }\n\n      return newTab;\n    }\n\n    function trackAction(e) {\n      var el = this;\n      var action = el.getAttribute('data-track-action');\n\n      tracker.send(createPayload(type, action), {anonymous: true});\n\n      return false;\n    }\n\n    var trackLinks = document.getElementsByTagName('a');\n\n    for (var i = 0, l = trackLinks.length; i < l; i++) {\n      var link = trackLinks[i];\n  \n      if (link.getAttribute('data-redirect-type')) {\n        trackLinks[i].addEventListener('click', trackLink, false);\n      } else if (link.getAttribute('data-track-action')) {\n        trackLinks[i].addEventListener('click', trackAction, false);\n      }\n    }\n\n  });\n\n  r.frames.postMessage(window.parent, 'ping.embed', {\n    config: config,\n  });\n\n})((window.rembeddit = window.rembeddit || {}), window.r, this);\n"
  },
  {
    "path": "r2/r2/public/static/js/embed/pixel-tracking.js",
    "content": "!function(App, window, undefined) {\n\n  var PixelTracker = App.PixelTracker = function(options) {\n    this._pixelTrackingUrl = options.url;\n    this._anonymousPixelTrackingUrl = options.anonymousUrl;\n  };\n\n  PixelTracker.prototype.send = function(payload, options, callback) {\n    // Overload #send(payload, callback)\n    if (typeof options === 'function') {\n      callback = options;\n      options = {};\n    }\n\n    options = options || {};\n    callback = callback || function() {};\n\n    var url = options.anonymous ?\n      this._anonymousPixelTrackingUrl : this._pixelTrackingUrl;\n\n    if (!payload || !url) {\n      callback();\n\n      return;\n    }\n\n    payload.uuid = payload.uuid || App.utils.uuid();\n\n    var image = new Image();\n    var buster = Math.round(Math.random() * 2147483647);\n\n    image.onload = callback;\n    image.src = url +\n                '?r=' + buster +\n                '&data=' + encodeURIComponent(JSON.stringify(payload));\n  };\n\n}((window.rembeddit = window.rembeddit || {}), this);\n"
  },
  {
    "path": "r2/r2/public/static/js/embed/utils.js",
    "content": ";(function(App, r, window, undefined) {\n  var hasOwnProperty = Object.prototype.hasOwnProperty;\n\n  App.utils = App.utils || {};\n\n  // uuid.js\n  App.utils.uuid = r.uuid;\n\n  // Given an object, serialize it into a set of urlencoded query parameters\n  App.utils.serialize = function(obj) {\n    var params = [];\n\n    for (var p in obj) {\n      if (obj.hasOwnProperty(p)) {\n        params.push(encodeURIComponent(p) + '=' + encodeURIComponent(obj[p]));\n      }\n    }\n\n    return params.join('&');\n  }\n\n})((window.rembeddit = window.rembeddit || {}), r, this);\n"
  },
  {
    "path": "r2/r2/public/static/js/embed.js",
    "content": ";(function($, undefined) {\n    /* Special interpolation to allow python %-style substitution. Only strings like %(blah)s are allowed. */\n    var INJECT_TEMPLATE = _.template(_.unescape(r.config.embed_inject_template), false, { \"escape\": /%\\(([^\\n\\)]+)\\)s/g});\n\n    var embedBodyTemplate = _.template(\n      '<h4  class=\"modal-title\">' +\n        _.escape(r._('Embed preview:')) +\n      '</h4>' +\n      '<div id=\"embed-preview\">' +\n          '<%= html %>' +\n      '</div>' +\n      '<% if (!root) { %>' +\n          '<div class=\"c-checkbox\">' +\n              '<label class=\"remember\">' +\n                  '<input type=\"checkbox\" name=\"parent\" <% if (parent) { %> checked <% } %>>' +\n                  _.escape(r._('Include parent comment.')) +\n              '</label>' +\n          '</div>' +\n      '<% } %>' +\n      '<div class=\"c-checkbox\">' +\n          '<label>' +\n              '<input type=\"checkbox\" name=\"live\" <% if (!live) { %> checked <% } %> data-rerender=\"false\">' +\n              _.escape(r._('Do not show comment if edited.')) +\n          '</label>' +\n          '&nbsp;' +\n          '<a href=\"javascript: void 0;\" class=\"c-toggle\" data-toggle=\"#live-help\">' +\n            _.escape(r._('Learn more')) +\n          '</a>' +\n          '<div id=\"live-help\" class=\"c-help-block c-toggle-content\">' +\n            '<p>' +\n              _.escape(r._('When checked, if an embedded comment is later edited, the embedded comment text will be replaced by a link back to the current version of the comment on reddit.')) +\n            '</p>' +\n            '<p>' +\n              '<a href=\"https://www.reddit.com/r/reddit.com/wiki/embeds\">' +\n                _.escape(r._('This parameter can be changed after embedding.')) +\n              '</a>' +\n            '</p>' +\n          '</div>' +\n      '</div>'\n    );\n\n    var embedFooterTemplate = _.template(\n      '<h4 class=\"modal-title\">' +\n          '<label for=\"embed-code\">' +\n              _.escape(r._('Copy this code and paste it into your website:')) +\n          '</label>' +\n      '</h4>' +\n      '<textarea class=\"c-form-control\" id=\"embed-code\" rows=\"3\" readonly>' +\n          '<%- html %>' +\n      '</textarea>'\n    );\n\n    var embedCodeTemplate = _.template(\n      '<div class=\"reddit-embed\" ' +\n         ' data-embed-media=\"<%- media %>\" ' +\n         '<% if (parent) { %> data-embed-parent=\"true\" <% } %>' +\n         '<% if (live) { %> data-embed-live=\"true\" <% } %>' +\n         ' data-embed-created=\"<%- new Date().toISOString() %>\">' +\n        '<a href=\"<%- comment %>\">Comment</a> from discussion <a href=\"<%- link %>\"><%- title %></a>.' +\n      '</div>'\n    );\n\n    function absolute(url) {\n      if (/^https?:\\/\\//.test(url)) {\n        return url;\n      }\n\n      return 'https://' + location.host + '/' + (url.replace(/^\\//, ''));\n    }\n\n    function getEmbedOptions(data) {\n      var defaults = {\n        live: true,\n        parent: false,\n        media: location.host,\n        created: (new Date()).toISOString(),\n      };\n\n      data = _.defaults({}, data, defaults);\n      data.comment = absolute(data.comment);\n      data.link = absolute(data.link);\n\n      return _.extend({\n        html: INJECT_TEMPLATE(data),\n      }, data);\n    }\n\n    function serializeOptions(options) {\n      return JSON.stringify(_.pick(options, 'live', 'parent'));\n    }\n\n    function initFrame(popup, options) {\n      var $preview = popup.$.find('#embed-preview');\n      var serializedOptions = typeof options !== 'string' ?\n        serializeOptions(options) : options;\n\n      window.rembeddit.preview = true;\n      window.rembeddit.init({track: false}, function() {\n        var height = 0;\n\n        var reflow = setInterval(function() {\n          var $next = $preview.find('iframe:last-child');\n          var newHeight = $next.height();\n\n          if (height !== newHeight) {\n            height = newHeight;\n          } else {\n            clearInterval(reflow);\n\n            $preview.find('iframe')\n                    .hide()\n                  .last()\n                    .show()\n                    .attr('data-options', serializedOptions);\n\n            $preview.css('height', 'auto');\n          }\n        }, 100);\n      });\n    }\n\n    var tracker = new window.rembeddit.PixelTracker({\n      url: r.config.eventtracker_url,\n    });\n\n    $('body').on('click', '.embed-comment', function(e) {\n      var $el = $(e.target);\n      var data = $el.data();\n      var embedOptions = getEmbedOptions(data);\n      var popup = new r.ui.Popup({\n        className: 'embed-modal',\n        content: embedBodyTemplate(embedOptions),\n        footer: embedFooterTemplate(embedOptions),\n      });\n      var $textarea = popup.$.find('textarea');\n      var $preview = popup.$.find('#embed-preview');\n      var created = false;\n\n      popup.$.find('[data-toggle]').togglable();\n\n      popup.$.on('change', '[type=\"checkbox\"]', function(e) {\n        var option = e.target.name;\n        var $option = $(e.target);\n        var prev = $el.data(option);\n\n        if (prev === undefined) {\n          prev = embedOptions[option];\n        }\n\n        $el.data(e.target.name, !prev);\n\n        var data = $el.data();\n        var options = getEmbedOptions(data);\n        var serializedOptions = serializeOptions(options);\n        var html = options.html;\n        var height = $preview.height();\n\n        $textarea.val(html);\n\n        if ($option.data('rerender') !== false) {\n          var selector = '[data-options=\"' + r.utils.escapeSelector(serializedOptions) + '\"]';\n          var $cached = $preview.find(selector);\n\n          if ($cached.length) {\n            $cached.show().siblings().hide();\n          } else {\n            $preview.height(height).append($(html).hide());\n\n            initFrame(popup, serializedOptions);\n          }\n        }\n      });\n\n      $textarea.on('focus', function(e) {\n        $(this).select().one('mouseup', function(e) {\n          e.preventDefault();\n        });\n\n        if (!created) {\n          var data = $el.data();\n          var options = getEmbedOptions(data);\n          var now = new Date();\n          var ts = now.getTime();\n\n          tracker.send({\n            'event_topic': 'embed',\n            'event_name': 'embed_create',\n            'event_ts': ts,\n            'event_ts_utc_offset': now.getTimezoneOffset() / -60,\n            'embed_type': options.parent ? 'comment_and_parent' : 'comment',\n            'user_agent': navigator.userAgent,\n            'user_id': r.config.user_id,\n            'logged_in_status': !!r.config.logged,\n            'sr_id': r.utils.fullnameToId(r.config.cur_site),\n            'sr_name': r.config.post_site,\n            'embed_id': r.utils.fullnameToId($el.thing_id()),\n            'embed_created_ts': ts,\n            'embed_control': options.live,\n            'embed_host_url': location.href,\n            'embed_version': window.rembeddit.VERSION,\n          });\n\n          created = true;\n        }\n      });\n\n      popup.on('closed.r.popup', function() {\n        popup.$.remove();\n      });\n\n      popup.on('show.r.popup', function() {\n        $preview.find('.reddit-embed').hide();\n      });\n\n      popup.on('opened.r.popup', function() {\n        initFrame(popup, embedOptions);\n      });\n\n      popup.show();\n\n    });\n\n})(window.jQuery);\n"
  },
  {
    "path": "r2/r2/public/static/js/errors.js",
    "content": "!function(r) {\n  var errors = {\n    'UNKNOWN_ERROR': r._('unknown error %(message)s'),\n    'NO_TEXT': r._('we need something here'),\n    'TOO_LONG': r._('this is too long (max: %(max_length)s)'),\n    'TOO_SHORT': r._('this is too short (min: %(min_length)s)'),\n    'SR_RULE_EXISTS': r._('A subreddit rule by that name already exists.'),\n    'SR_RULE_TOO_MANY': r._('This subreddit already has the maximum number of rules.'),\n  };\n\n  function CustomErrorPrototype() {}\n  CustomErrorPrototype.prototype = Error.prototype;\n\n  // used by the custom ApiError, tries to get a more accurate stack value by\n  // removing the top line (which should be the instantiation of the original\n  // generic Error object inside of ApiError)\n  function _getStack(err) {\n    var stack = err.stack;\n    return stack && stack.split('\\n').slice(1).join('\\n');\n  }\n\n  // creating custom Errors that behave correctly is weird\n  // http://stackoverflow.com/questions/8802845/inheriting-from-the-error-object-where-is-the-message-property\n  function ApiError(displayName, displayMessage, field, source) {\n    // allow use without new constructor\n    if (!(this instanceof ApiError)) {\n      return new ApiError(displayName, displayMessage, field, source);\n    }\n\n    var err = Error.call(this);\n\n    // IE11 doesn't set the stack property until the error is thrown.\n    // If we do nothing it will create a proper stack on its own.\n    if ('stack' in err) {\n      if ('captureStackTrace' in Error) {\n        // Chrome provides captureStackTrace for custom error traces\n        Error.captureStackTrace(this, ApiError);\n      } else {\n        // For Firefox/Safari/Other browsers, defer setting stack until accessed\n        try {\n          Object.defineProperty(this, 'stack', {\n            configurable: true,\n            get: function() {\n              var stack = _getStack(err);\n              return this.stack = stack;\n            },\n          })\n        } catch (e) {\n          // If that fails set the stack property immediately.\n          this.stack = _getStack(err);\n        }\n      }\n    }\n\n    // if we want to throw the error client-side, the message attribute\n    // should include both the error key and the display message.\n    var errorMessage = displayName + ' | ' + displayMessage;\n\n    this.name = 'ApiError';\n    this.message = errorMessage;\n    this.displayName = displayName;\n    this.displayMessage = displayMessage;\n    this.field = field || '';\n    this.source = source || 'api';\n  }\n\n  ApiError.prototype = new CustomErrorPrototype();\n\n\n  r.errors = {\n    create: function(name, message, field, source) {\n      return new ApiError(name, message, field, source);\n    },\n\n    formatAPIError: function(apiErrorArray) {\n      var key = apiErrorArray[0];\n      var message = apiErrorArray[1];\n      var field = apiErrorArray[2];\n\n      return new ApiError(key, message, field);\n    },\n\n    getAPIErrorsFromResponse: function(res) {\n      if (res && res.json && res.json.errors && res.json.errors.length) {\n        return res.json.errors.map(r.errors.formatAPIError);\n      } else if (!res || (res.error && typeof res.error === 'string')) {\n        var message = !res ? 'unknown' : res.error;\n        // return an array here for consistency\n        return [\n          r.errors.createAPIError('', 'UNKNOWN_ERROR', { message: message }),\n        ];\n      }\n    },\n\n    createAPIError: function(field, key, messageParams) {\n      var message = errors[key] || 'unknown';\n\n      if (messageParams) {\n        message = message.format(messageParams);\n      }\n\n      return new ApiError(key, message, field, 'client');\n    },\n\n    _getErrorFieldSelector: function(apiError) {\n      var selector = '.error.' + apiError.displayName;\n\n      if (apiError.field) {\n        selector += '.field-' + apiError.field;\n      }\n      \n      return selector;\n    },\n\n    showAPIError: function(form, apiError) {\n      var selector = this._getErrorFieldSelector(apiError);\n      $(form).find(selector)\n             .text(apiError.displayMessage)\n             .css('display', 'inline');\n    },\n\n    showAPIErrors: function(form, apiErrors) {\n      apiErrors.forEach(function(apiError) {\n        r.errors.showAPIError(form, apiError);\n      });\n    },\n\n    clearAPIErrors: function(form, apiErrors) {\n      var selector;\n      \n      if (!apiErrors) {\n        selector = '.error';\n      } else {\n        selector = apiErrors.map(this._getErrorFieldSelector).join(', ');\n      }\n\n      $(form).find(selector)\n             .text('')\n             .css('display', 'none');\n    },\n  };\n}(r);\n"
  },
  {
    "path": "r2/r2/public/static/js/events.js",
    "content": "!function(r) {\n  'use strict';\n\n  function postData(eventInfo) {\n    $.ajax({\n      method: 'POST',\n      url: eventInfo.url + '?' + jQuery.param(eventInfo.query),\n      data: eventInfo.data,\n      contentType: 'text/plain',\n      complete: eventInfo.done,\n    });\n  }\n\n  function calculateHash(key, data) {\n    var hash = CryptoJS.HmacSHA256(data, key);\n    return hash.toString(CryptoJS.enc.Hex);\n  }\n\n  var tracker;\n\n  r.events = {\n    init: function() {\n      if (r.config.events_collector_key &&\n          r.config.events_collector_secret &&\n          r.config.events_collector_url) {\n        tracker = new EventTracker(\n          r.config.events_collector_key,\n          r.config.events_collector_secret,\n          postData,\n          r.config.events_collector_url,\n          'reddit.com',\n          calculateHash\n        );\n      }\n    },\n\n    track: function(eventTopic, eventName, eventPayload) {\n      if (tracker) {\n        tracker.track(eventTopic, eventName, eventPayload);\n      }\n      return this;\n    },\n\n    send: function(done) {\n      if (tracker) {\n        tracker.send(done);\n      } else if (typeof done === 'function') {\n        done();\n      }\n      return this;\n    }\n  };\n\n}(r);\n"
  },
  {
    "path": "r2/r2/public/static/js/expando/nsfwflow.js",
    "content": "/*\n  NSFW flow for expanded media\n\n  In non-NSFW listings, we sometimes want to show an interstitial over NSFW\n  expando content\n\n  1. If the user has the \"no_profanity\" preference turned ON (normal)\n  2. If a user has not seen the interstitial before (warning)\n\n  In the former case, the user is given an option to turn the preference OFF\n  inline.  In the latter case, the user is informed that the content will be\n  displayed automatically in the future and given an option to turn the\n  preference ON.\n\n  On media embeds (i.e. embedly stuff) the content is replaced by a flat grey\n  box.  On media previews (i.e. our images) a blurred version of the image\n  is displayed with the interstitial prompt overlaid.\n */\n!function(r, undefined) {\n  var _state = {\n    over18Listing: false,\n    noProfanity: true,\n    nsfwMediaAcknowledged: false,\n    loggedIn: false,\n  };\n\n  r.hooks.get('setup').register(function() {\n    _state.over18Listing = r.config.listing_over_18;\n    _state.noProfanity = r.config.pref_no_profanity;\n    _state.nsfwMediaAcknowledged = r.config.nsfw_media_acknowledged;\n    _state.loggedIn = !!r.config.logged;\n  });\n\n  /*\n  The form used to update the user's no_profanity preference AND turn the\n  nsfw_media_acknowledged flag ON.  This is normally shown only after the\n  \"Always show NSFW media?\" link is clicked as a method of *undoing* the change,\n  but is shown by default on 1st-time warnings as a method of turning ON the\n  no_profanity preference.\n   */\n  var ExpandoInlinePreferenceForm = r.ui.FormBar.extend({\n    doneString: r._('We will ask before showing NSFW media.'),\n\n    defaults: {\n      text: '',\n      buttonLabel: '',\n      key: 'show_nsfw_media',\n      value: '',\n    },\n\n    render: function(templateProps) {\n      this._lastRenderProps = templateProps;\n      ExpandoInlinePreferenceForm.__super__.render.call(this, templateProps);\n    },\n\n    onSubmit: function(e) {\n      ExpandoInlinePreferenceForm.__super__.onSubmit.call(this, e);\n      var templateProps = _.defaults({\n        text: this.doneString,\n      }, this._lastRenderProps);\n      this.render(templateProps);\n      this.$el.addClass('expando-nsfw-flow-complete');\n      this.$el.find('button').remove();\n      this.$el.find('form').attr('disabled', true);\n    },\n  });\n\n\n  var ExpandoNSFWGate = Backbone.View.extend({\n    templateName: 'expando/nsfwgate',\n    \n    events: {\n      'click .expando-nsfw-gate-show-once': 'onButtonClick',\n      'click .expando-nsfw-gate-text': 'onTextClick',\n    },\n\n    defaults: {\n      buttonLabel: r._('Click to see NSFW'),\n      text: '',\n      type: 'normal',\n      style: 'interstitial',\n    },\n\n    initialize: function() {\n      this.render(this.options);\n    },\n\n    render: function(templateProps) {\n      templateProps = _.defaults(templateProps, this.defaults);\n      var content = r.templates.make(this.templateName, templateProps);\n      this.$el.html(content); \n    },\n\n    onButtonClick: function(e) {\n      e.preventDefault();\n      this.trigger('click:button');\n    },\n\n    onTextClick: function(e) {\n      e.preventDefault();\n      this.trigger('click:text');\n    },\n  });\n\n  /*\n  View controller\n   */\n  var ExpandoNSFWFlow = Backbone.View.extend({\n    strings: {\n      // normal, linkified text\n      normal: r._('Always show NSFW media?'),\n      changedToShow: r._('Ok, we changed your preferences to always show NSFW media.'),\n      changedToShowLabel: r._('Undo'),\n\n      // first time/warning\n      warning: r._('Your account is set up to always show this content in the future.'),\n      alwaysAsk: r._('Change your preferences to hide NSFW media?'),\n      alwaysAskLabel: r._('Hide NSFW'),\n    },\n\n    initialize: function() {      \n      var width = this.options.width;\n      var height = this.options.height;\n      var isOverlay = this.options.isOverlay;\n      var isWarning = this.options.isWarning;\n      \n      this.isOverlay = this.options.isOverlay;\n      this.isWarning = this.options.isWarning;\n\n      var interstitialOptions = {\n        width: width ? width + 'px' : '100%',\n        height: height ? height + 'px' : '100%',\n        style: isOverlay ? 'overlay' : 'interstitial',\n        type: isWarning ? 'warning' : 'normal',\n      };\n\n      if (_state.loggedIn) {\n        if (this.isWarning) {\n          interstitialOptions.text = this.strings.warning;\n        } else {\n          interstitialOptions.text = this.strings.normal;\n        }\n      }\n\n      this.interstitial = new ExpandoNSFWGate(interstitialOptions);\n      this.$el.append(this.interstitial.el);\n      this.listenTo(this.interstitial, 'click:button', this.onShowOnce);\n\n      if (!_state.loggedIn) { return; }\n\n      if (this.isWarning) {\n        this.initFormBar();\n      } else {\n        this.listenTo(this.interstitial, 'click:text', this.onShowAlways);\n      }\n    },\n\n    initFormBar: function() {\n      var text, buttonLabel;\n\n      if (this.isWarning) {\n        text = this.strings.alwaysAsk;\n        buttonLabel = this.strings.alwaysAskLabel;\n      } else {\n        text = this.strings.changedToShow;\n        buttonLabel = this.strings.changedToShowLabel;\n      }\n      \n      this.formBar = new ExpandoInlinePreferenceForm({\n        text: text,\n        buttonLabel: buttonLabel,\n        value: 'off',\n      });\n      this.$el.append(this.formBar.el);\n      this.listenTo(this.formBar, 'submit', this.updatePref);\n    },\n\n    onShowOnce: function() {\n      if (!_state.nsfwMediaAcknowledged) {\n        _state.nsfwMediaAcknowledged = true\n        this._updateNsfwMediaPrefs({}, this.show.bind(this));\n      } else {\n        this.show();\n      }\n    },\n\n    onShowAlways: function() {      \n      this.updatePref({ show_nsfw_media: 'on' });\n      this.initFormBar();\n    },\n\n    updatePref: function(fields) {\n      _state.noProfanity = fields.show_nsfw_media === 'off';\n      this._updateNsfwMediaPrefs(fields, function() {\n        if (fields.show_nsfw_media === 'on') {\n          this.show();\n        }\n      }.bind(this));\n    },\n\n    _updateNsfwMediaPrefs: function(fields, cb) {\n      $.request('set_nsfw_media_pref', fields, function(res) {\n        if (cb) { cb(); }\n      }, false, 'json', false);\n    },\n\n    show: function() {\n      this.trigger('show');\n      if (this.isOverlay) {\n        this.interstitial.$el.fadeOut();\n      } else {\n        this.interstitial.$el.remove();\n      }\n    },\n  });\n\n  /*\n  hook into expando events to inject the nsfw flow UI, so the main expando.js\n  code dosen't need to know about all the NSFW flow logic.\n   */\n  r.hooks.get('expando-pre-init').register(function() {\n    $(document.body).on('expando:create', function(e) {\n      var expando = e.expando;\n\n      var showInterstitial = (\n        // must be on a mixed-content listing\n        !_state.over18Listing &&\n        // and either have the \"safe(r) for work\" preference on\n        // or this is your first time seeing a nsfw media interstitial\n        (_state.noProfanity || !_state.nsfwMediaAcknowledged) &&\n        // and this is an NSFW link\n        expando.isNSFW && expando.linkType == 'link'\n      );\n\n      if (!showInterstitial) { return; }\n\n      var nsfwFlow;\n\n      function showNsfwFlow(e) {\n        e.preventDefault();\n\n        if (!expando.autoexpanded) {\n          expando.$expando.html(expando.cachedHTML);\n        }\n\n        var $expando = expando.$expando;\n        var $media = $expando.children();\n        var width = $media.width();\n        var height = $media.height();\n        // if it's a `.media-preview`, interstial is shown _over_ the blurred image\n        var isOverlay = $media.hasClass('media-preview');\n        var isWarning = !_state.noProfanity;\n\n        nsfwFlow = new ExpandoNSFWFlow({\n          width: width,\n          height: height,\n          isOverlay: isOverlay,\n          isWarning: isWarning,\n        });\n\n        expando.$expando.addClass('expando-with-nsfw-interstitial');\n        expando.listenTo(nsfwFlow, 'show', destroyNsfwFlow);\n\n        if (nsfwFlow.isOverlay) {\n          $media.append(nsfwFlow.el);\n        } else {\n          $expando.html(nsfwFlow.el);\n        }\n\n        expando.showExpandoContent();\n      }\n\n      function hideNsfwFlow(e) {\n        e.preventDefault();\n        expando.hideExpandoContent();\n      }\n\n      function destroyNsfwFlow() {\n        expando.stopListening(nsfwFlow, 'show', destroyNsfwFlow);\n        expando.$expando.removeClass('expando-with-nsfw-interstitial')\n        if (!nsfwFlow.isOverlay) {\n          expando.$expando.prepend(expando.cachedHTML);\n        }\n        expando.fireExpandEvent();\n        expando.$el.off('expando:show', showNsfwFlow);\n        expando.$el.off('expando:hide', hideNsfwFlow);\n      }\n\n      expando.$el.on('expando:show', showNsfwFlow);\n      expando.$el.on('expando:hide', hideNsfwFlow);\n    });\n  });\n}(r);\n"
  },
  {
    "path": "r2/r2/public/static/js/expando/nsfwgate.html",
    "content": "<div class=\"expando-nsfw-gate expando-nsfw-<%- thing.style %> expando-nsfw-<%- thing.type %>\"\n        style=\"width: <%- thing.width %>; height: <%- thing.height %>\">\n    <div class=\"expando-nsfw-gate-controls\">\n        <button class=\"expando-nsfw-gate-show-once\"><%- thing.buttonLabel %></button>\n        <p class=\"expando-nsfw-gate-text\"><%- thing.text %></p>\n    </div>\n</div>\n"
  },
  {
    "path": "r2/r2/public/static/js/expando.js",
    "content": "!function(r) {\n  function isPluginExpandoButton(elem) {\n    // temporary fix for RES http://redd.it/392zol\n    return elem.tagName === 'A';\n  }\n\n  var Expando = Backbone.View.extend({\n    buttonSelector: '.expando-button',\n    expandoSelector: '.expando',\n    expanded: false,\n\n    events: {\n      'click .expando-button': 'toggleExpando',\n    },\n\n    constructor: function() {\n      Backbone.View.prototype.constructor.apply(this, _.toArray(arguments));\n\n      this.afterInitialize();\n    },\n\n    initialize: function() {\n      this.$button = this.$el.find(this.buttonSelector);\n      this.$expando = this.$el.find(this.expandoSelector);\n    },\n\n    afterInitialize: function() {\n      this.expand();\n    },\n\n    toggleExpando: function(e) {\n      if (isPluginExpandoButton(e.target)) { return; }\n\n      this.expanded ? this.collapse() : this.expand();\n    },\n\n    expand: function() {\n      this.$button.addClass('expanded')\n                  .removeClass('collapsed');\n      this.expanded = true;\n      this.show();\n    },\n\n    show: function() {\n      this.$expando.show();\n    },\n\n    collapse: function() {\n      this.$button.addClass('collapsed')\n                  .removeClass('expanded');\n      this.expanded = false;\n      this.hide();\n    },\n\n    hide: function() {\n      this.$expando.hide();\n    }\n  });\n\n  var LinkExpando = Expando.extend({\n    events: _.extend({}, Expando.prototype.events, {\n      'click .open-expando': 'expand',\n    }),\n\n    initialize: function() {\n      Expando.prototype.initialize.call(this);\n\n      this.cachedHTML = this.$expando.data('cachedhtml');\n      this.loaded = !!this.cachedHTML;\n      this.id = this.$el.thing_id();\n      this.isNSFW = this.$el.hasClass('over18');\n      this.linkType = this.$el.hasClass('self') ? 'self' : 'link';\n      this.autoexpanded = this.options.autoexpanded;\n\n      if (this.autoexpanded) {\n        this.loaded = true;\n        this.cachedHTML = this.$expando.html();\n      }\n\n      var $e = $.Event('expando:create', { expando: this });\n      $(document.body).trigger($e);\n\n      if ($e.isDefaultPrevented()) { return; }\n\n      $(document).on('hide_thing_' + this.id, function() {\n        this.collapse();\n      }.bind(this));\n\n      // expando events\n      var linkURL = this.$el.children('.entry').find('a.title').attr('href');\n\n      if (/^\\//.test(linkURL)) {\n        var protocol = window.location.protocol;\n        var hostname = window.location.hostname;\n        linkURL = protocol + '//' + hostname + linkURL;\n      }\n\n      // event context\n      var eventData = {\n        linkIsNSFW: this.isNSFW,\n        linkType: this.linkType,\n        linkURL: linkURL,\n      };\n      \n      // note that hyphenated data attributes will be converted to camelCase\n      var thingData = this.$el.data();\n\n      if ('fullname' in thingData) {\n        eventData.linkFullname = thingData.fullname;\n      }\n\n      if ('timestamp' in thingData) {\n        eventData.linkCreated = thingData.timestamp;\n      }\n\n      if ('domain' in thingData) {\n        eventData.linkDomain = thingData.domain;\n      }\n\n      if ('authorFullname' in thingData) {\n        eventData.authorFullname = thingData.authorFullname;\n      }\n\n      if ('subreddit' in thingData) {\n        eventData.subredditName = thingData.subreddit;\n      }\n\n      if ('subredditFullname' in thingData) {\n        eventData.subredditFullname = thingData.subredditFullname;\n      }\n\n      this._expandoEventData = eventData;\n    },\n\n    collapse: function() {\n      LinkExpando.__super__.collapse.call(this);\n      this.autoexpanded = false;\n    },\n\n    show: function() {\n      if (!this.loaded) {\n        return $.request('expando', { link_id: this.id }, function(res) {\n          var expandoHTML = $.unsafe(res);\n          this.cachedHTML = expandoHTML;\n          this.loaded = true;\n          this.show();\n        }.bind(this), false, 'html', true);\n      }\n\n      var $e = $.Event('expando:show', { expando: this });\n      this.$el.trigger($e);\n\n      if ($e.isDefaultPrevented()) { return; }\n\n      if (!this.autoexpanded) {\n        this.$expando.html(this.cachedHTML);\n      }\n\n      if (!this._expandoEventData.provider) {\n        // this needs to be deferred until the actual embed markup is available.\n        var $media = this.$expando.children();\n\n        if ($media.is('iframe')) {\n          this._expandoEventData.provider = 'embedly';\n        } else {\n          this._expandoEventData.provider = 'reddit';\n        }\n      }\n\n      this.showExpandoContent();\n      this.fireExpandEvent();\n    },\n\n    showExpandoContent: function() {\n      this.$expando.removeClass('expando-uninitialized');\n      this.$expando.show();\n    },\n\n    fireExpandEvent: function() {\n      if (this.autoexpanded) {\n        this.autoexpanded = false;\n        r.analytics.expandoEvent('expand_default', this._expandoEventData);\n      } else {\n        r.analytics.expandoEvent('expand_user', this._expandoEventData);\n      }\n    },\n\n    hide: function() {\n      var $e = $.Event('expando:hide', { expando: this });\n      this.$el.trigger($e);\n\n      if ($e.isDefaultPrevented()) { return; }\n\n      this.hideExpandoContent();\n      this.fireCollapseEvent();\n    },\n\n    hideExpandoContent: function() {\n      this.$expando.hide().empty();\n    },\n\n    fireCollapseEvent: function() {\n      r.analytics.expandoEvent('collapse_user', this._expandoEventData);\n    },\n  });\n\n  var SearchResultLinkExpando = Expando.extend({\n    buttonSelector: '.search-expando-button',\n    expandoSelector: '.search-expando',\n\n    events: {\n      'click .search-expando-button': 'toggleExpando',\n    },\n\n    afterInitialize: function() {\n      var expandoHeight = this.$expando.innerHeight();\n      var contentHeight = this.$expando.find('.search-result-body').innerHeight();\n\n      if (contentHeight <= expandoHeight) {\n        this.$button.remove();\n        this.$expando.removeClass('collapsed');\n        this.undelegateEvents();\n      } else if (this.options.expanded) {\n        this.expand();\n      }\n    },\n\n    show: function() {\n      this.$expando.removeClass('collapsed');\n    },\n\n    hide: function() {\n      this.$expando.addClass('collapsed');\n    },\n  });\n\n  $(function() {\n    r.hooks.get('expando-pre-init').call();\n\n    var listingSelectors = [\n      '.linklisting',\n      '.organic-listing',\n      '.selfserve-subreddit-links',\n    ];\n\n    function initExpando($thing, autoexpanded) {\n      if ($thing.data('expando')) {\n        return;\n      }\n\n      $thing.data('expando', true);\n\n      var view = new LinkExpando({\n        el: $thing[0],\n        autoexpanded: autoexpanded,\n      });\n    }\n\n    $(listingSelectors.join(',')).on('click', '.expando-button', function(e) {\n      if (isPluginExpandoButton(e.target)) { return; }\n\n      var $thing = $(this).closest('.thing')\n      initExpando($thing, false);\n    });\n\n    $('.link .expando-button.expanded').each(function() {\n      var $thing = $(this).closest('.thing');\n      initExpando($thing, true);\n    });\n\n    var searchResultLinkThings = $('.search-expando-button').closest('.search-result-link');\n\n    searchResultLinkThings.each(function() {\n      new SearchResultLinkExpando({ el: this });\n    });\n  });\n}(r);\n"
  },
  {
    "path": "r2/r2/public/static/js/filter.js",
    "content": "r.filter = {}\n\nr.filter.init = function() {\n    var detailsEl = $('.filtered-details')\n    if (detailsEl.length) {\n        var multi = new r.filter.Filter({\n            path: detailsEl.data('path')\n        })\n        detailsEl.find('.subreddits a').each(function(i, e) {\n            multi.subreddits.add({name: $(e).data('name')})\n        })\n        multi.fetch({\n            error: _.bind(r.multi.mine.create, r.multi.mine, multi, {wait: true})\n        })\n\n        var detailsView = new r.multi.SubredditList({\n            model: multi,\n            itemView: r.filter.FilteredSubredditItem,\n            el: detailsEl\n        }).render()\n    }\n}\n\nr.filter.Filter = r.multi.MultiReddit.extend({\n    url: function() {\n        return r.utils.joinURLs('/api/filter', this.id)\n    }\n})\n\nr.filter.FilteredSubredditItem = r.multi.MultiSubredditItem.extend({\n    render: function() {\n        this.$el.append(this.template({\n            sr_name: this.model.get('name')\n        }))\n        return this\n    }\n})\n"
  },
  {
    "path": "r2/r2/public/static/js/flair.js",
    "content": "$(function() {\n    function showSaveButton(field) {\n        $(field).parent().parent().addClass(\"edited\");\n        $(field).parent().parent().find(\".status\").empty();\n    }\n\n    function onEdit() {\n        if ($(this).data(\"saved\") != $(this).val()) {\n            showSaveButton(this);\n        }\n    }\n\n    function onDelete(e) {\n        e.preventDefault()\n        return post_form(this.parentNode, e.data.action);\n    }\n\n    function onFocus() {\n        showSaveButton(this);\n    }\n\n    function onSubmit(e) {\n        e.preventDefault()\n        $(this).removeClass(\"edited\");\n        return post_form(this, e.data.action);\n    }\n\n    function toggleFlairSelector() {\n        open_menu(this);\n        $(this).addClass(\"active\");\n        return false;\n    }\n\n    function getFlairAttrs($el) {\n        if ($el.data('name')) {\n            return {name: $el.data('name')}\n        }\n        return {link: $el.thing_id()}\n    }\n\n    function selectFlairInSelector(e) {\n        $(\".flairselector li\").removeClass(\"selected\");\n        $(this).addClass(\"selected\");\n        var form = $(this).parent().parent().siblings(\"form\")[0];\n        $(form).children('input[name=\"flair_template_id\"]').val(this.id);\n        var customizer = $(form).children(\".customizer\");\n        var input = customizer.children(\"input\");\n        if ($(this).hasClass(\"texteditable\")) {\n            customizer.addClass(\"texteditable\");\n            input.removeAttr(\"disabled\");\n            input.css(\"display\", \"block\");\n            input.val($.trim($(this).find(\".flair, .linkflairlabel\").text())).select();\n            input.keyup(function() {\n                $(\".flairselection .flair, .flairselection .linkflairlabel\")\n                    .text($(input).val()).attr(\"title\", $(input).val());\n            });\n        } else {\n            customizer.removeClass(\"texteditable\");\n            input.attr(\"disabled\", \"disabled\").hide();\n        }\n        var remover = $(\".flairselector .flairremove\").detach();\n        $(\".flairselection\").html($(this).first().children().clone())\n            .append(remover);\n        $(\".flairselector .flairremove\").css(\"display\", \"inline-block\");\n        return false;\n    }\n\n    function removeFlairInSelector(e) {\n        var form = $(this).parent().parent();\n        $(form).children('input[name=\"flair_template_id\"]').val(\"\");\n        $(form).children(\".customizer\").hide();\n        var remover = $(\".flairselector .flairremove\").detach();\n        $(remover).hide();\n        $(\".flairselector li\").removeClass(\"selected\");\n        $(\".flairselection\").empty().append(remover);\n    }\n\n    function postFlairSelection(e) {\n        $(this).find(\".status\").html(r.config.status_msg.submitting).show()\n        var $btn = $(this.parentNode.parentNode).find('.flairselectbtn')\n        simple_post_form(this, \"selectflair\", getFlairAttrs($btn));\n        return false;\n    }\n\n    function openFlairSelector(e) {\n        if (r.access.isLinkRestricted(e.target)) {\n            return false;\n        }\n\n        close_menus(e);\n\n        var button = this;\n        var selector = $(button).siblings(\".flairselector\")[0];\n\n        function columnize(col) {\n            var min_cols = 1;\n            var max_cols = 3;\n            var min_col_width = 150;\n            var max_col_height = 10;\n            var length = $(col).children().length;\n            var num_cols =\n                Math.max(\n                    min_cols,\n                    Math.min(max_cols, Math.ceil(length / max_col_height)));\n            var height = Math.ceil(length / num_cols);\n            var col_width = Math.max(min_col_width, $(col).width());\n\n            // Fix the width of the ul before splitting it into columns. This\n            // This prevents it from shrinking if its widest element gets moved\n            // into one of the other generated columns.\n            $(col).width(col_width);\n\n            if (num_cols > 1) {\n                $(col).css('float', 'left');  // force IE7 to lay out properly\n\n                var num_short_cols = num_cols * height - length;\n\n                for (var i = 1; i < num_cols; i++) {\n                    var h = height;\n                    if (i <= num_short_cols) {\n                        h--;\n                    }\n                    var start = length - h;\n                    length -= h;\n                    var tail = $(col).children().slice(start).remove();\n                    $(tail).width(col_width);\n                    $(col).after($(\"<ul>\")\n                        .css('float', 'left')  // force IE7 to lay out properly\n                        .append(tail));\n                }\n            }\n\n            // return new width; add a little padding to each column, plus\n            // some extra padding in case a vertical scrollbar appears\n            return num_cols * (col_width + 5) + 50;\n        }\n\n        function handleResponse(r) {\n            $(selector).html(r);\n\n            var ul = $(\".flairselector ul\");\n            var width = Math.max(\n                200, ul.length ? columnize(ul) : $(\".error\").width() + 20);\n            var left = Math.max(\n                100, $(button).position().left + $(button).width() - width);\n\n            $(selector)\n                .height(\"auto\")\n                .width(width)\n                .css(\"left\", left + \"px\")\n                .click(false)\n                .find(\".flairselection\")\n                    .click(false)\n                .end()\n                .find(\"form\")\n                    // don't bubble clicks in the form up to the .click(false)\n                    .click(function(e) { e.stopPropagation(); })\n                    .submit(postFlairSelection)\n                .end()\n                .find(\".customizer input\")\n                    .attr(\"disabled\", \"disabled\")\n                .end()\n                .find(\"li.selected\")\n                    .each(selectFlairInSelector)\n                .end()\n                .find(\"li:not(.error)\")\n                    .click(selectFlairInSelector)\n                .end()\n                .find(\".flairremove\")\n                    .click(removeFlairInSelector)\n                .end();\n        }\n\n        $(selector)\n            .html('<img class=\"flairthrobber\" src=\"' + r.utils.staticURL('throbber.gif') + '\" />')\n            .addClass(\"active\")\n            .height(18).width(18)\n            .css(\"padding-left\", 4)\n            .css(\"padding-top\", 4)\n            .css(\"padding-bottom\", 4)\n            .css(\"padding-right\", 4)\n            .css(\"left\",\n                 ($(button).position().left + $(button).width() - 18) + \"px\")\n            .css(\"top\", $(button).position().top + \"px\");\n\n        var attrs = getFlairAttrs($(this))\n        $.request(\"flairselector\",  attrs, handleResponse, true, \"html\");\n        return false;\n    }\n\n    // Attach event handlers to the various flair forms that may be on page.\n\n    $(\"#tabbedpane-grant\").on(\"submit\",  \".flair-entry\", {\n        action: \"flair\",\n      }, onSubmit)\n\n    $(\"#tabbedpane-grant\").on(\"click\", \".flairdeletebtn\", {\n        action: \"deleteflair\",\n      }, onDelete)\n\n\n    $(\"#tabbedpane-templates, #tabbedpane-link_templates\").on(\"submit\", \".flair-entry\", {\n          action: \"flairtemplate\",\n      }, onSubmit)\n\n    $(\"form.clearflairtemplates\").on(\"submit\", {\n        action: \"clearflairtemplates\"\n      }, onSubmit)\n\n    $(\".flairlist\")\n        .on(\"focus\", \".flaircell input\", onFocus)\n        .on(\"keyup\", \".flaircell input\", onEdit)\n        .on(\"change\", \".flaircell input\", onEdit)\n        .on(\"click\", \".flairtemplate .flairdeletebtn\", {\n            action: \"deleteflairtemplate\",\n          }, onDelete)\n\n    // Event handlers for sidebar flair prefs.\n    $(\".flairtoggle\").submit(function() {\n        return post_form(this, 'setflairenabled');\n    });\n    $(\".flairtoggle input\").change(function() { $(this).parent().submit(); });\n\n    $(document).on(\"click\", \".tagline .flairselectbtn, .thing .flairselectbtn\", openFlairSelector);\n\n    $(\".flairselector .dropdown\").click(toggleFlairSelector);\n});\n"
  },
  {
    "path": "r2/r2/public/static/js/frames.js",
    "content": ";(function(r, global, undefined) {\n  'use strict';\n\n  var ALLOW_WILDCARD = '.*';\n  var DEFAULT_MESSAGE_NAMESPACE = '.postMessage';\n  var DEFAULT_POSTMESSAGE_OPTIONS = {\n    targetOrigin: '*',\n  };\n\n  var allowedOrigins = [ALLOW_WILDCARD];\n  var re_postMessageAllowedOrigin = compileOriginRegExp(allowedOrigins);\n  var messageNamespaces = [DEFAULT_MESSAGE_NAMESPACE];\n  var re_messageNamespaces = compileNamespaceRegExp(messageNamespaces);\n  var proxies = {};\n  var listening = false;\n\n  function receiveMessage(e) {\n    if (e.origin !== location.origin &&\n        !re_postMessageAllowedOrigin.test(e.origin)\n        && e.origin !== 'null') {\n      return;\n    }\n\n    try {\n      var message = JSON.parse(e.data);\n      var type = message.type;\n\n      // Namespace doesn't match, ignore\n      if (!re_messageNamespaces.test(type)) {\n        return;\n      }\n\n      var namespace = type.split('.', 2)[1];\n\n      if (proxies[namespace]) {\n        var proxyWith = proxies[namespace];\n\n        for (var i = 0; i < proxyWith.targets.length; i++) {\n          r.frames.postMessage(proxyWith.targets[i], type, message.data, message.options);\n        }\n      }\n\n      var customEvent = new CustomEvent(type, {detail: message.data});\n      customEvent.source = e.source;\n\n      global.dispatchEvent(customEvent);\n    } catch (x) {}\n  }\n\n  function _addEventListener(type, handler, useCapture) {\n    if ('addEventListener' in global) {\n      global.addEventListener(type, handler, useCapture);\n    } else if ('attachEvent' in global) {\n      global.attachEvent('on' + type, handler);\n    }\n  }\n\n  function _removeEventListener(type, handler, useCapture) {\n    if ('removeEventListener' in global) {\n      global.removeEventListener(type, handler);\n    } else if ('detachEvent' in global) {\n      global.attachEvent('on' + type, handler);\n    }\n  }\n\n\n  function compileOriginRegExp(origins) {\n    return new RegExp('^http(s)?:\\\\/\\\\/' + origins.join('|') + '$', 'i');\n  }\n\n  function compileNamespaceRegExp(namespaces) {\n    return new RegExp('\\\\.(?:' + namespaces.join('|') + ')$')\n  }\n\n  function isWildcard(origin) {\n    return /\\*/.test(origin);\n  }\n\n\n  /** @module frames */\n  /* @example\n   * // parent window\n   * // frames.listen('dfp')\n   * // frames.receiveMessageOnce('init.dfp', callback)\n   * @example\n   * // iframe\n   * // frames.postMessage(window.parent, 'init.dfp', data);\n   */\n  var frames = r.frames = {\n    /*\n     * Send a message to another window.\n     * param {Window} target The frame to deliver the message to.\n     * param {String} type The message type. (if it doesn't include a namespace the default namespace will be used)\n     * param {Object} data The data to send.\n     * param {Object} options The `postMessage` options.\n     * param {String} options.targetOrigin Specifies what the origin of otherWindow must be for the event to be dispatched.\n     */\n    postMessage: function (target, type, data, options) {\n      if (!/\\..+$/.test(type)) {\n        type += DEFAULT_MESSAGE_NAMESPACE;\n      }\n\n      options = options || {};\n      for (var key in DEFAULT_POSTMESSAGE_OPTIONS) {\n        if (!options.hasOwnProperty(key)) {\n          options[key] = DEFAULT_POSTMESSAGE_OPTIONS[key];\n        }\n      }\n\n      target.postMessage(JSON.stringify({type: type, data: data, options: options}), options.targetOrigin);\n    },\n\n    /*\n     * Receive a message from another window.\n     * param {Window} [source] The frame to that send the message.\n     * param {String} type The message type. (if it doesn't include a namespace the default namespace will be used)\n     * param {Function} callback The callback to invoke upon retrieval.\n     * param {Object} [context=this] The context the callback is invoked with.\n     * returns {Object} The listener.\n     */\n    receiveMessage: function (source, type, callback, context) {\n      if (typeof source === 'string') {\n        context = callback;\n        callback = type;\n        type = source;\n        source = null;\n      }\n\n      context = context || this;\n\n      var scoped = function(e) {\n        if (source &&\n            source !== e.source &&\n            source.contentWindow !== e.source) {\n          return;\n        }\n\n        callback.apply(context, arguments);\n      };\n\n      _addEventListener(type, scoped);\n\n      return {\n        off: function() { _removeEventListener(type, scoped); }\n      };\n    },\n\n\n    /*\n     * Proxies messages on a namespace from a frame to a specified target.\n     * param {String} namespace The namespace to proxy.\n     * targets {Array<Window>} [source] The frames to proxy messages to.\n     *  NOTE: supports a single frame as well.\n     */\n    proxy: function(namespace, targets) {\n      this.listen(namespace);\n\n      if (Object.prototype.toString.call(targets) !== '[object Array]') {\n        targets = [targets];\n      }\n\n      var namespaceProxies = proxies[namespace];\n\n      if (namespaceProxies) {\n        namespaceProxies.targets = [].concat(namespaceProxies.targets, target);\n      } else {\n        namespaceProxies = {\n          targets: targets,\n        };\n      }\n\n      proxies[namespace] = namespaceProxies;\n    },\n\n    /*\n     * Receive a message from another window once.\n     * param {Window} [source] The frame to that send the message.\n     * param {String} type The message type. (if it doesn't include a namespace the default namespace will be used)\n     * param {Function} callback The callback to invoke upon retrieval.\n     * param {Object} [context=this] The context the callback is invoked with.\n     * returns {Object} The listener.\n     */\n    receiveMessageOnce: function (source, type, callback, context) {\n      var listener = frames.receiveMessage(source, type, function() {\n        callback && callback.apply(this, arguments);\n\n        listener.off();\n      }, context);\n\n      return listener;\n    },\n\n    /*\n     * Adds an allowed origin to be listened to.\n     * param {String} origin The origin to be added.\n     */\n    addPostMessageOrigin: function (origin) {\n      if (isWildcard(origin)) {\n        allowedOrigins = [ALLOW_WILDCARD];\n      } else if (allowedOrigins.indexOf(origin) === -1) {\n        frames.removePostMessageOrigin(ALLOW_WILDCARD);\n\n        allowedOrigins.push(origin);\n\n        re_postMessageAllowedOrigin = compileOriginRegExp(allowedOrigins);\n      }\n    },\n\n    /*\n     * Removes an origin from the list of those listened to.\n     * param {String} origin The origin to be removed.\n     */\n    removePostMessageOrigin: function (origin) {\n      var index = allowedOrigins.indexOf(origin);\n\n      if (index !== -1) {\n        allowedOrigins.splice(index, 1);\n\n        re_postMessageAllowedOrigin = compileOriginRegExp(allowedOrigins);\n      }\n    },\n\n    /*\n     * Listens to messages on of the specified namespace.\n     * param {String} namespace The namespace to be listened to.\n     */\n    listen: function (namespace) {\n      if (messageNamespaces.indexOf(namespace) === -1) {\n        messageNamespaces.push(namespace);\n        re_messageNamespaces = compileNamespaceRegExp(messageNamespaces);\n      }\n\n      if (!listening) {\n        _addEventListener('message', receiveMessage);\n\n        listening = true;\n      }\n    },\n\n    /*\n     * Stops listening to messages on of the specified namespace.\n     * param {String} namespace The namespace to stop listening to.\n     */\n    stopListening: function (namespace) {\n      var index = messageNamespaces.indexOf(namespace);\n\n      if (index !== -1) {\n        messageNamespaces.splice(index, 1);\n\n        if (messageNamespaces.length) {\n          re_messageNamespaces = compileNamespaceRegExp(messageNamespaces);\n        } else {\n          _removeEventListener('message', receiveMessage);\n          listening = false;\n        }\n      }\n    },\n\n  };\n\n})((this.r = this.r || {}), this);\n"
  },
  {
    "path": "r2/r2/public/static/js/gate-popup.js",
    "content": "!function(r, undefined) {\n  r.ui = r.ui || {};\n\n  var activePopup = null;\n\n  function GatePopup(options) {\n    var realPopup;\n    \n    var fakePopup = {\n      show: function() {\n        if (!realPopup) {\n          initPopup();\n        }\n\n        if (activePopup === this) {\n          return;\n        } else if (activePopup) {\n          activePopup.hide();\n        }\n        activePopup = this;\n        realPopup.show();\n      },\n\n      hide: function() {\n        if (this === activePopup) {\n          activePopup = null;\n        }\n        realPopup.hide();\n      },\n    };\n    \n    function initPopup() {\n      var content = options.content || $('#' + options.templateId).html();\n\n      realPopup = new r.ui.Popup({\n        size: 'large',\n        content: content,\n        className: options.className,\n      });\n\n      realPopup.$.on('click', '.interstitial .c-btn', function(e) {\n        realPopup.hide();\n        return false;\n      });\n\n      realPopup.on('closed.r.popup', function() {\n        if (activePopup === fakePopup) {\n          activePopup = null;\n        }\n      });\n    }\n\n    return fakePopup;\n  };\n\n  r.ui.createGatePopup = function(options) {\n    if (!options) {\n      throw r.errors.createError('GATE_POPUP_JS_ERROR', 'no options given');\n    } else if (!(options.templateId || options.content)) {\n      throw r.errors.createError('GATE_POPUP_JS_ERROR', 'missing templateId or content option');\n    }\n\n    return new GatePopup(options);\n  };\n}(r);\n"
  },
  {
    "path": "r2/r2/public/static/js/gold.js",
    "content": "r.gold = {\n\n    _inlineGilding: false,\n\n    _googleCheckoutAnalyticsLoaded: false,\n\n    init: function () {\n        $('div.content').on(\n            'click',\n            '[name=\"message\"]',\n            this._toggleGiftMessage.bind(this)\n        );\n\n        $('div.content').on(\n            'click',\n            'a.give-gold, .gold-payment .close-button',\n            this._toggleThingGoldForm.bind(this)\n        );\n\n        // this fires when any of the checkout buttons are clicked\n        // updates the signed and giftmessage properties in the payment_blob\n        // failures should be rare and it's probably safe for any updates to be lost\n        $('div.content').on(\n            'click',\n            '.gold-button',\n            this._setGildingProperties.bind(this)\n        );\n\n        $('.stripe-gold').click(function(){\n            $(\"#stripe-payment\").slideToggle()\n        });\n\n        $('#stripe-payment.charge .stripe-submit').on('click', function() {\n            r.gold.tokenThenPost('stripecharge/gold')\n        });\n\n        $('#stripe-payment.modify .stripe-submit').on('click', function() {\n            r.gold.tokenThenPost('modify_subscription')\n        });\n\n        $('h3.toggle').on('click', function() {\n          $(this).toggleClass('toggled');\n          $(this).siblings('.details').slideToggle();\n        });\n\n        $('dt.toggle').on('click', function() {\n          $(this).toggleClass('toggled');\n          $(this).next('dd').slideToggle();\n        });\n\n        if ($('body').hasClass('gold-signup')) {\n          r.gold.signupForm.init();\n        }\n\n        $('form.creddits-gold .remaining').each(r.gold._renderCredditsAmount);\n\n        $(document.body).on('submit', 'form.creddits-gold', function(e) {\n          e.preventDefault();\n          e.stopPropagation();\n\n          r.gold._expendCreddits();\n\n          $(this).find('.gold-checkout:not(.creddits-gold)').hide();\n          return post_form(this, 'spendcreddits')\n        })\n    },\n\n    _toggleGiftMessage: function(e){\n        var messageCheckbox = e.target;\n        var includeMsg = messageCheckbox.checked;\n        var giftmessage_id = $(e.target).parents('.gold-form').find('[name=\"giftmessage\"]').attr('id');\n        var $form = $('#' + giftmessage_id);\n\n        $form.toggleClass('hidden', !includeMsg);\n    },\n    \n    _toggleThingGoldForm: function (e) {\n        if (r.access.isLinkRestricted(e.target)) {\n          return;\n        }\n\n        var $link = $(e.target);\n        var $thing = $link.thing();\n        var thingFullname = $link.thing_id();\n        var wrapId = 'gold_wrap_' + thingFullname;\n        var oldWrap = $('#' + wrapId);\n        var cloneClass;\n\n        if (oldWrap.length) {\n            oldWrap.toggle();\n            return false\n        }\n\n        this._inlineGilding = true;\n\n        r.analytics.fireFunnelEvent('gold', 'open-inline-form', {\n          tracker: 'goldTracker'\n        });\n\n        if (!this._googleCheckoutAnalyticsLoaded) {\n            // we're just gonna hope this loads fast enough since there's no\n            // way to know if it failed and we'd rather the form is still\n            // usable if things don't go well with the analytics stuff.\n            $.getScript('//checkout.google.com/files/digital/ga_post.js');\n            this._googleCheckoutAnalyticsLoaded = true\n        }\n\n        if ($thing.hasClass('link')) {\n            cloneClass = 'cloneable-link'\n        } else {\n            cloneClass = 'cloneable-comment'\n        }\n\n        var goldwrap = $('.gold-wrap.' + cloneClass + ':first').clone();\n        var form = goldwrap.find('.gold-form');\n        var authorName = $link.thing().find('.entry .author:first').text();\n        var passthroughs = form.find('.passthrough');\n        var cbBaseUrl = form.find('[name=\"cbbaseurl\"]').val();\n        var signed = !(form.find('[name=\"signed\"]')).is(':checked');\n\n        goldwrap\n          .removeClass(cloneClass)\n          .addClass('inline-gold')\n          .prop('id', wrapId);\n\n        form.find('p:first-child em').text(authorName);\n        form.find('button').attr('disabled', '');\n        passthroughs.val('');\n\n        $link.new_thing_child(goldwrap);\n\n        // show the throbber if this takes longer than 200ms\n        var workingTimer = setTimeout(function () {\n            form.addClass('working');\n            form.find('button').addClass('disabled')\n        }, 200);\n\n        $.request('generate_payment_blob.json', {thing: thingFullname, signed: signed}, function (token) {\n            clearTimeout(workingTimer);\n            form.removeClass('working');\n            passthroughs.val(token);\n            form.find('.stripe-gold').on('click', function() { window.open('/gold/creditgild/' + token) });\n            form.find('.coinbase-gold').on('click', function() { window.open(cbBaseUrl + \"?c=\" + token) });\n            form.find('button').removeAttr('disabled').removeClass('disabled')\n        });\n\n        return false\n    },\n\n    _setGildingProperties: function (e) {\n        var $button = $(e.target);\n        var thingFullname = $button.thing_id();\n\n        // If /gold, then don't set signed and message properties\n        if (!thingFullname) {\n          $button.parents('form').submit();\n          return;\n        }\n\n        var wrapId = 'gold_wrap_' + thingFullname;\n        var $goldwrap = $('#' + wrapId);\n        var passthroughs = $goldwrap.find('.passthrough');\n        var code = passthroughs.val();\n        var signed = !$goldwrap.find('[name=\"signed\"]').is(':checked');\n        var includeMsg = $goldwrap.find('[name=\"message\"]').is(':checked');\n        var giftmessage = \"\";\n\n        if (includeMsg) {\n          giftmessage = ($goldwrap.find('[name=\"giftmessage\"]')).val();\n        }\n\n        if (this._inlineGilding) {\n          var options = {\n            label: $button.closest('[data-vendor]').data('vendor'),\n            tracker: 'goldTracker'\n          };\n          r.analytics.fireFunnelEvent('gold', 'checkout', options);\n        }\n\n        $.request('modify_payment_blob.json', {code: code, signed: signed, message: giftmessage}, function() {\n          $button.parents('form').submit();\n        });\n    },\n\n    // When spending creddits, update the templates we use to generate the gilding form to display the\n    // new total creddits remaining, or hide it if we have less than the current cost of gilding (1 creddit).\n    _expendCreddits: function() {\n      $('.cloneable-comment, .cloneable-link').find('form.creddits-gold .remaining').each(function() {\n        var $this = $(this);\n        var currentCreddits = parseInt($this.data('current'), 10);\n        var totalCreddits = parseInt($this.data('total'), 10);\n        var newTotal = totalCreddits - currentCreddits;\n\n        if (newTotal < currentCreddits) {\n          $this.parents('form.creddits-gold').remove()\n        } else {\n          $(this).data('total', newTotal);\n          r.gold._renderCredditsAmount.apply(this)\n        }\n      })\n    },\n\n    _renderCredditsAmount: function() {\n      var $this = $(this);\n      var tpl = $this.data('template');\n      $this.html(_.template(tpl, _.omit($this.data(), 'template')))\n    },\n\n    gildThing: function (thing_fullname, new_title, specified_gilding_count) {\n        var thing = $('.id-' + thing_fullname);\n\n        if (!thing.length) {\n            console.log(\"couldn't gild thing \" + thing_fullname);\n            return\n        }\n\n        var tagline = thing.children('.entry').find('p.tagline'),\n            icon = tagline.find('.gilded-icon');\n\n        // when a thing is gilded interactively, we need to increment the\n        // gilding count displayed by the UI. however, when gildings are\n        // instantiated from a cached comment page via thingupdater, we can't\n        // simply increment the gilding count because we do not know if the\n        // cached comment page already includes the gilding in its count. To\n        // resolve this ambiguity, thingupdater will provide the correct\n        // gilding count as specified_gilding_count when calling this function.\n        var gilding_count;\n        if (specified_gilding_count != null) {\n            gilding_count = specified_gilding_count\n        } else {\n            gilding_count = icon.data('count') || 0;\n            gilding_count++\n        }\n\n        thing.addClass('gilded user-gilded');\n        if (!icon.length) {\n            icon = $('<span>')\n                        .addClass('gilded-icon');\n            tagline.append(icon)\n        }\n        icon\n            .attr('title', new_title)\n            .data('count', gilding_count);\n        if (gilding_count > 1) {\n            icon.text('x' + gilding_count)\n        }\n\n        thing.children('.entry').find('.give-gold').parent().remove()\n    },\n\n    tokenThenPost: function (dest) {\n        var postOnSuccess = function (status_code, response) {\n            var form = $('#stripe-payment'),\n                submit = form.find('.stripe-submit'),\n                status = form.find('.status'),\n                token = form.find('[name=\"stripeToken\"]');\n\n            if (response.error) {\n                submit.removeAttr('disabled');\n                status.html(response.error.message)\n            } else {\n                token.val(response.id);\n                post_form(form, dest)\n            }\n        };\n        r.gold.makeStripeToken(postOnSuccess)\n    },\n\n    makeStripeToken: function (responseHandler) {\n        var form = $('#stripe-payment'),\n            publicKey = form.find('[name=\"stripePublicKey\"]').val(),\n            submit = form.find('.stripe-submit'),\n            status = form.find('.status'),\n            token = form.find('[name=\"stripeToken\"]'),\n            cardName = form.find('.card-name').val(),\n            cardNumber = form.find('.card-number').val(),\n            cardCvc = form.find('.card-cvc').val(),\n            expiryMonth = form.find('.card-expiry-month').val(),\n            expiryYear = form.find('.card-expiry-year').val(),\n            cardAddress1 = form.find('.card-address_line1').val(),\n            cardAddress2 = form.find('.card-address_line2').val(),\n            cardCity = form.find('.card-address_city').val(),\n            cardState = form.find('.card-address_state').val(),\n            cardCountry = form.find('.card-address_country').val(),\n            cardZip = form.find('.card-address_zip').val();\n        Stripe.setPublishableKey(publicKey);\n\n        var showError = function(inputSelector, str) {\n          form.find('.status')\n            .addClass('error')\n            .text(str);\n          $(inputSelector).focus()\n        };\n\n        if (!cardName) {\n            showError('.card-name', r._('missing name'))\n        } else if (!(Stripe.validateCardNumber(cardNumber))) {\n            showError('.card-number', r._('invalid credit card number'))\n        } else if (!Stripe.validateExpiry(expiryMonth, expiryYear)) {\n            showError('.card-expiry-month', r._('invalid expiration date'))\n        } else if (!Stripe.validateCVC(cardCvc)) {\n            showError('.card-cvc', r._('invalid cvc'))\n        } else if (!cardAddress1) {\n            showError('.card-address_line1', r._('missing address'))\n        } else if (!cardCity) {\n            showError('.card-address_city', r._('missing city'))\n        } else if (!cardCountry) {\n            showError('.card-address_country', r._('missing country'))\n        } else {\n            status\n              .removeClass('error')\n              .text(r.config.status_msg.submitting);\n            submit.attr('disabled', 'disabled');\n            Stripe.createToken({\n                    name: cardName,\n                    number: cardNumber,\n                    cvc: cardCvc,\n                    exp_month: expiryMonth,\n                    exp_year: expiryYear,\n                    address_line1: cardAddress1,\n                    address_line2: cardAddress2,\n                    address_city: cardCity,\n                    address_state: cardState,\n                    address_country: cardCountry,\n                    address_zip: cardZip\n                }, responseHandler\n            )\n        }\n        return false\n    }\n};\n\nr.gold.signupForm = (function() {\n\n  // Get all field names relevant to this goldtype.\n  // This helps us keep a clean URL state.\n  function _getRelevantFields() {\n    var goldtype = $('#goldtype').val();\n    var fields = ['goldtype'];\n\n    switch (goldtype) {\n      case 'autorenew':\n        fields.push('period');\n        break;\n      case 'onetime':\n        fields.push('months');\n        break;\n      case 'code':\n        fields.push('months', 'email');\n        break;\n      case 'gift':\n        fields.push('months', 'recipient', 'signed', 'giftmessage');\n        break;\n      case 'creddits':\n        fields.push('num_creddits');\n        break\n    }\n\n    return fields\n  }\n\n  // Given a field, get its value, regardless of input type.\n  function _getFieldValue(field) {\n    var $field = $(field);\n\n    if ($field.is(':radio') && !$field.is(':checked')) {\n      throw 'Unchecked radio button has no value'\n    }\n\n    if ($field.is(':checkbox')) {\n      value = $field.is(':checked') ? $field.val() : null\n    } else if ($field.is('select')) {\n      value = $field.find('option:selected').val()\n    } else {\n      value = $field.val()\n    }\n\n    return value\n  }\n\n  function _updateUrlState() {\n    var a = $(\"<a />\").get(0);\n    var urlFields = _getRelevantFields();\n    var params = {};\n\n    if (!('replaceState' in window.history)) {\n      return\n    }\n\n    $('form.gold-form').find(':input').each(function() {\n      var $field = $(this)\n\n      if (!_.contains(urlFields, this.name)) {\n        return\n      }\n\n      try {\n        params[this.name] = _getFieldValue(this)\n      } catch(e) {\n        return\n      }\n    });\n\n    params['edit'] = true;\n\n    a.href = window.location.href;\n    a.search = $.param(params);\n    window.history.replaceState({}, \"\", a.href)\n  }\n\n  function _updateGoldType() {\n    var $gifttype = $('input[name=\"gifttype\"]:checked');\n    var $tab = $('.tab.active');\n    var isGift = $('#gift').is(':checked');\n    var goldtype;\n\n    if ($tab.prop('id') == 'autorenew') {\n      goldtype = 'autorenew'\n    } else if ($tab.prop('id') == 'creddits') {\n      goldtype = 'creddits'\n    } else if (isGift && $gifttype.length > 0) {\n      goldtype = $gifttype.val()\n    } else {\n      goldtype = 'onetime'\n    }\n\n    $('#goldtype').val(goldtype);\n    _updateUrlState()\n  }\n\n  function _setTabFocus(tab) {\n    $('#form-options, #payment-options').show();\n\n    $('.active').removeClass('active');\n    $('#redeem-a-code, .question').hide();\n\n    $(tab).addClass('active');\n    $(tab.hash).addClass('active');\n\n    _updateGoldType()\n  }\n\n  // On submit, pass only the relevant fields to the payment page, for clean URLs and proper\n  // display of the payment summary.\n  function _handleSubmit(e) {\n    e.stopPropagation();\n    e.preventDefault();\n\n    /* Our IE placeholder handling is miserable, clear out placeholder text before submission if we have it. */\n    $('#giftmessage, #recipient').each(function() {\n      var $this = $(this);\n      if ($this.val() === $this.attr('placeholder')) {\n        $this.val('')\n      }\n    });\n\n    // serializeArray returns an array of objects, turn it into key/value pairs\n    // since we're not worried about multi-value keys and it's what $.param expects\n    var fields = $('form.gold-form').serializeArray();\n    var fieldsAsDict = _.object(_.pluck(fields, 'name'), _.pluck(fields, 'value'));\n\n    // Only submit fields that are relevant to this goldtype\n    var submission = _.pick(fieldsAsDict, _getRelevantFields());\n\n    window.location = \"/gold/payment?\" + $.param(submission)\n  }\n\n  function init() {\n    var $form = $('form.gold-form');\n\n    $('a.tab-toggle').on('click', function(e) {\n      e.stopPropagation();\n      e.preventDefault();\n\n      _setTabFocus(this)\n    });\n\n    $('input[name=\"gift\"]').change(function() {\n      $('#gifting-details').slideToggle($(this).val());\n      _updateGoldType()\n    });\n\n    // Workaround for our form cloning to maintain back buttons. When we clone the form\n    // the selected attribute is respected more than the current state unless we explicitly alter it\n    $('.gold-dropdown').on('change', function() {\n      $(this).find('[selected]').removeAttr('selected');\n      $(this).find(':selected').get(0).setAttribute('selected', 'selected')\n    });\n\n    var hasPlaceholder = ('placeholder' in document.createElement('input'));\n    $('input[name=\"gifttype\"]').change(function() {\n      $('#gifttype-details-gift').toggleClass('hidden', this.value !== 'gift');\n      if (hasPlaceholder) {\n        $('#gifttype-details-gift :input:eq(0)').focus()\n      }\n      _updateGoldType()\n    });\n\n    $('#giftmessage').on('keyup', function() {\n      $('#message').prop('checked', $(this).val() !== '')\n    });\n\n    $form.on('submit', _handleSubmit);\n\n    $form.find(':input').on('change', _updateUrlState);\n\n    $('input[name=\"code\"]').on('focus', function() {\n      $('.redeem-submit').slideDown()\n    })\n  }\n\n  return {\n    'init': init\n  }\n}());\n\n!(function($) {\n    $.gild_thing = function (thing_fullname, new_title) {\n        r.gold.gildThing(thing_fullname, new_title);\n        $('#gold_wrap_' + thing_fullname).fadeOut(400)\n    }\n})(jQuery);\n"
  },
  {
    "path": "r2/r2/public/static/js/google-tag-manager/gtm-jail-listener.js",
    "content": ";(function(global, r, undefined) {\n  var jail = document.createElement('iframe');\n\n  r.frames.proxy('gtm', [jail.contentWindow, window.parent]);\n\n  jail.style.display = 'none';\n  jail.referrer = 'no-referrer';\n  jail.id = 'jail';\n  jail.name = window.name;\n  jail.src = '/gtm?id=' + global.CONTAINER_ID;\n\n  document.body.appendChild(jail);\n})(this, this.r);\n"
  },
  {
    "path": "r2/r2/public/static/js/google-tag-manager/gtm-listener.js",
    "content": ";(function(gtm, global, r, undefined) {\n  var dataLayer = global.googleTagManager = global.googleTagManager || [];\n\n  r.frames.listen('gtm');\n\n  r.frames.receiveMessage('data.gtm', function(e) {\n    dataLayer.push(e.detail);\n  });\n\n  r.frames.receiveMessage('event.gtm', function(e) {\n    dataLayer.push(e.detail);\n  });\n\n})((this.gtm = this.gtm || {}), this, this.r);\n"
  },
  {
    "path": "r2/r2/public/static/js/google-tag-manager/gtm.js",
    "content": ";(function(r, global, undefined) {\n  var jail = document.getElementById('gtm-jail');\n\n  r.gtm = {\n\n    trigger: function(eventName, payload) {\n      if (payload) {\n        this.set(payload);\n      }\n\n      r.frames.postMessage(jail.contentWindow, 'event.gtm', {\n        event: eventName,\n      });\n    },\n\n    set: function(data) {\n      r.frames.postMessage(jail.contentWindow, 'data.gtm', data);\n    },\n\n  };\n\n})((this.r = this.r || {}), this);\n"
  },
  {
    "path": "r2/r2/public/static/js/highlight.js",
    "content": "hljs.initHighlightingOnLoad()\n"
  },
  {
    "path": "r2/r2/public/static/js/hooks.js",
    "content": "/*\n  Provides a very simple hook system for one-off event hooks.\n\n  r.hooks.get('init').register(someFunction);\n  r.hooks.get('init').call();\n */\n\n!function(r) {\n  var hooks = {};\n\n  function Hook(name) {\n    this.name = name;\n    this.called = false;\n    this._callbacks = [];\n  }\n\n  Hook.prototype.register = function(callback) {\n    if (this.called) {\n      callback.call(window);\n    } else {\n      this._callbacks.push(callback);\n    }\n  };\n\n  Hook.prototype.call = function() {\n    if (this.called) {\n      throw 'Hook ' + this.name + ' already called.';\n    } else {\n      var callbacks = this._callbacks;\n      this.called = true;\n      this._callbacks = null;\n\n      for (var i = 0; i < callbacks.length; i++) {\n        callbacks[i].call(window);\n      }\n    }\n  };\n\n  r.hooks = {\n    get: function(name) {\n      if (name in hooks) {\n        return hooks[name];\n      } else {\n        var hook = new Hook(name);\n        hooks[name] = hook;\n        return hook;\n      }\n    },\n\n    call: function(name) {\n      return r.hooks.get(name).call();\n    },\n  };\n}((window.r = window.r || {}));\n"
  },
  {
    "path": "r2/r2/public/static/js/https-tester.js",
    "content": "!function(r) {\n\n  function makeLoadHandler(testImgType, loadState, config) {\n    return function() {\n      config.imgState[testImgType] = loadState;\n      sendHTTPSCompatResults(config);\n    };\n  }\n\n  function setUpHTTPSTestImage(type, url, config) {\n    var img = jQuery('<img>');\n    img.on('load', makeLoadHandler(type, true, config));\n    img.on('error', makeLoadHandler(type, false, config));\n    img.attr('src', url);\n  }\n\n  function sendHTTPSCompatResults(config) {\n    if (config.imgState['test'] === undefined ||\n        config.imgState['control'] === undefined) {\n      return;\n    }\n    // guard against handlers triggering multiple times for whatever reason\n    if (config.sentReport) {\n      return;\n    }\n    // failed due to cross-origin resource loading restrictions\n    // (or an ad blocker?) ignore.\n    if (config.imgState['control'] === false) {\n      return;\n    }\n\n    config.sentReport = true;\n    var result = config.imgState['test'];\n    var pixel = new Image();\n    var params = 'run_name=' + config.runName + '&valid=' + result + '&uuid=' + r.uuid();\n    pixel.src = config.logPixel + '?' + params;\n  }\n\n  window.runHTTPSCertTest = function(config) {\n    config = jQuery.extend({}, config);\n    config.imgState = {};\n    config.sentReport = false;\n    setUpHTTPSTestImage('control', config.controlImg, config);\n    setUpHTTPSTestImage('test', config.testImg, config);\n  }\n}(r);\n"
  },
  {
    "path": "r2/r2/public/static/js/i18n.js",
    "content": "r.i18n = {\n    jed: new Jed({\n        'locale_data': {\n            'messages': {\n                '': {\n                    'domain': 'messages',\n                    'lang': 'en'\n                }\n            }\n        }\n    }),\n\n    setPluralForms: function (pluralForms) {\n        this.jed.options.locale_data.messages[''].plural_forms = pluralForms\n    },\n\n    addMessages: function (messages) {\n        _.extend(this.jed.options.locale_data.messages, messages)\n    }\n}\n\nr._ = _.bind(r.i18n.jed.gettext, r.i18n.jed)\nr.P_ = _.bind(r.i18n.jed.ngettext, r.i18n.jed)\nr.N_ = _.identity\nr.NP_ = function (singular, plural) {\n    return [singular, plural]\n}\n"
  },
  {
    "path": "r2/r2/public/static/js/image-upload.js",
    "content": "!(function(global, $, r, undefined) {\n  'use strict';\n\n  var CALLBACK_PREFIX = '__image_upload__';\n\n  var DEFAULTS = {\n    max: 1024 * 500,\n    errors: {\n      unknown: r._('something went wrong.'),\n    }\n  };\n\n\n  var ImageUpload = function(element, options) {\n    this.initialize(element, options);\n  };\n\n  _.extend(ImageUpload.prototype, {\n    _callbacks: {},\n\n    initialize: function(element, options) {\n      this.el = element;\n      this.$el = $(element);\n      this.$file = this.$el.find('[type=\"file\"]');\n      this.file = this.$file.get(0);\n      this.options = _.defaults({}, this.$el.data(), options, DEFAULTS);\n      this._bindEvents();\n      this.ajax = !!global.FormData;\n\n      return this;\n    },\n\n    _bindEvents: function() {\n      this.$file.on('change', this._handleChange.bind(this));\n      this.$el.find('.c-image-upload-btn')\n        .on('click',this._triggerDialog.bind(this));\n    },\n\n    _triggerDialog: function() {\n      this.$file.click();\n    },\n\n    _handleChange: function() {\n      if (this.file.value) {\n        var files = this.file.files;\n\n        if (files && files.length && files[0].size > this.options.maxSize) {\n          this.$el.trigger('failed.imageUpload', [{\n            message: r._('too big. keep it under ' + r.utils.formatFileSize(this.options.maxSize)),\n          }]);\n\n          this._reset();\n        } else {\n          $.ajax({\n            url: this.options.url,\n            type: 'POST',\n            dataType: 'json',\n            data: _.extend({}, this.options.params, {\n              filepath: files[0].name,\n              uh: r.config.modhash,\n              ajax: this.ajax,\n              raw_json: '1',\n            }),\n          })\n          .fail(function(xhr) {\n            var resp = xhr.responseJSON;\n\n            this.$el.trigger('failed.imageUpload', [{\n              message: resp.message || this.options.errors.unknown,\n            }]);\n          }.bind(this))\n          .done(this._submit.bind(this));\n        }\n      }\n\n      return true;\n    },\n\n    _updateProgress: function(percentage) {\n      this._showProgress();\n\n      if (percentage) {\n        this._percentage = percentage;\n        this.$el.find('.c-progress-bar').css({ width: (percentage + '%') });\n      }\n    },\n\n    _showProgress: function() {\n      this.$el.find('.c-progress').show();\n    },\n\n    _hideProgress: function() {\n      this.$el.find('.c-progress').hide();\n      this.$el.find('.c-progress-bar').css({ width: 0 });\n    },\n\n    _submit: function(overrides) {\n      overrides = overrides || {};\n\n      this.$el.attr('action', overrides.action);\n\n      overrides.fields.forEach(function(field) {\n        var name = field.name;\n        var value = field.value;\n        var unset = value === '' || value === null\n        var $input = this.$el.find('[name=\"' + name + '\"]');\n\n        if (!$input.length) {\n          // Skip null values\n          if (unset) {\n            return;\n          }\n\n          $('<input type=\"hidden\">')\n            .attr('name', name)\n            .val(value)\n            .insertBefore(this.$file);\n        } else {\n          if (unset) {\n            $input.remove();\n          } else {\n            $input.val(value);\n          }\n        }\n      }.bind(this));\n\n      if (!this.ajax) {\n        var $redirect = this.$el.find('[name=\"success_action_redirect\"]');\n        var redirect = $redirect.val();\n        var callback = _.uniqueId(CALLBACK_PREFIX);\n\n        redirect = r.utils.replaceUrlParams(redirect, {callback: callback});\n\n        $redirect.val(redirect);\n\n        global.__s3_callbacks__[callback] = this._callbacks[callback] = this._iframeComplete.bind(this);\n        this._fakeProgressTo(10, 80);\n        this.$el.submit();\n      } else {\n        var fields = {};\n        var data = new FormData();\n        var fileInput;\n\n        this.$el.find('input').each(function() {\n          var el = this;\n          var $el = $(el);\n          var type = $el.attr('type');\n\n          if (type !== 'file') {\n            data.append($el.attr('name'),  $el.val());\n          }\n        });\n\n        data.append(this.file.name, this.file.files[0]);\n\n        this._showProgress();\n\n        $.ajax({\n          url: this.$el.attr('action'),\n          type: this.$el.attr('method'),\n          contentType: false,\n          processData: false,\n          data: data,\n          dataType: 'xml',\n          success: this._ajaxSuccess.bind(this),\n          error: this._ajaxError.bind(this),\n          progress: this._ajaxProgress.bind(this),\n          complete: this._reset.bind(this),\n          xhr: function() {\n            var xhr = $.ajaxSettings.xhr();\n\n            if (xhr instanceof global.XMLHttpRequest) {\n              xhr.addEventListener('progress', this.progress, false);\n            }\n            \n            if (xhr.upload) {\n              xhr.upload.addEventListener('progress', this.progress, false);\n            }\n            \n            return xhr;\n          },\n        });\n      }\n\n      this.$el.trigger('uploading.imageUpload');\n    },\n\n    _fakeProgressTo: function(start, end) {\n      var _fakeProgressInterval = this._fakeProgress = setInterval(function() {\n        if (this._percentage > end) {\n          clearInterval(_fakeProgressInterval);\n          return;\n        }\n\n        this._updateProgress((this._percentage || start) + 1);\n      }.bind(this), 200);\n    },\n\n    _ajaxProgress: function(e) {\n      var percentage = Math.round((e.loaded / e.total) * 100);\n\n      this._updateProgress(percentage);\n\n      this.$el.trigger('progress.imageUpload', [{\n        complete: e.loaded,\n        total: e.total,\n        percentage: percentage,\n      }]);\n    },\n\n    _updatePreview: function (url, callback) {\n      callback = callback || $.noop;\n\n      var $preview = this.$el.find('.c-image-upload-preview');\n      var src = r.utils.replaceUrlParams(url, {\n        cb: (+new Date()),\n      });\n\n      $preview.one('load', callback.bind(this));\n      $preview.attr('src', src);\n    },\n\n    _ajaxSuccess: function(xml) {\n      var $xml = $(xml);\n      var url = $xml.find('Location').text();\n\n      this._updatePreview(url, this._hideProgress);\n      this._updateProgress(100);\n      this.$el.trigger('success.imageUpload', [{\n        url: url,\n      }]);\n    },\n\n    _ajaxError: function(xhr) {\n      var $xml = $(xhr.responseXML);\n      var message = this.options.errors.unknown;\n\n      if ($xml && $xml.length) {\n        message = $xml.find('Message').text();\n      }\n\n      this._hideProgress();\n      this.$el.trigger('failed.imageUpload', [{\n        message: message,\n      }]);\n    },\n\n    _iframeComplete: function(data) {\n      this._updateProgress(100);\n      this._updatePreview(data.url, this._hideProgress);\n    },\n\n    _reset: function() {\n      this.$file.resetInput();\n      this.$el.trigger('reset.imageUpload');\n    },\n\n  });\n\n  function Plugin(option /* ,args... */) {\n    var args = _.toArray(arguments).slice(1);\n\n    if (option && /^get/.test(option)) {\n      var data = this.data('c.imageUpload');\n\n      return data && data[option].apply(data, args);\n    }\n\n    return this.each(function() {\n      var $el = $(this);\n      var data = $el.data('c.imageUpload');\n      var options = typeof option === 'object' && option;\n\n      if (!data) {\n        data = new ImageUpload(this, options);\n        $el.data('c.imageUpload', data);\n      }\n\n      if (typeof option === 'string') {\n        data[option].apply(data, args);\n      }\n    });\n  };\n\n  $.fn.imageUpload = Plugin;\n  $.fn.imageUpload.Constructor = ImageUpload;\n\n})(this, this.jQuery, this.r);\n"
  },
  {
    "path": "r2/r2/public/static/js/interestbar.js",
    "content": "r.interestbar = {\n    init: function() {\n        new r.ui.InterestBar($('.sr-interest-bar'))\n    }\n}\n\nr.ui.InterestBar = function() {\n    r.ui.Base.apply(this, arguments)\n    this.$query = this.$el.find('.query')\n    this.queryChangedDebounced = _.debounce($.proxy(this, 'queryChanged'), 500)\n    this.$query.on('keyup', $.proxy(this, 'keyPressed'))\n\n    this.$query\n        .on('focus', $.proxy(function() {\n            this.$el.addClass('focus')\n        }, this))\n        .on('blur', $.proxy(function() {\n            this.$el.removeClass('focus')\n        }, this))\n}\nr.ui.InterestBar.prototype = {\n    keyPressed: function() {\n        var query = this.$query.val()\n        query = $.trim(query)\n        if (query == this._lastQuery) {\n            return\n        } else {\n            this._lastQuery = query\n        }\n\n        this.queryChangedDebounced(query)\n        if (query && query.length > 1) {\n            this.$el.addClass('working')\n        } else {\n            this.hideResults()\n            this.$el.removeClass('working error')\n        }\n    },\n\n    queryChanged: function(query) {\n        if (query && query.length > 1) {\n            $.ajax({\n                url: '/api/subreddits_by_topic.json',\n                data: {'query': query},\n                success: $.proxy(this, 'displayResults'),\n                error: $.proxy(this, 'displayError')\n            })\n        }\n    },\n\n    displayResults: function(results) {\n        this.$el.removeClass('working error')\n\n        var first = this.$el.find('.results li:first'),\n            last = this.$el.find('.results li:last')\n\n        var item = _.template(\n            '<li><a href=\"/r/<%= name %>\" target=\"_blank\">'\n                +'/r/<%= name %>'\n            +'</a></li>'\n        )\n\n        this.$el.find('.results')\n            .empty()\n            .append(first)\n            .append(_.map(results, item).join(''))\n            .append(last)\n            .slideDown(150)\n    },\n\n    hideResults: function() {\n        this.$el.find('.results').slideUp(150)\n    },\n\n    displayError: function(xhr) {\n        this.$el\n            .removeClass('working')\n            .addClass('error')\n            .find('.error-caption')\n                .text(r._('an error occurred. please try again later! (status: %(status)s)').format({status: xhr.status}))\n\n        this.hideResults()\n    }\n}\n"
  },
  {
    "path": "r2/r2/public/static/js/jail.js",
    "content": ";(function(r, global, undefined) {\n  var jail = document.getElementById('gtm-sandbox');\n\n  r.jail = {\n\n    postMessage: function(id, eventName, data) {\n\n    },\n\n    trigger: function(eventName, payload) {\n      if (payload) {\n        this.set(payload);\n      }\n\n      r.frames.postMessage(sandbox.contentWindow, 'event.gtm', {\n        event: eventName,\n      });\n    },\n\n    set: function(data) {\n      r.frames.postMessage(sandbox.contentWindow, 'data.gtm', data);\n    },\n\n  };\n\n})((this.r = this.r || {}), this);\n"
  },
  {
    "path": "r2/r2/public/static/js/jquery.reddit.js",
    "content": "/* The reddit extension for jquery.  This file is intended to store\n * \"utils\" type function declarations and to add functionality to \"$\"\n * or \"jquery\" lookups. See \n *   http://docs.jquery.com/Plugins/Authoring \n * for the plug-in spec.\n*/\n\n(function($) {\n\n/* utility functions */\n\n$.log = function(message) {\n    if (window.console) {\n        if (window.console.debug)\n            window.console.debug(message);\n        else if (window.console.log)\n            window.console.log(message);\n    }\n    else\n        alert(message);\n};\n\n$.debug = function(message) {\n    if ($.with_default(r.config.debug, false)) {\n        return $.log(message);\n    }\n}\n$.fn.debug = function() { \n    $.debug($(this));\n    return $(this);\n}\n\n$.redirect = function(dest) {\n    window.location = dest;\n};\n\n$.fn.redirect = function(dest) {\n    /* for forms which are \"posting\" by ajax leading to a redirect */\n    $(this).filter(\"form\").find(\".status\").show().html(\"redirecting...\");\n    var target = $(this).attr('target');\n    if(target == \"_top\") {\n      var w = window;\n      while(w != w.parent) {\n        w = w.parent;\n      }\n      w.location = dest;\n    } else {\n      $.redirect(dest);\n    }\n    /* this should never happen, but for the sake of internal consistency */\n    return $(this)\n}\n\n$.refresh = function() {\n    window.location.reload(true);\n};\n\n$.defined = function(value) {\n    return (typeof(value) != \"undefined\");\n};\n\n$.with_default = function(value, alt) {\n    return $.defined(value) ? value : alt;\n};\n\n$.websafe = function(text) {\n    if(typeof(text) == \"string\") {\n        text = text.replace(/&/g, \"&amp;\")\n            .replace(/\"/g, '&quot;') /* \" */\n            .replace(/>/g, \"&gt;\").replace(/</g, \"&lt;\")\n    }\n    return (text || \"\");\n};\n\n$.unsafe = function(text) {\n    /* inverts websafe filtering of reddit app. */\n    if(typeof(text) == \"string\") {\n        text = text.replace(/&quot;/g, '\"')\n            .replace(/&gt;/g, \">\").replace(/&lt;/g, \"<\")\n            .replace(/&amp;/g, \"&\");\n    }\n    return (text || \"\");\n};\n\n$.uniq = function(list, max) {\n    /* $.unique only works on arrays of DOM elements */\n    var ret = [];\n    var seen = {};\n    var num = max ? max : list.length;\n    for(var i = 0; i < list.length && ret.length < num; i++) {\n        if(!seen[list[i]]) {\n            seen[list[i]] = true;\n            ret.push(list[i]);\n        }\n    }\n    return ret;\n};\n\n/* upgrade show and hide to trigger onshow/onhide events when fired. */\n(function(show, hide) {\n    $.fn.show = function(speed, callback) {\n        $(this).trigger(\"onshow\");\n        return show.call(this, speed, callback);\n    }\n    $.fn.hide = function(speed, callback) {\n        $(this).trigger(\"onhide\");\n        return hide.call(this, speed, callback);\n    }\n})($.fn.show, $.fn.hide);\n\n/* customized requests (formerly redditRequest) */\n\nvar _ajax_locks = {};\nfunction acquire_ajax_lock(op) {\n    if(_ajax_locks[op]) {\n        return false;\n    }\n    _ajax_locks[op] = true;\n    return true;\n};\n\nfunction release_ajax_lock(op) {\n    delete _ajax_locks[op];\n};\n\nfunction handleResponse(action) {\n    return function(res) {\n        if(res.jquery) {\n            var objs = {};\n            objs[0] = jQuery;\n            $.map(res.jquery, function(q) {\n                    var old_i = q[0], new_i = q[1], op = q[2], args = q[3];\n                    if (typeof(args) == \"string\") {\n                      args = $.unsafe(args);\n                    } else { // assume array\n                      for(var i = 0; args.length && i < args.length; i++)\n                        args[i] = $.unsafe(args[i]);\n                    }\n                    if (op == \"call\") \n                        objs[new_i] = objs[old_i].apply(objs[old_i]._obj, args);\n                    else if (op == \"attr\") {\n                        // remove beforeunload event handler if exists for redirects\n                        if(args == 'redirect') {\n                          $(window).off('beforeunload');\n                        }\n                        objs[new_i] = objs[old_i][args];\n                        if(objs[new_i])\n                            objs[new_i]._obj = objs[old_i];\n                        else {\n                            $.debug(\"unrecognized\");\n                        }\n                    } else if (op == \"refresh\") {\n                        $.refresh();\n                    } else {\n                        $.debug(\"unrecognized\");\n                    }\n                });\n        }\n    };\n};\n$.handleResponse = handleResponse;\n\nvar api_loc = '/api/';\n$.request = function(op, parameters, worker_in, block, type, \n                     get_only, errorhandler) {\n    /* \n       Uniquitous reddit AJAX poster.  Automatically addes\n       handleResponse(action) worker to deal with the API result.  The\n       current subreddit (r.config.post_site) and the user's modhash\n       (r.config.modhash) are also automatically sent across.\n     */\n    var action = op;\n    var worker = worker_in;\n\n    if (rate_limit(op)) {\n        if (errorhandler) {\n            errorhandler('ratelimit')\n        }\n        return\n    }\n\n    if (window != window.top) {\n        return\n    }\n\n    /* we have a lock if we are not blocking or if we have gotten a lock */\n    var have_lock = !$.with_default(block, false) || acquire_ajax_lock(action);\n\n    parameters = $.with_default(parameters, {});\n    worker_in  = $.with_default(worker_in, handleResponse(action));\n    type  = $.with_default(type, \"json\");\n\n    var form = $('form.warn-on-unload');\n\n    if (typeof(worker_in) != 'function')\n        worker_in  = handleResponse(action);\n    var worker = function(res) {\n        release_ajax_lock(action);\n\n        /*\n         * check if there exists a form that has the\n         * warn-on-unload class. Remove the beforeunload event\n         * listener if the form submission was successful\n         * and we dont warn the user on succesful form submissions\n         */\n        if($(form).length && res.success) {\n          $(window).off('beforeunload');\n        }\n        return worker_in(res);\n    };\n    /* do the same for the error handler, and make sure to release the lock*/\n    errorhandler_in = $.with_default(errorhandler, function() { });\n    errorhandler = function(r) {\n        release_ajax_lock(action);\n        return errorhandler_in(r);\n    };\n\n\n    get_only = $.with_default(get_only, false);\n\n    /* set the subreddit name if there is one */\n    if (r.config.post_site) \n        parameters.r = r.config.post_site;\n\n    /* add the modhash if the user is logged in */\n    if (r.config.logged) \n        parameters.uh = r.config.modhash;\n\n    parameters.renderstyle = r.config.renderstyle;\n\n    if(have_lock) {\n        op = api_loc + op;\n        /*if( document.location.host == r.config.ajax_domain ) \n            /* normal AJAX post */\n\n        $.ajax({ type: (get_only) ? \"GET\" : \"POST\",\n                    url: op, \n                    data: parameters, \n                    success: worker,\n                    error: errorhandler,\n                    dataType: type});\n        /*else { /* cross domain it is... * /\n            op = \"http://\" + r.config.ajax_domain + op + \"?callback=?\";\n            $.getJSON(op, parameters, worker);\n            } */\n    }\n};\n\nvar up_cls = \"up\";\nvar upmod_cls = \"upmod\";\nvar down_cls = \"down\";\nvar downmod_cls = \"downmod\";\n\nrate_limit = (function() {\n    var default_rate_limit = 333,  // default rate-limit duration (in ms)\n        rate_limits = {  // rate limit per-action (in ms, 0 = don't rate limit)\n            \"vote\": 333,\n            \"comment\": 333,\n            \"ignore\": 0,\n            \"ban\": 0,\n            \"unban\": 0,\n            \"assignad\": 0\n        },\n        last_dates = {}\n\n    // paranoia: copy global functions used to avoid tampering.\n    var _Date = Date\n\n    return function rate_limit(action) {\n        var now = new _Date(),\n            allowed_interval = action in rate_limits ?\n                               rate_limits[action] : default_rate_limit,\n            last_date = last_dates[action],\n            rate_limited = last_date && (now - last_date) < allowed_interval\n\n        last_dates[action] = now\n        return rate_limited\n    };\n})()\n\n$.fn.removeLinkFlairClass = function () {\n  $(this)\n    .removeClass(\"linkflair\")\n    .attr('class', function(i, c) {\n      return (c.replace(/(^|\\s)linkflair\\S+/g, ''));\n    });\n};\n\n$.fn.updateThing = function(update) {\n    var $thing = $(this);\n    var $entry = $thing.children('.entry');\n\n    if ('enemy' in update) {\n        // TODO: this will hide comments of enemies along with all of their\n        // children.  The better alternative would be to make it render as\n        // deleted.\n        $thing.remove();\n        return;\n    }\n\n    if ('friend' in update) {\n        var label = '<a class=\"friend\" title=\"friend\" href=\"/prefs/friends\">F</a>';\n        \n        $entry.find('.author')\n              .addClass('friend')\n              .next('.userattrs')\n              .each(function() {\n                    var $this = $(this);\n\n                    if (!$this.html()) {\n                        $this.html(' [' + label + ']');\n                    } else if (!$this.find('.friend').length) {\n                        $this.find('a:first').before(label + ',');\n                    }\n              });\n    }\n\n    if ('voted' in update) {\n        var $midcol = $thing.children('.midcol');\n        var $up = $midcol.find('.arrow.'+up_cls+', .arrow.'+upmod_cls);\n        var $down = $midcol.find('.arrow.'+down_cls+', .arrow.'+downmod_cls);\n        var $elems = $($midcol).add($entry);\n\n        switch (update.voted) {\n            case 1:\n                $elems.addClass('likes').removeClass('dislikes unvoted');\n                $up.removeClass(up_cls).addClass(upmod_cls);\n                $down.removeClass(downmod_cls).addClass(down_cls);\n            break;\n            case -1:\n                $elems.addClass('dislikes').removeClass('likes unvoted');\n                $up.removeClass(upmod_cls).addClass(up_cls);\n                $down.removeClass(down_cls).addClass(downmod_cls);\n            break;\n            default:\n                $elems.addClass('unvoted').removeClass('likes dislikes');\n                $up.removeClass(upmod_cls).addClass(up_cls);\n                $down.removeClass(downmod_cls).addClass(down_cls);\n        }\n    }\n\n    if ('saved' in update) {\n        $thing.addClass('saved');\n        $entry.find('.save-button a')\n              .text(r._('unsave'));\n    }\n}\n\n$.fn.resetInput = function() {\n  var $el = $(this);\n  $el.wrap('<form>').closest('form').get(0).reset();\n  $el.unwrap();\n\n  return this;\n};\n\n$.fn.show_unvotable_message = function() {\n  // deprecated\n};\n\n$.fn.thing = function() {\n    /* Returns the first thing that is a parent of the current element */\n    return this.parents(\".thing:first\");\n};\n\n$.fn.all_things_by_id = function() {\n    /* Returns the set of things that have the same ID as the current\n     * element's thing (we make no guarantee about uniqueness of\n     * things across multiple listings on the same page) */\n    return this.thing().add( $.things(this.thing_id()) );\n};\n\n$.fn.thing_id = function() {\n    /* Returns the (reddit) ID of the current element's thing */\n    var t = this.hasClass('thing') ? this : this.thing();\n\n    if (!t.length) {\n        return '';\n    }\n\n    var id = t.data('fullname');\n\n    if (id) {\n        return id;\n    }\n\n    // fallback to old, clunky way of getting id from class\n    id = $.grep(t.get(0).className.match(/\\S+/g),\n                function(i) { return i.match(/^id-/); }); \n    return (id.length) ? id[0].slice(3, id[0].length) : '';\n};\n\n$.things = function() {\n    /* \n     * accepts a list of thing_ids as the first argument and returns a\n     * jquery object consisting of the union of all things on the page\n     * that represent those things.\n     */\n    var sel = $.map(arguments, function(x) { return \".thing.id-\" + x; })\n       .join(\", \");\n    return $(sel);\n};\n\n$.fn.things = function() {\n    /* \n     * try to find all things that occur below a given selector, like:\n     * $('.organic-listing').things('t3_12345')\n     */\n    var sel = $.map(arguments, function(x) { return \".thing.id-\" + x; })\n       .join(\", \");\n    return this.find(sel);\n};\n\n$.listing = function(name) {\n    /* \n     * Given an element name (a sitetable ID or a thing ID, with\n     * optional siteTable_ at the front), return or generate a listing\n     * with the proper id for that name. \n     *\n     * In the case of a thing ID, this siteTable will be the listing\n     * in the child div of that thing's container.\n     * \n     * In the case of a general ID, it will be the listing of that\n     * name already present in the DOM.\n     *\n     * On failure, will return a JQuery object of zero length.\n     */\n    name = name || \"\";\n    var sitetable = \"siteTable\";\n    /* we'll add the hash specifier in later */\n    if (name.slice(0, 1) == \"#\" || name.slice(0, 1) == \".\")\n        name = name.slice(1, name.length);\n\n    /* lname should be the name of the actual listing (will always\n     * start with sitetable) while name should be the element it is\n     * named for (strip off sitetable if present) */\n    var lname = name;\n    if(name.slice(0, sitetable.length) != sitetable) \n        lname = sitetable + ( (name) ? (\"_\" + name): \"\");\n    else \n        name = name.slice(sitetable.length + 1, name.length);\n\n    var listing = $(\"#\" + lname).filter(\":first\");\n    /* did the $ lookup match anything? */\n    if (listing.length == 0) {\n        listing = $.things(name).find(\".child\")\n            .append(document.createElement('div'))\n            .children(\":last\")\n            .addClass(\"sitetable\")\n            .attr(\"id\", lname);\n    }\n    return listing;\n};\n\n\nvar thing_init_func = function() { };\n$.fn.set_thing_init = function(func) {\n    thing_init_func = func;\n    $(this).find(\".thing:not(.stub)\").each(function() { func(this) });\n};\n\n\n$.fn.new_thing_child = function(what, use_listing) {\n    var id = this.thing_id();\n    var where = (use_listing) ? $.listing(id) :\n        this.thing().find(\".child:first\");\n    \n    var new_form;\n    if (typeof(what) == \"string\") \n        new_form = where.prepend(what).children(\":first\");\n    else \n        new_form = what.hide()\n            .prependTo(where)\n            .show()\n            .find('input[name=\"parent\"]').val(id).end();\n    \n    return (new_form).randomize_ids();\n};\n\n$.fn.randomize_ids = function() {\n    var new_id = (Math.random() + \"\").split('.')[1]\n    $(this).find(\"*[id]\").each(function() {\n            $(this).attr('id', $(this).attr(\"id\") + new_id);\n        }).end()\n    .find(\"label\").each(function() {\n            $(this).attr('for', $(this).attr(\"for\") + new_id);\n        });\n    return $(this);\n}\n\n$.fn.replace_things = function(things, keep_children, reveal, stubs) {\n    /* Given the api-html structured things, insert them into the DOM\n     * in such a way as to remove any elements with the same thing_id.\n     * \"keep_children\" is a boolean to determine whether or not any\n     * existing child divs should be retained on the new thing (in the\n     * case of a comment tree, flags whether or not the new thing has\n     * the thread present) while \"reveal\" determines whether or not to\n     * animate the transition from old to new. */\n    var self = this,\n        map = $.map(things, function(thing) {\n            var data = thing.data;\n            var existing = $(self).things(data.id);\n            if(stubs) \n                existing = existing.filter(\".stub\");\n            if(existing.length == 0) {\n                var parent = $.things(data.parent);\n                if (parent.length) {\n                    existing = $(\"<div></div>\");\n                    parent.find(\".child:first\").append(existing);\n                }\n            }\n            existing.after($.unsafe(data.content));\n            var new_thing = existing.next();\n            if(keep_children) {\n                /* show the new thing */\n                new_thing.show()\n                    /* but hide its new content */\n                    .children(\".midcol, .entry\").hide().end()\n                    .children(\".child:first\")\n                    /* slop over the children */ \n                    .html(existing.children(\".child:first\")\n                          .remove().html())\n                    .end();\n                /* hide the old entry and show the new one */\n                if(reveal) {\n                    existing.hide();\n                    new_thing.children(\".midcol, .entry\").show();\n                }\n                new_thing.find(\".rank:first\")\n                    .html(existing.find(\".rank:first\").html());\n            }\n\n            /* hide and remove old. add in new */\n            if(reveal) {\n                existing.hide();\n                if(keep_children) \n                    new_thing.children(\".midcol, .entry\")\n                        .show();\n                else \n                    new_thing.show();\n                existing.remove();\n            }\n            else { \n                new_thing.hide();\n                existing.remove();\n             }\n\n            /* lastly, set the event handlers for these new things */\n            thing_init_func(new_thing);\n            $(document).trigger('new_thing', new_thing)\n            return new_thing;\n        });\n\n    $(document).trigger('new_things_inserted')\n    return map\n};\n\n\n$.insert_things = function(things, append) {\n    /* Insert new things into a listing.*/\n    var map = $.map(things, function(thing) {\n            var data = thing.data;\n            var s = $.listing(data.parent);\n            if(append)\n                s = s.append($.unsafe(data.content)).children(\".thing:last\");\n            else\n                s = s.prepend($.unsafe(data.content)).children(\".thing:first\");\n\n            thing_init_func(s.hide().show());\n            $(document).trigger('new_thing', s)\n            return s;\n        })\n    $(document).trigger('new_things_inserted')\n    return map\n};\n\n$.fn.delete_table_row = function(callback) {\n    var tr = this.parents(\"tr:first\").get(0);\n    var table = this.parents(\"table\").get(0);\n    if(tr) {\n        $(tr).fadeOut(function() {\n                table.deleteRow(tr.rowIndex);\n                if(callback) {\n                    callback();\n                }\n            });\n    } else if (callback) {\n        callback();\n    }\n};\n\n$.fn.insert_table_rows = function(rows, index) {\n    /* find the subset of the current selection that is a table, or\n     * the first parent of the current selection that is a table.*/\n    var tables = ((this.is(\"table\")) ? this.filter(\"table\") : \n                  this.parents(\"table:first\"));\n    $.map(tables.get(),\n          function(table) {\n              $.map(rows, function(row) {\n                      var i = index;\n                      if(i < 0)\n                          i = Math.max(table.rows.length + i + 1, 0);\n                      i = Math.min(i, table.rows.length);\n\n                      var $newRow = $(table.insertRow(i)),\n                          $toInsert = $($.parseHTML($.unsafe(row)))\n\n                      $toInsert.hide()\n                      $newRow.replaceWith($toInsert)\n                      $toInsert.trigger(\"insert-row\")\n                      $toInsert.css('display', 'table-row')\n                      $toInsert.fadeIn()\n                  });\n          });\n    return this;\n};\n\n\n$.fn.captcha = function(iden) {\n    /*  */\n    var c = this.find(\".capimage\");\n    if(iden) {\n        c.attr(\"src\", \"/captcha/\" + iden + \".png\")\n            .siblings('input[name=\"iden\"]').val(iden);\n    }\n    return c;\n};\n   \n\n/* Textarea handlers */\n$.fn.insertAtCursor = function(value) {\n    /* \"this\" refers to current jquery selection and may contain many\n     * non-textarea elements, so filter out and apply to each */\n    return $(this).filter(\"textarea\").each(function() {\n            /* this should be rebound to one of the elements in the orig list.*/\n            var textbox = $(this).get(0);\n            var orig_pos = textbox.scrollTop;\n        \n            if (document.selection) { /* IE */\n                textbox.focus();\n                var sel = document.selection.createRange();\n                sel.text = value;\n            }\n            else if (textbox.selectionStart) {\n                var prev_start = textbox.selectionStart;\n                textbox.value = \n                    textbox.value.substring(0, textbox.selectionStart) + \n                    value + \n                    textbox.value.substring(textbox.selectionEnd, \n                                            textbox.value.length);\n                prev_start += value.length;\n                textbox.setSelectionRange(prev_start, prev_start);\n            } else {\n                textbox.value += value;\n            }\n        \n            if(textbox.scrollHeight) {\n                textbox.scrollTop = orig_pos;\n            }\n        \n            $(this).focus();\n        })\n    .end();\n};\n\n$.fn.select_line = function(lineNo) {\n    return $(this).filter(\"textarea\").each(function() {\n            var newline = '\\n', newline_length = 1, caret_pos = 0;\n            var isIE = !!/msie [\\w.]+/.exec( navigator.userAgent.toLowerCase() );\n            if ( isIE ) { /* IE hack */\n                newline = '\\r';\n                newline_length = 0;\n                caret_pos = 1;\n            }\n            \n            var lines = $(this).val().split(newline);\n            \n            for(var x=0; x<lineNo-1; x++) \n                caret_pos += lines[x].length + newline_length;\n\n            var end_pos = caret_pos;\n            if (lineNo <= lines.length) \n                end_pos += lines[lineNo-1].length + newline_length;\n            \n            $(this).focus();\n            if(this.createTextRange) {   /* IE */\n                var start = this.createTextRange();\n                start.move('character', caret_pos);\n                var end = this.createTextRange();\n                end.move('character', end_pos);\n                start.setEndPoint(\"StartToEnd\", end);\n                start.select();\n            } else if (this.selectionStart) {\n                this.setSelectionRange(caret_pos, end_pos);\n            }\n            if(this.scrollHeight) {\n                var avgLineHight = this.scrollHeight / lines.length;\n                this.scrollTop = (lineNo-2) * avgLineHight;\n            }\n        });\n};\n\n\n$.apply_stylesheet = function(cssText) {\n    \n    var sheet_title = $(\"head\").children(\"link[title], style[title]\")\n        .filter(\":first\").attr(\"title\") || \"preferred stylesheet\";\n\n    if(document.styleSheets[0].cssText) {\n        /* of course IE has to do this differently from everyone else. */\n        var sheets = document.styleSheets;\n        for(var x=0; x < sheets.length; x++) \n            if(sheets[x].title == sheet_title) {\n                sheets[x].cssText = cssText;\n                break;\n            }\n    } else {\n        /* for everyone else, we walk <head> for the <link> or <style>\n         * that has the old stylesheet, and delete it. Then we add a\n         * <style> with the new one */\n        $(\"head\").children('*[title=\"' + sheet_title + '\"]').remove();\n\n        /* Hack to trigger a reflow so webkit browsers reset animations */\n        document.body.offsetHeight;\n\n        var stylesheet = $('<style type=\"text/css\" media=\"screen\"></style>')\n            .attr('title', sheet_title)\n            .text(cssText)\n            .appendTo('head')\n  }\n    \n};\n\n$.apply_stylesheet_url = function(cssUrl, srStyleEnabled) {\n  var sheetTitle = 'applied_subreddit_stylesheet';\n  var $stylesheet = $('link[title=\"' + sheetTitle + '\"]');\n  if ($stylesheet.length == 0) {\n    $('head').append('<link type=\"text/css\" title=\"' + sheetTitle + '\" rel=\"stylesheet\">');\n    $stylesheet = $('link[title=\"' + sheetTitle + '\"]');\n  }\n\n  $stylesheet.attr(\"href\", cssUrl);\n  $(\"#sr_style_enabled\").prop(\"checked\", srStyleEnabled);\n  $(\"#sr_style_throbber\")\n    .html(\"\")\n    .css(\"display\", \"none\");\n};\n\n$.apply_header_image = function(src, size, title) {\n  var $headerImage = $(\"#header-img\");\n  if ($headerImage.is(\"a\")) {\n    $headerImage\n      .attr(\"id\", \"header-img-a\")\n      .text(\"\")\n      .append('<img id=\"header-img\"/>');\n    $headerImage = $(\"#header-img\");\n  }\n  $headerImage.removeClass(\"default-header\");\n  $headerImage.attr(\"src\", src);\n  $headerImage.attr(\"title\", title);\n  if (size) {\n    $headerImage.attr(\"width\", size[0]);\n    $headerImage.attr(\"height\", size[1]);\n  } else {\n    $headerImage.removeAttr(\"width\");\n    $headerImage.removeAttr(\"height\");\n  }\n}\n\n$.remove_header_image = function() {\n  var $headerLink = $(\"#header-img-a\");\n\n  if ($headerLink) {\n    $headerLink\n      .addClass(\"default-header\")\n      .attr(\"id\", \"header-img\")\n      .empty();\n    $(\"#header-img\").empty();\n    $headerLink.attr(\"id\", \"header-img\");\n  }\n}\n\n/* namespace globals for cookies -- default prefix, security and domain */\nvar default_cookie_domain\n$.default_cookie_domain = function(domain) {\n    if (domain) {\n        default_cookie_domain = domain\n    }\n}\n\nvar default_cookie_security\n$.default_cookie_security = function(security) {\n    default_cookie_security = security\n}\n\nvar cookie_name_prefix = \"_\"\n$.cookie_name_prefix = function(name) {\n    if (name) {\n        cookie_name_prefix = name + \"_\"\n    }\n}\n\n/* old reddit-specific cookie functions */\n$.cookie_write = function(c) {\n    if (c.name) {\n        var options = {}\n        options.expires = c.expires\n        options.domain = c.domain || default_cookie_domain\n        options.path = c.path || '/'\n        options.secure = c.secure || default_cookie_security\n\n        var key = cookie_name_prefix + c.name,\n            value = c.data\n\n        if (value === null || value == '') {\n            value = null\n        } else if (typeof(value) != 'string') {\n            value = JSON.stringify(value)\n        }\n\n        $.cookie(key, value, options)\n    }\n}\n\n$.cookie_read = function(name, prefix) {\n    var prefixedName = (prefix || cookie_name_prefix) + name,\n        data = $.cookie(prefixedName)\n\n    try {\n        data = JSON.parse(data)\n    } catch(e) {}\n\n    return {name: name, data: data}\n}\n\n$.fn.highlight = function(text) {\n  if (!text) { return this; }\n\n  var escaped = $.websafe(text.trim()).replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n  var regex = new RegExp(\"\\\\b\" + escaped + \"\\\\b\", \"gi\");\n\n  return this.each(function() {\n    if (this.children.length) { return; }\n\n    this.innerHTML = this.innerHTML.replace(regex, function(matched) {\n      return \"<mark>\" + matched + \"</mark>\";\n    });\n  });\n};\n\n})(jQuery);\n"
  },
  {
    "path": "r2/r2/public/static/js/lib/backbone-1.0.0.js",
    "content": "//     Backbone.js 1.0.0\n\n//     (c) 2010-2013 Jeremy Ashkenas, DocumentCloud Inc.\n//     Backbone may be freely distributed under the MIT license.\n//     For all details and documentation:\n//     http://backbonejs.org\n\n(function(){\n\n  // Initial Setup\n  // -------------\n\n  // Save a reference to the global object (`window` in the browser, `exports`\n  // on the server).\n  var root = this;\n\n  // Save the previous value of the `Backbone` variable, so that it can be\n  // restored later on, if `noConflict` is used.\n  var previousBackbone = root.Backbone;\n\n  // Create local references to array methods we'll want to use later.\n  var array = [];\n  var push = array.push;\n  var slice = array.slice;\n  var splice = array.splice;\n\n  // The top-level namespace. All public Backbone classes and modules will\n  // be attached to this. Exported for both the browser and the server.\n  var Backbone;\n  if (typeof exports !== 'undefined') {\n    Backbone = exports;\n  } else {\n    Backbone = root.Backbone = {};\n  }\n\n  // Current version of the library. Keep in sync with `package.json`.\n  Backbone.VERSION = '1.0.0';\n\n  // Require Underscore, if we're on the server, and it's not already present.\n  var _ = root._;\n  if (!_ && (typeof require !== 'undefined')) _ = require('underscore');\n\n  // For Backbone's purposes, jQuery, Zepto, Ender, or My Library (kidding) owns\n  // the `$` variable.\n  Backbone.$ = root.jQuery || root.Zepto || root.ender || root.$;\n\n  // Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable\n  // to its previous owner. Returns a reference to this Backbone object.\n  Backbone.noConflict = function() {\n    root.Backbone = previousBackbone;\n    return this;\n  };\n\n  // Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option\n  // will fake `\"PUT\"` and `\"DELETE\"` requests via the `_method` parameter and\n  // set a `X-Http-Method-Override` header.\n  Backbone.emulateHTTP = false;\n\n  // Turn on `emulateJSON` to support legacy servers that can't deal with direct\n  // `application/json` requests ... will encode the body as\n  // `application/x-www-form-urlencoded` instead and will send the model in a\n  // form param named `model`.\n  Backbone.emulateJSON = false;\n\n  // Backbone.Events\n  // ---------------\n\n  // A module that can be mixed in to *any object* in order to provide it with\n  // custom events. You may bind with `on` or remove with `off` callback\n  // functions to an event; `trigger`-ing an event fires all callbacks in\n  // succession.\n  //\n  //     var object = {};\n  //     _.extend(object, Backbone.Events);\n  //     object.on('expand', function(){ alert('expanded'); });\n  //     object.trigger('expand');\n  //\n  var Events = Backbone.Events = {\n\n    // Bind an event to a `callback` function. Passing `\"all\"` will bind\n    // the callback to all events fired.\n    on: function(name, callback, context) {\n      if (!eventsApi(this, 'on', name, [callback, context]) || !callback) return this;\n      this._events || (this._events = {});\n      var events = this._events[name] || (this._events[name] = []);\n      events.push({callback: callback, context: context, ctx: context || this});\n      return this;\n    },\n\n    // Bind an event to only be triggered a single time. After the first time\n    // the callback is invoked, it will be removed.\n    once: function(name, callback, context) {\n      if (!eventsApi(this, 'once', name, [callback, context]) || !callback) return this;\n      var self = this;\n      var once = _.once(function() {\n        self.off(name, once);\n        callback.apply(this, arguments);\n      });\n      once._callback = callback;\n      return this.on(name, once, context);\n    },\n\n    // Remove one or many callbacks. If `context` is null, removes all\n    // callbacks with that function. If `callback` is null, removes all\n    // callbacks for the event. If `name` is null, removes all bound\n    // callbacks for all events.\n    off: function(name, callback, context) {\n      var retain, ev, events, names, i, l, j, k;\n      if (!this._events || !eventsApi(this, 'off', name, [callback, context])) return this;\n      if (!name && !callback && !context) {\n        this._events = {};\n        return this;\n      }\n\n      names = name ? [name] : _.keys(this._events);\n      for (i = 0, l = names.length; i < l; i++) {\n        name = names[i];\n        if (events = this._events[name]) {\n          this._events[name] = retain = [];\n          if (callback || context) {\n            for (j = 0, k = events.length; j < k; j++) {\n              ev = events[j];\n              if ((callback && callback !== ev.callback && callback !== ev.callback._callback) ||\n                  (context && context !== ev.context)) {\n                retain.push(ev);\n              }\n            }\n          }\n          if (!retain.length) delete this._events[name];\n        }\n      }\n\n      return this;\n    },\n\n    // Trigger one or many events, firing all bound callbacks. Callbacks are\n    // passed the same arguments as `trigger` is, apart from the event name\n    // (unless you're listening on `\"all\"`, which will cause your callback to\n    // receive the true name of the event as the first argument).\n    trigger: function(name) {\n      if (!this._events) return this;\n      var args = slice.call(arguments, 1);\n      if (!eventsApi(this, 'trigger', name, args)) return this;\n      var events = this._events[name];\n      var allEvents = this._events.all;\n      if (events) triggerEvents(events, args);\n      if (allEvents) triggerEvents(allEvents, arguments);\n      return this;\n    },\n\n    // Tell this object to stop listening to either specific events ... or\n    // to every object it's currently listening to.\n    stopListening: function(obj, name, callback) {\n      var listeners = this._listeners;\n      if (!listeners) return this;\n      var deleteListener = !name && !callback;\n      if (typeof name === 'object') callback = this;\n      if (obj) (listeners = {})[obj._listenerId] = obj;\n      for (var id in listeners) {\n        listeners[id].off(name, callback, this);\n        if (deleteListener) delete this._listeners[id];\n      }\n      return this;\n    }\n\n  };\n\n  // Regular expression used to split event strings.\n  var eventSplitter = /\\s+/;\n\n  // Implement fancy features of the Events API such as multiple event\n  // names `\"change blur\"` and jQuery-style event maps `{change: action}`\n  // in terms of the existing API.\n  var eventsApi = function(obj, action, name, rest) {\n    if (!name) return true;\n\n    // Handle event maps.\n    if (typeof name === 'object') {\n      for (var key in name) {\n        obj[action].apply(obj, [key, name[key]].concat(rest));\n      }\n      return false;\n    }\n\n    // Handle space separated event names.\n    if (eventSplitter.test(name)) {\n      var names = name.split(eventSplitter);\n      for (var i = 0, l = names.length; i < l; i++) {\n        obj[action].apply(obj, [names[i]].concat(rest));\n      }\n      return false;\n    }\n\n    return true;\n  };\n\n  // A difficult-to-believe, but optimized internal dispatch function for\n  // triggering events. Tries to keep the usual cases speedy (most internal\n  // Backbone events have 3 arguments).\n  var triggerEvents = function(events, args) {\n    var ev, i = -1, l = events.length, a1 = args[0], a2 = args[1], a3 = args[2];\n    switch (args.length) {\n      case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx); return;\n      case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1); return;\n      case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2); return;\n      case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2, a3); return;\n      default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args);\n    }\n  };\n\n  var listenMethods = {listenTo: 'on', listenToOnce: 'once'};\n\n  // Inversion-of-control versions of `on` and `once`. Tell *this* object to\n  // listen to an event in another object ... keeping track of what it's\n  // listening to.\n  _.each(listenMethods, function(implementation, method) {\n    Events[method] = function(obj, name, callback) {\n      var listeners = this._listeners || (this._listeners = {});\n      var id = obj._listenerId || (obj._listenerId = _.uniqueId('l'));\n      listeners[id] = obj;\n      if (typeof name === 'object') callback = this;\n      obj[implementation](name, callback, this);\n      return this;\n    };\n  });\n\n  // Aliases for backwards compatibility.\n  Events.bind   = Events.on;\n  Events.unbind = Events.off;\n\n  // Allow the `Backbone` object to serve as a global event bus, for folks who\n  // want global \"pubsub\" in a convenient place.\n  _.extend(Backbone, Events);\n\n  // Backbone.Model\n  // --------------\n\n  // Backbone **Models** are the basic data object in the framework --\n  // frequently representing a row in a table in a database on your server.\n  // A discrete chunk of data and a bunch of useful, related methods for\n  // performing computations and transformations on that data.\n\n  // Create a new model with the specified attributes. A client id (`cid`)\n  // is automatically generated and assigned for you.\n  var Model = Backbone.Model = function(attributes, options) {\n    var defaults;\n    var attrs = attributes || {};\n    options || (options = {});\n    this.cid = _.uniqueId('c');\n    this.attributes = {};\n    _.extend(this, _.pick(options, modelOptions));\n    if (options.parse) attrs = this.parse(attrs, options) || {};\n    if (defaults = _.result(this, 'defaults')) {\n      attrs = _.defaults({}, attrs, defaults);\n    }\n    this.set(attrs, options);\n    this.changed = {};\n    this.initialize.apply(this, arguments);\n  };\n\n  // A list of options to be attached directly to the model, if provided.\n  var modelOptions = ['url', 'urlRoot', 'collection'];\n\n  // Attach all inheritable methods to the Model prototype.\n  _.extend(Model.prototype, Events, {\n\n    // A hash of attributes whose current and previous value differ.\n    changed: null,\n\n    // The value returned during the last failed validation.\n    validationError: null,\n\n    // The default name for the JSON `id` attribute is `\"id\"`. MongoDB and\n    // CouchDB users may want to set this to `\"_id\"`.\n    idAttribute: 'id',\n\n    // Initialize is an empty function by default. Override it with your own\n    // initialization logic.\n    initialize: function(){},\n\n    // Return a copy of the model's `attributes` object.\n    toJSON: function(options) {\n      return _.clone(this.attributes);\n    },\n\n    // Proxy `Backbone.sync` by default -- but override this if you need\n    // custom syncing semantics for *this* particular model.\n    sync: function() {\n      return Backbone.sync.apply(this, arguments);\n    },\n\n    // Get the value of an attribute.\n    get: function(attr) {\n      return this.attributes[attr];\n    },\n\n    // Get the HTML-escaped value of an attribute.\n    escape: function(attr) {\n      return _.escape(this.get(attr));\n    },\n\n    // Returns `true` if the attribute contains a value that is not null\n    // or undefined.\n    has: function(attr) {\n      return this.get(attr) != null;\n    },\n\n    // Set a hash of model attributes on the object, firing `\"change\"`. This is\n    // the core primitive operation of a model, updating the data and notifying\n    // anyone who needs to know about the change in state. The heart of the beast.\n    set: function(key, val, options) {\n      var attr, attrs, unset, changes, silent, changing, prev, current;\n      if (key == null) return this;\n\n      // Handle both `\"key\", value` and `{key: value}` -style arguments.\n      if (typeof key === 'object') {\n        attrs = key;\n        options = val;\n      } else {\n        (attrs = {})[key] = val;\n      }\n\n      options || (options = {});\n\n      // Run validation.\n      if (!this._validate(attrs, options)) return false;\n\n      // Extract attributes and options.\n      unset           = options.unset;\n      silent          = options.silent;\n      changes         = [];\n      changing        = this._changing;\n      this._changing  = true;\n\n      if (!changing) {\n        this._previousAttributes = _.clone(this.attributes);\n        this.changed = {};\n      }\n      current = this.attributes, prev = this._previousAttributes;\n\n      // Check for changes of `id`.\n      if (this.idAttribute in attrs) this.id = attrs[this.idAttribute];\n\n      // For each `set` attribute, update or delete the current value.\n      for (attr in attrs) {\n        val = attrs[attr];\n        if (!_.isEqual(current[attr], val)) changes.push(attr);\n        if (!_.isEqual(prev[attr], val)) {\n          this.changed[attr] = val;\n        } else {\n          delete this.changed[attr];\n        }\n        unset ? delete current[attr] : current[attr] = val;\n      }\n\n      // Trigger all relevant attribute changes.\n      if (!silent) {\n        if (changes.length) this._pending = true;\n        for (var i = 0, l = changes.length; i < l; i++) {\n          this.trigger('change:' + changes[i], this, current[changes[i]], options);\n        }\n      }\n\n      // You might be wondering why there's a `while` loop here. Changes can\n      // be recursively nested within `\"change\"` events.\n      if (changing) return this;\n      if (!silent) {\n        while (this._pending) {\n          this._pending = false;\n          this.trigger('change', this, options);\n        }\n      }\n      this._pending = false;\n      this._changing = false;\n      return this;\n    },\n\n    // Remove an attribute from the model, firing `\"change\"`. `unset` is a noop\n    // if the attribute doesn't exist.\n    unset: function(attr, options) {\n      return this.set(attr, void 0, _.extend({}, options, {unset: true}));\n    },\n\n    // Clear all attributes on the model, firing `\"change\"`.\n    clear: function(options) {\n      var attrs = {};\n      for (var key in this.attributes) attrs[key] = void 0;\n      return this.set(attrs, _.extend({}, options, {unset: true}));\n    },\n\n    // Determine if the model has changed since the last `\"change\"` event.\n    // If you specify an attribute name, determine if that attribute has changed.\n    hasChanged: function(attr) {\n      if (attr == null) return !_.isEmpty(this.changed);\n      return _.has(this.changed, attr);\n    },\n\n    // Return an object containing all the attributes that have changed, or\n    // false if there are no changed attributes. Useful for determining what\n    // parts of a view need to be updated and/or what attributes need to be\n    // persisted to the server. Unset attributes will be set to undefined.\n    // You can also pass an attributes object to diff against the model,\n    // determining if there *would be* a change.\n    changedAttributes: function(diff) {\n      if (!diff) return this.hasChanged() ? _.clone(this.changed) : false;\n      var val, changed = false;\n      var old = this._changing ? this._previousAttributes : this.attributes;\n      for (var attr in diff) {\n        if (_.isEqual(old[attr], (val = diff[attr]))) continue;\n        (changed || (changed = {}))[attr] = val;\n      }\n      return changed;\n    },\n\n    // Get the previous value of an attribute, recorded at the time the last\n    // `\"change\"` event was fired.\n    previous: function(attr) {\n      if (attr == null || !this._previousAttributes) return null;\n      return this._previousAttributes[attr];\n    },\n\n    // Get all of the attributes of the model at the time of the previous\n    // `\"change\"` event.\n    previousAttributes: function() {\n      return _.clone(this._previousAttributes);\n    },\n\n    // Fetch the model from the server. If the server's representation of the\n    // model differs from its current attributes, they will be overridden,\n    // triggering a `\"change\"` event.\n    fetch: function(options) {\n      options = options ? _.clone(options) : {};\n      if (options.parse === void 0) options.parse = true;\n      var model = this;\n      var success = options.success;\n      options.success = function(resp) {\n        if (!model.set(model.parse(resp, options), options)) return false;\n        if (success) success(model, resp, options);\n        model.trigger('sync', model, resp, options);\n      };\n      wrapError(this, options);\n      return this.sync('read', this, options);\n    },\n\n    // Set a hash of model attributes, and sync the model to the server.\n    // If the server returns an attributes hash that differs, the model's\n    // state will be `set` again.\n    save: function(key, val, options) {\n      var attrs, method, xhr, attributes = this.attributes;\n\n      // Handle both `\"key\", value` and `{key: value}` -style arguments.\n      if (key == null || typeof key === 'object') {\n        attrs = key;\n        options = val;\n      } else {\n        (attrs = {})[key] = val;\n      }\n\n      // If we're not waiting and attributes exist, save acts as `set(attr).save(null, opts)`.\n      if (attrs && (!options || !options.wait) && !this.set(attrs, options)) return false;\n\n      options = _.extend({validate: true}, options);\n\n      // Do not persist invalid models.\n      if (!this._validate(attrs, options)) return false;\n\n      // Set temporary attributes if `{wait: true}`.\n      if (attrs && options.wait) {\n        this.attributes = _.extend({}, attributes, attrs);\n      }\n\n      // After a successful server-side save, the client is (optionally)\n      // updated with the server-side state.\n      if (options.parse === void 0) options.parse = true;\n      var model = this;\n      var success = options.success;\n      options.success = function(resp) {\n        // Ensure attributes are restored during synchronous saves.\n        model.attributes = attributes;\n        var serverAttrs = model.parse(resp, options);\n        if (options.wait) serverAttrs = _.extend(attrs || {}, serverAttrs);\n        if (_.isObject(serverAttrs) && !model.set(serverAttrs, options)) {\n          return false;\n        }\n        if (success) success(model, resp, options);\n        model.trigger('sync', model, resp, options);\n      };\n      wrapError(this, options);\n\n      method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update');\n      if (method === 'patch') options.attrs = attrs;\n      xhr = this.sync(method, this, options);\n\n      // Restore attributes.\n      if (attrs && options.wait) this.attributes = attributes;\n\n      return xhr;\n    },\n\n    // Destroy this model on the server if it was already persisted.\n    // Optimistically removes the model from its collection, if it has one.\n    // If `wait: true` is passed, waits for the server to respond before removal.\n    destroy: function(options) {\n      options = options ? _.clone(options) : {};\n      var model = this;\n      var success = options.success;\n\n      var destroy = function() {\n        model.trigger('destroy', model, model.collection, options);\n      };\n\n      options.success = function(resp) {\n        if (options.wait || model.isNew()) destroy();\n        if (success) success(model, resp, options);\n        if (!model.isNew()) model.trigger('sync', model, resp, options);\n      };\n\n      if (this.isNew()) {\n        options.success();\n        return false;\n      }\n      wrapError(this, options);\n\n      var xhr = this.sync('delete', this, options);\n      if (!options.wait) destroy();\n      return xhr;\n    },\n\n    // Default URL for the model's representation on the server -- if you're\n    // using Backbone's restful methods, override this to change the endpoint\n    // that will be called.\n    url: function() {\n      var base = _.result(this, 'urlRoot') || _.result(this.collection, 'url') || urlError();\n      if (this.isNew()) return base;\n      return base + (base.charAt(base.length - 1) === '/' ? '' : '/') + encodeURIComponent(this.id);\n    },\n\n    // **parse** converts a response into the hash of attributes to be `set` on\n    // the model. The default implementation is just to pass the response along.\n    parse: function(resp, options) {\n      return resp;\n    },\n\n    // Create a new model with identical attributes to this one.\n    clone: function() {\n      return new this.constructor(this.attributes);\n    },\n\n    // A model is new if it has never been saved to the server, and lacks an id.\n    isNew: function() {\n      return this.id == null;\n    },\n\n    // Check if the model is currently in a valid state.\n    isValid: function(options) {\n      return this._validate({}, _.extend(options || {}, { validate: true }));\n    },\n\n    // Run validation against the next complete set of model attributes,\n    // returning `true` if all is well. Otherwise, fire an `\"invalid\"` event.\n    _validate: function(attrs, options) {\n      if (!options.validate || !this.validate) return true;\n      attrs = _.extend({}, this.attributes, attrs);\n      var error = this.validationError = this.validate(attrs, options) || null;\n      if (!error) return true;\n      this.trigger('invalid', this, error, _.extend(options || {}, {validationError: error}));\n      return false;\n    }\n\n  });\n\n  // Underscore methods that we want to implement on the Model.\n  var modelMethods = ['keys', 'values', 'pairs', 'invert', 'pick', 'omit'];\n\n  // Mix in each Underscore method as a proxy to `Model#attributes`.\n  _.each(modelMethods, function(method) {\n    Model.prototype[method] = function() {\n      var args = slice.call(arguments);\n      args.unshift(this.attributes);\n      return _[method].apply(_, args);\n    };\n  });\n\n  // Backbone.Collection\n  // -------------------\n\n  // If models tend to represent a single row of data, a Backbone Collection is\n  // more analagous to a table full of data ... or a small slice or page of that\n  // table, or a collection of rows that belong together for a particular reason\n  // -- all of the messages in this particular folder, all of the documents\n  // belonging to this particular author, and so on. Collections maintain\n  // indexes of their models, both in order, and for lookup by `id`.\n\n  // Create a new **Collection**, perhaps to contain a specific type of `model`.\n  // If a `comparator` is specified, the Collection will maintain\n  // its models in sort order, as they're added and removed.\n  var Collection = Backbone.Collection = function(models, options) {\n    options || (options = {});\n    if (options.url) this.url = options.url;\n    if (options.model) this.model = options.model;\n    if (options.comparator !== void 0) this.comparator = options.comparator;\n    this._reset();\n    this.initialize.apply(this, arguments);\n    if (models) this.reset(models, _.extend({silent: true}, options));\n  };\n\n  // Default options for `Collection#set`.\n  var setOptions = {add: true, remove: true, merge: true};\n  var addOptions = {add: true, merge: false, remove: false};\n\n  // Define the Collection's inheritable methods.\n  _.extend(Collection.prototype, Events, {\n\n    // The default model for a collection is just a **Backbone.Model**.\n    // This should be overridden in most cases.\n    model: Model,\n\n    // Initialize is an empty function by default. Override it with your own\n    // initialization logic.\n    initialize: function(){},\n\n    // The JSON representation of a Collection is an array of the\n    // models' attributes.\n    toJSON: function(options) {\n      return this.map(function(model){ return model.toJSON(options); });\n    },\n\n    // Proxy `Backbone.sync` by default.\n    sync: function() {\n      return Backbone.sync.apply(this, arguments);\n    },\n\n    // Add a model, or list of models to the set.\n    add: function(models, options) {\n      return this.set(models, _.defaults(options || {}, addOptions));\n    },\n\n    // Remove a model, or a list of models from the set.\n    remove: function(models, options) {\n      models = _.isArray(models) ? models.slice() : [models];\n      options || (options = {});\n      var i, l, index, model;\n      for (i = 0, l = models.length; i < l; i++) {\n        model = this.get(models[i]);\n        if (!model) continue;\n        delete this._byId[model.id];\n        delete this._byId[model.cid];\n        index = this.indexOf(model);\n        this.models.splice(index, 1);\n        this.length--;\n        if (!options.silent) {\n          options.index = index;\n          model.trigger('remove', model, this, options);\n        }\n        this._removeReference(model);\n      }\n      return this;\n    },\n\n    // Update a collection by `set`-ing a new list of models, adding new ones,\n    // removing models that are no longer present, and merging models that\n    // already exist in the collection, as necessary. Similar to **Model#set**,\n    // the core operation for updating the data contained by the collection.\n    set: function(models, options) {\n      options = _.defaults(options || {}, setOptions);\n      if (options.parse) models = this.parse(models, options);\n      if (!_.isArray(models)) models = models ? [models] : [];\n      var i, l, model, attrs, existing, sort;\n      var at = options.at;\n      var sortable = this.comparator && (at == null) && options.sort !== false;\n      var sortAttr = _.isString(this.comparator) ? this.comparator : null;\n      var toAdd = [], toRemove = [], modelMap = {};\n\n      // Turn bare objects into model references, and prevent invalid models\n      // from being added.\n      for (i = 0, l = models.length; i < l; i++) {\n        if (!(model = this._prepareModel(models[i], options))) continue;\n\n        // If a duplicate is found, prevent it from being added and\n        // optionally merge it into the existing model.\n        if (existing = this.get(model)) {\n          if (options.remove) modelMap[existing.cid] = true;\n          if (options.merge) {\n            existing.set(model.attributes, options);\n            if (sortable && !sort && existing.hasChanged(sortAttr)) sort = true;\n          }\n\n        // This is a new model, push it to the `toAdd` list.\n        } else if (options.add) {\n          toAdd.push(model);\n\n          // Listen to added models' events, and index models for lookup by\n          // `id` and by `cid`.\n          model.on('all', this._onModelEvent, this);\n          this._byId[model.cid] = model;\n          if (model.id != null) this._byId[model.id] = model;\n        }\n      }\n\n      // Remove nonexistent models if appropriate.\n      if (options.remove) {\n        for (i = 0, l = this.length; i < l; ++i) {\n          if (!modelMap[(model = this.models[i]).cid]) toRemove.push(model);\n        }\n        if (toRemove.length) this.remove(toRemove, options);\n      }\n\n      // See if sorting is needed, update `length` and splice in new models.\n      if (toAdd.length) {\n        if (sortable) sort = true;\n        this.length += toAdd.length;\n        if (at != null) {\n          splice.apply(this.models, [at, 0].concat(toAdd));\n        } else {\n          push.apply(this.models, toAdd);\n        }\n      }\n\n      // Silently sort the collection if appropriate.\n      if (sort) this.sort({silent: true});\n\n      if (options.silent) return this;\n\n      // Trigger `add` events.\n      for (i = 0, l = toAdd.length; i < l; i++) {\n        (model = toAdd[i]).trigger('add', model, this, options);\n      }\n\n      // Trigger `sort` if the collection was sorted.\n      if (sort) this.trigger('sort', this, options);\n      return this;\n    },\n\n    // When you have more items than you want to add or remove individually,\n    // you can reset the entire set with a new list of models, without firing\n    // any granular `add` or `remove` events. Fires `reset` when finished.\n    // Useful for bulk operations and optimizations.\n    reset: function(models, options) {\n      options || (options = {});\n      for (var i = 0, l = this.models.length; i < l; i++) {\n        this._removeReference(this.models[i]);\n      }\n      options.previousModels = this.models;\n      this._reset();\n      this.add(models, _.extend({silent: true}, options));\n      if (!options.silent) this.trigger('reset', this, options);\n      return this;\n    },\n\n    // Add a model to the end of the collection.\n    push: function(model, options) {\n      model = this._prepareModel(model, options);\n      this.add(model, _.extend({at: this.length}, options));\n      return model;\n    },\n\n    // Remove a model from the end of the collection.\n    pop: function(options) {\n      var model = this.at(this.length - 1);\n      this.remove(model, options);\n      return model;\n    },\n\n    // Add a model to the beginning of the collection.\n    unshift: function(model, options) {\n      model = this._prepareModel(model, options);\n      this.add(model, _.extend({at: 0}, options));\n      return model;\n    },\n\n    // Remove a model from the beginning of the collection.\n    shift: function(options) {\n      var model = this.at(0);\n      this.remove(model, options);\n      return model;\n    },\n\n    // Slice out a sub-array of models from the collection.\n    slice: function(begin, end) {\n      return this.models.slice(begin, end);\n    },\n\n    // Get a model from the set by id.\n    get: function(obj) {\n      if (obj == null) return void 0;\n      return this._byId[obj.id != null ? obj.id : obj.cid || obj];\n    },\n\n    // Get the model at the given index.\n    at: function(index) {\n      return this.models[index];\n    },\n\n    // Return models with matching attributes. Useful for simple cases of\n    // `filter`.\n    where: function(attrs, first) {\n      if (_.isEmpty(attrs)) return first ? void 0 : [];\n      return this[first ? 'find' : 'filter'](function(model) {\n        for (var key in attrs) {\n          if (attrs[key] !== model.get(key)) return false;\n        }\n        return true;\n      });\n    },\n\n    // Return the first model with matching attributes. Useful for simple cases\n    // of `find`.\n    findWhere: function(attrs) {\n      return this.where(attrs, true);\n    },\n\n    // Force the collection to re-sort itself. You don't need to call this under\n    // normal circumstances, as the set will maintain sort order as each item\n    // is added.\n    sort: function(options) {\n      if (!this.comparator) throw new Error('Cannot sort a set without a comparator');\n      options || (options = {});\n\n      // Run sort based on type of `comparator`.\n      if (_.isString(this.comparator) || this.comparator.length === 1) {\n        this.models = this.sortBy(this.comparator, this);\n      } else {\n        this.models.sort(_.bind(this.comparator, this));\n      }\n\n      if (!options.silent) this.trigger('sort', this, options);\n      return this;\n    },\n\n    // Figure out the smallest index at which a model should be inserted so as\n    // to maintain order.\n    sortedIndex: function(model, value, context) {\n      value || (value = this.comparator);\n      var iterator = _.isFunction(value) ? value : function(model) {\n        return model.get(value);\n      };\n      return _.sortedIndex(this.models, model, iterator, context);\n    },\n\n    // Pluck an attribute from each model in the collection.\n    pluck: function(attr) {\n      return _.invoke(this.models, 'get', attr);\n    },\n\n    // Fetch the default set of models for this collection, resetting the\n    // collection when they arrive. If `reset: true` is passed, the response\n    // data will be passed through the `reset` method instead of `set`.\n    fetch: function(options) {\n      options = options ? _.clone(options) : {};\n      if (options.parse === void 0) options.parse = true;\n      var success = options.success;\n      var collection = this;\n      options.success = function(resp) {\n        var method = options.reset ? 'reset' : 'set';\n        collection[method](resp, options);\n        if (success) success(collection, resp, options);\n        collection.trigger('sync', collection, resp, options);\n      };\n      wrapError(this, options);\n      return this.sync('read', this, options);\n    },\n\n    // Create a new instance of a model in this collection. Add the model to the\n    // collection immediately, unless `wait: true` is passed, in which case we\n    // wait for the server to agree.\n    create: function(model, options) {\n      options = options ? _.clone(options) : {};\n      if (!(model = this._prepareModel(model, options))) return false;\n      if (!options.wait) this.add(model, options);\n      var collection = this;\n      var success = options.success;\n      options.success = function(resp) {\n        if (options.wait) collection.add(model, options);\n        if (success) success(model, resp, options);\n      };\n      model.save(null, options);\n      return model;\n    },\n\n    // **parse** converts a response into a list of models to be added to the\n    // collection. The default implementation is just to pass it through.\n    parse: function(resp, options) {\n      return resp;\n    },\n\n    // Create a new collection with an identical list of models as this one.\n    clone: function() {\n      return new this.constructor(this.models);\n    },\n\n    // Private method to reset all internal state. Called when the collection\n    // is first initialized or reset.\n    _reset: function() {\n      this.length = 0;\n      this.models = [];\n      this._byId  = {};\n    },\n\n    // Prepare a hash of attributes (or other model) to be added to this\n    // collection.\n    _prepareModel: function(attrs, options) {\n      if (attrs instanceof Model) {\n        if (!attrs.collection) attrs.collection = this;\n        return attrs;\n      }\n      options || (options = {});\n      options.collection = this;\n      var model = new this.model(attrs, options);\n      if (!model._validate(attrs, options)) {\n        this.trigger('invalid', this, attrs, options);\n        return false;\n      }\n      return model;\n    },\n\n    // Internal method to sever a model's ties to a collection.\n    _removeReference: function(model) {\n      if (this === model.collection) delete model.collection;\n      model.off('all', this._onModelEvent, this);\n    },\n\n    // Internal method called every time a model in the set fires an event.\n    // Sets need to update their indexes when models change ids. All other\n    // events simply proxy through. \"add\" and \"remove\" events that originate\n    // in other collections are ignored.\n    _onModelEvent: function(event, model, collection, options) {\n      if ((event === 'add' || event === 'remove') && collection !== this) return;\n      if (event === 'destroy') this.remove(model, options);\n      if (model && event === 'change:' + model.idAttribute) {\n        delete this._byId[model.previous(model.idAttribute)];\n        if (model.id != null) this._byId[model.id] = model;\n      }\n      this.trigger.apply(this, arguments);\n    }\n\n  });\n\n  // Underscore methods that we want to implement on the Collection.\n  // 90% of the core usefulness of Backbone Collections is actually implemented\n  // right here:\n  var methods = ['forEach', 'each', 'map', 'collect', 'reduce', 'foldl',\n    'inject', 'reduceRight', 'foldr', 'find', 'detect', 'filter', 'select',\n    'reject', 'every', 'all', 'some', 'any', 'include', 'contains', 'invoke',\n    'max', 'min', 'toArray', 'size', 'first', 'head', 'take', 'initial', 'rest',\n    'tail', 'drop', 'last', 'without', 'indexOf', 'shuffle', 'lastIndexOf',\n    'isEmpty', 'chain'];\n\n  // Mix in each Underscore method as a proxy to `Collection#models`.\n  _.each(methods, function(method) {\n    Collection.prototype[method] = function() {\n      var args = slice.call(arguments);\n      args.unshift(this.models);\n      return _[method].apply(_, args);\n    };\n  });\n\n  // Underscore methods that take a property name as an argument.\n  var attributeMethods = ['groupBy', 'countBy', 'sortBy'];\n\n  // Use attributes instead of properties.\n  _.each(attributeMethods, function(method) {\n    Collection.prototype[method] = function(value, context) {\n      var iterator = _.isFunction(value) ? value : function(model) {\n        return model.get(value);\n      };\n      return _[method](this.models, iterator, context);\n    };\n  });\n\n  // Backbone.View\n  // -------------\n\n  // Backbone Views are almost more convention than they are actual code. A View\n  // is simply a JavaScript object that represents a logical chunk of UI in the\n  // DOM. This might be a single item, an entire list, a sidebar or panel, or\n  // even the surrounding frame which wraps your whole app. Defining a chunk of\n  // UI as a **View** allows you to define your DOM events declaratively, without\n  // having to worry about render order ... and makes it easy for the view to\n  // react to specific changes in the state of your models.\n\n  // Creating a Backbone.View creates its initial element outside of the DOM,\n  // if an existing element is not provided...\n  var View = Backbone.View = function(options) {\n    this.cid = _.uniqueId('view');\n    this._configure(options || {});\n    this._ensureElement();\n    this.initialize.apply(this, arguments);\n    this.delegateEvents();\n  };\n\n  // Cached regex to split keys for `delegate`.\n  var delegateEventSplitter = /^(\\S+)\\s*(.*)$/;\n\n  // List of view options to be merged as properties.\n  var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events'];\n\n  // Set up all inheritable **Backbone.View** properties and methods.\n  _.extend(View.prototype, Events, {\n\n    // The default `tagName` of a View's element is `\"div\"`.\n    tagName: 'div',\n\n    // jQuery delegate for element lookup, scoped to DOM elements within the\n    // current view. This should be prefered to global lookups where possible.\n    $: function(selector) {\n      return this.$el.find(selector);\n    },\n\n    // Initialize is an empty function by default. Override it with your own\n    // initialization logic.\n    initialize: function(){},\n\n    // **render** is the core function that your view should override, in order\n    // to populate its element (`this.el`), with the appropriate HTML. The\n    // convention is for **render** to always return `this`.\n    render: function() {\n      return this;\n    },\n\n    // Remove this view by taking the element out of the DOM, and removing any\n    // applicable Backbone.Events listeners.\n    remove: function() {\n      this.$el.remove();\n      this.stopListening();\n      return this;\n    },\n\n    // Change the view's element (`this.el` property), including event\n    // re-delegation.\n    setElement: function(element, delegate) {\n      if (this.$el) this.undelegateEvents();\n      this.$el = element instanceof Backbone.$ ? element : Backbone.$(element);\n      this.el = this.$el[0];\n      if (delegate !== false) this.delegateEvents();\n      return this;\n    },\n\n    // Set callbacks, where `this.events` is a hash of\n    //\n    // *{\"event selector\": \"callback\"}*\n    //\n    //     {\n    //       'mousedown .title':  'edit',\n    //       'click .button':     'save'\n    //       'click .open':       function(e) { ... }\n    //     }\n    //\n    // pairs. Callbacks will be bound to the view, with `this` set properly.\n    // Uses event delegation for efficiency.\n    // Omitting the selector binds the event to `this.el`.\n    // This only works for delegate-able events: not `focus`, `blur`, and\n    // not `change`, `submit`, and `reset` in Internet Explorer.\n    delegateEvents: function(events) {\n      if (!(events || (events = _.result(this, 'events')))) return this;\n      this.undelegateEvents();\n      for (var key in events) {\n        var method = events[key];\n        if (!_.isFunction(method)) method = this[events[key]];\n        if (!method) continue;\n\n        var match = key.match(delegateEventSplitter);\n        var eventName = match[1], selector = match[2];\n        method = _.bind(method, this);\n        eventName += '.delegateEvents' + this.cid;\n        if (selector === '') {\n          this.$el.on(eventName, method);\n        } else {\n          this.$el.on(eventName, selector, method);\n        }\n      }\n      return this;\n    },\n\n    // Clears all callbacks previously bound to the view with `delegateEvents`.\n    // You usually don't need to use this, but may wish to if you have multiple\n    // Backbone views attached to the same DOM element.\n    undelegateEvents: function() {\n      this.$el.off('.delegateEvents' + this.cid);\n      return this;\n    },\n\n    // Performs the initial configuration of a View with a set of options.\n    // Keys with special meaning *(e.g. model, collection, id, className)* are\n    // attached directly to the view.  See `viewOptions` for an exhaustive\n    // list.\n    _configure: function(options) {\n      if (this.options) options = _.extend({}, _.result(this, 'options'), options);\n      _.extend(this, _.pick(options, viewOptions));\n      this.options = options;\n    },\n\n    // Ensure that the View has a DOM element to render into.\n    // If `this.el` is a string, pass it through `$()`, take the first\n    // matching element, and re-assign it to `el`. Otherwise, create\n    // an element from the `id`, `className` and `tagName` properties.\n    _ensureElement: function() {\n      if (!this.el) {\n        var attrs = _.extend({}, _.result(this, 'attributes'));\n        if (this.id) attrs.id = _.result(this, 'id');\n        if (this.className) attrs['class'] = _.result(this, 'className');\n        var $el = Backbone.$('<' + _.result(this, 'tagName') + '>').attr(attrs);\n        this.setElement($el, false);\n      } else {\n        this.setElement(_.result(this, 'el'), false);\n      }\n    }\n\n  });\n\n  // Backbone.sync\n  // -------------\n\n  // Override this function to change the manner in which Backbone persists\n  // models to the server. You will be passed the type of request, and the\n  // model in question. By default, makes a RESTful Ajax request\n  // to the model's `url()`. Some possible customizations could be:\n  //\n  // * Use `setTimeout` to batch rapid-fire updates into a single request.\n  // * Send up the models as XML instead of JSON.\n  // * Persist models via WebSockets instead of Ajax.\n  //\n  // Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests\n  // as `POST`, with a `_method` parameter containing the true HTTP method,\n  // as well as all requests with the body as `application/x-www-form-urlencoded`\n  // instead of `application/json` with the model in a param named `model`.\n  // Useful when interfacing with server-side languages like **PHP** that make\n  // it difficult to read the body of `PUT` requests.\n  Backbone.sync = function(method, model, options) {\n    var type = methodMap[method];\n\n    // Default options, unless specified.\n    _.defaults(options || (options = {}), {\n      emulateHTTP: Backbone.emulateHTTP,\n      emulateJSON: Backbone.emulateJSON\n    });\n\n    // Default JSON-request options.\n    var params = {type: type, dataType: 'json'};\n\n    // Ensure that we have a URL.\n    if (!options.url) {\n      params.url = _.result(model, 'url') || urlError();\n    }\n\n    // Ensure that we have the appropriate request data.\n    if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) {\n      params.contentType = 'application/json';\n      params.data = JSON.stringify(options.attrs || model.toJSON(options));\n    }\n\n    // For older servers, emulate JSON by encoding the request into an HTML-form.\n    if (options.emulateJSON) {\n      params.contentType = 'application/x-www-form-urlencoded';\n      params.data = params.data ? {model: params.data} : {};\n    }\n\n    // For older servers, emulate HTTP by mimicking the HTTP method with `_method`\n    // And an `X-HTTP-Method-Override` header.\n    if (options.emulateHTTP && (type === 'PUT' || type === 'DELETE' || type === 'PATCH')) {\n      params.type = 'POST';\n      if (options.emulateJSON) params.data._method = type;\n      var beforeSend = options.beforeSend;\n      options.beforeSend = function(xhr) {\n        xhr.setRequestHeader('X-HTTP-Method-Override', type);\n        if (beforeSend) return beforeSend.apply(this, arguments);\n      };\n    }\n\n    // Don't process data on a non-GET request.\n    if (params.type !== 'GET' && !options.emulateJSON) {\n      params.processData = false;\n    }\n\n    // If we're sending a `PATCH` request, and we're in an old Internet Explorer\n    // that still has ActiveX enabled by default, override jQuery to use that\n    // for XHR instead. Remove this line when jQuery supports `PATCH` on IE8.\n    if (params.type === 'PATCH' && window.ActiveXObject &&\n          !(window.external && window.external.msActiveXFilteringEnabled)) {\n      params.xhr = function() {\n        return new ActiveXObject(\"Microsoft.XMLHTTP\");\n      };\n    }\n\n    // Make the request, allowing the user to override any Ajax options.\n    var xhr = options.xhr = Backbone.ajax(_.extend(params, options));\n    model.trigger('request', model, xhr, options);\n    return xhr;\n  };\n\n  // Map from CRUD to HTTP for our default `Backbone.sync` implementation.\n  var methodMap = {\n    'create': 'POST',\n    'update': 'PUT',\n    'patch':  'PATCH',\n    'delete': 'DELETE',\n    'read':   'GET'\n  };\n\n  // Set the default implementation of `Backbone.ajax` to proxy through to `$`.\n  // Override this if you'd like to use a different library.\n  Backbone.ajax = function() {\n    return Backbone.$.ajax.apply(Backbone.$, arguments);\n  };\n\n  // Backbone.Router\n  // ---------------\n\n  // Routers map faux-URLs to actions, and fire events when routes are\n  // matched. Creating a new one sets its `routes` hash, if not set statically.\n  var Router = Backbone.Router = function(options) {\n    options || (options = {});\n    if (options.routes) this.routes = options.routes;\n    this._bindRoutes();\n    this.initialize.apply(this, arguments);\n  };\n\n  // Cached regular expressions for matching named param parts and splatted\n  // parts of route strings.\n  var optionalParam = /\\((.*?)\\)/g;\n  var namedParam    = /(\\(\\?)?:\\w+/g;\n  var splatParam    = /\\*\\w+/g;\n  var escapeRegExp  = /[\\-{}\\[\\]+?.,\\\\\\^$|#\\s]/g;\n\n  // Set up all inheritable **Backbone.Router** properties and methods.\n  _.extend(Router.prototype, Events, {\n\n    // Initialize is an empty function by default. Override it with your own\n    // initialization logic.\n    initialize: function(){},\n\n    // Manually bind a single named route to a callback. For example:\n    //\n    //     this.route('search/:query/p:num', 'search', function(query, num) {\n    //       ...\n    //     });\n    //\n    route: function(route, name, callback) {\n      if (!_.isRegExp(route)) route = this._routeToRegExp(route);\n      if (_.isFunction(name)) {\n        callback = name;\n        name = '';\n      }\n      if (!callback) callback = this[name];\n      var router = this;\n      Backbone.history.route(route, function(fragment) {\n        var args = router._extractParameters(route, fragment);\n        callback && callback.apply(router, args);\n        router.trigger.apply(router, ['route:' + name].concat(args));\n        router.trigger('route', name, args);\n        Backbone.history.trigger('route', router, name, args);\n      });\n      return this;\n    },\n\n    // Simple proxy to `Backbone.history` to save a fragment into the history.\n    navigate: function(fragment, options) {\n      Backbone.history.navigate(fragment, options);\n      return this;\n    },\n\n    // Bind all defined routes to `Backbone.history`. We have to reverse the\n    // order of the routes here to support behavior where the most general\n    // routes can be defined at the bottom of the route map.\n    _bindRoutes: function() {\n      if (!this.routes) return;\n      this.routes = _.result(this, 'routes');\n      var route, routes = _.keys(this.routes);\n      while ((route = routes.pop()) != null) {\n        this.route(route, this.routes[route]);\n      }\n    },\n\n    // Convert a route string into a regular expression, suitable for matching\n    // against the current location hash.\n    _routeToRegExp: function(route) {\n      route = route.replace(escapeRegExp, '\\\\$&')\n                   .replace(optionalParam, '(?:$1)?')\n                   .replace(namedParam, function(match, optional){\n                     return optional ? match : '([^\\/]+)';\n                   })\n                   .replace(splatParam, '(.*?)');\n      return new RegExp('^' + route + '$');\n    },\n\n    // Given a route, and a URL fragment that it matches, return the array of\n    // extracted decoded parameters. Empty or unmatched parameters will be\n    // treated as `null` to normalize cross-browser behavior.\n    _extractParameters: function(route, fragment) {\n      var params = route.exec(fragment).slice(1);\n      return _.map(params, function(param) {\n        return param ? decodeURIComponent(param) : null;\n      });\n    }\n\n  });\n\n  // Backbone.History\n  // ----------------\n\n  // Handles cross-browser history management, based on either\n  // [pushState](http://diveintohtml5.info/history.html) and real URLs, or\n  // [onhashchange](https://developer.mozilla.org/en-US/docs/DOM/window.onhashchange)\n  // and URL fragments. If the browser supports neither (old IE, natch),\n  // falls back to polling.\n  var History = Backbone.History = function() {\n    this.handlers = [];\n    _.bindAll(this, 'checkUrl');\n\n    // Ensure that `History` can be used outside of the browser.\n    if (typeof window !== 'undefined') {\n      this.location = window.location;\n      this.history = window.history;\n    }\n  };\n\n  // Cached regex for stripping a leading hash/slash and trailing space.\n  var routeStripper = /^[#\\/]|\\s+$/g;\n\n  // Cached regex for stripping leading and trailing slashes.\n  var rootStripper = /^\\/+|\\/+$/g;\n\n  // Cached regex for detecting MSIE.\n  var isExplorer = /msie [\\w.]+/;\n\n  // Cached regex for removing a trailing slash.\n  var trailingSlash = /\\/$/;\n\n  // Has the history handling already been started?\n  History.started = false;\n\n  // Set up all inheritable **Backbone.History** properties and methods.\n  _.extend(History.prototype, Events, {\n\n    // The default interval to poll for hash changes, if necessary, is\n    // twenty times a second.\n    interval: 50,\n\n    // Gets the true hash value. Cannot use location.hash directly due to bug\n    // in Firefox where location.hash will always be decoded.\n    getHash: function(window) {\n      var match = (window || this).location.href.match(/#(.*)$/);\n      return match ? match[1] : '';\n    },\n\n    // Get the cross-browser normalized URL fragment, either from the URL,\n    // the hash, or the override.\n    getFragment: function(fragment, forcePushState) {\n      if (fragment == null) {\n        if (this._hasPushState || !this._wantsHashChange || forcePushState) {\n          fragment = this.location.pathname;\n          var root = this.root.replace(trailingSlash, '');\n          if (!fragment.indexOf(root)) fragment = fragment.substr(root.length);\n        } else {\n          fragment = this.getHash();\n        }\n      }\n      return fragment.replace(routeStripper, '');\n    },\n\n    // Start the hash change handling, returning `true` if the current URL matches\n    // an existing route, and `false` otherwise.\n    start: function(options) {\n      if (History.started) throw new Error(\"Backbone.history has already been started\");\n      History.started = true;\n\n      // Figure out the initial configuration. Do we need an iframe?\n      // Is pushState desired ... is it available?\n      this.options          = _.extend({}, {root: '/'}, this.options, options);\n      this.root             = this.options.root;\n      this._wantsHashChange = this.options.hashChange !== false;\n      this._wantsPushState  = !!this.options.pushState;\n      this._hasPushState    = !!(this.options.pushState && this.history && this.history.pushState);\n      var fragment          = this.getFragment();\n      var docMode           = document.documentMode;\n      var oldIE             = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7));\n\n      // Normalize root to always include a leading and trailing slash.\n      this.root = ('/' + this.root + '/').replace(rootStripper, '/');\n\n      if (oldIE && this._wantsHashChange) {\n        this.iframe = Backbone.$('<iframe src=\"javascript:0\" tabindex=\"-1\" />').hide().appendTo('body')[0].contentWindow;\n        this.navigate(fragment);\n      }\n\n      // Depending on whether we're using pushState or hashes, and whether\n      // 'onhashchange' is supported, determine how we check the URL state.\n      if (this._hasPushState) {\n        Backbone.$(window).on('popstate', this.checkUrl);\n      } else if (this._wantsHashChange && ('onhashchange' in window) && !oldIE) {\n        Backbone.$(window).on('hashchange', this.checkUrl);\n      } else if (this._wantsHashChange) {\n        this._checkUrlInterval = setInterval(this.checkUrl, this.interval);\n      }\n\n      // Determine if we need to change the base url, for a pushState link\n      // opened by a non-pushState browser.\n      this.fragment = fragment;\n      var loc = this.location;\n      var atRoot = loc.pathname.replace(/[^\\/]$/, '$&/') === this.root;\n\n      // If we've started off with a route from a `pushState`-enabled browser,\n      // but we're currently in a browser that doesn't support it...\n      if (this._wantsHashChange && this._wantsPushState && !this._hasPushState && !atRoot) {\n        this.fragment = this.getFragment(null, true);\n        this.location.replace(this.root + this.location.search + '#' + this.fragment);\n        // Return immediately as browser will do redirect to new url\n        return true;\n\n      // Or if we've started out with a hash-based route, but we're currently\n      // in a browser where it could be `pushState`-based instead...\n      } else if (this._wantsPushState && this._hasPushState && atRoot && loc.hash) {\n        this.fragment = this.getHash().replace(routeStripper, '');\n        this.history.replaceState({}, document.title, this.root + this.fragment + loc.search);\n      }\n\n      if (!this.options.silent) return this.loadUrl();\n    },\n\n    // Disable Backbone.history, perhaps temporarily. Not useful in a real app,\n    // but possibly useful for unit testing Routers.\n    stop: function() {\n      Backbone.$(window).off('popstate', this.checkUrl).off('hashchange', this.checkUrl);\n      clearInterval(this._checkUrlInterval);\n      History.started = false;\n    },\n\n    // Add a route to be tested when the fragment changes. Routes added later\n    // may override previous routes.\n    route: function(route, callback) {\n      this.handlers.unshift({route: route, callback: callback});\n    },\n\n    // Checks the current URL to see if it has changed, and if it has,\n    // calls `loadUrl`, normalizing across the hidden iframe.\n    checkUrl: function(e) {\n      var current = this.getFragment();\n      if (current === this.fragment && this.iframe) {\n        current = this.getFragment(this.getHash(this.iframe));\n      }\n      if (current === this.fragment) return false;\n      if (this.iframe) this.navigate(current);\n      this.loadUrl() || this.loadUrl(this.getHash());\n    },\n\n    // Attempt to load the current URL fragment. If a route succeeds with a\n    // match, returns `true`. If no defined routes matches the fragment,\n    // returns `false`.\n    loadUrl: function(fragmentOverride) {\n      var fragment = this.fragment = this.getFragment(fragmentOverride);\n      var matched = _.any(this.handlers, function(handler) {\n        if (handler.route.test(fragment)) {\n          handler.callback(fragment);\n          return true;\n        }\n      });\n      return matched;\n    },\n\n    // Save a fragment into the hash history, or replace the URL state if the\n    // 'replace' option is passed. You are responsible for properly URL-encoding\n    // the fragment in advance.\n    //\n    // The options object can contain `trigger: true` if you wish to have the\n    // route callback be fired (not usually desirable), or `replace: true`, if\n    // you wish to modify the current URL without adding an entry to the history.\n    navigate: function(fragment, options) {\n      if (!History.started) return false;\n      if (!options || options === true) options = {trigger: options};\n      fragment = this.getFragment(fragment || '');\n      if (this.fragment === fragment) return;\n      this.fragment = fragment;\n      var url = this.root + fragment;\n\n      // If pushState is available, we use it to set the fragment as a real URL.\n      if (this._hasPushState) {\n        this.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, url);\n\n      // If hash changes haven't been explicitly disabled, update the hash\n      // fragment to store history.\n      } else if (this._wantsHashChange) {\n        this._updateHash(this.location, fragment, options.replace);\n        if (this.iframe && (fragment !== this.getFragment(this.getHash(this.iframe)))) {\n          // Opening and closing the iframe tricks IE7 and earlier to push a\n          // history entry on hash-tag change.  When replace is true, we don't\n          // want this.\n          if(!options.replace) this.iframe.document.open().close();\n          this._updateHash(this.iframe.location, fragment, options.replace);\n        }\n\n      // If you've told us that you explicitly don't want fallback hashchange-\n      // based history, then `navigate` becomes a page refresh.\n      } else {\n        return this.location.assign(url);\n      }\n      if (options.trigger) this.loadUrl(fragment);\n    },\n\n    // Update the hash location, either replacing the current entry, or adding\n    // a new one to the browser history.\n    _updateHash: function(location, fragment, replace) {\n      if (replace) {\n        var href = location.href.replace(/(javascript:|#).*$/, '');\n        location.replace(href + '#' + fragment);\n      } else {\n        // Some browsers require that `hash` contains a leading #.\n        location.hash = '#' + fragment;\n      }\n    }\n\n  });\n\n  // Create the default Backbone.history.\n  Backbone.history = new History;\n\n  // Helpers\n  // -------\n\n  // Helper function to correctly set up the prototype chain, for subclasses.\n  // Similar to `goog.inherits`, but uses a hash of prototype properties and\n  // class properties to be extended.\n  var extend = function(protoProps, staticProps) {\n    var parent = this;\n    var child;\n\n    // The constructor function for the new subclass is either defined by you\n    // (the \"constructor\" property in your `extend` definition), or defaulted\n    // by us to simply call the parent's constructor.\n    if (protoProps && _.has(protoProps, 'constructor')) {\n      child = protoProps.constructor;\n    } else {\n      child = function(){ return parent.apply(this, arguments); };\n    }\n\n    // Add static properties to the constructor function, if supplied.\n    _.extend(child, parent, staticProps);\n\n    // Set the prototype chain to inherit from `parent`, without calling\n    // `parent`'s constructor function.\n    var Surrogate = function(){ this.constructor = child; };\n    Surrogate.prototype = parent.prototype;\n    child.prototype = new Surrogate;\n\n    // Add prototype properties (instance properties) to the subclass,\n    // if supplied.\n    if (protoProps) _.extend(child.prototype, protoProps);\n\n    // Set a convenience property in case the parent's prototype is needed\n    // later.\n    child.__super__ = parent.prototype;\n\n    return child;\n  };\n\n  // Set up inheritance for the model, collection, router, view and history.\n  Model.extend = Collection.extend = Router.extend = View.extend = History.extend = extend;\n\n  // Throw an error when a URL is needed, and none is supplied.\n  var urlError = function() {\n    throw new Error('A \"url\" property or function must be specified');\n  };\n\n  // Wrap an optional error callback with a fallback error event.\n  var wrapError = function (model, options) {\n    var error = options.error;\n    options.error = function(resp) {\n      if (error) error(model, resp, options);\n      model.trigger('error', model, resp, options);\n    };\n  };\n\n}).call(this);\n"
  },
  {
    "path": "r2/r2/public/static/js/lib/bootstrap.modal.js",
    "content": "/* ========================================================================\n * Bootstrap: modal.js v3.2.0\n * http://getbootstrap.com/javascript/#modals\n * ========================================================================\n * Copyright 2011-2014 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * ======================================================================== */\n\n\n+function ($) {\n  'use strict';\n\n  // MODAL CLASS DEFINITION\n  // ======================\n\n  var Modal = function (element, options) {\n    this.options        = options\n    this.$body          = $(document.body)\n    this.$element       = $(element)\n    this.$backdrop      =\n    this.isShown        = null\n    this.scrollbarWidth = 0\n\n    if (this.options.remote) {\n      this.$element\n        .find('.modal-content')\n        .load(this.options.remote, $.proxy(function () {\n          this.$element.trigger('loaded.bs.modal')\n        }, this))\n    }\n  }\n\n  Modal.VERSION  = '3.2.0'\n\n  Modal.DEFAULTS = {\n    backdrop: true,\n    keyboard: true,\n    show: true\n  }\n\n  Modal.prototype.toggle = function (_relatedTarget) {\n    return this.isShown ? this.hide() : this.show(_relatedTarget)\n  }\n\n  Modal.prototype.show = function (_relatedTarget) {\n    var that = this\n    var e    = $.Event('show.bs.modal', { relatedTarget: _relatedTarget })\n\n    this.$element.trigger(e)\n\n    if (this.isShown || e.isDefaultPrevented()) return\n\n    this.isShown = true\n\n    this.checkScrollbar()\n    this.$body.addClass('modal-open')\n\n    this.setScrollbar()\n    this.escape()\n\n    this.$element.on('click.dismiss.bs.modal', '[data-dismiss=\"modal\"]', $.proxy(this.hide, this))\n\n    this.backdrop(function () {\n      var transition = $.support.transition && that.$element.hasClass('fade')\n\n      if (!that.$element.parent().length) {\n        that.$element.appendTo(that.$body) // don't move modals dom position\n      }\n\n      that.$element\n        .show()\n        .scrollTop(0)\n\n      if (transition) {\n        that.$element[0].offsetWidth // force reflow\n      }\n\n      that.$element\n        .addClass('in')\n        .attr('aria-hidden', false)\n\n      that.enforceFocus()\n\n      var e = $.Event('shown.bs.modal', { relatedTarget: _relatedTarget })\n\n      transition ?\n        that.$element.find('.modal-dialog') // wait for modal to slide in\n          .one('bsTransitionEnd', function () {\n            that.$element.trigger('focus').trigger(e)\n          })\n          .emulateTransitionEnd(300) :\n        that.$element.trigger('focus').trigger(e)\n    })\n  }\n\n  Modal.prototype.hide = function (e) {\n    if (e) e.preventDefault()\n\n    e = $.Event('hide.bs.modal')\n\n    this.$element.trigger(e)\n\n    if (!this.isShown || e.isDefaultPrevented()) return\n\n    this.isShown = false\n\n    this.$body.removeClass('modal-open')\n\n    this.resetScrollbar()\n    this.escape()\n\n    $(document).off('focusin.bs.modal')\n\n    this.$element\n      .removeClass('in')\n      .attr('aria-hidden', true)\n      .off('click.dismiss.bs.modal')\n\n    $.support.transition && this.$element.hasClass('fade') ?\n      this.$element\n        .one('bsTransitionEnd', $.proxy(this.hideModal, this))\n        .emulateTransitionEnd(300) :\n      this.hideModal()\n  }\n\n  Modal.prototype.enforceFocus = function () {\n    $(document)\n      .off('focusin.bs.modal') // guard against infinite focus loop\n      .on('focusin.bs.modal', $.proxy(function (e) {\n        if (this.$element[0] !== e.target && !this.$element.has(e.target).length) {\n          this.$element.trigger('focus')\n        }\n      }, this))\n  }\n\n  Modal.prototype.escape = function () {\n    if (this.isShown && this.options.keyboard) {\n      this.$element.on('keyup.dismiss.bs.modal', $.proxy(function (e) {\n        e.which == 27 && this.hide()\n      }, this))\n    } else if (!this.isShown) {\n      this.$element.off('keyup.dismiss.bs.modal')\n    }\n  }\n\n  Modal.prototype.hideModal = function () {\n    var that = this\n    this.$element.hide()\n    this.backdrop(function () {\n      that.$element.trigger('hidden.bs.modal')\n    })\n  }\n\n  Modal.prototype.removeBackdrop = function () {\n    this.$backdrop && this.$backdrop.remove()\n    this.$backdrop = null\n  }\n\n  Modal.prototype.backdrop = function (callback) {\n    var that = this\n    var animate = this.$element.hasClass('fade') ? 'fade' : ''\n\n    if (this.isShown && this.options.backdrop) {\n      var doAnimate = $.support.transition && animate\n\n      this.$backdrop = $('<div class=\"modal-backdrop ' + animate + '\" />')\n        .appendTo(this.$body)\n\n      this.$element.on('click.dismiss.bs.modal', $.proxy(function (e) {\n        if (e.target !== e.currentTarget) return\n        this.options.backdrop == 'static'\n          ? this.$element[0].focus.call(this.$element[0])\n          : this.hide.call(this)\n      }, this))\n\n      if (doAnimate) this.$backdrop[0].offsetWidth // force reflow\n\n      this.$backdrop.addClass('in')\n\n      if (!callback) return\n\n      doAnimate ?\n        this.$backdrop\n          .one('bsTransitionEnd', callback)\n          .emulateTransitionEnd(150) :\n        callback()\n\n    } else if (!this.isShown && this.$backdrop) {\n      this.$backdrop.removeClass('in')\n\n      var callbackRemove = function () {\n        that.removeBackdrop()\n        callback && callback()\n      }\n      $.support.transition && this.$element.hasClass('fade') ?\n        this.$backdrop\n          .one('bsTransitionEnd', callbackRemove)\n          .emulateTransitionEnd(150) :\n        callbackRemove()\n\n    } else if (callback) {\n      callback()\n    }\n  }\n\n  Modal.prototype.checkScrollbar = function () {\n    if (document.body.clientWidth >= window.innerWidth) return\n    this.scrollbarWidth = this.scrollbarWidth || this.measureScrollbar()\n  }\n\n  Modal.prototype.setScrollbar = function () {\n    var bodyPad = parseInt((this.$body.css('padding-right') || 0), 10)\n    if (this.scrollbarWidth) this.$body.css('padding-right', bodyPad + this.scrollbarWidth)\n  }\n\n  Modal.prototype.resetScrollbar = function () {\n    this.$body.css('padding-right', '')\n  }\n\n  Modal.prototype.measureScrollbar = function () { // thx walsh\n    var scrollDiv = document.createElement('div')\n    scrollDiv.className = 'modal-scrollbar-measure'\n    this.$body.append(scrollDiv)\n    var scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth\n    this.$body[0].removeChild(scrollDiv)\n    return scrollbarWidth\n  }\n\n\n  // MODAL PLUGIN DEFINITION\n  // =======================\n\n  function Plugin(option, _relatedTarget) {\n    return this.each(function () {\n      var $this   = $(this)\n      var data    = $this.data('bs.modal')\n      var options = $.extend({}, Modal.DEFAULTS, $this.data(), typeof option == 'object' && option)\n\n      if (!data) $this.data('bs.modal', (data = new Modal(this, options)))\n      if (typeof option == 'string') data[option](_relatedTarget)\n      else if (options.show) data.show(_relatedTarget)\n    })\n  }\n\n  var old = $.fn.modal\n\n  $.fn.modal             = Plugin\n  $.fn.modal.Constructor = Modal\n\n\n  // MODAL NO CONFLICT\n  // =================\n\n  $.fn.modal.noConflict = function () {\n    $.fn.modal = old\n    return this\n  }\n\n\n  // MODAL DATA-API\n  // ==============\n\n  $(document).on('click.bs.modal.data-api', '[data-toggle=\"modal\"]', function (e) {\n    var $this   = $(this)\n    var href    = $this.attr('href')\n    var $target = $($this.attr('data-target') || (href && href.replace(/.*(?=#[^\\s]+$)/, ''))) // strip for ie7\n    var option  = $target.data('bs.modal') ? 'toggle' : $.extend({ remote: !/#/.test(href) && href }, $target.data(), $this.data())\n\n    if ($this.is('a')) e.preventDefault()\n\n    $target.one('show.bs.modal', function (showEvent) {\n      if (showEvent.isDefaultPrevented()) return // only register focus restorer if modal will actually get shown\n      $target.one('hidden.bs.modal', function () {\n        $this.is(':visible') && $this.trigger('focus')\n      })\n    })\n    Plugin.call($target, option, this)\n  })\n\n}(jQuery);"
  },
  {
    "path": "r2/r2/public/static/js/lib/bootstrap.tooltip.js",
    "content": "/* ========================================================================\n * Bootstrap: tooltip.js v3.2.0\n * http://getbootstrap.com/javascript/#tooltip\n * Inspired by the original jQuery.tipsy by Jason Frame\n * ========================================================================\n * Copyright 2011-2014 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * ======================================================================== */\n\n\n+function ($) {\n  'use strict';\n\n  // TOOLTIP PUBLIC CLASS DEFINITION\n  // ===============================\n\n  var Tooltip = function (element, options) {\n    this.type       =\n    this.options    =\n    this.enabled    =\n    this.timeout    =\n    this.hoverState =\n    this.$element   = null\n\n    this.init('tooltip', element, options)\n  }\n\n  Tooltip.VERSION  = '3.2.0'\n\n  Tooltip.TRANSITION_DURATION = 150\n\n  Tooltip.DEFAULTS = {\n    animation: true,\n    placement: 'top',\n    selector: false,\n    template: '<div class=\"tooltip\" role=\"tooltip\"><div class=\"tooltip-arrow\"></div><div class=\"tooltip-inner\"></div></div>',\n    trigger: 'hover focus',\n    title: '',\n    delay: 0,\n    html: false,\n    container: false,\n    viewport: {\n      selector: 'body',\n      padding: 0\n    }\n  }\n\n  Tooltip.prototype.init = function (type, element, options) {\n    this.enabled   = true\n    this.type      = type\n    this.$element  = $(element)\n    this.options   = this.getOptions(options)\n    this.$viewport = this.options.viewport && $(this.options.viewport.selector || this.options.viewport)\n\n    var triggers = this.options.trigger.split(' ')\n\n    for (var i = triggers.length; i--;) {\n      var trigger = triggers[i]\n\n      if (trigger == 'click') {\n        this.$element.on('click.' + this.type, this.options.selector, $.proxy(this.toggle, this))\n      } else if (trigger != 'manual') {\n        var eventIn  = trigger == 'hover' ? 'mouseenter' : 'focusin'\n        var eventOut = trigger == 'hover' ? 'mouseleave' : 'focusout'\n\n        this.$element.on(eventIn  + '.' + this.type, this.options.selector, $.proxy(this.enter, this))\n        this.$element.on(eventOut + '.' + this.type, this.options.selector, $.proxy(this.leave, this))\n      }\n    }\n\n    this.options.selector ?\n      (this._options = $.extend({}, this.options, { trigger: 'manual', selector: '' })) :\n      this.fixTitle()\n  }\n\n  Tooltip.prototype.getDefaults = function () {\n    return Tooltip.DEFAULTS\n  }\n\n  Tooltip.prototype.getOptions = function (options) {\n    options = $.extend({}, this.getDefaults(), this.$element.data(), options)\n\n    if (options.delay && typeof options.delay == 'number') {\n      options.delay = {\n        show: options.delay,\n        hide: options.delay\n      }\n    }\n\n    return options\n  }\n\n  Tooltip.prototype.getDelegateOptions = function () {\n    var options  = {}\n    var defaults = this.getDefaults()\n\n    this._options && $.each(this._options, function (key, value) {\n      if (defaults[key] != value) options[key] = value\n    })\n\n    return options\n  }\n\n  Tooltip.prototype.enter = function (obj) {\n    var self = obj instanceof this.constructor ?\n      obj : $(obj.currentTarget).data('bs.' + this.type)\n\n    if (self && self.$tip && self.$tip.is(':visible')) {\n      self.hoverState = 'in'\n      return\n    }\n\n    if (!self) {\n      self = new this.constructor(obj.currentTarget, this.getDelegateOptions())\n      $(obj.currentTarget).data('bs.' + this.type, self)\n    }\n\n    clearTimeout(self.timeout)\n\n    self.hoverState = 'in'\n\n    if (!self.options.delay || !self.options.delay.show) return self.show()\n\n    self.timeout = setTimeout(function () {\n      if (self.hoverState == 'in') self.show()\n    }, self.options.delay.show)\n  }\n\n  Tooltip.prototype.leave = function (obj) {\n    var self = obj instanceof this.constructor ?\n      obj : $(obj.currentTarget).data('bs.' + this.type)\n\n    if (!self) {\n      self = new this.constructor(obj.currentTarget, this.getDelegateOptions())\n      $(obj.currentTarget).data('bs.' + this.type, self)\n    }\n\n    clearTimeout(self.timeout)\n\n    self.hoverState = 'out'\n\n    if (!self.options.delay || !self.options.delay.hide) return self.hide()\n\n    self.timeout = setTimeout(function () {\n      if (self.hoverState == 'out') self.hide()\n    }, self.options.delay.hide)\n  }\n\n  Tooltip.prototype.show = function () {\n    var e = $.Event('show.bs.' + this.type)\n\n    if (this.hasContent() && this.enabled) {\n      this.$element.trigger(e)\n\n      var inDom = $.contains(this.$element[0].ownerDocument.documentElement, this.$element[0])\n      if (e.isDefaultPrevented() || !inDom) return\n      var that = this\n\n      var $tip = this.tip()\n\n      var tipId = this.getUID(this.type)\n\n      this.setContent()\n      $tip.attr('id', tipId)\n      this.$element.attr('aria-describedby', tipId)\n\n      if (this.options.animation) $tip.addClass('fade')\n\n      var placement = typeof this.options.placement == 'function' ?\n        this.options.placement.call(this, $tip[0], this.$element[0]) :\n        this.options.placement\n\n      var autoToken = /\\s?auto?\\s?/i\n      var autoPlace = autoToken.test(placement)\n      if (autoPlace) placement = placement.replace(autoToken, '') || 'top'\n\n      $tip\n        .detach()\n        .css({ top: 0, left: 0, display: 'block' })\n        .addClass(placement)\n        .data('bs.' + this.type, this)\n\n      this.options.container ? $tip.appendTo(this.options.container) : $tip.insertAfter(this.$element)\n\n      var pos          = this.getPosition()\n      var actualWidth  = $tip[0].offsetWidth\n      var actualHeight = $tip[0].offsetHeight\n\n      if (autoPlace) {\n        var orgPlacement = placement\n        var $container   = this.options.container ? $(this.options.container) : this.$element.parent()\n        var containerDim = this.getPosition($container)\n\n        placement = placement == 'bottom' && pos.top   + pos.height          + actualHeight - containerDim.scroll > containerDim.height ? 'top'    :\n                    placement == 'top'    && pos.top   - containerDim.scroll - actualHeight < containerDim.top                          ? 'bottom' :\n                    placement == 'right'  && pos.right + actualWidth         > containerDim.width                                       ? 'left'   :\n                    placement == 'left'   && pos.left  - actualWidth         < containerDim.left                                        ? 'right'  :\n                    placement\n\n        $tip\n          .removeClass(orgPlacement)\n          .addClass(placement)\n      }\n\n      var calculatedOffset = this.getCalculatedOffset(placement, pos, actualWidth, actualHeight)\n\n      this.applyPlacement(calculatedOffset, placement)\n\n      var complete = function () {\n        that.$element.trigger('shown.bs.' + that.type)\n        that.hoverState = null\n      }\n\n      $.support.transition && this.$tip.hasClass('fade') ?\n        $tip\n          .one('bsTransitionEnd', complete)\n          .emulateTransitionEnd(Tooltip.TRANSITION_DURATION) :\n        complete()\n    }\n  }\n\n  Tooltip.prototype.applyPlacement = function (offset, placement) {\n    var $tip   = this.tip()\n    var width  = $tip[0].offsetWidth\n    var height = $tip[0].offsetHeight\n\n    // manually read margins because getBoundingClientRect includes difference\n    var marginTop = parseInt($tip.css('margin-top'), 10)\n    var marginLeft = parseInt($tip.css('margin-left'), 10)\n\n    // we must check for NaN for ie 8/9\n    if (isNaN(marginTop))  marginTop  = 0\n    if (isNaN(marginLeft)) marginLeft = 0\n\n    offset.top  = offset.top  + marginTop\n    offset.left = offset.left + marginLeft\n\n    // $.fn.offset doesn't round pixel values\n    // so we use setOffset directly with our own function B-0\n    $.offset.setOffset($tip[0], $.extend({\n      using: function (props) {\n        $tip.css({\n          top: Math.round(props.top),\n          left: Math.round(props.left)\n        })\n      }\n    }, offset), 0)\n\n    $tip.addClass('in')\n\n    // check to see if placing tip in new offset caused the tip to resize itself\n    var actualWidth  = $tip[0].offsetWidth\n    var actualHeight = $tip[0].offsetHeight\n\n    if (placement == 'top' && actualHeight != height) {\n      offset.top = offset.top + height - actualHeight\n    }\n\n    var delta = this.getViewportAdjustedDelta(placement, offset, actualWidth, actualHeight)\n\n    if (delta.left) offset.left += delta.left\n    else offset.top += delta.top\n\n    var isVertical          = /top|bottom/.test(placement)\n    var arrowDelta          = isVertical ? delta.left * 2 - width + actualWidth : delta.top * 2 - height + actualHeight\n    var arrowOffsetPosition = isVertical ? 'offsetWidth' : 'offsetHeight'\n\n    $tip.offset(offset)\n    this.replaceArrow(arrowDelta, $tip[0][arrowOffsetPosition], isVertical)\n  }\n\n  Tooltip.prototype.replaceArrow = function (delta, dimension, isHorizontal) {\n    this.arrow()\n      .css(isHorizontal ? 'left' : 'top', 50 * (1 - delta / dimension) + '%')\n      .css(isHorizontal ? 'top' : 'left', '')\n  }\n\n  Tooltip.prototype.setContent = function () {\n    var $tip  = this.tip()\n    var title = this.getTitle()\n\n    $tip.find('.tooltip-inner')[this.options.html ? 'html' : 'text'](title)\n    $tip.removeClass('fade in top bottom left right')\n  }\n\n  Tooltip.prototype.hide = function (callback) {\n    var that = this\n    var $tip = this.tip()\n    var e    = $.Event('hide.bs.' + this.type)\n\n    function complete() {\n      if (that.hoverState != 'in') $tip.detach()\n      that.$element\n        .removeAttr('aria-describedby')\n        .trigger('hidden.bs.' + that.type)\n      callback && callback()\n    }\n\n    this.$element.trigger(e)\n\n    if (e.isDefaultPrevented()) return\n\n    $tip.removeClass('in')\n\n    $.support.transition && this.$tip.hasClass('fade') ?\n      $tip\n        .one('bsTransitionEnd', complete)\n        .emulateTransitionEnd(Tooltip.TRANSITION_DURATION) :\n      complete()\n\n    this.hoverState = null\n\n    return this\n  }\n\n  Tooltip.prototype.fixTitle = function () {\n    var $e = this.$element\n    if ($e.attr('title') || typeof ($e.attr('data-original-title')) != 'string') {\n      $e.attr('data-original-title', $e.attr('title') || '').attr('title', '')\n    }\n  }\n\n  Tooltip.prototype.hasContent = function () {\n    return this.getTitle()\n  }\n\n  Tooltip.prototype.getPosition = function ($element) {\n    $element   = $element || this.$element\n\n    var el     = $element[0]\n    var isBody = el.tagName == 'BODY'\n\n    var elRect    = el.getBoundingClientRect()\n    if (elRect.width == null) {\n      // width and height are missing in IE8, so compute them manually; see https://github.com/twbs/bootstrap/issues/14093\n      elRect = $.extend({}, elRect, { width: elRect.right - elRect.left, height: elRect.bottom - elRect.top })\n    }\n    var elOffset  = isBody ? { top: 0, left: 0 } : $element.offset()\n    var scroll    = { scroll: isBody ? document.documentElement.scrollTop || document.body.scrollTop : $element.scrollTop() }\n    var outerDims = isBody ? { width: $(window).width(), height: $(window).height() } : null\n\n    return $.extend({}, elRect, scroll, outerDims, elOffset)\n  }\n\n  Tooltip.prototype.getCalculatedOffset = function (placement, pos, actualWidth, actualHeight) {\n    return placement == 'bottom' ? { top: pos.top + pos.height,   left: pos.left + pos.width / 2 - actualWidth / 2  } :\n           placement == 'top'    ? { top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2  } :\n           placement == 'left'   ? { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth } :\n        /* placement == 'right' */ { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width   }\n\n  }\n\n  Tooltip.prototype.getViewportAdjustedDelta = function (placement, pos, actualWidth, actualHeight) {\n    var delta = { top: 0, left: 0 }\n    if (!this.$viewport) return delta\n\n    var viewportPadding = this.options.viewport && this.options.viewport.padding || 0\n    var viewportDimensions = this.getPosition(this.$viewport)\n\n    if (/right|left/.test(placement)) {\n      var topEdgeOffset    = pos.top - viewportPadding - viewportDimensions.scroll\n      var bottomEdgeOffset = pos.top + viewportPadding - viewportDimensions.scroll + actualHeight\n      if (topEdgeOffset < viewportDimensions.top) { // top overflow\n        delta.top = viewportDimensions.top - topEdgeOffset\n      } else if (bottomEdgeOffset > viewportDimensions.top + viewportDimensions.height) { // bottom overflow\n        delta.top = viewportDimensions.top + viewportDimensions.height - bottomEdgeOffset\n      }\n    } else {\n      var leftEdgeOffset  = pos.left - viewportPadding\n      var rightEdgeOffset = pos.left + viewportPadding + actualWidth\n      if (leftEdgeOffset < viewportDimensions.left) { // left overflow\n        delta.left = viewportDimensions.left - leftEdgeOffset\n      } else if (rightEdgeOffset > viewportDimensions.width) { // right overflow\n        delta.left = viewportDimensions.left + viewportDimensions.width - rightEdgeOffset\n      }\n    }\n\n    return delta\n  }\n\n  Tooltip.prototype.getTitle = function () {\n    var title\n    var $e = this.$element\n    var o  = this.options\n\n    title = $e.attr('data-original-title')\n      || (typeof o.title == 'function' ? o.title.call($e[0]) :  o.title)\n\n    return title\n  }\n\n  Tooltip.prototype.getUID = function (prefix) {\n    do prefix += ~~(Math.random() * 1000000)\n    while (document.getElementById(prefix))\n    return prefix\n  }\n\n  Tooltip.prototype.tip = function () {\n    return (this.$tip = this.$tip || $(this.options.template))\n  }\n\n  Tooltip.prototype.arrow = function () {\n    return (this.$arrow = this.$arrow || this.tip().find('.tooltip-arrow'))\n  }\n\n  Tooltip.prototype.enable = function () {\n    this.enabled = true\n  }\n\n  Tooltip.prototype.disable = function () {\n    this.enabled = false\n  }\n\n  Tooltip.prototype.toggleEnabled = function () {\n    this.enabled = !this.enabled\n  }\n\n  Tooltip.prototype.toggle = function (e) {\n    var self = this\n    if (e) {\n      self = $(e.currentTarget).data('bs.' + this.type)\n      if (!self) {\n        self = new this.constructor(e.currentTarget, this.getDelegateOptions())\n        $(e.currentTarget).data('bs.' + this.type, self)\n      }\n    }\n\n    self.tip().hasClass('in') ? self.leave(self) : self.enter(self)\n  }\n\n  Tooltip.prototype.destroy = function () {\n    var that = this\n    clearTimeout(this.timeout)\n    this.hide(function () {\n      that.$element.off('.' + that.type).removeData('bs.' + that.type)\n    })\n  }\n\n\n  // TOOLTIP PLUGIN DEFINITION\n  // =========================\n\n  function Plugin(option) {\n    return this.each(function () {\n      var $this   = $(this)\n      var data    = $this.data('bs.tooltip')\n      var options = typeof option == 'object' && option\n\n      if (!data && option == 'destroy') return\n      if (!data) $this.data('bs.tooltip', (data = new Tooltip(this, options)))\n      if (typeof option == 'string') data[option]()\n    })\n  }\n\n  var old = $.fn.tooltip\n\n  $.fn.tooltip             = Plugin\n  $.fn.tooltip.Constructor = Tooltip\n\n\n  // TOOLTIP NO CONFLICT\n  // ===================\n\n  $.fn.tooltip.noConflict = function () {\n    $.fn.tooltip = old\n    return this\n  }\n\n}(jQuery);"
  },
  {
    "path": "r2/r2/public/static/js/lib/bootstrap.transition.js",
    "content": "/* ========================================================================\n * Bootstrap: transition.js v3.2.0\n * http://getbootstrap.com/javascript/#transitions\n * ========================================================================\n * Copyright 2011-2014 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * ======================================================================== */\n\n\n+function ($) {\n  'use strict';\n\n  // CSS TRANSITION SUPPORT (Shoutout: http://www.modernizr.com/)\n  // ============================================================\n\n  function transitionEnd() {\n    var el = document.createElement('bootstrap')\n\n    var transEndEventNames = {\n      WebkitTransition : 'webkitTransitionEnd',\n      MozTransition    : 'transitionend',\n      OTransition      : 'oTransitionEnd otransitionend',\n      transition       : 'transitionend'\n    }\n\n    for (var name in transEndEventNames) {\n      if (el.style[name] !== undefined) {\n        return { end: transEndEventNames[name] }\n      }\n    }\n\n    return false // explicit for ie8 (  ._.)\n  }\n\n  // http://blog.alexmaccaw.com/css-transitions\n  $.fn.emulateTransitionEnd = function (duration) {\n    var called = false\n    var $el = this\n    $(this).one('bsTransitionEnd', function () { called = true })\n    var callback = function () { if (!called) $($el).trigger($.support.transition.end) }\n    setTimeout(callback, duration)\n    return this\n  }\n\n  $(function () {\n    $.support.transition = transitionEnd()\n\n    if (!$.support.transition) return\n\n    $.event.special.bsTransitionEnd = {\n      bindType: $.support.transition.end,\n      delegateType: $.support.transition.end,\n      handle: function (e) {\n        if ($(e.target).is(this)) return e.handleObj.handler.apply(this, arguments)\n      }\n    }\n  })\n\n}(jQuery);"
  },
  {
    "path": "r2/r2/public/static/js/lib/es5-sham.js",
    "content": "/*!\n * https://github.com/es-shims/es5-shim\n * @license es5-shim Copyright 2009-2014 by contributors, MIT License\n * see https://github.com/es-shims/es5-shim/blob/master/LICENSE\n */\n\n// vim: ts=4 sts=4 sw=4 expandtab\n\n//Add semicolon to prevent IIFE from being passed as argument to concated code.\n;\n\n// UMD (Universal Module Definition)\n// see https://github.com/umdjs/umd/blob/master/returnExports.js\n(function (root, factory) {\n    'use strict';\n    /*global define, exports, module */\n    if (typeof define === 'function' && define.amd) {\n        // AMD. Register as an anonymous module.\n        define(factory);\n    } else if (typeof exports === 'object') {\n        // Node. Does not work with strict CommonJS, but\n        // only CommonJS-like enviroments that support module.exports,\n        // like Node.\n        module.exports = factory();\n    } else {\n        // Browser globals (root is window)\n        root.returnExports = factory();\n  }\n}(this, function () {\n\nvar call = Function.prototype.call;\nvar prototypeOfObject = Object.prototype;\nvar owns = call.bind(prototypeOfObject.hasOwnProperty);\n\n// If JS engine supports accessors creating shortcuts.\nvar defineGetter;\nvar defineSetter;\nvar lookupGetter;\nvar lookupSetter;\nvar supportsAccessors = owns(prototypeOfObject, '__defineGetter__');\nif (supportsAccessors) {\n    /*eslint-disable no-underscore-dangle */\n    defineGetter = call.bind(prototypeOfObject.__defineGetter__);\n    defineSetter = call.bind(prototypeOfObject.__defineSetter__);\n    lookupGetter = call.bind(prototypeOfObject.__lookupGetter__);\n    lookupSetter = call.bind(prototypeOfObject.__lookupSetter__);\n    /*eslint-enable no-underscore-dangle */\n}\n\n// ES5 15.2.3.2\n// http://es5.github.com/#x15.2.3.2\nif (!Object.getPrototypeOf) {\n    // https://github.com/es-shims/es5-shim/issues#issue/2\n    // http://ejohn.org/blog/objectgetprototypeof/\n    // recommended by fschaefer on github\n    //\n    // sure, and webreflection says ^_^\n    // ... this will nerever possibly return null\n    // ... Opera Mini breaks here with infinite loops\n    Object.getPrototypeOf = function getPrototypeOf(object) {\n        /*eslint-disable no-proto */\n        var proto = object.__proto__;\n        /*eslint-enable no-proto */\n        if (proto || proto === null) {\n            return proto;\n        } else if (object.constructor) {\n            return object.constructor.prototype;\n        } else {\n            return prototypeOfObject;\n        }\n    };\n}\n\n//ES5 15.2.3.3\n//http://es5.github.com/#x15.2.3.3\n\nfunction doesGetOwnPropertyDescriptorWork(object) {\n    try {\n        object.sentinel = 0;\n        return Object.getOwnPropertyDescriptor(object, 'sentinel').value === 0;\n    } catch (exception) {\n        // returns falsy\n    }\n}\n\n//check whether getOwnPropertyDescriptor works if it's given. Otherwise,\n//shim partially.\nif (Object.defineProperty) {\n    var getOwnPropertyDescriptorWorksOnObject = doesGetOwnPropertyDescriptorWork({});\n    var getOwnPropertyDescriptorWorksOnDom = typeof document === 'undefined' ||\n    doesGetOwnPropertyDescriptorWork(document.createElement('div'));\n    if (!getOwnPropertyDescriptorWorksOnDom || !getOwnPropertyDescriptorWorksOnObject) {\n        var getOwnPropertyDescriptorFallback = Object.getOwnPropertyDescriptor;\n    }\n}\n\nif (!Object.getOwnPropertyDescriptor || getOwnPropertyDescriptorFallback) {\n    var ERR_NON_OBJECT = 'Object.getOwnPropertyDescriptor called on a non-object: ';\n\n    /*eslint-disable no-proto */\n    Object.getOwnPropertyDescriptor = function getOwnPropertyDescriptor(object, property) {\n        if ((typeof object !== 'object' && typeof object !== 'function') || object === null) {\n            throw new TypeError(ERR_NON_OBJECT + object);\n        }\n\n        // make a valiant attempt to use the real getOwnPropertyDescriptor\n        // for I8's DOM elements.\n        if (getOwnPropertyDescriptorFallback) {\n            try {\n                return getOwnPropertyDescriptorFallback.call(Object, object, property);\n            } catch (exception) {\n                // try the shim if the real one doesn't work\n            }\n        }\n\n        var descriptor;\n\n        // If object does not owns property return undefined immediately.\n        if (!owns(object, property)) {\n            return descriptor;\n        }\n\n        // If object has a property then it's for sure both `enumerable` and\n        // `configurable`.\n        descriptor = { enumerable: true, configurable: true };\n\n        // If JS engine supports accessor properties then property may be a\n        // getter or setter.\n        if (supportsAccessors) {\n            // Unfortunately `__lookupGetter__` will return a getter even\n            // if object has own non getter property along with a same named\n            // inherited getter. To avoid misbehavior we temporary remove\n            // `__proto__` so that `__lookupGetter__` will return getter only\n            // if it's owned by an object.\n            var prototype = object.__proto__;\n            var notPrototypeOfObject = object !== prototypeOfObject;\n            // avoid recursion problem, breaking in Opera Mini when\n            // Object.getOwnPropertyDescriptor(Object.prototype, 'toString')\n            // or any other Object.prototype accessor\n            if (notPrototypeOfObject) {\n                object.__proto__ = prototypeOfObject;\n            }\n\n            var getter = lookupGetter(object, property);\n            var setter = lookupSetter(object, property);\n\n            if (notPrototypeOfObject) {\n                // Once we have getter and setter we can put values back.\n                object.__proto__ = prototype;\n            }\n\n            if (getter || setter) {\n                if (getter) {\n                    descriptor.get = getter;\n                }\n                if (setter) {\n                    descriptor.set = setter;\n                }\n                // If it was accessor property we're done and return here\n                // in order to avoid adding `value` to the descriptor.\n                return descriptor;\n            }\n        }\n\n        // If we got this far we know that object has an own property that is\n        // not an accessor so we set it as a value and return descriptor.\n        descriptor.value = object[property];\n        descriptor.writable = true;\n        return descriptor;\n    };\n    /*eslint-enable no-proto */\n}\n\n// ES5 15.2.3.4\n// http://es5.github.com/#x15.2.3.4\nif (!Object.getOwnPropertyNames) {\n    Object.getOwnPropertyNames = function getOwnPropertyNames(object) {\n        return Object.keys(object);\n    };\n}\n\n// ES5 15.2.3.5\n// http://es5.github.com/#x15.2.3.5\nif (!Object.create) {\n\n    // Contributed by Brandon Benvie, October, 2012\n    var createEmpty;\n    var supportsProto = !({ __proto__: null } instanceof Object);\n                        // the following produces false positives\n                        // in Opera Mini => not a reliable check\n                        // Object.prototype.__proto__ === null\n    /*global document */\n    if (supportsProto || typeof document === 'undefined') {\n        createEmpty = function () {\n            return { __proto__: null };\n        };\n    } else {\n        // In old IE __proto__ can't be used to manually set `null`, nor does\n        // any other method exist to make an object that inherits from nothing,\n        // aside from Object.prototype itself. Instead, create a new global\n        // object and *steal* its Object.prototype and strip it bare. This is\n        // used as the prototype to create nullary objects.\n        createEmpty = function () {\n            var iframe = document.createElement('iframe');\n            var parent = document.body || document.documentElement;\n            iframe.style.display = 'none';\n            parent.appendChild(iframe);\n            /*eslint-disable no-script-url */\n            iframe.src = 'javascript:';\n            /*eslint-enable no-script-url */\n            var empty = iframe.contentWindow.Object.prototype;\n            parent.removeChild(iframe);\n            iframe = null;\n            delete empty.constructor;\n            delete empty.hasOwnProperty;\n            delete empty.propertyIsEnumerable;\n            delete empty.isPrototypeOf;\n            delete empty.toLocaleString;\n            delete empty.toString;\n            delete empty.valueOf;\n            /*eslint-disable no-proto */\n            empty.__proto__ = null;\n            /*eslint-enable no-proto */\n\n            function Empty() {}\n            Empty.prototype = empty;\n            // short-circuit future calls\n            createEmpty = function () {\n                return new Empty();\n            };\n            return new Empty();\n        };\n    }\n\n    Object.create = function create(prototype, properties) {\n\n        var object;\n        function Type() {}  // An empty constructor.\n\n        if (prototype === null) {\n            object = createEmpty();\n        } else {\n            if (typeof prototype !== 'object' && typeof prototype !== 'function') {\n                // In the native implementation `parent` can be `null`\n                // OR *any* `instanceof Object`  (Object|Function|Array|RegExp|etc)\n                // Use `typeof` tho, b/c in old IE, DOM elements are not `instanceof Object`\n                // like they are in modern browsers. Using `Object.create` on DOM elements\n                // is...err...probably inappropriate, but the native version allows for it.\n                throw new TypeError('Object prototype may only be an Object or null'); // same msg as Chrome\n            }\n            Type.prototype = prototype;\n            object = new Type();\n            // IE has no built-in implementation of `Object.getPrototypeOf`\n            // neither `__proto__`, but this manually setting `__proto__` will\n            // guarantee that `Object.getPrototypeOf` will work as expected with\n            // objects created using `Object.create`\n            /*eslint-disable no-proto */\n            object.__proto__ = prototype;\n            /*eslint-enable no-proto */\n        }\n\n        if (properties !== void 0) {\n            Object.defineProperties(object, properties);\n        }\n\n        return object;\n    };\n}\n\n// ES5 15.2.3.6\n// http://es5.github.com/#x15.2.3.6\n\n// Patch for WebKit and IE8 standard mode\n// Designed by hax <hax.github.com>\n// related issue: https://github.com/es-shims/es5-shim/issues#issue/5\n// IE8 Reference:\n//     http://msdn.microsoft.com/en-us/library/dd282900.aspx\n//     http://msdn.microsoft.com/en-us/library/dd229916.aspx\n// WebKit Bugs:\n//     https://bugs.webkit.org/show_bug.cgi?id=36423\n\nfunction doesDefinePropertyWork(object) {\n    try {\n        Object.defineProperty(object, 'sentinel', {});\n        return 'sentinel' in object;\n    } catch (exception) {\n        // returns falsy\n    }\n}\n\n// check whether defineProperty works if it's given. Otherwise,\n// shim partially.\nif (Object.defineProperty) {\n    var definePropertyWorksOnObject = doesDefinePropertyWork({});\n    var definePropertyWorksOnDom = typeof document === 'undefined' ||\n        doesDefinePropertyWork(document.createElement('div'));\n    if (!definePropertyWorksOnObject || !definePropertyWorksOnDom) {\n        var definePropertyFallback = Object.defineProperty,\n            definePropertiesFallback = Object.defineProperties;\n    }\n}\n\nif (!Object.defineProperty || definePropertyFallback) {\n    var ERR_NON_OBJECT_DESCRIPTOR = 'Property description must be an object: ';\n    var ERR_NON_OBJECT_TARGET = 'Object.defineProperty called on non-object: ';\n    var ERR_ACCESSORS_NOT_SUPPORTED = 'getters & setters can not be defined on this javascript engine';\n\n    Object.defineProperty = function defineProperty(object, property, descriptor) {\n        if ((typeof object !== 'object' && typeof object !== 'function') || object === null) {\n            throw new TypeError(ERR_NON_OBJECT_TARGET + object);\n        }\n        if ((typeof descriptor !== 'object' && typeof descriptor !== 'function') || descriptor === null) {\n            throw new TypeError(ERR_NON_OBJECT_DESCRIPTOR + descriptor);\n        }\n        // make a valiant attempt to use the real defineProperty\n        // for I8's DOM elements.\n        if (definePropertyFallback) {\n            try {\n                return definePropertyFallback.call(Object, object, property, descriptor);\n            } catch (exception) {\n                // try the shim if the real one doesn't work\n            }\n        }\n\n        // If it's a data property.\n        if ('value' in descriptor) {\n            // fail silently if 'writable', 'enumerable', or 'configurable'\n            // are requested but not supported\n            /*\n            // alternate approach:\n            if ( // can't implement these features; allow false but not true\n                ('writable' in descriptor && !descriptor.writable) ||\n                ('enumerable' in descriptor && !descriptor.enumerable) ||\n                ('configurable' in descriptor && !descriptor.configurable)\n            ))\n                throw new RangeError(\n                    'This implementation of Object.defineProperty does not support configurable, enumerable, or writable.'\n                );\n            */\n\n            if (supportsAccessors && (lookupGetter(object, property) || lookupSetter(object, property))) {\n                // As accessors are supported only on engines implementing\n                // `__proto__` we can safely override `__proto__` while defining\n                // a property to make sure that we don't hit an inherited\n                // accessor.\n                /*eslint-disable no-proto */\n                var prototype = object.__proto__;\n                object.__proto__ = prototypeOfObject;\n                // Deleting a property anyway since getter / setter may be\n                // defined on object itself.\n                delete object[property];\n                object[property] = descriptor.value;\n                // Setting original `__proto__` back now.\n                object.__proto__ = prototype;\n                /*eslint-enable no-proto */\n            } else {\n                object[property] = descriptor.value;\n            }\n        } else {\n            if (!supportsAccessors) {\n                throw new TypeError(ERR_ACCESSORS_NOT_SUPPORTED);\n            }\n            // If we got that far then getters and setters can be defined !!\n            if ('get' in descriptor) {\n                defineGetter(object, property, descriptor.get);\n            }\n            if ('set' in descriptor) {\n                defineSetter(object, property, descriptor.set);\n            }\n        }\n        return object;\n    };\n}\n\n// ES5 15.2.3.7\n// http://es5.github.com/#x15.2.3.7\nif (!Object.defineProperties || definePropertiesFallback) {\n    Object.defineProperties = function defineProperties(object, properties) {\n        // make a valiant attempt to use the real defineProperties\n        if (definePropertiesFallback) {\n            try {\n                return definePropertiesFallback.call(Object, object, properties);\n            } catch (exception) {\n                // try the shim if the real one doesn't work\n            }\n        }\n\n        for (var property in properties) {\n            if (owns(properties, property) && property !== '__proto__') {\n                Object.defineProperty(object, property, properties[property]);\n            }\n        }\n        return object;\n    };\n}\n\n// ES5 15.2.3.8\n// http://es5.github.com/#x15.2.3.8\nif (!Object.seal) {\n    Object.seal = function seal(object) {\n        if (Object(object) !== object) {\n            throw new TypeError('Object.seal can only be called on Objects.');\n        }\n        // this is misleading and breaks feature-detection, but\n        // allows \"securable\" code to \"gracefully\" degrade to working\n        // but insecure code.\n        return object;\n    };\n}\n\n// ES5 15.2.3.9\n// http://es5.github.com/#x15.2.3.9\nif (!Object.freeze) {\n    Object.freeze = function freeze(object) {\n        if (Object(object) !== object) {\n            throw new TypeError('Object.freeze can only be called on Objects.');\n        }\n        // this is misleading and breaks feature-detection, but\n        // allows \"securable\" code to \"gracefully\" degrade to working\n        // but insecure code.\n        return object;\n    };\n}\n\n// detect a Rhino bug and patch it\ntry {\n    Object.freeze(function () {});\n} catch (exception) {\n    Object.freeze = (function freeze(freezeObject) {\n        return function freeze(object) {\n            if (typeof object === 'function') {\n                return object;\n            } else {\n                return freezeObject(object);\n            }\n        };\n    }(Object.freeze));\n}\n\n// ES5 15.2.3.10\n// http://es5.github.com/#x15.2.3.10\nif (!Object.preventExtensions) {\n    Object.preventExtensions = function preventExtensions(object) {\n        if (Object(object) !== object) {\n            throw new TypeError('Object.preventExtensions can only be called on Objects.');\n        }\n        // this is misleading and breaks feature-detection, but\n        // allows \"securable\" code to \"gracefully\" degrade to working\n        // but insecure code.\n        return object;\n    };\n}\n\n// ES5 15.2.3.11\n// http://es5.github.com/#x15.2.3.11\nif (!Object.isSealed) {\n    Object.isSealed = function isSealed(object) {\n        if (Object(object) !== object) {\n            throw new TypeError('Object.isSealed can only be called on Objects.');\n        }\n        return false;\n    };\n}\n\n// ES5 15.2.3.12\n// http://es5.github.com/#x15.2.3.12\nif (!Object.isFrozen) {\n    Object.isFrozen = function isFrozen(object) {\n        if (Object(object) !== object) {\n            throw new TypeError('Object.isFrozen can only be called on Objects.');\n        }\n        return false;\n    };\n}\n\n// ES5 15.2.3.13\n// http://es5.github.com/#x15.2.3.13\nif (!Object.isExtensible) {\n    Object.isExtensible = function isExtensible(object) {\n        // 1. If Type(O) is not Object throw a TypeError exception.\n        if (Object(object) !== object) {\n            throw new TypeError('Object.isExtensible can only be called on Objects.');\n        }\n        // 2. Return the Boolean value of the [[Extensible]] internal property of O.\n        var name = '';\n        while (owns(object, name)) {\n            name += '?';\n        }\n        object[name] = true;\n        var returnValue = owns(object, name);\n        delete object[name];\n        return returnValue;\n    };\n}\n\n}));"
  },
  {
    "path": "r2/r2/public/static/js/lib/es5-shim.js",
    "content": "/*!\n * https://github.com/es-shims/es5-shim\n * @license es5-shim Copyright 2009-2014 by contributors, MIT License\n * see https://github.com/es-shims/es5-shim/blob/master/LICENSE\n */\n\n// vim: ts=4 sts=4 sw=4 expandtab\n\n//Add semicolon to prevent IIFE from being passed as argument to concated code.\n;\n\n// UMD (Universal Module Definition)\n// see https://github.com/umdjs/umd/blob/master/returnExports.js\n(function (root, factory) {\n    if (typeof define === 'function' && define.amd) {\n        // AMD. Register as an anonymous module.\n        define(factory);\n    } else if (typeof exports === 'object') {\n        // Node. Does not work with strict CommonJS, but\n        // only CommonJS-like enviroments that support module.exports,\n        // like Node.\n        module.exports = factory();\n    } else {\n        // Browser globals (root is window)\n        root.returnExports = factory();\n    }\n}(this, function () {\n\n/**\n * Brings an environment as close to ECMAScript 5 compliance\n * as is possible with the facilities of erstwhile engines.\n *\n * Annotated ES5: http://es5.github.com/ (specific links below)\n * ES5 Spec: http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-262.pdf\n * Required reading: http://javascriptweblog.wordpress.com/2011/12/05/extending-javascript-natives/\n */\n\n// Shortcut to an often accessed properties, in order to avoid multiple\n// dereference that costs universally.\nvar call = Function.prototype.call;\nvar prototypeOfArray = Array.prototype;\nvar prototypeOfObject = Object.prototype;\nvar _Array_slice_ = prototypeOfArray.slice;\nvar array_splice = Array.prototype.splice;\nvar array_push = Array.prototype.push;\nvar array_unshift = Array.prototype.unshift;\n\n// Having a toString local variable name breaks in Opera so use _toString.\nvar _toString = prototypeOfObject.toString;\n\nvar isFunction = function (val) {\n    return prototypeOfObject.toString.call(val) === '[object Function]';\n};\nvar isRegex = function (val) {\n    return prototypeOfObject.toString.call(val) === '[object RegExp]';\n};\nvar isArray = function isArray(obj) {\n    return _toString.call(obj) === \"[object Array]\";\n};\nvar isArguments = function isArguments(value) {\n    var str = _toString.call(value);\n    var isArgs = str === '[object Arguments]';\n    if (!isArgs) {\n        isArgs = !isArray(str)\n            && value !== null\n            && typeof value === 'object'\n            && typeof value.length === 'number'\n            && value.length >= 0\n            && isFunction(value.callee);\n    }\n    return isArgs;\n};\n\n//\n// Function\n// ========\n//\n\n// ES-5 15.3.4.5\n// http://es5.github.com/#x15.3.4.5\n\nfunction Empty() {}\n\nif (!Function.prototype.bind) {\n    Function.prototype.bind = function bind(that) { // .length is 1\n        // 1. Let Target be the this value.\n        var target = this;\n        // 2. If IsCallable(Target) is false, throw a TypeError exception.\n        if (!isFunction(target)) {\n            throw new TypeError(\"Function.prototype.bind called on incompatible \" + target);\n        }\n        // 3. Let A be a new (possibly empty) internal list of all of the\n        //   argument values provided after thisArg (arg1, arg2 etc), in order.\n        // XXX slicedArgs will stand in for \"A\" if used\n        var args = _Array_slice_.call(arguments, 1); // for normal call\n        // 4. Let F be a new native ECMAScript object.\n        // 11. Set the [[Prototype]] internal property of F to the standard\n        //   built-in Function prototype object as specified in 15.3.3.1.\n        // 12. Set the [[Call]] internal property of F as described in\n        //   15.3.4.5.1.\n        // 13. Set the [[Construct]] internal property of F as described in\n        //   15.3.4.5.2.\n        // 14. Set the [[HasInstance]] internal property of F as described in\n        //   15.3.4.5.3.\n        var binder = function () {\n\n            if (this instanceof bound) {\n                // 15.3.4.5.2 [[Construct]]\n                // When the [[Construct]] internal method of a function object,\n                // F that was created using the bind function is called with a\n                // list of arguments ExtraArgs, the following steps are taken:\n                // 1. Let target be the value of F's [[TargetFunction]]\n                //   internal property.\n                // 2. If target has no [[Construct]] internal method, a\n                //   TypeError exception is thrown.\n                // 3. Let boundArgs be the value of F's [[BoundArgs]] internal\n                //   property.\n                // 4. Let args be a new list containing the same values as the\n                //   list boundArgs in the same order followed by the same\n                //   values as the list ExtraArgs in the same order.\n                // 5. Return the result of calling the [[Construct]] internal\n                //   method of target providing args as the arguments.\n\n                var result = target.apply(\n                    this,\n                    args.concat(_Array_slice_.call(arguments))\n                );\n                if (Object(result) === result) {\n                    return result;\n                }\n                return this;\n\n            } else {\n                // 15.3.4.5.1 [[Call]]\n                // When the [[Call]] internal method of a function object, F,\n                // which was created using the bind function is called with a\n                // this value and a list of arguments ExtraArgs, the following\n                // steps are taken:\n                // 1. Let boundArgs be the value of F's [[BoundArgs]] internal\n                //   property.\n                // 2. Let boundThis be the value of F's [[BoundThis]] internal\n                //   property.\n                // 3. Let target be the value of F's [[TargetFunction]] internal\n                //   property.\n                // 4. Let args be a new list containing the same values as the\n                //   list boundArgs in the same order followed by the same\n                //   values as the list ExtraArgs in the same order.\n                // 5. Return the result of calling the [[Call]] internal method\n                //   of target providing boundThis as the this value and\n                //   providing args as the arguments.\n\n                // equiv: target.call(this, ...boundArgs, ...args)\n                return target.apply(\n                    that,\n                    args.concat(_Array_slice_.call(arguments))\n                );\n\n            }\n\n        };\n\n        // 15. If the [[Class]] internal property of Target is \"Function\", then\n        //     a. Let L be the length property of Target minus the length of A.\n        //     b. Set the length own property of F to either 0 or L, whichever is\n        //       larger.\n        // 16. Else set the length own property of F to 0.\n\n        var boundLength = Math.max(0, target.length - args.length);\n\n        // 17. Set the attributes of the length own property of F to the values\n        //   specified in 15.3.5.1.\n        var boundArgs = [];\n        for (var i = 0; i < boundLength; i++) {\n            boundArgs.push(\"$\" + i);\n        }\n\n        // XXX Build a dynamic function with desired amount of arguments is the only\n        // way to set the length property of a function.\n        // In environments where Content Security Policies enabled (Chrome extensions,\n        // for ex.) all use of eval or Function costructor throws an exception.\n        // However in all of these environments Function.prototype.bind exists\n        // and so this code will never be executed.\n        var bound = Function(\"binder\", \"return function (\" + boundArgs.join(\",\") + \"){return binder.apply(this,arguments)}\")(binder);\n\n        if (target.prototype) {\n            Empty.prototype = target.prototype;\n            bound.prototype = new Empty();\n            // Clean up dangling references.\n            Empty.prototype = null;\n        }\n\n        // TODO\n        // 18. Set the [[Extensible]] internal property of F to true.\n\n        // TODO\n        // 19. Let thrower be the [[ThrowTypeError]] function Object (13.2.3).\n        // 20. Call the [[DefineOwnProperty]] internal method of F with\n        //   arguments \"caller\", PropertyDescriptor {[[Get]]: thrower, [[Set]]:\n        //   thrower, [[Enumerable]]: false, [[Configurable]]: false}, and\n        //   false.\n        // 21. Call the [[DefineOwnProperty]] internal method of F with\n        //   arguments \"arguments\", PropertyDescriptor {[[Get]]: thrower,\n        //   [[Set]]: thrower, [[Enumerable]]: false, [[Configurable]]: false},\n        //   and false.\n\n        // TODO\n        // NOTE Function objects created using Function.prototype.bind do not\n        // have a prototype property or the [[Code]], [[FormalParameters]], and\n        // [[Scope]] internal properties.\n        // XXX can't delete prototype in pure-js.\n\n        // 22. Return F.\n        return bound;\n    };\n}\n\n// _Please note: Shortcuts are defined after `Function.prototype.bind` as we\n// us it in defining shortcuts.\nvar owns = call.bind(prototypeOfObject.hasOwnProperty);\n\n// If JS engine supports accessors creating shortcuts.\nvar defineGetter;\nvar defineSetter;\nvar lookupGetter;\nvar lookupSetter;\nvar supportsAccessors;\nif ((supportsAccessors = owns(prototypeOfObject, \"__defineGetter__\"))) {\n    defineGetter = call.bind(prototypeOfObject.__defineGetter__);\n    defineSetter = call.bind(prototypeOfObject.__defineSetter__);\n    lookupGetter = call.bind(prototypeOfObject.__lookupGetter__);\n    lookupSetter = call.bind(prototypeOfObject.__lookupSetter__);\n}\n\n//\n// Array\n// =====\n//\n\n// ES5 15.4.4.12\n// http://es5.github.com/#x15.4.4.12\nvar spliceWorksWithEmptyObject = (function () {\n    var obj = {};\n    Array.prototype.splice.call(obj, 0, 0, 1);\n    return obj.length === 1;\n}());\nvar omittingSecondSpliceArgIsNoop = [1].splice(0).length === 0;\nvar spliceNoopReturnsEmptyArray = (function () {\n    var a = [1, 2];\n    var result = a.splice();\n    return a.length === 2 && isArray(result) && result.length === 0;\n}());\nif (spliceNoopReturnsEmptyArray) {\n    // Safari 5.0 bug where .split() returns undefined\n    Array.prototype.splice = function splice(start, deleteCount) {\n        if (arguments.length === 0) { return []; }\n        else { return array_splice.apply(this, arguments); }\n    };\n}\nif (!omittingSecondSpliceArgIsNoop || !spliceWorksWithEmptyObject) {\n    Array.prototype.splice = function splice(start, deleteCount) {\n        if (arguments.length === 0) { return []; }\n        var args = arguments;\n        this.length = Math.max(toInteger(this.length), 0);\n        if (arguments.length > 0 && typeof deleteCount !== 'number') {\n            args = _Array_slice_.call(arguments);\n            if (args.length < 2) { args.push(toInteger(deleteCount)); }\n            else { args[1] = toInteger(deleteCount); }\n        }\n        return array_splice.apply(this, args);\n    };\n}\n\n// ES5 15.4.4.12\n// http://es5.github.com/#x15.4.4.13\n// Return len+argCount.\n// [bugfix, ielt8]\n// IE < 8 bug: [].unshift(0) === undefined but should be \"1\"\nif ([].unshift(0) !== 1) {\n    Array.prototype.unshift = function () {\n        array_unshift.apply(this, arguments);\n        return this.length;\n    };\n}\n\n// ES5 15.4.3.2\n// http://es5.github.com/#x15.4.3.2\n// https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/isArray\nif (!Array.isArray) {\n    Array.isArray = isArray;\n}\n\n// The IsCallable() check in the Array functions\n// has been replaced with a strict check on the\n// internal class of the object to trap cases where\n// the provided function was actually a regular\n// expression literal, which in V8 and\n// JavaScriptCore is a typeof \"function\".  Only in\n// V8 are regular expression literals permitted as\n// reduce parameters, so it is desirable in the\n// general case for the shim to match the more\n// strict and common behavior of rejecting regular\n// expressions.\n\n// ES5 15.4.4.18\n// http://es5.github.com/#x15.4.4.18\n// https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/array/forEach\n\n// Check failure of by-index access of string characters (IE < 9)\n// and failure of `0 in boxedString` (Rhino)\nvar boxedString = Object(\"a\");\nvar splitString = boxedString[0] !== \"a\" || !(0 in boxedString);\n\nvar properlyBoxesContext = function properlyBoxed(method) {\n    // Check node 0.6.21 bug where third parameter is not boxed\n    var properlyBoxesNonStrict = true;\n    var properlyBoxesStrict = true;\n    if (method) {\n        method.call('foo', function (_, __, context) {\n            if (typeof context !== 'object') { properlyBoxesNonStrict = false; }\n        });\n\n        method.call([1], function () {\n            'use strict';\n            properlyBoxesStrict = typeof this === 'string';\n        }, 'x');\n    }\n    return !!method && properlyBoxesNonStrict && properlyBoxesStrict;\n};\n\nif (!Array.prototype.forEach || !properlyBoxesContext(Array.prototype.forEach)) {\n    Array.prototype.forEach = function forEach(fun /*, thisp*/) {\n        var object = toObject(this),\n            self = splitString && _toString.call(this) === \"[object String]\" ?\n                this.split(\"\") :\n                object,\n            thisp = arguments[1],\n            i = -1,\n            length = self.length >>> 0;\n\n        // If no callback function or if callback is not a callable function\n        if (!isFunction(fun)) {\n            throw new TypeError(); // TODO message\n        }\n\n        while (++i < length) {\n            if (i in self) {\n                // Invoke the callback function with call, passing arguments:\n                // context, property value, property key, thisArg object\n                // context\n                fun.call(thisp, self[i], i, object);\n            }\n        }\n    };\n}\n\n// ES5 15.4.4.19\n// http://es5.github.com/#x15.4.4.19\n// https://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Objects/Array/map\nif (!Array.prototype.map || !properlyBoxesContext(Array.prototype.map)) {\n    Array.prototype.map = function map(fun /*, thisp*/) {\n        var object = toObject(this),\n            self = splitString && _toString.call(this) === \"[object String]\" ?\n                this.split(\"\") :\n                object,\n            length = self.length >>> 0,\n            result = Array(length),\n            thisp = arguments[1];\n\n        // If no callback function or if callback is not a callable function\n        if (!isFunction(fun)) {\n            throw new TypeError(fun + \" is not a function\");\n        }\n\n        for (var i = 0; i < length; i++) {\n            if (i in self) {\n                result[i] = fun.call(thisp, self[i], i, object);\n            }\n        }\n        return result;\n    };\n}\n\n// ES5 15.4.4.20\n// http://es5.github.com/#x15.4.4.20\n// https://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Objects/Array/filter\nif (!Array.prototype.filter || !properlyBoxesContext(Array.prototype.filter)) {\n    Array.prototype.filter = function filter(fun /*, thisp */) {\n        var object = toObject(this),\n            self = splitString && _toString.call(this) === \"[object String]\" ?\n                this.split(\"\") :\n                    object,\n            length = self.length >>> 0,\n            result = [],\n            value,\n            thisp = arguments[1];\n\n        // If no callback function or if callback is not a callable function\n        if (!isFunction(fun)) {\n            throw new TypeError(fun + \" is not a function\");\n        }\n\n        for (var i = 0; i < length; i++) {\n            if (i in self) {\n                value = self[i];\n                if (fun.call(thisp, value, i, object)) {\n                    result.push(value);\n                }\n            }\n        }\n        return result;\n    };\n}\n\n// ES5 15.4.4.16\n// http://es5.github.com/#x15.4.4.16\n// https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/every\nif (!Array.prototype.every || !properlyBoxesContext(Array.prototype.every)) {\n    Array.prototype.every = function every(fun /*, thisp */) {\n        var object = toObject(this),\n            self = splitString && _toString.call(this) === \"[object String]\" ?\n                this.split(\"\") :\n                object,\n            length = self.length >>> 0,\n            thisp = arguments[1];\n\n        // If no callback function or if callback is not a callable function\n        if (!isFunction(fun)) {\n            throw new TypeError(fun + \" is not a function\");\n        }\n\n        for (var i = 0; i < length; i++) {\n            if (i in self && !fun.call(thisp, self[i], i, object)) {\n                return false;\n            }\n        }\n        return true;\n    };\n}\n\n// ES5 15.4.4.17\n// http://es5.github.com/#x15.4.4.17\n// https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/some\nif (!Array.prototype.some || !properlyBoxesContext(Array.prototype.some)) {\n    Array.prototype.some = function some(fun /*, thisp */) {\n        var object = toObject(this),\n            self = splitString && _toString.call(this) === \"[object String]\" ?\n                this.split(\"\") :\n                object,\n            length = self.length >>> 0,\n            thisp = arguments[1];\n\n        // If no callback function or if callback is not a callable function\n        if (!isFunction(fun)) {\n            throw new TypeError(fun + \" is not a function\");\n        }\n\n        for (var i = 0; i < length; i++) {\n            if (i in self && fun.call(thisp, self[i], i, object)) {\n                return true;\n            }\n        }\n        return false;\n    };\n}\n\n// ES5 15.4.4.21\n// http://es5.github.com/#x15.4.4.21\n// https://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Objects/Array/reduce\nvar reduceCoercesToObject = false;\nif (Array.prototype.reduce) {\n    reduceCoercesToObject = typeof Array.prototype.reduce.call('es5', function (_, __, ___, list) { return list; }) === 'object';\n}\nif (!Array.prototype.reduce || !reduceCoercesToObject) {\n    Array.prototype.reduce = function reduce(fun /*, initial*/) {\n        var object = toObject(this),\n            self = splitString && _toString.call(this) === \"[object String]\" ?\n                this.split(\"\") :\n                object,\n            length = self.length >>> 0;\n\n        // If no callback function or if callback is not a callable function\n        if (!isFunction(fun)) {\n            throw new TypeError(fun + \" is not a function\");\n        }\n\n        // no value to return if no initial value and an empty array\n        if (!length && arguments.length === 1) {\n            throw new TypeError(\"reduce of empty array with no initial value\");\n        }\n\n        var i = 0;\n        var result;\n        if (arguments.length >= 2) {\n            result = arguments[1];\n        } else {\n            do {\n                if (i in self) {\n                    result = self[i++];\n                    break;\n                }\n\n                // if array contains no values, no initial value to return\n                if (++i >= length) {\n                    throw new TypeError(\"reduce of empty array with no initial value\");\n                }\n            } while (true);\n        }\n\n        for (; i < length; i++) {\n            if (i in self) {\n                result = fun.call(void 0, result, self[i], i, object);\n            }\n        }\n\n        return result;\n    };\n}\n\n// ES5 15.4.4.22\n// http://es5.github.com/#x15.4.4.22\n// https://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Objects/Array/reduceRight\nvar reduceRightCoercesToObject = false;\nif (Array.prototype.reduceRight) {\n    reduceRightCoercesToObject = typeof Array.prototype.reduceRight.call('es5', function (_, __, ___, list) { return list; }) === 'object';\n}\nif (!Array.prototype.reduceRight || !reduceRightCoercesToObject) {\n    Array.prototype.reduceRight = function reduceRight(fun /*, initial*/) {\n        var object = toObject(this),\n            self = splitString && _toString.call(this) === \"[object String]\" ?\n                this.split(\"\") :\n                object,\n            length = self.length >>> 0;\n\n        // If no callback function or if callback is not a callable function\n        if (!isFunction(fun)) {\n            throw new TypeError(fun + \" is not a function\");\n        }\n\n        // no value to return if no initial value, empty array\n        if (!length && arguments.length === 1) {\n            throw new TypeError(\"reduceRight of empty array with no initial value\");\n        }\n\n        var result, i = length - 1;\n        if (arguments.length >= 2) {\n            result = arguments[1];\n        } else {\n            do {\n                if (i in self) {\n                    result = self[i--];\n                    break;\n                }\n\n                // if array contains no values, no initial value to return\n                if (--i < 0) {\n                    throw new TypeError(\"reduceRight of empty array with no initial value\");\n                }\n            } while (true);\n        }\n\n        if (i < 0) {\n            return result;\n        }\n\n        do {\n            if (i in self) {\n                result = fun.call(void 0, result, self[i], i, object);\n            }\n        } while (i--);\n\n        return result;\n    };\n}\n\n// ES5 15.4.4.14\n// http://es5.github.com/#x15.4.4.14\n// https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/indexOf\nif (!Array.prototype.indexOf || ([0, 1].indexOf(1, 2) !== -1)) {\n    Array.prototype.indexOf = function indexOf(sought /*, fromIndex */ ) {\n        var self = splitString && _toString.call(this) === \"[object String]\" ?\n                this.split(\"\") :\n                toObject(this),\n            length = self.length >>> 0;\n\n        if (!length) {\n            return -1;\n        }\n\n        var i = 0;\n        if (arguments.length > 1) {\n            i = toInteger(arguments[1]);\n        }\n\n        // handle negative indices\n        i = i >= 0 ? i : Math.max(0, length + i);\n        for (; i < length; i++) {\n            if (i in self && self[i] === sought) {\n                return i;\n            }\n        }\n        return -1;\n    };\n}\n\n// ES5 15.4.4.15\n// http://es5.github.com/#x15.4.4.15\n// https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/lastIndexOf\nif (!Array.prototype.lastIndexOf || ([0, 1].lastIndexOf(0, -3) !== -1)) {\n    Array.prototype.lastIndexOf = function lastIndexOf(sought /*, fromIndex */) {\n        var self = splitString && _toString.call(this) === \"[object String]\" ?\n                this.split(\"\") :\n                toObject(this),\n            length = self.length >>> 0;\n\n        if (!length) {\n            return -1;\n        }\n        var i = length - 1;\n        if (arguments.length > 1) {\n            i = Math.min(i, toInteger(arguments[1]));\n        }\n        // handle negative indices\n        i = i >= 0 ? i : length - Math.abs(i);\n        for (; i >= 0; i--) {\n            if (i in self && sought === self[i]) {\n                return i;\n            }\n        }\n        return -1;\n    };\n}\n\n//\n// Object\n// ======\n//\n\n// ES5 15.2.3.14\n// http://es5.github.com/#x15.2.3.14\nvar keysWorksWithArguments = Object.keys && (function () {\n    return Object.keys(arguments).length === 2;\n}(1, 2));\nif (!Object.keys) {\n    // http://whattheheadsaid.com/2010/10/a-safer-object-keys-compatibility-implementation\n    var hasDontEnumBug = !({'toString': null}).propertyIsEnumerable('toString'),\n        hasProtoEnumBug = (function () {}).propertyIsEnumerable('prototype'),\n        dontEnums = [\n            \"toString\",\n            \"toLocaleString\",\n            \"valueOf\",\n            \"hasOwnProperty\",\n            \"isPrototypeOf\",\n            \"propertyIsEnumerable\",\n            \"constructor\"\n        ],\n        dontEnumsLength = dontEnums.length;\n\n    Object.keys = function keys(object) {\n        var isFn = isFunction(object),\n            isArgs = isArguments(object),\n            isObject = object !== null && typeof object === 'object',\n            isString = isObject && _toString.call(object) === '[object String]';\n\n        if (!isObject && !isFn && !isArgs) {\n            throw new TypeError(\"Object.keys called on a non-object\");\n        }\n\n        var theKeys = [];\n        var skipProto = hasProtoEnumBug && isFn;\n        if (isString || isArgs) {\n            for (var i = 0; i < object.length; ++i) {\n                theKeys.push(String(i));\n            }\n        } else {\n            for (var name in object) {\n                if (!(skipProto && name === 'prototype') && owns(object, name)) {\n                    theKeys.push(String(name));\n                }\n            }\n        }\n\n        if (hasDontEnumBug) {\n            var ctor = object.constructor,\n                skipConstructor = ctor && ctor.prototype === object;\n            for (var j = 0; j < dontEnumsLength; j++) {\n                var dontEnum = dontEnums[j];\n                if (!(skipConstructor && dontEnum === 'constructor') && owns(object, dontEnum)) {\n                    theKeys.push(dontEnum);\n                }\n            }\n        }\n        return theKeys;\n    };\n} else if (!keysWorksWithArguments) {\n    // Safari 5.0 bug\n    var originalKeys = Object.keys;\n    Object.keys = function keys(object) {\n        if (isArguments(object)) {\n            return originalKeys(Array.prototype.slice.call(object));\n        } else {\n            return originalKeys(object);\n        }\n    };\n}\n\n//\n// Date\n// ====\n//\n\n// ES5 15.9.5.43\n// http://es5.github.com/#x15.9.5.43\n// This function returns a String value represent the instance in time\n// represented by this Date object. The format of the String is the Date Time\n// string format defined in 15.9.1.15. All fields are present in the String.\n// The time zone is always UTC, denoted by the suffix Z. If the time value of\n// this object is not a finite Number a RangeError exception is thrown.\nvar negativeDate = -62198755200000,\n    negativeYearString = \"-000001\";\nif (\n    !Date.prototype.toISOString ||\n    (new Date(negativeDate).toISOString().indexOf(negativeYearString) === -1)\n) {\n    Date.prototype.toISOString = function toISOString() {\n        var result, length, value, year, month;\n        if (!isFinite(this)) {\n            throw new RangeError(\"Date.prototype.toISOString called on non-finite value.\");\n        }\n\n        year = this.getUTCFullYear();\n\n        month = this.getUTCMonth();\n        // see https://github.com/es-shims/es5-shim/issues/111\n        year += Math.floor(month / 12);\n        month = (month % 12 + 12) % 12;\n\n        // the date time string format is specified in 15.9.1.15.\n        result = [month + 1, this.getUTCDate(), this.getUTCHours(), this.getUTCMinutes(), this.getUTCSeconds()];\n        year = (\n            (year < 0 ? \"-\" : (year > 9999 ? \"+\" : \"\")) +\n            (\"00000\" + Math.abs(year)).slice(0 <= year && year <= 9999 ? -4 : -6)\n        );\n\n        length = result.length;\n        while (length--) {\n            value = result[length];\n            // pad months, days, hours, minutes, and seconds to have two\n            // digits.\n            if (value < 10) {\n                result[length] = \"0\" + value;\n            }\n        }\n        // pad milliseconds to have three digits.\n        return (\n            year + \"-\" + result.slice(0, 2).join(\"-\") +\n            \"T\" + result.slice(2).join(\":\") + \".\" +\n            (\"000\" + this.getUTCMilliseconds()).slice(-3) + \"Z\"\n        );\n    };\n}\n\n\n// ES5 15.9.5.44\n// http://es5.github.com/#x15.9.5.44\n// This function provides a String representation of a Date object for use by\n// JSON.stringify (15.12.3).\nvar dateToJSONIsSupported = false;\ntry {\n    dateToJSONIsSupported = (\n        Date.prototype.toJSON &&\n        new Date(NaN).toJSON() === null &&\n        new Date(negativeDate).toJSON().indexOf(negativeYearString) !== -1 &&\n        Date.prototype.toJSON.call({ // generic\n            toISOString: function () {\n                return true;\n            }\n        })\n    );\n} catch (e) {\n}\nif (!dateToJSONIsSupported) {\n    Date.prototype.toJSON = function toJSON(key) {\n        // When the toJSON method is called with argument key, the following\n        // steps are taken:\n\n        // 1.  Let O be the result of calling ToObject, giving it the this\n        // value as its argument.\n        // 2. Let tv be toPrimitive(O, hint Number).\n        var o = Object(this),\n            tv = toPrimitive(o),\n            toISO;\n        // 3. If tv is a Number and is not finite, return null.\n        if (typeof tv === \"number\" && !isFinite(tv)) {\n            return null;\n        }\n        // 4. Let toISO be the result of calling the [[Get]] internal method of\n        // O with argument \"toISOString\".\n        toISO = o.toISOString;\n        // 5. If IsCallable(toISO) is false, throw a TypeError exception.\n        if (typeof toISO !== \"function\") {\n            throw new TypeError(\"toISOString property is not callable\");\n        }\n        // 6. Return the result of calling the [[Call]] internal method of\n        //  toISO with O as the this value and an empty argument list.\n        return toISO.call(o);\n\n        // NOTE 1 The argument is ignored.\n\n        // NOTE 2 The toJSON function is intentionally generic; it does not\n        // require that its this value be a Date object. Therefore, it can be\n        // transferred to other kinds of objects for use as a method. However,\n        // it does require that any such object have a toISOString method. An\n        // object is free to use the argument key to filter its\n        // stringification.\n    };\n}\n\n// ES5 15.9.4.2\n// http://es5.github.com/#x15.9.4.2\n// based on work shared by Daniel Friesen (dantman)\n// http://gist.github.com/303249\nvar supportsExtendedYears = Date.parse('+033658-09-27T01:46:40.000Z') === 1e15;\nvar acceptsInvalidDates = !isNaN(Date.parse('2012-04-04T24:00:00.500Z')) || !isNaN(Date.parse('2012-11-31T23:59:59.000Z'));\nvar doesNotParseY2KNewYear = isNaN(Date.parse(\"2000-01-01T00:00:00.000Z\"));\nif (!Date.parse || doesNotParseY2KNewYear || acceptsInvalidDates || !supportsExtendedYears) {\n    // XXX global assignment won't work in embeddings that use\n    // an alternate object for the context.\n    Date = (function (NativeDate) {\n\n        // Date.length === 7\n        function Date(Y, M, D, h, m, s, ms) {\n            var length = arguments.length;\n            if (this instanceof NativeDate) {\n                var date = length === 1 && String(Y) === Y ? // isString(Y)\n                    // We explicitly pass it through parse:\n                    new NativeDate(Date.parse(Y)) :\n                    // We have to manually make calls depending on argument\n                    // length here\n                    length >= 7 ? new NativeDate(Y, M, D, h, m, s, ms) :\n                    length >= 6 ? new NativeDate(Y, M, D, h, m, s) :\n                    length >= 5 ? new NativeDate(Y, M, D, h, m) :\n                    length >= 4 ? new NativeDate(Y, M, D, h) :\n                    length >= 3 ? new NativeDate(Y, M, D) :\n                    length >= 2 ? new NativeDate(Y, M) :\n                    length >= 1 ? new NativeDate(Y) :\n                                  new NativeDate();\n                // Prevent mixups with unfixed Date object\n                date.constructor = Date;\n                return date;\n            }\n            return NativeDate.apply(this, arguments);\n        }\n\n        // 15.9.1.15 Date Time String Format.\n        var isoDateExpression = new RegExp(\"^\" +\n            \"(\\\\d{4}|[\\+\\-]\\\\d{6})\" + // four-digit year capture or sign +\n                                      // 6-digit extended year\n            \"(?:-(\\\\d{2})\" + // optional month capture\n            \"(?:-(\\\\d{2})\" + // optional day capture\n            \"(?:\" + // capture hours:minutes:seconds.milliseconds\n                \"T(\\\\d{2})\" + // hours capture\n                \":(\\\\d{2})\" + // minutes capture\n                \"(?:\" + // optional :seconds.milliseconds\n                    \":(\\\\d{2})\" + // seconds capture\n                    \"(?:(\\\\.\\\\d{1,}))?\" + // milliseconds capture\n                \")?\" +\n            \"(\" + // capture UTC offset component\n                \"Z|\" + // UTC capture\n                \"(?:\" + // offset specifier +/-hours:minutes\n                    \"([-+])\" + // sign capture\n                    \"(\\\\d{2})\" + // hours offset capture\n                    \":(\\\\d{2})\" + // minutes offset capture\n                \")\" +\n            \")?)?)?)?\" +\n        \"$\");\n\n        var months = [\n            0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365\n        ];\n\n        function dayFromMonth(year, month) {\n            var t = month > 1 ? 1 : 0;\n            return (\n                months[month] +\n                Math.floor((year - 1969 + t) / 4) -\n                Math.floor((year - 1901 + t) / 100) +\n                Math.floor((year - 1601 + t) / 400) +\n                365 * (year - 1970)\n            );\n        }\n\n        function toUTC(t) {\n            return Number(new NativeDate(1970, 0, 1, 0, 0, 0, t));\n        }\n\n        // Copy any custom methods a 3rd party library may have added\n        for (var key in NativeDate) {\n            Date[key] = NativeDate[key];\n        }\n\n        // Copy \"native\" methods explicitly; they may be non-enumerable\n        Date.now = NativeDate.now;\n        Date.UTC = NativeDate.UTC;\n        Date.prototype = NativeDate.prototype;\n        Date.prototype.constructor = Date;\n\n        // Upgrade Date.parse to handle simplified ISO 8601 strings\n        Date.parse = function parse(string) {\n            var match = isoDateExpression.exec(string);\n            if (match) {\n                // parse months, days, hours, minutes, seconds, and milliseconds\n                // provide default values if necessary\n                // parse the UTC offset component\n                var year = Number(match[1]),\n                    month = Number(match[2] || 1) - 1,\n                    day = Number(match[3] || 1) - 1,\n                    hour = Number(match[4] || 0),\n                    minute = Number(match[5] || 0),\n                    second = Number(match[6] || 0),\n                    millisecond = Math.floor(Number(match[7] || 0) * 1000),\n                    // When time zone is missed, local offset should be used\n                    // (ES 5.1 bug)\n                    // see https://bugs.ecmascript.org/show_bug.cgi?id=112\n                    isLocalTime = Boolean(match[4] && !match[8]),\n                    signOffset = match[9] === \"-\" ? 1 : -1,\n                    hourOffset = Number(match[10] || 0),\n                    minuteOffset = Number(match[11] || 0),\n                    result;\n                if (\n                    hour < (\n                        minute > 0 || second > 0 || millisecond > 0 ?\n                        24 : 25\n                    ) &&\n                    minute < 60 && second < 60 && millisecond < 1000 &&\n                    month > -1 && month < 12 && hourOffset < 24 &&\n                    minuteOffset < 60 && // detect invalid offsets\n                    day > -1 &&\n                    day < (\n                        dayFromMonth(year, month + 1) -\n                        dayFromMonth(year, month)\n                    )\n                ) {\n                    result = (\n                        (dayFromMonth(year, month) + day) * 24 +\n                        hour +\n                        hourOffset * signOffset\n                    ) * 60;\n                    result = (\n                        (result + minute + minuteOffset * signOffset) * 60 +\n                        second\n                    ) * 1000 + millisecond;\n                    if (isLocalTime) {\n                        result = toUTC(result);\n                    }\n                    if (-8.64e15 <= result && result <= 8.64e15) {\n                        return result;\n                    }\n                }\n                return NaN;\n            }\n            return NativeDate.parse.apply(this, arguments);\n        };\n\n        return Date;\n    })(Date);\n}\n\n// ES5 15.9.4.4\n// http://es5.github.com/#x15.9.4.4\nif (!Date.now) {\n    Date.now = function now() {\n        return new Date().getTime();\n    };\n}\n\n\n//\n// Number\n// ======\n//\n\n// ES5.1 15.7.4.5\n// http://es5.github.com/#x15.7.4.5\nif (!Number.prototype.toFixed || (0.00008).toFixed(3) !== '0.000' || (0.9).toFixed(0) === '0' || (1.255).toFixed(2) !== '1.25' || (1000000000000000128).toFixed(0) !== \"1000000000000000128\") {\n    // Hide these variables and functions\n    (function () {\n        var base, size, data, i;\n\n        base = 1e7;\n        size = 6;\n        data = [0, 0, 0, 0, 0, 0];\n\n        function multiply(n, c) {\n            var i = -1;\n            while (++i < size) {\n                c += n * data[i];\n                data[i] = c % base;\n                c = Math.floor(c / base);\n            }\n        }\n\n        function divide(n) {\n            var i = size, c = 0;\n            while (--i >= 0) {\n                c += data[i];\n                data[i] = Math.floor(c / n);\n                c = (c % n) * base;\n            }\n        }\n\n        function numToString() {\n            var i = size;\n            var s = '';\n            while (--i >= 0) {\n                if (s !== '' || i === 0 || data[i] !== 0) {\n                    var t = String(data[i]);\n                    if (s === '') {\n                        s = t;\n                    } else {\n                        s += '0000000'.slice(0, 7 - t.length) + t;\n                    }\n                }\n            }\n            return s;\n        }\n\n        function pow(x, n, acc) {\n            return (n === 0 ? acc : (n % 2 === 1 ? pow(x, n - 1, acc * x) : pow(x * x, n / 2, acc)));\n        }\n\n        function log(x) {\n            var n = 0;\n            while (x >= 4096) {\n                n += 12;\n                x /= 4096;\n            }\n            while (x >= 2) {\n                n += 1;\n                x /= 2;\n            }\n            return n;\n        }\n\n        Number.prototype.toFixed = function toFixed(fractionDigits) {\n            var f, x, s, m, e, z, j, k;\n\n            // Test for NaN and round fractionDigits down\n            f = Number(fractionDigits);\n            f = f !== f ? 0 : Math.floor(f);\n\n            if (f < 0 || f > 20) {\n                throw new RangeError(\"Number.toFixed called with invalid number of decimals\");\n            }\n\n            x = Number(this);\n\n            // Test for NaN\n            if (x !== x) {\n                return \"NaN\";\n            }\n\n            // If it is too big or small, return the string value of the number\n            if (x <= -1e21 || x >= 1e21) {\n                return String(x);\n            }\n\n            s = \"\";\n\n            if (x < 0) {\n                s = \"-\";\n                x = -x;\n            }\n\n            m = \"0\";\n\n            if (x > 1e-21) {\n                // 1e-21 < x < 1e21\n                // -70 < log2(x) < 70\n                e = log(x * pow(2, 69, 1)) - 69;\n                z = (e < 0 ? x * pow(2, -e, 1) : x / pow(2, e, 1));\n                z *= 0x10000000000000; // Math.pow(2, 52);\n                e = 52 - e;\n\n                // -18 < e < 122\n                // x = z / 2 ^ e\n                if (e > 0) {\n                    multiply(0, z);\n                    j = f;\n\n                    while (j >= 7) {\n                        multiply(1e7, 0);\n                        j -= 7;\n                    }\n\n                    multiply(pow(10, j, 1), 0);\n                    j = e - 1;\n\n                    while (j >= 23) {\n                        divide(1 << 23);\n                        j -= 23;\n                    }\n\n                    divide(1 << j);\n                    multiply(1, 1);\n                    divide(2);\n                    m = numToString();\n                } else {\n                    multiply(0, z);\n                    multiply(1 << (-e), 0);\n                    m = numToString() + '0.00000000000000000000'.slice(2, 2 + f);\n                }\n            }\n\n            if (f > 0) {\n                k = m.length;\n\n                if (k <= f) {\n                    m = s + '0.0000000000000000000'.slice(0, f - k + 2) + m;\n                } else {\n                    m = s + m.slice(0, k - f) + '.' + m.slice(k - f);\n                }\n            } else {\n                m = s + m;\n            }\n\n            return m;\n        };\n    }());\n}\n\n\n//\n// String\n// ======\n//\n\n// ES5 15.5.4.14\n// http://es5.github.com/#x15.5.4.14\n\n// [bugfix, IE lt 9, firefox 4, Konqueror, Opera, obscure browsers]\n// Many browsers do not split properly with regular expressions or they\n// do not perform the split correctly under obscure conditions.\n// See http://blog.stevenlevithan.com/archives/cross-browser-split\n// I've tested in many browsers and this seems to cover the deviant ones:\n//    'ab'.split(/(?:ab)*/) should be [\"\", \"\"], not [\"\"]\n//    '.'.split(/(.?)(.?)/) should be [\"\", \".\", \"\", \"\"], not [\"\", \"\"]\n//    'tesst'.split(/(s)*/) should be [\"t\", undefined, \"e\", \"s\", \"t\"], not\n//       [undefined, \"t\", undefined, \"e\", ...]\n//    ''.split(/.?/) should be [], not [\"\"]\n//    '.'.split(/()()/) should be [\".\"], not [\"\", \"\", \".\"]\n\nvar string_split = String.prototype.split;\nif (\n    'ab'.split(/(?:ab)*/).length !== 2 ||\n    '.'.split(/(.?)(.?)/).length !== 4 ||\n    'tesst'.split(/(s)*/)[1] === \"t\" ||\n    'test'.split(/(?:)/, -1).length !== 4 ||\n    ''.split(/.?/).length ||\n    '.'.split(/()()/).length > 1\n) {\n    (function () {\n        var compliantExecNpcg = /()??/.exec(\"\")[1] === void 0; // NPCG: nonparticipating capturing group\n\n        String.prototype.split = function (separator, limit) {\n            var string = this;\n            if (separator === void 0 && limit === 0) {\n                return [];\n            }\n\n            // If `separator` is not a regex, use native split\n            if (_toString.call(separator) !== \"[object RegExp]\") {\n                return string_split.call(this, separator, limit);\n            }\n\n            var output = [],\n                flags = (separator.ignoreCase ? \"i\" : \"\") +\n                        (separator.multiline  ? \"m\" : \"\") +\n                        (separator.extended   ? \"x\" : \"\") + // Proposed for ES6\n                        (separator.sticky     ? \"y\" : \"\"), // Firefox 3+\n                lastLastIndex = 0,\n                // Make `global` and avoid `lastIndex` issues by working with a copy\n                separator2, match, lastIndex, lastLength;\n            separator = new RegExp(separator.source, flags + \"g\");\n            string += \"\"; // Type-convert\n            if (!compliantExecNpcg) {\n                // Doesn't need flags gy, but they don't hurt\n                separator2 = new RegExp(\"^\" + separator.source + \"$(?!\\\\s)\", flags);\n            }\n            /* Values for `limit`, per the spec:\n             * If undefined: 4294967295 // Math.pow(2, 32) - 1\n             * If 0, Infinity, or NaN: 0\n             * If positive number: limit = Math.floor(limit); if (limit > 4294967295) limit -= 4294967296;\n             * If negative number: 4294967296 - Math.floor(Math.abs(limit))\n             * If other: Type-convert, then use the above rules\n             */\n            limit = limit === void 0 ?\n                -1 >>> 0 : // Math.pow(2, 32) - 1\n                ToUint32(limit);\n            while (match = separator.exec(string)) {\n                // `separator.lastIndex` is not reliable cross-browser\n                lastIndex = match.index + match[0].length;\n                if (lastIndex > lastLastIndex) {\n                    output.push(string.slice(lastLastIndex, match.index));\n                    // Fix browsers whose `exec` methods don't consistently return `undefined` for\n                    // nonparticipating capturing groups\n                    if (!compliantExecNpcg && match.length > 1) {\n                        match[0].replace(separator2, function () {\n                            for (var i = 1; i < arguments.length - 2; i++) {\n                                if (arguments[i] === void 0) {\n                                    match[i] = void 0;\n                                }\n                            }\n                        });\n                    }\n                    if (match.length > 1 && match.index < string.length) {\n                        Array.prototype.push.apply(output, match.slice(1));\n                    }\n                    lastLength = match[0].length;\n                    lastLastIndex = lastIndex;\n                    if (output.length >= limit) {\n                        break;\n                    }\n                }\n                if (separator.lastIndex === match.index) {\n                    separator.lastIndex++; // Avoid an infinite loop\n                }\n            }\n            if (lastLastIndex === string.length) {\n                if (lastLength || !separator.test(\"\")) {\n                    output.push(\"\");\n                }\n            } else {\n                output.push(string.slice(lastLastIndex));\n            }\n            return output.length > limit ? output.slice(0, limit) : output;\n        };\n    }());\n\n// [bugfix, chrome]\n// If separator is undefined, then the result array contains just one String,\n// which is the this value (converted to a String). If limit is not undefined,\n// then the output array is truncated so that it contains no more than limit\n// elements.\n// \"0\".split(undefined, 0) -> []\n} else if (\"0\".split(void 0, 0).length) {\n    String.prototype.split = function split(separator, limit) {\n        if (separator === void 0 && limit === 0) { return []; }\n        return string_split.call(this, separator, limit);\n    };\n}\n\nvar str_replace = String.prototype.replace;\nvar replaceReportsGroupsCorrectly = (function () {\n    var groups = [];\n    'x'.replace(/x(.)?/g, function (match, group) {\n        groups.push(group);\n    });\n    return groups.length === 1 && typeof groups[0] === 'undefined';\n}());\n\nif (!replaceReportsGroupsCorrectly) {\n    String.prototype.replace = function replace(searchValue, replaceValue) {\n        var isFn = isFunction(replaceValue);\n        var hasCapturingGroups = isRegex(searchValue) && (/\\)[*?]/).test(searchValue.source);\n        if (!isFn || !hasCapturingGroups) {\n            return str_replace.call(this, searchValue, replaceValue);\n        } else {\n            var wrappedReplaceValue = function (match) {\n                var length = arguments.length;\n                var originalLastIndex = searchValue.lastIndex;\n                searchValue.lastIndex = 0;\n                var args = searchValue.exec(match);\n                searchValue.lastIndex = originalLastIndex;\n                args.push(arguments[length - 2], arguments[length - 1]);\n                return replaceValue.apply(this, args);\n            };\n            return str_replace.call(this, searchValue, wrappedReplaceValue);\n        }\n    };\n}\n\n// ECMA-262, 3rd B.2.3\n// Not an ECMAScript standard, although ECMAScript 3rd Edition has a\n// non-normative section suggesting uniform semantics and it should be\n// normalized across all browsers\n// [bugfix, IE lt 9] IE < 9 substr() with negative value not working in IE\nif (\"\".substr && \"0b\".substr(-1) !== \"b\") {\n    var string_substr = String.prototype.substr;\n    /**\n     *  Get the substring of a string\n     *  @param  {integer}  start   where to start the substring\n     *  @param  {integer}  length  how many characters to return\n     *  @return {string}\n     */\n    String.prototype.substr = function substr(start, length) {\n        return string_substr.call(\n            this,\n            start < 0 ? ((start = this.length + start) < 0 ? 0 : start) : start,\n            length\n        );\n    };\n}\n\n// ES5 15.5.4.20\n// whitespace from: http://es5.github.io/#x15.5.4.20\nvar ws = \"\\x09\\x0A\\x0B\\x0C\\x0D\\x20\\xA0\\u1680\\u180E\\u2000\\u2001\\u2002\\u2003\" +\n    \"\\u2004\\u2005\\u2006\\u2007\\u2008\\u2009\\u200A\\u202F\\u205F\\u3000\\u2028\" +\n    \"\\u2029\\uFEFF\";\nvar zeroWidth = '\\u200b';\nif (!String.prototype.trim || ws.trim() || !zeroWidth.trim()) {\n    // http://blog.stevenlevithan.com/archives/faster-trim-javascript\n    // http://perfectionkills.com/whitespace-deviations/\n    ws = \"[\" + ws + \"]\";\n    var trimBeginRegexp = new RegExp(\"^\" + ws + ws + \"*\"),\n        trimEndRegexp = new RegExp(ws + ws + \"*$\");\n    String.prototype.trim = function trim() {\n        if (this === void 0 || this === null) {\n            throw new TypeError(\"can't convert \" + this + \" to object\");\n        }\n        return String(this)\n            .replace(trimBeginRegexp, \"\")\n            .replace(trimEndRegexp, \"\");\n    };\n}\n\n// ES-5 15.1.2.2\nif (parseInt(ws + '08') !== 8 || parseInt(ws + '0x16') !== 22) {\n    parseInt = (function (origParseInt) {\n        var hexRegex = /^0[xX]/;\n        return function parseIntES5(str, radix) {\n            str = String(str).trim();\n            if (!Number(radix)) {\n                radix = hexRegex.test(str) ? 16 : 10;\n            }\n            return origParseInt(str, radix);\n        };\n    }(parseInt));\n}\n\n//\n// Util\n// ======\n//\n\n// ES5 9.4\n// http://es5.github.com/#x9.4\n// http://jsperf.com/to-integer\n\nfunction toInteger(n) {\n    n = +n;\n    if (n !== n) { // isNaN\n        n = 0;\n    } else if (n !== 0 && n !== (1 / 0) && n !== -(1 / 0)) {\n        n = (n > 0 || -1) * Math.floor(Math.abs(n));\n    }\n    return n;\n}\n\nfunction isPrimitive(input) {\n    var type = typeof input;\n    return (\n        input === null ||\n        type === \"undefined\" ||\n        type === \"boolean\" ||\n        type === \"number\" ||\n        type === \"string\"\n    );\n}\n\nfunction toPrimitive(input) {\n    var val, valueOf, toStr;\n    if (isPrimitive(input)) {\n        return input;\n    }\n    valueOf = input.valueOf;\n    if (isFunction(valueOf)) {\n        val = valueOf.call(input);\n        if (isPrimitive(val)) {\n            return val;\n        }\n    }\n    toStr = input.toString;\n    if (isFunction(toStr)) {\n        val = toStr.call(input);\n        if (isPrimitive(val)) {\n            return val;\n        }\n    }\n    throw new TypeError();\n}\n\n// ES5 9.9\n// http://es5.github.com/#x9.9\nvar toObject = function (o) {\n    if (o == null) { // this matches both null and undefined\n        throw new TypeError(\"can't convert \" + o + \" to object\");\n    }\n    return Object(o);\n};\n\nvar ToUint32 = function ToUint32(x) {\n    return x >>> 0;\n};\n\n}));\n"
  },
  {
    "path": "r2/r2/public/static/js/lib/event-tracker.js",
    "content": "/**\n * event-tracker\n * @version v1.1.2\n * DO NOT EDIT THIS FILE DIRECTLY! Edit the source at:\n * @source https://github.com/reddit/event-tracker\n */\n!function(global) {\n  'use strict';\n\n  // Aggressively match any non-numeric or alphebetic character. Also catches\n  // utf8, which is probably for the best.\n  var CLIENT_NAME_INVALID_CHARACTERS = /[^A-Za-z0-9]/;\n\n  // Stub out `now` so we can use a more precise number in uuid generation, if\n  // available.\n  function now() {\n    if (global.performance && typeof global.performance.now === 'function') {\n      return global.performance.now();\n    } else if (typeof Date.now === 'function') {\n      return Date.now();\n    } else {\n      return (new Date()).getTime();\n    }\n  }\n\n  // Pulled from elsewhere\n  function uuid(){\n    var d = now();\n\n    var id = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {\n      var r = (d + Math.random()*16) % 16 | 0;\n      d = Math.floor(d / 16);\n      return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);\n    });\n\n    return id;\n  }\n\n  /*\n   * Create a new event tracker.\n   *\n   * clientKey: the name of the secret key you must have to send events, like 'Test1'\n   * clientSecret: the secret key you must have to send events, like 'ab42sdfsafsc'\n   * postData: a function with the object arg ({url, data, query, headers, done}).\n   *   You'll supply a function that wraps jQuery.ajax or superagent.\n   * eventsUrl: the url of the events endpoint, like 'https://stats.redditmedia.com/events'\n   * appName: the name of your client app, like 'Alien Blue'\n   * calculateHash: a function that takes (key, string) and returns an HMAC\n   * config: an object containing optional configuration, such as:\n   *   bufferTimeout: an integer, after which ms, the buffer of events is sent\n   *     to the `postData` function;\n   *   bufferLength: an integer, after which the buffer contains this many\n   *     items, the buffer of events is sent to the `postData` function;\n   */\n  function EventTracker(clientKey, clientSecret, postData, eventsUrl, appName, calculateHash, config) {\n    config = config || {};\n\n    if (!clientKey) {\n      throw('Missing key; pass in event client key as the first argument.');\n    }\n\n    this.clientKey = clientKey;\n\n    if (!clientSecret) {\n      throw('Missing secret; pass in event client secret as the second argument.');\n    }\n\n    this.clientSecret = clientSecret;\n\n    if (!postData) {\n      throw('Missing post function; pass in ajax post function as the third argument.');\n    }\n\n    this.postData = postData;\n\n    if (!eventsUrl) {\n      throw('Missing url to post to; pass in url as the fourth argument.');\n    }\n\n    this.eventsUrl = eventsUrl;\n\n    if (!appName) {\n      throw('Missing appName; pass in appName as the fifth argument.');\n    }\n\n    this.appName = appName;\n\n    if (!calculateHash) {\n      throw('Missing calculateHash; pass in calculateHash as the sixth argument.');\n    }\n\n    this.calculateHash = calculateHash;\n\n    if (typeof window !== 'undefined') {\n      this.appendClientContext =\n        typeof config.appendClientContext === 'undefined' ? true : config.appendClientContext;\n    }\n\n    this.bufferTimeout = config.bufferTimeout || 100;\n    this.bufferLength = config.bufferLength || 40;\n    this.buffer = [];\n  }\n\n  /*\n   * Add an event to the buffer.\n   *\n   * topic: an event topic (such as `mod_events`)\n   * type: an event type for your topic (such as `ban)\n   * data payload: extra data, send whatever your heart desires\n   */\n  EventTracker.prototype.track = function trackEvent (topic, type, payload) {\n    var data = this._buildData(topic, type, payload || {});\n    this._buffer(data);\n  };\n\n  /*\n   * Immediately flush the buffer. Called internally as well during buffer\n   * timeout.\n   * done: optional callback to fire on complete.\n   */\n  EventTracker.prototype.send = function send(done) {\n    if (this.buffer.length) {\n      var data = JSON.stringify(this.buffer);\n\n      var hash = this.calculateHash(this.clientSecret, data);\n\n      var headers = {\n        'Content-Type': 'text/plain',\n      };\n\n      this.postData({\n        url: this.eventsUrl,\n        data: data,\n        headers: headers,\n        query: {\n          key: this.clientKey,\n          mac: hash,\n        },\n        done: done || function() {},\n      });\n\n      this.buffer = [];\n    }\n  };\n\n  EventTracker.prototype._validateClientName = function validateClientName(name) {\n    if (CLIENT_NAME_INVALID_CHARACTERS.test(name)) {\n      throw('Invalid client name, please use only letters or numbers', name);\n    }\n  }\n\n  /*\n   * Internal. Formats a payload to be sent to the event tracker.\n   */\n  EventTracker.prototype._buildData = function buildData (topic, type, payload) {\n    var now = new Date();\n\n    var data = {\n      event_topic: topic,\n      event_type: type,\n      event_ts: now.getTime(),\n      uuid: payload.uuid || uuid(),\n      payload: payload,\n    };\n\n    data.payload.app_name = this.appName;\n    data.payload.utc_offset = now.getTimezoneOffset() / -60;\n\n    if (this.appendClientContext) {\n      var clientContext = this._buildClientContext();\n      for (var c in clientContext) {\n        data.payload[c] = clientContext[c];\n      }\n    }\n\n    return data;\n  };\n\n  /*\n   * Internal. Adds events to the buffer, and flushes if necessary.\n   */\n  EventTracker.prototype._buffer = function buffer(data) {\n    this.buffer.push(data);\n\n    if (this.buffer.length >= this.bufferLength || !this.bufferTimeout) {\n      this.send();\n    } else if (this.bufferTimeout && !this.timer) {\n      this._resetTimer();\n    }\n  }\n\n  /*\n   * Internal. Resets the buffer timeout.\n   */\n  EventTracker.prototype._resetTimer = function resetTimer() {\n    if (this.timer) {\n      clearTimeout(this.timer);\n      this.timer = undefined;\n    }\n\n    var tracker = this;\n    this.timer = setTimeout(function() {\n      tracker.send();\n      tracker.timer = undefined;\n    }, this.bufferTimeout);\n  }\n\n  /*\n   * Internal. Adds certain browser-based properties to the payload if\n   * configured to do so.\n   */\n  EventTracker.prototype._buildClientContext = function buildClientContext () {\n    return {\n      user_agent: navigator.userAgent,\n      domain: document.location.host,\n      base_url: document.location.pathname + document.location.search + document.location.hash,\n    }\n  }\n\n  // Handle npm modules and window globals\n  if (typeof module !== 'undefined') {\n    module.exports = EventTracker;\n  } else {\n    global.EventTracker = EventTracker;\n  }\n}(typeof global !== 'undefined' ? global : this);\n"
  },
  {
    "path": "r2/r2/public/static/js/lib/highlight.pack.js",
    "content": "/*\nCopyright (c) 2006, Ivan Sagalaev\nAll rights reserved.\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n    * Redistributions of source code must retain the above copyright\n      notice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above copyright\n      notice, this list of conditions and the following disclaimer in the\n      documentation and/or other materials provided with the distribution.\n    * Neither the name of highlight.js nor the names of its contributors\n      may be used to endorse or promote products derived from this software\n      without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND ANY\nEXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY\nDIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\nLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND\nON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n*/\n\nvar hljs = new function() {\n\n  /* Utility functions */\n\n  function escape(value) {\n    return value.replace(/&/gm, '&amp;').replace(/</gm, '&lt;');\n  }\n\n  function findCode(pre) {\n    for (var node = pre.firstChild; node; node = node.nextSibling) {\n      if (node.nodeName == 'CODE')\n        return node;\n      if (!(node.nodeType == 3 && node.nodeValue.match(/\\s+/)))\n        break;\n    }\n  }\n\n  function blockText(block, ignoreNewLines) {\n    return Array.prototype.map.call(block.childNodes, function(node) {\n      if (node.nodeType == 3) {\n        return ignoreNewLines ? node.nodeValue.replace(/\\n/g, '') : node.nodeValue;\n      }\n      if (node.nodeName == 'BR') {\n        return '\\n';\n      }\n      return blockText(node, ignoreNewLines);\n    }).join('');\n  }\n\n  function blockLanguage(block) {\n    var classes = (block.className + ' ' + block.parentNode.className).split(/\\s+/);\n    classes = classes.map(function(c) {return c.replace(/^language-/, '')});\n    for (var i = 0; i < classes.length; i++) {\n      if (languages[classes[i]] || classes[i] == 'no-highlight') {\n        return classes[i];\n      }\n    }\n  }\n\n  /* Stream merging */\n\n  function nodeStream(node) {\n    var result = [];\n    (function _nodeStream(node, offset) {\n      for (var child = node.firstChild; child; child = child.nextSibling) {\n        if (child.nodeType == 3)\n          offset += child.nodeValue.length;\n        else if (child.nodeName == 'BR')\n          offset += 1;\n        else if (child.nodeType == 1) {\n          result.push({\n            event: 'start',\n            offset: offset,\n            node: child\n          });\n          offset = _nodeStream(child, offset);\n          result.push({\n            event: 'stop',\n            offset: offset,\n            node: child\n          });\n        }\n      }\n      return offset;\n    })(node, 0);\n    return result;\n  }\n\n  function mergeStreams(stream1, stream2, value) {\n    var processed = 0;\n    var result = '';\n    var nodeStack = [];\n\n    function selectStream() {\n      if (stream1.length && stream2.length) {\n        if (stream1[0].offset != stream2[0].offset)\n          return (stream1[0].offset < stream2[0].offset) ? stream1 : stream2;\n        else {\n          /*\n          To avoid starting the stream just before it should stop the order is\n          ensured that stream1 always starts first and closes last:\n\n          if (event1 == 'start' && event2 == 'start')\n            return stream1;\n          if (event1 == 'start' && event2 == 'stop')\n            return stream2;\n          if (event1 == 'stop' && event2 == 'start')\n            return stream1;\n          if (event1 == 'stop' && event2 == 'stop')\n            return stream2;\n\n          ... which is collapsed to:\n          */\n          return stream2[0].event == 'start' ? stream1 : stream2;\n        }\n      } else {\n        return stream1.length ? stream1 : stream2;\n      }\n    }\n\n    function open(node) {\n      function attr_str(a) {return ' ' + a.nodeName + '=\"' + escape(a.value) + '\"'};\n      return '<' + node.nodeName + Array.prototype.map.call(node.attributes, attr_str).join('') + '>';\n    }\n\n    while (stream1.length || stream2.length) {\n      var current = selectStream().splice(0, 1)[0];\n      result += escape(value.substr(processed, current.offset - processed));\n      processed = current.offset;\n      if ( current.event == 'start') {\n        result += open(current.node);\n        nodeStack.push(current.node);\n      } else if (current.event == 'stop') {\n        var node, i = nodeStack.length;\n        do {\n          i--;\n          node = nodeStack[i];\n          result += ('</' + node.nodeName.toLowerCase() + '>');\n        } while (node != current.node);\n        nodeStack.splice(i, 1);\n        while (i < nodeStack.length) {\n          result += open(nodeStack[i]);\n          i++;\n        }\n      }\n    }\n    return result + escape(value.substr(processed));\n  }\n\n  /* Initialization */\n\n  function compileLanguage(language) {\n\n    function langRe(value, global) {\n      return RegExp(\n        value,\n        'm' + (language.case_insensitive ? 'i' : '') + (global ? 'g' : '')\n      );\n    }\n\n    function compileMode(mode, parent) {\n      if (mode.compiled)\n        return;\n      mode.compiled = true;\n\n      var keywords = []; // used later with beginWithKeyword but filled as a side-effect of keywords compilation\n      if (mode.keywords) {\n        var compiled_keywords = {};\n\n        function flatten(className, str) {\n          str.split(' ').forEach(function(kw) {\n            var pair = kw.split('|');\n            compiled_keywords[pair[0]] = [className, pair[1] ? Number(pair[1]) : 1];\n            keywords.push(pair[0]);\n          });\n        }\n\n        mode.lexemsRe = langRe(mode.lexems || hljs.IDENT_RE, true);\n        if (typeof mode.keywords == 'string') { // string\n          flatten('keyword', mode.keywords)\n        } else {\n          for (var className in mode.keywords) {\n            if (!mode.keywords.hasOwnProperty(className))\n              continue;\n            flatten(className, mode.keywords[className]);\n          }\n        }\n        mode.keywords = compiled_keywords;\n      }\n      if (parent) {\n        if (mode.beginWithKeyword) {\n          mode.begin = '\\\\b(' + keywords.join('|') + ')\\\\s';\n        }\n        mode.beginRe = langRe(mode.begin ? mode.begin : '\\\\B|\\\\b');\n        if (!mode.end && !mode.endsWithParent)\n          mode.end = '\\\\B|\\\\b';\n        if (mode.end)\n          mode.endRe = langRe(mode.end);\n        mode.terminator_end = mode.end || '';\n        if (mode.endsWithParent && parent.terminator_end)\n          mode.terminator_end += (mode.end ? '|' : '') + parent.terminator_end;\n      }\n      if (mode.illegal)\n        mode.illegalRe = langRe(mode.illegal);\n      if (mode.relevance === undefined)\n        mode.relevance = 1;\n      if (!mode.contains) {\n        mode.contains = [];\n      }\n      for (var i = 0; i < mode.contains.length; i++) {\n        if (mode.contains[i] == 'self') {\n          mode.contains[i] = mode;\n        }\n        compileMode(mode.contains[i], mode);\n      }\n      if (mode.starts) {\n        compileMode(mode.starts, parent);\n      }\n\n      var terminators = [];\n      for (var i = 0; i < mode.contains.length; i++) {\n        terminators.push(mode.contains[i].begin);\n      }\n      if (mode.terminator_end) {\n        terminators.push(mode.terminator_end);\n      }\n      if (mode.illegal) {\n        terminators.push(mode.illegal);\n      }\n      mode.terminators = terminators.length ? langRe(terminators.join('|'), true) : {exec: function(s) {return null;}};\n    }\n\n    compileMode(language);\n  }\n\n  /*\n  Core highlighting function. Accepts a language name and a string with the\n  code to highlight. Returns an object with the following properties:\n\n  - relevance (int)\n  - keyword_count (int)\n  - value (an HTML string with highlighting markup)\n\n  */\n  function highlight(language_name, value) {\n\n    function subMode(lexem, mode) {\n      for (var i = 0; i < mode.contains.length; i++) {\n        var match = mode.contains[i].beginRe.exec(lexem);\n        if (match && match.index == 0) {\n          return mode.contains[i];\n        }\n      }\n    }\n\n    function endOfMode(mode, lexem) {\n      if (mode.end && mode.endRe.test(lexem)) {\n        return mode;\n      }\n      if (mode.endsWithParent) {\n        return endOfMode(mode.parent, lexem);\n      }\n    }\n\n    function isIllegal(lexem, mode) {\n      return mode.illegal && mode.illegalRe.test(lexem);\n    }\n\n    function keywordMatch(mode, match) {\n      var match_str = language.case_insensitive ? match[0].toLowerCase() : match[0];\n      return mode.keywords.hasOwnProperty(match_str) && mode.keywords[match_str];\n    }\n\n    function processKeywords() {\n      var buffer = escape(mode_buffer);\n      if (!top.keywords)\n        return buffer;\n      var result = '';\n      var last_index = 0;\n      top.lexemsRe.lastIndex = 0;\n      var match = top.lexemsRe.exec(buffer);\n      while (match) {\n        result += buffer.substr(last_index, match.index - last_index);\n        var keyword_match = keywordMatch(top, match);\n        if (keyword_match) {\n          keyword_count += keyword_match[1];\n          result += '<span class=\"'+ keyword_match[0] +'\">' + match[0] + '</span>';\n        } else {\n          result += match[0];\n        }\n        last_index = top.lexemsRe.lastIndex;\n        match = top.lexemsRe.exec(buffer);\n      }\n      return result + buffer.substr(last_index);\n    }\n\n    function processSubLanguage() {\n      if (top.subLanguage && !languages[top.subLanguage]) {\n        return escape(mode_buffer);\n      }\n      var result = top.subLanguage ? highlight(top.subLanguage, mode_buffer) : highlightAuto(mode_buffer);\n      // Counting embedded language score towards the host language may be disabled\n      // with zeroing the containing mode relevance. Usecase in point is Markdown that\n      // allows XML everywhere and makes every XML snippet to have a much larger Markdown\n      // score.\n      if (top.relevance > 0) {\n        keyword_count += result.keyword_count;\n        relevance += result.relevance;\n      }\n      return '<span class=\"' + result.language  + '\">' + result.value + '</span>';\n    }\n\n    function processBuffer() {\n      return top.subLanguage !== undefined ? processSubLanguage() : processKeywords();\n    }\n\n    function startNewMode(mode, lexem) {\n      var markup = mode.className? '<span class=\"' + mode.className + '\">': '';\n      if (mode.returnBegin) {\n        result += markup;\n        mode_buffer = '';\n      } else if (mode.excludeBegin) {\n        result += escape(lexem) + markup;\n        mode_buffer = '';\n      } else {\n        result += markup;\n        mode_buffer = lexem;\n      }\n      top = Object.create(mode, {parent: {value: top}});\n      relevance += mode.relevance;\n    }\n\n    function processModeInfo(buffer, lexem) {\n      mode_buffer += buffer;\n      if (lexem === undefined) {\n        result += processBuffer();\n        return;\n      }\n\n      var new_mode = subMode(lexem, top);\n      if (new_mode) {\n        result += processBuffer();\n        startNewMode(new_mode, lexem);\n        return new_mode.returnBegin;\n      }\n\n      var end_mode = endOfMode(top, lexem);\n      if (end_mode) {\n        if (!(end_mode.returnEnd || end_mode.excludeEnd)) {\n          mode_buffer += lexem;\n        }\n        result += processBuffer();\n        do {\n          if (top.className) {\n            result += '</span>';\n          }\n          top = top.parent;\n        } while (top != end_mode.parent);\n        if (end_mode.excludeEnd) {\n          result += escape(lexem);\n        }\n        mode_buffer = '';\n        if (end_mode.starts) {\n          startNewMode(end_mode.starts, '');\n        }\n        return end_mode.returnEnd;\n      }\n\n      if (isIllegal(lexem, top))\n        throw 'Illegal';\n    }\n\n    var language = languages[language_name];\n    compileLanguage(language);\n    var top = language;\n    var mode_buffer = '';\n    var relevance = 0;\n    var keyword_count = 0;\n    var result = '';\n    try {\n      var match, index = 0;\n      while (true) {\n        top.terminators.lastIndex = index;\n        match = top.terminators.exec(value);\n        if (!match)\n          break;\n        var return_lexem = processModeInfo(value.substr(index, match.index - index), match[0]);\n        index = match.index + (return_lexem ? 0 : match[0].length);\n      }\n      processModeInfo(value.substr(index), undefined);\n      return {\n        relevance: relevance,\n        keyword_count: keyword_count,\n        value: result,\n        language: language_name\n      };\n    } catch (e) {\n      if (e == 'Illegal') {\n        return {\n          relevance: 0,\n          keyword_count: 0,\n          value: escape(value)\n        };\n      } else {\n        throw e;\n      }\n    }\n  }\n\n  /*\n  Highlighting with language detection. Accepts a string with the code to\n  highlight. Returns an object with the following properties:\n\n  - language (detected language)\n  - relevance (int)\n  - keyword_count (int)\n  - value (an HTML string with highlighting markup)\n  - second_best (object with the same structure for second-best heuristically\n    detected language, may be absent)\n\n  */\n  function highlightAuto(text) {\n    var result = {\n      keyword_count: 0,\n      relevance: 0,\n      value: escape(text)\n    };\n    var second_best = result;\n    for (var key in languages) {\n      if (!languages.hasOwnProperty(key))\n        continue;\n      var current = highlight(key, text);\n      current.language = key;\n      if (current.keyword_count + current.relevance > second_best.keyword_count + second_best.relevance) {\n        second_best = current;\n      }\n      if (current.keyword_count + current.relevance > result.keyword_count + result.relevance) {\n        second_best = result;\n        result = current;\n      }\n    }\n    if (second_best.language) {\n      result.second_best = second_best;\n    }\n    return result;\n  }\n\n  /*\n  Post-processing of the highlighted markup:\n\n  - replace TABs with something more useful\n  - replace real line-breaks with '<br>' for non-pre containers\n\n  */\n  function fixMarkup(value, tabReplace, useBR) {\n    if (tabReplace) {\n      value = value.replace(/^((<[^>]+>|\\t)+)/gm, function(match, p1, offset, s) {\n        return p1.replace(/\\t/g, tabReplace);\n      });\n    }\n    if (useBR) {\n      value = value.replace(/\\n/g, '<br>');\n    }\n    return value;\n  }\n\n  /*\n  Applies highlighting to a DOM node containing code. Accepts a DOM node and\n  two optional parameters for fixMarkup.\n  */\n  function highlightBlock(block, tabReplace, useBR) {\n    var text = blockText(block, useBR);\n    var language = blockLanguage(block);\n    if (language == 'no-highlight')\n        return;\n    var result = language ? highlight(language, text) : highlightAuto(text);\n    language = result.language;\n    var original = nodeStream(block);\n    if (original.length) {\n      var pre = document.createElement('pre');\n      pre.innerHTML = result.value;\n      result.value = mergeStreams(original, nodeStream(pre), text);\n    }\n    result.value = fixMarkup(result.value, tabReplace, useBR);\n\n    var class_name = block.className;\n    if (!class_name.match('(\\\\s|^)(language-)?' + language + '(\\\\s|$)')) {\n      class_name = class_name ? (class_name + ' ' + language) : language;\n    }\n    block.innerHTML = result.value;\n    block.className = class_name;\n    block.result = {\n      language: language,\n      kw: result.keyword_count,\n      re: result.relevance\n    };\n    if (result.second_best) {\n      block.second_best = {\n        language: result.second_best.language,\n        kw: result.second_best.keyword_count,\n        re: result.second_best.relevance\n      };\n    }\n  }\n\n  /*\n  Applies highlighting to all <pre><code>..</code></pre> blocks on a page.\n  */\n  function initHighlighting() {\n    if (initHighlighting.called)\n      return;\n    initHighlighting.called = true;\n    Array.prototype.map.call(document.getElementsByTagName('pre'), findCode).\n      filter(Boolean).\n      forEach(function(code){highlightBlock(code, hljs.tabReplace)});\n  }\n\n  /*\n  Attaches highlighting to the page load event.\n  */\n  function initHighlightingOnLoad() {\n    window.addEventListener('DOMContentLoaded', initHighlighting, false);\n    window.addEventListener('load', initHighlighting, false);\n  }\n\n  var languages = {}; // a shortcut to avoid writing \"this.\" everywhere\n\n  /* Interface definition */\n\n  this.LANGUAGES = languages;\n  this.highlight = highlight;\n  this.highlightAuto = highlightAuto;\n  this.fixMarkup = fixMarkup;\n  this.highlightBlock = highlightBlock;\n  this.initHighlighting = initHighlighting;\n  this.initHighlightingOnLoad = initHighlightingOnLoad;\n\n  // Common regexps\n  this.IDENT_RE = '[a-zA-Z][a-zA-Z0-9_]*';\n  this.UNDERSCORE_IDENT_RE = '[a-zA-Z_][a-zA-Z0-9_]*';\n  this.NUMBER_RE = '\\\\b\\\\d+(\\\\.\\\\d+)?';\n  this.C_NUMBER_RE = '(\\\\b0[xX][a-fA-F0-9]+|(\\\\b\\\\d+(\\\\.\\\\d*)?|\\\\.\\\\d+)([eE][-+]?\\\\d+)?)'; // 0x..., 0..., decimal, float\n  this.BINARY_NUMBER_RE = '\\\\b(0b[01]+)'; // 0b...\n  this.RE_STARTERS_RE = '!|!=|!==|%|%=|&|&&|&=|\\\\*|\\\\*=|\\\\+|\\\\+=|,|\\\\.|-|-=|/|/=|:|;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|\\\\?|\\\\[|\\\\{|\\\\(|\\\\^|\\\\^=|\\\\||\\\\|=|\\\\|\\\\||~';\n\n  // Common modes\n  this.BACKSLASH_ESCAPE = {\n    begin: '\\\\\\\\[\\\\s\\\\S]', relevance: 0\n  };\n  this.APOS_STRING_MODE = {\n    className: 'string',\n    begin: '\\'', end: '\\'',\n    illegal: '\\\\n',\n    contains: [this.BACKSLASH_ESCAPE],\n    relevance: 0\n  };\n  this.QUOTE_STRING_MODE = {\n    className: 'string',\n    begin: '\"', end: '\"',\n    illegal: '\\\\n',\n    contains: [this.BACKSLASH_ESCAPE],\n    relevance: 0\n  };\n  this.C_LINE_COMMENT_MODE = {\n    className: 'comment',\n    begin: '//', end: '$'\n  };\n  this.C_BLOCK_COMMENT_MODE = {\n    className: 'comment',\n    begin: '/\\\\*', end: '\\\\*/'\n  };\n  this.HASH_COMMENT_MODE = {\n    className: 'comment',\n    begin: '#', end: '$'\n  };\n  this.NUMBER_MODE = {\n    className: 'number',\n    begin: this.NUMBER_RE,\n    relevance: 0\n  };\n  this.C_NUMBER_MODE = {\n    className: 'number',\n    begin: this.C_NUMBER_RE,\n    relevance: 0\n  };\n  this.BINARY_NUMBER_MODE = {\n    className: 'number',\n    begin: this.BINARY_NUMBER_RE,\n    relevance: 0\n  };\n\n  // Utility functions\n  this.inherit = function(parent, obj) {\n    var result = {}\n    for (var key in parent)\n      result[key] = parent[key];\n    if (obj)\n      for (var key in obj)\n        result[key] = obj[key];\n    return result;\n  }\n}();\nhljs.LANGUAGES['css'] = function(hljs) {\n  var FUNCTION = {\n    className: 'function',\n    begin: hljs.IDENT_RE + '\\\\(', end: '\\\\)',\n    contains: [hljs.NUMBER_MODE, hljs.APOS_STRING_MODE, hljs.QUOTE_STRING_MODE]\n  };\n  return {\n    case_insensitive: true,\n    illegal: '[=/|\\']',\n    contains: [\n      hljs.C_BLOCK_COMMENT_MODE,\n      {\n        className: 'id', begin: '\\\\#[A-Za-z0-9_-]+'\n      },\n      {\n        className: 'class', begin: '\\\\.[A-Za-z0-9_-]+',\n        relevance: 0\n      },\n      {\n        className: 'attr_selector',\n        begin: '\\\\[', end: '\\\\]',\n        illegal: '$'\n      },\n      {\n        className: 'pseudo',\n        begin: ':(:)?[a-zA-Z0-9\\\\_\\\\-\\\\+\\\\(\\\\)\\\\\"\\\\\\']+'\n      },\n      {\n        className: 'at_rule',\n        begin: '@(font-face|page)',\n        lexems: '[a-z-]+',\n        keywords: 'font-face page'\n      },\n      {\n        className: 'at_rule',\n        begin: '@', end: '[{;]', // at_rule eating first \"{\" is a good thing\n                                 // because it doesn’t let it to be parsed as\n                                 // a rule set but instead drops parser into\n                                 // the default mode which is how it should be.\n        excludeEnd: true,\n        keywords: 'import page media charset',\n        contains: [\n          FUNCTION,\n          hljs.APOS_STRING_MODE, hljs.QUOTE_STRING_MODE,\n          hljs.NUMBER_MODE\n        ]\n      },\n      {\n        className: 'tag', begin: hljs.IDENT_RE,\n        relevance: 0\n      },\n      {\n        className: 'rules',\n        begin: '{', end: '}',\n        illegal: '[^\\\\s]',\n        relevance: 0,\n        contains: [\n          hljs.C_BLOCK_COMMENT_MODE,\n          {\n            className: 'rule',\n            begin: '[^\\\\s]', returnBegin: true, end: ';', endsWithParent: true,\n            contains: [\n              {\n                className: 'attribute',\n                begin: '[A-Z\\\\_\\\\.\\\\-]+', end: ':',\n                excludeEnd: true,\n                illegal: '[^\\\\s]',\n                starts: {\n                  className: 'value',\n                  endsWithParent: true, excludeEnd: true,\n                  contains: [\n                    FUNCTION,\n                    hljs.NUMBER_MODE,\n                    hljs.QUOTE_STRING_MODE,\n                    hljs.APOS_STRING_MODE,\n                    hljs.C_BLOCK_COMMENT_MODE,\n                    {\n                      className: 'hexcolor', begin: '\\\\#[0-9A-F]+'\n                    },\n                    {\n                      className: 'important', begin: '!important'\n                    }\n                  ]\n                }\n              }\n            ]\n          }\n        ]\n      }\n    ]\n  };\n}(hljs);\n"
  },
  {
    "path": "r2/r2/public/static/js/lib/hmac-sha256.js",
    "content": "/*\nCryptoJS v3.1.2\ncode.google.com/p/crypto-js\n(c) 2009-2013 by Jeff Mott. All rights reserved.\ncode.google.com/p/crypto-js/wiki/License\n*/\nvar CryptoJS=CryptoJS||function(h,s){var f={},g=f.lib={},q=function(){},m=g.Base={extend:function(a){q.prototype=this;var c=new q;a&&c.mixIn(a);c.hasOwnProperty(\"init\")||(c.init=function(){c.$super.init.apply(this,arguments)});c.init.prototype=c;c.$super=this;return c},create:function(){var a=this.extend();a.init.apply(a,arguments);return a},init:function(){},mixIn:function(a){for(var c in a)a.hasOwnProperty(c)&&(this[c]=a[c]);a.hasOwnProperty(\"toString\")&&(this.toString=a.toString)},clone:function(){return this.init.prototype.extend(this)}},\nr=g.WordArray=m.extend({init:function(a,c){a=this.words=a||[];this.sigBytes=c!=s?c:4*a.length},toString:function(a){return(a||k).stringify(this)},concat:function(a){var c=this.words,d=a.words,b=this.sigBytes;a=a.sigBytes;this.clamp();if(b%4)for(var e=0;e<a;e++)c[b+e>>>2]|=(d[e>>>2]>>>24-8*(e%4)&255)<<24-8*((b+e)%4);else if(65535<d.length)for(e=0;e<a;e+=4)c[b+e>>>2]=d[e>>>2];else c.push.apply(c,d);this.sigBytes+=a;return this},clamp:function(){var a=this.words,c=this.sigBytes;a[c>>>2]&=4294967295<<\n32-8*(c%4);a.length=h.ceil(c/4)},clone:function(){var a=m.clone.call(this);a.words=this.words.slice(0);return a},random:function(a){for(var c=[],d=0;d<a;d+=4)c.push(4294967296*h.random()|0);return new r.init(c,a)}}),l=f.enc={},k=l.Hex={stringify:function(a){var c=a.words;a=a.sigBytes;for(var d=[],b=0;b<a;b++){var e=c[b>>>2]>>>24-8*(b%4)&255;d.push((e>>>4).toString(16));d.push((e&15).toString(16))}return d.join(\"\")},parse:function(a){for(var c=a.length,d=[],b=0;b<c;b+=2)d[b>>>3]|=parseInt(a.substr(b,\n2),16)<<24-4*(b%8);return new r.init(d,c/2)}},n=l.Latin1={stringify:function(a){var c=a.words;a=a.sigBytes;for(var d=[],b=0;b<a;b++)d.push(String.fromCharCode(c[b>>>2]>>>24-8*(b%4)&255));return d.join(\"\")},parse:function(a){for(var c=a.length,d=[],b=0;b<c;b++)d[b>>>2]|=(a.charCodeAt(b)&255)<<24-8*(b%4);return new r.init(d,c)}},j=l.Utf8={stringify:function(a){try{return decodeURIComponent(escape(n.stringify(a)))}catch(c){throw Error(\"Malformed UTF-8 data\");}},parse:function(a){return n.parse(unescape(encodeURIComponent(a)))}},\nu=g.BufferedBlockAlgorithm=m.extend({reset:function(){this._data=new r.init;this._nDataBytes=0},_append:function(a){\"string\"==typeof a&&(a=j.parse(a));this._data.concat(a);this._nDataBytes+=a.sigBytes},_process:function(a){var c=this._data,d=c.words,b=c.sigBytes,e=this.blockSize,f=b/(4*e),f=a?h.ceil(f):h.max((f|0)-this._minBufferSize,0);a=f*e;b=h.min(4*a,b);if(a){for(var g=0;g<a;g+=e)this._doProcessBlock(d,g);g=d.splice(0,a);c.sigBytes-=b}return new r.init(g,b)},clone:function(){var a=m.clone.call(this);\na._data=this._data.clone();return a},_minBufferSize:0});g.Hasher=u.extend({cfg:m.extend(),init:function(a){this.cfg=this.cfg.extend(a);this.reset()},reset:function(){u.reset.call(this);this._doReset()},update:function(a){this._append(a);this._process();return this},finalize:function(a){a&&this._append(a);return this._doFinalize()},blockSize:16,_createHelper:function(a){return function(c,d){return(new a.init(d)).finalize(c)}},_createHmacHelper:function(a){return function(c,d){return(new t.HMAC.init(a,\nd)).finalize(c)}}});var t=f.algo={};return f}(Math);\n(function(h){for(var s=CryptoJS,f=s.lib,g=f.WordArray,q=f.Hasher,f=s.algo,m=[],r=[],l=function(a){return 4294967296*(a-(a|0))|0},k=2,n=0;64>n;){var j;a:{j=k;for(var u=h.sqrt(j),t=2;t<=u;t++)if(!(j%t)){j=!1;break a}j=!0}j&&(8>n&&(m[n]=l(h.pow(k,0.5))),r[n]=l(h.pow(k,1/3)),n++);k++}var a=[],f=f.SHA256=q.extend({_doReset:function(){this._hash=new g.init(m.slice(0))},_doProcessBlock:function(c,d){for(var b=this._hash.words,e=b[0],f=b[1],g=b[2],j=b[3],h=b[4],m=b[5],n=b[6],q=b[7],p=0;64>p;p++){if(16>p)a[p]=\nc[d+p]|0;else{var k=a[p-15],l=a[p-2];a[p]=((k<<25|k>>>7)^(k<<14|k>>>18)^k>>>3)+a[p-7]+((l<<15|l>>>17)^(l<<13|l>>>19)^l>>>10)+a[p-16]}k=q+((h<<26|h>>>6)^(h<<21|h>>>11)^(h<<7|h>>>25))+(h&m^~h&n)+r[p]+a[p];l=((e<<30|e>>>2)^(e<<19|e>>>13)^(e<<10|e>>>22))+(e&f^e&g^f&g);q=n;n=m;m=h;h=j+k|0;j=g;g=f;f=e;e=k+l|0}b[0]=b[0]+e|0;b[1]=b[1]+f|0;b[2]=b[2]+g|0;b[3]=b[3]+j|0;b[4]=b[4]+h|0;b[5]=b[5]+m|0;b[6]=b[6]+n|0;b[7]=b[7]+q|0},_doFinalize:function(){var a=this._data,d=a.words,b=8*this._nDataBytes,e=8*a.sigBytes;\nd[e>>>5]|=128<<24-e%32;d[(e+64>>>9<<4)+14]=h.floor(b/4294967296);d[(e+64>>>9<<4)+15]=b;a.sigBytes=4*d.length;this._process();return this._hash},clone:function(){var a=q.clone.call(this);a._hash=this._hash.clone();return a}});s.SHA256=q._createHelper(f);s.HmacSHA256=q._createHmacHelper(f)})(Math);\n(function(){var h=CryptoJS,s=h.enc.Utf8;h.algo.HMAC=h.lib.Base.extend({init:function(f,g){f=this._hasher=new f.init;\"string\"==typeof g&&(g=s.parse(g));var h=f.blockSize,m=4*h;g.sigBytes>m&&(g=f.finalize(g));g.clamp();for(var r=this._oKey=g.clone(),l=this._iKey=g.clone(),k=r.words,n=l.words,j=0;j<h;j++)k[j]^=1549556828,n[j]^=909522486;r.sigBytes=l.sigBytes=m;this.reset()},reset:function(){var f=this._hasher;f.reset();f.update(this._iKey)},update:function(f){this._hasher.update(f);return this},finalize:function(f){var g=\nthis._hasher;f=g.finalize(f);g.reset();return g.finalize(this._oKey.clone().concat(f))}})})();\n"
  },
  {
    "path": "r2/r2/public/static/js/lib/html5shiv.js",
    "content": "(function(l,f){function m(){var a=e.elements;return\"string\"==typeof a?a.split(\" \"):a}function i(a){var b=n[a[o]];b||(b={},h++,a[o]=h,n[h]=b);return b}function p(a,b,c){b||(b=f);if(g)return b.createElement(a);c||(c=i(b));b=c.cache[a]?c.cache[a].cloneNode():r.test(a)?(c.cache[a]=c.createElem(a)).cloneNode():c.createElem(a);return b.canHaveChildren&&!s.test(a)?c.frag.appendChild(b):b}function t(a,b){if(!b.cache)b.cache={},b.createElem=a.createElement,b.createFrag=a.createDocumentFragment,b.frag=b.createFrag();\na.createElement=function(c){return!e.shivMethods?b.createElem(c):p(c,a,b)};a.createDocumentFragment=Function(\"h,f\",\"return function(){var n=f.cloneNode(),c=n.createElement;h.shivMethods&&(\"+m().join().replace(/\\w+/g,function(a){b.createElem(a);b.frag.createElement(a);return'c(\"'+a+'\")'})+\");return n}\")(e,b.frag)}function q(a){a||(a=f);var b=i(a);if(e.shivCSS&&!j&&!b.hasCSS){var c,d=a;c=d.createElement(\"p\");d=d.getElementsByTagName(\"head\")[0]||d.documentElement;c.innerHTML=\"x<style>article,aside,figcaption,figure,footer,header,hgroup,nav,section{display:block}mark{background:#FF0;color:#000}</style>\";\nc=d.insertBefore(c.lastChild,d.firstChild);b.hasCSS=!!c}g||t(a,b);return a}var k=l.html5||{},s=/^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i,r=/^<|^(?:a|b|button|code|div|fieldset|form|h1|h2|h3|h4|h5|h6|i|iframe|img|input|label|li|link|ol|option|p|param|q|script|select|span|strong|style|table|tbody|td|textarea|tfoot|th|thead|tr|ul)$/i,j,o=\"_html5shiv\",h=0,n={},g;(function(){try{var a=f.createElement(\"a\");a.innerHTML=\"<xyz></xyz>\";j=\"hidden\"in a;var b;if(!(b=1==a.childNodes.length)){f.createElement(\"a\");\nvar c=f.createDocumentFragment();b=\"undefined\"==typeof c.cloneNode||\"undefined\"==typeof c.createDocumentFragment||\"undefined\"==typeof c.createElement}g=b}catch(d){g=j=!0}})();var e={elements:k.elements||\"abbr article aside audio bdi canvas data datalist details figcaption figure footer header hgroup mark meter nav output progress section summary time video\",shivCSS:!1!==k.shivCSS,supportsUnknownElements:g,shivMethods:!1!==k.shivMethods,type:\"default\",shivDocument:q,createElement:p,createDocumentFragment:function(a,\nb){a||(a=f);if(g)return a.createDocumentFragment();for(var b=b||i(a),c=b.frag.cloneNode(),d=0,e=m(),h=e.length;d<h;d++)c.createElement(e[d]);return c}};l.html5=e;q(f)})(this,document);\n"
  },
  {
    "path": "r2/r2/public/static/js/lib/jed.js",
    "content": "/*\njed.js\nv0.5.0beta\n\nhttps://github.com/SlexAxton/Jed\n-----------\nA gettext compatible i18n library for modern JavaScript Applications\n\nby Alex Sexton - AlexSexton [at] gmail - @SlexAxton\nWTFPL license for use\nDojo CLA for contributions\n\nJed offers the entire applicable GNU gettext spec'd set of\nfunctions, but also offers some nicer wrappers around them.\nThe api for gettext was written for a language with no function\noverloading, so Jed allows a little more of that.\n\nMany thanks to Joshua I. Miller - unrtst@cpan.org - who wrote\ngettext.js back in 2008. I was able to vet a lot of my ideas\nagainst his. I also made sure Jed passed against his tests\nin order to offer easy upgrades -- jsgettext.berlios.de\n*/\n\n// some modifications have been made to work with reddit; they are commented\n// below with a \"reddit\" prefix.\n(function (root, undef) {\n\n  // reddit: *snip* - Jed's miniature underscore.js implementation was removed\n  // from here since we already have full underscore.\n\n  // Jed is a constructor function\n  var Jed = function ( options ) {\n    // Some minimal defaults\n    this.defaults = {\n      \"locale_data\" : {\n        \"messages\" : {\n          \"\" : {\n            \"domain\"       : \"messages\",\n            \"lang\"         : \"en\",\n            \"plural_forms\" : \"(n != 1);\" // reddit: simplified\n          }\n          // There are no default keys, though\n        }\n      },\n      // The default domain if one is missing\n      \"domain\" : \"messages\"\n    };\n\n    // Mix in the sent options with the default options\n    this.options = _.extend( {}, this.defaults, options );\n    this.textdomain( this.options.domain );\n\n    if ( options.domain && ! this.options.locale_data[ this.options.domain ] ) {\n      throw new Error('Text domain set to non-existent domain: `' + options.domain + '`');\n    }\n  };\n\n  // The gettext spec sets this character as the default\n  // delimiter for context lookups.\n  // e.g.: context\\u0004key\n  // If your translation company uses something different,\n  // just change this at any time and it will use that instead.\n  Jed.context_delimiter = String.fromCharCode( 4 );\n\n  // reddit: this function was modified to bypass the (removed) parser\n  var getPluralFormFunc = _.memoize(function( pluralFormString ) {\n    // turns true/false into 1 and 0 for plural indexing\n    function pluralFormsCoerced( pluralFormFunc, n ) {\n      var val = pluralFormFunc(n);\n      return (val === true ? 1 : val ? val : 0);\n    }\n\n    var pluralFormString = pluralFormString || '(n != 1)';\n    return _.partial(pluralFormsCoerced, new Function('n', 'return ' + pluralFormString));\n  })\n\n  // reddit: *snip* - Jed's fluent API was removed since we won't be using it.\n\n  // Add functions to the Jed prototype.\n  // These will be the functions on the object that's returned\n  // from creating a `new Jed()`\n  // These seem redundant, but they gzip pretty well.\n  _.extend( Jed.prototype, {\n    textdomain : function ( domain ) {\n      if ( ! domain ) {\n        return this._textdomain;\n      }\n      this._textdomain = domain;\n    },\n\n    gettext : function ( key ) {\n      return this.dcnpgettext.call( this, undef, undef, key );\n    },\n\n    dgettext : function ( domain, key ) {\n     return this.dcnpgettext.call( this, domain, undef, key );\n    },\n\n    dcgettext : function ( domain , key /*, category */ ) {\n      // Ignores the category anyways\n      return this.dcnpgettext.call( this, domain, undef, key );\n    },\n\n    ngettext : function ( skey, pkey, val ) {\n      return this.dcnpgettext.call( this, undef, undef, skey, pkey, val );\n    },\n\n    dngettext : function ( domain, skey, pkey, val ) {\n      return this.dcnpgettext.call( this, domain, undef, skey, pkey, val );\n    },\n\n    dcngettext : function ( domain, skey, pkey, val/*, category */) {\n      return this.dcnpgettext.call( this, domain, undef, skey, pkey, val );\n    },\n\n    pgettext : function ( context, key ) {\n      return this.dcnpgettext.call( this, undef, context, key );\n    },\n\n    dpgettext : function ( domain, context, key ) {\n      return this.dcnpgettext.call( this, domain, context, key );\n    },\n\n    dcpgettext : function ( domain, context, key/*, category */) {\n      return this.dcnpgettext.call( this, domain, context, key );\n    },\n\n    npgettext : function ( context, skey, pkey, val ) {\n      return this.dcnpgettext.call( this, undef, context, skey, pkey, val );\n    },\n\n    dnpgettext : function ( domain, context, skey, pkey, val ) {\n      return this.dcnpgettext.call( this, domain, context, skey, pkey, val );\n    },\n\n    // The most fully qualified gettext function. It has every option.\n    // Since it has every option, we can use it from every other method.\n    // This is the bread and butter.\n    // Technically there should be one more argument in this function for 'Category',\n    // but since we never use it, we might as well not waste the bytes to define it.\n    dcnpgettext : function ( domain, context, singular_key, plural_key, val ) {\n      // Set some defaults\n\n      plural_key = plural_key || singular_key;\n\n      // Use the global domain default if one\n      // isn't explicitly passed in\n      domain = domain || this._textdomain;\n\n      // Default the value to the singular case\n      val = typeof val == 'undefined' ? 1 : val;\n\n      var fallback;\n\n      // Handle special cases\n\n      // No options found\n      if ( ! this.options ) {\n        // There's likely something wrong, but we'll return the correct key for english\n        // We do this by instantiating a brand new Jed instance with the default set\n        // for everything that could be broken.\n        fallback = new Jed();\n        return fallback.dcnpgettext.call( fallback, undefined, undefined, singular_key, plural_key, val );\n      }\n\n      // No translation data provided\n      if ( ! this.options.locale_data ) {\n        throw new Error('No locale data provided.');\n      }\n\n      if ( ! this.options.locale_data[ domain ] ) {\n        throw new Error('Domain `' + domain + '` was not found.');\n      }\n\n      if ( ! this.options.locale_data[ domain ][ \"\" ] ) {\n        throw new Error('No locale meta information provided.');\n      }\n\n      // Make sure we have a truthy key. Otherwise we might start looking\n      // into the empty string key, which is the options for the locale\n      // data.\n      if ( ! singular_key ) {\n        throw new Error('No translation key found.');\n      }\n\n      // Handle invalid numbers, but try casting strings for good measure\n      if ( typeof val != 'number' ) {\n        val = parseInt( val, 10 );\n\n        if ( isNaN( val ) ) {\n          throw new Error('The number that was passed in is not a number.');\n        }\n      }\n\n      var key  = context ? context + Jed.context_delimiter + singular_key : singular_key,\n          locale_data = this.options.locale_data,\n          dict = locale_data[ domain ],\n          pluralForms = dict[\"\"].plural_forms,\n          val_idx = getPluralFormFunc(pluralForms)(val) + 1,\n          val_list,\n          res;\n\n      // Throw an error if a domain isn't found\n      if ( ! dict ) {\n        throw new Error('No domain named `' + domain + '` could be found.');\n      }\n\n      val_list = dict[ key ];\n\n      // If there is no match, then revert back to\n      // english style singular/plural with the keys passed in.\n      if ( ! val_list || val_idx >= val_list.length ) {\n        if (this.options.missing_key_callback) {\n          this.options.missing_key_callback(key);\n        }\n        res = [ null, singular_key, plural_key ];\n        return res[ getPluralFormFunc()( val ) + 1 ];\n      }\n\n      res = val_list[ val_idx ];\n\n      // This includes empty strings on purpose\n      if ( ! res  ) {\n        res = [ null, singular_key, plural_key ];\n        return res[ getPluralFormFunc()( val ) + 1 ];\n      }\n      return res;\n    }\n  });\n\n  // reddit: *snip* - the Jed-supplied sprintf implementation was removed from here\n\n  // reddit: *snip* - Jed's plural forms expression parser was replaced with an\n  // eval-based system relying on build-time validity checking\n\n  // reddit: *snip* the npm stuff was removed\n  root[\"Jed\"] = Jed;\n})(this);\n"
  },
  {
    "path": "r2/r2/public/static/js/lib/jquery-1.11.1.js",
    "content": "/*!\n * jQuery JavaScript Library v1.11.1\n * http://jquery.com/\n *\n * Includes Sizzle.js\n * http://sizzlejs.com/\n *\n * Copyright 2005, 2014 jQuery Foundation, Inc. and other contributors\n * Released under the MIT license\n * http://jquery.org/license\n *\n * Date: 2014-05-01T17:42Z\n */\n\n(function( global, factory ) {\n\n\tif ( typeof module === \"object\" && typeof module.exports === \"object\" ) {\n\t\t// For CommonJS and CommonJS-like environments where a proper window is present,\n\t\t// execute the factory and get jQuery\n\t\t// For environments that do not inherently posses a window with a document\n\t\t// (such as Node.js), expose a jQuery-making factory as module.exports\n\t\t// This accentuates the need for the creation of a real window\n\t\t// e.g. var jQuery = require(\"jquery\")(window);\n\t\t// See ticket #14549 for more info\n\t\tmodule.exports = global.document ?\n\t\t\tfactory( global, true ) :\n\t\t\tfunction( w ) {\n\t\t\t\tif ( !w.document ) {\n\t\t\t\t\tthrow new Error( \"jQuery requires a window with a document\" );\n\t\t\t\t}\n\t\t\t\treturn factory( w );\n\t\t\t};\n\t} else {\n\t\tfactory( global );\n\t}\n\n// Pass this if window is not defined yet\n}(typeof window !== \"undefined\" ? window : this, function( window, noGlobal ) {\n\n// Can't do this because several apps including ASP.NET trace\n// the stack via arguments.caller.callee and Firefox dies if\n// you try to trace through \"use strict\" call chains. (#13335)\n// Support: Firefox 18+\n//\n\nvar deletedIds = [];\n\nvar slice = deletedIds.slice;\n\nvar concat = deletedIds.concat;\n\nvar push = deletedIds.push;\n\nvar indexOf = deletedIds.indexOf;\n\nvar class2type = {};\n\nvar toString = class2type.toString;\n\nvar hasOwn = class2type.hasOwnProperty;\n\nvar support = {};\n\n\n\nvar\n\tversion = \"1.11.1\",\n\n\t// Define a local copy of jQuery\n\tjQuery = function( selector, context ) {\n\t\t// The jQuery object is actually just the init constructor 'enhanced'\n\t\t// Need init if jQuery is called (just allow error to be thrown if not included)\n\t\treturn new jQuery.fn.init( selector, context );\n\t},\n\n\t// Support: Android<4.1, IE<9\n\t// Make sure we trim BOM and NBSP\n\trtrim = /^[\\s\\uFEFF\\xA0]+|[\\s\\uFEFF\\xA0]+$/g,\n\n\t// Matches dashed string for camelizing\n\trmsPrefix = /^-ms-/,\n\trdashAlpha = /-([\\da-z])/gi,\n\n\t// Used by jQuery.camelCase as callback to replace()\n\tfcamelCase = function( all, letter ) {\n\t\treturn letter.toUpperCase();\n\t};\n\njQuery.fn = jQuery.prototype = {\n\t// The current version of jQuery being used\n\tjquery: version,\n\n\tconstructor: jQuery,\n\n\t// Start with an empty selector\n\tselector: \"\",\n\n\t// The default length of a jQuery object is 0\n\tlength: 0,\n\n\ttoArray: function() {\n\t\treturn slice.call( this );\n\t},\n\n\t// Get the Nth element in the matched element set OR\n\t// Get the whole matched element set as a clean array\n\tget: function( num ) {\n\t\treturn num != null ?\n\n\t\t\t// Return just the one element from the set\n\t\t\t( num < 0 ? this[ num + this.length ] : this[ num ] ) :\n\n\t\t\t// Return all the elements in a clean array\n\t\t\tslice.call( this );\n\t},\n\n\t// Take an array of elements and push it onto the stack\n\t// (returning the new matched element set)\n\tpushStack: function( elems ) {\n\n\t\t// Build a new jQuery matched element set\n\t\tvar ret = jQuery.merge( this.constructor(), elems );\n\n\t\t// Add the old object onto the stack (as a reference)\n\t\tret.prevObject = this;\n\t\tret.context = this.context;\n\n\t\t// Return the newly-formed element set\n\t\treturn ret;\n\t},\n\n\t// Execute a callback for every element in the matched set.\n\t// (You can seed the arguments with an array of args, but this is\n\t// only used internally.)\n\teach: function( callback, args ) {\n\t\treturn jQuery.each( this, callback, args );\n\t},\n\n\tmap: function( callback ) {\n\t\treturn this.pushStack( jQuery.map(this, function( elem, i ) {\n\t\t\treturn callback.call( elem, i, elem );\n\t\t}));\n\t},\n\n\tslice: function() {\n\t\treturn this.pushStack( slice.apply( this, arguments ) );\n\t},\n\n\tfirst: function() {\n\t\treturn this.eq( 0 );\n\t},\n\n\tlast: function() {\n\t\treturn this.eq( -1 );\n\t},\n\n\teq: function( i ) {\n\t\tvar len = this.length,\n\t\t\tj = +i + ( i < 0 ? len : 0 );\n\t\treturn this.pushStack( j >= 0 && j < len ? [ this[j] ] : [] );\n\t},\n\n\tend: function() {\n\t\treturn this.prevObject || this.constructor(null);\n\t},\n\n\t// For internal use only.\n\t// Behaves like an Array's method, not like a jQuery method.\n\tpush: push,\n\tsort: deletedIds.sort,\n\tsplice: deletedIds.splice\n};\n\njQuery.extend = jQuery.fn.extend = function() {\n\tvar src, copyIsArray, copy, name, options, clone,\n\t\ttarget = arguments[0] || {},\n\t\ti = 1,\n\t\tlength = arguments.length,\n\t\tdeep = false;\n\n\t// Handle a deep copy situation\n\tif ( typeof target === \"boolean\" ) {\n\t\tdeep = target;\n\n\t\t// skip the boolean and the target\n\t\ttarget = arguments[ i ] || {};\n\t\ti++;\n\t}\n\n\t// Handle case when target is a string or something (possible in deep copy)\n\tif ( typeof target !== \"object\" && !jQuery.isFunction(target) ) {\n\t\ttarget = {};\n\t}\n\n\t// extend jQuery itself if only one argument is passed\n\tif ( i === length ) {\n\t\ttarget = this;\n\t\ti--;\n\t}\n\n\tfor ( ; i < length; i++ ) {\n\t\t// Only deal with non-null/undefined values\n\t\tif ( (options = arguments[ i ]) != null ) {\n\t\t\t// Extend the base object\n\t\t\tfor ( name in options ) {\n\t\t\t\tsrc = target[ name ];\n\t\t\t\tcopy = options[ name ];\n\n\t\t\t\t// Prevent never-ending loop\n\t\t\t\tif ( target === copy ) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t// Recurse if we're merging plain objects or arrays\n\t\t\t\tif ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) {\n\t\t\t\t\tif ( copyIsArray ) {\n\t\t\t\t\t\tcopyIsArray = false;\n\t\t\t\t\t\tclone = src && jQuery.isArray(src) ? src : [];\n\n\t\t\t\t\t} else {\n\t\t\t\t\t\tclone = src && jQuery.isPlainObject(src) ? src : {};\n\t\t\t\t\t}\n\n\t\t\t\t\t// Never move original objects, clone them\n\t\t\t\t\ttarget[ name ] = jQuery.extend( deep, clone, copy );\n\n\t\t\t\t// Don't bring in undefined values\n\t\t\t\t} else if ( copy !== undefined ) {\n\t\t\t\t\ttarget[ name ] = copy;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Return the modified object\n\treturn target;\n};\n\njQuery.extend({\n\t// Unique for each copy of jQuery on the page\n\texpando: \"jQuery\" + ( version + Math.random() ).replace( /\\D/g, \"\" ),\n\n\t// Assume jQuery is ready without the ready module\n\tisReady: true,\n\n\terror: function( msg ) {\n\t\tthrow new Error( msg );\n\t},\n\n\tnoop: function() {},\n\n\t// See test/unit/core.js for details concerning isFunction.\n\t// Since version 1.3, DOM methods and functions like alert\n\t// aren't supported. They return false on IE (#2968).\n\tisFunction: function( obj ) {\n\t\treturn jQuery.type(obj) === \"function\";\n\t},\n\n\tisArray: Array.isArray || function( obj ) {\n\t\treturn jQuery.type(obj) === \"array\";\n\t},\n\n\tisWindow: function( obj ) {\n\t\t/* jshint eqeqeq: false */\n\t\treturn obj != null && obj == obj.window;\n\t},\n\n\tisNumeric: function( obj ) {\n\t\t// parseFloat NaNs numeric-cast false positives (null|true|false|\"\")\n\t\t// ...but misinterprets leading-number strings, particularly hex literals (\"0x...\")\n\t\t// subtraction forces infinities to NaN\n\t\treturn !jQuery.isArray( obj ) && obj - parseFloat( obj ) >= 0;\n\t},\n\n\tisEmptyObject: function( obj ) {\n\t\tvar name;\n\t\tfor ( name in obj ) {\n\t\t\treturn false;\n\t\t}\n\t\treturn true;\n\t},\n\n\tisPlainObject: function( obj ) {\n\t\tvar key;\n\n\t\t// Must be an Object.\n\t\t// Because of IE, we also have to check the presence of the constructor property.\n\t\t// Make sure that DOM nodes and window objects don't pass through, as well\n\t\tif ( !obj || jQuery.type(obj) !== \"object\" || obj.nodeType || jQuery.isWindow( obj ) ) {\n\t\t\treturn false;\n\t\t}\n\n\t\ttry {\n\t\t\t// Not own constructor property must be Object\n\t\t\tif ( obj.constructor &&\n\t\t\t\t!hasOwn.call(obj, \"constructor\") &&\n\t\t\t\t!hasOwn.call(obj.constructor.prototype, \"isPrototypeOf\") ) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t} catch ( e ) {\n\t\t\t// IE8,9 Will throw exceptions on certain host objects #9897\n\t\t\treturn false;\n\t\t}\n\n\t\t// Support: IE<9\n\t\t// Handle iteration over inherited properties before own properties.\n\t\tif ( support.ownLast ) {\n\t\t\tfor ( key in obj ) {\n\t\t\t\treturn hasOwn.call( obj, key );\n\t\t\t}\n\t\t}\n\n\t\t// Own properties are enumerated firstly, so to speed up,\n\t\t// if last one is own, then all properties are own.\n\t\tfor ( key in obj ) {}\n\n\t\treturn key === undefined || hasOwn.call( obj, key );\n\t},\n\n\ttype: function( obj ) {\n\t\tif ( obj == null ) {\n\t\t\treturn obj + \"\";\n\t\t}\n\t\treturn typeof obj === \"object\" || typeof obj === \"function\" ?\n\t\t\tclass2type[ toString.call(obj) ] || \"object\" :\n\t\t\ttypeof obj;\n\t},\n\n\t// Evaluates a script in a global context\n\t// Workarounds based on findings by Jim Driscoll\n\t// http://weblogs.java.net/blog/driscoll/archive/2009/09/08/eval-javascript-global-context\n\tglobalEval: function( data ) {\n\t\tif ( data && jQuery.trim( data ) ) {\n\t\t\t// We use execScript on Internet Explorer\n\t\t\t// We use an anonymous function so that context is window\n\t\t\t// rather than jQuery in Firefox\n\t\t\t( window.execScript || function( data ) {\n\t\t\t\twindow[ \"eval\" ].call( window, data );\n\t\t\t} )( data );\n\t\t}\n\t},\n\n\t// Convert dashed to camelCase; used by the css and data modules\n\t// Microsoft forgot to hump their vendor prefix (#9572)\n\tcamelCase: function( string ) {\n\t\treturn string.replace( rmsPrefix, \"ms-\" ).replace( rdashAlpha, fcamelCase );\n\t},\n\n\tnodeName: function( elem, name ) {\n\t\treturn elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase();\n\t},\n\n\t// args is for internal usage only\n\teach: function( obj, callback, args ) {\n\t\tvar value,\n\t\t\ti = 0,\n\t\t\tlength = obj.length,\n\t\t\tisArray = isArraylike( obj );\n\n\t\tif ( args ) {\n\t\t\tif ( isArray ) {\n\t\t\t\tfor ( ; i < length; i++ ) {\n\t\t\t\t\tvalue = callback.apply( obj[ i ], args );\n\n\t\t\t\t\tif ( value === false ) {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tfor ( i in obj ) {\n\t\t\t\t\tvalue = callback.apply( obj[ i ], args );\n\n\t\t\t\t\tif ( value === false ) {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t// A special, fast, case for the most common use of each\n\t\t} else {\n\t\t\tif ( isArray ) {\n\t\t\t\tfor ( ; i < length; i++ ) {\n\t\t\t\t\tvalue = callback.call( obj[ i ], i, obj[ i ] );\n\n\t\t\t\t\tif ( value === false ) {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tfor ( i in obj ) {\n\t\t\t\t\tvalue = callback.call( obj[ i ], i, obj[ i ] );\n\n\t\t\t\t\tif ( value === false ) {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn obj;\n\t},\n\n\t// Support: Android<4.1, IE<9\n\ttrim: function( text ) {\n\t\treturn text == null ?\n\t\t\t\"\" :\n\t\t\t( text + \"\" ).replace( rtrim, \"\" );\n\t},\n\n\t// results is for internal usage only\n\tmakeArray: function( arr, results ) {\n\t\tvar ret = results || [];\n\n\t\tif ( arr != null ) {\n\t\t\tif ( isArraylike( Object(arr) ) ) {\n\t\t\t\tjQuery.merge( ret,\n\t\t\t\t\ttypeof arr === \"string\" ?\n\t\t\t\t\t[ arr ] : arr\n\t\t\t\t);\n\t\t\t} else {\n\t\t\t\tpush.call( ret, arr );\n\t\t\t}\n\t\t}\n\n\t\treturn ret;\n\t},\n\n\tinArray: function( elem, arr, i ) {\n\t\tvar len;\n\n\t\tif ( arr ) {\n\t\t\tif ( indexOf ) {\n\t\t\t\treturn indexOf.call( arr, elem, i );\n\t\t\t}\n\n\t\t\tlen = arr.length;\n\t\t\ti = i ? i < 0 ? Math.max( 0, len + i ) : i : 0;\n\n\t\t\tfor ( ; i < len; i++ ) {\n\t\t\t\t// Skip accessing in sparse arrays\n\t\t\t\tif ( i in arr && arr[ i ] === elem ) {\n\t\t\t\t\treturn i;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn -1;\n\t},\n\n\tmerge: function( first, second ) {\n\t\tvar len = +second.length,\n\t\t\tj = 0,\n\t\t\ti = first.length;\n\n\t\twhile ( j < len ) {\n\t\t\tfirst[ i++ ] = second[ j++ ];\n\t\t}\n\n\t\t// Support: IE<9\n\t\t// Workaround casting of .length to NaN on otherwise arraylike objects (e.g., NodeLists)\n\t\tif ( len !== len ) {\n\t\t\twhile ( second[j] !== undefined ) {\n\t\t\t\tfirst[ i++ ] = second[ j++ ];\n\t\t\t}\n\t\t}\n\n\t\tfirst.length = i;\n\n\t\treturn first;\n\t},\n\n\tgrep: function( elems, callback, invert ) {\n\t\tvar callbackInverse,\n\t\t\tmatches = [],\n\t\t\ti = 0,\n\t\t\tlength = elems.length,\n\t\t\tcallbackExpect = !invert;\n\n\t\t// Go through the array, only saving the items\n\t\t// that pass the validator function\n\t\tfor ( ; i < length; i++ ) {\n\t\t\tcallbackInverse = !callback( elems[ i ], i );\n\t\t\tif ( callbackInverse !== callbackExpect ) {\n\t\t\t\tmatches.push( elems[ i ] );\n\t\t\t}\n\t\t}\n\n\t\treturn matches;\n\t},\n\n\t// arg is for internal usage only\n\tmap: function( elems, callback, arg ) {\n\t\tvar value,\n\t\t\ti = 0,\n\t\t\tlength = elems.length,\n\t\t\tisArray = isArraylike( elems ),\n\t\t\tret = [];\n\n\t\t// Go through the array, translating each of the items to their new values\n\t\tif ( isArray ) {\n\t\t\tfor ( ; i < length; i++ ) {\n\t\t\t\tvalue = callback( elems[ i ], i, arg );\n\n\t\t\t\tif ( value != null ) {\n\t\t\t\t\tret.push( value );\n\t\t\t\t}\n\t\t\t}\n\n\t\t// Go through every key on the object,\n\t\t} else {\n\t\t\tfor ( i in elems ) {\n\t\t\t\tvalue = callback( elems[ i ], i, arg );\n\n\t\t\t\tif ( value != null ) {\n\t\t\t\t\tret.push( value );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Flatten any nested arrays\n\t\treturn concat.apply( [], ret );\n\t},\n\n\t// A global GUID counter for objects\n\tguid: 1,\n\n\t// Bind a function to a context, optionally partially applying any\n\t// arguments.\n\tproxy: function( fn, context ) {\n\t\tvar args, proxy, tmp;\n\n\t\tif ( typeof context === \"string\" ) {\n\t\t\ttmp = fn[ context ];\n\t\t\tcontext = fn;\n\t\t\tfn = tmp;\n\t\t}\n\n\t\t// Quick check to determine if target is callable, in the spec\n\t\t// this throws a TypeError, but we will just return undefined.\n\t\tif ( !jQuery.isFunction( fn ) ) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\t// Simulated bind\n\t\targs = slice.call( arguments, 2 );\n\t\tproxy = function() {\n\t\t\treturn fn.apply( context || this, args.concat( slice.call( arguments ) ) );\n\t\t};\n\n\t\t// Set the guid of unique handler to the same of original handler, so it can be removed\n\t\tproxy.guid = fn.guid = fn.guid || jQuery.guid++;\n\n\t\treturn proxy;\n\t},\n\n\tnow: function() {\n\t\treturn +( new Date() );\n\t},\n\n\t// jQuery.support is not used in Core but other projects attach their\n\t// properties to it so it needs to exist.\n\tsupport: support\n});\n\n// Populate the class2type map\njQuery.each(\"Boolean Number String Function Array Date RegExp Object Error\".split(\" \"), function(i, name) {\n\tclass2type[ \"[object \" + name + \"]\" ] = name.toLowerCase();\n});\n\nfunction isArraylike( obj ) {\n\tvar length = obj.length,\n\t\ttype = jQuery.type( obj );\n\n\tif ( type === \"function\" || jQuery.isWindow( obj ) ) {\n\t\treturn false;\n\t}\n\n\tif ( obj.nodeType === 1 && length ) {\n\t\treturn true;\n\t}\n\n\treturn type === \"array\" || length === 0 ||\n\t\ttypeof length === \"number\" && length > 0 && ( length - 1 ) in obj;\n}\nvar Sizzle =\n/*!\n * Sizzle CSS Selector Engine v1.10.19\n * http://sizzlejs.com/\n *\n * Copyright 2013 jQuery Foundation, Inc. and other contributors\n * Released under the MIT license\n * http://jquery.org/license\n *\n * Date: 2014-04-18\n */\n(function( window ) {\n\nvar i,\n\tsupport,\n\tExpr,\n\tgetText,\n\tisXML,\n\ttokenize,\n\tcompile,\n\tselect,\n\toutermostContext,\n\tsortInput,\n\thasDuplicate,\n\n\t// Local document vars\n\tsetDocument,\n\tdocument,\n\tdocElem,\n\tdocumentIsHTML,\n\trbuggyQSA,\n\trbuggyMatches,\n\tmatches,\n\tcontains,\n\n\t// Instance-specific data\n\texpando = \"sizzle\" + -(new Date()),\n\tpreferredDoc = window.document,\n\tdirruns = 0,\n\tdone = 0,\n\tclassCache = createCache(),\n\ttokenCache = createCache(),\n\tcompilerCache = createCache(),\n\tsortOrder = function( a, b ) {\n\t\tif ( a === b ) {\n\t\t\thasDuplicate = true;\n\t\t}\n\t\treturn 0;\n\t},\n\n\t// General-purpose constants\n\tstrundefined = typeof undefined,\n\tMAX_NEGATIVE = 1 << 31,\n\n\t// Instance methods\n\thasOwn = ({}).hasOwnProperty,\n\tarr = [],\n\tpop = arr.pop,\n\tpush_native = arr.push,\n\tpush = arr.push,\n\tslice = arr.slice,\n\t// Use a stripped-down indexOf if we can't use a native one\n\tindexOf = arr.indexOf || function( elem ) {\n\t\tvar i = 0,\n\t\t\tlen = this.length;\n\t\tfor ( ; i < len; i++ ) {\n\t\t\tif ( this[i] === elem ) {\n\t\t\t\treturn i;\n\t\t\t}\n\t\t}\n\t\treturn -1;\n\t},\n\n\tbooleans = \"checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped\",\n\n\t// Regular expressions\n\n\t// Whitespace characters http://www.w3.org/TR/css3-selectors/#whitespace\n\twhitespace = \"[\\\\x20\\\\t\\\\r\\\\n\\\\f]\",\n\t// http://www.w3.org/TR/css3-syntax/#characters\n\tcharacterEncoding = \"(?:\\\\\\\\.|[\\\\w-]|[^\\\\x00-\\\\xa0])+\",\n\n\t// Loosely modeled on CSS identifier characters\n\t// An unquoted value should be a CSS identifier http://www.w3.org/TR/css3-selectors/#attribute-selectors\n\t// Proper syntax: http://www.w3.org/TR/CSS21/syndata.html#value-def-identifier\n\tidentifier = characterEncoding.replace( \"w\", \"w#\" ),\n\n\t// Attribute selectors: http://www.w3.org/TR/selectors/#attribute-selectors\n\tattributes = \"\\\\[\" + whitespace + \"*(\" + characterEncoding + \")(?:\" + whitespace +\n\t\t// Operator (capture 2)\n\t\t\"*([*^$|!~]?=)\" + whitespace +\n\t\t// \"Attribute values must be CSS identifiers [capture 5] or strings [capture 3 or capture 4]\"\n\t\t\"*(?:'((?:\\\\\\\\.|[^\\\\\\\\'])*)'|\\\"((?:\\\\\\\\.|[^\\\\\\\\\\\"])*)\\\"|(\" + identifier + \"))|)\" + whitespace +\n\t\t\"*\\\\]\",\n\n\tpseudos = \":(\" + characterEncoding + \")(?:\\\\((\" +\n\t\t// To reduce the number of selectors needing tokenize in the preFilter, prefer arguments:\n\t\t// 1. quoted (capture 3; capture 4 or capture 5)\n\t\t\"('((?:\\\\\\\\.|[^\\\\\\\\'])*)'|\\\"((?:\\\\\\\\.|[^\\\\\\\\\\\"])*)\\\")|\" +\n\t\t// 2. simple (capture 6)\n\t\t\"((?:\\\\\\\\.|[^\\\\\\\\()[\\\\]]|\" + attributes + \")*)|\" +\n\t\t// 3. anything else (capture 2)\n\t\t\".*\" +\n\t\t\")\\\\)|)\",\n\n\t// Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter\n\trtrim = new RegExp( \"^\" + whitespace + \"+|((?:^|[^\\\\\\\\])(?:\\\\\\\\.)*)\" + whitespace + \"+$\", \"g\" ),\n\n\trcomma = new RegExp( \"^\" + whitespace + \"*,\" + whitespace + \"*\" ),\n\trcombinators = new RegExp( \"^\" + whitespace + \"*([>+~]|\" + whitespace + \")\" + whitespace + \"*\" ),\n\n\trattributeQuotes = new RegExp( \"=\" + whitespace + \"*([^\\\\]'\\\"]*?)\" + whitespace + \"*\\\\]\", \"g\" ),\n\n\trpseudo = new RegExp( pseudos ),\n\tridentifier = new RegExp( \"^\" + identifier + \"$\" ),\n\n\tmatchExpr = {\n\t\t\"ID\": new RegExp( \"^#(\" + characterEncoding + \")\" ),\n\t\t\"CLASS\": new RegExp( \"^\\\\.(\" + characterEncoding + \")\" ),\n\t\t\"TAG\": new RegExp( \"^(\" + characterEncoding.replace( \"w\", \"w*\" ) + \")\" ),\n\t\t\"ATTR\": new RegExp( \"^\" + attributes ),\n\t\t\"PSEUDO\": new RegExp( \"^\" + pseudos ),\n\t\t\"CHILD\": new RegExp( \"^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\\\(\" + whitespace +\n\t\t\t\"*(even|odd|(([+-]|)(\\\\d*)n|)\" + whitespace + \"*(?:([+-]|)\" + whitespace +\n\t\t\t\"*(\\\\d+)|))\" + whitespace + \"*\\\\)|)\", \"i\" ),\n\t\t\"bool\": new RegExp( \"^(?:\" + booleans + \")$\", \"i\" ),\n\t\t// For use in libraries implementing .is()\n\t\t// We use this for POS matching in `select`\n\t\t\"needsContext\": new RegExp( \"^\" + whitespace + \"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\\\(\" +\n\t\t\twhitespace + \"*((?:-\\\\d)?\\\\d*)\" + whitespace + \"*\\\\)|)(?=[^-]|$)\", \"i\" )\n\t},\n\n\trinputs = /^(?:input|select|textarea|button)$/i,\n\trheader = /^h\\d$/i,\n\n\trnative = /^[^{]+\\{\\s*\\[native \\w/,\n\n\t// Easily-parseable/retrievable ID or TAG or CLASS selectors\n\trquickExpr = /^(?:#([\\w-]+)|(\\w+)|\\.([\\w-]+))$/,\n\n\trsibling = /[+~]/,\n\trescape = /'|\\\\/g,\n\n\t// CSS escapes http://www.w3.org/TR/CSS21/syndata.html#escaped-characters\n\trunescape = new RegExp( \"\\\\\\\\([\\\\da-f]{1,6}\" + whitespace + \"?|(\" + whitespace + \")|.)\", \"ig\" ),\n\tfunescape = function( _, escaped, escapedWhitespace ) {\n\t\tvar high = \"0x\" + escaped - 0x10000;\n\t\t// NaN means non-codepoint\n\t\t// Support: Firefox<24\n\t\t// Workaround erroneous numeric interpretation of +\"0x\"\n\t\treturn high !== high || escapedWhitespace ?\n\t\t\tescaped :\n\t\t\thigh < 0 ?\n\t\t\t\t// BMP codepoint\n\t\t\t\tString.fromCharCode( high + 0x10000 ) :\n\t\t\t\t// Supplemental Plane codepoint (surrogate pair)\n\t\t\t\tString.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 );\n\t};\n\n// Optimize for push.apply( _, NodeList )\ntry {\n\tpush.apply(\n\t\t(arr = slice.call( preferredDoc.childNodes )),\n\t\tpreferredDoc.childNodes\n\t);\n\t// Support: Android<4.0\n\t// Detect silently failing push.apply\n\tarr[ preferredDoc.childNodes.length ].nodeType;\n} catch ( e ) {\n\tpush = { apply: arr.length ?\n\n\t\t// Leverage slice if possible\n\t\tfunction( target, els ) {\n\t\t\tpush_native.apply( target, slice.call(els) );\n\t\t} :\n\n\t\t// Support: IE<9\n\t\t// Otherwise append directly\n\t\tfunction( target, els ) {\n\t\t\tvar j = target.length,\n\t\t\t\ti = 0;\n\t\t\t// Can't trust NodeList.length\n\t\t\twhile ( (target[j++] = els[i++]) ) {}\n\t\t\ttarget.length = j - 1;\n\t\t}\n\t};\n}\n\nfunction Sizzle( selector, context, results, seed ) {\n\tvar match, elem, m, nodeType,\n\t\t// QSA vars\n\t\ti, groups, old, nid, newContext, newSelector;\n\n\tif ( ( context ? context.ownerDocument || context : preferredDoc ) !== document ) {\n\t\tsetDocument( context );\n\t}\n\n\tcontext = context || document;\n\tresults = results || [];\n\n\tif ( !selector || typeof selector !== \"string\" ) {\n\t\treturn results;\n\t}\n\n\tif ( (nodeType = context.nodeType) !== 1 && nodeType !== 9 ) {\n\t\treturn [];\n\t}\n\n\tif ( documentIsHTML && !seed ) {\n\n\t\t// Shortcuts\n\t\tif ( (match = rquickExpr.exec( selector )) ) {\n\t\t\t// Speed-up: Sizzle(\"#ID\")\n\t\t\tif ( (m = match[1]) ) {\n\t\t\t\tif ( nodeType === 9 ) {\n\t\t\t\t\telem = context.getElementById( m );\n\t\t\t\t\t// Check parentNode to catch when Blackberry 4.6 returns\n\t\t\t\t\t// nodes that are no longer in the document (jQuery #6963)\n\t\t\t\t\tif ( elem && elem.parentNode ) {\n\t\t\t\t\t\t// Handle the case where IE, Opera, and Webkit return items\n\t\t\t\t\t\t// by name instead of ID\n\t\t\t\t\t\tif ( elem.id === m ) {\n\t\t\t\t\t\t\tresults.push( elem );\n\t\t\t\t\t\t\treturn results;\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\treturn results;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// Context is not a document\n\t\t\t\t\tif ( context.ownerDocument && (elem = context.ownerDocument.getElementById( m )) &&\n\t\t\t\t\t\tcontains( context, elem ) && elem.id === m ) {\n\t\t\t\t\t\tresults.push( elem );\n\t\t\t\t\t\treturn results;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t// Speed-up: Sizzle(\"TAG\")\n\t\t\t} else if ( match[2] ) {\n\t\t\t\tpush.apply( results, context.getElementsByTagName( selector ) );\n\t\t\t\treturn results;\n\n\t\t\t// Speed-up: Sizzle(\".CLASS\")\n\t\t\t} else if ( (m = match[3]) && support.getElementsByClassName && context.getElementsByClassName ) {\n\t\t\t\tpush.apply( results, context.getElementsByClassName( m ) );\n\t\t\t\treturn results;\n\t\t\t}\n\t\t}\n\n\t\t// QSA path\n\t\tif ( support.qsa && (!rbuggyQSA || !rbuggyQSA.test( selector )) ) {\n\t\t\tnid = old = expando;\n\t\t\tnewContext = context;\n\t\t\tnewSelector = nodeType === 9 && selector;\n\n\t\t\t// qSA works strangely on Element-rooted queries\n\t\t\t// We can work around this by specifying an extra ID on the root\n\t\t\t// and working up from there (Thanks to Andrew Dupont for the technique)\n\t\t\t// IE 8 doesn't work on object elements\n\t\t\tif ( nodeType === 1 && context.nodeName.toLowerCase() !== \"object\" ) {\n\t\t\t\tgroups = tokenize( selector );\n\n\t\t\t\tif ( (old = context.getAttribute(\"id\")) ) {\n\t\t\t\t\tnid = old.replace( rescape, \"\\\\$&\" );\n\t\t\t\t} else {\n\t\t\t\t\tcontext.setAttribute( \"id\", nid );\n\t\t\t\t}\n\t\t\t\tnid = \"[id='\" + nid + \"'] \";\n\n\t\t\t\ti = groups.length;\n\t\t\t\twhile ( i-- ) {\n\t\t\t\t\tgroups[i] = nid + toSelector( groups[i] );\n\t\t\t\t}\n\t\t\t\tnewContext = rsibling.test( selector ) && testContext( context.parentNode ) || context;\n\t\t\t\tnewSelector = groups.join(\",\");\n\t\t\t}\n\n\t\t\tif ( newSelector ) {\n\t\t\t\ttry {\n\t\t\t\t\tpush.apply( results,\n\t\t\t\t\t\tnewContext.querySelectorAll( newSelector )\n\t\t\t\t\t);\n\t\t\t\t\treturn results;\n\t\t\t\t} catch(qsaError) {\n\t\t\t\t} finally {\n\t\t\t\t\tif ( !old ) {\n\t\t\t\t\t\tcontext.removeAttribute(\"id\");\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// All others\n\treturn select( selector.replace( rtrim, \"$1\" ), context, results, seed );\n}\n\n/**\n * Create key-value caches of limited size\n * @returns {Function(string, Object)} Returns the Object data after storing it on itself with\n *\tproperty name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength)\n *\tdeleting the oldest entry\n */\nfunction createCache() {\n\tvar keys = [];\n\n\tfunction cache( key, value ) {\n\t\t// Use (key + \" \") to avoid collision with native prototype properties (see Issue #157)\n\t\tif ( keys.push( key + \" \" ) > Expr.cacheLength ) {\n\t\t\t// Only keep the most recent entries\n\t\t\tdelete cache[ keys.shift() ];\n\t\t}\n\t\treturn (cache[ key + \" \" ] = value);\n\t}\n\treturn cache;\n}\n\n/**\n * Mark a function for special use by Sizzle\n * @param {Function} fn The function to mark\n */\nfunction markFunction( fn ) {\n\tfn[ expando ] = true;\n\treturn fn;\n}\n\n/**\n * Support testing using an element\n * @param {Function} fn Passed the created div and expects a boolean result\n */\nfunction assert( fn ) {\n\tvar div = document.createElement(\"div\");\n\n\ttry {\n\t\treturn !!fn( div );\n\t} catch (e) {\n\t\treturn false;\n\t} finally {\n\t\t// Remove from its parent by default\n\t\tif ( div.parentNode ) {\n\t\t\tdiv.parentNode.removeChild( div );\n\t\t}\n\t\t// release memory in IE\n\t\tdiv = null;\n\t}\n}\n\n/**\n * Adds the same handler for all of the specified attrs\n * @param {String} attrs Pipe-separated list of attributes\n * @param {Function} handler The method that will be applied\n */\nfunction addHandle( attrs, handler ) {\n\tvar arr = attrs.split(\"|\"),\n\t\ti = attrs.length;\n\n\twhile ( i-- ) {\n\t\tExpr.attrHandle[ arr[i] ] = handler;\n\t}\n}\n\n/**\n * Checks document order of two siblings\n * @param {Element} a\n * @param {Element} b\n * @returns {Number} Returns less than 0 if a precedes b, greater than 0 if a follows b\n */\nfunction siblingCheck( a, b ) {\n\tvar cur = b && a,\n\t\tdiff = cur && a.nodeType === 1 && b.nodeType === 1 &&\n\t\t\t( ~b.sourceIndex || MAX_NEGATIVE ) -\n\t\t\t( ~a.sourceIndex || MAX_NEGATIVE );\n\n\t// Use IE sourceIndex if available on both nodes\n\tif ( diff ) {\n\t\treturn diff;\n\t}\n\n\t// Check if b follows a\n\tif ( cur ) {\n\t\twhile ( (cur = cur.nextSibling) ) {\n\t\t\tif ( cur === b ) {\n\t\t\t\treturn -1;\n\t\t\t}\n\t\t}\n\t}\n\n\treturn a ? 1 : -1;\n}\n\n/**\n * Returns a function to use in pseudos for input types\n * @param {String} type\n */\nfunction createInputPseudo( type ) {\n\treturn function( elem ) {\n\t\tvar name = elem.nodeName.toLowerCase();\n\t\treturn name === \"input\" && elem.type === type;\n\t};\n}\n\n/**\n * Returns a function to use in pseudos for buttons\n * @param {String} type\n */\nfunction createButtonPseudo( type ) {\n\treturn function( elem ) {\n\t\tvar name = elem.nodeName.toLowerCase();\n\t\treturn (name === \"input\" || name === \"button\") && elem.type === type;\n\t};\n}\n\n/**\n * Returns a function to use in pseudos for positionals\n * @param {Function} fn\n */\nfunction createPositionalPseudo( fn ) {\n\treturn markFunction(function( argument ) {\n\t\targument = +argument;\n\t\treturn markFunction(function( seed, matches ) {\n\t\t\tvar j,\n\t\t\t\tmatchIndexes = fn( [], seed.length, argument ),\n\t\t\t\ti = matchIndexes.length;\n\n\t\t\t// Match elements found at the specified indexes\n\t\t\twhile ( i-- ) {\n\t\t\t\tif ( seed[ (j = matchIndexes[i]) ] ) {\n\t\t\t\t\tseed[j] = !(matches[j] = seed[j]);\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t});\n}\n\n/**\n * Checks a node for validity as a Sizzle context\n * @param {Element|Object=} context\n * @returns {Element|Object|Boolean} The input node if acceptable, otherwise a falsy value\n */\nfunction testContext( context ) {\n\treturn context && typeof context.getElementsByTagName !== strundefined && context;\n}\n\n// Expose support vars for convenience\nsupport = Sizzle.support = {};\n\n/**\n * Detects XML nodes\n * @param {Element|Object} elem An element or a document\n * @returns {Boolean} True iff elem is a non-HTML XML node\n */\nisXML = Sizzle.isXML = function( elem ) {\n\t// documentElement is verified for cases where it doesn't yet exist\n\t// (such as loading iframes in IE - #4833)\n\tvar documentElement = elem && (elem.ownerDocument || elem).documentElement;\n\treturn documentElement ? documentElement.nodeName !== \"HTML\" : false;\n};\n\n/**\n * Sets document-related variables once based on the current document\n * @param {Element|Object} [doc] An element or document object to use to set the document\n * @returns {Object} Returns the current document\n */\nsetDocument = Sizzle.setDocument = function( node ) {\n\tvar hasCompare,\n\t\tdoc = node ? node.ownerDocument || node : preferredDoc,\n\t\tparent = doc.defaultView;\n\n\t// If no document and documentElement is available, return\n\tif ( doc === document || doc.nodeType !== 9 || !doc.documentElement ) {\n\t\treturn document;\n\t}\n\n\t// Set our document\n\tdocument = doc;\n\tdocElem = doc.documentElement;\n\n\t// Support tests\n\tdocumentIsHTML = !isXML( doc );\n\n\t// Support: IE>8\n\t// If iframe document is assigned to \"document\" variable and if iframe has been reloaded,\n\t// IE will throw \"permission denied\" error when accessing \"document\" variable, see jQuery #13936\n\t// IE6-8 do not support the defaultView property so parent will be undefined\n\tif ( parent && parent !== parent.top ) {\n\t\t// IE11 does not have attachEvent, so all must suffer\n\t\tif ( parent.addEventListener ) {\n\t\t\tparent.addEventListener( \"unload\", function() {\n\t\t\t\tsetDocument();\n\t\t\t}, false );\n\t\t} else if ( parent.attachEvent ) {\n\t\t\tparent.attachEvent( \"onunload\", function() {\n\t\t\t\tsetDocument();\n\t\t\t});\n\t\t}\n\t}\n\n\t/* Attributes\n\t---------------------------------------------------------------------- */\n\n\t// Support: IE<8\n\t// Verify that getAttribute really returns attributes and not properties (excepting IE8 booleans)\n\tsupport.attributes = assert(function( div ) {\n\t\tdiv.className = \"i\";\n\t\treturn !div.getAttribute(\"className\");\n\t});\n\n\t/* getElement(s)By*\n\t---------------------------------------------------------------------- */\n\n\t// Check if getElementsByTagName(\"*\") returns only elements\n\tsupport.getElementsByTagName = assert(function( div ) {\n\t\tdiv.appendChild( doc.createComment(\"\") );\n\t\treturn !div.getElementsByTagName(\"*\").length;\n\t});\n\n\t// Check if getElementsByClassName can be trusted\n\tsupport.getElementsByClassName = rnative.test( doc.getElementsByClassName ) && assert(function( div ) {\n\t\tdiv.innerHTML = \"<div class='a'></div><div class='a i'></div>\";\n\n\t\t// Support: Safari<4\n\t\t// Catch class over-caching\n\t\tdiv.firstChild.className = \"i\";\n\t\t// Support: Opera<10\n\t\t// Catch gEBCN failure to find non-leading classes\n\t\treturn div.getElementsByClassName(\"i\").length === 2;\n\t});\n\n\t// Support: IE<10\n\t// Check if getElementById returns elements by name\n\t// The broken getElementById methods don't pick up programatically-set names,\n\t// so use a roundabout getElementsByName test\n\tsupport.getById = assert(function( div ) {\n\t\tdocElem.appendChild( div ).id = expando;\n\t\treturn !doc.getElementsByName || !doc.getElementsByName( expando ).length;\n\t});\n\n\t// ID find and filter\n\tif ( support.getById ) {\n\t\tExpr.find[\"ID\"] = function( id, context ) {\n\t\t\tif ( typeof context.getElementById !== strundefined && documentIsHTML ) {\n\t\t\t\tvar m = context.getElementById( id );\n\t\t\t\t// Check parentNode to catch when Blackberry 4.6 returns\n\t\t\t\t// nodes that are no longer in the document #6963\n\t\t\t\treturn m && m.parentNode ? [ m ] : [];\n\t\t\t}\n\t\t};\n\t\tExpr.filter[\"ID\"] = function( id ) {\n\t\t\tvar attrId = id.replace( runescape, funescape );\n\t\t\treturn function( elem ) {\n\t\t\t\treturn elem.getAttribute(\"id\") === attrId;\n\t\t\t};\n\t\t};\n\t} else {\n\t\t// Support: IE6/7\n\t\t// getElementById is not reliable as a find shortcut\n\t\tdelete Expr.find[\"ID\"];\n\n\t\tExpr.filter[\"ID\"] =  function( id ) {\n\t\t\tvar attrId = id.replace( runescape, funescape );\n\t\t\treturn function( elem ) {\n\t\t\t\tvar node = typeof elem.getAttributeNode !== strundefined && elem.getAttributeNode(\"id\");\n\t\t\t\treturn node && node.value === attrId;\n\t\t\t};\n\t\t};\n\t}\n\n\t// Tag\n\tExpr.find[\"TAG\"] = support.getElementsByTagName ?\n\t\tfunction( tag, context ) {\n\t\t\tif ( typeof context.getElementsByTagName !== strundefined ) {\n\t\t\t\treturn context.getElementsByTagName( tag );\n\t\t\t}\n\t\t} :\n\t\tfunction( tag, context ) {\n\t\t\tvar elem,\n\t\t\t\ttmp = [],\n\t\t\t\ti = 0,\n\t\t\t\tresults = context.getElementsByTagName( tag );\n\n\t\t\t// Filter out possible comments\n\t\t\tif ( tag === \"*\" ) {\n\t\t\t\twhile ( (elem = results[i++]) ) {\n\t\t\t\t\tif ( elem.nodeType === 1 ) {\n\t\t\t\t\t\ttmp.push( elem );\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\treturn tmp;\n\t\t\t}\n\t\t\treturn results;\n\t\t};\n\n\t// Class\n\tExpr.find[\"CLASS\"] = support.getElementsByClassName && function( className, context ) {\n\t\tif ( typeof context.getElementsByClassName !== strundefined && documentIsHTML ) {\n\t\t\treturn context.getElementsByClassName( className );\n\t\t}\n\t};\n\n\t/* QSA/matchesSelector\n\t---------------------------------------------------------------------- */\n\n\t// QSA and matchesSelector support\n\n\t// matchesSelector(:active) reports false when true (IE9/Opera 11.5)\n\trbuggyMatches = [];\n\n\t// qSa(:focus) reports false when true (Chrome 21)\n\t// We allow this because of a bug in IE8/9 that throws an error\n\t// whenever `document.activeElement` is accessed on an iframe\n\t// So, we allow :focus to pass through QSA all the time to avoid the IE error\n\t// See http://bugs.jquery.com/ticket/13378\n\trbuggyQSA = [];\n\n\tif ( (support.qsa = rnative.test( doc.querySelectorAll )) ) {\n\t\t// Build QSA regex\n\t\t// Regex strategy adopted from Diego Perini\n\t\tassert(function( div ) {\n\t\t\t// Select is set to empty string on purpose\n\t\t\t// This is to test IE's treatment of not explicitly\n\t\t\t// setting a boolean content attribute,\n\t\t\t// since its presence should be enough\n\t\t\t// http://bugs.jquery.com/ticket/12359\n\t\t\tdiv.innerHTML = \"<select msallowclip=''><option selected=''></option></select>\";\n\n\t\t\t// Support: IE8, Opera 11-12.16\n\t\t\t// Nothing should be selected when empty strings follow ^= or $= or *=\n\t\t\t// The test attribute must be unknown in Opera but \"safe\" for WinRT\n\t\t\t// http://msdn.microsoft.com/en-us/library/ie/hh465388.aspx#attribute_section\n\t\t\tif ( div.querySelectorAll(\"[msallowclip^='']\").length ) {\n\t\t\t\trbuggyQSA.push( \"[*^$]=\" + whitespace + \"*(?:''|\\\"\\\")\" );\n\t\t\t}\n\n\t\t\t// Support: IE8\n\t\t\t// Boolean attributes and \"value\" are not treated correctly\n\t\t\tif ( !div.querySelectorAll(\"[selected]\").length ) {\n\t\t\t\trbuggyQSA.push( \"\\\\[\" + whitespace + \"*(?:value|\" + booleans + \")\" );\n\t\t\t}\n\n\t\t\t// Webkit/Opera - :checked should return selected option elements\n\t\t\t// http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked\n\t\t\t// IE8 throws error here and will not see later tests\n\t\t\tif ( !div.querySelectorAll(\":checked\").length ) {\n\t\t\t\trbuggyQSA.push(\":checked\");\n\t\t\t}\n\t\t});\n\n\t\tassert(function( div ) {\n\t\t\t// Support: Windows 8 Native Apps\n\t\t\t// The type and name attributes are restricted during .innerHTML assignment\n\t\t\tvar input = doc.createElement(\"input\");\n\t\t\tinput.setAttribute( \"type\", \"hidden\" );\n\t\t\tdiv.appendChild( input ).setAttribute( \"name\", \"D\" );\n\n\t\t\t// Support: IE8\n\t\t\t// Enforce case-sensitivity of name attribute\n\t\t\tif ( div.querySelectorAll(\"[name=d]\").length ) {\n\t\t\t\trbuggyQSA.push( \"name\" + whitespace + \"*[*^$|!~]?=\" );\n\t\t\t}\n\n\t\t\t// FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled)\n\t\t\t// IE8 throws error here and will not see later tests\n\t\t\tif ( !div.querySelectorAll(\":enabled\").length ) {\n\t\t\t\trbuggyQSA.push( \":enabled\", \":disabled\" );\n\t\t\t}\n\n\t\t\t// Opera 10-11 does not throw on post-comma invalid pseudos\n\t\t\tdiv.querySelectorAll(\"*,:x\");\n\t\t\trbuggyQSA.push(\",.*:\");\n\t\t});\n\t}\n\n\tif ( (support.matchesSelector = rnative.test( (matches = docElem.matches ||\n\t\tdocElem.webkitMatchesSelector ||\n\t\tdocElem.mozMatchesSelector ||\n\t\tdocElem.oMatchesSelector ||\n\t\tdocElem.msMatchesSelector) )) ) {\n\n\t\tassert(function( div ) {\n\t\t\t// Check to see if it's possible to do matchesSelector\n\t\t\t// on a disconnected node (IE 9)\n\t\t\tsupport.disconnectedMatch = matches.call( div, \"div\" );\n\n\t\t\t// This should fail with an exception\n\t\t\t// Gecko does not error, returns false instead\n\t\t\tmatches.call( div, \"[s!='']:x\" );\n\t\t\trbuggyMatches.push( \"!=\", pseudos );\n\t\t});\n\t}\n\n\trbuggyQSA = rbuggyQSA.length && new RegExp( rbuggyQSA.join(\"|\") );\n\trbuggyMatches = rbuggyMatches.length && new RegExp( rbuggyMatches.join(\"|\") );\n\n\t/* Contains\n\t---------------------------------------------------------------------- */\n\thasCompare = rnative.test( docElem.compareDocumentPosition );\n\n\t// Element contains another\n\t// Purposefully does not implement inclusive descendent\n\t// As in, an element does not contain itself\n\tcontains = hasCompare || rnative.test( docElem.contains ) ?\n\t\tfunction( a, b ) {\n\t\t\tvar adown = a.nodeType === 9 ? a.documentElement : a,\n\t\t\t\tbup = b && b.parentNode;\n\t\t\treturn a === bup || !!( bup && bup.nodeType === 1 && (\n\t\t\t\tadown.contains ?\n\t\t\t\t\tadown.contains( bup ) :\n\t\t\t\t\ta.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16\n\t\t\t));\n\t\t} :\n\t\tfunction( a, b ) {\n\t\t\tif ( b ) {\n\t\t\t\twhile ( (b = b.parentNode) ) {\n\t\t\t\t\tif ( b === a ) {\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false;\n\t\t};\n\n\t/* Sorting\n\t---------------------------------------------------------------------- */\n\n\t// Document order sorting\n\tsortOrder = hasCompare ?\n\tfunction( a, b ) {\n\n\t\t// Flag for duplicate removal\n\t\tif ( a === b ) {\n\t\t\thasDuplicate = true;\n\t\t\treturn 0;\n\t\t}\n\n\t\t// Sort on method existence if only one input has compareDocumentPosition\n\t\tvar compare = !a.compareDocumentPosition - !b.compareDocumentPosition;\n\t\tif ( compare ) {\n\t\t\treturn compare;\n\t\t}\n\n\t\t// Calculate position if both inputs belong to the same document\n\t\tcompare = ( a.ownerDocument || a ) === ( b.ownerDocument || b ) ?\n\t\t\ta.compareDocumentPosition( b ) :\n\n\t\t\t// Otherwise we know they are disconnected\n\t\t\t1;\n\n\t\t// Disconnected nodes\n\t\tif ( compare & 1 ||\n\t\t\t(!support.sortDetached && b.compareDocumentPosition( a ) === compare) ) {\n\n\t\t\t// Choose the first element that is related to our preferred document\n\t\t\tif ( a === doc || a.ownerDocument === preferredDoc && contains(preferredDoc, a) ) {\n\t\t\t\treturn -1;\n\t\t\t}\n\t\t\tif ( b === doc || b.ownerDocument === preferredDoc && contains(preferredDoc, b) ) {\n\t\t\t\treturn 1;\n\t\t\t}\n\n\t\t\t// Maintain original order\n\t\t\treturn sortInput ?\n\t\t\t\t( indexOf.call( sortInput, a ) - indexOf.call( sortInput, b ) ) :\n\t\t\t\t0;\n\t\t}\n\n\t\treturn compare & 4 ? -1 : 1;\n\t} :\n\tfunction( a, b ) {\n\t\t// Exit early if the nodes are identical\n\t\tif ( a === b ) {\n\t\t\thasDuplicate = true;\n\t\t\treturn 0;\n\t\t}\n\n\t\tvar cur,\n\t\t\ti = 0,\n\t\t\taup = a.parentNode,\n\t\t\tbup = b.parentNode,\n\t\t\tap = [ a ],\n\t\t\tbp = [ b ];\n\n\t\t// Parentless nodes are either documents or disconnected\n\t\tif ( !aup || !bup ) {\n\t\t\treturn a === doc ? -1 :\n\t\t\t\tb === doc ? 1 :\n\t\t\t\taup ? -1 :\n\t\t\t\tbup ? 1 :\n\t\t\t\tsortInput ?\n\t\t\t\t( indexOf.call( sortInput, a ) - indexOf.call( sortInput, b ) ) :\n\t\t\t\t0;\n\n\t\t// If the nodes are siblings, we can do a quick check\n\t\t} else if ( aup === bup ) {\n\t\t\treturn siblingCheck( a, b );\n\t\t}\n\n\t\t// Otherwise we need full lists of their ancestors for comparison\n\t\tcur = a;\n\t\twhile ( (cur = cur.parentNode) ) {\n\t\t\tap.unshift( cur );\n\t\t}\n\t\tcur = b;\n\t\twhile ( (cur = cur.parentNode) ) {\n\t\t\tbp.unshift( cur );\n\t\t}\n\n\t\t// Walk down the tree looking for a discrepancy\n\t\twhile ( ap[i] === bp[i] ) {\n\t\t\ti++;\n\t\t}\n\n\t\treturn i ?\n\t\t\t// Do a sibling check if the nodes have a common ancestor\n\t\t\tsiblingCheck( ap[i], bp[i] ) :\n\n\t\t\t// Otherwise nodes in our document sort first\n\t\t\tap[i] === preferredDoc ? -1 :\n\t\t\tbp[i] === preferredDoc ? 1 :\n\t\t\t0;\n\t};\n\n\treturn doc;\n};\n\nSizzle.matches = function( expr, elements ) {\n\treturn Sizzle( expr, null, null, elements );\n};\n\nSizzle.matchesSelector = function( elem, expr ) {\n\t// Set document vars if needed\n\tif ( ( elem.ownerDocument || elem ) !== document ) {\n\t\tsetDocument( elem );\n\t}\n\n\t// Make sure that attribute selectors are quoted\n\texpr = expr.replace( rattributeQuotes, \"='$1']\" );\n\n\tif ( support.matchesSelector && documentIsHTML &&\n\t\t( !rbuggyMatches || !rbuggyMatches.test( expr ) ) &&\n\t\t( !rbuggyQSA     || !rbuggyQSA.test( expr ) ) ) {\n\n\t\ttry {\n\t\t\tvar ret = matches.call( elem, expr );\n\n\t\t\t// IE 9's matchesSelector returns false on disconnected nodes\n\t\t\tif ( ret || support.disconnectedMatch ||\n\t\t\t\t\t// As well, disconnected nodes are said to be in a document\n\t\t\t\t\t// fragment in IE 9\n\t\t\t\t\telem.document && elem.document.nodeType !== 11 ) {\n\t\t\t\treturn ret;\n\t\t\t}\n\t\t} catch(e) {}\n\t}\n\n\treturn Sizzle( expr, document, null, [ elem ] ).length > 0;\n};\n\nSizzle.contains = function( context, elem ) {\n\t// Set document vars if needed\n\tif ( ( context.ownerDocument || context ) !== document ) {\n\t\tsetDocument( context );\n\t}\n\treturn contains( context, elem );\n};\n\nSizzle.attr = function( elem, name ) {\n\t// Set document vars if needed\n\tif ( ( elem.ownerDocument || elem ) !== document ) {\n\t\tsetDocument( elem );\n\t}\n\n\tvar fn = Expr.attrHandle[ name.toLowerCase() ],\n\t\t// Don't get fooled by Object.prototype properties (jQuery #13807)\n\t\tval = fn && hasOwn.call( Expr.attrHandle, name.toLowerCase() ) ?\n\t\t\tfn( elem, name, !documentIsHTML ) :\n\t\t\tundefined;\n\n\treturn val !== undefined ?\n\t\tval :\n\t\tsupport.attributes || !documentIsHTML ?\n\t\t\telem.getAttribute( name ) :\n\t\t\t(val = elem.getAttributeNode(name)) && val.specified ?\n\t\t\t\tval.value :\n\t\t\t\tnull;\n};\n\nSizzle.error = function( msg ) {\n\tthrow new Error( \"Syntax error, unrecognized expression: \" + msg );\n};\n\n/**\n * Document sorting and removing duplicates\n * @param {ArrayLike} results\n */\nSizzle.uniqueSort = function( results ) {\n\tvar elem,\n\t\tduplicates = [],\n\t\tj = 0,\n\t\ti = 0;\n\n\t// Unless we *know* we can detect duplicates, assume their presence\n\thasDuplicate = !support.detectDuplicates;\n\tsortInput = !support.sortStable && results.slice( 0 );\n\tresults.sort( sortOrder );\n\n\tif ( hasDuplicate ) {\n\t\twhile ( (elem = results[i++]) ) {\n\t\t\tif ( elem === results[ i ] ) {\n\t\t\t\tj = duplicates.push( i );\n\t\t\t}\n\t\t}\n\t\twhile ( j-- ) {\n\t\t\tresults.splice( duplicates[ j ], 1 );\n\t\t}\n\t}\n\n\t// Clear input after sorting to release objects\n\t// See https://github.com/jquery/sizzle/pull/225\n\tsortInput = null;\n\n\treturn results;\n};\n\n/**\n * Utility function for retrieving the text value of an array of DOM nodes\n * @param {Array|Element} elem\n */\ngetText = Sizzle.getText = function( elem ) {\n\tvar node,\n\t\tret = \"\",\n\t\ti = 0,\n\t\tnodeType = elem.nodeType;\n\n\tif ( !nodeType ) {\n\t\t// If no nodeType, this is expected to be an array\n\t\twhile ( (node = elem[i++]) ) {\n\t\t\t// Do not traverse comment nodes\n\t\t\tret += getText( node );\n\t\t}\n\t} else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) {\n\t\t// Use textContent for elements\n\t\t// innerText usage removed for consistency of new lines (jQuery #11153)\n\t\tif ( typeof elem.textContent === \"string\" ) {\n\t\t\treturn elem.textContent;\n\t\t} else {\n\t\t\t// Traverse its children\n\t\t\tfor ( elem = elem.firstChild; elem; elem = elem.nextSibling ) {\n\t\t\t\tret += getText( elem );\n\t\t\t}\n\t\t}\n\t} else if ( nodeType === 3 || nodeType === 4 ) {\n\t\treturn elem.nodeValue;\n\t}\n\t// Do not include comment or processing instruction nodes\n\n\treturn ret;\n};\n\nExpr = Sizzle.selectors = {\n\n\t// Can be adjusted by the user\n\tcacheLength: 50,\n\n\tcreatePseudo: markFunction,\n\n\tmatch: matchExpr,\n\n\tattrHandle: {},\n\n\tfind: {},\n\n\trelative: {\n\t\t\">\": { dir: \"parentNode\", first: true },\n\t\t\" \": { dir: \"parentNode\" },\n\t\t\"+\": { dir: \"previousSibling\", first: true },\n\t\t\"~\": { dir: \"previousSibling\" }\n\t},\n\n\tpreFilter: {\n\t\t\"ATTR\": function( match ) {\n\t\t\tmatch[1] = match[1].replace( runescape, funescape );\n\n\t\t\t// Move the given value to match[3] whether quoted or unquoted\n\t\t\tmatch[3] = ( match[3] || match[4] || match[5] || \"\" ).replace( runescape, funescape );\n\n\t\t\tif ( match[2] === \"~=\" ) {\n\t\t\t\tmatch[3] = \" \" + match[3] + \" \";\n\t\t\t}\n\n\t\t\treturn match.slice( 0, 4 );\n\t\t},\n\n\t\t\"CHILD\": function( match ) {\n\t\t\t/* matches from matchExpr[\"CHILD\"]\n\t\t\t\t1 type (only|nth|...)\n\t\t\t\t2 what (child|of-type)\n\t\t\t\t3 argument (even|odd|\\d*|\\d*n([+-]\\d+)?|...)\n\t\t\t\t4 xn-component of xn+y argument ([+-]?\\d*n|)\n\t\t\t\t5 sign of xn-component\n\t\t\t\t6 x of xn-component\n\t\t\t\t7 sign of y-component\n\t\t\t\t8 y of y-component\n\t\t\t*/\n\t\t\tmatch[1] = match[1].toLowerCase();\n\n\t\t\tif ( match[1].slice( 0, 3 ) === \"nth\" ) {\n\t\t\t\t// nth-* requires argument\n\t\t\t\tif ( !match[3] ) {\n\t\t\t\t\tSizzle.error( match[0] );\n\t\t\t\t}\n\n\t\t\t\t// numeric x and y parameters for Expr.filter.CHILD\n\t\t\t\t// remember that false/true cast respectively to 0/1\n\t\t\t\tmatch[4] = +( match[4] ? match[5] + (match[6] || 1) : 2 * ( match[3] === \"even\" || match[3] === \"odd\" ) );\n\t\t\t\tmatch[5] = +( ( match[7] + match[8] ) || match[3] === \"odd\" );\n\n\t\t\t// other types prohibit arguments\n\t\t\t} else if ( match[3] ) {\n\t\t\t\tSizzle.error( match[0] );\n\t\t\t}\n\n\t\t\treturn match;\n\t\t},\n\n\t\t\"PSEUDO\": function( match ) {\n\t\t\tvar excess,\n\t\t\t\tunquoted = !match[6] && match[2];\n\n\t\t\tif ( matchExpr[\"CHILD\"].test( match[0] ) ) {\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\t// Accept quoted arguments as-is\n\t\t\tif ( match[3] ) {\n\t\t\t\tmatch[2] = match[4] || match[5] || \"\";\n\n\t\t\t// Strip excess characters from unquoted arguments\n\t\t\t} else if ( unquoted && rpseudo.test( unquoted ) &&\n\t\t\t\t// Get excess from tokenize (recursively)\n\t\t\t\t(excess = tokenize( unquoted, true )) &&\n\t\t\t\t// advance to the next closing parenthesis\n\t\t\t\t(excess = unquoted.indexOf( \")\", unquoted.length - excess ) - unquoted.length) ) {\n\n\t\t\t\t// excess is a negative index\n\t\t\t\tmatch[0] = match[0].slice( 0, excess );\n\t\t\t\tmatch[2] = unquoted.slice( 0, excess );\n\t\t\t}\n\n\t\t\t// Return only captures needed by the pseudo filter method (type and argument)\n\t\t\treturn match.slice( 0, 3 );\n\t\t}\n\t},\n\n\tfilter: {\n\n\t\t\"TAG\": function( nodeNameSelector ) {\n\t\t\tvar nodeName = nodeNameSelector.replace( runescape, funescape ).toLowerCase();\n\t\t\treturn nodeNameSelector === \"*\" ?\n\t\t\t\tfunction() { return true; } :\n\t\t\t\tfunction( elem ) {\n\t\t\t\t\treturn elem.nodeName && elem.nodeName.toLowerCase() === nodeName;\n\t\t\t\t};\n\t\t},\n\n\t\t\"CLASS\": function( className ) {\n\t\t\tvar pattern = classCache[ className + \" \" ];\n\n\t\t\treturn pattern ||\n\t\t\t\t(pattern = new RegExp( \"(^|\" + whitespace + \")\" + className + \"(\" + whitespace + \"|$)\" )) &&\n\t\t\t\tclassCache( className, function( elem ) {\n\t\t\t\t\treturn pattern.test( typeof elem.className === \"string\" && elem.className || typeof elem.getAttribute !== strundefined && elem.getAttribute(\"class\") || \"\" );\n\t\t\t\t});\n\t\t},\n\n\t\t\"ATTR\": function( name, operator, check ) {\n\t\t\treturn function( elem ) {\n\t\t\t\tvar result = Sizzle.attr( elem, name );\n\n\t\t\t\tif ( result == null ) {\n\t\t\t\t\treturn operator === \"!=\";\n\t\t\t\t}\n\t\t\t\tif ( !operator ) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\n\t\t\t\tresult += \"\";\n\n\t\t\t\treturn operator === \"=\" ? result === check :\n\t\t\t\t\toperator === \"!=\" ? result !== check :\n\t\t\t\t\toperator === \"^=\" ? check && result.indexOf( check ) === 0 :\n\t\t\t\t\toperator === \"*=\" ? check && result.indexOf( check ) > -1 :\n\t\t\t\t\toperator === \"$=\" ? check && result.slice( -check.length ) === check :\n\t\t\t\t\toperator === \"~=\" ? ( \" \" + result + \" \" ).indexOf( check ) > -1 :\n\t\t\t\t\toperator === \"|=\" ? result === check || result.slice( 0, check.length + 1 ) === check + \"-\" :\n\t\t\t\t\tfalse;\n\t\t\t};\n\t\t},\n\n\t\t\"CHILD\": function( type, what, argument, first, last ) {\n\t\t\tvar simple = type.slice( 0, 3 ) !== \"nth\",\n\t\t\t\tforward = type.slice( -4 ) !== \"last\",\n\t\t\t\tofType = what === \"of-type\";\n\n\t\t\treturn first === 1 && last === 0 ?\n\n\t\t\t\t// Shortcut for :nth-*(n)\n\t\t\t\tfunction( elem ) {\n\t\t\t\t\treturn !!elem.parentNode;\n\t\t\t\t} :\n\n\t\t\t\tfunction( elem, context, xml ) {\n\t\t\t\t\tvar cache, outerCache, node, diff, nodeIndex, start,\n\t\t\t\t\t\tdir = simple !== forward ? \"nextSibling\" : \"previousSibling\",\n\t\t\t\t\t\tparent = elem.parentNode,\n\t\t\t\t\t\tname = ofType && elem.nodeName.toLowerCase(),\n\t\t\t\t\t\tuseCache = !xml && !ofType;\n\n\t\t\t\t\tif ( parent ) {\n\n\t\t\t\t\t\t// :(first|last|only)-(child|of-type)\n\t\t\t\t\t\tif ( simple ) {\n\t\t\t\t\t\t\twhile ( dir ) {\n\t\t\t\t\t\t\t\tnode = elem;\n\t\t\t\t\t\t\t\twhile ( (node = node[ dir ]) ) {\n\t\t\t\t\t\t\t\t\tif ( ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1 ) {\n\t\t\t\t\t\t\t\t\t\treturn false;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t// Reverse direction for :only-* (if we haven't yet done so)\n\t\t\t\t\t\t\t\tstart = dir = type === \"only\" && !start && \"nextSibling\";\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn true;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tstart = [ forward ? parent.firstChild : parent.lastChild ];\n\n\t\t\t\t\t\t// non-xml :nth-child(...) stores cache data on `parent`\n\t\t\t\t\t\tif ( forward && useCache ) {\n\t\t\t\t\t\t\t// Seek `elem` from a previously-cached index\n\t\t\t\t\t\t\touterCache = parent[ expando ] || (parent[ expando ] = {});\n\t\t\t\t\t\t\tcache = outerCache[ type ] || [];\n\t\t\t\t\t\t\tnodeIndex = cache[0] === dirruns && cache[1];\n\t\t\t\t\t\t\tdiff = cache[0] === dirruns && cache[2];\n\t\t\t\t\t\t\tnode = nodeIndex && parent.childNodes[ nodeIndex ];\n\n\t\t\t\t\t\t\twhile ( (node = ++nodeIndex && node && node[ dir ] ||\n\n\t\t\t\t\t\t\t\t// Fallback to seeking `elem` from the start\n\t\t\t\t\t\t\t\t(diff = nodeIndex = 0) || start.pop()) ) {\n\n\t\t\t\t\t\t\t\t// When found, cache indexes on `parent` and break\n\t\t\t\t\t\t\t\tif ( node.nodeType === 1 && ++diff && node === elem ) {\n\t\t\t\t\t\t\t\t\touterCache[ type ] = [ dirruns, nodeIndex, diff ];\n\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Use previously-cached element index if available\n\t\t\t\t\t\t} else if ( useCache && (cache = (elem[ expando ] || (elem[ expando ] = {}))[ type ]) && cache[0] === dirruns ) {\n\t\t\t\t\t\t\tdiff = cache[1];\n\n\t\t\t\t\t\t// xml :nth-child(...) or :nth-last-child(...) or :nth(-last)?-of-type(...)\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// Use the same loop as above to seek `elem` from the start\n\t\t\t\t\t\t\twhile ( (node = ++nodeIndex && node && node[ dir ] ||\n\t\t\t\t\t\t\t\t(diff = nodeIndex = 0) || start.pop()) ) {\n\n\t\t\t\t\t\t\t\tif ( ( ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1 ) && ++diff ) {\n\t\t\t\t\t\t\t\t\t// Cache the index of each encountered element\n\t\t\t\t\t\t\t\t\tif ( useCache ) {\n\t\t\t\t\t\t\t\t\t\t(node[ expando ] || (node[ expando ] = {}))[ type ] = [ dirruns, diff ];\n\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\tif ( node === elem ) {\n\t\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Incorporate the offset, then check against cycle size\n\t\t\t\t\t\tdiff -= last;\n\t\t\t\t\t\treturn diff === first || ( diff % first === 0 && diff / first >= 0 );\n\t\t\t\t\t}\n\t\t\t\t};\n\t\t},\n\n\t\t\"PSEUDO\": function( pseudo, argument ) {\n\t\t\t// pseudo-class names are case-insensitive\n\t\t\t// http://www.w3.org/TR/selectors/#pseudo-classes\n\t\t\t// Prioritize by case sensitivity in case custom pseudos are added with uppercase letters\n\t\t\t// Remember that setFilters inherits from pseudos\n\t\t\tvar args,\n\t\t\t\tfn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] ||\n\t\t\t\t\tSizzle.error( \"unsupported pseudo: \" + pseudo );\n\n\t\t\t// The user may use createPseudo to indicate that\n\t\t\t// arguments are needed to create the filter function\n\t\t\t// just as Sizzle does\n\t\t\tif ( fn[ expando ] ) {\n\t\t\t\treturn fn( argument );\n\t\t\t}\n\n\t\t\t// But maintain support for old signatures\n\t\t\tif ( fn.length > 1 ) {\n\t\t\t\targs = [ pseudo, pseudo, \"\", argument ];\n\t\t\t\treturn Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ?\n\t\t\t\t\tmarkFunction(function( seed, matches ) {\n\t\t\t\t\t\tvar idx,\n\t\t\t\t\t\t\tmatched = fn( seed, argument ),\n\t\t\t\t\t\t\ti = matched.length;\n\t\t\t\t\t\twhile ( i-- ) {\n\t\t\t\t\t\t\tidx = indexOf.call( seed, matched[i] );\n\t\t\t\t\t\t\tseed[ idx ] = !( matches[ idx ] = matched[i] );\n\t\t\t\t\t\t}\n\t\t\t\t\t}) :\n\t\t\t\t\tfunction( elem ) {\n\t\t\t\t\t\treturn fn( elem, 0, args );\n\t\t\t\t\t};\n\t\t\t}\n\n\t\t\treturn fn;\n\t\t}\n\t},\n\n\tpseudos: {\n\t\t// Potentially complex pseudos\n\t\t\"not\": markFunction(function( selector ) {\n\t\t\t// Trim the selector passed to compile\n\t\t\t// to avoid treating leading and trailing\n\t\t\t// spaces as combinators\n\t\t\tvar input = [],\n\t\t\t\tresults = [],\n\t\t\t\tmatcher = compile( selector.replace( rtrim, \"$1\" ) );\n\n\t\t\treturn matcher[ expando ] ?\n\t\t\t\tmarkFunction(function( seed, matches, context, xml ) {\n\t\t\t\t\tvar elem,\n\t\t\t\t\t\tunmatched = matcher( seed, null, xml, [] ),\n\t\t\t\t\t\ti = seed.length;\n\n\t\t\t\t\t// Match elements unmatched by `matcher`\n\t\t\t\t\twhile ( i-- ) {\n\t\t\t\t\t\tif ( (elem = unmatched[i]) ) {\n\t\t\t\t\t\t\tseed[i] = !(matches[i] = elem);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}) :\n\t\t\t\tfunction( elem, context, xml ) {\n\t\t\t\t\tinput[0] = elem;\n\t\t\t\t\tmatcher( input, null, xml, results );\n\t\t\t\t\treturn !results.pop();\n\t\t\t\t};\n\t\t}),\n\n\t\t\"has\": markFunction(function( selector ) {\n\t\t\treturn function( elem ) {\n\t\t\t\treturn Sizzle( selector, elem ).length > 0;\n\t\t\t};\n\t\t}),\n\n\t\t\"contains\": markFunction(function( text ) {\n\t\t\treturn function( elem ) {\n\t\t\t\treturn ( elem.textContent || elem.innerText || getText( elem ) ).indexOf( text ) > -1;\n\t\t\t};\n\t\t}),\n\n\t\t// \"Whether an element is represented by a :lang() selector\n\t\t// is based solely on the element's language value\n\t\t// being equal to the identifier C,\n\t\t// or beginning with the identifier C immediately followed by \"-\".\n\t\t// The matching of C against the element's language value is performed case-insensitively.\n\t\t// The identifier C does not have to be a valid language name.\"\n\t\t// http://www.w3.org/TR/selectors/#lang-pseudo\n\t\t\"lang\": markFunction( function( lang ) {\n\t\t\t// lang value must be a valid identifier\n\t\t\tif ( !ridentifier.test(lang || \"\") ) {\n\t\t\t\tSizzle.error( \"unsupported lang: \" + lang );\n\t\t\t}\n\t\t\tlang = lang.replace( runescape, funescape ).toLowerCase();\n\t\t\treturn function( elem ) {\n\t\t\t\tvar elemLang;\n\t\t\t\tdo {\n\t\t\t\t\tif ( (elemLang = documentIsHTML ?\n\t\t\t\t\t\telem.lang :\n\t\t\t\t\t\telem.getAttribute(\"xml:lang\") || elem.getAttribute(\"lang\")) ) {\n\n\t\t\t\t\t\telemLang = elemLang.toLowerCase();\n\t\t\t\t\t\treturn elemLang === lang || elemLang.indexOf( lang + \"-\" ) === 0;\n\t\t\t\t\t}\n\t\t\t\t} while ( (elem = elem.parentNode) && elem.nodeType === 1 );\n\t\t\t\treturn false;\n\t\t\t};\n\t\t}),\n\n\t\t// Miscellaneous\n\t\t\"target\": function( elem ) {\n\t\t\tvar hash = window.location && window.location.hash;\n\t\t\treturn hash && hash.slice( 1 ) === elem.id;\n\t\t},\n\n\t\t\"root\": function( elem ) {\n\t\t\treturn elem === docElem;\n\t\t},\n\n\t\t\"focus\": function( elem ) {\n\t\t\treturn elem === document.activeElement && (!document.hasFocus || document.hasFocus()) && !!(elem.type || elem.href || ~elem.tabIndex);\n\t\t},\n\n\t\t// Boolean properties\n\t\t\"enabled\": function( elem ) {\n\t\t\treturn elem.disabled === false;\n\t\t},\n\n\t\t\"disabled\": function( elem ) {\n\t\t\treturn elem.disabled === true;\n\t\t},\n\n\t\t\"checked\": function( elem ) {\n\t\t\t// In CSS3, :checked should return both checked and selected elements\n\t\t\t// http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked\n\t\t\tvar nodeName = elem.nodeName.toLowerCase();\n\t\t\treturn (nodeName === \"input\" && !!elem.checked) || (nodeName === \"option\" && !!elem.selected);\n\t\t},\n\n\t\t\"selected\": function( elem ) {\n\t\t\t// Accessing this property makes selected-by-default\n\t\t\t// options in Safari work properly\n\t\t\tif ( elem.parentNode ) {\n\t\t\t\telem.parentNode.selectedIndex;\n\t\t\t}\n\n\t\t\treturn elem.selected === true;\n\t\t},\n\n\t\t// Contents\n\t\t\"empty\": function( elem ) {\n\t\t\t// http://www.w3.org/TR/selectors/#empty-pseudo\n\t\t\t// :empty is negated by element (1) or content nodes (text: 3; cdata: 4; entity ref: 5),\n\t\t\t//   but not by others (comment: 8; processing instruction: 7; etc.)\n\t\t\t// nodeType < 6 works because attributes (2) do not appear as children\n\t\t\tfor ( elem = elem.firstChild; elem; elem = elem.nextSibling ) {\n\t\t\t\tif ( elem.nodeType < 6 ) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn true;\n\t\t},\n\n\t\t\"parent\": function( elem ) {\n\t\t\treturn !Expr.pseudos[\"empty\"]( elem );\n\t\t},\n\n\t\t// Element/input types\n\t\t\"header\": function( elem ) {\n\t\t\treturn rheader.test( elem.nodeName );\n\t\t},\n\n\t\t\"input\": function( elem ) {\n\t\t\treturn rinputs.test( elem.nodeName );\n\t\t},\n\n\t\t\"button\": function( elem ) {\n\t\t\tvar name = elem.nodeName.toLowerCase();\n\t\t\treturn name === \"input\" && elem.type === \"button\" || name === \"button\";\n\t\t},\n\n\t\t\"text\": function( elem ) {\n\t\t\tvar attr;\n\t\t\treturn elem.nodeName.toLowerCase() === \"input\" &&\n\t\t\t\telem.type === \"text\" &&\n\n\t\t\t\t// Support: IE<8\n\t\t\t\t// New HTML5 attribute values (e.g., \"search\") appear with elem.type === \"text\"\n\t\t\t\t( (attr = elem.getAttribute(\"type\")) == null || attr.toLowerCase() === \"text\" );\n\t\t},\n\n\t\t// Position-in-collection\n\t\t\"first\": createPositionalPseudo(function() {\n\t\t\treturn [ 0 ];\n\t\t}),\n\n\t\t\"last\": createPositionalPseudo(function( matchIndexes, length ) {\n\t\t\treturn [ length - 1 ];\n\t\t}),\n\n\t\t\"eq\": createPositionalPseudo(function( matchIndexes, length, argument ) {\n\t\t\treturn [ argument < 0 ? argument + length : argument ];\n\t\t}),\n\n\t\t\"even\": createPositionalPseudo(function( matchIndexes, length ) {\n\t\t\tvar i = 0;\n\t\t\tfor ( ; i < length; i += 2 ) {\n\t\t\t\tmatchIndexes.push( i );\n\t\t\t}\n\t\t\treturn matchIndexes;\n\t\t}),\n\n\t\t\"odd\": createPositionalPseudo(function( matchIndexes, length ) {\n\t\t\tvar i = 1;\n\t\t\tfor ( ; i < length; i += 2 ) {\n\t\t\t\tmatchIndexes.push( i );\n\t\t\t}\n\t\t\treturn matchIndexes;\n\t\t}),\n\n\t\t\"lt\": createPositionalPseudo(function( matchIndexes, length, argument ) {\n\t\t\tvar i = argument < 0 ? argument + length : argument;\n\t\t\tfor ( ; --i >= 0; ) {\n\t\t\t\tmatchIndexes.push( i );\n\t\t\t}\n\t\t\treturn matchIndexes;\n\t\t}),\n\n\t\t\"gt\": createPositionalPseudo(function( matchIndexes, length, argument ) {\n\t\t\tvar i = argument < 0 ? argument + length : argument;\n\t\t\tfor ( ; ++i < length; ) {\n\t\t\t\tmatchIndexes.push( i );\n\t\t\t}\n\t\t\treturn matchIndexes;\n\t\t})\n\t}\n};\n\nExpr.pseudos[\"nth\"] = Expr.pseudos[\"eq\"];\n\n// Add button/input type pseudos\nfor ( i in { radio: true, checkbox: true, file: true, password: true, image: true } ) {\n\tExpr.pseudos[ i ] = createInputPseudo( i );\n}\nfor ( i in { submit: true, reset: true } ) {\n\tExpr.pseudos[ i ] = createButtonPseudo( i );\n}\n\n// Easy API for creating new setFilters\nfunction setFilters() {}\nsetFilters.prototype = Expr.filters = Expr.pseudos;\nExpr.setFilters = new setFilters();\n\ntokenize = Sizzle.tokenize = function( selector, parseOnly ) {\n\tvar matched, match, tokens, type,\n\t\tsoFar, groups, preFilters,\n\t\tcached = tokenCache[ selector + \" \" ];\n\n\tif ( cached ) {\n\t\treturn parseOnly ? 0 : cached.slice( 0 );\n\t}\n\n\tsoFar = selector;\n\tgroups = [];\n\tpreFilters = Expr.preFilter;\n\n\twhile ( soFar ) {\n\n\t\t// Comma and first run\n\t\tif ( !matched || (match = rcomma.exec( soFar )) ) {\n\t\t\tif ( match ) {\n\t\t\t\t// Don't consume trailing commas as valid\n\t\t\t\tsoFar = soFar.slice( match[0].length ) || soFar;\n\t\t\t}\n\t\t\tgroups.push( (tokens = []) );\n\t\t}\n\n\t\tmatched = false;\n\n\t\t// Combinators\n\t\tif ( (match = rcombinators.exec( soFar )) ) {\n\t\t\tmatched = match.shift();\n\t\t\ttokens.push({\n\t\t\t\tvalue: matched,\n\t\t\t\t// Cast descendant combinators to space\n\t\t\t\ttype: match[0].replace( rtrim, \" \" )\n\t\t\t});\n\t\t\tsoFar = soFar.slice( matched.length );\n\t\t}\n\n\t\t// Filters\n\t\tfor ( type in Expr.filter ) {\n\t\t\tif ( (match = matchExpr[ type ].exec( soFar )) && (!preFilters[ type ] ||\n\t\t\t\t(match = preFilters[ type ]( match ))) ) {\n\t\t\t\tmatched = match.shift();\n\t\t\t\ttokens.push({\n\t\t\t\t\tvalue: matched,\n\t\t\t\t\ttype: type,\n\t\t\t\t\tmatches: match\n\t\t\t\t});\n\t\t\t\tsoFar = soFar.slice( matched.length );\n\t\t\t}\n\t\t}\n\n\t\tif ( !matched ) {\n\t\t\tbreak;\n\t\t}\n\t}\n\n\t// Return the length of the invalid excess\n\t// if we're just parsing\n\t// Otherwise, throw an error or return tokens\n\treturn parseOnly ?\n\t\tsoFar.length :\n\t\tsoFar ?\n\t\t\tSizzle.error( selector ) :\n\t\t\t// Cache the tokens\n\t\t\ttokenCache( selector, groups ).slice( 0 );\n};\n\nfunction toSelector( tokens ) {\n\tvar i = 0,\n\t\tlen = tokens.length,\n\t\tselector = \"\";\n\tfor ( ; i < len; i++ ) {\n\t\tselector += tokens[i].value;\n\t}\n\treturn selector;\n}\n\nfunction addCombinator( matcher, combinator, base ) {\n\tvar dir = combinator.dir,\n\t\tcheckNonElements = base && dir === \"parentNode\",\n\t\tdoneName = done++;\n\n\treturn combinator.first ?\n\t\t// Check against closest ancestor/preceding element\n\t\tfunction( elem, context, xml ) {\n\t\t\twhile ( (elem = elem[ dir ]) ) {\n\t\t\t\tif ( elem.nodeType === 1 || checkNonElements ) {\n\t\t\t\t\treturn matcher( elem, context, xml );\n\t\t\t\t}\n\t\t\t}\n\t\t} :\n\n\t\t// Check against all ancestor/preceding elements\n\t\tfunction( elem, context, xml ) {\n\t\t\tvar oldCache, outerCache,\n\t\t\t\tnewCache = [ dirruns, doneName ];\n\n\t\t\t// We can't set arbitrary data on XML nodes, so they don't benefit from dir caching\n\t\t\tif ( xml ) {\n\t\t\t\twhile ( (elem = elem[ dir ]) ) {\n\t\t\t\t\tif ( elem.nodeType === 1 || checkNonElements ) {\n\t\t\t\t\t\tif ( matcher( elem, context, xml ) ) {\n\t\t\t\t\t\t\treturn true;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\twhile ( (elem = elem[ dir ]) ) {\n\t\t\t\t\tif ( elem.nodeType === 1 || checkNonElements ) {\n\t\t\t\t\t\touterCache = elem[ expando ] || (elem[ expando ] = {});\n\t\t\t\t\t\tif ( (oldCache = outerCache[ dir ]) &&\n\t\t\t\t\t\t\toldCache[ 0 ] === dirruns && oldCache[ 1 ] === doneName ) {\n\n\t\t\t\t\t\t\t// Assign to newCache so results back-propagate to previous elements\n\t\t\t\t\t\t\treturn (newCache[ 2 ] = oldCache[ 2 ]);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// Reuse newcache so results back-propagate to previous elements\n\t\t\t\t\t\t\touterCache[ dir ] = newCache;\n\n\t\t\t\t\t\t\t// A match means we're done; a fail means we have to keep checking\n\t\t\t\t\t\t\tif ( (newCache[ 2 ] = matcher( elem, context, xml )) ) {\n\t\t\t\t\t\t\t\treturn true;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t};\n}\n\nfunction elementMatcher( matchers ) {\n\treturn matchers.length > 1 ?\n\t\tfunction( elem, context, xml ) {\n\t\t\tvar i = matchers.length;\n\t\t\twhile ( i-- ) {\n\t\t\t\tif ( !matchers[i]( elem, context, xml ) ) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn true;\n\t\t} :\n\t\tmatchers[0];\n}\n\nfunction multipleContexts( selector, contexts, results ) {\n\tvar i = 0,\n\t\tlen = contexts.length;\n\tfor ( ; i < len; i++ ) {\n\t\tSizzle( selector, contexts[i], results );\n\t}\n\treturn results;\n}\n\nfunction condense( unmatched, map, filter, context, xml ) {\n\tvar elem,\n\t\tnewUnmatched = [],\n\t\ti = 0,\n\t\tlen = unmatched.length,\n\t\tmapped = map != null;\n\n\tfor ( ; i < len; i++ ) {\n\t\tif ( (elem = unmatched[i]) ) {\n\t\t\tif ( !filter || filter( elem, context, xml ) ) {\n\t\t\t\tnewUnmatched.push( elem );\n\t\t\t\tif ( mapped ) {\n\t\t\t\t\tmap.push( i );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn newUnmatched;\n}\n\nfunction setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) {\n\tif ( postFilter && !postFilter[ expando ] ) {\n\t\tpostFilter = setMatcher( postFilter );\n\t}\n\tif ( postFinder && !postFinder[ expando ] ) {\n\t\tpostFinder = setMatcher( postFinder, postSelector );\n\t}\n\treturn markFunction(function( seed, results, context, xml ) {\n\t\tvar temp, i, elem,\n\t\t\tpreMap = [],\n\t\t\tpostMap = [],\n\t\t\tpreexisting = results.length,\n\n\t\t\t// Get initial elements from seed or context\n\t\t\telems = seed || multipleContexts( selector || \"*\", context.nodeType ? [ context ] : context, [] ),\n\n\t\t\t// Prefilter to get matcher input, preserving a map for seed-results synchronization\n\t\t\tmatcherIn = preFilter && ( seed || !selector ) ?\n\t\t\t\tcondense( elems, preMap, preFilter, context, xml ) :\n\t\t\t\telems,\n\n\t\t\tmatcherOut = matcher ?\n\t\t\t\t// If we have a postFinder, or filtered seed, or non-seed postFilter or preexisting results,\n\t\t\t\tpostFinder || ( seed ? preFilter : preexisting || postFilter ) ?\n\n\t\t\t\t\t// ...intermediate processing is necessary\n\t\t\t\t\t[] :\n\n\t\t\t\t\t// ...otherwise use results directly\n\t\t\t\t\tresults :\n\t\t\t\tmatcherIn;\n\n\t\t// Find primary matches\n\t\tif ( matcher ) {\n\t\t\tmatcher( matcherIn, matcherOut, context, xml );\n\t\t}\n\n\t\t// Apply postFilter\n\t\tif ( postFilter ) {\n\t\t\ttemp = condense( matcherOut, postMap );\n\t\t\tpostFilter( temp, [], context, xml );\n\n\t\t\t// Un-match failing elements by moving them back to matcherIn\n\t\t\ti = temp.length;\n\t\t\twhile ( i-- ) {\n\t\t\t\tif ( (elem = temp[i]) ) {\n\t\t\t\t\tmatcherOut[ postMap[i] ] = !(matcherIn[ postMap[i] ] = elem);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif ( seed ) {\n\t\t\tif ( postFinder || preFilter ) {\n\t\t\t\tif ( postFinder ) {\n\t\t\t\t\t// Get the final matcherOut by condensing this intermediate into postFinder contexts\n\t\t\t\t\ttemp = [];\n\t\t\t\t\ti = matcherOut.length;\n\t\t\t\t\twhile ( i-- ) {\n\t\t\t\t\t\tif ( (elem = matcherOut[i]) ) {\n\t\t\t\t\t\t\t// Restore matcherIn since elem is not yet a final match\n\t\t\t\t\t\t\ttemp.push( (matcherIn[i] = elem) );\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tpostFinder( null, (matcherOut = []), temp, xml );\n\t\t\t\t}\n\n\t\t\t\t// Move matched elements from seed to results to keep them synchronized\n\t\t\t\ti = matcherOut.length;\n\t\t\t\twhile ( i-- ) {\n\t\t\t\t\tif ( (elem = matcherOut[i]) &&\n\t\t\t\t\t\t(temp = postFinder ? indexOf.call( seed, elem ) : preMap[i]) > -1 ) {\n\n\t\t\t\t\t\tseed[temp] = !(results[temp] = elem);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t// Add elements to results, through postFinder if defined\n\t\t} else {\n\t\t\tmatcherOut = condense(\n\t\t\t\tmatcherOut === results ?\n\t\t\t\t\tmatcherOut.splice( preexisting, matcherOut.length ) :\n\t\t\t\t\tmatcherOut\n\t\t\t);\n\t\t\tif ( postFinder ) {\n\t\t\t\tpostFinder( null, results, matcherOut, xml );\n\t\t\t} else {\n\t\t\t\tpush.apply( results, matcherOut );\n\t\t\t}\n\t\t}\n\t});\n}\n\nfunction matcherFromTokens( tokens ) {\n\tvar checkContext, matcher, j,\n\t\tlen = tokens.length,\n\t\tleadingRelative = Expr.relative[ tokens[0].type ],\n\t\timplicitRelative = leadingRelative || Expr.relative[\" \"],\n\t\ti = leadingRelative ? 1 : 0,\n\n\t\t// The foundational matcher ensures that elements are reachable from top-level context(s)\n\t\tmatchContext = addCombinator( function( elem ) {\n\t\t\treturn elem === checkContext;\n\t\t}, implicitRelative, true ),\n\t\tmatchAnyContext = addCombinator( function( elem ) {\n\t\t\treturn indexOf.call( checkContext, elem ) > -1;\n\t\t}, implicitRelative, true ),\n\t\tmatchers = [ function( elem, context, xml ) {\n\t\t\treturn ( !leadingRelative && ( xml || context !== outermostContext ) ) || (\n\t\t\t\t(checkContext = context).nodeType ?\n\t\t\t\t\tmatchContext( elem, context, xml ) :\n\t\t\t\t\tmatchAnyContext( elem, context, xml ) );\n\t\t} ];\n\n\tfor ( ; i < len; i++ ) {\n\t\tif ( (matcher = Expr.relative[ tokens[i].type ]) ) {\n\t\t\tmatchers = [ addCombinator(elementMatcher( matchers ), matcher) ];\n\t\t} else {\n\t\t\tmatcher = Expr.filter[ tokens[i].type ].apply( null, tokens[i].matches );\n\n\t\t\t// Return special upon seeing a positional matcher\n\t\t\tif ( matcher[ expando ] ) {\n\t\t\t\t// Find the next relative operator (if any) for proper handling\n\t\t\t\tj = ++i;\n\t\t\t\tfor ( ; j < len; j++ ) {\n\t\t\t\t\tif ( Expr.relative[ tokens[j].type ] ) {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn setMatcher(\n\t\t\t\t\ti > 1 && elementMatcher( matchers ),\n\t\t\t\t\ti > 1 && toSelector(\n\t\t\t\t\t\t// If the preceding token was a descendant combinator, insert an implicit any-element `*`\n\t\t\t\t\t\ttokens.slice( 0, i - 1 ).concat({ value: tokens[ i - 2 ].type === \" \" ? \"*\" : \"\" })\n\t\t\t\t\t).replace( rtrim, \"$1\" ),\n\t\t\t\t\tmatcher,\n\t\t\t\t\ti < j && matcherFromTokens( tokens.slice( i, j ) ),\n\t\t\t\t\tj < len && matcherFromTokens( (tokens = tokens.slice( j )) ),\n\t\t\t\t\tj < len && toSelector( tokens )\n\t\t\t\t);\n\t\t\t}\n\t\t\tmatchers.push( matcher );\n\t\t}\n\t}\n\n\treturn elementMatcher( matchers );\n}\n\nfunction matcherFromGroupMatchers( elementMatchers, setMatchers ) {\n\tvar bySet = setMatchers.length > 0,\n\t\tbyElement = elementMatchers.length > 0,\n\t\tsuperMatcher = function( seed, context, xml, results, outermost ) {\n\t\t\tvar elem, j, matcher,\n\t\t\t\tmatchedCount = 0,\n\t\t\t\ti = \"0\",\n\t\t\t\tunmatched = seed && [],\n\t\t\t\tsetMatched = [],\n\t\t\t\tcontextBackup = outermostContext,\n\t\t\t\t// We must always have either seed elements or outermost context\n\t\t\t\telems = seed || byElement && Expr.find[\"TAG\"]( \"*\", outermost ),\n\t\t\t\t// Use integer dirruns iff this is the outermost matcher\n\t\t\t\tdirrunsUnique = (dirruns += contextBackup == null ? 1 : Math.random() || 0.1),\n\t\t\t\tlen = elems.length;\n\n\t\t\tif ( outermost ) {\n\t\t\t\toutermostContext = context !== document && context;\n\t\t\t}\n\n\t\t\t// Add elements passing elementMatchers directly to results\n\t\t\t// Keep `i` a string if there are no elements so `matchedCount` will be \"00\" below\n\t\t\t// Support: IE<9, Safari\n\t\t\t// Tolerate NodeList properties (IE: \"length\"; Safari: <number>) matching elements by id\n\t\t\tfor ( ; i !== len && (elem = elems[i]) != null; i++ ) {\n\t\t\t\tif ( byElement && elem ) {\n\t\t\t\t\tj = 0;\n\t\t\t\t\twhile ( (matcher = elementMatchers[j++]) ) {\n\t\t\t\t\t\tif ( matcher( elem, context, xml ) ) {\n\t\t\t\t\t\t\tresults.push( elem );\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif ( outermost ) {\n\t\t\t\t\t\tdirruns = dirrunsUnique;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Track unmatched elements for set filters\n\t\t\t\tif ( bySet ) {\n\t\t\t\t\t// They will have gone through all possible matchers\n\t\t\t\t\tif ( (elem = !matcher && elem) ) {\n\t\t\t\t\t\tmatchedCount--;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Lengthen the array for every element, matched or not\n\t\t\t\t\tif ( seed ) {\n\t\t\t\t\t\tunmatched.push( elem );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Apply set filters to unmatched elements\n\t\t\tmatchedCount += i;\n\t\t\tif ( bySet && i !== matchedCount ) {\n\t\t\t\tj = 0;\n\t\t\t\twhile ( (matcher = setMatchers[j++]) ) {\n\t\t\t\t\tmatcher( unmatched, setMatched, context, xml );\n\t\t\t\t}\n\n\t\t\t\tif ( seed ) {\n\t\t\t\t\t// Reintegrate element matches to eliminate the need for sorting\n\t\t\t\t\tif ( matchedCount > 0 ) {\n\t\t\t\t\t\twhile ( i-- ) {\n\t\t\t\t\t\t\tif ( !(unmatched[i] || setMatched[i]) ) {\n\t\t\t\t\t\t\t\tsetMatched[i] = pop.call( results );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// Discard index placeholder values to get only actual matches\n\t\t\t\t\tsetMatched = condense( setMatched );\n\t\t\t\t}\n\n\t\t\t\t// Add matches to results\n\t\t\t\tpush.apply( results, setMatched );\n\n\t\t\t\t// Seedless set matches succeeding multiple successful matchers stipulate sorting\n\t\t\t\tif ( outermost && !seed && setMatched.length > 0 &&\n\t\t\t\t\t( matchedCount + setMatchers.length ) > 1 ) {\n\n\t\t\t\t\tSizzle.uniqueSort( results );\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Override manipulation of globals by nested matchers\n\t\t\tif ( outermost ) {\n\t\t\t\tdirruns = dirrunsUnique;\n\t\t\t\toutermostContext = contextBackup;\n\t\t\t}\n\n\t\t\treturn unmatched;\n\t\t};\n\n\treturn bySet ?\n\t\tmarkFunction( superMatcher ) :\n\t\tsuperMatcher;\n}\n\ncompile = Sizzle.compile = function( selector, match /* Internal Use Only */ ) {\n\tvar i,\n\t\tsetMatchers = [],\n\t\telementMatchers = [],\n\t\tcached = compilerCache[ selector + \" \" ];\n\n\tif ( !cached ) {\n\t\t// Generate a function of recursive functions that can be used to check each element\n\t\tif ( !match ) {\n\t\t\tmatch = tokenize( selector );\n\t\t}\n\t\ti = match.length;\n\t\twhile ( i-- ) {\n\t\t\tcached = matcherFromTokens( match[i] );\n\t\t\tif ( cached[ expando ] ) {\n\t\t\t\tsetMatchers.push( cached );\n\t\t\t} else {\n\t\t\t\telementMatchers.push( cached );\n\t\t\t}\n\t\t}\n\n\t\t// Cache the compiled function\n\t\tcached = compilerCache( selector, matcherFromGroupMatchers( elementMatchers, setMatchers ) );\n\n\t\t// Save selector and tokenization\n\t\tcached.selector = selector;\n\t}\n\treturn cached;\n};\n\n/**\n * A low-level selection function that works with Sizzle's compiled\n *  selector functions\n * @param {String|Function} selector A selector or a pre-compiled\n *  selector function built with Sizzle.compile\n * @param {Element} context\n * @param {Array} [results]\n * @param {Array} [seed] A set of elements to match against\n */\nselect = Sizzle.select = function( selector, context, results, seed ) {\n\tvar i, tokens, token, type, find,\n\t\tcompiled = typeof selector === \"function\" && selector,\n\t\tmatch = !seed && tokenize( (selector = compiled.selector || selector) );\n\n\tresults = results || [];\n\n\t// Try to minimize operations if there is no seed and only one group\n\tif ( match.length === 1 ) {\n\n\t\t// Take a shortcut and set the context if the root selector is an ID\n\t\ttokens = match[0] = match[0].slice( 0 );\n\t\tif ( tokens.length > 2 && (token = tokens[0]).type === \"ID\" &&\n\t\t\t\tsupport.getById && context.nodeType === 9 && documentIsHTML &&\n\t\t\t\tExpr.relative[ tokens[1].type ] ) {\n\n\t\t\tcontext = ( Expr.find[\"ID\"]( token.matches[0].replace(runescape, funescape), context ) || [] )[0];\n\t\t\tif ( !context ) {\n\t\t\t\treturn results;\n\n\t\t\t// Precompiled matchers will still verify ancestry, so step up a level\n\t\t\t} else if ( compiled ) {\n\t\t\t\tcontext = context.parentNode;\n\t\t\t}\n\n\t\t\tselector = selector.slice( tokens.shift().value.length );\n\t\t}\n\n\t\t// Fetch a seed set for right-to-left matching\n\t\ti = matchExpr[\"needsContext\"].test( selector ) ? 0 : tokens.length;\n\t\twhile ( i-- ) {\n\t\t\ttoken = tokens[i];\n\n\t\t\t// Abort if we hit a combinator\n\t\t\tif ( Expr.relative[ (type = token.type) ] ) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif ( (find = Expr.find[ type ]) ) {\n\t\t\t\t// Search, expanding context for leading sibling combinators\n\t\t\t\tif ( (seed = find(\n\t\t\t\t\ttoken.matches[0].replace( runescape, funescape ),\n\t\t\t\t\trsibling.test( tokens[0].type ) && testContext( context.parentNode ) || context\n\t\t\t\t)) ) {\n\n\t\t\t\t\t// If seed is empty or no tokens remain, we can return early\n\t\t\t\t\ttokens.splice( i, 1 );\n\t\t\t\t\tselector = seed.length && toSelector( tokens );\n\t\t\t\t\tif ( !selector ) {\n\t\t\t\t\t\tpush.apply( results, seed );\n\t\t\t\t\t\treturn results;\n\t\t\t\t\t}\n\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Compile and execute a filtering function if one is not provided\n\t// Provide `match` to avoid retokenization if we modified the selector above\n\t( compiled || compile( selector, match ) )(\n\t\tseed,\n\t\tcontext,\n\t\t!documentIsHTML,\n\t\tresults,\n\t\trsibling.test( selector ) && testContext( context.parentNode ) || context\n\t);\n\treturn results;\n};\n\n// One-time assignments\n\n// Sort stability\nsupport.sortStable = expando.split(\"\").sort( sortOrder ).join(\"\") === expando;\n\n// Support: Chrome<14\n// Always assume duplicates if they aren't passed to the comparison function\nsupport.detectDuplicates = !!hasDuplicate;\n\n// Initialize against the default document\nsetDocument();\n\n// Support: Webkit<537.32 - Safari 6.0.3/Chrome 25 (fixed in Chrome 27)\n// Detached nodes confoundingly follow *each other*\nsupport.sortDetached = assert(function( div1 ) {\n\t// Should return 1, but returns 4 (following)\n\treturn div1.compareDocumentPosition( document.createElement(\"div\") ) & 1;\n});\n\n// Support: IE<8\n// Prevent attribute/property \"interpolation\"\n// http://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx\nif ( !assert(function( div ) {\n\tdiv.innerHTML = \"<a href='#'></a>\";\n\treturn div.firstChild.getAttribute(\"href\") === \"#\" ;\n}) ) {\n\taddHandle( \"type|href|height|width\", function( elem, name, isXML ) {\n\t\tif ( !isXML ) {\n\t\t\treturn elem.getAttribute( name, name.toLowerCase() === \"type\" ? 1 : 2 );\n\t\t}\n\t});\n}\n\n// Support: IE<9\n// Use defaultValue in place of getAttribute(\"value\")\nif ( !support.attributes || !assert(function( div ) {\n\tdiv.innerHTML = \"<input/>\";\n\tdiv.firstChild.setAttribute( \"value\", \"\" );\n\treturn div.firstChild.getAttribute( \"value\" ) === \"\";\n}) ) {\n\taddHandle( \"value\", function( elem, name, isXML ) {\n\t\tif ( !isXML && elem.nodeName.toLowerCase() === \"input\" ) {\n\t\t\treturn elem.defaultValue;\n\t\t}\n\t});\n}\n\n// Support: IE<9\n// Use getAttributeNode to fetch booleans when getAttribute lies\nif ( !assert(function( div ) {\n\treturn div.getAttribute(\"disabled\") == null;\n}) ) {\n\taddHandle( booleans, function( elem, name, isXML ) {\n\t\tvar val;\n\t\tif ( !isXML ) {\n\t\t\treturn elem[ name ] === true ? name.toLowerCase() :\n\t\t\t\t\t(val = elem.getAttributeNode( name )) && val.specified ?\n\t\t\t\t\tval.value :\n\t\t\t\tnull;\n\t\t}\n\t});\n}\n\nreturn Sizzle;\n\n})( window );\n\n\n\njQuery.find = Sizzle;\njQuery.expr = Sizzle.selectors;\njQuery.expr[\":\"] = jQuery.expr.pseudos;\njQuery.unique = Sizzle.uniqueSort;\njQuery.text = Sizzle.getText;\njQuery.isXMLDoc = Sizzle.isXML;\njQuery.contains = Sizzle.contains;\n\n\n\nvar rneedsContext = jQuery.expr.match.needsContext;\n\nvar rsingleTag = (/^<(\\w+)\\s*\\/?>(?:<\\/\\1>|)$/);\n\n\n\nvar risSimple = /^.[^:#\\[\\.,]*$/;\n\n// Implement the identical functionality for filter and not\nfunction winnow( elements, qualifier, not ) {\n\tif ( jQuery.isFunction( qualifier ) ) {\n\t\treturn jQuery.grep( elements, function( elem, i ) {\n\t\t\t/* jshint -W018 */\n\t\t\treturn !!qualifier.call( elem, i, elem ) !== not;\n\t\t});\n\n\t}\n\n\tif ( qualifier.nodeType ) {\n\t\treturn jQuery.grep( elements, function( elem ) {\n\t\t\treturn ( elem === qualifier ) !== not;\n\t\t});\n\n\t}\n\n\tif ( typeof qualifier === \"string\" ) {\n\t\tif ( risSimple.test( qualifier ) ) {\n\t\t\treturn jQuery.filter( qualifier, elements, not );\n\t\t}\n\n\t\tqualifier = jQuery.filter( qualifier, elements );\n\t}\n\n\treturn jQuery.grep( elements, function( elem ) {\n\t\treturn ( jQuery.inArray( elem, qualifier ) >= 0 ) !== not;\n\t});\n}\n\njQuery.filter = function( expr, elems, not ) {\n\tvar elem = elems[ 0 ];\n\n\tif ( not ) {\n\t\texpr = \":not(\" + expr + \")\";\n\t}\n\n\treturn elems.length === 1 && elem.nodeType === 1 ?\n\t\tjQuery.find.matchesSelector( elem, expr ) ? [ elem ] : [] :\n\t\tjQuery.find.matches( expr, jQuery.grep( elems, function( elem ) {\n\t\t\treturn elem.nodeType === 1;\n\t\t}));\n};\n\njQuery.fn.extend({\n\tfind: function( selector ) {\n\t\tvar i,\n\t\t\tret = [],\n\t\t\tself = this,\n\t\t\tlen = self.length;\n\n\t\tif ( typeof selector !== \"string\" ) {\n\t\t\treturn this.pushStack( jQuery( selector ).filter(function() {\n\t\t\t\tfor ( i = 0; i < len; i++ ) {\n\t\t\t\t\tif ( jQuery.contains( self[ i ], this ) ) {\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}) );\n\t\t}\n\n\t\tfor ( i = 0; i < len; i++ ) {\n\t\t\tjQuery.find( selector, self[ i ], ret );\n\t\t}\n\n\t\t// Needed because $( selector, context ) becomes $( context ).find( selector )\n\t\tret = this.pushStack( len > 1 ? jQuery.unique( ret ) : ret );\n\t\tret.selector = this.selector ? this.selector + \" \" + selector : selector;\n\t\treturn ret;\n\t},\n\tfilter: function( selector ) {\n\t\treturn this.pushStack( winnow(this, selector || [], false) );\n\t},\n\tnot: function( selector ) {\n\t\treturn this.pushStack( winnow(this, selector || [], true) );\n\t},\n\tis: function( selector ) {\n\t\treturn !!winnow(\n\t\t\tthis,\n\n\t\t\t// If this is a positional/relative selector, check membership in the returned set\n\t\t\t// so $(\"p:first\").is(\"p:last\") won't return true for a doc with two \"p\".\n\t\t\ttypeof selector === \"string\" && rneedsContext.test( selector ) ?\n\t\t\t\tjQuery( selector ) :\n\t\t\t\tselector || [],\n\t\t\tfalse\n\t\t).length;\n\t}\n});\n\n\n// Initialize a jQuery object\n\n\n// A central reference to the root jQuery(document)\nvar rootjQuery,\n\n\t// Use the correct document accordingly with window argument (sandbox)\n\tdocument = window.document,\n\n\t// A simple way to check for HTML strings\n\t// Prioritize #id over <tag> to avoid XSS via location.hash (#9521)\n\t// Strict HTML recognition (#11290: must start with <)\n\trquickExpr = /^(?:\\s*(<[\\w\\W]+>)[^>]*|#([\\w-]*))$/,\n\n\tinit = jQuery.fn.init = function( selector, context ) {\n\t\tvar match, elem;\n\n\t\t// HANDLE: $(\"\"), $(null), $(undefined), $(false)\n\t\tif ( !selector ) {\n\t\t\treturn this;\n\t\t}\n\n\t\t// Handle HTML strings\n\t\tif ( typeof selector === \"string\" ) {\n\t\t\tif ( selector.charAt(0) === \"<\" && selector.charAt( selector.length - 1 ) === \">\" && selector.length >= 3 ) {\n\t\t\t\t// Assume that strings that start and end with <> are HTML and skip the regex check\n\t\t\t\tmatch = [ null, selector, null ];\n\n\t\t\t} else {\n\t\t\t\tmatch = rquickExpr.exec( selector );\n\t\t\t}\n\n\t\t\t// Match html or make sure no context is specified for #id\n\t\t\tif ( match && (match[1] || !context) ) {\n\n\t\t\t\t// HANDLE: $(html) -> $(array)\n\t\t\t\tif ( match[1] ) {\n\t\t\t\t\tcontext = context instanceof jQuery ? context[0] : context;\n\n\t\t\t\t\t// scripts is true for back-compat\n\t\t\t\t\t// Intentionally let the error be thrown if parseHTML is not present\n\t\t\t\t\tjQuery.merge( this, jQuery.parseHTML(\n\t\t\t\t\t\tmatch[1],\n\t\t\t\t\t\tcontext && context.nodeType ? context.ownerDocument || context : document,\n\t\t\t\t\t\ttrue\n\t\t\t\t\t) );\n\n\t\t\t\t\t// HANDLE: $(html, props)\n\t\t\t\t\tif ( rsingleTag.test( match[1] ) && jQuery.isPlainObject( context ) ) {\n\t\t\t\t\t\tfor ( match in context ) {\n\t\t\t\t\t\t\t// Properties of context are called as methods if possible\n\t\t\t\t\t\t\tif ( jQuery.isFunction( this[ match ] ) ) {\n\t\t\t\t\t\t\t\tthis[ match ]( context[ match ] );\n\n\t\t\t\t\t\t\t// ...and otherwise set as attributes\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tthis.attr( match, context[ match ] );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\treturn this;\n\n\t\t\t\t// HANDLE: $(#id)\n\t\t\t\t} else {\n\t\t\t\t\telem = document.getElementById( match[2] );\n\n\t\t\t\t\t// Check parentNode to catch when Blackberry 4.6 returns\n\t\t\t\t\t// nodes that are no longer in the document #6963\n\t\t\t\t\tif ( elem && elem.parentNode ) {\n\t\t\t\t\t\t// Handle the case where IE and Opera return items\n\t\t\t\t\t\t// by name instead of ID\n\t\t\t\t\t\tif ( elem.id !== match[2] ) {\n\t\t\t\t\t\t\treturn rootjQuery.find( selector );\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Otherwise, we inject the element directly into the jQuery object\n\t\t\t\t\t\tthis.length = 1;\n\t\t\t\t\t\tthis[0] = elem;\n\t\t\t\t\t}\n\n\t\t\t\t\tthis.context = document;\n\t\t\t\t\tthis.selector = selector;\n\t\t\t\t\treturn this;\n\t\t\t\t}\n\n\t\t\t// HANDLE: $(expr, $(...))\n\t\t\t} else if ( !context || context.jquery ) {\n\t\t\t\treturn ( context || rootjQuery ).find( selector );\n\n\t\t\t// HANDLE: $(expr, context)\n\t\t\t// (which is just equivalent to: $(context).find(expr)\n\t\t\t} else {\n\t\t\t\treturn this.constructor( context ).find( selector );\n\t\t\t}\n\n\t\t// HANDLE: $(DOMElement)\n\t\t} else if ( selector.nodeType ) {\n\t\t\tthis.context = this[0] = selector;\n\t\t\tthis.length = 1;\n\t\t\treturn this;\n\n\t\t// HANDLE: $(function)\n\t\t// Shortcut for document ready\n\t\t} else if ( jQuery.isFunction( selector ) ) {\n\t\t\treturn typeof rootjQuery.ready !== \"undefined\" ?\n\t\t\t\trootjQuery.ready( selector ) :\n\t\t\t\t// Execute immediately if ready is not present\n\t\t\t\tselector( jQuery );\n\t\t}\n\n\t\tif ( selector.selector !== undefined ) {\n\t\t\tthis.selector = selector.selector;\n\t\t\tthis.context = selector.context;\n\t\t}\n\n\t\treturn jQuery.makeArray( selector, this );\n\t};\n\n// Give the init function the jQuery prototype for later instantiation\ninit.prototype = jQuery.fn;\n\n// Initialize central reference\nrootjQuery = jQuery( document );\n\n\nvar rparentsprev = /^(?:parents|prev(?:Until|All))/,\n\t// methods guaranteed to produce a unique set when starting from a unique set\n\tguaranteedUnique = {\n\t\tchildren: true,\n\t\tcontents: true,\n\t\tnext: true,\n\t\tprev: true\n\t};\n\njQuery.extend({\n\tdir: function( elem, dir, until ) {\n\t\tvar matched = [],\n\t\t\tcur = elem[ dir ];\n\n\t\twhile ( cur && cur.nodeType !== 9 && (until === undefined || cur.nodeType !== 1 || !jQuery( cur ).is( until )) ) {\n\t\t\tif ( cur.nodeType === 1 ) {\n\t\t\t\tmatched.push( cur );\n\t\t\t}\n\t\t\tcur = cur[dir];\n\t\t}\n\t\treturn matched;\n\t},\n\n\tsibling: function( n, elem ) {\n\t\tvar r = [];\n\n\t\tfor ( ; n; n = n.nextSibling ) {\n\t\t\tif ( n.nodeType === 1 && n !== elem ) {\n\t\t\t\tr.push( n );\n\t\t\t}\n\t\t}\n\n\t\treturn r;\n\t}\n});\n\njQuery.fn.extend({\n\thas: function( target ) {\n\t\tvar i,\n\t\t\ttargets = jQuery( target, this ),\n\t\t\tlen = targets.length;\n\n\t\treturn this.filter(function() {\n\t\t\tfor ( i = 0; i < len; i++ ) {\n\t\t\t\tif ( jQuery.contains( this, targets[i] ) ) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t},\n\n\tclosest: function( selectors, context ) {\n\t\tvar cur,\n\t\t\ti = 0,\n\t\t\tl = this.length,\n\t\t\tmatched = [],\n\t\t\tpos = rneedsContext.test( selectors ) || typeof selectors !== \"string\" ?\n\t\t\t\tjQuery( selectors, context || this.context ) :\n\t\t\t\t0;\n\n\t\tfor ( ; i < l; i++ ) {\n\t\t\tfor ( cur = this[i]; cur && cur !== context; cur = cur.parentNode ) {\n\t\t\t\t// Always skip document fragments\n\t\t\t\tif ( cur.nodeType < 11 && (pos ?\n\t\t\t\t\tpos.index(cur) > -1 :\n\n\t\t\t\t\t// Don't pass non-elements to Sizzle\n\t\t\t\t\tcur.nodeType === 1 &&\n\t\t\t\t\t\tjQuery.find.matchesSelector(cur, selectors)) ) {\n\n\t\t\t\t\tmatched.push( cur );\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn this.pushStack( matched.length > 1 ? jQuery.unique( matched ) : matched );\n\t},\n\n\t// Determine the position of an element within\n\t// the matched set of elements\n\tindex: function( elem ) {\n\n\t\t// No argument, return index in parent\n\t\tif ( !elem ) {\n\t\t\treturn ( this[0] && this[0].parentNode ) ? this.first().prevAll().length : -1;\n\t\t}\n\n\t\t// index in selector\n\t\tif ( typeof elem === \"string\" ) {\n\t\t\treturn jQuery.inArray( this[0], jQuery( elem ) );\n\t\t}\n\n\t\t// Locate the position of the desired element\n\t\treturn jQuery.inArray(\n\t\t\t// If it receives a jQuery object, the first element is used\n\t\t\telem.jquery ? elem[0] : elem, this );\n\t},\n\n\tadd: function( selector, context ) {\n\t\treturn this.pushStack(\n\t\t\tjQuery.unique(\n\t\t\t\tjQuery.merge( this.get(), jQuery( selector, context ) )\n\t\t\t)\n\t\t);\n\t},\n\n\taddBack: function( selector ) {\n\t\treturn this.add( selector == null ?\n\t\t\tthis.prevObject : this.prevObject.filter(selector)\n\t\t);\n\t}\n});\n\nfunction sibling( cur, dir ) {\n\tdo {\n\t\tcur = cur[ dir ];\n\t} while ( cur && cur.nodeType !== 1 );\n\n\treturn cur;\n}\n\njQuery.each({\n\tparent: function( elem ) {\n\t\tvar parent = elem.parentNode;\n\t\treturn parent && parent.nodeType !== 11 ? parent : null;\n\t},\n\tparents: function( elem ) {\n\t\treturn jQuery.dir( elem, \"parentNode\" );\n\t},\n\tparentsUntil: function( elem, i, until ) {\n\t\treturn jQuery.dir( elem, \"parentNode\", until );\n\t},\n\tnext: function( elem ) {\n\t\treturn sibling( elem, \"nextSibling\" );\n\t},\n\tprev: function( elem ) {\n\t\treturn sibling( elem, \"previousSibling\" );\n\t},\n\tnextAll: function( elem ) {\n\t\treturn jQuery.dir( elem, \"nextSibling\" );\n\t},\n\tprevAll: function( elem ) {\n\t\treturn jQuery.dir( elem, \"previousSibling\" );\n\t},\n\tnextUntil: function( elem, i, until ) {\n\t\treturn jQuery.dir( elem, \"nextSibling\", until );\n\t},\n\tprevUntil: function( elem, i, until ) {\n\t\treturn jQuery.dir( elem, \"previousSibling\", until );\n\t},\n\tsiblings: function( elem ) {\n\t\treturn jQuery.sibling( ( elem.parentNode || {} ).firstChild, elem );\n\t},\n\tchildren: function( elem ) {\n\t\treturn jQuery.sibling( elem.firstChild );\n\t},\n\tcontents: function( elem ) {\n\t\treturn jQuery.nodeName( elem, \"iframe\" ) ?\n\t\t\telem.contentDocument || elem.contentWindow.document :\n\t\t\tjQuery.merge( [], elem.childNodes );\n\t}\n}, function( name, fn ) {\n\tjQuery.fn[ name ] = function( until, selector ) {\n\t\tvar ret = jQuery.map( this, fn, until );\n\n\t\tif ( name.slice( -5 ) !== \"Until\" ) {\n\t\t\tselector = until;\n\t\t}\n\n\t\tif ( selector && typeof selector === \"string\" ) {\n\t\t\tret = jQuery.filter( selector, ret );\n\t\t}\n\n\t\tif ( this.length > 1 ) {\n\t\t\t// Remove duplicates\n\t\t\tif ( !guaranteedUnique[ name ] ) {\n\t\t\t\tret = jQuery.unique( ret );\n\t\t\t}\n\n\t\t\t// Reverse order for parents* and prev-derivatives\n\t\t\tif ( rparentsprev.test( name ) ) {\n\t\t\t\tret = ret.reverse();\n\t\t\t}\n\t\t}\n\n\t\treturn this.pushStack( ret );\n\t};\n});\nvar rnotwhite = (/\\S+/g);\n\n\n\n// String to Object options format cache\nvar optionsCache = {};\n\n// Convert String-formatted options into Object-formatted ones and store in cache\nfunction createOptions( options ) {\n\tvar object = optionsCache[ options ] = {};\n\tjQuery.each( options.match( rnotwhite ) || [], function( _, flag ) {\n\t\tobject[ flag ] = true;\n\t});\n\treturn object;\n}\n\n/*\n * Create a callback list using the following parameters:\n *\n *\toptions: an optional list of space-separated options that will change how\n *\t\t\tthe callback list behaves or a more traditional option object\n *\n * By default a callback list will act like an event callback list and can be\n * \"fired\" multiple times.\n *\n * Possible options:\n *\n *\tonce:\t\t\twill ensure the callback list can only be fired once (like a Deferred)\n *\n *\tmemory:\t\t\twill keep track of previous values and will call any callback added\n *\t\t\t\t\tafter the list has been fired right away with the latest \"memorized\"\n *\t\t\t\t\tvalues (like a Deferred)\n *\n *\tunique:\t\t\twill ensure a callback can only be added once (no duplicate in the list)\n *\n *\tstopOnFalse:\tinterrupt callings when a callback returns false\n *\n */\njQuery.Callbacks = function( options ) {\n\n\t// Convert options from String-formatted to Object-formatted if needed\n\t// (we check in cache first)\n\toptions = typeof options === \"string\" ?\n\t\t( optionsCache[ options ] || createOptions( options ) ) :\n\t\tjQuery.extend( {}, options );\n\n\tvar // Flag to know if list is currently firing\n\t\tfiring,\n\t\t// Last fire value (for non-forgettable lists)\n\t\tmemory,\n\t\t// Flag to know if list was already fired\n\t\tfired,\n\t\t// End of the loop when firing\n\t\tfiringLength,\n\t\t// Index of currently firing callback (modified by remove if needed)\n\t\tfiringIndex,\n\t\t// First callback to fire (used internally by add and fireWith)\n\t\tfiringStart,\n\t\t// Actual callback list\n\t\tlist = [],\n\t\t// Stack of fire calls for repeatable lists\n\t\tstack = !options.once && [],\n\t\t// Fire callbacks\n\t\tfire = function( data ) {\n\t\t\tmemory = options.memory && data;\n\t\t\tfired = true;\n\t\t\tfiringIndex = firingStart || 0;\n\t\t\tfiringStart = 0;\n\t\t\tfiringLength = list.length;\n\t\t\tfiring = true;\n\t\t\tfor ( ; list && firingIndex < firingLength; firingIndex++ ) {\n\t\t\t\tif ( list[ firingIndex ].apply( data[ 0 ], data[ 1 ] ) === false && options.stopOnFalse ) {\n\t\t\t\t\tmemory = false; // To prevent further calls using add\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\tfiring = false;\n\t\t\tif ( list ) {\n\t\t\t\tif ( stack ) {\n\t\t\t\t\tif ( stack.length ) {\n\t\t\t\t\t\tfire( stack.shift() );\n\t\t\t\t\t}\n\t\t\t\t} else if ( memory ) {\n\t\t\t\t\tlist = [];\n\t\t\t\t} else {\n\t\t\t\t\tself.disable();\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\t// Actual Callbacks object\n\t\tself = {\n\t\t\t// Add a callback or a collection of callbacks to the list\n\t\t\tadd: function() {\n\t\t\t\tif ( list ) {\n\t\t\t\t\t// First, we save the current length\n\t\t\t\t\tvar start = list.length;\n\t\t\t\t\t(function add( args ) {\n\t\t\t\t\t\tjQuery.each( args, function( _, arg ) {\n\t\t\t\t\t\t\tvar type = jQuery.type( arg );\n\t\t\t\t\t\t\tif ( type === \"function\" ) {\n\t\t\t\t\t\t\t\tif ( !options.unique || !self.has( arg ) ) {\n\t\t\t\t\t\t\t\t\tlist.push( arg );\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} else if ( arg && arg.length && type !== \"string\" ) {\n\t\t\t\t\t\t\t\t// Inspect recursively\n\t\t\t\t\t\t\t\tadd( arg );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t});\n\t\t\t\t\t})( arguments );\n\t\t\t\t\t// Do we need to add the callbacks to the\n\t\t\t\t\t// current firing batch?\n\t\t\t\t\tif ( firing ) {\n\t\t\t\t\t\tfiringLength = list.length;\n\t\t\t\t\t// With memory, if we're not firing then\n\t\t\t\t\t// we should call right away\n\t\t\t\t\t} else if ( memory ) {\n\t\t\t\t\t\tfiringStart = start;\n\t\t\t\t\t\tfire( memory );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn this;\n\t\t\t},\n\t\t\t// Remove a callback from the list\n\t\t\tremove: function() {\n\t\t\t\tif ( list ) {\n\t\t\t\t\tjQuery.each( arguments, function( _, arg ) {\n\t\t\t\t\t\tvar index;\n\t\t\t\t\t\twhile ( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) {\n\t\t\t\t\t\t\tlist.splice( index, 1 );\n\t\t\t\t\t\t\t// Handle firing indexes\n\t\t\t\t\t\t\tif ( firing ) {\n\t\t\t\t\t\t\t\tif ( index <= firingLength ) {\n\t\t\t\t\t\t\t\t\tfiringLength--;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tif ( index <= firingIndex ) {\n\t\t\t\t\t\t\t\t\tfiringIndex--;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\treturn this;\n\t\t\t},\n\t\t\t// Check if a given callback is in the list.\n\t\t\t// If no argument is given, return whether or not list has callbacks attached.\n\t\t\thas: function( fn ) {\n\t\t\t\treturn fn ? jQuery.inArray( fn, list ) > -1 : !!( list && list.length );\n\t\t\t},\n\t\t\t// Remove all callbacks from the list\n\t\t\tempty: function() {\n\t\t\t\tlist = [];\n\t\t\t\tfiringLength = 0;\n\t\t\t\treturn this;\n\t\t\t},\n\t\t\t// Have the list do nothing anymore\n\t\t\tdisable: function() {\n\t\t\t\tlist = stack = memory = undefined;\n\t\t\t\treturn this;\n\t\t\t},\n\t\t\t// Is it disabled?\n\t\t\tdisabled: function() {\n\t\t\t\treturn !list;\n\t\t\t},\n\t\t\t// Lock the list in its current state\n\t\t\tlock: function() {\n\t\t\t\tstack = undefined;\n\t\t\t\tif ( !memory ) {\n\t\t\t\t\tself.disable();\n\t\t\t\t}\n\t\t\t\treturn this;\n\t\t\t},\n\t\t\t// Is it locked?\n\t\t\tlocked: function() {\n\t\t\t\treturn !stack;\n\t\t\t},\n\t\t\t// Call all callbacks with the given context and arguments\n\t\t\tfireWith: function( context, args ) {\n\t\t\t\tif ( list && ( !fired || stack ) ) {\n\t\t\t\t\targs = args || [];\n\t\t\t\t\targs = [ context, args.slice ? args.slice() : args ];\n\t\t\t\t\tif ( firing ) {\n\t\t\t\t\t\tstack.push( args );\n\t\t\t\t\t} else {\n\t\t\t\t\t\tfire( args );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn this;\n\t\t\t},\n\t\t\t// Call all the callbacks with the given arguments\n\t\t\tfire: function() {\n\t\t\t\tself.fireWith( this, arguments );\n\t\t\t\treturn this;\n\t\t\t},\n\t\t\t// To know if the callbacks have already been called at least once\n\t\t\tfired: function() {\n\t\t\t\treturn !!fired;\n\t\t\t}\n\t\t};\n\n\treturn self;\n};\n\n\njQuery.extend({\n\n\tDeferred: function( func ) {\n\t\tvar tuples = [\n\t\t\t\t// action, add listener, listener list, final state\n\t\t\t\t[ \"resolve\", \"done\", jQuery.Callbacks(\"once memory\"), \"resolved\" ],\n\t\t\t\t[ \"reject\", \"fail\", jQuery.Callbacks(\"once memory\"), \"rejected\" ],\n\t\t\t\t[ \"notify\", \"progress\", jQuery.Callbacks(\"memory\") ]\n\t\t\t],\n\t\t\tstate = \"pending\",\n\t\t\tpromise = {\n\t\t\t\tstate: function() {\n\t\t\t\t\treturn state;\n\t\t\t\t},\n\t\t\t\talways: function() {\n\t\t\t\t\tdeferred.done( arguments ).fail( arguments );\n\t\t\t\t\treturn this;\n\t\t\t\t},\n\t\t\t\tthen: function( /* fnDone, fnFail, fnProgress */ ) {\n\t\t\t\t\tvar fns = arguments;\n\t\t\t\t\treturn jQuery.Deferred(function( newDefer ) {\n\t\t\t\t\t\tjQuery.each( tuples, function( i, tuple ) {\n\t\t\t\t\t\t\tvar fn = jQuery.isFunction( fns[ i ] ) && fns[ i ];\n\t\t\t\t\t\t\t// deferred[ done | fail | progress ] for forwarding actions to newDefer\n\t\t\t\t\t\t\tdeferred[ tuple[1] ](function() {\n\t\t\t\t\t\t\t\tvar returned = fn && fn.apply( this, arguments );\n\t\t\t\t\t\t\t\tif ( returned && jQuery.isFunction( returned.promise ) ) {\n\t\t\t\t\t\t\t\t\treturned.promise()\n\t\t\t\t\t\t\t\t\t\t.done( newDefer.resolve )\n\t\t\t\t\t\t\t\t\t\t.fail( newDefer.reject )\n\t\t\t\t\t\t\t\t\t\t.progress( newDefer.notify );\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tnewDefer[ tuple[ 0 ] + \"With\" ]( this === promise ? newDefer.promise() : this, fn ? [ returned ] : arguments );\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t});\n\t\t\t\t\t\tfns = null;\n\t\t\t\t\t}).promise();\n\t\t\t\t},\n\t\t\t\t// Get a promise for this deferred\n\t\t\t\t// If obj is provided, the promise aspect is added to the object\n\t\t\t\tpromise: function( obj ) {\n\t\t\t\t\treturn obj != null ? jQuery.extend( obj, promise ) : promise;\n\t\t\t\t}\n\t\t\t},\n\t\t\tdeferred = {};\n\n\t\t// Keep pipe for back-compat\n\t\tpromise.pipe = promise.then;\n\n\t\t// Add list-specific methods\n\t\tjQuery.each( tuples, function( i, tuple ) {\n\t\t\tvar list = tuple[ 2 ],\n\t\t\t\tstateString = tuple[ 3 ];\n\n\t\t\t// promise[ done | fail | progress ] = list.add\n\t\t\tpromise[ tuple[1] ] = list.add;\n\n\t\t\t// Handle state\n\t\t\tif ( stateString ) {\n\t\t\t\tlist.add(function() {\n\t\t\t\t\t// state = [ resolved | rejected ]\n\t\t\t\t\tstate = stateString;\n\n\t\t\t\t// [ reject_list | resolve_list ].disable; progress_list.lock\n\t\t\t\t}, tuples[ i ^ 1 ][ 2 ].disable, tuples[ 2 ][ 2 ].lock );\n\t\t\t}\n\n\t\t\t// deferred[ resolve | reject | notify ]\n\t\t\tdeferred[ tuple[0] ] = function() {\n\t\t\t\tdeferred[ tuple[0] + \"With\" ]( this === deferred ? promise : this, arguments );\n\t\t\t\treturn this;\n\t\t\t};\n\t\t\tdeferred[ tuple[0] + \"With\" ] = list.fireWith;\n\t\t});\n\n\t\t// Make the deferred a promise\n\t\tpromise.promise( deferred );\n\n\t\t// Call given func if any\n\t\tif ( func ) {\n\t\t\tfunc.call( deferred, deferred );\n\t\t}\n\n\t\t// All done!\n\t\treturn deferred;\n\t},\n\n\t// Deferred helper\n\twhen: function( subordinate /* , ..., subordinateN */ ) {\n\t\tvar i = 0,\n\t\t\tresolveValues = slice.call( arguments ),\n\t\t\tlength = resolveValues.length,\n\n\t\t\t// the count of uncompleted subordinates\n\t\t\tremaining = length !== 1 || ( subordinate && jQuery.isFunction( subordinate.promise ) ) ? length : 0,\n\n\t\t\t// the master Deferred. If resolveValues consist of only a single Deferred, just use that.\n\t\t\tdeferred = remaining === 1 ? subordinate : jQuery.Deferred(),\n\n\t\t\t// Update function for both resolve and progress values\n\t\t\tupdateFunc = function( i, contexts, values ) {\n\t\t\t\treturn function( value ) {\n\t\t\t\t\tcontexts[ i ] = this;\n\t\t\t\t\tvalues[ i ] = arguments.length > 1 ? slice.call( arguments ) : value;\n\t\t\t\t\tif ( values === progressValues ) {\n\t\t\t\t\t\tdeferred.notifyWith( contexts, values );\n\n\t\t\t\t\t} else if ( !(--remaining) ) {\n\t\t\t\t\t\tdeferred.resolveWith( contexts, values );\n\t\t\t\t\t}\n\t\t\t\t};\n\t\t\t},\n\n\t\t\tprogressValues, progressContexts, resolveContexts;\n\n\t\t// add listeners to Deferred subordinates; treat others as resolved\n\t\tif ( length > 1 ) {\n\t\t\tprogressValues = new Array( length );\n\t\t\tprogressContexts = new Array( length );\n\t\t\tresolveContexts = new Array( length );\n\t\t\tfor ( ; i < length; i++ ) {\n\t\t\t\tif ( resolveValues[ i ] && jQuery.isFunction( resolveValues[ i ].promise ) ) {\n\t\t\t\t\tresolveValues[ i ].promise()\n\t\t\t\t\t\t.done( updateFunc( i, resolveContexts, resolveValues ) )\n\t\t\t\t\t\t.fail( deferred.reject )\n\t\t\t\t\t\t.progress( updateFunc( i, progressContexts, progressValues ) );\n\t\t\t\t} else {\n\t\t\t\t\t--remaining;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// if we're not waiting on anything, resolve the master\n\t\tif ( !remaining ) {\n\t\t\tdeferred.resolveWith( resolveContexts, resolveValues );\n\t\t}\n\n\t\treturn deferred.promise();\n\t}\n});\n\n\n// The deferred used on DOM ready\nvar readyList;\n\njQuery.fn.ready = function( fn ) {\n\t// Add the callback\n\tjQuery.ready.promise().done( fn );\n\n\treturn this;\n};\n\njQuery.extend({\n\t// Is the DOM ready to be used? Set to true once it occurs.\n\tisReady: false,\n\n\t// A counter to track how many items to wait for before\n\t// the ready event fires. See #6781\n\treadyWait: 1,\n\n\t// Hold (or release) the ready event\n\tholdReady: function( hold ) {\n\t\tif ( hold ) {\n\t\t\tjQuery.readyWait++;\n\t\t} else {\n\t\t\tjQuery.ready( true );\n\t\t}\n\t},\n\n\t// Handle when the DOM is ready\n\tready: function( wait ) {\n\n\t\t// Abort if there are pending holds or we're already ready\n\t\tif ( wait === true ? --jQuery.readyWait : jQuery.isReady ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443).\n\t\tif ( !document.body ) {\n\t\t\treturn setTimeout( jQuery.ready );\n\t\t}\n\n\t\t// Remember that the DOM is ready\n\t\tjQuery.isReady = true;\n\n\t\t// If a normal DOM Ready event fired, decrement, and wait if need be\n\t\tif ( wait !== true && --jQuery.readyWait > 0 ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// If there are functions bound, to execute\n\t\treadyList.resolveWith( document, [ jQuery ] );\n\n\t\t// Trigger any bound ready events\n\t\tif ( jQuery.fn.triggerHandler ) {\n\t\t\tjQuery( document ).triggerHandler( \"ready\" );\n\t\t\tjQuery( document ).off( \"ready\" );\n\t\t}\n\t}\n});\n\n/**\n * Clean-up method for dom ready events\n */\nfunction detach() {\n\tif ( document.addEventListener ) {\n\t\tdocument.removeEventListener( \"DOMContentLoaded\", completed, false );\n\t\twindow.removeEventListener( \"load\", completed, false );\n\n\t} else {\n\t\tdocument.detachEvent( \"onreadystatechange\", completed );\n\t\twindow.detachEvent( \"onload\", completed );\n\t}\n}\n\n/**\n * The ready event handler and self cleanup method\n */\nfunction completed() {\n\t// readyState === \"complete\" is good enough for us to call the dom ready in oldIE\n\tif ( document.addEventListener || event.type === \"load\" || document.readyState === \"complete\" ) {\n\t\tdetach();\n\t\tjQuery.ready();\n\t}\n}\n\njQuery.ready.promise = function( obj ) {\n\tif ( !readyList ) {\n\n\t\treadyList = jQuery.Deferred();\n\n\t\t// Catch cases where $(document).ready() is called after the browser event has already occurred.\n\t\t// we once tried to use readyState \"interactive\" here, but it caused issues like the one\n\t\t// discovered by ChrisS here: http://bugs.jquery.com/ticket/12282#comment:15\n\t\tif ( document.readyState === \"complete\" ) {\n\t\t\t// Handle it asynchronously to allow scripts the opportunity to delay ready\n\t\t\tsetTimeout( jQuery.ready );\n\n\t\t// Standards-based browsers support DOMContentLoaded\n\t\t} else if ( document.addEventListener ) {\n\t\t\t// Use the handy event callback\n\t\t\tdocument.addEventListener( \"DOMContentLoaded\", completed, false );\n\n\t\t\t// A fallback to window.onload, that will always work\n\t\t\twindow.addEventListener( \"load\", completed, false );\n\n\t\t// If IE event model is used\n\t\t} else {\n\t\t\t// Ensure firing before onload, maybe late but safe also for iframes\n\t\t\tdocument.attachEvent( \"onreadystatechange\", completed );\n\n\t\t\t// A fallback to window.onload, that will always work\n\t\t\twindow.attachEvent( \"onload\", completed );\n\n\t\t\t// If IE and not a frame\n\t\t\t// continually check to see if the document is ready\n\t\t\tvar top = false;\n\n\t\t\ttry {\n\t\t\t\ttop = window.frameElement == null && document.documentElement;\n\t\t\t} catch(e) {}\n\n\t\t\tif ( top && top.doScroll ) {\n\t\t\t\t(function doScrollCheck() {\n\t\t\t\t\tif ( !jQuery.isReady ) {\n\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t// Use the trick by Diego Perini\n\t\t\t\t\t\t\t// http://javascript.nwbox.com/IEContentLoaded/\n\t\t\t\t\t\t\ttop.doScroll(\"left\");\n\t\t\t\t\t\t} catch(e) {\n\t\t\t\t\t\t\treturn setTimeout( doScrollCheck, 50 );\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// detach all dom ready events\n\t\t\t\t\t\tdetach();\n\n\t\t\t\t\t\t// and execute any waiting functions\n\t\t\t\t\t\tjQuery.ready();\n\t\t\t\t\t}\n\t\t\t\t})();\n\t\t\t}\n\t\t}\n\t}\n\treturn readyList.promise( obj );\n};\n\n\nvar strundefined = typeof undefined;\n\n\n\n// Support: IE<9\n// Iteration over object's inherited properties before its own\nvar i;\nfor ( i in jQuery( support ) ) {\n\tbreak;\n}\nsupport.ownLast = i !== \"0\";\n\n// Note: most support tests are defined in their respective modules.\n// false until the test is run\nsupport.inlineBlockNeedsLayout = false;\n\n// Execute ASAP in case we need to set body.style.zoom\njQuery(function() {\n\t// Minified: var a,b,c,d\n\tvar val, div, body, container;\n\n\tbody = document.getElementsByTagName( \"body\" )[ 0 ];\n\tif ( !body || !body.style ) {\n\t\t// Return for frameset docs that don't have a body\n\t\treturn;\n\t}\n\n\t// Setup\n\tdiv = document.createElement( \"div\" );\n\tcontainer = document.createElement( \"div\" );\n\tcontainer.style.cssText = \"position:absolute;border:0;width:0;height:0;top:0;left:-9999px\";\n\tbody.appendChild( container ).appendChild( div );\n\n\tif ( typeof div.style.zoom !== strundefined ) {\n\t\t// Support: IE<8\n\t\t// Check if natively block-level elements act like inline-block\n\t\t// elements when setting their display to 'inline' and giving\n\t\t// them layout\n\t\tdiv.style.cssText = \"display:inline;margin:0;border:0;padding:1px;width:1px;zoom:1\";\n\n\t\tsupport.inlineBlockNeedsLayout = val = div.offsetWidth === 3;\n\t\tif ( val ) {\n\t\t\t// Prevent IE 6 from affecting layout for positioned elements #11048\n\t\t\t// Prevent IE from shrinking the body in IE 7 mode #12869\n\t\t\t// Support: IE<8\n\t\t\tbody.style.zoom = 1;\n\t\t}\n\t}\n\n\tbody.removeChild( container );\n});\n\n\n\n\n(function() {\n\tvar div = document.createElement( \"div\" );\n\n\t// Execute the test only if not already executed in another module.\n\tif (support.deleteExpando == null) {\n\t\t// Support: IE<9\n\t\tsupport.deleteExpando = true;\n\t\ttry {\n\t\t\tdelete div.test;\n\t\t} catch( e ) {\n\t\t\tsupport.deleteExpando = false;\n\t\t}\n\t}\n\n\t// Null elements to avoid leaks in IE.\n\tdiv = null;\n})();\n\n\n/**\n * Determines whether an object can have data\n */\njQuery.acceptData = function( elem ) {\n\tvar noData = jQuery.noData[ (elem.nodeName + \" \").toLowerCase() ],\n\t\tnodeType = +elem.nodeType || 1;\n\n\t// Do not set data on non-element DOM nodes because it will not be cleared (#8335).\n\treturn nodeType !== 1 && nodeType !== 9 ?\n\t\tfalse :\n\n\t\t// Nodes accept data unless otherwise specified; rejection can be conditional\n\t\t!noData || noData !== true && elem.getAttribute(\"classid\") === noData;\n};\n\n\nvar rbrace = /^(?:\\{[\\w\\W]*\\}|\\[[\\w\\W]*\\])$/,\n\trmultiDash = /([A-Z])/g;\n\nfunction dataAttr( elem, key, data ) {\n\t// If nothing was found internally, try to fetch any\n\t// data from the HTML5 data-* attribute\n\tif ( data === undefined && elem.nodeType === 1 ) {\n\n\t\tvar name = \"data-\" + key.replace( rmultiDash, \"-$1\" ).toLowerCase();\n\n\t\tdata = elem.getAttribute( name );\n\n\t\tif ( typeof data === \"string\" ) {\n\t\t\ttry {\n\t\t\t\tdata = data === \"true\" ? true :\n\t\t\t\t\tdata === \"false\" ? false :\n\t\t\t\t\tdata === \"null\" ? null :\n\t\t\t\t\t// Only convert to a number if it doesn't change the string\n\t\t\t\t\t+data + \"\" === data ? +data :\n\t\t\t\t\trbrace.test( data ) ? jQuery.parseJSON( data ) :\n\t\t\t\t\tdata;\n\t\t\t} catch( e ) {}\n\n\t\t\t// Make sure we set the data so it isn't changed later\n\t\t\tjQuery.data( elem, key, data );\n\n\t\t} else {\n\t\t\tdata = undefined;\n\t\t}\n\t}\n\n\treturn data;\n}\n\n// checks a cache object for emptiness\nfunction isEmptyDataObject( obj ) {\n\tvar name;\n\tfor ( name in obj ) {\n\n\t\t// if the public data object is empty, the private is still empty\n\t\tif ( name === \"data\" && jQuery.isEmptyObject( obj[name] ) ) {\n\t\t\tcontinue;\n\t\t}\n\t\tif ( name !== \"toJSON\" ) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\treturn true;\n}\n\nfunction internalData( elem, name, data, pvt /* Internal Use Only */ ) {\n\tif ( !jQuery.acceptData( elem ) ) {\n\t\treturn;\n\t}\n\n\tvar ret, thisCache,\n\t\tinternalKey = jQuery.expando,\n\n\t\t// We have to handle DOM nodes and JS objects differently because IE6-7\n\t\t// can't GC object references properly across the DOM-JS boundary\n\t\tisNode = elem.nodeType,\n\n\t\t// Only DOM nodes need the global jQuery cache; JS object data is\n\t\t// attached directly to the object so GC can occur automatically\n\t\tcache = isNode ? jQuery.cache : elem,\n\n\t\t// Only defining an ID for JS objects if its cache already exists allows\n\t\t// the code to shortcut on the same path as a DOM node with no cache\n\t\tid = isNode ? elem[ internalKey ] : elem[ internalKey ] && internalKey;\n\n\t// Avoid doing any more work than we need to when trying to get data on an\n\t// object that has no data at all\n\tif ( (!id || !cache[id] || (!pvt && !cache[id].data)) && data === undefined && typeof name === \"string\" ) {\n\t\treturn;\n\t}\n\n\tif ( !id ) {\n\t\t// Only DOM nodes need a new unique ID for each element since their data\n\t\t// ends up in the global cache\n\t\tif ( isNode ) {\n\t\t\tid = elem[ internalKey ] = deletedIds.pop() || jQuery.guid++;\n\t\t} else {\n\t\t\tid = internalKey;\n\t\t}\n\t}\n\n\tif ( !cache[ id ] ) {\n\t\t// Avoid exposing jQuery metadata on plain JS objects when the object\n\t\t// is serialized using JSON.stringify\n\t\tcache[ id ] = isNode ? {} : { toJSON: jQuery.noop };\n\t}\n\n\t// An object can be passed to jQuery.data instead of a key/value pair; this gets\n\t// shallow copied over onto the existing cache\n\tif ( typeof name === \"object\" || typeof name === \"function\" ) {\n\t\tif ( pvt ) {\n\t\t\tcache[ id ] = jQuery.extend( cache[ id ], name );\n\t\t} else {\n\t\t\tcache[ id ].data = jQuery.extend( cache[ id ].data, name );\n\t\t}\n\t}\n\n\tthisCache = cache[ id ];\n\n\t// jQuery data() is stored in a separate object inside the object's internal data\n\t// cache in order to avoid key collisions between internal data and user-defined\n\t// data.\n\tif ( !pvt ) {\n\t\tif ( !thisCache.data ) {\n\t\t\tthisCache.data = {};\n\t\t}\n\n\t\tthisCache = thisCache.data;\n\t}\n\n\tif ( data !== undefined ) {\n\t\tthisCache[ jQuery.camelCase( name ) ] = data;\n\t}\n\n\t// Check for both converted-to-camel and non-converted data property names\n\t// If a data property was specified\n\tif ( typeof name === \"string\" ) {\n\n\t\t// First Try to find as-is property data\n\t\tret = thisCache[ name ];\n\n\t\t// Test for null|undefined property data\n\t\tif ( ret == null ) {\n\n\t\t\t// Try to find the camelCased property\n\t\t\tret = thisCache[ jQuery.camelCase( name ) ];\n\t\t}\n\t} else {\n\t\tret = thisCache;\n\t}\n\n\treturn ret;\n}\n\nfunction internalRemoveData( elem, name, pvt ) {\n\tif ( !jQuery.acceptData( elem ) ) {\n\t\treturn;\n\t}\n\n\tvar thisCache, i,\n\t\tisNode = elem.nodeType,\n\n\t\t// See jQuery.data for more information\n\t\tcache = isNode ? jQuery.cache : elem,\n\t\tid = isNode ? elem[ jQuery.expando ] : jQuery.expando;\n\n\t// If there is already no cache entry for this object, there is no\n\t// purpose in continuing\n\tif ( !cache[ id ] ) {\n\t\treturn;\n\t}\n\n\tif ( name ) {\n\n\t\tthisCache = pvt ? cache[ id ] : cache[ id ].data;\n\n\t\tif ( thisCache ) {\n\n\t\t\t// Support array or space separated string names for data keys\n\t\t\tif ( !jQuery.isArray( name ) ) {\n\n\t\t\t\t// try the string as a key before any manipulation\n\t\t\t\tif ( name in thisCache ) {\n\t\t\t\t\tname = [ name ];\n\t\t\t\t} else {\n\n\t\t\t\t\t// split the camel cased version by spaces unless a key with the spaces exists\n\t\t\t\t\tname = jQuery.camelCase( name );\n\t\t\t\t\tif ( name in thisCache ) {\n\t\t\t\t\t\tname = [ name ];\n\t\t\t\t\t} else {\n\t\t\t\t\t\tname = name.split(\" \");\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// If \"name\" is an array of keys...\n\t\t\t\t// When data is initially created, via (\"key\", \"val\") signature,\n\t\t\t\t// keys will be converted to camelCase.\n\t\t\t\t// Since there is no way to tell _how_ a key was added, remove\n\t\t\t\t// both plain key and camelCase key. #12786\n\t\t\t\t// This will only penalize the array argument path.\n\t\t\t\tname = name.concat( jQuery.map( name, jQuery.camelCase ) );\n\t\t\t}\n\n\t\t\ti = name.length;\n\t\t\twhile ( i-- ) {\n\t\t\t\tdelete thisCache[ name[i] ];\n\t\t\t}\n\n\t\t\t// If there is no data left in the cache, we want to continue\n\t\t\t// and let the cache object itself get destroyed\n\t\t\tif ( pvt ? !isEmptyDataObject(thisCache) : !jQuery.isEmptyObject(thisCache) ) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t}\n\n\t// See jQuery.data for more information\n\tif ( !pvt ) {\n\t\tdelete cache[ id ].data;\n\n\t\t// Don't destroy the parent cache unless the internal data object\n\t\t// had been the only thing left in it\n\t\tif ( !isEmptyDataObject( cache[ id ] ) ) {\n\t\t\treturn;\n\t\t}\n\t}\n\n\t// Destroy the cache\n\tif ( isNode ) {\n\t\tjQuery.cleanData( [ elem ], true );\n\n\t// Use delete when supported for expandos or `cache` is not a window per isWindow (#10080)\n\t/* jshint eqeqeq: false */\n\t} else if ( support.deleteExpando || cache != cache.window ) {\n\t\t/* jshint eqeqeq: true */\n\t\tdelete cache[ id ];\n\n\t// When all else fails, null\n\t} else {\n\t\tcache[ id ] = null;\n\t}\n}\n\njQuery.extend({\n\tcache: {},\n\n\t// The following elements (space-suffixed to avoid Object.prototype collisions)\n\t// throw uncatchable exceptions if you attempt to set expando properties\n\tnoData: {\n\t\t\"applet \": true,\n\t\t\"embed \": true,\n\t\t// ...but Flash objects (which have this classid) *can* handle expandos\n\t\t\"object \": \"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000\"\n\t},\n\n\thasData: function( elem ) {\n\t\telem = elem.nodeType ? jQuery.cache[ elem[jQuery.expando] ] : elem[ jQuery.expando ];\n\t\treturn !!elem && !isEmptyDataObject( elem );\n\t},\n\n\tdata: function( elem, name, data ) {\n\t\treturn internalData( elem, name, data );\n\t},\n\n\tremoveData: function( elem, name ) {\n\t\treturn internalRemoveData( elem, name );\n\t},\n\n\t// For internal use only.\n\t_data: function( elem, name, data ) {\n\t\treturn internalData( elem, name, data, true );\n\t},\n\n\t_removeData: function( elem, name ) {\n\t\treturn internalRemoveData( elem, name, true );\n\t}\n});\n\njQuery.fn.extend({\n\tdata: function( key, value ) {\n\t\tvar i, name, data,\n\t\t\telem = this[0],\n\t\t\tattrs = elem && elem.attributes;\n\n\t\t// Special expections of .data basically thwart jQuery.access,\n\t\t// so implement the relevant behavior ourselves\n\n\t\t// Gets all values\n\t\tif ( key === undefined ) {\n\t\t\tif ( this.length ) {\n\t\t\t\tdata = jQuery.data( elem );\n\n\t\t\t\tif ( elem.nodeType === 1 && !jQuery._data( elem, \"parsedAttrs\" ) ) {\n\t\t\t\t\ti = attrs.length;\n\t\t\t\t\twhile ( i-- ) {\n\n\t\t\t\t\t\t// Support: IE11+\n\t\t\t\t\t\t// The attrs elements can be null (#14894)\n\t\t\t\t\t\tif ( attrs[ i ] ) {\n\t\t\t\t\t\t\tname = attrs[ i ].name;\n\t\t\t\t\t\t\tif ( name.indexOf( \"data-\" ) === 0 ) {\n\t\t\t\t\t\t\t\tname = jQuery.camelCase( name.slice(5) );\n\t\t\t\t\t\t\t\tdataAttr( elem, name, data[ name ] );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tjQuery._data( elem, \"parsedAttrs\", true );\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn data;\n\t\t}\n\n\t\t// Sets multiple values\n\t\tif ( typeof key === \"object\" ) {\n\t\t\treturn this.each(function() {\n\t\t\t\tjQuery.data( this, key );\n\t\t\t});\n\t\t}\n\n\t\treturn arguments.length > 1 ?\n\n\t\t\t// Sets one value\n\t\t\tthis.each(function() {\n\t\t\t\tjQuery.data( this, key, value );\n\t\t\t}) :\n\n\t\t\t// Gets one value\n\t\t\t// Try to fetch any internally stored data first\n\t\t\telem ? dataAttr( elem, key, jQuery.data( elem, key ) ) : undefined;\n\t},\n\n\tremoveData: function( key ) {\n\t\treturn this.each(function() {\n\t\t\tjQuery.removeData( this, key );\n\t\t});\n\t}\n});\n\n\njQuery.extend({\n\tqueue: function( elem, type, data ) {\n\t\tvar queue;\n\n\t\tif ( elem ) {\n\t\t\ttype = ( type || \"fx\" ) + \"queue\";\n\t\t\tqueue = jQuery._data( elem, type );\n\n\t\t\t// Speed up dequeue by getting out quickly if this is just a lookup\n\t\t\tif ( data ) {\n\t\t\t\tif ( !queue || jQuery.isArray(data) ) {\n\t\t\t\t\tqueue = jQuery._data( elem, type, jQuery.makeArray(data) );\n\t\t\t\t} else {\n\t\t\t\t\tqueue.push( data );\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn queue || [];\n\t\t}\n\t},\n\n\tdequeue: function( elem, type ) {\n\t\ttype = type || \"fx\";\n\n\t\tvar queue = jQuery.queue( elem, type ),\n\t\t\tstartLength = queue.length,\n\t\t\tfn = queue.shift(),\n\t\t\thooks = jQuery._queueHooks( elem, type ),\n\t\t\tnext = function() {\n\t\t\t\tjQuery.dequeue( elem, type );\n\t\t\t};\n\n\t\t// If the fx queue is dequeued, always remove the progress sentinel\n\t\tif ( fn === \"inprogress\" ) {\n\t\t\tfn = queue.shift();\n\t\t\tstartLength--;\n\t\t}\n\n\t\tif ( fn ) {\n\n\t\t\t// Add a progress sentinel to prevent the fx queue from being\n\t\t\t// automatically dequeued\n\t\t\tif ( type === \"fx\" ) {\n\t\t\t\tqueue.unshift( \"inprogress\" );\n\t\t\t}\n\n\t\t\t// clear up the last queue stop function\n\t\t\tdelete hooks.stop;\n\t\t\tfn.call( elem, next, hooks );\n\t\t}\n\n\t\tif ( !startLength && hooks ) {\n\t\t\thooks.empty.fire();\n\t\t}\n\t},\n\n\t// not intended for public consumption - generates a queueHooks object, or returns the current one\n\t_queueHooks: function( elem, type ) {\n\t\tvar key = type + \"queueHooks\";\n\t\treturn jQuery._data( elem, key ) || jQuery._data( elem, key, {\n\t\t\tempty: jQuery.Callbacks(\"once memory\").add(function() {\n\t\t\t\tjQuery._removeData( elem, type + \"queue\" );\n\t\t\t\tjQuery._removeData( elem, key );\n\t\t\t})\n\t\t});\n\t}\n});\n\njQuery.fn.extend({\n\tqueue: function( type, data ) {\n\t\tvar setter = 2;\n\n\t\tif ( typeof type !== \"string\" ) {\n\t\t\tdata = type;\n\t\t\ttype = \"fx\";\n\t\t\tsetter--;\n\t\t}\n\n\t\tif ( arguments.length < setter ) {\n\t\t\treturn jQuery.queue( this[0], type );\n\t\t}\n\n\t\treturn data === undefined ?\n\t\t\tthis :\n\t\t\tthis.each(function() {\n\t\t\t\tvar queue = jQuery.queue( this, type, data );\n\n\t\t\t\t// ensure a hooks for this queue\n\t\t\t\tjQuery._queueHooks( this, type );\n\n\t\t\t\tif ( type === \"fx\" && queue[0] !== \"inprogress\" ) {\n\t\t\t\t\tjQuery.dequeue( this, type );\n\t\t\t\t}\n\t\t\t});\n\t},\n\tdequeue: function( type ) {\n\t\treturn this.each(function() {\n\t\t\tjQuery.dequeue( this, type );\n\t\t});\n\t},\n\tclearQueue: function( type ) {\n\t\treturn this.queue( type || \"fx\", [] );\n\t},\n\t// Get a promise resolved when queues of a certain type\n\t// are emptied (fx is the type by default)\n\tpromise: function( type, obj ) {\n\t\tvar tmp,\n\t\t\tcount = 1,\n\t\t\tdefer = jQuery.Deferred(),\n\t\t\telements = this,\n\t\t\ti = this.length,\n\t\t\tresolve = function() {\n\t\t\t\tif ( !( --count ) ) {\n\t\t\t\t\tdefer.resolveWith( elements, [ elements ] );\n\t\t\t\t}\n\t\t\t};\n\n\t\tif ( typeof type !== \"string\" ) {\n\t\t\tobj = type;\n\t\t\ttype = undefined;\n\t\t}\n\t\ttype = type || \"fx\";\n\n\t\twhile ( i-- ) {\n\t\t\ttmp = jQuery._data( elements[ i ], type + \"queueHooks\" );\n\t\t\tif ( tmp && tmp.empty ) {\n\t\t\t\tcount++;\n\t\t\t\ttmp.empty.add( resolve );\n\t\t\t}\n\t\t}\n\t\tresolve();\n\t\treturn defer.promise( obj );\n\t}\n});\nvar pnum = (/[+-]?(?:\\d*\\.|)\\d+(?:[eE][+-]?\\d+|)/).source;\n\nvar cssExpand = [ \"Top\", \"Right\", \"Bottom\", \"Left\" ];\n\nvar isHidden = function( elem, el ) {\n\t\t// isHidden might be called from jQuery#filter function;\n\t\t// in that case, element will be second argument\n\t\telem = el || elem;\n\t\treturn jQuery.css( elem, \"display\" ) === \"none\" || !jQuery.contains( elem.ownerDocument, elem );\n\t};\n\n\n\n// Multifunctional method to get and set values of a collection\n// The value/s can optionally be executed if it's a function\nvar access = jQuery.access = function( elems, fn, key, value, chainable, emptyGet, raw ) {\n\tvar i = 0,\n\t\tlength = elems.length,\n\t\tbulk = key == null;\n\n\t// Sets many values\n\tif ( jQuery.type( key ) === \"object\" ) {\n\t\tchainable = true;\n\t\tfor ( i in key ) {\n\t\t\tjQuery.access( elems, fn, i, key[i], true, emptyGet, raw );\n\t\t}\n\n\t// Sets one value\n\t} else if ( value !== undefined ) {\n\t\tchainable = true;\n\n\t\tif ( !jQuery.isFunction( value ) ) {\n\t\t\traw = true;\n\t\t}\n\n\t\tif ( bulk ) {\n\t\t\t// Bulk operations run against the entire set\n\t\t\tif ( raw ) {\n\t\t\t\tfn.call( elems, value );\n\t\t\t\tfn = null;\n\n\t\t\t// ...except when executing function values\n\t\t\t} else {\n\t\t\t\tbulk = fn;\n\t\t\t\tfn = function( elem, key, value ) {\n\t\t\t\t\treturn bulk.call( jQuery( elem ), value );\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\tif ( fn ) {\n\t\t\tfor ( ; i < length; i++ ) {\n\t\t\t\tfn( elems[i], key, raw ? value : value.call( elems[i], i, fn( elems[i], key ) ) );\n\t\t\t}\n\t\t}\n\t}\n\n\treturn chainable ?\n\t\telems :\n\n\t\t// Gets\n\t\tbulk ?\n\t\t\tfn.call( elems ) :\n\t\t\tlength ? fn( elems[0], key ) : emptyGet;\n};\nvar rcheckableType = (/^(?:checkbox|radio)$/i);\n\n\n\n(function() {\n\t// Minified: var a,b,c\n\tvar input = document.createElement( \"input\" ),\n\t\tdiv = document.createElement( \"div\" ),\n\t\tfragment = document.createDocumentFragment();\n\n\t// Setup\n\tdiv.innerHTML = \"  <link/><table></table><a href='/a'>a</a><input type='checkbox'/>\";\n\n\t// IE strips leading whitespace when .innerHTML is used\n\tsupport.leadingWhitespace = div.firstChild.nodeType === 3;\n\n\t// Make sure that tbody elements aren't automatically inserted\n\t// IE will insert them into empty tables\n\tsupport.tbody = !div.getElementsByTagName( \"tbody\" ).length;\n\n\t// Make sure that link elements get serialized correctly by innerHTML\n\t// This requires a wrapper element in IE\n\tsupport.htmlSerialize = !!div.getElementsByTagName( \"link\" ).length;\n\n\t// Makes sure cloning an html5 element does not cause problems\n\t// Where outerHTML is undefined, this still works\n\tsupport.html5Clone =\n\t\tdocument.createElement( \"nav\" ).cloneNode( true ).outerHTML !== \"<:nav></:nav>\";\n\n\t// Check if a disconnected checkbox will retain its checked\n\t// value of true after appended to the DOM (IE6/7)\n\tinput.type = \"checkbox\";\n\tinput.checked = true;\n\tfragment.appendChild( input );\n\tsupport.appendChecked = input.checked;\n\n\t// Make sure textarea (and checkbox) defaultValue is properly cloned\n\t// Support: IE6-IE11+\n\tdiv.innerHTML = \"<textarea>x</textarea>\";\n\tsupport.noCloneChecked = !!div.cloneNode( true ).lastChild.defaultValue;\n\n\t// #11217 - WebKit loses check when the name is after the checked attribute\n\tfragment.appendChild( div );\n\tdiv.innerHTML = \"<input type='radio' checked='checked' name='t'/>\";\n\n\t// Support: Safari 5.1, iOS 5.1, Android 4.x, Android 2.3\n\t// old WebKit doesn't clone checked state correctly in fragments\n\tsupport.checkClone = div.cloneNode( true ).cloneNode( true ).lastChild.checked;\n\n\t// Support: IE<9\n\t// Opera does not clone events (and typeof div.attachEvent === undefined).\n\t// IE9-10 clones events bound via attachEvent, but they don't trigger with .click()\n\tsupport.noCloneEvent = true;\n\tif ( div.attachEvent ) {\n\t\tdiv.attachEvent( \"onclick\", function() {\n\t\t\tsupport.noCloneEvent = false;\n\t\t});\n\n\t\tdiv.cloneNode( true ).click();\n\t}\n\n\t// Execute the test only if not already executed in another module.\n\tif (support.deleteExpando == null) {\n\t\t// Support: IE<9\n\t\tsupport.deleteExpando = true;\n\t\ttry {\n\t\t\tdelete div.test;\n\t\t} catch( e ) {\n\t\t\tsupport.deleteExpando = false;\n\t\t}\n\t}\n})();\n\n\n(function() {\n\tvar i, eventName,\n\t\tdiv = document.createElement( \"div\" );\n\n\t// Support: IE<9 (lack submit/change bubble), Firefox 23+ (lack focusin event)\n\tfor ( i in { submit: true, change: true, focusin: true }) {\n\t\teventName = \"on\" + i;\n\n\t\tif ( !(support[ i + \"Bubbles\" ] = eventName in window) ) {\n\t\t\t// Beware of CSP restrictions (https://developer.mozilla.org/en/Security/CSP)\n\t\t\tdiv.setAttribute( eventName, \"t\" );\n\t\t\tsupport[ i + \"Bubbles\" ] = div.attributes[ eventName ].expando === false;\n\t\t}\n\t}\n\n\t// Null elements to avoid leaks in IE.\n\tdiv = null;\n})();\n\n\nvar rformElems = /^(?:input|select|textarea)$/i,\n\trkeyEvent = /^key/,\n\trmouseEvent = /^(?:mouse|pointer|contextmenu)|click/,\n\trfocusMorph = /^(?:focusinfocus|focusoutblur)$/,\n\trtypenamespace = /^([^.]*)(?:\\.(.+)|)$/;\n\nfunction returnTrue() {\n\treturn true;\n}\n\nfunction returnFalse() {\n\treturn false;\n}\n\nfunction safeActiveElement() {\n\ttry {\n\t\treturn document.activeElement;\n\t} catch ( err ) { }\n}\n\n/*\n * Helper functions for managing events -- not part of the public interface.\n * Props to Dean Edwards' addEvent library for many of the ideas.\n */\njQuery.event = {\n\n\tglobal: {},\n\n\tadd: function( elem, types, handler, data, selector ) {\n\t\tvar tmp, events, t, handleObjIn,\n\t\t\tspecial, eventHandle, handleObj,\n\t\t\thandlers, type, namespaces, origType,\n\t\t\telemData = jQuery._data( elem );\n\n\t\t// Don't attach events to noData or text/comment nodes (but allow plain objects)\n\t\tif ( !elemData ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Caller can pass in an object of custom data in lieu of the handler\n\t\tif ( handler.handler ) {\n\t\t\thandleObjIn = handler;\n\t\t\thandler = handleObjIn.handler;\n\t\t\tselector = handleObjIn.selector;\n\t\t}\n\n\t\t// Make sure that the handler has a unique ID, used to find/remove it later\n\t\tif ( !handler.guid ) {\n\t\t\thandler.guid = jQuery.guid++;\n\t\t}\n\n\t\t// Init the element's event structure and main handler, if this is the first\n\t\tif ( !(events = elemData.events) ) {\n\t\t\tevents = elemData.events = {};\n\t\t}\n\t\tif ( !(eventHandle = elemData.handle) ) {\n\t\t\teventHandle = elemData.handle = function( e ) {\n\t\t\t\t// Discard the second event of a jQuery.event.trigger() and\n\t\t\t\t// when an event is called after a page has unloaded\n\t\t\t\treturn typeof jQuery !== strundefined && (!e || jQuery.event.triggered !== e.type) ?\n\t\t\t\t\tjQuery.event.dispatch.apply( eventHandle.elem, arguments ) :\n\t\t\t\t\tundefined;\n\t\t\t};\n\t\t\t// Add elem as a property of the handle fn to prevent a memory leak with IE non-native events\n\t\t\teventHandle.elem = elem;\n\t\t}\n\n\t\t// Handle multiple events separated by a space\n\t\ttypes = ( types || \"\" ).match( rnotwhite ) || [ \"\" ];\n\t\tt = types.length;\n\t\twhile ( t-- ) {\n\t\t\ttmp = rtypenamespace.exec( types[t] ) || [];\n\t\t\ttype = origType = tmp[1];\n\t\t\tnamespaces = ( tmp[2] || \"\" ).split( \".\" ).sort();\n\n\t\t\t// There *must* be a type, no attaching namespace-only handlers\n\t\t\tif ( !type ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// If event changes its type, use the special event handlers for the changed type\n\t\t\tspecial = jQuery.event.special[ type ] || {};\n\n\t\t\t// If selector defined, determine special event api type, otherwise given type\n\t\t\ttype = ( selector ? special.delegateType : special.bindType ) || type;\n\n\t\t\t// Update special based on newly reset type\n\t\t\tspecial = jQuery.event.special[ type ] || {};\n\n\t\t\t// handleObj is passed to all event handlers\n\t\t\thandleObj = jQuery.extend({\n\t\t\t\ttype: type,\n\t\t\t\torigType: origType,\n\t\t\t\tdata: data,\n\t\t\t\thandler: handler,\n\t\t\t\tguid: handler.guid,\n\t\t\t\tselector: selector,\n\t\t\t\tneedsContext: selector && jQuery.expr.match.needsContext.test( selector ),\n\t\t\t\tnamespace: namespaces.join(\".\")\n\t\t\t}, handleObjIn );\n\n\t\t\t// Init the event handler queue if we're the first\n\t\t\tif ( !(handlers = events[ type ]) ) {\n\t\t\t\thandlers = events[ type ] = [];\n\t\t\t\thandlers.delegateCount = 0;\n\n\t\t\t\t// Only use addEventListener/attachEvent if the special events handler returns false\n\t\t\t\tif ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) {\n\t\t\t\t\t// Bind the global event handler to the element\n\t\t\t\t\tif ( elem.addEventListener ) {\n\t\t\t\t\t\telem.addEventListener( type, eventHandle, false );\n\n\t\t\t\t\t} else if ( elem.attachEvent ) {\n\t\t\t\t\t\telem.attachEvent( \"on\" + type, eventHandle );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif ( special.add ) {\n\t\t\t\tspecial.add.call( elem, handleObj );\n\n\t\t\t\tif ( !handleObj.handler.guid ) {\n\t\t\t\t\thandleObj.handler.guid = handler.guid;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Add to the element's handler list, delegates in front\n\t\t\tif ( selector ) {\n\t\t\t\thandlers.splice( handlers.delegateCount++, 0, handleObj );\n\t\t\t} else {\n\t\t\t\thandlers.push( handleObj );\n\t\t\t}\n\n\t\t\t// Keep track of which events have ever been used, for event optimization\n\t\t\tjQuery.event.global[ type ] = true;\n\t\t}\n\n\t\t// Nullify elem to prevent memory leaks in IE\n\t\telem = null;\n\t},\n\n\t// Detach an event or set of events from an element\n\tremove: function( elem, types, handler, selector, mappedTypes ) {\n\t\tvar j, handleObj, tmp,\n\t\t\torigCount, t, events,\n\t\t\tspecial, handlers, type,\n\t\t\tnamespaces, origType,\n\t\t\telemData = jQuery.hasData( elem ) && jQuery._data( elem );\n\n\t\tif ( !elemData || !(events = elemData.events) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Once for each type.namespace in types; type may be omitted\n\t\ttypes = ( types || \"\" ).match( rnotwhite ) || [ \"\" ];\n\t\tt = types.length;\n\t\twhile ( t-- ) {\n\t\t\ttmp = rtypenamespace.exec( types[t] ) || [];\n\t\t\ttype = origType = tmp[1];\n\t\t\tnamespaces = ( tmp[2] || \"\" ).split( \".\" ).sort();\n\n\t\t\t// Unbind all events (on this namespace, if provided) for the element\n\t\t\tif ( !type ) {\n\t\t\t\tfor ( type in events ) {\n\t\t\t\t\tjQuery.event.remove( elem, type + types[ t ], handler, selector, true );\n\t\t\t\t}\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tspecial = jQuery.event.special[ type ] || {};\n\t\t\ttype = ( selector ? special.delegateType : special.bindType ) || type;\n\t\t\thandlers = events[ type ] || [];\n\t\t\ttmp = tmp[2] && new RegExp( \"(^|\\\\.)\" + namespaces.join(\"\\\\.(?:.*\\\\.|)\") + \"(\\\\.|$)\" );\n\n\t\t\t// Remove matching events\n\t\t\torigCount = j = handlers.length;\n\t\t\twhile ( j-- ) {\n\t\t\t\thandleObj = handlers[ j ];\n\n\t\t\t\tif ( ( mappedTypes || origType === handleObj.origType ) &&\n\t\t\t\t\t( !handler || handler.guid === handleObj.guid ) &&\n\t\t\t\t\t( !tmp || tmp.test( handleObj.namespace ) ) &&\n\t\t\t\t\t( !selector || selector === handleObj.selector || selector === \"**\" && handleObj.selector ) ) {\n\t\t\t\t\thandlers.splice( j, 1 );\n\n\t\t\t\t\tif ( handleObj.selector ) {\n\t\t\t\t\t\thandlers.delegateCount--;\n\t\t\t\t\t}\n\t\t\t\t\tif ( special.remove ) {\n\t\t\t\t\t\tspecial.remove.call( elem, handleObj );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Remove generic event handler if we removed something and no more handlers exist\n\t\t\t// (avoids potential for endless recursion during removal of special event handlers)\n\t\t\tif ( origCount && !handlers.length ) {\n\t\t\t\tif ( !special.teardown || special.teardown.call( elem, namespaces, elemData.handle ) === false ) {\n\t\t\t\t\tjQuery.removeEvent( elem, type, elemData.handle );\n\t\t\t\t}\n\n\t\t\t\tdelete events[ type ];\n\t\t\t}\n\t\t}\n\n\t\t// Remove the expando if it's no longer used\n\t\tif ( jQuery.isEmptyObject( events ) ) {\n\t\t\tdelete elemData.handle;\n\n\t\t\t// removeData also checks for emptiness and clears the expando if empty\n\t\t\t// so use it instead of delete\n\t\t\tjQuery._removeData( elem, \"events\" );\n\t\t}\n\t},\n\n\ttrigger: function( event, data, elem, onlyHandlers ) {\n\t\tvar handle, ontype, cur,\n\t\t\tbubbleType, special, tmp, i,\n\t\t\teventPath = [ elem || document ],\n\t\t\ttype = hasOwn.call( event, \"type\" ) ? event.type : event,\n\t\t\tnamespaces = hasOwn.call( event, \"namespace\" ) ? event.namespace.split(\".\") : [];\n\n\t\tcur = tmp = elem = elem || document;\n\n\t\t// Don't do events on text and comment nodes\n\t\tif ( elem.nodeType === 3 || elem.nodeType === 8 ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// focus/blur morphs to focusin/out; ensure we're not firing them right now\n\t\tif ( rfocusMorph.test( type + jQuery.event.triggered ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\tif ( type.indexOf(\".\") >= 0 ) {\n\t\t\t// Namespaced trigger; create a regexp to match event type in handle()\n\t\t\tnamespaces = type.split(\".\");\n\t\t\ttype = namespaces.shift();\n\t\t\tnamespaces.sort();\n\t\t}\n\t\tontype = type.indexOf(\":\") < 0 && \"on\" + type;\n\n\t\t// Caller can pass in a jQuery.Event object, Object, or just an event type string\n\t\tevent = event[ jQuery.expando ] ?\n\t\t\tevent :\n\t\t\tnew jQuery.Event( type, typeof event === \"object\" && event );\n\n\t\t// Trigger bitmask: & 1 for native handlers; & 2 for jQuery (always true)\n\t\tevent.isTrigger = onlyHandlers ? 2 : 3;\n\t\tevent.namespace = namespaces.join(\".\");\n\t\tevent.namespace_re = event.namespace ?\n\t\t\tnew RegExp( \"(^|\\\\.)\" + namespaces.join(\"\\\\.(?:.*\\\\.|)\") + \"(\\\\.|$)\" ) :\n\t\t\tnull;\n\n\t\t// Clean up the event in case it is being reused\n\t\tevent.result = undefined;\n\t\tif ( !event.target ) {\n\t\t\tevent.target = elem;\n\t\t}\n\n\t\t// Clone any incoming data and prepend the event, creating the handler arg list\n\t\tdata = data == null ?\n\t\t\t[ event ] :\n\t\t\tjQuery.makeArray( data, [ event ] );\n\n\t\t// Allow special events to draw outside the lines\n\t\tspecial = jQuery.event.special[ type ] || {};\n\t\tif ( !onlyHandlers && special.trigger && special.trigger.apply( elem, data ) === false ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Determine event propagation path in advance, per W3C events spec (#9951)\n\t\t// Bubble up to document, then to window; watch for a global ownerDocument var (#9724)\n\t\tif ( !onlyHandlers && !special.noBubble && !jQuery.isWindow( elem ) ) {\n\n\t\t\tbubbleType = special.delegateType || type;\n\t\t\tif ( !rfocusMorph.test( bubbleType + type ) ) {\n\t\t\t\tcur = cur.parentNode;\n\t\t\t}\n\t\t\tfor ( ; cur; cur = cur.parentNode ) {\n\t\t\t\teventPath.push( cur );\n\t\t\t\ttmp = cur;\n\t\t\t}\n\n\t\t\t// Only add window if we got to document (e.g., not plain obj or detached DOM)\n\t\t\tif ( tmp === (elem.ownerDocument || document) ) {\n\t\t\t\teventPath.push( tmp.defaultView || tmp.parentWindow || window );\n\t\t\t}\n\t\t}\n\n\t\t// Fire handlers on the event path\n\t\ti = 0;\n\t\twhile ( (cur = eventPath[i++]) && !event.isPropagationStopped() ) {\n\n\t\t\tevent.type = i > 1 ?\n\t\t\t\tbubbleType :\n\t\t\t\tspecial.bindType || type;\n\n\t\t\t// jQuery handler\n\t\t\thandle = ( jQuery._data( cur, \"events\" ) || {} )[ event.type ] && jQuery._data( cur, \"handle\" );\n\t\t\tif ( handle ) {\n\t\t\t\thandle.apply( cur, data );\n\t\t\t}\n\n\t\t\t// Native handler\n\t\t\thandle = ontype && cur[ ontype ];\n\t\t\tif ( handle && handle.apply && jQuery.acceptData( cur ) ) {\n\t\t\t\tevent.result = handle.apply( cur, data );\n\t\t\t\tif ( event.result === false ) {\n\t\t\t\t\tevent.preventDefault();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tevent.type = type;\n\n\t\t// If nobody prevented the default action, do it now\n\t\tif ( !onlyHandlers && !event.isDefaultPrevented() ) {\n\n\t\t\tif ( (!special._default || special._default.apply( eventPath.pop(), data ) === false) &&\n\t\t\t\tjQuery.acceptData( elem ) ) {\n\n\t\t\t\t// Call a native DOM method on the target with the same name name as the event.\n\t\t\t\t// Can't use an .isFunction() check here because IE6/7 fails that test.\n\t\t\t\t// Don't do default actions on window, that's where global variables be (#6170)\n\t\t\t\tif ( ontype && elem[ type ] && !jQuery.isWindow( elem ) ) {\n\n\t\t\t\t\t// Don't re-trigger an onFOO event when we call its FOO() method\n\t\t\t\t\ttmp = elem[ ontype ];\n\n\t\t\t\t\tif ( tmp ) {\n\t\t\t\t\t\telem[ ontype ] = null;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Prevent re-triggering of the same event, since we already bubbled it above\n\t\t\t\t\tjQuery.event.triggered = type;\n\t\t\t\t\ttry {\n\t\t\t\t\t\telem[ type ]();\n\t\t\t\t\t} catch ( e ) {\n\t\t\t\t\t\t// IE<9 dies on focus/blur to hidden element (#1486,#12518)\n\t\t\t\t\t\t// only reproducible on winXP IE8 native, not IE9 in IE8 mode\n\t\t\t\t\t}\n\t\t\t\t\tjQuery.event.triggered = undefined;\n\n\t\t\t\t\tif ( tmp ) {\n\t\t\t\t\t\telem[ ontype ] = tmp;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn event.result;\n\t},\n\n\tdispatch: function( event ) {\n\n\t\t// Make a writable jQuery.Event from the native event object\n\t\tevent = jQuery.event.fix( event );\n\n\t\tvar i, ret, handleObj, matched, j,\n\t\t\thandlerQueue = [],\n\t\t\targs = slice.call( arguments ),\n\t\t\thandlers = ( jQuery._data( this, \"events\" ) || {} )[ event.type ] || [],\n\t\t\tspecial = jQuery.event.special[ event.type ] || {};\n\n\t\t// Use the fix-ed jQuery.Event rather than the (read-only) native event\n\t\targs[0] = event;\n\t\tevent.delegateTarget = this;\n\n\t\t// Call the preDispatch hook for the mapped type, and let it bail if desired\n\t\tif ( special.preDispatch && special.preDispatch.call( this, event ) === false ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Determine handlers\n\t\thandlerQueue = jQuery.event.handlers.call( this, event, handlers );\n\n\t\t// Run delegates first; they may want to stop propagation beneath us\n\t\ti = 0;\n\t\twhile ( (matched = handlerQueue[ i++ ]) && !event.isPropagationStopped() ) {\n\t\t\tevent.currentTarget = matched.elem;\n\n\t\t\tj = 0;\n\t\t\twhile ( (handleObj = matched.handlers[ j++ ]) && !event.isImmediatePropagationStopped() ) {\n\n\t\t\t\t// Triggered event must either 1) have no namespace, or\n\t\t\t\t// 2) have namespace(s) a subset or equal to those in the bound event (both can have no namespace).\n\t\t\t\tif ( !event.namespace_re || event.namespace_re.test( handleObj.namespace ) ) {\n\n\t\t\t\t\tevent.handleObj = handleObj;\n\t\t\t\t\tevent.data = handleObj.data;\n\n\t\t\t\t\tret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler )\n\t\t\t\t\t\t\t.apply( matched.elem, args );\n\n\t\t\t\t\tif ( ret !== undefined ) {\n\t\t\t\t\t\tif ( (event.result = ret) === false ) {\n\t\t\t\t\t\t\tevent.preventDefault();\n\t\t\t\t\t\t\tevent.stopPropagation();\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Call the postDispatch hook for the mapped type\n\t\tif ( special.postDispatch ) {\n\t\t\tspecial.postDispatch.call( this, event );\n\t\t}\n\n\t\treturn event.result;\n\t},\n\n\thandlers: function( event, handlers ) {\n\t\tvar sel, handleObj, matches, i,\n\t\t\thandlerQueue = [],\n\t\t\tdelegateCount = handlers.delegateCount,\n\t\t\tcur = event.target;\n\n\t\t// Find delegate handlers\n\t\t// Black-hole SVG <use> instance trees (#13180)\n\t\t// Avoid non-left-click bubbling in Firefox (#3861)\n\t\tif ( delegateCount && cur.nodeType && (!event.button || event.type !== \"click\") ) {\n\n\t\t\t/* jshint eqeqeq: false */\n\t\t\tfor ( ; cur != this; cur = cur.parentNode || this ) {\n\t\t\t\t/* jshint eqeqeq: true */\n\n\t\t\t\t// Don't check non-elements (#13208)\n\t\t\t\t// Don't process clicks on disabled elements (#6911, #8165, #11382, #11764)\n\t\t\t\tif ( cur.nodeType === 1 && (cur.disabled !== true || event.type !== \"click\") ) {\n\t\t\t\t\tmatches = [];\n\t\t\t\t\tfor ( i = 0; i < delegateCount; i++ ) {\n\t\t\t\t\t\thandleObj = handlers[ i ];\n\n\t\t\t\t\t\t// Don't conflict with Object.prototype properties (#13203)\n\t\t\t\t\t\tsel = handleObj.selector + \" \";\n\n\t\t\t\t\t\tif ( matches[ sel ] === undefined ) {\n\t\t\t\t\t\t\tmatches[ sel ] = handleObj.needsContext ?\n\t\t\t\t\t\t\t\tjQuery( sel, this ).index( cur ) >= 0 :\n\t\t\t\t\t\t\t\tjQuery.find( sel, this, null, [ cur ] ).length;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif ( matches[ sel ] ) {\n\t\t\t\t\t\t\tmatches.push( handleObj );\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif ( matches.length ) {\n\t\t\t\t\t\thandlerQueue.push({ elem: cur, handlers: matches });\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Add the remaining (directly-bound) handlers\n\t\tif ( delegateCount < handlers.length ) {\n\t\t\thandlerQueue.push({ elem: this, handlers: handlers.slice( delegateCount ) });\n\t\t}\n\n\t\treturn handlerQueue;\n\t},\n\n\tfix: function( event ) {\n\t\tif ( event[ jQuery.expando ] ) {\n\t\t\treturn event;\n\t\t}\n\n\t\t// Create a writable copy of the event object and normalize some properties\n\t\tvar i, prop, copy,\n\t\t\ttype = event.type,\n\t\t\toriginalEvent = event,\n\t\t\tfixHook = this.fixHooks[ type ];\n\n\t\tif ( !fixHook ) {\n\t\t\tthis.fixHooks[ type ] = fixHook =\n\t\t\t\trmouseEvent.test( type ) ? this.mouseHooks :\n\t\t\t\trkeyEvent.test( type ) ? this.keyHooks :\n\t\t\t\t{};\n\t\t}\n\t\tcopy = fixHook.props ? this.props.concat( fixHook.props ) : this.props;\n\n\t\tevent = new jQuery.Event( originalEvent );\n\n\t\ti = copy.length;\n\t\twhile ( i-- ) {\n\t\t\tprop = copy[ i ];\n\t\t\tevent[ prop ] = originalEvent[ prop ];\n\t\t}\n\n\t\t// Support: IE<9\n\t\t// Fix target property (#1925)\n\t\tif ( !event.target ) {\n\t\t\tevent.target = originalEvent.srcElement || document;\n\t\t}\n\n\t\t// Support: Chrome 23+, Safari?\n\t\t// Target should not be a text node (#504, #13143)\n\t\tif ( event.target.nodeType === 3 ) {\n\t\t\tevent.target = event.target.parentNode;\n\t\t}\n\n\t\t// Support: IE<9\n\t\t// For mouse/key events, metaKey==false if it's undefined (#3368, #11328)\n\t\tevent.metaKey = !!event.metaKey;\n\n\t\treturn fixHook.filter ? fixHook.filter( event, originalEvent ) : event;\n\t},\n\n\t// Includes some event props shared by KeyEvent and MouseEvent\n\tprops: \"altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which\".split(\" \"),\n\n\tfixHooks: {},\n\n\tkeyHooks: {\n\t\tprops: \"char charCode key keyCode\".split(\" \"),\n\t\tfilter: function( event, original ) {\n\n\t\t\t// Add which for key events\n\t\t\tif ( event.which == null ) {\n\t\t\t\tevent.which = original.charCode != null ? original.charCode : original.keyCode;\n\t\t\t}\n\n\t\t\treturn event;\n\t\t}\n\t},\n\n\tmouseHooks: {\n\t\tprops: \"button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement\".split(\" \"),\n\t\tfilter: function( event, original ) {\n\t\t\tvar body, eventDoc, doc,\n\t\t\t\tbutton = original.button,\n\t\t\t\tfromElement = original.fromElement;\n\n\t\t\t// Calculate pageX/Y if missing and clientX/Y available\n\t\t\tif ( event.pageX == null && original.clientX != null ) {\n\t\t\t\teventDoc = event.target.ownerDocument || document;\n\t\t\t\tdoc = eventDoc.documentElement;\n\t\t\t\tbody = eventDoc.body;\n\n\t\t\t\tevent.pageX = original.clientX + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - ( doc && doc.clientLeft || body && body.clientLeft || 0 );\n\t\t\t\tevent.pageY = original.clientY + ( doc && doc.scrollTop  || body && body.scrollTop  || 0 ) - ( doc && doc.clientTop  || body && body.clientTop  || 0 );\n\t\t\t}\n\n\t\t\t// Add relatedTarget, if necessary\n\t\t\tif ( !event.relatedTarget && fromElement ) {\n\t\t\t\tevent.relatedTarget = fromElement === event.target ? original.toElement : fromElement;\n\t\t\t}\n\n\t\t\t// Add which for click: 1 === left; 2 === middle; 3 === right\n\t\t\t// Note: button is not normalized, so don't use it\n\t\t\tif ( !event.which && button !== undefined ) {\n\t\t\t\tevent.which = ( button & 1 ? 1 : ( button & 2 ? 3 : ( button & 4 ? 2 : 0 ) ) );\n\t\t\t}\n\n\t\t\treturn event;\n\t\t}\n\t},\n\n\tspecial: {\n\t\tload: {\n\t\t\t// Prevent triggered image.load events from bubbling to window.load\n\t\t\tnoBubble: true\n\t\t},\n\t\tfocus: {\n\t\t\t// Fire native event if possible so blur/focus sequence is correct\n\t\t\ttrigger: function() {\n\t\t\t\tif ( this !== safeActiveElement() && this.focus ) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tthis.focus();\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t} catch ( e ) {\n\t\t\t\t\t\t// Support: IE<9\n\t\t\t\t\t\t// If we error on focus to hidden element (#1486, #12518),\n\t\t\t\t\t\t// let .trigger() run the handlers\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t},\n\t\t\tdelegateType: \"focusin\"\n\t\t},\n\t\tblur: {\n\t\t\ttrigger: function() {\n\t\t\t\tif ( this === safeActiveElement() && this.blur ) {\n\t\t\t\t\tthis.blur();\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t},\n\t\t\tdelegateType: \"focusout\"\n\t\t},\n\t\tclick: {\n\t\t\t// For checkbox, fire native event so checked state will be right\n\t\t\ttrigger: function() {\n\t\t\t\tif ( jQuery.nodeName( this, \"input\" ) && this.type === \"checkbox\" && this.click ) {\n\t\t\t\t\tthis.click();\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t},\n\n\t\t\t// For cross-browser consistency, don't fire native .click() on links\n\t\t\t_default: function( event ) {\n\t\t\t\treturn jQuery.nodeName( event.target, \"a\" );\n\t\t\t}\n\t\t},\n\n\t\tbeforeunload: {\n\t\t\tpostDispatch: function( event ) {\n\n\t\t\t\t// Support: Firefox 20+\n\t\t\t\t// Firefox doesn't alert if the returnValue field is not set.\n\t\t\t\tif ( event.result !== undefined && event.originalEvent ) {\n\t\t\t\t\tevent.originalEvent.returnValue = event.result;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t},\n\n\tsimulate: function( type, elem, event, bubble ) {\n\t\t// Piggyback on a donor event to simulate a different one.\n\t\t// Fake originalEvent to avoid donor's stopPropagation, but if the\n\t\t// simulated event prevents default then we do the same on the donor.\n\t\tvar e = jQuery.extend(\n\t\t\tnew jQuery.Event(),\n\t\t\tevent,\n\t\t\t{\n\t\t\t\ttype: type,\n\t\t\t\tisSimulated: true,\n\t\t\t\toriginalEvent: {}\n\t\t\t}\n\t\t);\n\t\tif ( bubble ) {\n\t\t\tjQuery.event.trigger( e, null, elem );\n\t\t} else {\n\t\t\tjQuery.event.dispatch.call( elem, e );\n\t\t}\n\t\tif ( e.isDefaultPrevented() ) {\n\t\t\tevent.preventDefault();\n\t\t}\n\t}\n};\n\njQuery.removeEvent = document.removeEventListener ?\n\tfunction( elem, type, handle ) {\n\t\tif ( elem.removeEventListener ) {\n\t\t\telem.removeEventListener( type, handle, false );\n\t\t}\n\t} :\n\tfunction( elem, type, handle ) {\n\t\tvar name = \"on\" + type;\n\n\t\tif ( elem.detachEvent ) {\n\n\t\t\t// #8545, #7054, preventing memory leaks for custom events in IE6-8\n\t\t\t// detachEvent needed property on element, by name of that event, to properly expose it to GC\n\t\t\tif ( typeof elem[ name ] === strundefined ) {\n\t\t\t\telem[ name ] = null;\n\t\t\t}\n\n\t\t\telem.detachEvent( name, handle );\n\t\t}\n\t};\n\njQuery.Event = function( src, props ) {\n\t// Allow instantiation without the 'new' keyword\n\tif ( !(this instanceof jQuery.Event) ) {\n\t\treturn new jQuery.Event( src, props );\n\t}\n\n\t// Event object\n\tif ( src && src.type ) {\n\t\tthis.originalEvent = src;\n\t\tthis.type = src.type;\n\n\t\t// Events bubbling up the document may have been marked as prevented\n\t\t// by a handler lower down the tree; reflect the correct value.\n\t\tthis.isDefaultPrevented = src.defaultPrevented ||\n\t\t\t\tsrc.defaultPrevented === undefined &&\n\t\t\t\t// Support: IE < 9, Android < 4.0\n\t\t\t\tsrc.returnValue === false ?\n\t\t\treturnTrue :\n\t\t\treturnFalse;\n\n\t// Event type\n\t} else {\n\t\tthis.type = src;\n\t}\n\n\t// Put explicitly provided properties onto the event object\n\tif ( props ) {\n\t\tjQuery.extend( this, props );\n\t}\n\n\t// Create a timestamp if incoming event doesn't have one\n\tthis.timeStamp = src && src.timeStamp || jQuery.now();\n\n\t// Mark it as fixed\n\tthis[ jQuery.expando ] = true;\n};\n\n// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding\n// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html\njQuery.Event.prototype = {\n\tisDefaultPrevented: returnFalse,\n\tisPropagationStopped: returnFalse,\n\tisImmediatePropagationStopped: returnFalse,\n\n\tpreventDefault: function() {\n\t\tvar e = this.originalEvent;\n\n\t\tthis.isDefaultPrevented = returnTrue;\n\t\tif ( !e ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// If preventDefault exists, run it on the original event\n\t\tif ( e.preventDefault ) {\n\t\t\te.preventDefault();\n\n\t\t// Support: IE\n\t\t// Otherwise set the returnValue property of the original event to false\n\t\t} else {\n\t\t\te.returnValue = false;\n\t\t}\n\t},\n\tstopPropagation: function() {\n\t\tvar e = this.originalEvent;\n\n\t\tthis.isPropagationStopped = returnTrue;\n\t\tif ( !e ) {\n\t\t\treturn;\n\t\t}\n\t\t// If stopPropagation exists, run it on the original event\n\t\tif ( e.stopPropagation ) {\n\t\t\te.stopPropagation();\n\t\t}\n\n\t\t// Support: IE\n\t\t// Set the cancelBubble property of the original event to true\n\t\te.cancelBubble = true;\n\t},\n\tstopImmediatePropagation: function() {\n\t\tvar e = this.originalEvent;\n\n\t\tthis.isImmediatePropagationStopped = returnTrue;\n\n\t\tif ( e && e.stopImmediatePropagation ) {\n\t\t\te.stopImmediatePropagation();\n\t\t}\n\n\t\tthis.stopPropagation();\n\t}\n};\n\n// Create mouseenter/leave events using mouseover/out and event-time checks\njQuery.each({\n\tmouseenter: \"mouseover\",\n\tmouseleave: \"mouseout\",\n\tpointerenter: \"pointerover\",\n\tpointerleave: \"pointerout\"\n}, function( orig, fix ) {\n\tjQuery.event.special[ orig ] = {\n\t\tdelegateType: fix,\n\t\tbindType: fix,\n\n\t\thandle: function( event ) {\n\t\t\tvar ret,\n\t\t\t\ttarget = this,\n\t\t\t\trelated = event.relatedTarget,\n\t\t\t\thandleObj = event.handleObj;\n\n\t\t\t// For mousenter/leave call the handler if related is outside the target.\n\t\t\t// NB: No relatedTarget if the mouse left/entered the browser window\n\t\t\tif ( !related || (related !== target && !jQuery.contains( target, related )) ) {\n\t\t\t\tevent.type = handleObj.origType;\n\t\t\t\tret = handleObj.handler.apply( this, arguments );\n\t\t\t\tevent.type = fix;\n\t\t\t}\n\t\t\treturn ret;\n\t\t}\n\t};\n});\n\n// IE submit delegation\nif ( !support.submitBubbles ) {\n\n\tjQuery.event.special.submit = {\n\t\tsetup: function() {\n\t\t\t// Only need this for delegated form submit events\n\t\t\tif ( jQuery.nodeName( this, \"form\" ) ) {\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\t// Lazy-add a submit handler when a descendant form may potentially be submitted\n\t\t\tjQuery.event.add( this, \"click._submit keypress._submit\", function( e ) {\n\t\t\t\t// Node name check avoids a VML-related crash in IE (#9807)\n\t\t\t\tvar elem = e.target,\n\t\t\t\t\tform = jQuery.nodeName( elem, \"input\" ) || jQuery.nodeName( elem, \"button\" ) ? elem.form : undefined;\n\t\t\t\tif ( form && !jQuery._data( form, \"submitBubbles\" ) ) {\n\t\t\t\t\tjQuery.event.add( form, \"submit._submit\", function( event ) {\n\t\t\t\t\t\tevent._submit_bubble = true;\n\t\t\t\t\t});\n\t\t\t\t\tjQuery._data( form, \"submitBubbles\", true );\n\t\t\t\t}\n\t\t\t});\n\t\t\t// return undefined since we don't need an event listener\n\t\t},\n\n\t\tpostDispatch: function( event ) {\n\t\t\t// If form was submitted by the user, bubble the event up the tree\n\t\t\tif ( event._submit_bubble ) {\n\t\t\t\tdelete event._submit_bubble;\n\t\t\t\tif ( this.parentNode && !event.isTrigger ) {\n\t\t\t\t\tjQuery.event.simulate( \"submit\", this.parentNode, event, true );\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\tteardown: function() {\n\t\t\t// Only need this for delegated form submit events\n\t\t\tif ( jQuery.nodeName( this, \"form\" ) ) {\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\t// Remove delegated handlers; cleanData eventually reaps submit handlers attached above\n\t\t\tjQuery.event.remove( this, \"._submit\" );\n\t\t}\n\t};\n}\n\n// IE change delegation and checkbox/radio fix\nif ( !support.changeBubbles ) {\n\n\tjQuery.event.special.change = {\n\n\t\tsetup: function() {\n\n\t\t\tif ( rformElems.test( this.nodeName ) ) {\n\t\t\t\t// IE doesn't fire change on a check/radio until blur; trigger it on click\n\t\t\t\t// after a propertychange. Eat the blur-change in special.change.handle.\n\t\t\t\t// This still fires onchange a second time for check/radio after blur.\n\t\t\t\tif ( this.type === \"checkbox\" || this.type === \"radio\" ) {\n\t\t\t\t\tjQuery.event.add( this, \"propertychange._change\", function( event ) {\n\t\t\t\t\t\tif ( event.originalEvent.propertyName === \"checked\" ) {\n\t\t\t\t\t\t\tthis._just_changed = true;\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t\t\tjQuery.event.add( this, \"click._change\", function( event ) {\n\t\t\t\t\t\tif ( this._just_changed && !event.isTrigger ) {\n\t\t\t\t\t\t\tthis._just_changed = false;\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// Allow triggered, simulated change events (#11500)\n\t\t\t\t\t\tjQuery.event.simulate( \"change\", this, event, true );\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\t// Delegated event; lazy-add a change handler on descendant inputs\n\t\t\tjQuery.event.add( this, \"beforeactivate._change\", function( e ) {\n\t\t\t\tvar elem = e.target;\n\n\t\t\t\tif ( rformElems.test( elem.nodeName ) && !jQuery._data( elem, \"changeBubbles\" ) ) {\n\t\t\t\t\tjQuery.event.add( elem, \"change._change\", function( event ) {\n\t\t\t\t\t\tif ( this.parentNode && !event.isSimulated && !event.isTrigger ) {\n\t\t\t\t\t\t\tjQuery.event.simulate( \"change\", this.parentNode, event, true );\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t\t\tjQuery._data( elem, \"changeBubbles\", true );\n\t\t\t\t}\n\t\t\t});\n\t\t},\n\n\t\thandle: function( event ) {\n\t\t\tvar elem = event.target;\n\n\t\t\t// Swallow native change events from checkbox/radio, we already triggered them above\n\t\t\tif ( this !== elem || event.isSimulated || event.isTrigger || (elem.type !== \"radio\" && elem.type !== \"checkbox\") ) {\n\t\t\t\treturn event.handleObj.handler.apply( this, arguments );\n\t\t\t}\n\t\t},\n\n\t\tteardown: function() {\n\t\t\tjQuery.event.remove( this, \"._change\" );\n\n\t\t\treturn !rformElems.test( this.nodeName );\n\t\t}\n\t};\n}\n\n// Create \"bubbling\" focus and blur events\nif ( !support.focusinBubbles ) {\n\tjQuery.each({ focus: \"focusin\", blur: \"focusout\" }, function( orig, fix ) {\n\n\t\t// Attach a single capturing handler on the document while someone wants focusin/focusout\n\t\tvar handler = function( event ) {\n\t\t\t\tjQuery.event.simulate( fix, event.target, jQuery.event.fix( event ), true );\n\t\t\t};\n\n\t\tjQuery.event.special[ fix ] = {\n\t\t\tsetup: function() {\n\t\t\t\tvar doc = this.ownerDocument || this,\n\t\t\t\t\tattaches = jQuery._data( doc, fix );\n\n\t\t\t\tif ( !attaches ) {\n\t\t\t\t\tdoc.addEventListener( orig, handler, true );\n\t\t\t\t}\n\t\t\t\tjQuery._data( doc, fix, ( attaches || 0 ) + 1 );\n\t\t\t},\n\t\t\tteardown: function() {\n\t\t\t\tvar doc = this.ownerDocument || this,\n\t\t\t\t\tattaches = jQuery._data( doc, fix ) - 1;\n\n\t\t\t\tif ( !attaches ) {\n\t\t\t\t\tdoc.removeEventListener( orig, handler, true );\n\t\t\t\t\tjQuery._removeData( doc, fix );\n\t\t\t\t} else {\n\t\t\t\t\tjQuery._data( doc, fix, attaches );\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t});\n}\n\njQuery.fn.extend({\n\n\ton: function( types, selector, data, fn, /*INTERNAL*/ one ) {\n\t\tvar type, origFn;\n\n\t\t// Types can be a map of types/handlers\n\t\tif ( typeof types === \"object\" ) {\n\t\t\t// ( types-Object, selector, data )\n\t\t\tif ( typeof selector !== \"string\" ) {\n\t\t\t\t// ( types-Object, data )\n\t\t\t\tdata = data || selector;\n\t\t\t\tselector = undefined;\n\t\t\t}\n\t\t\tfor ( type in types ) {\n\t\t\t\tthis.on( type, selector, data, types[ type ], one );\n\t\t\t}\n\t\t\treturn this;\n\t\t}\n\n\t\tif ( data == null && fn == null ) {\n\t\t\t// ( types, fn )\n\t\t\tfn = selector;\n\t\t\tdata = selector = undefined;\n\t\t} else if ( fn == null ) {\n\t\t\tif ( typeof selector === \"string\" ) {\n\t\t\t\t// ( types, selector, fn )\n\t\t\t\tfn = data;\n\t\t\t\tdata = undefined;\n\t\t\t} else {\n\t\t\t\t// ( types, data, fn )\n\t\t\t\tfn = data;\n\t\t\t\tdata = selector;\n\t\t\t\tselector = undefined;\n\t\t\t}\n\t\t}\n\t\tif ( fn === false ) {\n\t\t\tfn = returnFalse;\n\t\t} else if ( !fn ) {\n\t\t\treturn this;\n\t\t}\n\n\t\tif ( one === 1 ) {\n\t\t\torigFn = fn;\n\t\t\tfn = function( event ) {\n\t\t\t\t// Can use an empty set, since event contains the info\n\t\t\t\tjQuery().off( event );\n\t\t\t\treturn origFn.apply( this, arguments );\n\t\t\t};\n\t\t\t// Use same guid so caller can remove using origFn\n\t\t\tfn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ );\n\t\t}\n\t\treturn this.each( function() {\n\t\t\tjQuery.event.add( this, types, fn, data, selector );\n\t\t});\n\t},\n\tone: function( types, selector, data, fn ) {\n\t\treturn this.on( types, selector, data, fn, 1 );\n\t},\n\toff: function( types, selector, fn ) {\n\t\tvar handleObj, type;\n\t\tif ( types && types.preventDefault && types.handleObj ) {\n\t\t\t// ( event )  dispatched jQuery.Event\n\t\t\thandleObj = types.handleObj;\n\t\t\tjQuery( types.delegateTarget ).off(\n\t\t\t\thandleObj.namespace ? handleObj.origType + \".\" + handleObj.namespace : handleObj.origType,\n\t\t\t\thandleObj.selector,\n\t\t\t\thandleObj.handler\n\t\t\t);\n\t\t\treturn this;\n\t\t}\n\t\tif ( typeof types === \"object\" ) {\n\t\t\t// ( types-object [, selector] )\n\t\t\tfor ( type in types ) {\n\t\t\t\tthis.off( type, selector, types[ type ] );\n\t\t\t}\n\t\t\treturn this;\n\t\t}\n\t\tif ( selector === false || typeof selector === \"function\" ) {\n\t\t\t// ( types [, fn] )\n\t\t\tfn = selector;\n\t\t\tselector = undefined;\n\t\t}\n\t\tif ( fn === false ) {\n\t\t\tfn = returnFalse;\n\t\t}\n\t\treturn this.each(function() {\n\t\t\tjQuery.event.remove( this, types, fn, selector );\n\t\t});\n\t},\n\n\ttrigger: function( type, data ) {\n\t\treturn this.each(function() {\n\t\t\tjQuery.event.trigger( type, data, this );\n\t\t});\n\t},\n\ttriggerHandler: function( type, data ) {\n\t\tvar elem = this[0];\n\t\tif ( elem ) {\n\t\t\treturn jQuery.event.trigger( type, data, elem, true );\n\t\t}\n\t}\n});\n\n\nfunction createSafeFragment( document ) {\n\tvar list = nodeNames.split( \"|\" ),\n\t\tsafeFrag = document.createDocumentFragment();\n\n\tif ( safeFrag.createElement ) {\n\t\twhile ( list.length ) {\n\t\t\tsafeFrag.createElement(\n\t\t\t\tlist.pop()\n\t\t\t);\n\t\t}\n\t}\n\treturn safeFrag;\n}\n\nvar nodeNames = \"abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|\" +\n\t\t\"header|hgroup|mark|meter|nav|output|progress|section|summary|time|video\",\n\trinlinejQuery = / jQuery\\d+=\"(?:null|\\d+)\"/g,\n\trnoshimcache = new RegExp(\"<(?:\" + nodeNames + \")[\\\\s/>]\", \"i\"),\n\trleadingWhitespace = /^\\s+/,\n\trxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\\w:]+)[^>]*)\\/>/gi,\n\trtagName = /<([\\w:]+)/,\n\trtbody = /<tbody/i,\n\trhtml = /<|&#?\\w+;/,\n\trnoInnerhtml = /<(?:script|style|link)/i,\n\t// checked=\"checked\" or checked\n\trchecked = /checked\\s*(?:[^=]|=\\s*.checked.)/i,\n\trscriptType = /^$|\\/(?:java|ecma)script/i,\n\trscriptTypeMasked = /^true\\/(.*)/,\n\trcleanScript = /^\\s*<!(?:\\[CDATA\\[|--)|(?:\\]\\]|--)>\\s*$/g,\n\n\t// We have to close these tags to support XHTML (#13200)\n\twrapMap = {\n\t\toption: [ 1, \"<select multiple='multiple'>\", \"</select>\" ],\n\t\tlegend: [ 1, \"<fieldset>\", \"</fieldset>\" ],\n\t\tarea: [ 1, \"<map>\", \"</map>\" ],\n\t\tparam: [ 1, \"<object>\", \"</object>\" ],\n\t\tthead: [ 1, \"<table>\", \"</table>\" ],\n\t\ttr: [ 2, \"<table><tbody>\", \"</tbody></table>\" ],\n\t\tcol: [ 2, \"<table><tbody></tbody><colgroup>\", \"</colgroup></table>\" ],\n\t\ttd: [ 3, \"<table><tbody><tr>\", \"</tr></tbody></table>\" ],\n\n\t\t// IE6-8 can't serialize link, script, style, or any html5 (NoScope) tags,\n\t\t// unless wrapped in a div with non-breaking characters in front of it.\n\t\t_default: support.htmlSerialize ? [ 0, \"\", \"\" ] : [ 1, \"X<div>\", \"</div>\"  ]\n\t},\n\tsafeFragment = createSafeFragment( document ),\n\tfragmentDiv = safeFragment.appendChild( document.createElement(\"div\") );\n\nwrapMap.optgroup = wrapMap.option;\nwrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead;\nwrapMap.th = wrapMap.td;\n\nfunction getAll( context, tag ) {\n\tvar elems, elem,\n\t\ti = 0,\n\t\tfound = typeof context.getElementsByTagName !== strundefined ? context.getElementsByTagName( tag || \"*\" ) :\n\t\t\ttypeof context.querySelectorAll !== strundefined ? context.querySelectorAll( tag || \"*\" ) :\n\t\t\tundefined;\n\n\tif ( !found ) {\n\t\tfor ( found = [], elems = context.childNodes || context; (elem = elems[i]) != null; i++ ) {\n\t\t\tif ( !tag || jQuery.nodeName( elem, tag ) ) {\n\t\t\t\tfound.push( elem );\n\t\t\t} else {\n\t\t\t\tjQuery.merge( found, getAll( elem, tag ) );\n\t\t\t}\n\t\t}\n\t}\n\n\treturn tag === undefined || tag && jQuery.nodeName( context, tag ) ?\n\t\tjQuery.merge( [ context ], found ) :\n\t\tfound;\n}\n\n// Used in buildFragment, fixes the defaultChecked property\nfunction fixDefaultChecked( elem ) {\n\tif ( rcheckableType.test( elem.type ) ) {\n\t\telem.defaultChecked = elem.checked;\n\t}\n}\n\n// Support: IE<8\n// Manipulating tables requires a tbody\nfunction manipulationTarget( elem, content ) {\n\treturn jQuery.nodeName( elem, \"table\" ) &&\n\t\tjQuery.nodeName( content.nodeType !== 11 ? content : content.firstChild, \"tr\" ) ?\n\n\t\telem.getElementsByTagName(\"tbody\")[0] ||\n\t\t\telem.appendChild( elem.ownerDocument.createElement(\"tbody\") ) :\n\t\telem;\n}\n\n// Replace/restore the type attribute of script elements for safe DOM manipulation\nfunction disableScript( elem ) {\n\telem.type = (jQuery.find.attr( elem, \"type\" ) !== null) + \"/\" + elem.type;\n\treturn elem;\n}\nfunction restoreScript( elem ) {\n\tvar match = rscriptTypeMasked.exec( elem.type );\n\tif ( match ) {\n\t\telem.type = match[1];\n\t} else {\n\t\telem.removeAttribute(\"type\");\n\t}\n\treturn elem;\n}\n\n// Mark scripts as having already been evaluated\nfunction setGlobalEval( elems, refElements ) {\n\tvar elem,\n\t\ti = 0;\n\tfor ( ; (elem = elems[i]) != null; i++ ) {\n\t\tjQuery._data( elem, \"globalEval\", !refElements || jQuery._data( refElements[i], \"globalEval\" ) );\n\t}\n}\n\nfunction cloneCopyEvent( src, dest ) {\n\n\tif ( dest.nodeType !== 1 || !jQuery.hasData( src ) ) {\n\t\treturn;\n\t}\n\n\tvar type, i, l,\n\t\toldData = jQuery._data( src ),\n\t\tcurData = jQuery._data( dest, oldData ),\n\t\tevents = oldData.events;\n\n\tif ( events ) {\n\t\tdelete curData.handle;\n\t\tcurData.events = {};\n\n\t\tfor ( type in events ) {\n\t\t\tfor ( i = 0, l = events[ type ].length; i < l; i++ ) {\n\t\t\t\tjQuery.event.add( dest, type, events[ type ][ i ] );\n\t\t\t}\n\t\t}\n\t}\n\n\t// make the cloned public data object a copy from the original\n\tif ( curData.data ) {\n\t\tcurData.data = jQuery.extend( {}, curData.data );\n\t}\n}\n\nfunction fixCloneNodeIssues( src, dest ) {\n\tvar nodeName, e, data;\n\n\t// We do not need to do anything for non-Elements\n\tif ( dest.nodeType !== 1 ) {\n\t\treturn;\n\t}\n\n\tnodeName = dest.nodeName.toLowerCase();\n\n\t// IE6-8 copies events bound via attachEvent when using cloneNode.\n\tif ( !support.noCloneEvent && dest[ jQuery.expando ] ) {\n\t\tdata = jQuery._data( dest );\n\n\t\tfor ( e in data.events ) {\n\t\t\tjQuery.removeEvent( dest, e, data.handle );\n\t\t}\n\n\t\t// Event data gets referenced instead of copied if the expando gets copied too\n\t\tdest.removeAttribute( jQuery.expando );\n\t}\n\n\t// IE blanks contents when cloning scripts, and tries to evaluate newly-set text\n\tif ( nodeName === \"script\" && dest.text !== src.text ) {\n\t\tdisableScript( dest ).text = src.text;\n\t\trestoreScript( dest );\n\n\t// IE6-10 improperly clones children of object elements using classid.\n\t// IE10 throws NoModificationAllowedError if parent is null, #12132.\n\t} else if ( nodeName === \"object\" ) {\n\t\tif ( dest.parentNode ) {\n\t\t\tdest.outerHTML = src.outerHTML;\n\t\t}\n\n\t\t// This path appears unavoidable for IE9. When cloning an object\n\t\t// element in IE9, the outerHTML strategy above is not sufficient.\n\t\t// If the src has innerHTML and the destination does not,\n\t\t// copy the src.innerHTML into the dest.innerHTML. #10324\n\t\tif ( support.html5Clone && ( src.innerHTML && !jQuery.trim(dest.innerHTML) ) ) {\n\t\t\tdest.innerHTML = src.innerHTML;\n\t\t}\n\n\t} else if ( nodeName === \"input\" && rcheckableType.test( src.type ) ) {\n\t\t// IE6-8 fails to persist the checked state of a cloned checkbox\n\t\t// or radio button. Worse, IE6-7 fail to give the cloned element\n\t\t// a checked appearance if the defaultChecked value isn't also set\n\n\t\tdest.defaultChecked = dest.checked = src.checked;\n\n\t\t// IE6-7 get confused and end up setting the value of a cloned\n\t\t// checkbox/radio button to an empty string instead of \"on\"\n\t\tif ( dest.value !== src.value ) {\n\t\t\tdest.value = src.value;\n\t\t}\n\n\t// IE6-8 fails to return the selected option to the default selected\n\t// state when cloning options\n\t} else if ( nodeName === \"option\" ) {\n\t\tdest.defaultSelected = dest.selected = src.defaultSelected;\n\n\t// IE6-8 fails to set the defaultValue to the correct value when\n\t// cloning other types of input fields\n\t} else if ( nodeName === \"input\" || nodeName === \"textarea\" ) {\n\t\tdest.defaultValue = src.defaultValue;\n\t}\n}\n\njQuery.extend({\n\tclone: function( elem, dataAndEvents, deepDataAndEvents ) {\n\t\tvar destElements, node, clone, i, srcElements,\n\t\t\tinPage = jQuery.contains( elem.ownerDocument, elem );\n\n\t\tif ( support.html5Clone || jQuery.isXMLDoc(elem) || !rnoshimcache.test( \"<\" + elem.nodeName + \">\" ) ) {\n\t\t\tclone = elem.cloneNode( true );\n\n\t\t// IE<=8 does not properly clone detached, unknown element nodes\n\t\t} else {\n\t\t\tfragmentDiv.innerHTML = elem.outerHTML;\n\t\t\tfragmentDiv.removeChild( clone = fragmentDiv.firstChild );\n\t\t}\n\n\t\tif ( (!support.noCloneEvent || !support.noCloneChecked) &&\n\t\t\t\t(elem.nodeType === 1 || elem.nodeType === 11) && !jQuery.isXMLDoc(elem) ) {\n\n\t\t\t// We eschew Sizzle here for performance reasons: http://jsperf.com/getall-vs-sizzle/2\n\t\t\tdestElements = getAll( clone );\n\t\t\tsrcElements = getAll( elem );\n\n\t\t\t// Fix all IE cloning issues\n\t\t\tfor ( i = 0; (node = srcElements[i]) != null; ++i ) {\n\t\t\t\t// Ensure that the destination node is not null; Fixes #9587\n\t\t\t\tif ( destElements[i] ) {\n\t\t\t\t\tfixCloneNodeIssues( node, destElements[i] );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Copy the events from the original to the clone\n\t\tif ( dataAndEvents ) {\n\t\t\tif ( deepDataAndEvents ) {\n\t\t\t\tsrcElements = srcElements || getAll( elem );\n\t\t\t\tdestElements = destElements || getAll( clone );\n\n\t\t\t\tfor ( i = 0; (node = srcElements[i]) != null; i++ ) {\n\t\t\t\t\tcloneCopyEvent( node, destElements[i] );\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tcloneCopyEvent( elem, clone );\n\t\t\t}\n\t\t}\n\n\t\t// Preserve script evaluation history\n\t\tdestElements = getAll( clone, \"script\" );\n\t\tif ( destElements.length > 0 ) {\n\t\t\tsetGlobalEval( destElements, !inPage && getAll( elem, \"script\" ) );\n\t\t}\n\n\t\tdestElements = srcElements = node = null;\n\n\t\t// Return the cloned set\n\t\treturn clone;\n\t},\n\n\tbuildFragment: function( elems, context, scripts, selection ) {\n\t\tvar j, elem, contains,\n\t\t\ttmp, tag, tbody, wrap,\n\t\t\tl = elems.length,\n\n\t\t\t// Ensure a safe fragment\n\t\t\tsafe = createSafeFragment( context ),\n\n\t\t\tnodes = [],\n\t\t\ti = 0;\n\n\t\tfor ( ; i < l; i++ ) {\n\t\t\telem = elems[ i ];\n\n\t\t\tif ( elem || elem === 0 ) {\n\n\t\t\t\t// Add nodes directly\n\t\t\t\tif ( jQuery.type( elem ) === \"object\" ) {\n\t\t\t\t\tjQuery.merge( nodes, elem.nodeType ? [ elem ] : elem );\n\n\t\t\t\t// Convert non-html into a text node\n\t\t\t\t} else if ( !rhtml.test( elem ) ) {\n\t\t\t\t\tnodes.push( context.createTextNode( elem ) );\n\n\t\t\t\t// Convert html into DOM nodes\n\t\t\t\t} else {\n\t\t\t\t\ttmp = tmp || safe.appendChild( context.createElement(\"div\") );\n\n\t\t\t\t\t// Deserialize a standard representation\n\t\t\t\t\ttag = (rtagName.exec( elem ) || [ \"\", \"\" ])[ 1 ].toLowerCase();\n\t\t\t\t\twrap = wrapMap[ tag ] || wrapMap._default;\n\n\t\t\t\t\ttmp.innerHTML = wrap[1] + elem.replace( rxhtmlTag, \"<$1></$2>\" ) + wrap[2];\n\n\t\t\t\t\t// Descend through wrappers to the right content\n\t\t\t\t\tj = wrap[0];\n\t\t\t\t\twhile ( j-- ) {\n\t\t\t\t\t\ttmp = tmp.lastChild;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Manually add leading whitespace removed by IE\n\t\t\t\t\tif ( !support.leadingWhitespace && rleadingWhitespace.test( elem ) ) {\n\t\t\t\t\t\tnodes.push( context.createTextNode( rleadingWhitespace.exec( elem )[0] ) );\n\t\t\t\t\t}\n\n\t\t\t\t\t// Remove IE's autoinserted <tbody> from table fragments\n\t\t\t\t\tif ( !support.tbody ) {\n\n\t\t\t\t\t\t// String was a <table>, *may* have spurious <tbody>\n\t\t\t\t\t\telem = tag === \"table\" && !rtbody.test( elem ) ?\n\t\t\t\t\t\t\ttmp.firstChild :\n\n\t\t\t\t\t\t\t// String was a bare <thead> or <tfoot>\n\t\t\t\t\t\t\twrap[1] === \"<table>\" && !rtbody.test( elem ) ?\n\t\t\t\t\t\t\t\ttmp :\n\t\t\t\t\t\t\t\t0;\n\n\t\t\t\t\t\tj = elem && elem.childNodes.length;\n\t\t\t\t\t\twhile ( j-- ) {\n\t\t\t\t\t\t\tif ( jQuery.nodeName( (tbody = elem.childNodes[j]), \"tbody\" ) && !tbody.childNodes.length ) {\n\t\t\t\t\t\t\t\telem.removeChild( tbody );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tjQuery.merge( nodes, tmp.childNodes );\n\n\t\t\t\t\t// Fix #12392 for WebKit and IE > 9\n\t\t\t\t\ttmp.textContent = \"\";\n\n\t\t\t\t\t// Fix #12392 for oldIE\n\t\t\t\t\twhile ( tmp.firstChild ) {\n\t\t\t\t\t\ttmp.removeChild( tmp.firstChild );\n\t\t\t\t\t}\n\n\t\t\t\t\t// Remember the top-level container for proper cleanup\n\t\t\t\t\ttmp = safe.lastChild;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Fix #11356: Clear elements from fragment\n\t\tif ( tmp ) {\n\t\t\tsafe.removeChild( tmp );\n\t\t}\n\n\t\t// Reset defaultChecked for any radios and checkboxes\n\t\t// about to be appended to the DOM in IE 6/7 (#8060)\n\t\tif ( !support.appendChecked ) {\n\t\t\tjQuery.grep( getAll( nodes, \"input\" ), fixDefaultChecked );\n\t\t}\n\n\t\ti = 0;\n\t\twhile ( (elem = nodes[ i++ ]) ) {\n\n\t\t\t// #4087 - If origin and destination elements are the same, and this is\n\t\t\t// that element, do not do anything\n\t\t\tif ( selection && jQuery.inArray( elem, selection ) !== -1 ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tcontains = jQuery.contains( elem.ownerDocument, elem );\n\n\t\t\t// Append to fragment\n\t\t\ttmp = getAll( safe.appendChild( elem ), \"script\" );\n\n\t\t\t// Preserve script evaluation history\n\t\t\tif ( contains ) {\n\t\t\t\tsetGlobalEval( tmp );\n\t\t\t}\n\n\t\t\t// Capture executables\n\t\t\tif ( scripts ) {\n\t\t\t\tj = 0;\n\t\t\t\twhile ( (elem = tmp[ j++ ]) ) {\n\t\t\t\t\tif ( rscriptType.test( elem.type || \"\" ) ) {\n\t\t\t\t\t\tscripts.push( elem );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\ttmp = null;\n\n\t\treturn safe;\n\t},\n\n\tcleanData: function( elems, /* internal */ acceptData ) {\n\t\tvar elem, type, id, data,\n\t\t\ti = 0,\n\t\t\tinternalKey = jQuery.expando,\n\t\t\tcache = jQuery.cache,\n\t\t\tdeleteExpando = support.deleteExpando,\n\t\t\tspecial = jQuery.event.special;\n\n\t\tfor ( ; (elem = elems[i]) != null; i++ ) {\n\t\t\tif ( acceptData || jQuery.acceptData( elem ) ) {\n\n\t\t\t\tid = elem[ internalKey ];\n\t\t\t\tdata = id && cache[ id ];\n\n\t\t\t\tif ( data ) {\n\t\t\t\t\tif ( data.events ) {\n\t\t\t\t\t\tfor ( type in data.events ) {\n\t\t\t\t\t\t\tif ( special[ type ] ) {\n\t\t\t\t\t\t\t\tjQuery.event.remove( elem, type );\n\n\t\t\t\t\t\t\t// This is a shortcut to avoid jQuery.event.remove's overhead\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tjQuery.removeEvent( elem, type, data.handle );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// Remove cache only if it was not already removed by jQuery.event.remove\n\t\t\t\t\tif ( cache[ id ] ) {\n\n\t\t\t\t\t\tdelete cache[ id ];\n\n\t\t\t\t\t\t// IE does not allow us to delete expando properties from nodes,\n\t\t\t\t\t\t// nor does it have a removeAttribute function on Document nodes;\n\t\t\t\t\t\t// we must handle all of these cases\n\t\t\t\t\t\tif ( deleteExpando ) {\n\t\t\t\t\t\t\tdelete elem[ internalKey ];\n\n\t\t\t\t\t\t} else if ( typeof elem.removeAttribute !== strundefined ) {\n\t\t\t\t\t\t\telem.removeAttribute( internalKey );\n\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\telem[ internalKey ] = null;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tdeletedIds.push( id );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n});\n\njQuery.fn.extend({\n\ttext: function( value ) {\n\t\treturn access( this, function( value ) {\n\t\t\treturn value === undefined ?\n\t\t\t\tjQuery.text( this ) :\n\t\t\t\tthis.empty().append( ( this[0] && this[0].ownerDocument || document ).createTextNode( value ) );\n\t\t}, null, value, arguments.length );\n\t},\n\n\tappend: function() {\n\t\treturn this.domManip( arguments, function( elem ) {\n\t\t\tif ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {\n\t\t\t\tvar target = manipulationTarget( this, elem );\n\t\t\t\ttarget.appendChild( elem );\n\t\t\t}\n\t\t});\n\t},\n\n\tprepend: function() {\n\t\treturn this.domManip( arguments, function( elem ) {\n\t\t\tif ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {\n\t\t\t\tvar target = manipulationTarget( this, elem );\n\t\t\t\ttarget.insertBefore( elem, target.firstChild );\n\t\t\t}\n\t\t});\n\t},\n\n\tbefore: function() {\n\t\treturn this.domManip( arguments, function( elem ) {\n\t\t\tif ( this.parentNode ) {\n\t\t\t\tthis.parentNode.insertBefore( elem, this );\n\t\t\t}\n\t\t});\n\t},\n\n\tafter: function() {\n\t\treturn this.domManip( arguments, function( elem ) {\n\t\t\tif ( this.parentNode ) {\n\t\t\t\tthis.parentNode.insertBefore( elem, this.nextSibling );\n\t\t\t}\n\t\t});\n\t},\n\n\tremove: function( selector, keepData /* Internal Use Only */ ) {\n\t\tvar elem,\n\t\t\telems = selector ? jQuery.filter( selector, this ) : this,\n\t\t\ti = 0;\n\n\t\tfor ( ; (elem = elems[i]) != null; i++ ) {\n\n\t\t\tif ( !keepData && elem.nodeType === 1 ) {\n\t\t\t\tjQuery.cleanData( getAll( elem ) );\n\t\t\t}\n\n\t\t\tif ( elem.parentNode ) {\n\t\t\t\tif ( keepData && jQuery.contains( elem.ownerDocument, elem ) ) {\n\t\t\t\t\tsetGlobalEval( getAll( elem, \"script\" ) );\n\t\t\t\t}\n\t\t\t\telem.parentNode.removeChild( elem );\n\t\t\t}\n\t\t}\n\n\t\treturn this;\n\t},\n\n\tempty: function() {\n\t\tvar elem,\n\t\t\ti = 0;\n\n\t\tfor ( ; (elem = this[i]) != null; i++ ) {\n\t\t\t// Remove element nodes and prevent memory leaks\n\t\t\tif ( elem.nodeType === 1 ) {\n\t\t\t\tjQuery.cleanData( getAll( elem, false ) );\n\t\t\t}\n\n\t\t\t// Remove any remaining nodes\n\t\t\twhile ( elem.firstChild ) {\n\t\t\t\telem.removeChild( elem.firstChild );\n\t\t\t}\n\n\t\t\t// If this is a select, ensure that it displays empty (#12336)\n\t\t\t// Support: IE<9\n\t\t\tif ( elem.options && jQuery.nodeName( elem, \"select\" ) ) {\n\t\t\t\telem.options.length = 0;\n\t\t\t}\n\t\t}\n\n\t\treturn this;\n\t},\n\n\tclone: function( dataAndEvents, deepDataAndEvents ) {\n\t\tdataAndEvents = dataAndEvents == null ? false : dataAndEvents;\n\t\tdeepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents;\n\n\t\treturn this.map(function() {\n\t\t\treturn jQuery.clone( this, dataAndEvents, deepDataAndEvents );\n\t\t});\n\t},\n\n\thtml: function( value ) {\n\t\treturn access( this, function( value ) {\n\t\t\tvar elem = this[ 0 ] || {},\n\t\t\t\ti = 0,\n\t\t\t\tl = this.length;\n\n\t\t\tif ( value === undefined ) {\n\t\t\t\treturn elem.nodeType === 1 ?\n\t\t\t\t\telem.innerHTML.replace( rinlinejQuery, \"\" ) :\n\t\t\t\t\tundefined;\n\t\t\t}\n\n\t\t\t// See if we can take a shortcut and just use innerHTML\n\t\t\tif ( typeof value === \"string\" && !rnoInnerhtml.test( value ) &&\n\t\t\t\t( support.htmlSerialize || !rnoshimcache.test( value )  ) &&\n\t\t\t\t( support.leadingWhitespace || !rleadingWhitespace.test( value ) ) &&\n\t\t\t\t!wrapMap[ (rtagName.exec( value ) || [ \"\", \"\" ])[ 1 ].toLowerCase() ] ) {\n\n\t\t\t\tvalue = value.replace( rxhtmlTag, \"<$1></$2>\" );\n\n\t\t\t\ttry {\n\t\t\t\t\tfor (; i < l; i++ ) {\n\t\t\t\t\t\t// Remove element nodes and prevent memory leaks\n\t\t\t\t\t\telem = this[i] || {};\n\t\t\t\t\t\tif ( elem.nodeType === 1 ) {\n\t\t\t\t\t\t\tjQuery.cleanData( getAll( elem, false ) );\n\t\t\t\t\t\t\telem.innerHTML = value;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\telem = 0;\n\n\t\t\t\t// If using innerHTML throws an exception, use the fallback method\n\t\t\t\t} catch(e) {}\n\t\t\t}\n\n\t\t\tif ( elem ) {\n\t\t\t\tthis.empty().append( value );\n\t\t\t}\n\t\t}, null, value, arguments.length );\n\t},\n\n\treplaceWith: function() {\n\t\tvar arg = arguments[ 0 ];\n\n\t\t// Make the changes, replacing each context element with the new content\n\t\tthis.domManip( arguments, function( elem ) {\n\t\t\targ = this.parentNode;\n\n\t\t\tjQuery.cleanData( getAll( this ) );\n\n\t\t\tif ( arg ) {\n\t\t\t\targ.replaceChild( elem, this );\n\t\t\t}\n\t\t});\n\n\t\t// Force removal if there was no new content (e.g., from empty arguments)\n\t\treturn arg && (arg.length || arg.nodeType) ? this : this.remove();\n\t},\n\n\tdetach: function( selector ) {\n\t\treturn this.remove( selector, true );\n\t},\n\n\tdomManip: function( args, callback ) {\n\n\t\t// Flatten any nested arrays\n\t\targs = concat.apply( [], args );\n\n\t\tvar first, node, hasScripts,\n\t\t\tscripts, doc, fragment,\n\t\t\ti = 0,\n\t\t\tl = this.length,\n\t\t\tset = this,\n\t\t\tiNoClone = l - 1,\n\t\t\tvalue = args[0],\n\t\t\tisFunction = jQuery.isFunction( value );\n\n\t\t// We can't cloneNode fragments that contain checked, in WebKit\n\t\tif ( isFunction ||\n\t\t\t\t( l > 1 && typeof value === \"string\" &&\n\t\t\t\t\t!support.checkClone && rchecked.test( value ) ) ) {\n\t\t\treturn this.each(function( index ) {\n\t\t\t\tvar self = set.eq( index );\n\t\t\t\tif ( isFunction ) {\n\t\t\t\t\targs[0] = value.call( this, index, self.html() );\n\t\t\t\t}\n\t\t\t\tself.domManip( args, callback );\n\t\t\t});\n\t\t}\n\n\t\tif ( l ) {\n\t\t\tfragment = jQuery.buildFragment( args, this[ 0 ].ownerDocument, false, this );\n\t\t\tfirst = fragment.firstChild;\n\n\t\t\tif ( fragment.childNodes.length === 1 ) {\n\t\t\t\tfragment = first;\n\t\t\t}\n\n\t\t\tif ( first ) {\n\t\t\t\tscripts = jQuery.map( getAll( fragment, \"script\" ), disableScript );\n\t\t\t\thasScripts = scripts.length;\n\n\t\t\t\t// Use the original fragment for the last item instead of the first because it can end up\n\t\t\t\t// being emptied incorrectly in certain situations (#8070).\n\t\t\t\tfor ( ; i < l; i++ ) {\n\t\t\t\t\tnode = fragment;\n\n\t\t\t\t\tif ( i !== iNoClone ) {\n\t\t\t\t\t\tnode = jQuery.clone( node, true, true );\n\n\t\t\t\t\t\t// Keep references to cloned scripts for later restoration\n\t\t\t\t\t\tif ( hasScripts ) {\n\t\t\t\t\t\t\tjQuery.merge( scripts, getAll( node, \"script\" ) );\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tcallback.call( this[i], node, i );\n\t\t\t\t}\n\n\t\t\t\tif ( hasScripts ) {\n\t\t\t\t\tdoc = scripts[ scripts.length - 1 ].ownerDocument;\n\n\t\t\t\t\t// Reenable scripts\n\t\t\t\t\tjQuery.map( scripts, restoreScript );\n\n\t\t\t\t\t// Evaluate executable scripts on first document insertion\n\t\t\t\t\tfor ( i = 0; i < hasScripts; i++ ) {\n\t\t\t\t\t\tnode = scripts[ i ];\n\t\t\t\t\t\tif ( rscriptType.test( node.type || \"\" ) &&\n\t\t\t\t\t\t\t!jQuery._data( node, \"globalEval\" ) && jQuery.contains( doc, node ) ) {\n\n\t\t\t\t\t\t\tif ( node.src ) {\n\t\t\t\t\t\t\t\t// Optional AJAX dependency, but won't run scripts if not present\n\t\t\t\t\t\t\t\tif ( jQuery._evalUrl ) {\n\t\t\t\t\t\t\t\t\tjQuery._evalUrl( node.src );\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tjQuery.globalEval( ( node.text || node.textContent || node.innerHTML || \"\" ).replace( rcleanScript, \"\" ) );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Fix #11809: Avoid leaking memory\n\t\t\t\tfragment = first = null;\n\t\t\t}\n\t\t}\n\n\t\treturn this;\n\t}\n});\n\njQuery.each({\n\tappendTo: \"append\",\n\tprependTo: \"prepend\",\n\tinsertBefore: \"before\",\n\tinsertAfter: \"after\",\n\treplaceAll: \"replaceWith\"\n}, function( name, original ) {\n\tjQuery.fn[ name ] = function( selector ) {\n\t\tvar elems,\n\t\t\ti = 0,\n\t\t\tret = [],\n\t\t\tinsert = jQuery( selector ),\n\t\t\tlast = insert.length - 1;\n\n\t\tfor ( ; i <= last; i++ ) {\n\t\t\telems = i === last ? this : this.clone(true);\n\t\t\tjQuery( insert[i] )[ original ]( elems );\n\n\t\t\t// Modern browsers can apply jQuery collections as arrays, but oldIE needs a .get()\n\t\t\tpush.apply( ret, elems.get() );\n\t\t}\n\n\t\treturn this.pushStack( ret );\n\t};\n});\n\n\nvar iframe,\n\telemdisplay = {};\n\n/**\n * Retrieve the actual display of a element\n * @param {String} name nodeName of the element\n * @param {Object} doc Document object\n */\n// Called only from within defaultDisplay\nfunction actualDisplay( name, doc ) {\n\tvar style,\n\t\telem = jQuery( doc.createElement( name ) ).appendTo( doc.body ),\n\n\t\t// getDefaultComputedStyle might be reliably used only on attached element\n\t\tdisplay = window.getDefaultComputedStyle && ( style = window.getDefaultComputedStyle( elem[ 0 ] ) ) ?\n\n\t\t\t// Use of this method is a temporary fix (more like optmization) until something better comes along,\n\t\t\t// since it was removed from specification and supported only in FF\n\t\t\tstyle.display : jQuery.css( elem[ 0 ], \"display\" );\n\n\t// We don't have any data stored on the element,\n\t// so use \"detach\" method as fast way to get rid of the element\n\telem.detach();\n\n\treturn display;\n}\n\n/**\n * Try to determine the default display value of an element\n * @param {String} nodeName\n */\nfunction defaultDisplay( nodeName ) {\n\tvar doc = document,\n\t\tdisplay = elemdisplay[ nodeName ];\n\n\tif ( !display ) {\n\t\tdisplay = actualDisplay( nodeName, doc );\n\n\t\t// If the simple way fails, read from inside an iframe\n\t\tif ( display === \"none\" || !display ) {\n\n\t\t\t// Use the already-created iframe if possible\n\t\t\tiframe = (iframe || jQuery( \"<iframe frameborder='0' width='0' height='0'/>\" )).appendTo( doc.documentElement );\n\n\t\t\t// Always write a new HTML skeleton so Webkit and Firefox don't choke on reuse\n\t\t\tdoc = ( iframe[ 0 ].contentWindow || iframe[ 0 ].contentDocument ).document;\n\n\t\t\t// Support: IE\n\t\t\tdoc.write();\n\t\t\tdoc.close();\n\n\t\t\tdisplay = actualDisplay( nodeName, doc );\n\t\t\tiframe.detach();\n\t\t}\n\n\t\t// Store the correct default display\n\t\telemdisplay[ nodeName ] = display;\n\t}\n\n\treturn display;\n}\n\n\n(function() {\n\tvar shrinkWrapBlocksVal;\n\n\tsupport.shrinkWrapBlocks = function() {\n\t\tif ( shrinkWrapBlocksVal != null ) {\n\t\t\treturn shrinkWrapBlocksVal;\n\t\t}\n\n\t\t// Will be changed later if needed.\n\t\tshrinkWrapBlocksVal = false;\n\n\t\t// Minified: var b,c,d\n\t\tvar div, body, container;\n\n\t\tbody = document.getElementsByTagName( \"body\" )[ 0 ];\n\t\tif ( !body || !body.style ) {\n\t\t\t// Test fired too early or in an unsupported environment, exit.\n\t\t\treturn;\n\t\t}\n\n\t\t// Setup\n\t\tdiv = document.createElement( \"div\" );\n\t\tcontainer = document.createElement( \"div\" );\n\t\tcontainer.style.cssText = \"position:absolute;border:0;width:0;height:0;top:0;left:-9999px\";\n\t\tbody.appendChild( container ).appendChild( div );\n\n\t\t// Support: IE6\n\t\t// Check if elements with layout shrink-wrap their children\n\t\tif ( typeof div.style.zoom !== strundefined ) {\n\t\t\t// Reset CSS: box-sizing; display; margin; border\n\t\t\tdiv.style.cssText =\n\t\t\t\t// Support: Firefox<29, Android 2.3\n\t\t\t\t// Vendor-prefix box-sizing\n\t\t\t\t\"-webkit-box-sizing:content-box;-moz-box-sizing:content-box;\" +\n\t\t\t\t\"box-sizing:content-box;display:block;margin:0;border:0;\" +\n\t\t\t\t\"padding:1px;width:1px;zoom:1\";\n\t\t\tdiv.appendChild( document.createElement( \"div\" ) ).style.width = \"5px\";\n\t\t\tshrinkWrapBlocksVal = div.offsetWidth !== 3;\n\t\t}\n\n\t\tbody.removeChild( container );\n\n\t\treturn shrinkWrapBlocksVal;\n\t};\n\n})();\nvar rmargin = (/^margin/);\n\nvar rnumnonpx = new RegExp( \"^(\" + pnum + \")(?!px)[a-z%]+$\", \"i\" );\n\n\n\nvar getStyles, curCSS,\n\trposition = /^(top|right|bottom|left)$/;\n\nif ( window.getComputedStyle ) {\n\tgetStyles = function( elem ) {\n\t\treturn elem.ownerDocument.defaultView.getComputedStyle( elem, null );\n\t};\n\n\tcurCSS = function( elem, name, computed ) {\n\t\tvar width, minWidth, maxWidth, ret,\n\t\t\tstyle = elem.style;\n\n\t\tcomputed = computed || getStyles( elem );\n\n\t\t// getPropertyValue is only needed for .css('filter') in IE9, see #12537\n\t\tret = computed ? computed.getPropertyValue( name ) || computed[ name ] : undefined;\n\n\t\tif ( computed ) {\n\n\t\t\tif ( ret === \"\" && !jQuery.contains( elem.ownerDocument, elem ) ) {\n\t\t\t\tret = jQuery.style( elem, name );\n\t\t\t}\n\n\t\t\t// A tribute to the \"awesome hack by Dean Edwards\"\n\t\t\t// Chrome < 17 and Safari 5.0 uses \"computed value\" instead of \"used value\" for margin-right\n\t\t\t// Safari 5.1.7 (at least) returns percentage for a larger set of values, but width seems to be reliably pixels\n\t\t\t// this is against the CSSOM draft spec: http://dev.w3.org/csswg/cssom/#resolved-values\n\t\t\tif ( rnumnonpx.test( ret ) && rmargin.test( name ) ) {\n\n\t\t\t\t// Remember the original values\n\t\t\t\twidth = style.width;\n\t\t\t\tminWidth = style.minWidth;\n\t\t\t\tmaxWidth = style.maxWidth;\n\n\t\t\t\t// Put in the new values to get a computed value out\n\t\t\t\tstyle.minWidth = style.maxWidth = style.width = ret;\n\t\t\t\tret = computed.width;\n\n\t\t\t\t// Revert the changed values\n\t\t\t\tstyle.width = width;\n\t\t\t\tstyle.minWidth = minWidth;\n\t\t\t\tstyle.maxWidth = maxWidth;\n\t\t\t}\n\t\t}\n\n\t\t// Support: IE\n\t\t// IE returns zIndex value as an integer.\n\t\treturn ret === undefined ?\n\t\t\tret :\n\t\t\tret + \"\";\n\t};\n} else if ( document.documentElement.currentStyle ) {\n\tgetStyles = function( elem ) {\n\t\treturn elem.currentStyle;\n\t};\n\n\tcurCSS = function( elem, name, computed ) {\n\t\tvar left, rs, rsLeft, ret,\n\t\t\tstyle = elem.style;\n\n\t\tcomputed = computed || getStyles( elem );\n\t\tret = computed ? computed[ name ] : undefined;\n\n\t\t// Avoid setting ret to empty string here\n\t\t// so we don't default to auto\n\t\tif ( ret == null && style && style[ name ] ) {\n\t\t\tret = style[ name ];\n\t\t}\n\n\t\t// From the awesome hack by Dean Edwards\n\t\t// http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291\n\n\t\t// If we're not dealing with a regular pixel number\n\t\t// but a number that has a weird ending, we need to convert it to pixels\n\t\t// but not position css attributes, as those are proportional to the parent element instead\n\t\t// and we can't measure the parent instead because it might trigger a \"stacking dolls\" problem\n\t\tif ( rnumnonpx.test( ret ) && !rposition.test( name ) ) {\n\n\t\t\t// Remember the original values\n\t\t\tleft = style.left;\n\t\t\trs = elem.runtimeStyle;\n\t\t\trsLeft = rs && rs.left;\n\n\t\t\t// Put in the new values to get a computed value out\n\t\t\tif ( rsLeft ) {\n\t\t\t\trs.left = elem.currentStyle.left;\n\t\t\t}\n\t\t\tstyle.left = name === \"fontSize\" ? \"1em\" : ret;\n\t\t\tret = style.pixelLeft + \"px\";\n\n\t\t\t// Revert the changed values\n\t\t\tstyle.left = left;\n\t\t\tif ( rsLeft ) {\n\t\t\t\trs.left = rsLeft;\n\t\t\t}\n\t\t}\n\n\t\t// Support: IE\n\t\t// IE returns zIndex value as an integer.\n\t\treturn ret === undefined ?\n\t\t\tret :\n\t\t\tret + \"\" || \"auto\";\n\t};\n}\n\n\n\n\nfunction addGetHookIf( conditionFn, hookFn ) {\n\t// Define the hook, we'll check on the first run if it's really needed.\n\treturn {\n\t\tget: function() {\n\t\t\tvar condition = conditionFn();\n\n\t\t\tif ( condition == null ) {\n\t\t\t\t// The test was not ready at this point; screw the hook this time\n\t\t\t\t// but check again when needed next time.\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif ( condition ) {\n\t\t\t\t// Hook not needed (or it's not possible to use it due to missing dependency),\n\t\t\t\t// remove it.\n\t\t\t\t// Since there are no other hooks for marginRight, remove the whole object.\n\t\t\t\tdelete this.get;\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Hook needed; redefine it so that the support test is not executed again.\n\n\t\t\treturn (this.get = hookFn).apply( this, arguments );\n\t\t}\n\t};\n}\n\n\n(function() {\n\t// Minified: var b,c,d,e,f,g, h,i\n\tvar div, style, a, pixelPositionVal, boxSizingReliableVal,\n\t\treliableHiddenOffsetsVal, reliableMarginRightVal;\n\n\t// Setup\n\tdiv = document.createElement( \"div\" );\n\tdiv.innerHTML = \"  <link/><table></table><a href='/a'>a</a><input type='checkbox'/>\";\n\ta = div.getElementsByTagName( \"a\" )[ 0 ];\n\tstyle = a && a.style;\n\n\t// Finish early in limited (non-browser) environments\n\tif ( !style ) {\n\t\treturn;\n\t}\n\n\tstyle.cssText = \"float:left;opacity:.5\";\n\n\t// Support: IE<9\n\t// Make sure that element opacity exists (as opposed to filter)\n\tsupport.opacity = style.opacity === \"0.5\";\n\n\t// Verify style float existence\n\t// (IE uses styleFloat instead of cssFloat)\n\tsupport.cssFloat = !!style.cssFloat;\n\n\tdiv.style.backgroundClip = \"content-box\";\n\tdiv.cloneNode( true ).style.backgroundClip = \"\";\n\tsupport.clearCloneStyle = div.style.backgroundClip === \"content-box\";\n\n\t// Support: Firefox<29, Android 2.3\n\t// Vendor-prefix box-sizing\n\tsupport.boxSizing = style.boxSizing === \"\" || style.MozBoxSizing === \"\" ||\n\t\tstyle.WebkitBoxSizing === \"\";\n\n\tjQuery.extend(support, {\n\t\treliableHiddenOffsets: function() {\n\t\t\tif ( reliableHiddenOffsetsVal == null ) {\n\t\t\t\tcomputeStyleTests();\n\t\t\t}\n\t\t\treturn reliableHiddenOffsetsVal;\n\t\t},\n\n\t\tboxSizingReliable: function() {\n\t\t\tif ( boxSizingReliableVal == null ) {\n\t\t\t\tcomputeStyleTests();\n\t\t\t}\n\t\t\treturn boxSizingReliableVal;\n\t\t},\n\n\t\tpixelPosition: function() {\n\t\t\tif ( pixelPositionVal == null ) {\n\t\t\t\tcomputeStyleTests();\n\t\t\t}\n\t\t\treturn pixelPositionVal;\n\t\t},\n\n\t\t// Support: Android 2.3\n\t\treliableMarginRight: function() {\n\t\t\tif ( reliableMarginRightVal == null ) {\n\t\t\t\tcomputeStyleTests();\n\t\t\t}\n\t\t\treturn reliableMarginRightVal;\n\t\t}\n\t});\n\n\tfunction computeStyleTests() {\n\t\t// Minified: var b,c,d,j\n\t\tvar div, body, container, contents;\n\n\t\tbody = document.getElementsByTagName( \"body\" )[ 0 ];\n\t\tif ( !body || !body.style ) {\n\t\t\t// Test fired too early or in an unsupported environment, exit.\n\t\t\treturn;\n\t\t}\n\n\t\t// Setup\n\t\tdiv = document.createElement( \"div\" );\n\t\tcontainer = document.createElement( \"div\" );\n\t\tcontainer.style.cssText = \"position:absolute;border:0;width:0;height:0;top:0;left:-9999px\";\n\t\tbody.appendChild( container ).appendChild( div );\n\n\t\tdiv.style.cssText =\n\t\t\t// Support: Firefox<29, Android 2.3\n\t\t\t// Vendor-prefix box-sizing\n\t\t\t\"-webkit-box-sizing:border-box;-moz-box-sizing:border-box;\" +\n\t\t\t\"box-sizing:border-box;display:block;margin-top:1%;top:1%;\" +\n\t\t\t\"border:1px;padding:1px;width:4px;position:absolute\";\n\n\t\t// Support: IE<9\n\t\t// Assume reasonable values in the absence of getComputedStyle\n\t\tpixelPositionVal = boxSizingReliableVal = false;\n\t\treliableMarginRightVal = true;\n\n\t\t// Check for getComputedStyle so that this code is not run in IE<9.\n\t\tif ( window.getComputedStyle ) {\n\t\t\tpixelPositionVal = ( window.getComputedStyle( div, null ) || {} ).top !== \"1%\";\n\t\t\tboxSizingReliableVal =\n\t\t\t\t( window.getComputedStyle( div, null ) || { width: \"4px\" } ).width === \"4px\";\n\n\t\t\t// Support: Android 2.3\n\t\t\t// Div with explicit width and no margin-right incorrectly\n\t\t\t// gets computed margin-right based on width of container (#3333)\n\t\t\t// WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right\n\t\t\tcontents = div.appendChild( document.createElement( \"div\" ) );\n\n\t\t\t// Reset CSS: box-sizing; display; margin; border; padding\n\t\t\tcontents.style.cssText = div.style.cssText =\n\t\t\t\t// Support: Firefox<29, Android 2.3\n\t\t\t\t// Vendor-prefix box-sizing\n\t\t\t\t\"-webkit-box-sizing:content-box;-moz-box-sizing:content-box;\" +\n\t\t\t\t\"box-sizing:content-box;display:block;margin:0;border:0;padding:0\";\n\t\t\tcontents.style.marginRight = contents.style.width = \"0\";\n\t\t\tdiv.style.width = \"1px\";\n\n\t\t\treliableMarginRightVal =\n\t\t\t\t!parseFloat( ( window.getComputedStyle( contents, null ) || {} ).marginRight );\n\t\t}\n\n\t\t// Support: IE8\n\t\t// Check if table cells still have offsetWidth/Height when they are set\n\t\t// to display:none and there are still other visible table cells in a\n\t\t// table row; if so, offsetWidth/Height are not reliable for use when\n\t\t// determining if an element has been hidden directly using\n\t\t// display:none (it is still safe to use offsets if a parent element is\n\t\t// hidden; don safety goggles and see bug #4512 for more information).\n\t\tdiv.innerHTML = \"<table><tr><td></td><td>t</td></tr></table>\";\n\t\tcontents = div.getElementsByTagName( \"td\" );\n\t\tcontents[ 0 ].style.cssText = \"margin:0;border:0;padding:0;display:none\";\n\t\treliableHiddenOffsetsVal = contents[ 0 ].offsetHeight === 0;\n\t\tif ( reliableHiddenOffsetsVal ) {\n\t\t\tcontents[ 0 ].style.display = \"\";\n\t\t\tcontents[ 1 ].style.display = \"none\";\n\t\t\treliableHiddenOffsetsVal = contents[ 0 ].offsetHeight === 0;\n\t\t}\n\n\t\tbody.removeChild( container );\n\t}\n\n})();\n\n\n// A method for quickly swapping in/out CSS properties to get correct calculations.\njQuery.swap = function( elem, options, callback, args ) {\n\tvar ret, name,\n\t\told = {};\n\n\t// Remember the old values, and insert the new ones\n\tfor ( name in options ) {\n\t\told[ name ] = elem.style[ name ];\n\t\telem.style[ name ] = options[ name ];\n\t}\n\n\tret = callback.apply( elem, args || [] );\n\n\t// Revert the old values\n\tfor ( name in options ) {\n\t\telem.style[ name ] = old[ name ];\n\t}\n\n\treturn ret;\n};\n\n\nvar\n\t\tralpha = /alpha\\([^)]*\\)/i,\n\tropacity = /opacity\\s*=\\s*([^)]*)/,\n\n\t// swappable if display is none or starts with table except \"table\", \"table-cell\", or \"table-caption\"\n\t// see here for display values: https://developer.mozilla.org/en-US/docs/CSS/display\n\trdisplayswap = /^(none|table(?!-c[ea]).+)/,\n\trnumsplit = new RegExp( \"^(\" + pnum + \")(.*)$\", \"i\" ),\n\trrelNum = new RegExp( \"^([+-])=(\" + pnum + \")\", \"i\" ),\n\n\tcssShow = { position: \"absolute\", visibility: \"hidden\", display: \"block\" },\n\tcssNormalTransform = {\n\t\tletterSpacing: \"0\",\n\t\tfontWeight: \"400\"\n\t},\n\n\tcssPrefixes = [ \"Webkit\", \"O\", \"Moz\", \"ms\" ];\n\n\n// return a css property mapped to a potentially vendor prefixed property\nfunction vendorPropName( style, name ) {\n\n\t// shortcut for names that are not vendor prefixed\n\tif ( name in style ) {\n\t\treturn name;\n\t}\n\n\t// check for vendor prefixed names\n\tvar capName = name.charAt(0).toUpperCase() + name.slice(1),\n\t\torigName = name,\n\t\ti = cssPrefixes.length;\n\n\twhile ( i-- ) {\n\t\tname = cssPrefixes[ i ] + capName;\n\t\tif ( name in style ) {\n\t\t\treturn name;\n\t\t}\n\t}\n\n\treturn origName;\n}\n\nfunction showHide( elements, show ) {\n\tvar display, elem, hidden,\n\t\tvalues = [],\n\t\tindex = 0,\n\t\tlength = elements.length;\n\n\tfor ( ; index < length; index++ ) {\n\t\telem = elements[ index ];\n\t\tif ( !elem.style ) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tvalues[ index ] = jQuery._data( elem, \"olddisplay\" );\n\t\tdisplay = elem.style.display;\n\t\tif ( show ) {\n\t\t\t// Reset the inline display of this element to learn if it is\n\t\t\t// being hidden by cascaded rules or not\n\t\t\tif ( !values[ index ] && display === \"none\" ) {\n\t\t\t\telem.style.display = \"\";\n\t\t\t}\n\n\t\t\t// Set elements which have been overridden with display: none\n\t\t\t// in a stylesheet to whatever the default browser style is\n\t\t\t// for such an element\n\t\t\tif ( elem.style.display === \"\" && isHidden( elem ) ) {\n\t\t\t\tvalues[ index ] = jQuery._data( elem, \"olddisplay\", defaultDisplay(elem.nodeName) );\n\t\t\t}\n\t\t} else {\n\t\t\thidden = isHidden( elem );\n\n\t\t\tif ( display && display !== \"none\" || !hidden ) {\n\t\t\t\tjQuery._data( elem, \"olddisplay\", hidden ? display : jQuery.css( elem, \"display\" ) );\n\t\t\t}\n\t\t}\n\t}\n\n\t// Set the display of most of the elements in a second loop\n\t// to avoid the constant reflow\n\tfor ( index = 0; index < length; index++ ) {\n\t\telem = elements[ index ];\n\t\tif ( !elem.style ) {\n\t\t\tcontinue;\n\t\t}\n\t\tif ( !show || elem.style.display === \"none\" || elem.style.display === \"\" ) {\n\t\t\telem.style.display = show ? values[ index ] || \"\" : \"none\";\n\t\t}\n\t}\n\n\treturn elements;\n}\n\nfunction setPositiveNumber( elem, value, subtract ) {\n\tvar matches = rnumsplit.exec( value );\n\treturn matches ?\n\t\t// Guard against undefined \"subtract\", e.g., when used as in cssHooks\n\t\tMath.max( 0, matches[ 1 ] - ( subtract || 0 ) ) + ( matches[ 2 ] || \"px\" ) :\n\t\tvalue;\n}\n\nfunction augmentWidthOrHeight( elem, name, extra, isBorderBox, styles ) {\n\tvar i = extra === ( isBorderBox ? \"border\" : \"content\" ) ?\n\t\t// If we already have the right measurement, avoid augmentation\n\t\t4 :\n\t\t// Otherwise initialize for horizontal or vertical properties\n\t\tname === \"width\" ? 1 : 0,\n\n\t\tval = 0;\n\n\tfor ( ; i < 4; i += 2 ) {\n\t\t// both box models exclude margin, so add it if we want it\n\t\tif ( extra === \"margin\" ) {\n\t\t\tval += jQuery.css( elem, extra + cssExpand[ i ], true, styles );\n\t\t}\n\n\t\tif ( isBorderBox ) {\n\t\t\t// border-box includes padding, so remove it if we want content\n\t\t\tif ( extra === \"content\" ) {\n\t\t\t\tval -= jQuery.css( elem, \"padding\" + cssExpand[ i ], true, styles );\n\t\t\t}\n\n\t\t\t// at this point, extra isn't border nor margin, so remove border\n\t\t\tif ( extra !== \"margin\" ) {\n\t\t\t\tval -= jQuery.css( elem, \"border\" + cssExpand[ i ] + \"Width\", true, styles );\n\t\t\t}\n\t\t} else {\n\t\t\t// at this point, extra isn't content, so add padding\n\t\t\tval += jQuery.css( elem, \"padding\" + cssExpand[ i ], true, styles );\n\n\t\t\t// at this point, extra isn't content nor padding, so add border\n\t\t\tif ( extra !== \"padding\" ) {\n\t\t\t\tval += jQuery.css( elem, \"border\" + cssExpand[ i ] + \"Width\", true, styles );\n\t\t\t}\n\t\t}\n\t}\n\n\treturn val;\n}\n\nfunction getWidthOrHeight( elem, name, extra ) {\n\n\t// Start with offset property, which is equivalent to the border-box value\n\tvar valueIsBorderBox = true,\n\t\tval = name === \"width\" ? elem.offsetWidth : elem.offsetHeight,\n\t\tstyles = getStyles( elem ),\n\t\tisBorderBox = support.boxSizing && jQuery.css( elem, \"boxSizing\", false, styles ) === \"border-box\";\n\n\t// some non-html elements return undefined for offsetWidth, so check for null/undefined\n\t// svg - https://bugzilla.mozilla.org/show_bug.cgi?id=649285\n\t// MathML - https://bugzilla.mozilla.org/show_bug.cgi?id=491668\n\tif ( val <= 0 || val == null ) {\n\t\t// Fall back to computed then uncomputed css if necessary\n\t\tval = curCSS( elem, name, styles );\n\t\tif ( val < 0 || val == null ) {\n\t\t\tval = elem.style[ name ];\n\t\t}\n\n\t\t// Computed unit is not pixels. Stop here and return.\n\t\tif ( rnumnonpx.test(val) ) {\n\t\t\treturn val;\n\t\t}\n\n\t\t// we need the check for style in case a browser which returns unreliable values\n\t\t// for getComputedStyle silently falls back to the reliable elem.style\n\t\tvalueIsBorderBox = isBorderBox && ( support.boxSizingReliable() || val === elem.style[ name ] );\n\n\t\t// Normalize \"\", auto, and prepare for extra\n\t\tval = parseFloat( val ) || 0;\n\t}\n\n\t// use the active box-sizing model to add/subtract irrelevant styles\n\treturn ( val +\n\t\taugmentWidthOrHeight(\n\t\t\telem,\n\t\t\tname,\n\t\t\textra || ( isBorderBox ? \"border\" : \"content\" ),\n\t\t\tvalueIsBorderBox,\n\t\t\tstyles\n\t\t)\n\t) + \"px\";\n}\n\njQuery.extend({\n\t// Add in style property hooks for overriding the default\n\t// behavior of getting and setting a style property\n\tcssHooks: {\n\t\topacity: {\n\t\t\tget: function( elem, computed ) {\n\t\t\t\tif ( computed ) {\n\t\t\t\t\t// We should always get a number back from opacity\n\t\t\t\t\tvar ret = curCSS( elem, \"opacity\" );\n\t\t\t\t\treturn ret === \"\" ? \"1\" : ret;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t},\n\n\t// Don't automatically add \"px\" to these possibly-unitless properties\n\tcssNumber: {\n\t\t\"columnCount\": true,\n\t\t\"fillOpacity\": true,\n\t\t\"flexGrow\": true,\n\t\t\"flexShrink\": true,\n\t\t\"fontWeight\": true,\n\t\t\"lineHeight\": true,\n\t\t\"opacity\": true,\n\t\t\"order\": true,\n\t\t\"orphans\": true,\n\t\t\"widows\": true,\n\t\t\"zIndex\": true,\n\t\t\"zoom\": true\n\t},\n\n\t// Add in properties whose names you wish to fix before\n\t// setting or getting the value\n\tcssProps: {\n\t\t// normalize float css property\n\t\t\"float\": support.cssFloat ? \"cssFloat\" : \"styleFloat\"\n\t},\n\n\t// Get and set the style property on a DOM Node\n\tstyle: function( elem, name, value, extra ) {\n\t\t// Don't set styles on text and comment nodes\n\t\tif ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Make sure that we're working with the right name\n\t\tvar ret, type, hooks,\n\t\t\torigName = jQuery.camelCase( name ),\n\t\t\tstyle = elem.style;\n\n\t\tname = jQuery.cssProps[ origName ] || ( jQuery.cssProps[ origName ] = vendorPropName( style, origName ) );\n\n\t\t// gets hook for the prefixed version\n\t\t// followed by the unprefixed version\n\t\thooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ];\n\n\t\t// Check if we're setting a value\n\t\tif ( value !== undefined ) {\n\t\t\ttype = typeof value;\n\n\t\t\t// convert relative number strings (+= or -=) to relative numbers. #7345\n\t\t\tif ( type === \"string\" && (ret = rrelNum.exec( value )) ) {\n\t\t\t\tvalue = ( ret[1] + 1 ) * ret[2] + parseFloat( jQuery.css( elem, name ) );\n\t\t\t\t// Fixes bug #9237\n\t\t\t\ttype = \"number\";\n\t\t\t}\n\n\t\t\t// Make sure that null and NaN values aren't set. See: #7116\n\t\t\tif ( value == null || value !== value ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// If a number was passed in, add 'px' to the (except for certain CSS properties)\n\t\t\tif ( type === \"number\" && !jQuery.cssNumber[ origName ] ) {\n\t\t\t\tvalue += \"px\";\n\t\t\t}\n\n\t\t\t// Fixes #8908, it can be done more correctly by specifing setters in cssHooks,\n\t\t\t// but it would mean to define eight (for every problematic property) identical functions\n\t\t\tif ( !support.clearCloneStyle && value === \"\" && name.indexOf(\"background\") === 0 ) {\n\t\t\t\tstyle[ name ] = \"inherit\";\n\t\t\t}\n\n\t\t\t// If a hook was provided, use that value, otherwise just set the specified value\n\t\t\tif ( !hooks || !(\"set\" in hooks) || (value = hooks.set( elem, value, extra )) !== undefined ) {\n\n\t\t\t\t// Support: IE\n\t\t\t\t// Swallow errors from 'invalid' CSS values (#5509)\n\t\t\t\ttry {\n\t\t\t\t\tstyle[ name ] = value;\n\t\t\t\t} catch(e) {}\n\t\t\t}\n\n\t\t} else {\n\t\t\t// If a hook was provided get the non-computed value from there\n\t\t\tif ( hooks && \"get\" in hooks && (ret = hooks.get( elem, false, extra )) !== undefined ) {\n\t\t\t\treturn ret;\n\t\t\t}\n\n\t\t\t// Otherwise just get the value from the style object\n\t\t\treturn style[ name ];\n\t\t}\n\t},\n\n\tcss: function( elem, name, extra, styles ) {\n\t\tvar num, val, hooks,\n\t\t\torigName = jQuery.camelCase( name );\n\n\t\t// Make sure that we're working with the right name\n\t\tname = jQuery.cssProps[ origName ] || ( jQuery.cssProps[ origName ] = vendorPropName( elem.style, origName ) );\n\n\t\t// gets hook for the prefixed version\n\t\t// followed by the unprefixed version\n\t\thooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ];\n\n\t\t// If a hook was provided get the computed value from there\n\t\tif ( hooks && \"get\" in hooks ) {\n\t\t\tval = hooks.get( elem, true, extra );\n\t\t}\n\n\t\t// Otherwise, if a way to get the computed value exists, use that\n\t\tif ( val === undefined ) {\n\t\t\tval = curCSS( elem, name, styles );\n\t\t}\n\n\t\t//convert \"normal\" to computed value\n\t\tif ( val === \"normal\" && name in cssNormalTransform ) {\n\t\t\tval = cssNormalTransform[ name ];\n\t\t}\n\n\t\t// Return, converting to number if forced or a qualifier was provided and val looks numeric\n\t\tif ( extra === \"\" || extra ) {\n\t\t\tnum = parseFloat( val );\n\t\t\treturn extra === true || jQuery.isNumeric( num ) ? num || 0 : val;\n\t\t}\n\t\treturn val;\n\t}\n});\n\njQuery.each([ \"height\", \"width\" ], function( i, name ) {\n\tjQuery.cssHooks[ name ] = {\n\t\tget: function( elem, computed, extra ) {\n\t\t\tif ( computed ) {\n\t\t\t\t// certain elements can have dimension info if we invisibly show them\n\t\t\t\t// however, it must have a current display style that would benefit from this\n\t\t\t\treturn rdisplayswap.test( jQuery.css( elem, \"display\" ) ) && elem.offsetWidth === 0 ?\n\t\t\t\t\tjQuery.swap( elem, cssShow, function() {\n\t\t\t\t\t\treturn getWidthOrHeight( elem, name, extra );\n\t\t\t\t\t}) :\n\t\t\t\t\tgetWidthOrHeight( elem, name, extra );\n\t\t\t}\n\t\t},\n\n\t\tset: function( elem, value, extra ) {\n\t\t\tvar styles = extra && getStyles( elem );\n\t\t\treturn setPositiveNumber( elem, value, extra ?\n\t\t\t\taugmentWidthOrHeight(\n\t\t\t\t\telem,\n\t\t\t\t\tname,\n\t\t\t\t\textra,\n\t\t\t\t\tsupport.boxSizing && jQuery.css( elem, \"boxSizing\", false, styles ) === \"border-box\",\n\t\t\t\t\tstyles\n\t\t\t\t) : 0\n\t\t\t);\n\t\t}\n\t};\n});\n\nif ( !support.opacity ) {\n\tjQuery.cssHooks.opacity = {\n\t\tget: function( elem, computed ) {\n\t\t\t// IE uses filters for opacity\n\t\t\treturn ropacity.test( (computed && elem.currentStyle ? elem.currentStyle.filter : elem.style.filter) || \"\" ) ?\n\t\t\t\t( 0.01 * parseFloat( RegExp.$1 ) ) + \"\" :\n\t\t\t\tcomputed ? \"1\" : \"\";\n\t\t},\n\n\t\tset: function( elem, value ) {\n\t\t\tvar style = elem.style,\n\t\t\t\tcurrentStyle = elem.currentStyle,\n\t\t\t\topacity = jQuery.isNumeric( value ) ? \"alpha(opacity=\" + value * 100 + \")\" : \"\",\n\t\t\t\tfilter = currentStyle && currentStyle.filter || style.filter || \"\";\n\n\t\t\t// IE has trouble with opacity if it does not have layout\n\t\t\t// Force it by setting the zoom level\n\t\t\tstyle.zoom = 1;\n\n\t\t\t// if setting opacity to 1, and no other filters exist - attempt to remove filter attribute #6652\n\t\t\t// if value === \"\", then remove inline opacity #12685\n\t\t\tif ( ( value >= 1 || value === \"\" ) &&\n\t\t\t\t\tjQuery.trim( filter.replace( ralpha, \"\" ) ) === \"\" &&\n\t\t\t\t\tstyle.removeAttribute ) {\n\n\t\t\t\t// Setting style.filter to null, \"\" & \" \" still leave \"filter:\" in the cssText\n\t\t\t\t// if \"filter:\" is present at all, clearType is disabled, we want to avoid this\n\t\t\t\t// style.removeAttribute is IE Only, but so apparently is this code path...\n\t\t\t\tstyle.removeAttribute( \"filter\" );\n\n\t\t\t\t// if there is no filter style applied in a css rule or unset inline opacity, we are done\n\t\t\t\tif ( value === \"\" || currentStyle && !currentStyle.filter ) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// otherwise, set new filter values\n\t\t\tstyle.filter = ralpha.test( filter ) ?\n\t\t\t\tfilter.replace( ralpha, opacity ) :\n\t\t\t\tfilter + \" \" + opacity;\n\t\t}\n\t};\n}\n\njQuery.cssHooks.marginRight = addGetHookIf( support.reliableMarginRight,\n\tfunction( elem, computed ) {\n\t\tif ( computed ) {\n\t\t\t// WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right\n\t\t\t// Work around by temporarily setting element display to inline-block\n\t\t\treturn jQuery.swap( elem, { \"display\": \"inline-block\" },\n\t\t\t\tcurCSS, [ elem, \"marginRight\" ] );\n\t\t}\n\t}\n);\n\n// These hooks are used by animate to expand properties\njQuery.each({\n\tmargin: \"\",\n\tpadding: \"\",\n\tborder: \"Width\"\n}, function( prefix, suffix ) {\n\tjQuery.cssHooks[ prefix + suffix ] = {\n\t\texpand: function( value ) {\n\t\t\tvar i = 0,\n\t\t\t\texpanded = {},\n\n\t\t\t\t// assumes a single number if not a string\n\t\t\t\tparts = typeof value === \"string\" ? value.split(\" \") : [ value ];\n\n\t\t\tfor ( ; i < 4; i++ ) {\n\t\t\t\texpanded[ prefix + cssExpand[ i ] + suffix ] =\n\t\t\t\t\tparts[ i ] || parts[ i - 2 ] || parts[ 0 ];\n\t\t\t}\n\n\t\t\treturn expanded;\n\t\t}\n\t};\n\n\tif ( !rmargin.test( prefix ) ) {\n\t\tjQuery.cssHooks[ prefix + suffix ].set = setPositiveNumber;\n\t}\n});\n\njQuery.fn.extend({\n\tcss: function( name, value ) {\n\t\treturn access( this, function( elem, name, value ) {\n\t\t\tvar styles, len,\n\t\t\t\tmap = {},\n\t\t\t\ti = 0;\n\n\t\t\tif ( jQuery.isArray( name ) ) {\n\t\t\t\tstyles = getStyles( elem );\n\t\t\t\tlen = name.length;\n\n\t\t\t\tfor ( ; i < len; i++ ) {\n\t\t\t\t\tmap[ name[ i ] ] = jQuery.css( elem, name[ i ], false, styles );\n\t\t\t\t}\n\n\t\t\t\treturn map;\n\t\t\t}\n\n\t\t\treturn value !== undefined ?\n\t\t\t\tjQuery.style( elem, name, value ) :\n\t\t\t\tjQuery.css( elem, name );\n\t\t}, name, value, arguments.length > 1 );\n\t},\n\tshow: function() {\n\t\treturn showHide( this, true );\n\t},\n\thide: function() {\n\t\treturn showHide( this );\n\t},\n\ttoggle: function( state ) {\n\t\tif ( typeof state === \"boolean\" ) {\n\t\t\treturn state ? this.show() : this.hide();\n\t\t}\n\n\t\treturn this.each(function() {\n\t\t\tif ( isHidden( this ) ) {\n\t\t\t\tjQuery( this ).show();\n\t\t\t} else {\n\t\t\t\tjQuery( this ).hide();\n\t\t\t}\n\t\t});\n\t}\n});\n\n\nfunction Tween( elem, options, prop, end, easing ) {\n\treturn new Tween.prototype.init( elem, options, prop, end, easing );\n}\njQuery.Tween = Tween;\n\nTween.prototype = {\n\tconstructor: Tween,\n\tinit: function( elem, options, prop, end, easing, unit ) {\n\t\tthis.elem = elem;\n\t\tthis.prop = prop;\n\t\tthis.easing = easing || \"swing\";\n\t\tthis.options = options;\n\t\tthis.start = this.now = this.cur();\n\t\tthis.end = end;\n\t\tthis.unit = unit || ( jQuery.cssNumber[ prop ] ? \"\" : \"px\" );\n\t},\n\tcur: function() {\n\t\tvar hooks = Tween.propHooks[ this.prop ];\n\n\t\treturn hooks && hooks.get ?\n\t\t\thooks.get( this ) :\n\t\t\tTween.propHooks._default.get( this );\n\t},\n\trun: function( percent ) {\n\t\tvar eased,\n\t\t\thooks = Tween.propHooks[ this.prop ];\n\n\t\tif ( this.options.duration ) {\n\t\t\tthis.pos = eased = jQuery.easing[ this.easing ](\n\t\t\t\tpercent, this.options.duration * percent, 0, 1, this.options.duration\n\t\t\t);\n\t\t} else {\n\t\t\tthis.pos = eased = percent;\n\t\t}\n\t\tthis.now = ( this.end - this.start ) * eased + this.start;\n\n\t\tif ( this.options.step ) {\n\t\t\tthis.options.step.call( this.elem, this.now, this );\n\t\t}\n\n\t\tif ( hooks && hooks.set ) {\n\t\t\thooks.set( this );\n\t\t} else {\n\t\t\tTween.propHooks._default.set( this );\n\t\t}\n\t\treturn this;\n\t}\n};\n\nTween.prototype.init.prototype = Tween.prototype;\n\nTween.propHooks = {\n\t_default: {\n\t\tget: function( tween ) {\n\t\t\tvar result;\n\n\t\t\tif ( tween.elem[ tween.prop ] != null &&\n\t\t\t\t(!tween.elem.style || tween.elem.style[ tween.prop ] == null) ) {\n\t\t\t\treturn tween.elem[ tween.prop ];\n\t\t\t}\n\n\t\t\t// passing an empty string as a 3rd parameter to .css will automatically\n\t\t\t// attempt a parseFloat and fallback to a string if the parse fails\n\t\t\t// so, simple values such as \"10px\" are parsed to Float.\n\t\t\t// complex values such as \"rotate(1rad)\" are returned as is.\n\t\t\tresult = jQuery.css( tween.elem, tween.prop, \"\" );\n\t\t\t// Empty strings, null, undefined and \"auto\" are converted to 0.\n\t\t\treturn !result || result === \"auto\" ? 0 : result;\n\t\t},\n\t\tset: function( tween ) {\n\t\t\t// use step hook for back compat - use cssHook if its there - use .style if its\n\t\t\t// available and use plain properties where available\n\t\t\tif ( jQuery.fx.step[ tween.prop ] ) {\n\t\t\t\tjQuery.fx.step[ tween.prop ]( tween );\n\t\t\t} else if ( tween.elem.style && ( tween.elem.style[ jQuery.cssProps[ tween.prop ] ] != null || jQuery.cssHooks[ tween.prop ] ) ) {\n\t\t\t\tjQuery.style( tween.elem, tween.prop, tween.now + tween.unit );\n\t\t\t} else {\n\t\t\t\ttween.elem[ tween.prop ] = tween.now;\n\t\t\t}\n\t\t}\n\t}\n};\n\n// Support: IE <=9\n// Panic based approach to setting things on disconnected nodes\n\nTween.propHooks.scrollTop = Tween.propHooks.scrollLeft = {\n\tset: function( tween ) {\n\t\tif ( tween.elem.nodeType && tween.elem.parentNode ) {\n\t\t\ttween.elem[ tween.prop ] = tween.now;\n\t\t}\n\t}\n};\n\njQuery.easing = {\n\tlinear: function( p ) {\n\t\treturn p;\n\t},\n\tswing: function( p ) {\n\t\treturn 0.5 - Math.cos( p * Math.PI ) / 2;\n\t}\n};\n\njQuery.fx = Tween.prototype.init;\n\n// Back Compat <1.8 extension point\njQuery.fx.step = {};\n\n\n\n\nvar\n\tfxNow, timerId,\n\trfxtypes = /^(?:toggle|show|hide)$/,\n\trfxnum = new RegExp( \"^(?:([+-])=|)(\" + pnum + \")([a-z%]*)$\", \"i\" ),\n\trrun = /queueHooks$/,\n\tanimationPrefilters = [ defaultPrefilter ],\n\ttweeners = {\n\t\t\"*\": [ function( prop, value ) {\n\t\t\tvar tween = this.createTween( prop, value ),\n\t\t\t\ttarget = tween.cur(),\n\t\t\t\tparts = rfxnum.exec( value ),\n\t\t\t\tunit = parts && parts[ 3 ] || ( jQuery.cssNumber[ prop ] ? \"\" : \"px\" ),\n\n\t\t\t\t// Starting value computation is required for potential unit mismatches\n\t\t\t\tstart = ( jQuery.cssNumber[ prop ] || unit !== \"px\" && +target ) &&\n\t\t\t\t\trfxnum.exec( jQuery.css( tween.elem, prop ) ),\n\t\t\t\tscale = 1,\n\t\t\t\tmaxIterations = 20;\n\n\t\t\tif ( start && start[ 3 ] !== unit ) {\n\t\t\t\t// Trust units reported by jQuery.css\n\t\t\t\tunit = unit || start[ 3 ];\n\n\t\t\t\t// Make sure we update the tween properties later on\n\t\t\t\tparts = parts || [];\n\n\t\t\t\t// Iteratively approximate from a nonzero starting point\n\t\t\t\tstart = +target || 1;\n\n\t\t\t\tdo {\n\t\t\t\t\t// If previous iteration zeroed out, double until we get *something*\n\t\t\t\t\t// Use a string for doubling factor so we don't accidentally see scale as unchanged below\n\t\t\t\t\tscale = scale || \".5\";\n\n\t\t\t\t\t// Adjust and apply\n\t\t\t\t\tstart = start / scale;\n\t\t\t\t\tjQuery.style( tween.elem, prop, start + unit );\n\n\t\t\t\t// Update scale, tolerating zero or NaN from tween.cur()\n\t\t\t\t// And breaking the loop if scale is unchanged or perfect, or if we've just had enough\n\t\t\t\t} while ( scale !== (scale = tween.cur() / target) && scale !== 1 && --maxIterations );\n\t\t\t}\n\n\t\t\t// Update tween properties\n\t\t\tif ( parts ) {\n\t\t\t\tstart = tween.start = +start || +target || 0;\n\t\t\t\ttween.unit = unit;\n\t\t\t\t// If a +=/-= token was provided, we're doing a relative animation\n\t\t\t\ttween.end = parts[ 1 ] ?\n\t\t\t\t\tstart + ( parts[ 1 ] + 1 ) * parts[ 2 ] :\n\t\t\t\t\t+parts[ 2 ];\n\t\t\t}\n\n\t\t\treturn tween;\n\t\t} ]\n\t};\n\n// Animations created synchronously will run synchronously\nfunction createFxNow() {\n\tsetTimeout(function() {\n\t\tfxNow = undefined;\n\t});\n\treturn ( fxNow = jQuery.now() );\n}\n\n// Generate parameters to create a standard animation\nfunction genFx( type, includeWidth ) {\n\tvar which,\n\t\tattrs = { height: type },\n\t\ti = 0;\n\n\t// if we include width, step value is 1 to do all cssExpand values,\n\t// if we don't include width, step value is 2 to skip over Left and Right\n\tincludeWidth = includeWidth ? 1 : 0;\n\tfor ( ; i < 4 ; i += 2 - includeWidth ) {\n\t\twhich = cssExpand[ i ];\n\t\tattrs[ \"margin\" + which ] = attrs[ \"padding\" + which ] = type;\n\t}\n\n\tif ( includeWidth ) {\n\t\tattrs.opacity = attrs.width = type;\n\t}\n\n\treturn attrs;\n}\n\nfunction createTween( value, prop, animation ) {\n\tvar tween,\n\t\tcollection = ( tweeners[ prop ] || [] ).concat( tweeners[ \"*\" ] ),\n\t\tindex = 0,\n\t\tlength = collection.length;\n\tfor ( ; index < length; index++ ) {\n\t\tif ( (tween = collection[ index ].call( animation, prop, value )) ) {\n\n\t\t\t// we're done with this property\n\t\t\treturn tween;\n\t\t}\n\t}\n}\n\nfunction defaultPrefilter( elem, props, opts ) {\n\t/* jshint validthis: true */\n\tvar prop, value, toggle, tween, hooks, oldfire, display, checkDisplay,\n\t\tanim = this,\n\t\torig = {},\n\t\tstyle = elem.style,\n\t\thidden = elem.nodeType && isHidden( elem ),\n\t\tdataShow = jQuery._data( elem, \"fxshow\" );\n\n\t// handle queue: false promises\n\tif ( !opts.queue ) {\n\t\thooks = jQuery._queueHooks( elem, \"fx\" );\n\t\tif ( hooks.unqueued == null ) {\n\t\t\thooks.unqueued = 0;\n\t\t\toldfire = hooks.empty.fire;\n\t\t\thooks.empty.fire = function() {\n\t\t\t\tif ( !hooks.unqueued ) {\n\t\t\t\t\toldfire();\n\t\t\t\t}\n\t\t\t};\n\t\t}\n\t\thooks.unqueued++;\n\n\t\tanim.always(function() {\n\t\t\t// doing this makes sure that the complete handler will be called\n\t\t\t// before this completes\n\t\t\tanim.always(function() {\n\t\t\t\thooks.unqueued--;\n\t\t\t\tif ( !jQuery.queue( elem, \"fx\" ).length ) {\n\t\t\t\t\thooks.empty.fire();\n\t\t\t\t}\n\t\t\t});\n\t\t});\n\t}\n\n\t// height/width overflow pass\n\tif ( elem.nodeType === 1 && ( \"height\" in props || \"width\" in props ) ) {\n\t\t// Make sure that nothing sneaks out\n\t\t// Record all 3 overflow attributes because IE does not\n\t\t// change the overflow attribute when overflowX and\n\t\t// overflowY are set to the same value\n\t\topts.overflow = [ style.overflow, style.overflowX, style.overflowY ];\n\n\t\t// Set display property to inline-block for height/width\n\t\t// animations on inline elements that are having width/height animated\n\t\tdisplay = jQuery.css( elem, \"display\" );\n\n\t\t// Test default display if display is currently \"none\"\n\t\tcheckDisplay = display === \"none\" ?\n\t\t\tjQuery._data( elem, \"olddisplay\" ) || defaultDisplay( elem.nodeName ) : display;\n\n\t\tif ( checkDisplay === \"inline\" && jQuery.css( elem, \"float\" ) === \"none\" ) {\n\n\t\t\t// inline-level elements accept inline-block;\n\t\t\t// block-level elements need to be inline with layout\n\t\t\tif ( !support.inlineBlockNeedsLayout || defaultDisplay( elem.nodeName ) === \"inline\" ) {\n\t\t\t\tstyle.display = \"inline-block\";\n\t\t\t} else {\n\t\t\t\tstyle.zoom = 1;\n\t\t\t}\n\t\t}\n\t}\n\n\tif ( opts.overflow ) {\n\t\tstyle.overflow = \"hidden\";\n\t\tif ( !support.shrinkWrapBlocks() ) {\n\t\t\tanim.always(function() {\n\t\t\t\tstyle.overflow = opts.overflow[ 0 ];\n\t\t\t\tstyle.overflowX = opts.overflow[ 1 ];\n\t\t\t\tstyle.overflowY = opts.overflow[ 2 ];\n\t\t\t});\n\t\t}\n\t}\n\n\t// show/hide pass\n\tfor ( prop in props ) {\n\t\tvalue = props[ prop ];\n\t\tif ( rfxtypes.exec( value ) ) {\n\t\t\tdelete props[ prop ];\n\t\t\ttoggle = toggle || value === \"toggle\";\n\t\t\tif ( value === ( hidden ? \"hide\" : \"show\" ) ) {\n\n\t\t\t\t// If there is dataShow left over from a stopped hide or show and we are going to proceed with show, we should pretend to be hidden\n\t\t\t\tif ( value === \"show\" && dataShow && dataShow[ prop ] !== undefined ) {\n\t\t\t\t\thidden = true;\n\t\t\t\t} else {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t}\n\t\t\torig[ prop ] = dataShow && dataShow[ prop ] || jQuery.style( elem, prop );\n\n\t\t// Any non-fx value stops us from restoring the original display value\n\t\t} else {\n\t\t\tdisplay = undefined;\n\t\t}\n\t}\n\n\tif ( !jQuery.isEmptyObject( orig ) ) {\n\t\tif ( dataShow ) {\n\t\t\tif ( \"hidden\" in dataShow ) {\n\t\t\t\thidden = dataShow.hidden;\n\t\t\t}\n\t\t} else {\n\t\t\tdataShow = jQuery._data( elem, \"fxshow\", {} );\n\t\t}\n\n\t\t// store state if its toggle - enables .stop().toggle() to \"reverse\"\n\t\tif ( toggle ) {\n\t\t\tdataShow.hidden = !hidden;\n\t\t}\n\t\tif ( hidden ) {\n\t\t\tjQuery( elem ).show();\n\t\t} else {\n\t\t\tanim.done(function() {\n\t\t\t\tjQuery( elem ).hide();\n\t\t\t});\n\t\t}\n\t\tanim.done(function() {\n\t\t\tvar prop;\n\t\t\tjQuery._removeData( elem, \"fxshow\" );\n\t\t\tfor ( prop in orig ) {\n\t\t\t\tjQuery.style( elem, prop, orig[ prop ] );\n\t\t\t}\n\t\t});\n\t\tfor ( prop in orig ) {\n\t\t\ttween = createTween( hidden ? dataShow[ prop ] : 0, prop, anim );\n\n\t\t\tif ( !( prop in dataShow ) ) {\n\t\t\t\tdataShow[ prop ] = tween.start;\n\t\t\t\tif ( hidden ) {\n\t\t\t\t\ttween.end = tween.start;\n\t\t\t\t\ttween.start = prop === \"width\" || prop === \"height\" ? 1 : 0;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t// If this is a noop like .hide().hide(), restore an overwritten display value\n\t} else if ( (display === \"none\" ? defaultDisplay( elem.nodeName ) : display) === \"inline\" ) {\n\t\tstyle.display = display;\n\t}\n}\n\nfunction propFilter( props, specialEasing ) {\n\tvar index, name, easing, value, hooks;\n\n\t// camelCase, specialEasing and expand cssHook pass\n\tfor ( index in props ) {\n\t\tname = jQuery.camelCase( index );\n\t\teasing = specialEasing[ name ];\n\t\tvalue = props[ index ];\n\t\tif ( jQuery.isArray( value ) ) {\n\t\t\teasing = value[ 1 ];\n\t\t\tvalue = props[ index ] = value[ 0 ];\n\t\t}\n\n\t\tif ( index !== name ) {\n\t\t\tprops[ name ] = value;\n\t\t\tdelete props[ index ];\n\t\t}\n\n\t\thooks = jQuery.cssHooks[ name ];\n\t\tif ( hooks && \"expand\" in hooks ) {\n\t\t\tvalue = hooks.expand( value );\n\t\t\tdelete props[ name ];\n\n\t\t\t// not quite $.extend, this wont overwrite keys already present.\n\t\t\t// also - reusing 'index' from above because we have the correct \"name\"\n\t\t\tfor ( index in value ) {\n\t\t\t\tif ( !( index in props ) ) {\n\t\t\t\t\tprops[ index ] = value[ index ];\n\t\t\t\t\tspecialEasing[ index ] = easing;\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tspecialEasing[ name ] = easing;\n\t\t}\n\t}\n}\n\nfunction Animation( elem, properties, options ) {\n\tvar result,\n\t\tstopped,\n\t\tindex = 0,\n\t\tlength = animationPrefilters.length,\n\t\tdeferred = jQuery.Deferred().always( function() {\n\t\t\t// don't match elem in the :animated selector\n\t\t\tdelete tick.elem;\n\t\t}),\n\t\ttick = function() {\n\t\t\tif ( stopped ) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tvar currentTime = fxNow || createFxNow(),\n\t\t\t\tremaining = Math.max( 0, animation.startTime + animation.duration - currentTime ),\n\t\t\t\t// archaic crash bug won't allow us to use 1 - ( 0.5 || 0 ) (#12497)\n\t\t\t\ttemp = remaining / animation.duration || 0,\n\t\t\t\tpercent = 1 - temp,\n\t\t\t\tindex = 0,\n\t\t\t\tlength = animation.tweens.length;\n\n\t\t\tfor ( ; index < length ; index++ ) {\n\t\t\t\tanimation.tweens[ index ].run( percent );\n\t\t\t}\n\n\t\t\tdeferred.notifyWith( elem, [ animation, percent, remaining ]);\n\n\t\t\tif ( percent < 1 && length ) {\n\t\t\t\treturn remaining;\n\t\t\t} else {\n\t\t\t\tdeferred.resolveWith( elem, [ animation ] );\n\t\t\t\treturn false;\n\t\t\t}\n\t\t},\n\t\tanimation = deferred.promise({\n\t\t\telem: elem,\n\t\t\tprops: jQuery.extend( {}, properties ),\n\t\t\topts: jQuery.extend( true, { specialEasing: {} }, options ),\n\t\t\toriginalProperties: properties,\n\t\t\toriginalOptions: options,\n\t\t\tstartTime: fxNow || createFxNow(),\n\t\t\tduration: options.duration,\n\t\t\ttweens: [],\n\t\t\tcreateTween: function( prop, end ) {\n\t\t\t\tvar tween = jQuery.Tween( elem, animation.opts, prop, end,\n\t\t\t\t\t\tanimation.opts.specialEasing[ prop ] || animation.opts.easing );\n\t\t\t\tanimation.tweens.push( tween );\n\t\t\t\treturn tween;\n\t\t\t},\n\t\t\tstop: function( gotoEnd ) {\n\t\t\t\tvar index = 0,\n\t\t\t\t\t// if we are going to the end, we want to run all the tweens\n\t\t\t\t\t// otherwise we skip this part\n\t\t\t\t\tlength = gotoEnd ? animation.tweens.length : 0;\n\t\t\t\tif ( stopped ) {\n\t\t\t\t\treturn this;\n\t\t\t\t}\n\t\t\t\tstopped = true;\n\t\t\t\tfor ( ; index < length ; index++ ) {\n\t\t\t\t\tanimation.tweens[ index ].run( 1 );\n\t\t\t\t}\n\n\t\t\t\t// resolve when we played the last frame\n\t\t\t\t// otherwise, reject\n\t\t\t\tif ( gotoEnd ) {\n\t\t\t\t\tdeferred.resolveWith( elem, [ animation, gotoEnd ] );\n\t\t\t\t} else {\n\t\t\t\t\tdeferred.rejectWith( elem, [ animation, gotoEnd ] );\n\t\t\t\t}\n\t\t\t\treturn this;\n\t\t\t}\n\t\t}),\n\t\tprops = animation.props;\n\n\tpropFilter( props, animation.opts.specialEasing );\n\n\tfor ( ; index < length ; index++ ) {\n\t\tresult = animationPrefilters[ index ].call( animation, elem, props, animation.opts );\n\t\tif ( result ) {\n\t\t\treturn result;\n\t\t}\n\t}\n\n\tjQuery.map( props, createTween, animation );\n\n\tif ( jQuery.isFunction( animation.opts.start ) ) {\n\t\tanimation.opts.start.call( elem, animation );\n\t}\n\n\tjQuery.fx.timer(\n\t\tjQuery.extend( tick, {\n\t\t\telem: elem,\n\t\t\tanim: animation,\n\t\t\tqueue: animation.opts.queue\n\t\t})\n\t);\n\n\t// attach callbacks from options\n\treturn animation.progress( animation.opts.progress )\n\t\t.done( animation.opts.done, animation.opts.complete )\n\t\t.fail( animation.opts.fail )\n\t\t.always( animation.opts.always );\n}\n\njQuery.Animation = jQuery.extend( Animation, {\n\ttweener: function( props, callback ) {\n\t\tif ( jQuery.isFunction( props ) ) {\n\t\t\tcallback = props;\n\t\t\tprops = [ \"*\" ];\n\t\t} else {\n\t\t\tprops = props.split(\" \");\n\t\t}\n\n\t\tvar prop,\n\t\t\tindex = 0,\n\t\t\tlength = props.length;\n\n\t\tfor ( ; index < length ; index++ ) {\n\t\t\tprop = props[ index ];\n\t\t\ttweeners[ prop ] = tweeners[ prop ] || [];\n\t\t\ttweeners[ prop ].unshift( callback );\n\t\t}\n\t},\n\n\tprefilter: function( callback, prepend ) {\n\t\tif ( prepend ) {\n\t\t\tanimationPrefilters.unshift( callback );\n\t\t} else {\n\t\t\tanimationPrefilters.push( callback );\n\t\t}\n\t}\n});\n\njQuery.speed = function( speed, easing, fn ) {\n\tvar opt = speed && typeof speed === \"object\" ? jQuery.extend( {}, speed ) : {\n\t\tcomplete: fn || !fn && easing ||\n\t\t\tjQuery.isFunction( speed ) && speed,\n\t\tduration: speed,\n\t\teasing: fn && easing || easing && !jQuery.isFunction( easing ) && easing\n\t};\n\n\topt.duration = jQuery.fx.off ? 0 : typeof opt.duration === \"number\" ? opt.duration :\n\t\topt.duration in jQuery.fx.speeds ? jQuery.fx.speeds[ opt.duration ] : jQuery.fx.speeds._default;\n\n\t// normalize opt.queue - true/undefined/null -> \"fx\"\n\tif ( opt.queue == null || opt.queue === true ) {\n\t\topt.queue = \"fx\";\n\t}\n\n\t// Queueing\n\topt.old = opt.complete;\n\n\topt.complete = function() {\n\t\tif ( jQuery.isFunction( opt.old ) ) {\n\t\t\topt.old.call( this );\n\t\t}\n\n\t\tif ( opt.queue ) {\n\t\t\tjQuery.dequeue( this, opt.queue );\n\t\t}\n\t};\n\n\treturn opt;\n};\n\njQuery.fn.extend({\n\tfadeTo: function( speed, to, easing, callback ) {\n\n\t\t// show any hidden elements after setting opacity to 0\n\t\treturn this.filter( isHidden ).css( \"opacity\", 0 ).show()\n\n\t\t\t// animate to the value specified\n\t\t\t.end().animate({ opacity: to }, speed, easing, callback );\n\t},\n\tanimate: function( prop, speed, easing, callback ) {\n\t\tvar empty = jQuery.isEmptyObject( prop ),\n\t\t\toptall = jQuery.speed( speed, easing, callback ),\n\t\t\tdoAnimation = function() {\n\t\t\t\t// Operate on a copy of prop so per-property easing won't be lost\n\t\t\t\tvar anim = Animation( this, jQuery.extend( {}, prop ), optall );\n\n\t\t\t\t// Empty animations, or finishing resolves immediately\n\t\t\t\tif ( empty || jQuery._data( this, \"finish\" ) ) {\n\t\t\t\t\tanim.stop( true );\n\t\t\t\t}\n\t\t\t};\n\t\t\tdoAnimation.finish = doAnimation;\n\n\t\treturn empty || optall.queue === false ?\n\t\t\tthis.each( doAnimation ) :\n\t\t\tthis.queue( optall.queue, doAnimation );\n\t},\n\tstop: function( type, clearQueue, gotoEnd ) {\n\t\tvar stopQueue = function( hooks ) {\n\t\t\tvar stop = hooks.stop;\n\t\t\tdelete hooks.stop;\n\t\t\tstop( gotoEnd );\n\t\t};\n\n\t\tif ( typeof type !== \"string\" ) {\n\t\t\tgotoEnd = clearQueue;\n\t\t\tclearQueue = type;\n\t\t\ttype = undefined;\n\t\t}\n\t\tif ( clearQueue && type !== false ) {\n\t\t\tthis.queue( type || \"fx\", [] );\n\t\t}\n\n\t\treturn this.each(function() {\n\t\t\tvar dequeue = true,\n\t\t\t\tindex = type != null && type + \"queueHooks\",\n\t\t\t\ttimers = jQuery.timers,\n\t\t\t\tdata = jQuery._data( this );\n\n\t\t\tif ( index ) {\n\t\t\t\tif ( data[ index ] && data[ index ].stop ) {\n\t\t\t\t\tstopQueue( data[ index ] );\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tfor ( index in data ) {\n\t\t\t\t\tif ( data[ index ] && data[ index ].stop && rrun.test( index ) ) {\n\t\t\t\t\t\tstopQueue( data[ index ] );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tfor ( index = timers.length; index--; ) {\n\t\t\t\tif ( timers[ index ].elem === this && (type == null || timers[ index ].queue === type) ) {\n\t\t\t\t\ttimers[ index ].anim.stop( gotoEnd );\n\t\t\t\t\tdequeue = false;\n\t\t\t\t\ttimers.splice( index, 1 );\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// start the next in the queue if the last step wasn't forced\n\t\t\t// timers currently will call their complete callbacks, which will dequeue\n\t\t\t// but only if they were gotoEnd\n\t\t\tif ( dequeue || !gotoEnd ) {\n\t\t\t\tjQuery.dequeue( this, type );\n\t\t\t}\n\t\t});\n\t},\n\tfinish: function( type ) {\n\t\tif ( type !== false ) {\n\t\t\ttype = type || \"fx\";\n\t\t}\n\t\treturn this.each(function() {\n\t\t\tvar index,\n\t\t\t\tdata = jQuery._data( this ),\n\t\t\t\tqueue = data[ type + \"queue\" ],\n\t\t\t\thooks = data[ type + \"queueHooks\" ],\n\t\t\t\ttimers = jQuery.timers,\n\t\t\t\tlength = queue ? queue.length : 0;\n\n\t\t\t// enable finishing flag on private data\n\t\t\tdata.finish = true;\n\n\t\t\t// empty the queue first\n\t\t\tjQuery.queue( this, type, [] );\n\n\t\t\tif ( hooks && hooks.stop ) {\n\t\t\t\thooks.stop.call( this, true );\n\t\t\t}\n\n\t\t\t// look for any active animations, and finish them\n\t\t\tfor ( index = timers.length; index--; ) {\n\t\t\t\tif ( timers[ index ].elem === this && timers[ index ].queue === type ) {\n\t\t\t\t\ttimers[ index ].anim.stop( true );\n\t\t\t\t\ttimers.splice( index, 1 );\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// look for any animations in the old queue and finish them\n\t\t\tfor ( index = 0; index < length; index++ ) {\n\t\t\t\tif ( queue[ index ] && queue[ index ].finish ) {\n\t\t\t\t\tqueue[ index ].finish.call( this );\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// turn off finishing flag\n\t\t\tdelete data.finish;\n\t\t});\n\t}\n});\n\njQuery.each([ \"toggle\", \"show\", \"hide\" ], function( i, name ) {\n\tvar cssFn = jQuery.fn[ name ];\n\tjQuery.fn[ name ] = function( speed, easing, callback ) {\n\t\treturn speed == null || typeof speed === \"boolean\" ?\n\t\t\tcssFn.apply( this, arguments ) :\n\t\t\tthis.animate( genFx( name, true ), speed, easing, callback );\n\t};\n});\n\n// Generate shortcuts for custom animations\njQuery.each({\n\tslideDown: genFx(\"show\"),\n\tslideUp: genFx(\"hide\"),\n\tslideToggle: genFx(\"toggle\"),\n\tfadeIn: { opacity: \"show\" },\n\tfadeOut: { opacity: \"hide\" },\n\tfadeToggle: { opacity: \"toggle\" }\n}, function( name, props ) {\n\tjQuery.fn[ name ] = function( speed, easing, callback ) {\n\t\treturn this.animate( props, speed, easing, callback );\n\t};\n});\n\njQuery.timers = [];\njQuery.fx.tick = function() {\n\tvar timer,\n\t\ttimers = jQuery.timers,\n\t\ti = 0;\n\n\tfxNow = jQuery.now();\n\n\tfor ( ; i < timers.length; i++ ) {\n\t\ttimer = timers[ i ];\n\t\t// Checks the timer has not already been removed\n\t\tif ( !timer() && timers[ i ] === timer ) {\n\t\t\ttimers.splice( i--, 1 );\n\t\t}\n\t}\n\n\tif ( !timers.length ) {\n\t\tjQuery.fx.stop();\n\t}\n\tfxNow = undefined;\n};\n\njQuery.fx.timer = function( timer ) {\n\tjQuery.timers.push( timer );\n\tif ( timer() ) {\n\t\tjQuery.fx.start();\n\t} else {\n\t\tjQuery.timers.pop();\n\t}\n};\n\njQuery.fx.interval = 13;\n\njQuery.fx.start = function() {\n\tif ( !timerId ) {\n\t\ttimerId = setInterval( jQuery.fx.tick, jQuery.fx.interval );\n\t}\n};\n\njQuery.fx.stop = function() {\n\tclearInterval( timerId );\n\ttimerId = null;\n};\n\njQuery.fx.speeds = {\n\tslow: 600,\n\tfast: 200,\n\t// Default speed\n\t_default: 400\n};\n\n\n// Based off of the plugin by Clint Helfers, with permission.\n// http://blindsignals.com/index.php/2009/07/jquery-delay/\njQuery.fn.delay = function( time, type ) {\n\ttime = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time;\n\ttype = type || \"fx\";\n\n\treturn this.queue( type, function( next, hooks ) {\n\t\tvar timeout = setTimeout( next, time );\n\t\thooks.stop = function() {\n\t\t\tclearTimeout( timeout );\n\t\t};\n\t});\n};\n\n\n(function() {\n\t// Minified: var a,b,c,d,e\n\tvar input, div, select, a, opt;\n\n\t// Setup\n\tdiv = document.createElement( \"div\" );\n\tdiv.setAttribute( \"className\", \"t\" );\n\tdiv.innerHTML = \"  <link/><table></table><a href='/a'>a</a><input type='checkbox'/>\";\n\ta = div.getElementsByTagName(\"a\")[ 0 ];\n\n\t// First batch of tests.\n\tselect = document.createElement(\"select\");\n\topt = select.appendChild( document.createElement(\"option\") );\n\tinput = div.getElementsByTagName(\"input\")[ 0 ];\n\n\ta.style.cssText = \"top:1px\";\n\n\t// Test setAttribute on camelCase class. If it works, we need attrFixes when doing get/setAttribute (ie6/7)\n\tsupport.getSetAttribute = div.className !== \"t\";\n\n\t// Get the style information from getAttribute\n\t// (IE uses .cssText instead)\n\tsupport.style = /top/.test( a.getAttribute(\"style\") );\n\n\t// Make sure that URLs aren't manipulated\n\t// (IE normalizes it by default)\n\tsupport.hrefNormalized = a.getAttribute(\"href\") === \"/a\";\n\n\t// Check the default checkbox/radio value (\"\" on WebKit; \"on\" elsewhere)\n\tsupport.checkOn = !!input.value;\n\n\t// Make sure that a selected-by-default option has a working selected property.\n\t// (WebKit defaults to false instead of true, IE too, if it's in an optgroup)\n\tsupport.optSelected = opt.selected;\n\n\t// Tests for enctype support on a form (#6743)\n\tsupport.enctype = !!document.createElement(\"form\").enctype;\n\n\t// Make sure that the options inside disabled selects aren't marked as disabled\n\t// (WebKit marks them as disabled)\n\tselect.disabled = true;\n\tsupport.optDisabled = !opt.disabled;\n\n\t// Support: IE8 only\n\t// Check if we can trust getAttribute(\"value\")\n\tinput = document.createElement( \"input\" );\n\tinput.setAttribute( \"value\", \"\" );\n\tsupport.input = input.getAttribute( \"value\" ) === \"\";\n\n\t// Check if an input maintains its value after becoming a radio\n\tinput.value = \"t\";\n\tinput.setAttribute( \"type\", \"radio\" );\n\tsupport.radioValue = input.value === \"t\";\n})();\n\n\nvar rreturn = /\\r/g;\n\njQuery.fn.extend({\n\tval: function( value ) {\n\t\tvar hooks, ret, isFunction,\n\t\t\telem = this[0];\n\n\t\tif ( !arguments.length ) {\n\t\t\tif ( elem ) {\n\t\t\t\thooks = jQuery.valHooks[ elem.type ] || jQuery.valHooks[ elem.nodeName.toLowerCase() ];\n\n\t\t\t\tif ( hooks && \"get\" in hooks && (ret = hooks.get( elem, \"value\" )) !== undefined ) {\n\t\t\t\t\treturn ret;\n\t\t\t\t}\n\n\t\t\t\tret = elem.value;\n\n\t\t\t\treturn typeof ret === \"string\" ?\n\t\t\t\t\t// handle most common string cases\n\t\t\t\t\tret.replace(rreturn, \"\") :\n\t\t\t\t\t// handle cases where value is null/undef or number\n\t\t\t\t\tret == null ? \"\" : ret;\n\t\t\t}\n\n\t\t\treturn;\n\t\t}\n\n\t\tisFunction = jQuery.isFunction( value );\n\n\t\treturn this.each(function( i ) {\n\t\t\tvar val;\n\n\t\t\tif ( this.nodeType !== 1 ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif ( isFunction ) {\n\t\t\t\tval = value.call( this, i, jQuery( this ).val() );\n\t\t\t} else {\n\t\t\t\tval = value;\n\t\t\t}\n\n\t\t\t// Treat null/undefined as \"\"; convert numbers to string\n\t\t\tif ( val == null ) {\n\t\t\t\tval = \"\";\n\t\t\t} else if ( typeof val === \"number\" ) {\n\t\t\t\tval += \"\";\n\t\t\t} else if ( jQuery.isArray( val ) ) {\n\t\t\t\tval = jQuery.map( val, function( value ) {\n\t\t\t\t\treturn value == null ? \"\" : value + \"\";\n\t\t\t\t});\n\t\t\t}\n\n\t\t\thooks = jQuery.valHooks[ this.type ] || jQuery.valHooks[ this.nodeName.toLowerCase() ];\n\n\t\t\t// If set returns undefined, fall back to normal setting\n\t\t\tif ( !hooks || !(\"set\" in hooks) || hooks.set( this, val, \"value\" ) === undefined ) {\n\t\t\t\tthis.value = val;\n\t\t\t}\n\t\t});\n\t}\n});\n\njQuery.extend({\n\tvalHooks: {\n\t\toption: {\n\t\t\tget: function( elem ) {\n\t\t\t\tvar val = jQuery.find.attr( elem, \"value\" );\n\t\t\t\treturn val != null ?\n\t\t\t\t\tval :\n\t\t\t\t\t// Support: IE10-11+\n\t\t\t\t\t// option.text throws exceptions (#14686, #14858)\n\t\t\t\t\tjQuery.trim( jQuery.text( elem ) );\n\t\t\t}\n\t\t},\n\t\tselect: {\n\t\t\tget: function( elem ) {\n\t\t\t\tvar value, option,\n\t\t\t\t\toptions = elem.options,\n\t\t\t\t\tindex = elem.selectedIndex,\n\t\t\t\t\tone = elem.type === \"select-one\" || index < 0,\n\t\t\t\t\tvalues = one ? null : [],\n\t\t\t\t\tmax = one ? index + 1 : options.length,\n\t\t\t\t\ti = index < 0 ?\n\t\t\t\t\t\tmax :\n\t\t\t\t\t\tone ? index : 0;\n\n\t\t\t\t// Loop through all the selected options\n\t\t\t\tfor ( ; i < max; i++ ) {\n\t\t\t\t\toption = options[ i ];\n\n\t\t\t\t\t// oldIE doesn't update selected after form reset (#2551)\n\t\t\t\t\tif ( ( option.selected || i === index ) &&\n\t\t\t\t\t\t\t// Don't return options that are disabled or in a disabled optgroup\n\t\t\t\t\t\t\t( support.optDisabled ? !option.disabled : option.getAttribute(\"disabled\") === null ) &&\n\t\t\t\t\t\t\t( !option.parentNode.disabled || !jQuery.nodeName( option.parentNode, \"optgroup\" ) ) ) {\n\n\t\t\t\t\t\t// Get the specific value for the option\n\t\t\t\t\t\tvalue = jQuery( option ).val();\n\n\t\t\t\t\t\t// We don't need an array for one selects\n\t\t\t\t\t\tif ( one ) {\n\t\t\t\t\t\t\treturn value;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Multi-Selects return an array\n\t\t\t\t\t\tvalues.push( value );\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\treturn values;\n\t\t\t},\n\n\t\t\tset: function( elem, value ) {\n\t\t\t\tvar optionSet, option,\n\t\t\t\t\toptions = elem.options,\n\t\t\t\t\tvalues = jQuery.makeArray( value ),\n\t\t\t\t\ti = options.length;\n\n\t\t\t\twhile ( i-- ) {\n\t\t\t\t\toption = options[ i ];\n\n\t\t\t\t\tif ( jQuery.inArray( jQuery.valHooks.option.get( option ), values ) >= 0 ) {\n\n\t\t\t\t\t\t// Support: IE6\n\t\t\t\t\t\t// When new option element is added to select box we need to\n\t\t\t\t\t\t// force reflow of newly added node in order to workaround delay\n\t\t\t\t\t\t// of initialization properties\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\toption.selected = optionSet = true;\n\n\t\t\t\t\t\t} catch ( _ ) {\n\n\t\t\t\t\t\t\t// Will be executed only in IE6\n\t\t\t\t\t\t\toption.scrollHeight;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t} else {\n\t\t\t\t\t\toption.selected = false;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Force browsers to behave consistently when non-matching value is set\n\t\t\t\tif ( !optionSet ) {\n\t\t\t\t\telem.selectedIndex = -1;\n\t\t\t\t}\n\n\t\t\t\treturn options;\n\t\t\t}\n\t\t}\n\t}\n});\n\n// Radios and checkboxes getter/setter\njQuery.each([ \"radio\", \"checkbox\" ], function() {\n\tjQuery.valHooks[ this ] = {\n\t\tset: function( elem, value ) {\n\t\t\tif ( jQuery.isArray( value ) ) {\n\t\t\t\treturn ( elem.checked = jQuery.inArray( jQuery(elem).val(), value ) >= 0 );\n\t\t\t}\n\t\t}\n\t};\n\tif ( !support.checkOn ) {\n\t\tjQuery.valHooks[ this ].get = function( elem ) {\n\t\t\t// Support: Webkit\n\t\t\t// \"\" is returned instead of \"on\" if a value isn't specified\n\t\t\treturn elem.getAttribute(\"value\") === null ? \"on\" : elem.value;\n\t\t};\n\t}\n});\n\n\n\n\nvar nodeHook, boolHook,\n\tattrHandle = jQuery.expr.attrHandle,\n\truseDefault = /^(?:checked|selected)$/i,\n\tgetSetAttribute = support.getSetAttribute,\n\tgetSetInput = support.input;\n\njQuery.fn.extend({\n\tattr: function( name, value ) {\n\t\treturn access( this, jQuery.attr, name, value, arguments.length > 1 );\n\t},\n\n\tremoveAttr: function( name ) {\n\t\treturn this.each(function() {\n\t\t\tjQuery.removeAttr( this, name );\n\t\t});\n\t}\n});\n\njQuery.extend({\n\tattr: function( elem, name, value ) {\n\t\tvar hooks, ret,\n\t\t\tnType = elem.nodeType;\n\n\t\t// don't get/set attributes on text, comment and attribute nodes\n\t\tif ( !elem || nType === 3 || nType === 8 || nType === 2 ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Fallback to prop when attributes are not supported\n\t\tif ( typeof elem.getAttribute === strundefined ) {\n\t\t\treturn jQuery.prop( elem, name, value );\n\t\t}\n\n\t\t// All attributes are lowercase\n\t\t// Grab necessary hook if one is defined\n\t\tif ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) {\n\t\t\tname = name.toLowerCase();\n\t\t\thooks = jQuery.attrHooks[ name ] ||\n\t\t\t\t( jQuery.expr.match.bool.test( name ) ? boolHook : nodeHook );\n\t\t}\n\n\t\tif ( value !== undefined ) {\n\n\t\t\tif ( value === null ) {\n\t\t\t\tjQuery.removeAttr( elem, name );\n\n\t\t\t} else if ( hooks && \"set\" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ) {\n\t\t\t\treturn ret;\n\n\t\t\t} else {\n\t\t\t\telem.setAttribute( name, value + \"\" );\n\t\t\t\treturn value;\n\t\t\t}\n\n\t\t} else if ( hooks && \"get\" in hooks && (ret = hooks.get( elem, name )) !== null ) {\n\t\t\treturn ret;\n\n\t\t} else {\n\t\t\tret = jQuery.find.attr( elem, name );\n\n\t\t\t// Non-existent attributes return null, we normalize to undefined\n\t\t\treturn ret == null ?\n\t\t\t\tundefined :\n\t\t\t\tret;\n\t\t}\n\t},\n\n\tremoveAttr: function( elem, value ) {\n\t\tvar name, propName,\n\t\t\ti = 0,\n\t\t\tattrNames = value && value.match( rnotwhite );\n\n\t\tif ( attrNames && elem.nodeType === 1 ) {\n\t\t\twhile ( (name = attrNames[i++]) ) {\n\t\t\t\tpropName = jQuery.propFix[ name ] || name;\n\n\t\t\t\t// Boolean attributes get special treatment (#10870)\n\t\t\t\tif ( jQuery.expr.match.bool.test( name ) ) {\n\t\t\t\t\t// Set corresponding property to false\n\t\t\t\t\tif ( getSetInput && getSetAttribute || !ruseDefault.test( name ) ) {\n\t\t\t\t\t\telem[ propName ] = false;\n\t\t\t\t\t// Support: IE<9\n\t\t\t\t\t// Also clear defaultChecked/defaultSelected (if appropriate)\n\t\t\t\t\t} else {\n\t\t\t\t\t\telem[ jQuery.camelCase( \"default-\" + name ) ] =\n\t\t\t\t\t\t\telem[ propName ] = false;\n\t\t\t\t\t}\n\n\t\t\t\t// See #9699 for explanation of this approach (setting first, then removal)\n\t\t\t\t} else {\n\t\t\t\t\tjQuery.attr( elem, name, \"\" );\n\t\t\t\t}\n\n\t\t\t\telem.removeAttribute( getSetAttribute ? name : propName );\n\t\t\t}\n\t\t}\n\t},\n\n\tattrHooks: {\n\t\ttype: {\n\t\t\tset: function( elem, value ) {\n\t\t\t\tif ( !support.radioValue && value === \"radio\" && jQuery.nodeName(elem, \"input\") ) {\n\t\t\t\t\t// Setting the type on a radio button after the value resets the value in IE6-9\n\t\t\t\t\t// Reset value to default in case type is set after value during creation\n\t\t\t\t\tvar val = elem.value;\n\t\t\t\t\telem.setAttribute( \"type\", value );\n\t\t\t\t\tif ( val ) {\n\t\t\t\t\t\telem.value = val;\n\t\t\t\t\t}\n\t\t\t\t\treturn value;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n});\n\n// Hook for boolean attributes\nboolHook = {\n\tset: function( elem, value, name ) {\n\t\tif ( value === false ) {\n\t\t\t// Remove boolean attributes when set to false\n\t\t\tjQuery.removeAttr( elem, name );\n\t\t} else if ( getSetInput && getSetAttribute || !ruseDefault.test( name ) ) {\n\t\t\t// IE<8 needs the *property* name\n\t\t\telem.setAttribute( !getSetAttribute && jQuery.propFix[ name ] || name, name );\n\n\t\t// Use defaultChecked and defaultSelected for oldIE\n\t\t} else {\n\t\t\telem[ jQuery.camelCase( \"default-\" + name ) ] = elem[ name ] = true;\n\t\t}\n\n\t\treturn name;\n\t}\n};\n\n// Retrieve booleans specially\njQuery.each( jQuery.expr.match.bool.source.match( /\\w+/g ), function( i, name ) {\n\n\tvar getter = attrHandle[ name ] || jQuery.find.attr;\n\n\tattrHandle[ name ] = getSetInput && getSetAttribute || !ruseDefault.test( name ) ?\n\t\tfunction( elem, name, isXML ) {\n\t\t\tvar ret, handle;\n\t\t\tif ( !isXML ) {\n\t\t\t\t// Avoid an infinite loop by temporarily removing this function from the getter\n\t\t\t\thandle = attrHandle[ name ];\n\t\t\t\tattrHandle[ name ] = ret;\n\t\t\t\tret = getter( elem, name, isXML ) != null ?\n\t\t\t\t\tname.toLowerCase() :\n\t\t\t\t\tnull;\n\t\t\t\tattrHandle[ name ] = handle;\n\t\t\t}\n\t\t\treturn ret;\n\t\t} :\n\t\tfunction( elem, name, isXML ) {\n\t\t\tif ( !isXML ) {\n\t\t\t\treturn elem[ jQuery.camelCase( \"default-\" + name ) ] ?\n\t\t\t\t\tname.toLowerCase() :\n\t\t\t\t\tnull;\n\t\t\t}\n\t\t};\n});\n\n// fix oldIE attroperties\nif ( !getSetInput || !getSetAttribute ) {\n\tjQuery.attrHooks.value = {\n\t\tset: function( elem, value, name ) {\n\t\t\tif ( jQuery.nodeName( elem, \"input\" ) ) {\n\t\t\t\t// Does not return so that setAttribute is also used\n\t\t\t\telem.defaultValue = value;\n\t\t\t} else {\n\t\t\t\t// Use nodeHook if defined (#1954); otherwise setAttribute is fine\n\t\t\t\treturn nodeHook && nodeHook.set( elem, value, name );\n\t\t\t}\n\t\t}\n\t};\n}\n\n// IE6/7 do not support getting/setting some attributes with get/setAttribute\nif ( !getSetAttribute ) {\n\n\t// Use this for any attribute in IE6/7\n\t// This fixes almost every IE6/7 issue\n\tnodeHook = {\n\t\tset: function( elem, value, name ) {\n\t\t\t// Set the existing or create a new attribute node\n\t\t\tvar ret = elem.getAttributeNode( name );\n\t\t\tif ( !ret ) {\n\t\t\t\telem.setAttributeNode(\n\t\t\t\t\t(ret = elem.ownerDocument.createAttribute( name ))\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tret.value = value += \"\";\n\n\t\t\t// Break association with cloned elements by also using setAttribute (#9646)\n\t\t\tif ( name === \"value\" || value === elem.getAttribute( name ) ) {\n\t\t\t\treturn value;\n\t\t\t}\n\t\t}\n\t};\n\n\t// Some attributes are constructed with empty-string values when not defined\n\tattrHandle.id = attrHandle.name = attrHandle.coords =\n\t\tfunction( elem, name, isXML ) {\n\t\t\tvar ret;\n\t\t\tif ( !isXML ) {\n\t\t\t\treturn (ret = elem.getAttributeNode( name )) && ret.value !== \"\" ?\n\t\t\t\t\tret.value :\n\t\t\t\t\tnull;\n\t\t\t}\n\t\t};\n\n\t// Fixing value retrieval on a button requires this module\n\tjQuery.valHooks.button = {\n\t\tget: function( elem, name ) {\n\t\t\tvar ret = elem.getAttributeNode( name );\n\t\t\tif ( ret && ret.specified ) {\n\t\t\t\treturn ret.value;\n\t\t\t}\n\t\t},\n\t\tset: nodeHook.set\n\t};\n\n\t// Set contenteditable to false on removals(#10429)\n\t// Setting to empty string throws an error as an invalid value\n\tjQuery.attrHooks.contenteditable = {\n\t\tset: function( elem, value, name ) {\n\t\t\tnodeHook.set( elem, value === \"\" ? false : value, name );\n\t\t}\n\t};\n\n\t// Set width and height to auto instead of 0 on empty string( Bug #8150 )\n\t// This is for removals\n\tjQuery.each([ \"width\", \"height\" ], function( i, name ) {\n\t\tjQuery.attrHooks[ name ] = {\n\t\t\tset: function( elem, value ) {\n\t\t\t\tif ( value === \"\" ) {\n\t\t\t\t\telem.setAttribute( name, \"auto\" );\n\t\t\t\t\treturn value;\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t});\n}\n\nif ( !support.style ) {\n\tjQuery.attrHooks.style = {\n\t\tget: function( elem ) {\n\t\t\t// Return undefined in the case of empty string\n\t\t\t// Note: IE uppercases css property names, but if we were to .toLowerCase()\n\t\t\t// .cssText, that would destroy case senstitivity in URL's, like in \"background\"\n\t\t\treturn elem.style.cssText || undefined;\n\t\t},\n\t\tset: function( elem, value ) {\n\t\t\treturn ( elem.style.cssText = value + \"\" );\n\t\t}\n\t};\n}\n\n\n\n\nvar rfocusable = /^(?:input|select|textarea|button|object)$/i,\n\trclickable = /^(?:a|area)$/i;\n\njQuery.fn.extend({\n\tprop: function( name, value ) {\n\t\treturn access( this, jQuery.prop, name, value, arguments.length > 1 );\n\t},\n\n\tremoveProp: function( name ) {\n\t\tname = jQuery.propFix[ name ] || name;\n\t\treturn this.each(function() {\n\t\t\t// try/catch handles cases where IE balks (such as removing a property on window)\n\t\t\ttry {\n\t\t\t\tthis[ name ] = undefined;\n\t\t\t\tdelete this[ name ];\n\t\t\t} catch( e ) {}\n\t\t});\n\t}\n});\n\njQuery.extend({\n\tpropFix: {\n\t\t\"for\": \"htmlFor\",\n\t\t\"class\": \"className\"\n\t},\n\n\tprop: function( elem, name, value ) {\n\t\tvar ret, hooks, notxml,\n\t\t\tnType = elem.nodeType;\n\n\t\t// don't get/set properties on text, comment and attribute nodes\n\t\tif ( !elem || nType === 3 || nType === 8 || nType === 2 ) {\n\t\t\treturn;\n\t\t}\n\n\t\tnotxml = nType !== 1 || !jQuery.isXMLDoc( elem );\n\n\t\tif ( notxml ) {\n\t\t\t// Fix name and attach hooks\n\t\t\tname = jQuery.propFix[ name ] || name;\n\t\t\thooks = jQuery.propHooks[ name ];\n\t\t}\n\n\t\tif ( value !== undefined ) {\n\t\t\treturn hooks && \"set\" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ?\n\t\t\t\tret :\n\t\t\t\t( elem[ name ] = value );\n\n\t\t} else {\n\t\t\treturn hooks && \"get\" in hooks && (ret = hooks.get( elem, name )) !== null ?\n\t\t\t\tret :\n\t\t\t\telem[ name ];\n\t\t}\n\t},\n\n\tpropHooks: {\n\t\ttabIndex: {\n\t\t\tget: function( elem ) {\n\t\t\t\t// elem.tabIndex doesn't always return the correct value when it hasn't been explicitly set\n\t\t\t\t// http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/\n\t\t\t\t// Use proper attribute retrieval(#12072)\n\t\t\t\tvar tabindex = jQuery.find.attr( elem, \"tabindex\" );\n\n\t\t\t\treturn tabindex ?\n\t\t\t\t\tparseInt( tabindex, 10 ) :\n\t\t\t\t\trfocusable.test( elem.nodeName ) || rclickable.test( elem.nodeName ) && elem.href ?\n\t\t\t\t\t\t0 :\n\t\t\t\t\t\t-1;\n\t\t\t}\n\t\t}\n\t}\n});\n\n// Some attributes require a special call on IE\n// http://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx\nif ( !support.hrefNormalized ) {\n\t// href/src property should get the full normalized URL (#10299/#12915)\n\tjQuery.each([ \"href\", \"src\" ], function( i, name ) {\n\t\tjQuery.propHooks[ name ] = {\n\t\t\tget: function( elem ) {\n\t\t\t\treturn elem.getAttribute( name, 4 );\n\t\t\t}\n\t\t};\n\t});\n}\n\n// Support: Safari, IE9+\n// mis-reports the default selected property of an option\n// Accessing the parent's selectedIndex property fixes it\nif ( !support.optSelected ) {\n\tjQuery.propHooks.selected = {\n\t\tget: function( elem ) {\n\t\t\tvar parent = elem.parentNode;\n\n\t\t\tif ( parent ) {\n\t\t\t\tparent.selectedIndex;\n\n\t\t\t\t// Make sure that it also works with optgroups, see #5701\n\t\t\t\tif ( parent.parentNode ) {\n\t\t\t\t\tparent.parentNode.selectedIndex;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn null;\n\t\t}\n\t};\n}\n\njQuery.each([\n\t\"tabIndex\",\n\t\"readOnly\",\n\t\"maxLength\",\n\t\"cellSpacing\",\n\t\"cellPadding\",\n\t\"rowSpan\",\n\t\"colSpan\",\n\t\"useMap\",\n\t\"frameBorder\",\n\t\"contentEditable\"\n], function() {\n\tjQuery.propFix[ this.toLowerCase() ] = this;\n});\n\n// IE6/7 call enctype encoding\nif ( !support.enctype ) {\n\tjQuery.propFix.enctype = \"encoding\";\n}\n\n\n\n\nvar rclass = /[\\t\\r\\n\\f]/g;\n\njQuery.fn.extend({\n\taddClass: function( value ) {\n\t\tvar classes, elem, cur, clazz, j, finalValue,\n\t\t\ti = 0,\n\t\t\tlen = this.length,\n\t\t\tproceed = typeof value === \"string\" && value;\n\n\t\tif ( jQuery.isFunction( value ) ) {\n\t\t\treturn this.each(function( j ) {\n\t\t\t\tjQuery( this ).addClass( value.call( this, j, this.className ) );\n\t\t\t});\n\t\t}\n\n\t\tif ( proceed ) {\n\t\t\t// The disjunction here is for better compressibility (see removeClass)\n\t\t\tclasses = ( value || \"\" ).match( rnotwhite ) || [];\n\n\t\t\tfor ( ; i < len; i++ ) {\n\t\t\t\telem = this[ i ];\n\t\t\t\tcur = elem.nodeType === 1 && ( elem.className ?\n\t\t\t\t\t( \" \" + elem.className + \" \" ).replace( rclass, \" \" ) :\n\t\t\t\t\t\" \"\n\t\t\t\t);\n\n\t\t\t\tif ( cur ) {\n\t\t\t\t\tj = 0;\n\t\t\t\t\twhile ( (clazz = classes[j++]) ) {\n\t\t\t\t\t\tif ( cur.indexOf( \" \" + clazz + \" \" ) < 0 ) {\n\t\t\t\t\t\t\tcur += clazz + \" \";\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// only assign if different to avoid unneeded rendering.\n\t\t\t\t\tfinalValue = jQuery.trim( cur );\n\t\t\t\t\tif ( elem.className !== finalValue ) {\n\t\t\t\t\t\telem.className = finalValue;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn this;\n\t},\n\n\tremoveClass: function( value ) {\n\t\tvar classes, elem, cur, clazz, j, finalValue,\n\t\t\ti = 0,\n\t\t\tlen = this.length,\n\t\t\tproceed = arguments.length === 0 || typeof value === \"string\" && value;\n\n\t\tif ( jQuery.isFunction( value ) ) {\n\t\t\treturn this.each(function( j ) {\n\t\t\t\tjQuery( this ).removeClass( value.call( this, j, this.className ) );\n\t\t\t});\n\t\t}\n\t\tif ( proceed ) {\n\t\t\tclasses = ( value || \"\" ).match( rnotwhite ) || [];\n\n\t\t\tfor ( ; i < len; i++ ) {\n\t\t\t\telem = this[ i ];\n\t\t\t\t// This expression is here for better compressibility (see addClass)\n\t\t\t\tcur = elem.nodeType === 1 && ( elem.className ?\n\t\t\t\t\t( \" \" + elem.className + \" \" ).replace( rclass, \" \" ) :\n\t\t\t\t\t\"\"\n\t\t\t\t);\n\n\t\t\t\tif ( cur ) {\n\t\t\t\t\tj = 0;\n\t\t\t\t\twhile ( (clazz = classes[j++]) ) {\n\t\t\t\t\t\t// Remove *all* instances\n\t\t\t\t\t\twhile ( cur.indexOf( \" \" + clazz + \" \" ) >= 0 ) {\n\t\t\t\t\t\t\tcur = cur.replace( \" \" + clazz + \" \", \" \" );\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// only assign if different to avoid unneeded rendering.\n\t\t\t\t\tfinalValue = value ? jQuery.trim( cur ) : \"\";\n\t\t\t\t\tif ( elem.className !== finalValue ) {\n\t\t\t\t\t\telem.className = finalValue;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn this;\n\t},\n\n\ttoggleClass: function( value, stateVal ) {\n\t\tvar type = typeof value;\n\n\t\tif ( typeof stateVal === \"boolean\" && type === \"string\" ) {\n\t\t\treturn stateVal ? this.addClass( value ) : this.removeClass( value );\n\t\t}\n\n\t\tif ( jQuery.isFunction( value ) ) {\n\t\t\treturn this.each(function( i ) {\n\t\t\t\tjQuery( this ).toggleClass( value.call(this, i, this.className, stateVal), stateVal );\n\t\t\t});\n\t\t}\n\n\t\treturn this.each(function() {\n\t\t\tif ( type === \"string\" ) {\n\t\t\t\t// toggle individual class names\n\t\t\t\tvar className,\n\t\t\t\t\ti = 0,\n\t\t\t\t\tself = jQuery( this ),\n\t\t\t\t\tclassNames = value.match( rnotwhite ) || [];\n\n\t\t\t\twhile ( (className = classNames[ i++ ]) ) {\n\t\t\t\t\t// check each className given, space separated list\n\t\t\t\t\tif ( self.hasClass( className ) ) {\n\t\t\t\t\t\tself.removeClass( className );\n\t\t\t\t\t} else {\n\t\t\t\t\t\tself.addClass( className );\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t// Toggle whole class name\n\t\t\t} else if ( type === strundefined || type === \"boolean\" ) {\n\t\t\t\tif ( this.className ) {\n\t\t\t\t\t// store className if set\n\t\t\t\t\tjQuery._data( this, \"__className__\", this.className );\n\t\t\t\t}\n\n\t\t\t\t// If the element has a class name or if we're passed \"false\",\n\t\t\t\t// then remove the whole classname (if there was one, the above saved it).\n\t\t\t\t// Otherwise bring back whatever was previously saved (if anything),\n\t\t\t\t// falling back to the empty string if nothing was stored.\n\t\t\t\tthis.className = this.className || value === false ? \"\" : jQuery._data( this, \"__className__\" ) || \"\";\n\t\t\t}\n\t\t});\n\t},\n\n\thasClass: function( selector ) {\n\t\tvar className = \" \" + selector + \" \",\n\t\t\ti = 0,\n\t\t\tl = this.length;\n\t\tfor ( ; i < l; i++ ) {\n\t\t\tif ( this[i].nodeType === 1 && (\" \" + this[i].className + \" \").replace(rclass, \" \").indexOf( className ) >= 0 ) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\t}\n});\n\n\n\n\n// Return jQuery for attributes-only inclusion\n\n\njQuery.each( (\"blur focus focusin focusout load resize scroll unload click dblclick \" +\n\t\"mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave \" +\n\t\"change select submit keydown keypress keyup error contextmenu\").split(\" \"), function( i, name ) {\n\n\t// Handle event binding\n\tjQuery.fn[ name ] = function( data, fn ) {\n\t\treturn arguments.length > 0 ?\n\t\t\tthis.on( name, null, data, fn ) :\n\t\t\tthis.trigger( name );\n\t};\n});\n\njQuery.fn.extend({\n\thover: function( fnOver, fnOut ) {\n\t\treturn this.mouseenter( fnOver ).mouseleave( fnOut || fnOver );\n\t},\n\n\tbind: function( types, data, fn ) {\n\t\treturn this.on( types, null, data, fn );\n\t},\n\tunbind: function( types, fn ) {\n\t\treturn this.off( types, null, fn );\n\t},\n\n\tdelegate: function( selector, types, data, fn ) {\n\t\treturn this.on( types, selector, data, fn );\n\t},\n\tundelegate: function( selector, types, fn ) {\n\t\t// ( namespace ) or ( selector, types [, fn] )\n\t\treturn arguments.length === 1 ? this.off( selector, \"**\" ) : this.off( types, selector || \"**\", fn );\n\t}\n});\n\n\nvar nonce = jQuery.now();\n\nvar rquery = (/\\?/);\n\n\n\nvar rvalidtokens = /(,)|(\\[|{)|(}|])|\"(?:[^\"\\\\\\r\\n]|\\\\[\"\\\\\\/bfnrt]|\\\\u[\\da-fA-F]{4})*\"\\s*:?|true|false|null|-?(?!0\\d)\\d+(?:\\.\\d+|)(?:[eE][+-]?\\d+|)/g;\n\njQuery.parseJSON = function( data ) {\n\t// Attempt to parse using the native JSON parser first\n\tif ( window.JSON && window.JSON.parse ) {\n\t\t// Support: Android 2.3\n\t\t// Workaround failure to string-cast null input\n\t\treturn window.JSON.parse( data + \"\" );\n\t}\n\n\tvar requireNonComma,\n\t\tdepth = null,\n\t\tstr = jQuery.trim( data + \"\" );\n\n\t// Guard against invalid (and possibly dangerous) input by ensuring that nothing remains\n\t// after removing valid tokens\n\treturn str && !jQuery.trim( str.replace( rvalidtokens, function( token, comma, open, close ) {\n\n\t\t// Force termination if we see a misplaced comma\n\t\tif ( requireNonComma && comma ) {\n\t\t\tdepth = 0;\n\t\t}\n\n\t\t// Perform no more replacements after returning to outermost depth\n\t\tif ( depth === 0 ) {\n\t\t\treturn token;\n\t\t}\n\n\t\t// Commas must not follow \"[\", \"{\", or \",\"\n\t\trequireNonComma = open || comma;\n\n\t\t// Determine new depth\n\t\t// array/object open (\"[\" or \"{\"): depth += true - false (increment)\n\t\t// array/object close (\"]\" or \"}\"): depth += false - true (decrement)\n\t\t// other cases (\",\" or primitive): depth += true - true (numeric cast)\n\t\tdepth += !close - !open;\n\n\t\t// Remove this token\n\t\treturn \"\";\n\t}) ) ?\n\t\t( Function( \"return \" + str ) )() :\n\t\tjQuery.error( \"Invalid JSON: \" + data );\n};\n\n\n// Cross-browser xml parsing\njQuery.parseXML = function( data ) {\n\tvar xml, tmp;\n\tif ( !data || typeof data !== \"string\" ) {\n\t\treturn null;\n\t}\n\ttry {\n\t\tif ( window.DOMParser ) { // Standard\n\t\t\ttmp = new DOMParser();\n\t\t\txml = tmp.parseFromString( data, \"text/xml\" );\n\t\t} else { // IE\n\t\t\txml = new ActiveXObject( \"Microsoft.XMLDOM\" );\n\t\t\txml.async = \"false\";\n\t\t\txml.loadXML( data );\n\t\t}\n\t} catch( e ) {\n\t\txml = undefined;\n\t}\n\tif ( !xml || !xml.documentElement || xml.getElementsByTagName( \"parsererror\" ).length ) {\n\t\tjQuery.error( \"Invalid XML: \" + data );\n\t}\n\treturn xml;\n};\n\n\nvar\n\t// Document location\n\tajaxLocParts,\n\tajaxLocation,\n\n\trhash = /#.*$/,\n\trts = /([?&])_=[^&]*/,\n\trheaders = /^(.*?):[ \\t]*([^\\r\\n]*)\\r?$/mg, // IE leaves an \\r character at EOL\n\t// #7653, #8125, #8152: local protocol detection\n\trlocalProtocol = /^(?:about|app|app-storage|.+-extension|file|res|widget):$/,\n\trnoContent = /^(?:GET|HEAD)$/,\n\trprotocol = /^\\/\\//,\n\trurl = /^([\\w.+-]+:)(?:\\/\\/(?:[^\\/?#]*@|)([^\\/?#:]*)(?::(\\d+)|)|)/,\n\n\t/* Prefilters\n\t * 1) They are useful to introduce custom dataTypes (see ajax/jsonp.js for an example)\n\t * 2) These are called:\n\t *    - BEFORE asking for a transport\n\t *    - AFTER param serialization (s.data is a string if s.processData is true)\n\t * 3) key is the dataType\n\t * 4) the catchall symbol \"*\" can be used\n\t * 5) execution will start with transport dataType and THEN continue down to \"*\" if needed\n\t */\n\tprefilters = {},\n\n\t/* Transports bindings\n\t * 1) key is the dataType\n\t * 2) the catchall symbol \"*\" can be used\n\t * 3) selection will start with transport dataType and THEN go to \"*\" if needed\n\t */\n\ttransports = {},\n\n\t// Avoid comment-prolog char sequence (#10098); must appease lint and evade compression\n\tallTypes = \"*/\".concat(\"*\");\n\n// #8138, IE may throw an exception when accessing\n// a field from window.location if document.domain has been set\ntry {\n\tajaxLocation = location.href;\n} catch( e ) {\n\t// Use the href attribute of an A element\n\t// since IE will modify it given document.location\n\tajaxLocation = document.createElement( \"a\" );\n\tajaxLocation.href = \"\";\n\tajaxLocation = ajaxLocation.href;\n}\n\n// Segment location into parts\najaxLocParts = rurl.exec( ajaxLocation.toLowerCase() ) || [];\n\n// Base \"constructor\" for jQuery.ajaxPrefilter and jQuery.ajaxTransport\nfunction addToPrefiltersOrTransports( structure ) {\n\n\t// dataTypeExpression is optional and defaults to \"*\"\n\treturn function( dataTypeExpression, func ) {\n\n\t\tif ( typeof dataTypeExpression !== \"string\" ) {\n\t\t\tfunc = dataTypeExpression;\n\t\t\tdataTypeExpression = \"*\";\n\t\t}\n\n\t\tvar dataType,\n\t\t\ti = 0,\n\t\t\tdataTypes = dataTypeExpression.toLowerCase().match( rnotwhite ) || [];\n\n\t\tif ( jQuery.isFunction( func ) ) {\n\t\t\t// For each dataType in the dataTypeExpression\n\t\t\twhile ( (dataType = dataTypes[i++]) ) {\n\t\t\t\t// Prepend if requested\n\t\t\t\tif ( dataType.charAt( 0 ) === \"+\" ) {\n\t\t\t\t\tdataType = dataType.slice( 1 ) || \"*\";\n\t\t\t\t\t(structure[ dataType ] = structure[ dataType ] || []).unshift( func );\n\n\t\t\t\t// Otherwise append\n\t\t\t\t} else {\n\t\t\t\t\t(structure[ dataType ] = structure[ dataType ] || []).push( func );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t};\n}\n\n// Base inspection function for prefilters and transports\nfunction inspectPrefiltersOrTransports( structure, options, originalOptions, jqXHR ) {\n\n\tvar inspected = {},\n\t\tseekingTransport = ( structure === transports );\n\n\tfunction inspect( dataType ) {\n\t\tvar selected;\n\t\tinspected[ dataType ] = true;\n\t\tjQuery.each( structure[ dataType ] || [], function( _, prefilterOrFactory ) {\n\t\t\tvar dataTypeOrTransport = prefilterOrFactory( options, originalOptions, jqXHR );\n\t\t\tif ( typeof dataTypeOrTransport === \"string\" && !seekingTransport && !inspected[ dataTypeOrTransport ] ) {\n\t\t\t\toptions.dataTypes.unshift( dataTypeOrTransport );\n\t\t\t\tinspect( dataTypeOrTransport );\n\t\t\t\treturn false;\n\t\t\t} else if ( seekingTransport ) {\n\t\t\t\treturn !( selected = dataTypeOrTransport );\n\t\t\t}\n\t\t});\n\t\treturn selected;\n\t}\n\n\treturn inspect( options.dataTypes[ 0 ] ) || !inspected[ \"*\" ] && inspect( \"*\" );\n}\n\n// A special extend for ajax options\n// that takes \"flat\" options (not to be deep extended)\n// Fixes #9887\nfunction ajaxExtend( target, src ) {\n\tvar deep, key,\n\t\tflatOptions = jQuery.ajaxSettings.flatOptions || {};\n\n\tfor ( key in src ) {\n\t\tif ( src[ key ] !== undefined ) {\n\t\t\t( flatOptions[ key ] ? target : ( deep || (deep = {}) ) )[ key ] = src[ key ];\n\t\t}\n\t}\n\tif ( deep ) {\n\t\tjQuery.extend( true, target, deep );\n\t}\n\n\treturn target;\n}\n\n/* Handles responses to an ajax request:\n * - finds the right dataType (mediates between content-type and expected dataType)\n * - returns the corresponding response\n */\nfunction ajaxHandleResponses( s, jqXHR, responses ) {\n\tvar firstDataType, ct, finalDataType, type,\n\t\tcontents = s.contents,\n\t\tdataTypes = s.dataTypes;\n\n\t// Remove auto dataType and get content-type in the process\n\twhile ( dataTypes[ 0 ] === \"*\" ) {\n\t\tdataTypes.shift();\n\t\tif ( ct === undefined ) {\n\t\t\tct = s.mimeType || jqXHR.getResponseHeader(\"Content-Type\");\n\t\t}\n\t}\n\n\t// Check if we're dealing with a known content-type\n\tif ( ct ) {\n\t\tfor ( type in contents ) {\n\t\t\tif ( contents[ type ] && contents[ type ].test( ct ) ) {\n\t\t\t\tdataTypes.unshift( type );\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\n\t// Check to see if we have a response for the expected dataType\n\tif ( dataTypes[ 0 ] in responses ) {\n\t\tfinalDataType = dataTypes[ 0 ];\n\t} else {\n\t\t// Try convertible dataTypes\n\t\tfor ( type in responses ) {\n\t\t\tif ( !dataTypes[ 0 ] || s.converters[ type + \" \" + dataTypes[0] ] ) {\n\t\t\t\tfinalDataType = type;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif ( !firstDataType ) {\n\t\t\t\tfirstDataType = type;\n\t\t\t}\n\t\t}\n\t\t// Or just use first one\n\t\tfinalDataType = finalDataType || firstDataType;\n\t}\n\n\t// If we found a dataType\n\t// We add the dataType to the list if needed\n\t// and return the corresponding response\n\tif ( finalDataType ) {\n\t\tif ( finalDataType !== dataTypes[ 0 ] ) {\n\t\t\tdataTypes.unshift( finalDataType );\n\t\t}\n\t\treturn responses[ finalDataType ];\n\t}\n}\n\n/* Chain conversions given the request and the original response\n * Also sets the responseXXX fields on the jqXHR instance\n */\nfunction ajaxConvert( s, response, jqXHR, isSuccess ) {\n\tvar conv2, current, conv, tmp, prev,\n\t\tconverters = {},\n\t\t// Work with a copy of dataTypes in case we need to modify it for conversion\n\t\tdataTypes = s.dataTypes.slice();\n\n\t// Create converters map with lowercased keys\n\tif ( dataTypes[ 1 ] ) {\n\t\tfor ( conv in s.converters ) {\n\t\t\tconverters[ conv.toLowerCase() ] = s.converters[ conv ];\n\t\t}\n\t}\n\n\tcurrent = dataTypes.shift();\n\n\t// Convert to each sequential dataType\n\twhile ( current ) {\n\n\t\tif ( s.responseFields[ current ] ) {\n\t\t\tjqXHR[ s.responseFields[ current ] ] = response;\n\t\t}\n\n\t\t// Apply the dataFilter if provided\n\t\tif ( !prev && isSuccess && s.dataFilter ) {\n\t\t\tresponse = s.dataFilter( response, s.dataType );\n\t\t}\n\n\t\tprev = current;\n\t\tcurrent = dataTypes.shift();\n\n\t\tif ( current ) {\n\n\t\t\t// There's only work to do if current dataType is non-auto\n\t\t\tif ( current === \"*\" ) {\n\n\t\t\t\tcurrent = prev;\n\n\t\t\t// Convert response if prev dataType is non-auto and differs from current\n\t\t\t} else if ( prev !== \"*\" && prev !== current ) {\n\n\t\t\t\t// Seek a direct converter\n\t\t\t\tconv = converters[ prev + \" \" + current ] || converters[ \"* \" + current ];\n\n\t\t\t\t// If none found, seek a pair\n\t\t\t\tif ( !conv ) {\n\t\t\t\t\tfor ( conv2 in converters ) {\n\n\t\t\t\t\t\t// If conv2 outputs current\n\t\t\t\t\t\ttmp = conv2.split( \" \" );\n\t\t\t\t\t\tif ( tmp[ 1 ] === current ) {\n\n\t\t\t\t\t\t\t// If prev can be converted to accepted input\n\t\t\t\t\t\t\tconv = converters[ prev + \" \" + tmp[ 0 ] ] ||\n\t\t\t\t\t\t\t\tconverters[ \"* \" + tmp[ 0 ] ];\n\t\t\t\t\t\t\tif ( conv ) {\n\t\t\t\t\t\t\t\t// Condense equivalence converters\n\t\t\t\t\t\t\t\tif ( conv === true ) {\n\t\t\t\t\t\t\t\t\tconv = converters[ conv2 ];\n\n\t\t\t\t\t\t\t\t// Otherwise, insert the intermediate dataType\n\t\t\t\t\t\t\t\t} else if ( converters[ conv2 ] !== true ) {\n\t\t\t\t\t\t\t\t\tcurrent = tmp[ 0 ];\n\t\t\t\t\t\t\t\t\tdataTypes.unshift( tmp[ 1 ] );\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Apply converter (if not an equivalence)\n\t\t\t\tif ( conv !== true ) {\n\n\t\t\t\t\t// Unless errors are allowed to bubble, catch and return them\n\t\t\t\t\tif ( conv && s[ \"throws\" ] ) {\n\t\t\t\t\t\tresponse = conv( response );\n\t\t\t\t\t} else {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tresponse = conv( response );\n\t\t\t\t\t\t} catch ( e ) {\n\t\t\t\t\t\t\treturn { state: \"parsererror\", error: conv ? e : \"No conversion from \" + prev + \" to \" + current };\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn { state: \"success\", data: response };\n}\n\njQuery.extend({\n\n\t// Counter for holding the number of active queries\n\tactive: 0,\n\n\t// Last-Modified header cache for next request\n\tlastModified: {},\n\tetag: {},\n\n\tajaxSettings: {\n\t\turl: ajaxLocation,\n\t\ttype: \"GET\",\n\t\tisLocal: rlocalProtocol.test( ajaxLocParts[ 1 ] ),\n\t\tglobal: true,\n\t\tprocessData: true,\n\t\tasync: true,\n\t\tcontentType: \"application/x-www-form-urlencoded; charset=UTF-8\",\n\t\t/*\n\t\ttimeout: 0,\n\t\tdata: null,\n\t\tdataType: null,\n\t\tusername: null,\n\t\tpassword: null,\n\t\tcache: null,\n\t\tthrows: false,\n\t\ttraditional: false,\n\t\theaders: {},\n\t\t*/\n\n\t\taccepts: {\n\t\t\t\"*\": allTypes,\n\t\t\ttext: \"text/plain\",\n\t\t\thtml: \"text/html\",\n\t\t\txml: \"application/xml, text/xml\",\n\t\t\tjson: \"application/json, text/javascript\"\n\t\t},\n\n\t\tcontents: {\n\t\t\txml: /xml/,\n\t\t\thtml: /html/,\n\t\t\tjson: /json/\n\t\t},\n\n\t\tresponseFields: {\n\t\t\txml: \"responseXML\",\n\t\t\ttext: \"responseText\",\n\t\t\tjson: \"responseJSON\"\n\t\t},\n\n\t\t// Data converters\n\t\t// Keys separate source (or catchall \"*\") and destination types with a single space\n\t\tconverters: {\n\n\t\t\t// Convert anything to text\n\t\t\t\"* text\": String,\n\n\t\t\t// Text to html (true = no transformation)\n\t\t\t\"text html\": true,\n\n\t\t\t// Evaluate text as a json expression\n\t\t\t\"text json\": jQuery.parseJSON,\n\n\t\t\t// Parse text as xml\n\t\t\t\"text xml\": jQuery.parseXML\n\t\t},\n\n\t\t// For options that shouldn't be deep extended:\n\t\t// you can add your own custom options here if\n\t\t// and when you create one that shouldn't be\n\t\t// deep extended (see ajaxExtend)\n\t\tflatOptions: {\n\t\t\turl: true,\n\t\t\tcontext: true\n\t\t}\n\t},\n\n\t// Creates a full fledged settings object into target\n\t// with both ajaxSettings and settings fields.\n\t// If target is omitted, writes into ajaxSettings.\n\tajaxSetup: function( target, settings ) {\n\t\treturn settings ?\n\n\t\t\t// Building a settings object\n\t\t\tajaxExtend( ajaxExtend( target, jQuery.ajaxSettings ), settings ) :\n\n\t\t\t// Extending ajaxSettings\n\t\t\tajaxExtend( jQuery.ajaxSettings, target );\n\t},\n\n\tajaxPrefilter: addToPrefiltersOrTransports( prefilters ),\n\tajaxTransport: addToPrefiltersOrTransports( transports ),\n\n\t// Main method\n\tajax: function( url, options ) {\n\n\t\t// If url is an object, simulate pre-1.5 signature\n\t\tif ( typeof url === \"object\" ) {\n\t\t\toptions = url;\n\t\t\turl = undefined;\n\t\t}\n\n\t\t// Force options to be an object\n\t\toptions = options || {};\n\n\t\tvar // Cross-domain detection vars\n\t\t\tparts,\n\t\t\t// Loop variable\n\t\t\ti,\n\t\t\t// URL without anti-cache param\n\t\t\tcacheURL,\n\t\t\t// Response headers as string\n\t\t\tresponseHeadersString,\n\t\t\t// timeout handle\n\t\t\ttimeoutTimer,\n\n\t\t\t// To know if global events are to be dispatched\n\t\t\tfireGlobals,\n\n\t\t\ttransport,\n\t\t\t// Response headers\n\t\t\tresponseHeaders,\n\t\t\t// Create the final options object\n\t\t\ts = jQuery.ajaxSetup( {}, options ),\n\t\t\t// Callbacks context\n\t\t\tcallbackContext = s.context || s,\n\t\t\t// Context for global events is callbackContext if it is a DOM node or jQuery collection\n\t\t\tglobalEventContext = s.context && ( callbackContext.nodeType || callbackContext.jquery ) ?\n\t\t\t\tjQuery( callbackContext ) :\n\t\t\t\tjQuery.event,\n\t\t\t// Deferreds\n\t\t\tdeferred = jQuery.Deferred(),\n\t\t\tcompleteDeferred = jQuery.Callbacks(\"once memory\"),\n\t\t\t// Status-dependent callbacks\n\t\t\tstatusCode = s.statusCode || {},\n\t\t\t// Headers (they are sent all at once)\n\t\t\trequestHeaders = {},\n\t\t\trequestHeadersNames = {},\n\t\t\t// The jqXHR state\n\t\t\tstate = 0,\n\t\t\t// Default abort message\n\t\t\tstrAbort = \"canceled\",\n\t\t\t// Fake xhr\n\t\t\tjqXHR = {\n\t\t\t\treadyState: 0,\n\n\t\t\t\t// Builds headers hashtable if needed\n\t\t\t\tgetResponseHeader: function( key ) {\n\t\t\t\t\tvar match;\n\t\t\t\t\tif ( state === 2 ) {\n\t\t\t\t\t\tif ( !responseHeaders ) {\n\t\t\t\t\t\t\tresponseHeaders = {};\n\t\t\t\t\t\t\twhile ( (match = rheaders.exec( responseHeadersString )) ) {\n\t\t\t\t\t\t\t\tresponseHeaders[ match[1].toLowerCase() ] = match[ 2 ];\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tmatch = responseHeaders[ key.toLowerCase() ];\n\t\t\t\t\t}\n\t\t\t\t\treturn match == null ? null : match;\n\t\t\t\t},\n\n\t\t\t\t// Raw string\n\t\t\t\tgetAllResponseHeaders: function() {\n\t\t\t\t\treturn state === 2 ? responseHeadersString : null;\n\t\t\t\t},\n\n\t\t\t\t// Caches the header\n\t\t\t\tsetRequestHeader: function( name, value ) {\n\t\t\t\t\tvar lname = name.toLowerCase();\n\t\t\t\t\tif ( !state ) {\n\t\t\t\t\t\tname = requestHeadersNames[ lname ] = requestHeadersNames[ lname ] || name;\n\t\t\t\t\t\trequestHeaders[ name ] = value;\n\t\t\t\t\t}\n\t\t\t\t\treturn this;\n\t\t\t\t},\n\n\t\t\t\t// Overrides response content-type header\n\t\t\t\toverrideMimeType: function( type ) {\n\t\t\t\t\tif ( !state ) {\n\t\t\t\t\t\ts.mimeType = type;\n\t\t\t\t\t}\n\t\t\t\t\treturn this;\n\t\t\t\t},\n\n\t\t\t\t// Status-dependent callbacks\n\t\t\t\tstatusCode: function( map ) {\n\t\t\t\t\tvar code;\n\t\t\t\t\tif ( map ) {\n\t\t\t\t\t\tif ( state < 2 ) {\n\t\t\t\t\t\t\tfor ( code in map ) {\n\t\t\t\t\t\t\t\t// Lazy-add the new callback in a way that preserves old ones\n\t\t\t\t\t\t\t\tstatusCode[ code ] = [ statusCode[ code ], map[ code ] ];\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// Execute the appropriate callbacks\n\t\t\t\t\t\t\tjqXHR.always( map[ jqXHR.status ] );\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\treturn this;\n\t\t\t\t},\n\n\t\t\t\t// Cancel the request\n\t\t\t\tabort: function( statusText ) {\n\t\t\t\t\tvar finalText = statusText || strAbort;\n\t\t\t\t\tif ( transport ) {\n\t\t\t\t\t\ttransport.abort( finalText );\n\t\t\t\t\t}\n\t\t\t\t\tdone( 0, finalText );\n\t\t\t\t\treturn this;\n\t\t\t\t}\n\t\t\t};\n\n\t\t// Attach deferreds\n\t\tdeferred.promise( jqXHR ).complete = completeDeferred.add;\n\t\tjqXHR.success = jqXHR.done;\n\t\tjqXHR.error = jqXHR.fail;\n\n\t\t// Remove hash character (#7531: and string promotion)\n\t\t// Add protocol if not provided (#5866: IE7 issue with protocol-less urls)\n\t\t// Handle falsy url in the settings object (#10093: consistency with old signature)\n\t\t// We also use the url parameter if available\n\t\ts.url = ( ( url || s.url || ajaxLocation ) + \"\" ).replace( rhash, \"\" ).replace( rprotocol, ajaxLocParts[ 1 ] + \"//\" );\n\n\t\t// Alias method option to type as per ticket #12004\n\t\ts.type = options.method || options.type || s.method || s.type;\n\n\t\t// Extract dataTypes list\n\t\ts.dataTypes = jQuery.trim( s.dataType || \"*\" ).toLowerCase().match( rnotwhite ) || [ \"\" ];\n\n\t\t// A cross-domain request is in order when we have a protocol:host:port mismatch\n\t\tif ( s.crossDomain == null ) {\n\t\t\tparts = rurl.exec( s.url.toLowerCase() );\n\t\t\ts.crossDomain = !!( parts &&\n\t\t\t\t( parts[ 1 ] !== ajaxLocParts[ 1 ] || parts[ 2 ] !== ajaxLocParts[ 2 ] ||\n\t\t\t\t\t( parts[ 3 ] || ( parts[ 1 ] === \"http:\" ? \"80\" : \"443\" ) ) !==\n\t\t\t\t\t\t( ajaxLocParts[ 3 ] || ( ajaxLocParts[ 1 ] === \"http:\" ? \"80\" : \"443\" ) ) )\n\t\t\t);\n\t\t}\n\n\t\t// Convert data if not already a string\n\t\tif ( s.data && s.processData && typeof s.data !== \"string\" ) {\n\t\t\ts.data = jQuery.param( s.data, s.traditional );\n\t\t}\n\n\t\t// Apply prefilters\n\t\tinspectPrefiltersOrTransports( prefilters, s, options, jqXHR );\n\n\t\t// If request was aborted inside a prefilter, stop there\n\t\tif ( state === 2 ) {\n\t\t\treturn jqXHR;\n\t\t}\n\n\t\t// We can fire global events as of now if asked to\n\t\tfireGlobals = s.global;\n\n\t\t// Watch for a new set of requests\n\t\tif ( fireGlobals && jQuery.active++ === 0 ) {\n\t\t\tjQuery.event.trigger(\"ajaxStart\");\n\t\t}\n\n\t\t// Uppercase the type\n\t\ts.type = s.type.toUpperCase();\n\n\t\t// Determine if request has content\n\t\ts.hasContent = !rnoContent.test( s.type );\n\n\t\t// Save the URL in case we're toying with the If-Modified-Since\n\t\t// and/or If-None-Match header later on\n\t\tcacheURL = s.url;\n\n\t\t// More options handling for requests with no content\n\t\tif ( !s.hasContent ) {\n\n\t\t\t// If data is available, append data to url\n\t\t\tif ( s.data ) {\n\t\t\t\tcacheURL = ( s.url += ( rquery.test( cacheURL ) ? \"&\" : \"?\" ) + s.data );\n\t\t\t\t// #9682: remove data so that it's not used in an eventual retry\n\t\t\t\tdelete s.data;\n\t\t\t}\n\n\t\t\t// Add anti-cache in url if needed\n\t\t\tif ( s.cache === false ) {\n\t\t\t\ts.url = rts.test( cacheURL ) ?\n\n\t\t\t\t\t// If there is already a '_' parameter, set its value\n\t\t\t\t\tcacheURL.replace( rts, \"$1_=\" + nonce++ ) :\n\n\t\t\t\t\t// Otherwise add one to the end\n\t\t\t\t\tcacheURL + ( rquery.test( cacheURL ) ? \"&\" : \"?\" ) + \"_=\" + nonce++;\n\t\t\t}\n\t\t}\n\n\t\t// Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode.\n\t\tif ( s.ifModified ) {\n\t\t\tif ( jQuery.lastModified[ cacheURL ] ) {\n\t\t\t\tjqXHR.setRequestHeader( \"If-Modified-Since\", jQuery.lastModified[ cacheURL ] );\n\t\t\t}\n\t\t\tif ( jQuery.etag[ cacheURL ] ) {\n\t\t\t\tjqXHR.setRequestHeader( \"If-None-Match\", jQuery.etag[ cacheURL ] );\n\t\t\t}\n\t\t}\n\n\t\t// Set the correct header, if data is being sent\n\t\tif ( s.data && s.hasContent && s.contentType !== false || options.contentType ) {\n\t\t\tjqXHR.setRequestHeader( \"Content-Type\", s.contentType );\n\t\t}\n\n\t\t// Set the Accepts header for the server, depending on the dataType\n\t\tjqXHR.setRequestHeader(\n\t\t\t\"Accept\",\n\t\t\ts.dataTypes[ 0 ] && s.accepts[ s.dataTypes[0] ] ?\n\t\t\t\ts.accepts[ s.dataTypes[0] ] + ( s.dataTypes[ 0 ] !== \"*\" ? \", \" + allTypes + \"; q=0.01\" : \"\" ) :\n\t\t\t\ts.accepts[ \"*\" ]\n\t\t);\n\n\t\t// Check for headers option\n\t\tfor ( i in s.headers ) {\n\t\t\tjqXHR.setRequestHeader( i, s.headers[ i ] );\n\t\t}\n\n\t\t// Allow custom headers/mimetypes and early abort\n\t\tif ( s.beforeSend && ( s.beforeSend.call( callbackContext, jqXHR, s ) === false || state === 2 ) ) {\n\t\t\t// Abort if not done already and return\n\t\t\treturn jqXHR.abort();\n\t\t}\n\n\t\t// aborting is no longer a cancellation\n\t\tstrAbort = \"abort\";\n\n\t\t// Install callbacks on deferreds\n\t\tfor ( i in { success: 1, error: 1, complete: 1 } ) {\n\t\t\tjqXHR[ i ]( s[ i ] );\n\t\t}\n\n\t\t// Get transport\n\t\ttransport = inspectPrefiltersOrTransports( transports, s, options, jqXHR );\n\n\t\t// If no transport, we auto-abort\n\t\tif ( !transport ) {\n\t\t\tdone( -1, \"No Transport\" );\n\t\t} else {\n\t\t\tjqXHR.readyState = 1;\n\n\t\t\t// Send global event\n\t\t\tif ( fireGlobals ) {\n\t\t\t\tglobalEventContext.trigger( \"ajaxSend\", [ jqXHR, s ] );\n\t\t\t}\n\t\t\t// Timeout\n\t\t\tif ( s.async && s.timeout > 0 ) {\n\t\t\t\ttimeoutTimer = setTimeout(function() {\n\t\t\t\t\tjqXHR.abort(\"timeout\");\n\t\t\t\t}, s.timeout );\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\tstate = 1;\n\t\t\t\ttransport.send( requestHeaders, done );\n\t\t\t} catch ( e ) {\n\t\t\t\t// Propagate exception as error if not done\n\t\t\t\tif ( state < 2 ) {\n\t\t\t\t\tdone( -1, e );\n\t\t\t\t// Simply rethrow otherwise\n\t\t\t\t} else {\n\t\t\t\t\tthrow e;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Callback for when everything is done\n\t\tfunction done( status, nativeStatusText, responses, headers ) {\n\t\t\tvar isSuccess, success, error, response, modified,\n\t\t\t\tstatusText = nativeStatusText;\n\n\t\t\t// Called once\n\t\t\tif ( state === 2 ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// State is \"done\" now\n\t\t\tstate = 2;\n\n\t\t\t// Clear timeout if it exists\n\t\t\tif ( timeoutTimer ) {\n\t\t\t\tclearTimeout( timeoutTimer );\n\t\t\t}\n\n\t\t\t// Dereference transport for early garbage collection\n\t\t\t// (no matter how long the jqXHR object will be used)\n\t\t\ttransport = undefined;\n\n\t\t\t// Cache response headers\n\t\t\tresponseHeadersString = headers || \"\";\n\n\t\t\t// Set readyState\n\t\t\tjqXHR.readyState = status > 0 ? 4 : 0;\n\n\t\t\t// Determine if successful\n\t\t\tisSuccess = status >= 200 && status < 300 || status === 304;\n\n\t\t\t// Get response data\n\t\t\tif ( responses ) {\n\t\t\t\tresponse = ajaxHandleResponses( s, jqXHR, responses );\n\t\t\t}\n\n\t\t\t// Convert no matter what (that way responseXXX fields are always set)\n\t\t\tresponse = ajaxConvert( s, response, jqXHR, isSuccess );\n\n\t\t\t// If successful, handle type chaining\n\t\t\tif ( isSuccess ) {\n\n\t\t\t\t// Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode.\n\t\t\t\tif ( s.ifModified ) {\n\t\t\t\t\tmodified = jqXHR.getResponseHeader(\"Last-Modified\");\n\t\t\t\t\tif ( modified ) {\n\t\t\t\t\t\tjQuery.lastModified[ cacheURL ] = modified;\n\t\t\t\t\t}\n\t\t\t\t\tmodified = jqXHR.getResponseHeader(\"etag\");\n\t\t\t\t\tif ( modified ) {\n\t\t\t\t\t\tjQuery.etag[ cacheURL ] = modified;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// if no content\n\t\t\t\tif ( status === 204 || s.type === \"HEAD\" ) {\n\t\t\t\t\tstatusText = \"nocontent\";\n\n\t\t\t\t// if not modified\n\t\t\t\t} else if ( status === 304 ) {\n\t\t\t\t\tstatusText = \"notmodified\";\n\n\t\t\t\t// If we have data, let's convert it\n\t\t\t\t} else {\n\t\t\t\t\tstatusText = response.state;\n\t\t\t\t\tsuccess = response.data;\n\t\t\t\t\terror = response.error;\n\t\t\t\t\tisSuccess = !error;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// We extract error from statusText\n\t\t\t\t// then normalize statusText and status for non-aborts\n\t\t\t\terror = statusText;\n\t\t\t\tif ( status || !statusText ) {\n\t\t\t\t\tstatusText = \"error\";\n\t\t\t\t\tif ( status < 0 ) {\n\t\t\t\t\t\tstatus = 0;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Set data for the fake xhr object\n\t\t\tjqXHR.status = status;\n\t\t\tjqXHR.statusText = ( nativeStatusText || statusText ) + \"\";\n\n\t\t\t// Success/Error\n\t\t\tif ( isSuccess ) {\n\t\t\t\tdeferred.resolveWith( callbackContext, [ success, statusText, jqXHR ] );\n\t\t\t} else {\n\t\t\t\tdeferred.rejectWith( callbackContext, [ jqXHR, statusText, error ] );\n\t\t\t}\n\n\t\t\t// Status-dependent callbacks\n\t\t\tjqXHR.statusCode( statusCode );\n\t\t\tstatusCode = undefined;\n\n\t\t\tif ( fireGlobals ) {\n\t\t\t\tglobalEventContext.trigger( isSuccess ? \"ajaxSuccess\" : \"ajaxError\",\n\t\t\t\t\t[ jqXHR, s, isSuccess ? success : error ] );\n\t\t\t}\n\n\t\t\t// Complete\n\t\t\tcompleteDeferred.fireWith( callbackContext, [ jqXHR, statusText ] );\n\n\t\t\tif ( fireGlobals ) {\n\t\t\t\tglobalEventContext.trigger( \"ajaxComplete\", [ jqXHR, s ] );\n\t\t\t\t// Handle the global AJAX counter\n\t\t\t\tif ( !( --jQuery.active ) ) {\n\t\t\t\t\tjQuery.event.trigger(\"ajaxStop\");\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn jqXHR;\n\t},\n\n\tgetJSON: function( url, data, callback ) {\n\t\treturn jQuery.get( url, data, callback, \"json\" );\n\t},\n\n\tgetScript: function( url, callback ) {\n\t\treturn jQuery.get( url, undefined, callback, \"script\" );\n\t}\n});\n\njQuery.each( [ \"get\", \"post\" ], function( i, method ) {\n\tjQuery[ method ] = function( url, data, callback, type ) {\n\t\t// shift arguments if data argument was omitted\n\t\tif ( jQuery.isFunction( data ) ) {\n\t\t\ttype = type || callback;\n\t\t\tcallback = data;\n\t\t\tdata = undefined;\n\t\t}\n\n\t\treturn jQuery.ajax({\n\t\t\turl: url,\n\t\t\ttype: method,\n\t\t\tdataType: type,\n\t\t\tdata: data,\n\t\t\tsuccess: callback\n\t\t});\n\t};\n});\n\n// Attach a bunch of functions for handling common AJAX events\njQuery.each( [ \"ajaxStart\", \"ajaxStop\", \"ajaxComplete\", \"ajaxError\", \"ajaxSuccess\", \"ajaxSend\" ], function( i, type ) {\n\tjQuery.fn[ type ] = function( fn ) {\n\t\treturn this.on( type, fn );\n\t};\n});\n\n\njQuery._evalUrl = function( url ) {\n\treturn jQuery.ajax({\n\t\turl: url,\n\t\ttype: \"GET\",\n\t\tdataType: \"script\",\n\t\tasync: false,\n\t\tglobal: false,\n\t\t\"throws\": true\n\t});\n};\n\n\njQuery.fn.extend({\n\twrapAll: function( html ) {\n\t\tif ( jQuery.isFunction( html ) ) {\n\t\t\treturn this.each(function(i) {\n\t\t\t\tjQuery(this).wrapAll( html.call(this, i) );\n\t\t\t});\n\t\t}\n\n\t\tif ( this[0] ) {\n\t\t\t// The elements to wrap the target around\n\t\t\tvar wrap = jQuery( html, this[0].ownerDocument ).eq(0).clone(true);\n\n\t\t\tif ( this[0].parentNode ) {\n\t\t\t\twrap.insertBefore( this[0] );\n\t\t\t}\n\n\t\t\twrap.map(function() {\n\t\t\t\tvar elem = this;\n\n\t\t\t\twhile ( elem.firstChild && elem.firstChild.nodeType === 1 ) {\n\t\t\t\t\telem = elem.firstChild;\n\t\t\t\t}\n\n\t\t\t\treturn elem;\n\t\t\t}).append( this );\n\t\t}\n\n\t\treturn this;\n\t},\n\n\twrapInner: function( html ) {\n\t\tif ( jQuery.isFunction( html ) ) {\n\t\t\treturn this.each(function(i) {\n\t\t\t\tjQuery(this).wrapInner( html.call(this, i) );\n\t\t\t});\n\t\t}\n\n\t\treturn this.each(function() {\n\t\t\tvar self = jQuery( this ),\n\t\t\t\tcontents = self.contents();\n\n\t\t\tif ( contents.length ) {\n\t\t\t\tcontents.wrapAll( html );\n\n\t\t\t} else {\n\t\t\t\tself.append( html );\n\t\t\t}\n\t\t});\n\t},\n\n\twrap: function( html ) {\n\t\tvar isFunction = jQuery.isFunction( html );\n\n\t\treturn this.each(function(i) {\n\t\t\tjQuery( this ).wrapAll( isFunction ? html.call(this, i) : html );\n\t\t});\n\t},\n\n\tunwrap: function() {\n\t\treturn this.parent().each(function() {\n\t\t\tif ( !jQuery.nodeName( this, \"body\" ) ) {\n\t\t\t\tjQuery( this ).replaceWith( this.childNodes );\n\t\t\t}\n\t\t}).end();\n\t}\n});\n\n\njQuery.expr.filters.hidden = function( elem ) {\n\t// Support: Opera <= 12.12\n\t// Opera reports offsetWidths and offsetHeights less than zero on some elements\n\treturn elem.offsetWidth <= 0 && elem.offsetHeight <= 0 ||\n\t\t(!support.reliableHiddenOffsets() &&\n\t\t\t((elem.style && elem.style.display) || jQuery.css( elem, \"display\" )) === \"none\");\n};\n\njQuery.expr.filters.visible = function( elem ) {\n\treturn !jQuery.expr.filters.hidden( elem );\n};\n\n\n\n\nvar r20 = /%20/g,\n\trbracket = /\\[\\]$/,\n\trCRLF = /\\r?\\n/g,\n\trsubmitterTypes = /^(?:submit|button|image|reset|file)$/i,\n\trsubmittable = /^(?:input|select|textarea|keygen)/i;\n\nfunction buildParams( prefix, obj, traditional, add ) {\n\tvar name;\n\n\tif ( jQuery.isArray( obj ) ) {\n\t\t// Serialize array item.\n\t\tjQuery.each( obj, function( i, v ) {\n\t\t\tif ( traditional || rbracket.test( prefix ) ) {\n\t\t\t\t// Treat each array item as a scalar.\n\t\t\t\tadd( prefix, v );\n\n\t\t\t} else {\n\t\t\t\t// Item is non-scalar (array or object), encode its numeric index.\n\t\t\t\tbuildParams( prefix + \"[\" + ( typeof v === \"object\" ? i : \"\" ) + \"]\", v, traditional, add );\n\t\t\t}\n\t\t});\n\n\t} else if ( !traditional && jQuery.type( obj ) === \"object\" ) {\n\t\t// Serialize object item.\n\t\tfor ( name in obj ) {\n\t\t\tbuildParams( prefix + \"[\" + name + \"]\", obj[ name ], traditional, add );\n\t\t}\n\n\t} else {\n\t\t// Serialize scalar item.\n\t\tadd( prefix, obj );\n\t}\n}\n\n// Serialize an array of form elements or a set of\n// key/values into a query string\njQuery.param = function( a, traditional ) {\n\tvar prefix,\n\t\ts = [],\n\t\tadd = function( key, value ) {\n\t\t\t// If value is a function, invoke it and return its value\n\t\t\tvalue = jQuery.isFunction( value ) ? value() : ( value == null ? \"\" : value );\n\t\t\ts[ s.length ] = encodeURIComponent( key ) + \"=\" + encodeURIComponent( value );\n\t\t};\n\n\t// Set traditional to true for jQuery <= 1.3.2 behavior.\n\tif ( traditional === undefined ) {\n\t\ttraditional = jQuery.ajaxSettings && jQuery.ajaxSettings.traditional;\n\t}\n\n\t// If an array was passed in, assume that it is an array of form elements.\n\tif ( jQuery.isArray( a ) || ( a.jquery && !jQuery.isPlainObject( a ) ) ) {\n\t\t// Serialize the form elements\n\t\tjQuery.each( a, function() {\n\t\t\tadd( this.name, this.value );\n\t\t});\n\n\t} else {\n\t\t// If traditional, encode the \"old\" way (the way 1.3.2 or older\n\t\t// did it), otherwise encode params recursively.\n\t\tfor ( prefix in a ) {\n\t\t\tbuildParams( prefix, a[ prefix ], traditional, add );\n\t\t}\n\t}\n\n\t// Return the resulting serialization\n\treturn s.join( \"&\" ).replace( r20, \"+\" );\n};\n\njQuery.fn.extend({\n\tserialize: function() {\n\t\treturn jQuery.param( this.serializeArray() );\n\t},\n\tserializeArray: function() {\n\t\treturn this.map(function() {\n\t\t\t// Can add propHook for \"elements\" to filter or add form elements\n\t\t\tvar elements = jQuery.prop( this, \"elements\" );\n\t\t\treturn elements ? jQuery.makeArray( elements ) : this;\n\t\t})\n\t\t.filter(function() {\n\t\t\tvar type = this.type;\n\t\t\t// Use .is(\":disabled\") so that fieldset[disabled] works\n\t\t\treturn this.name && !jQuery( this ).is( \":disabled\" ) &&\n\t\t\t\trsubmittable.test( this.nodeName ) && !rsubmitterTypes.test( type ) &&\n\t\t\t\t( this.checked || !rcheckableType.test( type ) );\n\t\t})\n\t\t.map(function( i, elem ) {\n\t\t\tvar val = jQuery( this ).val();\n\n\t\t\treturn val == null ?\n\t\t\t\tnull :\n\t\t\t\tjQuery.isArray( val ) ?\n\t\t\t\t\tjQuery.map( val, function( val ) {\n\t\t\t\t\t\treturn { name: elem.name, value: val.replace( rCRLF, \"\\r\\n\" ) };\n\t\t\t\t\t}) :\n\t\t\t\t\t{ name: elem.name, value: val.replace( rCRLF, \"\\r\\n\" ) };\n\t\t}).get();\n\t}\n});\n\n\n// Create the request object\n// (This is still attached to ajaxSettings for backward compatibility)\njQuery.ajaxSettings.xhr = window.ActiveXObject !== undefined ?\n\t// Support: IE6+\n\tfunction() {\n\n\t\t// XHR cannot access local files, always use ActiveX for that case\n\t\treturn !this.isLocal &&\n\n\t\t\t// Support: IE7-8\n\t\t\t// oldIE XHR does not support non-RFC2616 methods (#13240)\n\t\t\t// See http://msdn.microsoft.com/en-us/library/ie/ms536648(v=vs.85).aspx\n\t\t\t// and http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9\n\t\t\t// Although this check for six methods instead of eight\n\t\t\t// since IE also does not support \"trace\" and \"connect\"\n\t\t\t/^(get|post|head|put|delete|options)$/i.test( this.type ) &&\n\n\t\t\tcreateStandardXHR() || createActiveXHR();\n\t} :\n\t// For all other browsers, use the standard XMLHttpRequest object\n\tcreateStandardXHR;\n\nvar xhrId = 0,\n\txhrCallbacks = {},\n\txhrSupported = jQuery.ajaxSettings.xhr();\n\n// Support: IE<10\n// Open requests must be manually aborted on unload (#5280)\nif ( window.ActiveXObject ) {\n\tjQuery( window ).on( \"unload\", function() {\n\t\tfor ( var key in xhrCallbacks ) {\n\t\t\txhrCallbacks[ key ]( undefined, true );\n\t\t}\n\t});\n}\n\n// Determine support properties\nsupport.cors = !!xhrSupported && ( \"withCredentials\" in xhrSupported );\nxhrSupported = support.ajax = !!xhrSupported;\n\n// Create transport if the browser can provide an xhr\nif ( xhrSupported ) {\n\n\tjQuery.ajaxTransport(function( options ) {\n\t\t// Cross domain only allowed if supported through XMLHttpRequest\n\t\tif ( !options.crossDomain || support.cors ) {\n\n\t\t\tvar callback;\n\n\t\t\treturn {\n\t\t\t\tsend: function( headers, complete ) {\n\t\t\t\t\tvar i,\n\t\t\t\t\t\txhr = options.xhr(),\n\t\t\t\t\t\tid = ++xhrId;\n\n\t\t\t\t\t// Open the socket\n\t\t\t\t\txhr.open( options.type, options.url, options.async, options.username, options.password );\n\n\t\t\t\t\t// Apply custom fields if provided\n\t\t\t\t\tif ( options.xhrFields ) {\n\t\t\t\t\t\tfor ( i in options.xhrFields ) {\n\t\t\t\t\t\t\txhr[ i ] = options.xhrFields[ i ];\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// Override mime type if needed\n\t\t\t\t\tif ( options.mimeType && xhr.overrideMimeType ) {\n\t\t\t\t\t\txhr.overrideMimeType( options.mimeType );\n\t\t\t\t\t}\n\n\t\t\t\t\t// X-Requested-With header\n\t\t\t\t\t// For cross-domain requests, seeing as conditions for a preflight are\n\t\t\t\t\t// akin to a jigsaw puzzle, we simply never set it to be sure.\n\t\t\t\t\t// (it can always be set on a per-request basis or even using ajaxSetup)\n\t\t\t\t\t// For same-domain requests, won't change header if already provided.\n\t\t\t\t\tif ( !options.crossDomain && !headers[\"X-Requested-With\"] ) {\n\t\t\t\t\t\theaders[\"X-Requested-With\"] = \"XMLHttpRequest\";\n\t\t\t\t\t}\n\n\t\t\t\t\t// Set headers\n\t\t\t\t\tfor ( i in headers ) {\n\t\t\t\t\t\t// Support: IE<9\n\t\t\t\t\t\t// IE's ActiveXObject throws a 'Type Mismatch' exception when setting\n\t\t\t\t\t\t// request header to a null-value.\n\t\t\t\t\t\t//\n\t\t\t\t\t\t// To keep consistent with other XHR implementations, cast the value\n\t\t\t\t\t\t// to string and ignore `undefined`.\n\t\t\t\t\t\tif ( headers[ i ] !== undefined ) {\n\t\t\t\t\t\t\txhr.setRequestHeader( i, headers[ i ] + \"\" );\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// Do send the request\n\t\t\t\t\t// This may raise an exception which is actually\n\t\t\t\t\t// handled in jQuery.ajax (so no try/catch here)\n\t\t\t\t\txhr.send( ( options.hasContent && options.data ) || null );\n\n\t\t\t\t\t// Listener\n\t\t\t\t\tcallback = function( _, isAbort ) {\n\t\t\t\t\t\tvar status, statusText, responses;\n\n\t\t\t\t\t\t// Was never called and is aborted or complete\n\t\t\t\t\t\tif ( callback && ( isAbort || xhr.readyState === 4 ) ) {\n\t\t\t\t\t\t\t// Clean up\n\t\t\t\t\t\t\tdelete xhrCallbacks[ id ];\n\t\t\t\t\t\t\tcallback = undefined;\n\t\t\t\t\t\t\txhr.onreadystatechange = jQuery.noop;\n\n\t\t\t\t\t\t\t// Abort manually if needed\n\t\t\t\t\t\t\tif ( isAbort ) {\n\t\t\t\t\t\t\t\tif ( xhr.readyState !== 4 ) {\n\t\t\t\t\t\t\t\t\txhr.abort();\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tresponses = {};\n\t\t\t\t\t\t\t\tstatus = xhr.status;\n\n\t\t\t\t\t\t\t\t// Support: IE<10\n\t\t\t\t\t\t\t\t// Accessing binary-data responseText throws an exception\n\t\t\t\t\t\t\t\t// (#11426)\n\t\t\t\t\t\t\t\tif ( typeof xhr.responseText === \"string\" ) {\n\t\t\t\t\t\t\t\t\tresponses.text = xhr.responseText;\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t// Firefox throws an exception when accessing\n\t\t\t\t\t\t\t\t// statusText for faulty cross-domain requests\n\t\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\t\tstatusText = xhr.statusText;\n\t\t\t\t\t\t\t\t} catch( e ) {\n\t\t\t\t\t\t\t\t\t// We normalize with Webkit giving an empty statusText\n\t\t\t\t\t\t\t\t\tstatusText = \"\";\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t// Filter status for non standard behaviors\n\n\t\t\t\t\t\t\t\t// If the request is local and we have data: assume a success\n\t\t\t\t\t\t\t\t// (success with no data won't get notified, that's the best we\n\t\t\t\t\t\t\t\t// can do given current implementations)\n\t\t\t\t\t\t\t\tif ( !status && options.isLocal && !options.crossDomain ) {\n\t\t\t\t\t\t\t\t\tstatus = responses.text ? 200 : 404;\n\t\t\t\t\t\t\t\t// IE - #1450: sometimes returns 1223 when it should be 204\n\t\t\t\t\t\t\t\t} else if ( status === 1223 ) {\n\t\t\t\t\t\t\t\t\tstatus = 204;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Call complete if needed\n\t\t\t\t\t\tif ( responses ) {\n\t\t\t\t\t\t\tcomplete( status, statusText, responses, xhr.getAllResponseHeaders() );\n\t\t\t\t\t\t}\n\t\t\t\t\t};\n\n\t\t\t\t\tif ( !options.async ) {\n\t\t\t\t\t\t// if we're in sync mode we fire the callback\n\t\t\t\t\t\tcallback();\n\t\t\t\t\t} else if ( xhr.readyState === 4 ) {\n\t\t\t\t\t\t// (IE6 & IE7) if it's in cache and has been\n\t\t\t\t\t\t// retrieved directly we need to fire the callback\n\t\t\t\t\t\tsetTimeout( callback );\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Add to the list of active xhr callbacks\n\t\t\t\t\t\txhr.onreadystatechange = xhrCallbacks[ id ] = callback;\n\t\t\t\t\t}\n\t\t\t\t},\n\n\t\t\t\tabort: function() {\n\t\t\t\t\tif ( callback ) {\n\t\t\t\t\t\tcallback( undefined, true );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t};\n\t\t}\n\t});\n}\n\n// Functions to create xhrs\nfunction createStandardXHR() {\n\ttry {\n\t\treturn new window.XMLHttpRequest();\n\t} catch( e ) {}\n}\n\nfunction createActiveXHR() {\n\ttry {\n\t\treturn new window.ActiveXObject( \"Microsoft.XMLHTTP\" );\n\t} catch( e ) {}\n}\n\n\n\n\n// Install script dataType\njQuery.ajaxSetup({\n\taccepts: {\n\t\tscript: \"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript\"\n\t},\n\tcontents: {\n\t\tscript: /(?:java|ecma)script/\n\t},\n\tconverters: {\n\t\t\"text script\": function( text ) {\n\t\t\tjQuery.globalEval( text );\n\t\t\treturn text;\n\t\t}\n\t}\n});\n\n// Handle cache's special case and global\njQuery.ajaxPrefilter( \"script\", function( s ) {\n\tif ( s.cache === undefined ) {\n\t\ts.cache = false;\n\t}\n\tif ( s.crossDomain ) {\n\t\ts.type = \"GET\";\n\t\ts.global = false;\n\t}\n});\n\n// Bind script tag hack transport\njQuery.ajaxTransport( \"script\", function(s) {\n\n\t// This transport only deals with cross domain requests\n\tif ( s.crossDomain ) {\n\n\t\tvar script,\n\t\t\thead = document.head || jQuery(\"head\")[0] || document.documentElement;\n\n\t\treturn {\n\n\t\t\tsend: function( _, callback ) {\n\n\t\t\t\tscript = document.createElement(\"script\");\n\n\t\t\t\tscript.async = true;\n\n\t\t\t\tif ( s.scriptCharset ) {\n\t\t\t\t\tscript.charset = s.scriptCharset;\n\t\t\t\t}\n\n\t\t\t\tscript.src = s.url;\n\n\t\t\t\t// Attach handlers for all browsers\n\t\t\t\tscript.onload = script.onreadystatechange = function( _, isAbort ) {\n\n\t\t\t\t\tif ( isAbort || !script.readyState || /loaded|complete/.test( script.readyState ) ) {\n\n\t\t\t\t\t\t// Handle memory leak in IE\n\t\t\t\t\t\tscript.onload = script.onreadystatechange = null;\n\n\t\t\t\t\t\t// Remove the script\n\t\t\t\t\t\tif ( script.parentNode ) {\n\t\t\t\t\t\t\tscript.parentNode.removeChild( script );\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Dereference the script\n\t\t\t\t\t\tscript = null;\n\n\t\t\t\t\t\t// Callback if not abort\n\t\t\t\t\t\tif ( !isAbort ) {\n\t\t\t\t\t\t\tcallback( 200, \"success\" );\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t};\n\n\t\t\t\t// Circumvent IE6 bugs with base elements (#2709 and #4378) by prepending\n\t\t\t\t// Use native DOM manipulation to avoid our domManip AJAX trickery\n\t\t\t\thead.insertBefore( script, head.firstChild );\n\t\t\t},\n\n\t\t\tabort: function() {\n\t\t\t\tif ( script ) {\n\t\t\t\t\tscript.onload( undefined, true );\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t}\n});\n\n\n\n\nvar oldCallbacks = [],\n\trjsonp = /(=)\\?(?=&|$)|\\?\\?/;\n\n// Default jsonp settings\njQuery.ajaxSetup({\n\tjsonp: \"callback\",\n\tjsonpCallback: function() {\n\t\tvar callback = oldCallbacks.pop() || ( jQuery.expando + \"_\" + ( nonce++ ) );\n\t\tthis[ callback ] = true;\n\t\treturn callback;\n\t}\n});\n\n// Detect, normalize options and install callbacks for jsonp requests\njQuery.ajaxPrefilter( \"json jsonp\", function( s, originalSettings, jqXHR ) {\n\n\tvar callbackName, overwritten, responseContainer,\n\t\tjsonProp = s.jsonp !== false && ( rjsonp.test( s.url ) ?\n\t\t\t\"url\" :\n\t\t\ttypeof s.data === \"string\" && !( s.contentType || \"\" ).indexOf(\"application/x-www-form-urlencoded\") && rjsonp.test( s.data ) && \"data\"\n\t\t);\n\n\t// Handle iff the expected data type is \"jsonp\" or we have a parameter to set\n\tif ( jsonProp || s.dataTypes[ 0 ] === \"jsonp\" ) {\n\n\t\t// Get callback name, remembering preexisting value associated with it\n\t\tcallbackName = s.jsonpCallback = jQuery.isFunction( s.jsonpCallback ) ?\n\t\t\ts.jsonpCallback() :\n\t\t\ts.jsonpCallback;\n\n\t\t// Insert callback into url or form data\n\t\tif ( jsonProp ) {\n\t\t\ts[ jsonProp ] = s[ jsonProp ].replace( rjsonp, \"$1\" + callbackName );\n\t\t} else if ( s.jsonp !== false ) {\n\t\t\ts.url += ( rquery.test( s.url ) ? \"&\" : \"?\" ) + s.jsonp + \"=\" + callbackName;\n\t\t}\n\n\t\t// Use data converter to retrieve json after script execution\n\t\ts.converters[\"script json\"] = function() {\n\t\t\tif ( !responseContainer ) {\n\t\t\t\tjQuery.error( callbackName + \" was not called\" );\n\t\t\t}\n\t\t\treturn responseContainer[ 0 ];\n\t\t};\n\n\t\t// force json dataType\n\t\ts.dataTypes[ 0 ] = \"json\";\n\n\t\t// Install callback\n\t\toverwritten = window[ callbackName ];\n\t\twindow[ callbackName ] = function() {\n\t\t\tresponseContainer = arguments;\n\t\t};\n\n\t\t// Clean-up function (fires after converters)\n\t\tjqXHR.always(function() {\n\t\t\t// Restore preexisting value\n\t\t\twindow[ callbackName ] = overwritten;\n\n\t\t\t// Save back as free\n\t\t\tif ( s[ callbackName ] ) {\n\t\t\t\t// make sure that re-using the options doesn't screw things around\n\t\t\t\ts.jsonpCallback = originalSettings.jsonpCallback;\n\n\t\t\t\t// save the callback name for future use\n\t\t\t\toldCallbacks.push( callbackName );\n\t\t\t}\n\n\t\t\t// Call if it was a function and we have a response\n\t\t\tif ( responseContainer && jQuery.isFunction( overwritten ) ) {\n\t\t\t\toverwritten( responseContainer[ 0 ] );\n\t\t\t}\n\n\t\t\tresponseContainer = overwritten = undefined;\n\t\t});\n\n\t\t// Delegate to script\n\t\treturn \"script\";\n\t}\n});\n\n\n\n\n// data: string of html\n// context (optional): If specified, the fragment will be created in this context, defaults to document\n// keepScripts (optional): If true, will include scripts passed in the html string\njQuery.parseHTML = function( data, context, keepScripts ) {\n\tif ( !data || typeof data !== \"string\" ) {\n\t\treturn null;\n\t}\n\tif ( typeof context === \"boolean\" ) {\n\t\tkeepScripts = context;\n\t\tcontext = false;\n\t}\n\tcontext = context || document;\n\n\tvar parsed = rsingleTag.exec( data ),\n\t\tscripts = !keepScripts && [];\n\n\t// Single tag\n\tif ( parsed ) {\n\t\treturn [ context.createElement( parsed[1] ) ];\n\t}\n\n\tparsed = jQuery.buildFragment( [ data ], context, scripts );\n\n\tif ( scripts && scripts.length ) {\n\t\tjQuery( scripts ).remove();\n\t}\n\n\treturn jQuery.merge( [], parsed.childNodes );\n};\n\n\n// Keep a copy of the old load method\nvar _load = jQuery.fn.load;\n\n/**\n * Load a url into a page\n */\njQuery.fn.load = function( url, params, callback ) {\n\tif ( typeof url !== \"string\" && _load ) {\n\t\treturn _load.apply( this, arguments );\n\t}\n\n\tvar selector, response, type,\n\t\tself = this,\n\t\toff = url.indexOf(\" \");\n\n\tif ( off >= 0 ) {\n\t\tselector = jQuery.trim( url.slice( off, url.length ) );\n\t\turl = url.slice( 0, off );\n\t}\n\n\t// If it's a function\n\tif ( jQuery.isFunction( params ) ) {\n\n\t\t// We assume that it's the callback\n\t\tcallback = params;\n\t\tparams = undefined;\n\n\t// Otherwise, build a param string\n\t} else if ( params && typeof params === \"object\" ) {\n\t\ttype = \"POST\";\n\t}\n\n\t// If we have elements to modify, make the request\n\tif ( self.length > 0 ) {\n\t\tjQuery.ajax({\n\t\t\turl: url,\n\n\t\t\t// if \"type\" variable is undefined, then \"GET\" method will be used\n\t\t\ttype: type,\n\t\t\tdataType: \"html\",\n\t\t\tdata: params\n\t\t}).done(function( responseText ) {\n\n\t\t\t// Save response for use in complete callback\n\t\t\tresponse = arguments;\n\n\t\t\tself.html( selector ?\n\n\t\t\t\t// If a selector was specified, locate the right elements in a dummy div\n\t\t\t\t// Exclude scripts to avoid IE 'Permission Denied' errors\n\t\t\t\tjQuery(\"<div>\").append( jQuery.parseHTML( responseText ) ).find( selector ) :\n\n\t\t\t\t// Otherwise use the full result\n\t\t\t\tresponseText );\n\n\t\t}).complete( callback && function( jqXHR, status ) {\n\t\t\tself.each( callback, response || [ jqXHR.responseText, status, jqXHR ] );\n\t\t});\n\t}\n\n\treturn this;\n};\n\n\n\n\njQuery.expr.filters.animated = function( elem ) {\n\treturn jQuery.grep(jQuery.timers, function( fn ) {\n\t\treturn elem === fn.elem;\n\t}).length;\n};\n\n\n\n\n\nvar docElem = window.document.documentElement;\n\n/**\n * Gets a window from an element\n */\nfunction getWindow( elem ) {\n\treturn jQuery.isWindow( elem ) ?\n\t\telem :\n\t\telem.nodeType === 9 ?\n\t\t\telem.defaultView || elem.parentWindow :\n\t\t\tfalse;\n}\n\njQuery.offset = {\n\tsetOffset: function( elem, options, i ) {\n\t\tvar curPosition, curLeft, curCSSTop, curTop, curOffset, curCSSLeft, calculatePosition,\n\t\t\tposition = jQuery.css( elem, \"position\" ),\n\t\t\tcurElem = jQuery( elem ),\n\t\t\tprops = {};\n\n\t\t// set position first, in-case top/left are set even on static elem\n\t\tif ( position === \"static\" ) {\n\t\t\telem.style.position = \"relative\";\n\t\t}\n\n\t\tcurOffset = curElem.offset();\n\t\tcurCSSTop = jQuery.css( elem, \"top\" );\n\t\tcurCSSLeft = jQuery.css( elem, \"left\" );\n\t\tcalculatePosition = ( position === \"absolute\" || position === \"fixed\" ) &&\n\t\t\tjQuery.inArray(\"auto\", [ curCSSTop, curCSSLeft ] ) > -1;\n\n\t\t// need to be able to calculate position if either top or left is auto and position is either absolute or fixed\n\t\tif ( calculatePosition ) {\n\t\t\tcurPosition = curElem.position();\n\t\t\tcurTop = curPosition.top;\n\t\t\tcurLeft = curPosition.left;\n\t\t} else {\n\t\t\tcurTop = parseFloat( curCSSTop ) || 0;\n\t\t\tcurLeft = parseFloat( curCSSLeft ) || 0;\n\t\t}\n\n\t\tif ( jQuery.isFunction( options ) ) {\n\t\t\toptions = options.call( elem, i, curOffset );\n\t\t}\n\n\t\tif ( options.top != null ) {\n\t\t\tprops.top = ( options.top - curOffset.top ) + curTop;\n\t\t}\n\t\tif ( options.left != null ) {\n\t\t\tprops.left = ( options.left - curOffset.left ) + curLeft;\n\t\t}\n\n\t\tif ( \"using\" in options ) {\n\t\t\toptions.using.call( elem, props );\n\t\t} else {\n\t\t\tcurElem.css( props );\n\t\t}\n\t}\n};\n\njQuery.fn.extend({\n\toffset: function( options ) {\n\t\tif ( arguments.length ) {\n\t\t\treturn options === undefined ?\n\t\t\t\tthis :\n\t\t\t\tthis.each(function( i ) {\n\t\t\t\t\tjQuery.offset.setOffset( this, options, i );\n\t\t\t\t});\n\t\t}\n\n\t\tvar docElem, win,\n\t\t\tbox = { top: 0, left: 0 },\n\t\t\telem = this[ 0 ],\n\t\t\tdoc = elem && elem.ownerDocument;\n\n\t\tif ( !doc ) {\n\t\t\treturn;\n\t\t}\n\n\t\tdocElem = doc.documentElement;\n\n\t\t// Make sure it's not a disconnected DOM node\n\t\tif ( !jQuery.contains( docElem, elem ) ) {\n\t\t\treturn box;\n\t\t}\n\n\t\t// If we don't have gBCR, just use 0,0 rather than error\n\t\t// BlackBerry 5, iOS 3 (original iPhone)\n\t\tif ( typeof elem.getBoundingClientRect !== strundefined ) {\n\t\t\tbox = elem.getBoundingClientRect();\n\t\t}\n\t\twin = getWindow( doc );\n\t\treturn {\n\t\t\ttop: box.top  + ( win.pageYOffset || docElem.scrollTop )  - ( docElem.clientTop  || 0 ),\n\t\t\tleft: box.left + ( win.pageXOffset || docElem.scrollLeft ) - ( docElem.clientLeft || 0 )\n\t\t};\n\t},\n\n\tposition: function() {\n\t\tif ( !this[ 0 ] ) {\n\t\t\treturn;\n\t\t}\n\n\t\tvar offsetParent, offset,\n\t\t\tparentOffset = { top: 0, left: 0 },\n\t\t\telem = this[ 0 ];\n\n\t\t// fixed elements are offset from window (parentOffset = {top:0, left: 0}, because it is its only offset parent\n\t\tif ( jQuery.css( elem, \"position\" ) === \"fixed\" ) {\n\t\t\t// we assume that getBoundingClientRect is available when computed position is fixed\n\t\t\toffset = elem.getBoundingClientRect();\n\t\t} else {\n\t\t\t// Get *real* offsetParent\n\t\t\toffsetParent = this.offsetParent();\n\n\t\t\t// Get correct offsets\n\t\t\toffset = this.offset();\n\t\t\tif ( !jQuery.nodeName( offsetParent[ 0 ], \"html\" ) ) {\n\t\t\t\tparentOffset = offsetParent.offset();\n\t\t\t}\n\n\t\t\t// Add offsetParent borders\n\t\t\tparentOffset.top  += jQuery.css( offsetParent[ 0 ], \"borderTopWidth\", true );\n\t\t\tparentOffset.left += jQuery.css( offsetParent[ 0 ], \"borderLeftWidth\", true );\n\t\t}\n\n\t\t// Subtract parent offsets and element margins\n\t\t// note: when an element has margin: auto the offsetLeft and marginLeft\n\t\t// are the same in Safari causing offset.left to incorrectly be 0\n\t\treturn {\n\t\t\ttop:  offset.top  - parentOffset.top - jQuery.css( elem, \"marginTop\", true ),\n\t\t\tleft: offset.left - parentOffset.left - jQuery.css( elem, \"marginLeft\", true)\n\t\t};\n\t},\n\n\toffsetParent: function() {\n\t\treturn this.map(function() {\n\t\t\tvar offsetParent = this.offsetParent || docElem;\n\n\t\t\twhile ( offsetParent && ( !jQuery.nodeName( offsetParent, \"html\" ) && jQuery.css( offsetParent, \"position\" ) === \"static\" ) ) {\n\t\t\t\toffsetParent = offsetParent.offsetParent;\n\t\t\t}\n\t\t\treturn offsetParent || docElem;\n\t\t});\n\t}\n});\n\n// Create scrollLeft and scrollTop methods\njQuery.each( { scrollLeft: \"pageXOffset\", scrollTop: \"pageYOffset\" }, function( method, prop ) {\n\tvar top = /Y/.test( prop );\n\n\tjQuery.fn[ method ] = function( val ) {\n\t\treturn access( this, function( elem, method, val ) {\n\t\t\tvar win = getWindow( elem );\n\n\t\t\tif ( val === undefined ) {\n\t\t\t\treturn win ? (prop in win) ? win[ prop ] :\n\t\t\t\t\twin.document.documentElement[ method ] :\n\t\t\t\t\telem[ method ];\n\t\t\t}\n\n\t\t\tif ( win ) {\n\t\t\t\twin.scrollTo(\n\t\t\t\t\t!top ? val : jQuery( win ).scrollLeft(),\n\t\t\t\t\ttop ? val : jQuery( win ).scrollTop()\n\t\t\t\t);\n\n\t\t\t} else {\n\t\t\t\telem[ method ] = val;\n\t\t\t}\n\t\t}, method, val, arguments.length, null );\n\t};\n});\n\n// Add the top/left cssHooks using jQuery.fn.position\n// Webkit bug: https://bugs.webkit.org/show_bug.cgi?id=29084\n// getComputedStyle returns percent when specified for top/left/bottom/right\n// rather than make the css module depend on the offset module, we just check for it here\njQuery.each( [ \"top\", \"left\" ], function( i, prop ) {\n\tjQuery.cssHooks[ prop ] = addGetHookIf( support.pixelPosition,\n\t\tfunction( elem, computed ) {\n\t\t\tif ( computed ) {\n\t\t\t\tcomputed = curCSS( elem, prop );\n\t\t\t\t// if curCSS returns percentage, fallback to offset\n\t\t\t\treturn rnumnonpx.test( computed ) ?\n\t\t\t\t\tjQuery( elem ).position()[ prop ] + \"px\" :\n\t\t\t\t\tcomputed;\n\t\t\t}\n\t\t}\n\t);\n});\n\n\n// Create innerHeight, innerWidth, height, width, outerHeight and outerWidth methods\njQuery.each( { Height: \"height\", Width: \"width\" }, function( name, type ) {\n\tjQuery.each( { padding: \"inner\" + name, content: type, \"\": \"outer\" + name }, function( defaultExtra, funcName ) {\n\t\t// margin is only for outerHeight, outerWidth\n\t\tjQuery.fn[ funcName ] = function( margin, value ) {\n\t\t\tvar chainable = arguments.length && ( defaultExtra || typeof margin !== \"boolean\" ),\n\t\t\t\textra = defaultExtra || ( margin === true || value === true ? \"margin\" : \"border\" );\n\n\t\t\treturn access( this, function( elem, type, value ) {\n\t\t\t\tvar doc;\n\n\t\t\t\tif ( jQuery.isWindow( elem ) ) {\n\t\t\t\t\t// As of 5/8/2012 this will yield incorrect results for Mobile Safari, but there\n\t\t\t\t\t// isn't a whole lot we can do. See pull request at this URL for discussion:\n\t\t\t\t\t// https://github.com/jquery/jquery/pull/764\n\t\t\t\t\treturn elem.document.documentElement[ \"client\" + name ];\n\t\t\t\t}\n\n\t\t\t\t// Get document width or height\n\t\t\t\tif ( elem.nodeType === 9 ) {\n\t\t\t\t\tdoc = elem.documentElement;\n\n\t\t\t\t\t// Either scroll[Width/Height] or offset[Width/Height] or client[Width/Height], whichever is greatest\n\t\t\t\t\t// unfortunately, this causes bug #3838 in IE6/8 only, but there is currently no good, small way to fix it.\n\t\t\t\t\treturn Math.max(\n\t\t\t\t\t\telem.body[ \"scroll\" + name ], doc[ \"scroll\" + name ],\n\t\t\t\t\t\telem.body[ \"offset\" + name ], doc[ \"offset\" + name ],\n\t\t\t\t\t\tdoc[ \"client\" + name ]\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\treturn value === undefined ?\n\t\t\t\t\t// Get width or height on the element, requesting but not forcing parseFloat\n\t\t\t\t\tjQuery.css( elem, type, extra ) :\n\n\t\t\t\t\t// Set width or height on the element\n\t\t\t\t\tjQuery.style( elem, type, value, extra );\n\t\t\t}, type, chainable ? margin : undefined, chainable, null );\n\t\t};\n\t});\n});\n\n\n// The number of elements contained in the matched element set\njQuery.fn.size = function() {\n\treturn this.length;\n};\n\njQuery.fn.andSelf = jQuery.fn.addBack;\n\n\n\n\n// Register as a named AMD module, since jQuery can be concatenated with other\n// files that may use define, but not via a proper concatenation script that\n// understands anonymous AMD modules. A named AMD is safest and most robust\n// way to register. Lowercase jquery is used because AMD module names are\n// derived from file names, and jQuery is normally delivered in a lowercase\n// file name. Do this after creating the global so that if an AMD module wants\n// to call noConflict to hide this version of jQuery, it will work.\n\n// Note that for maximum portability, libraries that are not jQuery should\n// declare themselves as anonymous modules, and avoid setting a global if an\n// AMD loader is present. jQuery is a special case. For more information, see\n// https://github.com/jrburke/requirejs/wiki/Updating-existing-libraries#wiki-anon\n\nif ( typeof define === \"function\" && define.amd ) {\n\tdefine( \"jquery\", [], function() {\n\t\treturn jQuery;\n\t});\n}\n\n\n\n\nvar\n\t// Map over jQuery in case of overwrite\n\t_jQuery = window.jQuery,\n\n\t// Map over the $ in case of overwrite\n\t_$ = window.$;\n\njQuery.noConflict = function( deep ) {\n\tif ( window.$ === jQuery ) {\n\t\twindow.$ = _$;\n\t}\n\n\tif ( deep && window.jQuery === jQuery ) {\n\t\twindow.jQuery = _jQuery;\n\t}\n\n\treturn jQuery;\n};\n\n// Expose jQuery and $ identifiers, even in\n// AMD (#7102#comment:10, https://github.com/jquery/jquery/pull/557)\n// and CommonJS for browser emulators (#13566)\nif ( typeof noGlobal === strundefined ) {\n\twindow.jQuery = window.$ = jQuery;\n}\n\n\n\n\nreturn jQuery;\n\n}));\n"
  },
  {
    "path": "r2/r2/public/static/js/lib/jquery-2.1.1.js",
    "content": "/*!\n * jQuery JavaScript Library v2.1.1\n * http://jquery.com/\n *\n * Includes Sizzle.js\n * http://sizzlejs.com/\n *\n * Copyright 2005, 2014 jQuery Foundation, Inc. and other contributors\n * Released under the MIT license\n * http://jquery.org/license\n *\n * Date: 2014-05-01T17:11Z\n */\n\n(function( global, factory ) {\n\n\tif ( typeof module === \"object\" && typeof module.exports === \"object\" ) {\n\t\t// For CommonJS and CommonJS-like environments where a proper window is present,\n\t\t// execute the factory and get jQuery\n\t\t// For environments that do not inherently posses a window with a document\n\t\t// (such as Node.js), expose a jQuery-making factory as module.exports\n\t\t// This accentuates the need for the creation of a real window\n\t\t// e.g. var jQuery = require(\"jquery\")(window);\n\t\t// See ticket #14549 for more info\n\t\tmodule.exports = global.document ?\n\t\t\tfactory( global, true ) :\n\t\t\tfunction( w ) {\n\t\t\t\tif ( !w.document ) {\n\t\t\t\t\tthrow new Error( \"jQuery requires a window with a document\" );\n\t\t\t\t}\n\t\t\t\treturn factory( w );\n\t\t\t};\n\t} else {\n\t\tfactory( global );\n\t}\n\n// Pass this if window is not defined yet\n}(typeof window !== \"undefined\" ? window : this, function( window, noGlobal ) {\n\n// Can't do this because several apps including ASP.NET trace\n// the stack via arguments.caller.callee and Firefox dies if\n// you try to trace through \"use strict\" call chains. (#13335)\n// Support: Firefox 18+\n//\n\nvar arr = [];\n\nvar slice = arr.slice;\n\nvar concat = arr.concat;\n\nvar push = arr.push;\n\nvar indexOf = arr.indexOf;\n\nvar class2type = {};\n\nvar toString = class2type.toString;\n\nvar hasOwn = class2type.hasOwnProperty;\n\nvar support = {};\n\n\n\nvar\n\t// Use the correct document accordingly with window argument (sandbox)\n\tdocument = window.document,\n\n\tversion = \"2.1.1\",\n\n\t// Define a local copy of jQuery\n\tjQuery = function( selector, context ) {\n\t\t// The jQuery object is actually just the init constructor 'enhanced'\n\t\t// Need init if jQuery is called (just allow error to be thrown if not included)\n\t\treturn new jQuery.fn.init( selector, context );\n\t},\n\n\t// Support: Android<4.1\n\t// Make sure we trim BOM and NBSP\n\trtrim = /^[\\s\\uFEFF\\xA0]+|[\\s\\uFEFF\\xA0]+$/g,\n\n\t// Matches dashed string for camelizing\n\trmsPrefix = /^-ms-/,\n\trdashAlpha = /-([\\da-z])/gi,\n\n\t// Used by jQuery.camelCase as callback to replace()\n\tfcamelCase = function( all, letter ) {\n\t\treturn letter.toUpperCase();\n\t};\n\njQuery.fn = jQuery.prototype = {\n\t// The current version of jQuery being used\n\tjquery: version,\n\n\tconstructor: jQuery,\n\n\t// Start with an empty selector\n\tselector: \"\",\n\n\t// The default length of a jQuery object is 0\n\tlength: 0,\n\n\ttoArray: function() {\n\t\treturn slice.call( this );\n\t},\n\n\t// Get the Nth element in the matched element set OR\n\t// Get the whole matched element set as a clean array\n\tget: function( num ) {\n\t\treturn num != null ?\n\n\t\t\t// Return just the one element from the set\n\t\t\t( num < 0 ? this[ num + this.length ] : this[ num ] ) :\n\n\t\t\t// Return all the elements in a clean array\n\t\t\tslice.call( this );\n\t},\n\n\t// Take an array of elements and push it onto the stack\n\t// (returning the new matched element set)\n\tpushStack: function( elems ) {\n\n\t\t// Build a new jQuery matched element set\n\t\tvar ret = jQuery.merge( this.constructor(), elems );\n\n\t\t// Add the old object onto the stack (as a reference)\n\t\tret.prevObject = this;\n\t\tret.context = this.context;\n\n\t\t// Return the newly-formed element set\n\t\treturn ret;\n\t},\n\n\t// Execute a callback for every element in the matched set.\n\t// (You can seed the arguments with an array of args, but this is\n\t// only used internally.)\n\teach: function( callback, args ) {\n\t\treturn jQuery.each( this, callback, args );\n\t},\n\n\tmap: function( callback ) {\n\t\treturn this.pushStack( jQuery.map(this, function( elem, i ) {\n\t\t\treturn callback.call( elem, i, elem );\n\t\t}));\n\t},\n\n\tslice: function() {\n\t\treturn this.pushStack( slice.apply( this, arguments ) );\n\t},\n\n\tfirst: function() {\n\t\treturn this.eq( 0 );\n\t},\n\n\tlast: function() {\n\t\treturn this.eq( -1 );\n\t},\n\n\teq: function( i ) {\n\t\tvar len = this.length,\n\t\t\tj = +i + ( i < 0 ? len : 0 );\n\t\treturn this.pushStack( j >= 0 && j < len ? [ this[j] ] : [] );\n\t},\n\n\tend: function() {\n\t\treturn this.prevObject || this.constructor(null);\n\t},\n\n\t// For internal use only.\n\t// Behaves like an Array's method, not like a jQuery method.\n\tpush: push,\n\tsort: arr.sort,\n\tsplice: arr.splice\n};\n\njQuery.extend = jQuery.fn.extend = function() {\n\tvar options, name, src, copy, copyIsArray, clone,\n\t\ttarget = arguments[0] || {},\n\t\ti = 1,\n\t\tlength = arguments.length,\n\t\tdeep = false;\n\n\t// Handle a deep copy situation\n\tif ( typeof target === \"boolean\" ) {\n\t\tdeep = target;\n\n\t\t// skip the boolean and the target\n\t\ttarget = arguments[ i ] || {};\n\t\ti++;\n\t}\n\n\t// Handle case when target is a string or something (possible in deep copy)\n\tif ( typeof target !== \"object\" && !jQuery.isFunction(target) ) {\n\t\ttarget = {};\n\t}\n\n\t// extend jQuery itself if only one argument is passed\n\tif ( i === length ) {\n\t\ttarget = this;\n\t\ti--;\n\t}\n\n\tfor ( ; i < length; i++ ) {\n\t\t// Only deal with non-null/undefined values\n\t\tif ( (options = arguments[ i ]) != null ) {\n\t\t\t// Extend the base object\n\t\t\tfor ( name in options ) {\n\t\t\t\tsrc = target[ name ];\n\t\t\t\tcopy = options[ name ];\n\n\t\t\t\t// Prevent never-ending loop\n\t\t\t\tif ( target === copy ) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t// Recurse if we're merging plain objects or arrays\n\t\t\t\tif ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) {\n\t\t\t\t\tif ( copyIsArray ) {\n\t\t\t\t\t\tcopyIsArray = false;\n\t\t\t\t\t\tclone = src && jQuery.isArray(src) ? src : [];\n\n\t\t\t\t\t} else {\n\t\t\t\t\t\tclone = src && jQuery.isPlainObject(src) ? src : {};\n\t\t\t\t\t}\n\n\t\t\t\t\t// Never move original objects, clone them\n\t\t\t\t\ttarget[ name ] = jQuery.extend( deep, clone, copy );\n\n\t\t\t\t// Don't bring in undefined values\n\t\t\t\t} else if ( copy !== undefined ) {\n\t\t\t\t\ttarget[ name ] = copy;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Return the modified object\n\treturn target;\n};\n\njQuery.extend({\n\t// Unique for each copy of jQuery on the page\n\texpando: \"jQuery\" + ( version + Math.random() ).replace( /\\D/g, \"\" ),\n\n\t// Assume jQuery is ready without the ready module\n\tisReady: true,\n\n\terror: function( msg ) {\n\t\tthrow new Error( msg );\n\t},\n\n\tnoop: function() {},\n\n\t// See test/unit/core.js for details concerning isFunction.\n\t// Since version 1.3, DOM methods and functions like alert\n\t// aren't supported. They return false on IE (#2968).\n\tisFunction: function( obj ) {\n\t\treturn jQuery.type(obj) === \"function\";\n\t},\n\n\tisArray: Array.isArray,\n\n\tisWindow: function( obj ) {\n\t\treturn obj != null && obj === obj.window;\n\t},\n\n\tisNumeric: function( obj ) {\n\t\t// parseFloat NaNs numeric-cast false positives (null|true|false|\"\")\n\t\t// ...but misinterprets leading-number strings, particularly hex literals (\"0x...\")\n\t\t// subtraction forces infinities to NaN\n\t\treturn !jQuery.isArray( obj ) && obj - parseFloat( obj ) >= 0;\n\t},\n\n\tisPlainObject: function( obj ) {\n\t\t// Not plain objects:\n\t\t// - Any object or value whose internal [[Class]] property is not \"[object Object]\"\n\t\t// - DOM nodes\n\t\t// - window\n\t\tif ( jQuery.type( obj ) !== \"object\" || obj.nodeType || jQuery.isWindow( obj ) ) {\n\t\t\treturn false;\n\t\t}\n\n\t\tif ( obj.constructor &&\n\t\t\t\t!hasOwn.call( obj.constructor.prototype, \"isPrototypeOf\" ) ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// If the function hasn't returned already, we're confident that\n\t\t// |obj| is a plain object, created by {} or constructed with new Object\n\t\treturn true;\n\t},\n\n\tisEmptyObject: function( obj ) {\n\t\tvar name;\n\t\tfor ( name in obj ) {\n\t\t\treturn false;\n\t\t}\n\t\treturn true;\n\t},\n\n\ttype: function( obj ) {\n\t\tif ( obj == null ) {\n\t\t\treturn obj + \"\";\n\t\t}\n\t\t// Support: Android < 4.0, iOS < 6 (functionish RegExp)\n\t\treturn typeof obj === \"object\" || typeof obj === \"function\" ?\n\t\t\tclass2type[ toString.call(obj) ] || \"object\" :\n\t\t\ttypeof obj;\n\t},\n\n\t// Evaluates a script in a global context\n\tglobalEval: function( code ) {\n\t\tvar script,\n\t\t\tindirect = eval;\n\n\t\tcode = jQuery.trim( code );\n\n\t\tif ( code ) {\n\t\t\t// If the code includes a valid, prologue position\n\t\t\t// strict mode pragma, execute code by injecting a\n\t\t\t// script tag into the document.\n\t\t\tif ( code.indexOf(\"use strict\") === 1 ) {\n\t\t\t\tscript = document.createElement(\"script\");\n\t\t\t\tscript.text = code;\n\t\t\t\tdocument.head.appendChild( script ).parentNode.removeChild( script );\n\t\t\t} else {\n\t\t\t// Otherwise, avoid the DOM node creation, insertion\n\t\t\t// and removal by using an indirect global eval\n\t\t\t\tindirect( code );\n\t\t\t}\n\t\t}\n\t},\n\n\t// Convert dashed to camelCase; used by the css and data modules\n\t// Microsoft forgot to hump their vendor prefix (#9572)\n\tcamelCase: function( string ) {\n\t\treturn string.replace( rmsPrefix, \"ms-\" ).replace( rdashAlpha, fcamelCase );\n\t},\n\n\tnodeName: function( elem, name ) {\n\t\treturn elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase();\n\t},\n\n\t// args is for internal usage only\n\teach: function( obj, callback, args ) {\n\t\tvar value,\n\t\t\ti = 0,\n\t\t\tlength = obj.length,\n\t\t\tisArray = isArraylike( obj );\n\n\t\tif ( args ) {\n\t\t\tif ( isArray ) {\n\t\t\t\tfor ( ; i < length; i++ ) {\n\t\t\t\t\tvalue = callback.apply( obj[ i ], args );\n\n\t\t\t\t\tif ( value === false ) {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tfor ( i in obj ) {\n\t\t\t\t\tvalue = callback.apply( obj[ i ], args );\n\n\t\t\t\t\tif ( value === false ) {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t// A special, fast, case for the most common use of each\n\t\t} else {\n\t\t\tif ( isArray ) {\n\t\t\t\tfor ( ; i < length; i++ ) {\n\t\t\t\t\tvalue = callback.call( obj[ i ], i, obj[ i ] );\n\n\t\t\t\t\tif ( value === false ) {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tfor ( i in obj ) {\n\t\t\t\t\tvalue = callback.call( obj[ i ], i, obj[ i ] );\n\n\t\t\t\t\tif ( value === false ) {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn obj;\n\t},\n\n\t// Support: Android<4.1\n\ttrim: function( text ) {\n\t\treturn text == null ?\n\t\t\t\"\" :\n\t\t\t( text + \"\" ).replace( rtrim, \"\" );\n\t},\n\n\t// results is for internal usage only\n\tmakeArray: function( arr, results ) {\n\t\tvar ret = results || [];\n\n\t\tif ( arr != null ) {\n\t\t\tif ( isArraylike( Object(arr) ) ) {\n\t\t\t\tjQuery.merge( ret,\n\t\t\t\t\ttypeof arr === \"string\" ?\n\t\t\t\t\t[ arr ] : arr\n\t\t\t\t);\n\t\t\t} else {\n\t\t\t\tpush.call( ret, arr );\n\t\t\t}\n\t\t}\n\n\t\treturn ret;\n\t},\n\n\tinArray: function( elem, arr, i ) {\n\t\treturn arr == null ? -1 : indexOf.call( arr, elem, i );\n\t},\n\n\tmerge: function( first, second ) {\n\t\tvar len = +second.length,\n\t\t\tj = 0,\n\t\t\ti = first.length;\n\n\t\tfor ( ; j < len; j++ ) {\n\t\t\tfirst[ i++ ] = second[ j ];\n\t\t}\n\n\t\tfirst.length = i;\n\n\t\treturn first;\n\t},\n\n\tgrep: function( elems, callback, invert ) {\n\t\tvar callbackInverse,\n\t\t\tmatches = [],\n\t\t\ti = 0,\n\t\t\tlength = elems.length,\n\t\t\tcallbackExpect = !invert;\n\n\t\t// Go through the array, only saving the items\n\t\t// that pass the validator function\n\t\tfor ( ; i < length; i++ ) {\n\t\t\tcallbackInverse = !callback( elems[ i ], i );\n\t\t\tif ( callbackInverse !== callbackExpect ) {\n\t\t\t\tmatches.push( elems[ i ] );\n\t\t\t}\n\t\t}\n\n\t\treturn matches;\n\t},\n\n\t// arg is for internal usage only\n\tmap: function( elems, callback, arg ) {\n\t\tvar value,\n\t\t\ti = 0,\n\t\t\tlength = elems.length,\n\t\t\tisArray = isArraylike( elems ),\n\t\t\tret = [];\n\n\t\t// Go through the array, translating each of the items to their new values\n\t\tif ( isArray ) {\n\t\t\tfor ( ; i < length; i++ ) {\n\t\t\t\tvalue = callback( elems[ i ], i, arg );\n\n\t\t\t\tif ( value != null ) {\n\t\t\t\t\tret.push( value );\n\t\t\t\t}\n\t\t\t}\n\n\t\t// Go through every key on the object,\n\t\t} else {\n\t\t\tfor ( i in elems ) {\n\t\t\t\tvalue = callback( elems[ i ], i, arg );\n\n\t\t\t\tif ( value != null ) {\n\t\t\t\t\tret.push( value );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Flatten any nested arrays\n\t\treturn concat.apply( [], ret );\n\t},\n\n\t// A global GUID counter for objects\n\tguid: 1,\n\n\t// Bind a function to a context, optionally partially applying any\n\t// arguments.\n\tproxy: function( fn, context ) {\n\t\tvar tmp, args, proxy;\n\n\t\tif ( typeof context === \"string\" ) {\n\t\t\ttmp = fn[ context ];\n\t\t\tcontext = fn;\n\t\t\tfn = tmp;\n\t\t}\n\n\t\t// Quick check to determine if target is callable, in the spec\n\t\t// this throws a TypeError, but we will just return undefined.\n\t\tif ( !jQuery.isFunction( fn ) ) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\t// Simulated bind\n\t\targs = slice.call( arguments, 2 );\n\t\tproxy = function() {\n\t\t\treturn fn.apply( context || this, args.concat( slice.call( arguments ) ) );\n\t\t};\n\n\t\t// Set the guid of unique handler to the same of original handler, so it can be removed\n\t\tproxy.guid = fn.guid = fn.guid || jQuery.guid++;\n\n\t\treturn proxy;\n\t},\n\n\tnow: Date.now,\n\n\t// jQuery.support is not used in Core but other projects attach their\n\t// properties to it so it needs to exist.\n\tsupport: support\n});\n\n// Populate the class2type map\njQuery.each(\"Boolean Number String Function Array Date RegExp Object Error\".split(\" \"), function(i, name) {\n\tclass2type[ \"[object \" + name + \"]\" ] = name.toLowerCase();\n});\n\nfunction isArraylike( obj ) {\n\tvar length = obj.length,\n\t\ttype = jQuery.type( obj );\n\n\tif ( type === \"function\" || jQuery.isWindow( obj ) ) {\n\t\treturn false;\n\t}\n\n\tif ( obj.nodeType === 1 && length ) {\n\t\treturn true;\n\t}\n\n\treturn type === \"array\" || length === 0 ||\n\t\ttypeof length === \"number\" && length > 0 && ( length - 1 ) in obj;\n}\nvar Sizzle =\n/*!\n * Sizzle CSS Selector Engine v1.10.19\n * http://sizzlejs.com/\n *\n * Copyright 2013 jQuery Foundation, Inc. and other contributors\n * Released under the MIT license\n * http://jquery.org/license\n *\n * Date: 2014-04-18\n */\n(function( window ) {\n\nvar i,\n\tsupport,\n\tExpr,\n\tgetText,\n\tisXML,\n\ttokenize,\n\tcompile,\n\tselect,\n\toutermostContext,\n\tsortInput,\n\thasDuplicate,\n\n\t// Local document vars\n\tsetDocument,\n\tdocument,\n\tdocElem,\n\tdocumentIsHTML,\n\trbuggyQSA,\n\trbuggyMatches,\n\tmatches,\n\tcontains,\n\n\t// Instance-specific data\n\texpando = \"sizzle\" + -(new Date()),\n\tpreferredDoc = window.document,\n\tdirruns = 0,\n\tdone = 0,\n\tclassCache = createCache(),\n\ttokenCache = createCache(),\n\tcompilerCache = createCache(),\n\tsortOrder = function( a, b ) {\n\t\tif ( a === b ) {\n\t\t\thasDuplicate = true;\n\t\t}\n\t\treturn 0;\n\t},\n\n\t// General-purpose constants\n\tstrundefined = typeof undefined,\n\tMAX_NEGATIVE = 1 << 31,\n\n\t// Instance methods\n\thasOwn = ({}).hasOwnProperty,\n\tarr = [],\n\tpop = arr.pop,\n\tpush_native = arr.push,\n\tpush = arr.push,\n\tslice = arr.slice,\n\t// Use a stripped-down indexOf if we can't use a native one\n\tindexOf = arr.indexOf || function( elem ) {\n\t\tvar i = 0,\n\t\t\tlen = this.length;\n\t\tfor ( ; i < len; i++ ) {\n\t\t\tif ( this[i] === elem ) {\n\t\t\t\treturn i;\n\t\t\t}\n\t\t}\n\t\treturn -1;\n\t},\n\n\tbooleans = \"checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped\",\n\n\t// Regular expressions\n\n\t// Whitespace characters http://www.w3.org/TR/css3-selectors/#whitespace\n\twhitespace = \"[\\\\x20\\\\t\\\\r\\\\n\\\\f]\",\n\t// http://www.w3.org/TR/css3-syntax/#characters\n\tcharacterEncoding = \"(?:\\\\\\\\.|[\\\\w-]|[^\\\\x00-\\\\xa0])+\",\n\n\t// Loosely modeled on CSS identifier characters\n\t// An unquoted value should be a CSS identifier http://www.w3.org/TR/css3-selectors/#attribute-selectors\n\t// Proper syntax: http://www.w3.org/TR/CSS21/syndata.html#value-def-identifier\n\tidentifier = characterEncoding.replace( \"w\", \"w#\" ),\n\n\t// Attribute selectors: http://www.w3.org/TR/selectors/#attribute-selectors\n\tattributes = \"\\\\[\" + whitespace + \"*(\" + characterEncoding + \")(?:\" + whitespace +\n\t\t// Operator (capture 2)\n\t\t\"*([*^$|!~]?=)\" + whitespace +\n\t\t// \"Attribute values must be CSS identifiers [capture 5] or strings [capture 3 or capture 4]\"\n\t\t\"*(?:'((?:\\\\\\\\.|[^\\\\\\\\'])*)'|\\\"((?:\\\\\\\\.|[^\\\\\\\\\\\"])*)\\\"|(\" + identifier + \"))|)\" + whitespace +\n\t\t\"*\\\\]\",\n\n\tpseudos = \":(\" + characterEncoding + \")(?:\\\\((\" +\n\t\t// To reduce the number of selectors needing tokenize in the preFilter, prefer arguments:\n\t\t// 1. quoted (capture 3; capture 4 or capture 5)\n\t\t\"('((?:\\\\\\\\.|[^\\\\\\\\'])*)'|\\\"((?:\\\\\\\\.|[^\\\\\\\\\\\"])*)\\\")|\" +\n\t\t// 2. simple (capture 6)\n\t\t\"((?:\\\\\\\\.|[^\\\\\\\\()[\\\\]]|\" + attributes + \")*)|\" +\n\t\t// 3. anything else (capture 2)\n\t\t\".*\" +\n\t\t\")\\\\)|)\",\n\n\t// Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter\n\trtrim = new RegExp( \"^\" + whitespace + \"+|((?:^|[^\\\\\\\\])(?:\\\\\\\\.)*)\" + whitespace + \"+$\", \"g\" ),\n\n\trcomma = new RegExp( \"^\" + whitespace + \"*,\" + whitespace + \"*\" ),\n\trcombinators = new RegExp( \"^\" + whitespace + \"*([>+~]|\" + whitespace + \")\" + whitespace + \"*\" ),\n\n\trattributeQuotes = new RegExp( \"=\" + whitespace + \"*([^\\\\]'\\\"]*?)\" + whitespace + \"*\\\\]\", \"g\" ),\n\n\trpseudo = new RegExp( pseudos ),\n\tridentifier = new RegExp( \"^\" + identifier + \"$\" ),\n\n\tmatchExpr = {\n\t\t\"ID\": new RegExp( \"^#(\" + characterEncoding + \")\" ),\n\t\t\"CLASS\": new RegExp( \"^\\\\.(\" + characterEncoding + \")\" ),\n\t\t\"TAG\": new RegExp( \"^(\" + characterEncoding.replace( \"w\", \"w*\" ) + \")\" ),\n\t\t\"ATTR\": new RegExp( \"^\" + attributes ),\n\t\t\"PSEUDO\": new RegExp( \"^\" + pseudos ),\n\t\t\"CHILD\": new RegExp( \"^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\\\(\" + whitespace +\n\t\t\t\"*(even|odd|(([+-]|)(\\\\d*)n|)\" + whitespace + \"*(?:([+-]|)\" + whitespace +\n\t\t\t\"*(\\\\d+)|))\" + whitespace + \"*\\\\)|)\", \"i\" ),\n\t\t\"bool\": new RegExp( \"^(?:\" + booleans + \")$\", \"i\" ),\n\t\t// For use in libraries implementing .is()\n\t\t// We use this for POS matching in `select`\n\t\t\"needsContext\": new RegExp( \"^\" + whitespace + \"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\\\(\" +\n\t\t\twhitespace + \"*((?:-\\\\d)?\\\\d*)\" + whitespace + \"*\\\\)|)(?=[^-]|$)\", \"i\" )\n\t},\n\n\trinputs = /^(?:input|select|textarea|button)$/i,\n\trheader = /^h\\d$/i,\n\n\trnative = /^[^{]+\\{\\s*\\[native \\w/,\n\n\t// Easily-parseable/retrievable ID or TAG or CLASS selectors\n\trquickExpr = /^(?:#([\\w-]+)|(\\w+)|\\.([\\w-]+))$/,\n\n\trsibling = /[+~]/,\n\trescape = /'|\\\\/g,\n\n\t// CSS escapes http://www.w3.org/TR/CSS21/syndata.html#escaped-characters\n\trunescape = new RegExp( \"\\\\\\\\([\\\\da-f]{1,6}\" + whitespace + \"?|(\" + whitespace + \")|.)\", \"ig\" ),\n\tfunescape = function( _, escaped, escapedWhitespace ) {\n\t\tvar high = \"0x\" + escaped - 0x10000;\n\t\t// NaN means non-codepoint\n\t\t// Support: Firefox<24\n\t\t// Workaround erroneous numeric interpretation of +\"0x\"\n\t\treturn high !== high || escapedWhitespace ?\n\t\t\tescaped :\n\t\t\thigh < 0 ?\n\t\t\t\t// BMP codepoint\n\t\t\t\tString.fromCharCode( high + 0x10000 ) :\n\t\t\t\t// Supplemental Plane codepoint (surrogate pair)\n\t\t\t\tString.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 );\n\t};\n\n// Optimize for push.apply( _, NodeList )\ntry {\n\tpush.apply(\n\t\t(arr = slice.call( preferredDoc.childNodes )),\n\t\tpreferredDoc.childNodes\n\t);\n\t// Support: Android<4.0\n\t// Detect silently failing push.apply\n\tarr[ preferredDoc.childNodes.length ].nodeType;\n} catch ( e ) {\n\tpush = { apply: arr.length ?\n\n\t\t// Leverage slice if possible\n\t\tfunction( target, els ) {\n\t\t\tpush_native.apply( target, slice.call(els) );\n\t\t} :\n\n\t\t// Support: IE<9\n\t\t// Otherwise append directly\n\t\tfunction( target, els ) {\n\t\t\tvar j = target.length,\n\t\t\t\ti = 0;\n\t\t\t// Can't trust NodeList.length\n\t\t\twhile ( (target[j++] = els[i++]) ) {}\n\t\t\ttarget.length = j - 1;\n\t\t}\n\t};\n}\n\nfunction Sizzle( selector, context, results, seed ) {\n\tvar match, elem, m, nodeType,\n\t\t// QSA vars\n\t\ti, groups, old, nid, newContext, newSelector;\n\n\tif ( ( context ? context.ownerDocument || context : preferredDoc ) !== document ) {\n\t\tsetDocument( context );\n\t}\n\n\tcontext = context || document;\n\tresults = results || [];\n\n\tif ( !selector || typeof selector !== \"string\" ) {\n\t\treturn results;\n\t}\n\n\tif ( (nodeType = context.nodeType) !== 1 && nodeType !== 9 ) {\n\t\treturn [];\n\t}\n\n\tif ( documentIsHTML && !seed ) {\n\n\t\t// Shortcuts\n\t\tif ( (match = rquickExpr.exec( selector )) ) {\n\t\t\t// Speed-up: Sizzle(\"#ID\")\n\t\t\tif ( (m = match[1]) ) {\n\t\t\t\tif ( nodeType === 9 ) {\n\t\t\t\t\telem = context.getElementById( m );\n\t\t\t\t\t// Check parentNode to catch when Blackberry 4.6 returns\n\t\t\t\t\t// nodes that are no longer in the document (jQuery #6963)\n\t\t\t\t\tif ( elem && elem.parentNode ) {\n\t\t\t\t\t\t// Handle the case where IE, Opera, and Webkit return items\n\t\t\t\t\t\t// by name instead of ID\n\t\t\t\t\t\tif ( elem.id === m ) {\n\t\t\t\t\t\t\tresults.push( elem );\n\t\t\t\t\t\t\treturn results;\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\treturn results;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// Context is not a document\n\t\t\t\t\tif ( context.ownerDocument && (elem = context.ownerDocument.getElementById( m )) &&\n\t\t\t\t\t\tcontains( context, elem ) && elem.id === m ) {\n\t\t\t\t\t\tresults.push( elem );\n\t\t\t\t\t\treturn results;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t// Speed-up: Sizzle(\"TAG\")\n\t\t\t} else if ( match[2] ) {\n\t\t\t\tpush.apply( results, context.getElementsByTagName( selector ) );\n\t\t\t\treturn results;\n\n\t\t\t// Speed-up: Sizzle(\".CLASS\")\n\t\t\t} else if ( (m = match[3]) && support.getElementsByClassName && context.getElementsByClassName ) {\n\t\t\t\tpush.apply( results, context.getElementsByClassName( m ) );\n\t\t\t\treturn results;\n\t\t\t}\n\t\t}\n\n\t\t// QSA path\n\t\tif ( support.qsa && (!rbuggyQSA || !rbuggyQSA.test( selector )) ) {\n\t\t\tnid = old = expando;\n\t\t\tnewContext = context;\n\t\t\tnewSelector = nodeType === 9 && selector;\n\n\t\t\t// qSA works strangely on Element-rooted queries\n\t\t\t// We can work around this by specifying an extra ID on the root\n\t\t\t// and working up from there (Thanks to Andrew Dupont for the technique)\n\t\t\t// IE 8 doesn't work on object elements\n\t\t\tif ( nodeType === 1 && context.nodeName.toLowerCase() !== \"object\" ) {\n\t\t\t\tgroups = tokenize( selector );\n\n\t\t\t\tif ( (old = context.getAttribute(\"id\")) ) {\n\t\t\t\t\tnid = old.replace( rescape, \"\\\\$&\" );\n\t\t\t\t} else {\n\t\t\t\t\tcontext.setAttribute( \"id\", nid );\n\t\t\t\t}\n\t\t\t\tnid = \"[id='\" + nid + \"'] \";\n\n\t\t\t\ti = groups.length;\n\t\t\t\twhile ( i-- ) {\n\t\t\t\t\tgroups[i] = nid + toSelector( groups[i] );\n\t\t\t\t}\n\t\t\t\tnewContext = rsibling.test( selector ) && testContext( context.parentNode ) || context;\n\t\t\t\tnewSelector = groups.join(\",\");\n\t\t\t}\n\n\t\t\tif ( newSelector ) {\n\t\t\t\ttry {\n\t\t\t\t\tpush.apply( results,\n\t\t\t\t\t\tnewContext.querySelectorAll( newSelector )\n\t\t\t\t\t);\n\t\t\t\t\treturn results;\n\t\t\t\t} catch(qsaError) {\n\t\t\t\t} finally {\n\t\t\t\t\tif ( !old ) {\n\t\t\t\t\t\tcontext.removeAttribute(\"id\");\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// All others\n\treturn select( selector.replace( rtrim, \"$1\" ), context, results, seed );\n}\n\n/**\n * Create key-value caches of limited size\n * @returns {Function(string, Object)} Returns the Object data after storing it on itself with\n *\tproperty name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength)\n *\tdeleting the oldest entry\n */\nfunction createCache() {\n\tvar keys = [];\n\n\tfunction cache( key, value ) {\n\t\t// Use (key + \" \") to avoid collision with native prototype properties (see Issue #157)\n\t\tif ( keys.push( key + \" \" ) > Expr.cacheLength ) {\n\t\t\t// Only keep the most recent entries\n\t\t\tdelete cache[ keys.shift() ];\n\t\t}\n\t\treturn (cache[ key + \" \" ] = value);\n\t}\n\treturn cache;\n}\n\n/**\n * Mark a function for special use by Sizzle\n * @param {Function} fn The function to mark\n */\nfunction markFunction( fn ) {\n\tfn[ expando ] = true;\n\treturn fn;\n}\n\n/**\n * Support testing using an element\n * @param {Function} fn Passed the created div and expects a boolean result\n */\nfunction assert( fn ) {\n\tvar div = document.createElement(\"div\");\n\n\ttry {\n\t\treturn !!fn( div );\n\t} catch (e) {\n\t\treturn false;\n\t} finally {\n\t\t// Remove from its parent by default\n\t\tif ( div.parentNode ) {\n\t\t\tdiv.parentNode.removeChild( div );\n\t\t}\n\t\t// release memory in IE\n\t\tdiv = null;\n\t}\n}\n\n/**\n * Adds the same handler for all of the specified attrs\n * @param {String} attrs Pipe-separated list of attributes\n * @param {Function} handler The method that will be applied\n */\nfunction addHandle( attrs, handler ) {\n\tvar arr = attrs.split(\"|\"),\n\t\ti = attrs.length;\n\n\twhile ( i-- ) {\n\t\tExpr.attrHandle[ arr[i] ] = handler;\n\t}\n}\n\n/**\n * Checks document order of two siblings\n * @param {Element} a\n * @param {Element} b\n * @returns {Number} Returns less than 0 if a precedes b, greater than 0 if a follows b\n */\nfunction siblingCheck( a, b ) {\n\tvar cur = b && a,\n\t\tdiff = cur && a.nodeType === 1 && b.nodeType === 1 &&\n\t\t\t( ~b.sourceIndex || MAX_NEGATIVE ) -\n\t\t\t( ~a.sourceIndex || MAX_NEGATIVE );\n\n\t// Use IE sourceIndex if available on both nodes\n\tif ( diff ) {\n\t\treturn diff;\n\t}\n\n\t// Check if b follows a\n\tif ( cur ) {\n\t\twhile ( (cur = cur.nextSibling) ) {\n\t\t\tif ( cur === b ) {\n\t\t\t\treturn -1;\n\t\t\t}\n\t\t}\n\t}\n\n\treturn a ? 1 : -1;\n}\n\n/**\n * Returns a function to use in pseudos for input types\n * @param {String} type\n */\nfunction createInputPseudo( type ) {\n\treturn function( elem ) {\n\t\tvar name = elem.nodeName.toLowerCase();\n\t\treturn name === \"input\" && elem.type === type;\n\t};\n}\n\n/**\n * Returns a function to use in pseudos for buttons\n * @param {String} type\n */\nfunction createButtonPseudo( type ) {\n\treturn function( elem ) {\n\t\tvar name = elem.nodeName.toLowerCase();\n\t\treturn (name === \"input\" || name === \"button\") && elem.type === type;\n\t};\n}\n\n/**\n * Returns a function to use in pseudos for positionals\n * @param {Function} fn\n */\nfunction createPositionalPseudo( fn ) {\n\treturn markFunction(function( argument ) {\n\t\targument = +argument;\n\t\treturn markFunction(function( seed, matches ) {\n\t\t\tvar j,\n\t\t\t\tmatchIndexes = fn( [], seed.length, argument ),\n\t\t\t\ti = matchIndexes.length;\n\n\t\t\t// Match elements found at the specified indexes\n\t\t\twhile ( i-- ) {\n\t\t\t\tif ( seed[ (j = matchIndexes[i]) ] ) {\n\t\t\t\t\tseed[j] = !(matches[j] = seed[j]);\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t});\n}\n\n/**\n * Checks a node for validity as a Sizzle context\n * @param {Element|Object=} context\n * @returns {Element|Object|Boolean} The input node if acceptable, otherwise a falsy value\n */\nfunction testContext( context ) {\n\treturn context && typeof context.getElementsByTagName !== strundefined && context;\n}\n\n// Expose support vars for convenience\nsupport = Sizzle.support = {};\n\n/**\n * Detects XML nodes\n * @param {Element|Object} elem An element or a document\n * @returns {Boolean} True iff elem is a non-HTML XML node\n */\nisXML = Sizzle.isXML = function( elem ) {\n\t// documentElement is verified for cases where it doesn't yet exist\n\t// (such as loading iframes in IE - #4833)\n\tvar documentElement = elem && (elem.ownerDocument || elem).documentElement;\n\treturn documentElement ? documentElement.nodeName !== \"HTML\" : false;\n};\n\n/**\n * Sets document-related variables once based on the current document\n * @param {Element|Object} [doc] An element or document object to use to set the document\n * @returns {Object} Returns the current document\n */\nsetDocument = Sizzle.setDocument = function( node ) {\n\tvar hasCompare,\n\t\tdoc = node ? node.ownerDocument || node : preferredDoc,\n\t\tparent = doc.defaultView;\n\n\t// If no document and documentElement is available, return\n\tif ( doc === document || doc.nodeType !== 9 || !doc.documentElement ) {\n\t\treturn document;\n\t}\n\n\t// Set our document\n\tdocument = doc;\n\tdocElem = doc.documentElement;\n\n\t// Support tests\n\tdocumentIsHTML = !isXML( doc );\n\n\t// Support: IE>8\n\t// If iframe document is assigned to \"document\" variable and if iframe has been reloaded,\n\t// IE will throw \"permission denied\" error when accessing \"document\" variable, see jQuery #13936\n\t// IE6-8 do not support the defaultView property so parent will be undefined\n\tif ( parent && parent !== parent.top ) {\n\t\t// IE11 does not have attachEvent, so all must suffer\n\t\tif ( parent.addEventListener ) {\n\t\t\tparent.addEventListener( \"unload\", function() {\n\t\t\t\tsetDocument();\n\t\t\t}, false );\n\t\t} else if ( parent.attachEvent ) {\n\t\t\tparent.attachEvent( \"onunload\", function() {\n\t\t\t\tsetDocument();\n\t\t\t});\n\t\t}\n\t}\n\n\t/* Attributes\n\t---------------------------------------------------------------------- */\n\n\t// Support: IE<8\n\t// Verify that getAttribute really returns attributes and not properties (excepting IE8 booleans)\n\tsupport.attributes = assert(function( div ) {\n\t\tdiv.className = \"i\";\n\t\treturn !div.getAttribute(\"className\");\n\t});\n\n\t/* getElement(s)By*\n\t---------------------------------------------------------------------- */\n\n\t// Check if getElementsByTagName(\"*\") returns only elements\n\tsupport.getElementsByTagName = assert(function( div ) {\n\t\tdiv.appendChild( doc.createComment(\"\") );\n\t\treturn !div.getElementsByTagName(\"*\").length;\n\t});\n\n\t// Check if getElementsByClassName can be trusted\n\tsupport.getElementsByClassName = rnative.test( doc.getElementsByClassName ) && assert(function( div ) {\n\t\tdiv.innerHTML = \"<div class='a'></div><div class='a i'></div>\";\n\n\t\t// Support: Safari<4\n\t\t// Catch class over-caching\n\t\tdiv.firstChild.className = \"i\";\n\t\t// Support: Opera<10\n\t\t// Catch gEBCN failure to find non-leading classes\n\t\treturn div.getElementsByClassName(\"i\").length === 2;\n\t});\n\n\t// Support: IE<10\n\t// Check if getElementById returns elements by name\n\t// The broken getElementById methods don't pick up programatically-set names,\n\t// so use a roundabout getElementsByName test\n\tsupport.getById = assert(function( div ) {\n\t\tdocElem.appendChild( div ).id = expando;\n\t\treturn !doc.getElementsByName || !doc.getElementsByName( expando ).length;\n\t});\n\n\t// ID find and filter\n\tif ( support.getById ) {\n\t\tExpr.find[\"ID\"] = function( id, context ) {\n\t\t\tif ( typeof context.getElementById !== strundefined && documentIsHTML ) {\n\t\t\t\tvar m = context.getElementById( id );\n\t\t\t\t// Check parentNode to catch when Blackberry 4.6 returns\n\t\t\t\t// nodes that are no longer in the document #6963\n\t\t\t\treturn m && m.parentNode ? [ m ] : [];\n\t\t\t}\n\t\t};\n\t\tExpr.filter[\"ID\"] = function( id ) {\n\t\t\tvar attrId = id.replace( runescape, funescape );\n\t\t\treturn function( elem ) {\n\t\t\t\treturn elem.getAttribute(\"id\") === attrId;\n\t\t\t};\n\t\t};\n\t} else {\n\t\t// Support: IE6/7\n\t\t// getElementById is not reliable as a find shortcut\n\t\tdelete Expr.find[\"ID\"];\n\n\t\tExpr.filter[\"ID\"] =  function( id ) {\n\t\t\tvar attrId = id.replace( runescape, funescape );\n\t\t\treturn function( elem ) {\n\t\t\t\tvar node = typeof elem.getAttributeNode !== strundefined && elem.getAttributeNode(\"id\");\n\t\t\t\treturn node && node.value === attrId;\n\t\t\t};\n\t\t};\n\t}\n\n\t// Tag\n\tExpr.find[\"TAG\"] = support.getElementsByTagName ?\n\t\tfunction( tag, context ) {\n\t\t\tif ( typeof context.getElementsByTagName !== strundefined ) {\n\t\t\t\treturn context.getElementsByTagName( tag );\n\t\t\t}\n\t\t} :\n\t\tfunction( tag, context ) {\n\t\t\tvar elem,\n\t\t\t\ttmp = [],\n\t\t\t\ti = 0,\n\t\t\t\tresults = context.getElementsByTagName( tag );\n\n\t\t\t// Filter out possible comments\n\t\t\tif ( tag === \"*\" ) {\n\t\t\t\twhile ( (elem = results[i++]) ) {\n\t\t\t\t\tif ( elem.nodeType === 1 ) {\n\t\t\t\t\t\ttmp.push( elem );\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\treturn tmp;\n\t\t\t}\n\t\t\treturn results;\n\t\t};\n\n\t// Class\n\tExpr.find[\"CLASS\"] = support.getElementsByClassName && function( className, context ) {\n\t\tif ( typeof context.getElementsByClassName !== strundefined && documentIsHTML ) {\n\t\t\treturn context.getElementsByClassName( className );\n\t\t}\n\t};\n\n\t/* QSA/matchesSelector\n\t---------------------------------------------------------------------- */\n\n\t// QSA and matchesSelector support\n\n\t// matchesSelector(:active) reports false when true (IE9/Opera 11.5)\n\trbuggyMatches = [];\n\n\t// qSa(:focus) reports false when true (Chrome 21)\n\t// We allow this because of a bug in IE8/9 that throws an error\n\t// whenever `document.activeElement` is accessed on an iframe\n\t// So, we allow :focus to pass through QSA all the time to avoid the IE error\n\t// See http://bugs.jquery.com/ticket/13378\n\trbuggyQSA = [];\n\n\tif ( (support.qsa = rnative.test( doc.querySelectorAll )) ) {\n\t\t// Build QSA regex\n\t\t// Regex strategy adopted from Diego Perini\n\t\tassert(function( div ) {\n\t\t\t// Select is set to empty string on purpose\n\t\t\t// This is to test IE's treatment of not explicitly\n\t\t\t// setting a boolean content attribute,\n\t\t\t// since its presence should be enough\n\t\t\t// http://bugs.jquery.com/ticket/12359\n\t\t\tdiv.innerHTML = \"<select msallowclip=''><option selected=''></option></select>\";\n\n\t\t\t// Support: IE8, Opera 11-12.16\n\t\t\t// Nothing should be selected when empty strings follow ^= or $= or *=\n\t\t\t// The test attribute must be unknown in Opera but \"safe\" for WinRT\n\t\t\t// http://msdn.microsoft.com/en-us/library/ie/hh465388.aspx#attribute_section\n\t\t\tif ( div.querySelectorAll(\"[msallowclip^='']\").length ) {\n\t\t\t\trbuggyQSA.push( \"[*^$]=\" + whitespace + \"*(?:''|\\\"\\\")\" );\n\t\t\t}\n\n\t\t\t// Support: IE8\n\t\t\t// Boolean attributes and \"value\" are not treated correctly\n\t\t\tif ( !div.querySelectorAll(\"[selected]\").length ) {\n\t\t\t\trbuggyQSA.push( \"\\\\[\" + whitespace + \"*(?:value|\" + booleans + \")\" );\n\t\t\t}\n\n\t\t\t// Webkit/Opera - :checked should return selected option elements\n\t\t\t// http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked\n\t\t\t// IE8 throws error here and will not see later tests\n\t\t\tif ( !div.querySelectorAll(\":checked\").length ) {\n\t\t\t\trbuggyQSA.push(\":checked\");\n\t\t\t}\n\t\t});\n\n\t\tassert(function( div ) {\n\t\t\t// Support: Windows 8 Native Apps\n\t\t\t// The type and name attributes are restricted during .innerHTML assignment\n\t\t\tvar input = doc.createElement(\"input\");\n\t\t\tinput.setAttribute( \"type\", \"hidden\" );\n\t\t\tdiv.appendChild( input ).setAttribute( \"name\", \"D\" );\n\n\t\t\t// Support: IE8\n\t\t\t// Enforce case-sensitivity of name attribute\n\t\t\tif ( div.querySelectorAll(\"[name=d]\").length ) {\n\t\t\t\trbuggyQSA.push( \"name\" + whitespace + \"*[*^$|!~]?=\" );\n\t\t\t}\n\n\t\t\t// FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled)\n\t\t\t// IE8 throws error here and will not see later tests\n\t\t\tif ( !div.querySelectorAll(\":enabled\").length ) {\n\t\t\t\trbuggyQSA.push( \":enabled\", \":disabled\" );\n\t\t\t}\n\n\t\t\t// Opera 10-11 does not throw on post-comma invalid pseudos\n\t\t\tdiv.querySelectorAll(\"*,:x\");\n\t\t\trbuggyQSA.push(\",.*:\");\n\t\t});\n\t}\n\n\tif ( (support.matchesSelector = rnative.test( (matches = docElem.matches ||\n\t\tdocElem.webkitMatchesSelector ||\n\t\tdocElem.mozMatchesSelector ||\n\t\tdocElem.oMatchesSelector ||\n\t\tdocElem.msMatchesSelector) )) ) {\n\n\t\tassert(function( div ) {\n\t\t\t// Check to see if it's possible to do matchesSelector\n\t\t\t// on a disconnected node (IE 9)\n\t\t\tsupport.disconnectedMatch = matches.call( div, \"div\" );\n\n\t\t\t// This should fail with an exception\n\t\t\t// Gecko does not error, returns false instead\n\t\t\tmatches.call( div, \"[s!='']:x\" );\n\t\t\trbuggyMatches.push( \"!=\", pseudos );\n\t\t});\n\t}\n\n\trbuggyQSA = rbuggyQSA.length && new RegExp( rbuggyQSA.join(\"|\") );\n\trbuggyMatches = rbuggyMatches.length && new RegExp( rbuggyMatches.join(\"|\") );\n\n\t/* Contains\n\t---------------------------------------------------------------------- */\n\thasCompare = rnative.test( docElem.compareDocumentPosition );\n\n\t// Element contains another\n\t// Purposefully does not implement inclusive descendent\n\t// As in, an element does not contain itself\n\tcontains = hasCompare || rnative.test( docElem.contains ) ?\n\t\tfunction( a, b ) {\n\t\t\tvar adown = a.nodeType === 9 ? a.documentElement : a,\n\t\t\t\tbup = b && b.parentNode;\n\t\t\treturn a === bup || !!( bup && bup.nodeType === 1 && (\n\t\t\t\tadown.contains ?\n\t\t\t\t\tadown.contains( bup ) :\n\t\t\t\t\ta.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16\n\t\t\t));\n\t\t} :\n\t\tfunction( a, b ) {\n\t\t\tif ( b ) {\n\t\t\t\twhile ( (b = b.parentNode) ) {\n\t\t\t\t\tif ( b === a ) {\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false;\n\t\t};\n\n\t/* Sorting\n\t---------------------------------------------------------------------- */\n\n\t// Document order sorting\n\tsortOrder = hasCompare ?\n\tfunction( a, b ) {\n\n\t\t// Flag for duplicate removal\n\t\tif ( a === b ) {\n\t\t\thasDuplicate = true;\n\t\t\treturn 0;\n\t\t}\n\n\t\t// Sort on method existence if only one input has compareDocumentPosition\n\t\tvar compare = !a.compareDocumentPosition - !b.compareDocumentPosition;\n\t\tif ( compare ) {\n\t\t\treturn compare;\n\t\t}\n\n\t\t// Calculate position if both inputs belong to the same document\n\t\tcompare = ( a.ownerDocument || a ) === ( b.ownerDocument || b ) ?\n\t\t\ta.compareDocumentPosition( b ) :\n\n\t\t\t// Otherwise we know they are disconnected\n\t\t\t1;\n\n\t\t// Disconnected nodes\n\t\tif ( compare & 1 ||\n\t\t\t(!support.sortDetached && b.compareDocumentPosition( a ) === compare) ) {\n\n\t\t\t// Choose the first element that is related to our preferred document\n\t\t\tif ( a === doc || a.ownerDocument === preferredDoc && contains(preferredDoc, a) ) {\n\t\t\t\treturn -1;\n\t\t\t}\n\t\t\tif ( b === doc || b.ownerDocument === preferredDoc && contains(preferredDoc, b) ) {\n\t\t\t\treturn 1;\n\t\t\t}\n\n\t\t\t// Maintain original order\n\t\t\treturn sortInput ?\n\t\t\t\t( indexOf.call( sortInput, a ) - indexOf.call( sortInput, b ) ) :\n\t\t\t\t0;\n\t\t}\n\n\t\treturn compare & 4 ? -1 : 1;\n\t} :\n\tfunction( a, b ) {\n\t\t// Exit early if the nodes are identical\n\t\tif ( a === b ) {\n\t\t\thasDuplicate = true;\n\t\t\treturn 0;\n\t\t}\n\n\t\tvar cur,\n\t\t\ti = 0,\n\t\t\taup = a.parentNode,\n\t\t\tbup = b.parentNode,\n\t\t\tap = [ a ],\n\t\t\tbp = [ b ];\n\n\t\t// Parentless nodes are either documents or disconnected\n\t\tif ( !aup || !bup ) {\n\t\t\treturn a === doc ? -1 :\n\t\t\t\tb === doc ? 1 :\n\t\t\t\taup ? -1 :\n\t\t\t\tbup ? 1 :\n\t\t\t\tsortInput ?\n\t\t\t\t( indexOf.call( sortInput, a ) - indexOf.call( sortInput, b ) ) :\n\t\t\t\t0;\n\n\t\t// If the nodes are siblings, we can do a quick check\n\t\t} else if ( aup === bup ) {\n\t\t\treturn siblingCheck( a, b );\n\t\t}\n\n\t\t// Otherwise we need full lists of their ancestors for comparison\n\t\tcur = a;\n\t\twhile ( (cur = cur.parentNode) ) {\n\t\t\tap.unshift( cur );\n\t\t}\n\t\tcur = b;\n\t\twhile ( (cur = cur.parentNode) ) {\n\t\t\tbp.unshift( cur );\n\t\t}\n\n\t\t// Walk down the tree looking for a discrepancy\n\t\twhile ( ap[i] === bp[i] ) {\n\t\t\ti++;\n\t\t}\n\n\t\treturn i ?\n\t\t\t// Do a sibling check if the nodes have a common ancestor\n\t\t\tsiblingCheck( ap[i], bp[i] ) :\n\n\t\t\t// Otherwise nodes in our document sort first\n\t\t\tap[i] === preferredDoc ? -1 :\n\t\t\tbp[i] === preferredDoc ? 1 :\n\t\t\t0;\n\t};\n\n\treturn doc;\n};\n\nSizzle.matches = function( expr, elements ) {\n\treturn Sizzle( expr, null, null, elements );\n};\n\nSizzle.matchesSelector = function( elem, expr ) {\n\t// Set document vars if needed\n\tif ( ( elem.ownerDocument || elem ) !== document ) {\n\t\tsetDocument( elem );\n\t}\n\n\t// Make sure that attribute selectors are quoted\n\texpr = expr.replace( rattributeQuotes, \"='$1']\" );\n\n\tif ( support.matchesSelector && documentIsHTML &&\n\t\t( !rbuggyMatches || !rbuggyMatches.test( expr ) ) &&\n\t\t( !rbuggyQSA     || !rbuggyQSA.test( expr ) ) ) {\n\n\t\ttry {\n\t\t\tvar ret = matches.call( elem, expr );\n\n\t\t\t// IE 9's matchesSelector returns false on disconnected nodes\n\t\t\tif ( ret || support.disconnectedMatch ||\n\t\t\t\t\t// As well, disconnected nodes are said to be in a document\n\t\t\t\t\t// fragment in IE 9\n\t\t\t\t\telem.document && elem.document.nodeType !== 11 ) {\n\t\t\t\treturn ret;\n\t\t\t}\n\t\t} catch(e) {}\n\t}\n\n\treturn Sizzle( expr, document, null, [ elem ] ).length > 0;\n};\n\nSizzle.contains = function( context, elem ) {\n\t// Set document vars if needed\n\tif ( ( context.ownerDocument || context ) !== document ) {\n\t\tsetDocument( context );\n\t}\n\treturn contains( context, elem );\n};\n\nSizzle.attr = function( elem, name ) {\n\t// Set document vars if needed\n\tif ( ( elem.ownerDocument || elem ) !== document ) {\n\t\tsetDocument( elem );\n\t}\n\n\tvar fn = Expr.attrHandle[ name.toLowerCase() ],\n\t\t// Don't get fooled by Object.prototype properties (jQuery #13807)\n\t\tval = fn && hasOwn.call( Expr.attrHandle, name.toLowerCase() ) ?\n\t\t\tfn( elem, name, !documentIsHTML ) :\n\t\t\tundefined;\n\n\treturn val !== undefined ?\n\t\tval :\n\t\tsupport.attributes || !documentIsHTML ?\n\t\t\telem.getAttribute( name ) :\n\t\t\t(val = elem.getAttributeNode(name)) && val.specified ?\n\t\t\t\tval.value :\n\t\t\t\tnull;\n};\n\nSizzle.error = function( msg ) {\n\tthrow new Error( \"Syntax error, unrecognized expression: \" + msg );\n};\n\n/**\n * Document sorting and removing duplicates\n * @param {ArrayLike} results\n */\nSizzle.uniqueSort = function( results ) {\n\tvar elem,\n\t\tduplicates = [],\n\t\tj = 0,\n\t\ti = 0;\n\n\t// Unless we *know* we can detect duplicates, assume their presence\n\thasDuplicate = !support.detectDuplicates;\n\tsortInput = !support.sortStable && results.slice( 0 );\n\tresults.sort( sortOrder );\n\n\tif ( hasDuplicate ) {\n\t\twhile ( (elem = results[i++]) ) {\n\t\t\tif ( elem === results[ i ] ) {\n\t\t\t\tj = duplicates.push( i );\n\t\t\t}\n\t\t}\n\t\twhile ( j-- ) {\n\t\t\tresults.splice( duplicates[ j ], 1 );\n\t\t}\n\t}\n\n\t// Clear input after sorting to release objects\n\t// See https://github.com/jquery/sizzle/pull/225\n\tsortInput = null;\n\n\treturn results;\n};\n\n/**\n * Utility function for retrieving the text value of an array of DOM nodes\n * @param {Array|Element} elem\n */\ngetText = Sizzle.getText = function( elem ) {\n\tvar node,\n\t\tret = \"\",\n\t\ti = 0,\n\t\tnodeType = elem.nodeType;\n\n\tif ( !nodeType ) {\n\t\t// If no nodeType, this is expected to be an array\n\t\twhile ( (node = elem[i++]) ) {\n\t\t\t// Do not traverse comment nodes\n\t\t\tret += getText( node );\n\t\t}\n\t} else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) {\n\t\t// Use textContent for elements\n\t\t// innerText usage removed for consistency of new lines (jQuery #11153)\n\t\tif ( typeof elem.textContent === \"string\" ) {\n\t\t\treturn elem.textContent;\n\t\t} else {\n\t\t\t// Traverse its children\n\t\t\tfor ( elem = elem.firstChild; elem; elem = elem.nextSibling ) {\n\t\t\t\tret += getText( elem );\n\t\t\t}\n\t\t}\n\t} else if ( nodeType === 3 || nodeType === 4 ) {\n\t\treturn elem.nodeValue;\n\t}\n\t// Do not include comment or processing instruction nodes\n\n\treturn ret;\n};\n\nExpr = Sizzle.selectors = {\n\n\t// Can be adjusted by the user\n\tcacheLength: 50,\n\n\tcreatePseudo: markFunction,\n\n\tmatch: matchExpr,\n\n\tattrHandle: {},\n\n\tfind: {},\n\n\trelative: {\n\t\t\">\": { dir: \"parentNode\", first: true },\n\t\t\" \": { dir: \"parentNode\" },\n\t\t\"+\": { dir: \"previousSibling\", first: true },\n\t\t\"~\": { dir: \"previousSibling\" }\n\t},\n\n\tpreFilter: {\n\t\t\"ATTR\": function( match ) {\n\t\t\tmatch[1] = match[1].replace( runescape, funescape );\n\n\t\t\t// Move the given value to match[3] whether quoted or unquoted\n\t\t\tmatch[3] = ( match[3] || match[4] || match[5] || \"\" ).replace( runescape, funescape );\n\n\t\t\tif ( match[2] === \"~=\" ) {\n\t\t\t\tmatch[3] = \" \" + match[3] + \" \";\n\t\t\t}\n\n\t\t\treturn match.slice( 0, 4 );\n\t\t},\n\n\t\t\"CHILD\": function( match ) {\n\t\t\t/* matches from matchExpr[\"CHILD\"]\n\t\t\t\t1 type (only|nth|...)\n\t\t\t\t2 what (child|of-type)\n\t\t\t\t3 argument (even|odd|\\d*|\\d*n([+-]\\d+)?|...)\n\t\t\t\t4 xn-component of xn+y argument ([+-]?\\d*n|)\n\t\t\t\t5 sign of xn-component\n\t\t\t\t6 x of xn-component\n\t\t\t\t7 sign of y-component\n\t\t\t\t8 y of y-component\n\t\t\t*/\n\t\t\tmatch[1] = match[1].toLowerCase();\n\n\t\t\tif ( match[1].slice( 0, 3 ) === \"nth\" ) {\n\t\t\t\t// nth-* requires argument\n\t\t\t\tif ( !match[3] ) {\n\t\t\t\t\tSizzle.error( match[0] );\n\t\t\t\t}\n\n\t\t\t\t// numeric x and y parameters for Expr.filter.CHILD\n\t\t\t\t// remember that false/true cast respectively to 0/1\n\t\t\t\tmatch[4] = +( match[4] ? match[5] + (match[6] || 1) : 2 * ( match[3] === \"even\" || match[3] === \"odd\" ) );\n\t\t\t\tmatch[5] = +( ( match[7] + match[8] ) || match[3] === \"odd\" );\n\n\t\t\t// other types prohibit arguments\n\t\t\t} else if ( match[3] ) {\n\t\t\t\tSizzle.error( match[0] );\n\t\t\t}\n\n\t\t\treturn match;\n\t\t},\n\n\t\t\"PSEUDO\": function( match ) {\n\t\t\tvar excess,\n\t\t\t\tunquoted = !match[6] && match[2];\n\n\t\t\tif ( matchExpr[\"CHILD\"].test( match[0] ) ) {\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\t// Accept quoted arguments as-is\n\t\t\tif ( match[3] ) {\n\t\t\t\tmatch[2] = match[4] || match[5] || \"\";\n\n\t\t\t// Strip excess characters from unquoted arguments\n\t\t\t} else if ( unquoted && rpseudo.test( unquoted ) &&\n\t\t\t\t// Get excess from tokenize (recursively)\n\t\t\t\t(excess = tokenize( unquoted, true )) &&\n\t\t\t\t// advance to the next closing parenthesis\n\t\t\t\t(excess = unquoted.indexOf( \")\", unquoted.length - excess ) - unquoted.length) ) {\n\n\t\t\t\t// excess is a negative index\n\t\t\t\tmatch[0] = match[0].slice( 0, excess );\n\t\t\t\tmatch[2] = unquoted.slice( 0, excess );\n\t\t\t}\n\n\t\t\t// Return only captures needed by the pseudo filter method (type and argument)\n\t\t\treturn match.slice( 0, 3 );\n\t\t}\n\t},\n\n\tfilter: {\n\n\t\t\"TAG\": function( nodeNameSelector ) {\n\t\t\tvar nodeName = nodeNameSelector.replace( runescape, funescape ).toLowerCase();\n\t\t\treturn nodeNameSelector === \"*\" ?\n\t\t\t\tfunction() { return true; } :\n\t\t\t\tfunction( elem ) {\n\t\t\t\t\treturn elem.nodeName && elem.nodeName.toLowerCase() === nodeName;\n\t\t\t\t};\n\t\t},\n\n\t\t\"CLASS\": function( className ) {\n\t\t\tvar pattern = classCache[ className + \" \" ];\n\n\t\t\treturn pattern ||\n\t\t\t\t(pattern = new RegExp( \"(^|\" + whitespace + \")\" + className + \"(\" + whitespace + \"|$)\" )) &&\n\t\t\t\tclassCache( className, function( elem ) {\n\t\t\t\t\treturn pattern.test( typeof elem.className === \"string\" && elem.className || typeof elem.getAttribute !== strundefined && elem.getAttribute(\"class\") || \"\" );\n\t\t\t\t});\n\t\t},\n\n\t\t\"ATTR\": function( name, operator, check ) {\n\t\t\treturn function( elem ) {\n\t\t\t\tvar result = Sizzle.attr( elem, name );\n\n\t\t\t\tif ( result == null ) {\n\t\t\t\t\treturn operator === \"!=\";\n\t\t\t\t}\n\t\t\t\tif ( !operator ) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\n\t\t\t\tresult += \"\";\n\n\t\t\t\treturn operator === \"=\" ? result === check :\n\t\t\t\t\toperator === \"!=\" ? result !== check :\n\t\t\t\t\toperator === \"^=\" ? check && result.indexOf( check ) === 0 :\n\t\t\t\t\toperator === \"*=\" ? check && result.indexOf( check ) > -1 :\n\t\t\t\t\toperator === \"$=\" ? check && result.slice( -check.length ) === check :\n\t\t\t\t\toperator === \"~=\" ? ( \" \" + result + \" \" ).indexOf( check ) > -1 :\n\t\t\t\t\toperator === \"|=\" ? result === check || result.slice( 0, check.length + 1 ) === check + \"-\" :\n\t\t\t\t\tfalse;\n\t\t\t};\n\t\t},\n\n\t\t\"CHILD\": function( type, what, argument, first, last ) {\n\t\t\tvar simple = type.slice( 0, 3 ) !== \"nth\",\n\t\t\t\tforward = type.slice( -4 ) !== \"last\",\n\t\t\t\tofType = what === \"of-type\";\n\n\t\t\treturn first === 1 && last === 0 ?\n\n\t\t\t\t// Shortcut for :nth-*(n)\n\t\t\t\tfunction( elem ) {\n\t\t\t\t\treturn !!elem.parentNode;\n\t\t\t\t} :\n\n\t\t\t\tfunction( elem, context, xml ) {\n\t\t\t\t\tvar cache, outerCache, node, diff, nodeIndex, start,\n\t\t\t\t\t\tdir = simple !== forward ? \"nextSibling\" : \"previousSibling\",\n\t\t\t\t\t\tparent = elem.parentNode,\n\t\t\t\t\t\tname = ofType && elem.nodeName.toLowerCase(),\n\t\t\t\t\t\tuseCache = !xml && !ofType;\n\n\t\t\t\t\tif ( parent ) {\n\n\t\t\t\t\t\t// :(first|last|only)-(child|of-type)\n\t\t\t\t\t\tif ( simple ) {\n\t\t\t\t\t\t\twhile ( dir ) {\n\t\t\t\t\t\t\t\tnode = elem;\n\t\t\t\t\t\t\t\twhile ( (node = node[ dir ]) ) {\n\t\t\t\t\t\t\t\t\tif ( ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1 ) {\n\t\t\t\t\t\t\t\t\t\treturn false;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t// Reverse direction for :only-* (if we haven't yet done so)\n\t\t\t\t\t\t\t\tstart = dir = type === \"only\" && !start && \"nextSibling\";\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn true;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tstart = [ forward ? parent.firstChild : parent.lastChild ];\n\n\t\t\t\t\t\t// non-xml :nth-child(...) stores cache data on `parent`\n\t\t\t\t\t\tif ( forward && useCache ) {\n\t\t\t\t\t\t\t// Seek `elem` from a previously-cached index\n\t\t\t\t\t\t\touterCache = parent[ expando ] || (parent[ expando ] = {});\n\t\t\t\t\t\t\tcache = outerCache[ type ] || [];\n\t\t\t\t\t\t\tnodeIndex = cache[0] === dirruns && cache[1];\n\t\t\t\t\t\t\tdiff = cache[0] === dirruns && cache[2];\n\t\t\t\t\t\t\tnode = nodeIndex && parent.childNodes[ nodeIndex ];\n\n\t\t\t\t\t\t\twhile ( (node = ++nodeIndex && node && node[ dir ] ||\n\n\t\t\t\t\t\t\t\t// Fallback to seeking `elem` from the start\n\t\t\t\t\t\t\t\t(diff = nodeIndex = 0) || start.pop()) ) {\n\n\t\t\t\t\t\t\t\t// When found, cache indexes on `parent` and break\n\t\t\t\t\t\t\t\tif ( node.nodeType === 1 && ++diff && node === elem ) {\n\t\t\t\t\t\t\t\t\touterCache[ type ] = [ dirruns, nodeIndex, diff ];\n\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Use previously-cached element index if available\n\t\t\t\t\t\t} else if ( useCache && (cache = (elem[ expando ] || (elem[ expando ] = {}))[ type ]) && cache[0] === dirruns ) {\n\t\t\t\t\t\t\tdiff = cache[1];\n\n\t\t\t\t\t\t// xml :nth-child(...) or :nth-last-child(...) or :nth(-last)?-of-type(...)\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// Use the same loop as above to seek `elem` from the start\n\t\t\t\t\t\t\twhile ( (node = ++nodeIndex && node && node[ dir ] ||\n\t\t\t\t\t\t\t\t(diff = nodeIndex = 0) || start.pop()) ) {\n\n\t\t\t\t\t\t\t\tif ( ( ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1 ) && ++diff ) {\n\t\t\t\t\t\t\t\t\t// Cache the index of each encountered element\n\t\t\t\t\t\t\t\t\tif ( useCache ) {\n\t\t\t\t\t\t\t\t\t\t(node[ expando ] || (node[ expando ] = {}))[ type ] = [ dirruns, diff ];\n\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\tif ( node === elem ) {\n\t\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Incorporate the offset, then check against cycle size\n\t\t\t\t\t\tdiff -= last;\n\t\t\t\t\t\treturn diff === first || ( diff % first === 0 && diff / first >= 0 );\n\t\t\t\t\t}\n\t\t\t\t};\n\t\t},\n\n\t\t\"PSEUDO\": function( pseudo, argument ) {\n\t\t\t// pseudo-class names are case-insensitive\n\t\t\t// http://www.w3.org/TR/selectors/#pseudo-classes\n\t\t\t// Prioritize by case sensitivity in case custom pseudos are added with uppercase letters\n\t\t\t// Remember that setFilters inherits from pseudos\n\t\t\tvar args,\n\t\t\t\tfn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] ||\n\t\t\t\t\tSizzle.error( \"unsupported pseudo: \" + pseudo );\n\n\t\t\t// The user may use createPseudo to indicate that\n\t\t\t// arguments are needed to create the filter function\n\t\t\t// just as Sizzle does\n\t\t\tif ( fn[ expando ] ) {\n\t\t\t\treturn fn( argument );\n\t\t\t}\n\n\t\t\t// But maintain support for old signatures\n\t\t\tif ( fn.length > 1 ) {\n\t\t\t\targs = [ pseudo, pseudo, \"\", argument ];\n\t\t\t\treturn Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ?\n\t\t\t\t\tmarkFunction(function( seed, matches ) {\n\t\t\t\t\t\tvar idx,\n\t\t\t\t\t\t\tmatched = fn( seed, argument ),\n\t\t\t\t\t\t\ti = matched.length;\n\t\t\t\t\t\twhile ( i-- ) {\n\t\t\t\t\t\t\tidx = indexOf.call( seed, matched[i] );\n\t\t\t\t\t\t\tseed[ idx ] = !( matches[ idx ] = matched[i] );\n\t\t\t\t\t\t}\n\t\t\t\t\t}) :\n\t\t\t\t\tfunction( elem ) {\n\t\t\t\t\t\treturn fn( elem, 0, args );\n\t\t\t\t\t};\n\t\t\t}\n\n\t\t\treturn fn;\n\t\t}\n\t},\n\n\tpseudos: {\n\t\t// Potentially complex pseudos\n\t\t\"not\": markFunction(function( selector ) {\n\t\t\t// Trim the selector passed to compile\n\t\t\t// to avoid treating leading and trailing\n\t\t\t// spaces as combinators\n\t\t\tvar input = [],\n\t\t\t\tresults = [],\n\t\t\t\tmatcher = compile( selector.replace( rtrim, \"$1\" ) );\n\n\t\t\treturn matcher[ expando ] ?\n\t\t\t\tmarkFunction(function( seed, matches, context, xml ) {\n\t\t\t\t\tvar elem,\n\t\t\t\t\t\tunmatched = matcher( seed, null, xml, [] ),\n\t\t\t\t\t\ti = seed.length;\n\n\t\t\t\t\t// Match elements unmatched by `matcher`\n\t\t\t\t\twhile ( i-- ) {\n\t\t\t\t\t\tif ( (elem = unmatched[i]) ) {\n\t\t\t\t\t\t\tseed[i] = !(matches[i] = elem);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}) :\n\t\t\t\tfunction( elem, context, xml ) {\n\t\t\t\t\tinput[0] = elem;\n\t\t\t\t\tmatcher( input, null, xml, results );\n\t\t\t\t\treturn !results.pop();\n\t\t\t\t};\n\t\t}),\n\n\t\t\"has\": markFunction(function( selector ) {\n\t\t\treturn function( elem ) {\n\t\t\t\treturn Sizzle( selector, elem ).length > 0;\n\t\t\t};\n\t\t}),\n\n\t\t\"contains\": markFunction(function( text ) {\n\t\t\treturn function( elem ) {\n\t\t\t\treturn ( elem.textContent || elem.innerText || getText( elem ) ).indexOf( text ) > -1;\n\t\t\t};\n\t\t}),\n\n\t\t// \"Whether an element is represented by a :lang() selector\n\t\t// is based solely on the element's language value\n\t\t// being equal to the identifier C,\n\t\t// or beginning with the identifier C immediately followed by \"-\".\n\t\t// The matching of C against the element's language value is performed case-insensitively.\n\t\t// The identifier C does not have to be a valid language name.\"\n\t\t// http://www.w3.org/TR/selectors/#lang-pseudo\n\t\t\"lang\": markFunction( function( lang ) {\n\t\t\t// lang value must be a valid identifier\n\t\t\tif ( !ridentifier.test(lang || \"\") ) {\n\t\t\t\tSizzle.error( \"unsupported lang: \" + lang );\n\t\t\t}\n\t\t\tlang = lang.replace( runescape, funescape ).toLowerCase();\n\t\t\treturn function( elem ) {\n\t\t\t\tvar elemLang;\n\t\t\t\tdo {\n\t\t\t\t\tif ( (elemLang = documentIsHTML ?\n\t\t\t\t\t\telem.lang :\n\t\t\t\t\t\telem.getAttribute(\"xml:lang\") || elem.getAttribute(\"lang\")) ) {\n\n\t\t\t\t\t\telemLang = elemLang.toLowerCase();\n\t\t\t\t\t\treturn elemLang === lang || elemLang.indexOf( lang + \"-\" ) === 0;\n\t\t\t\t\t}\n\t\t\t\t} while ( (elem = elem.parentNode) && elem.nodeType === 1 );\n\t\t\t\treturn false;\n\t\t\t};\n\t\t}),\n\n\t\t// Miscellaneous\n\t\t\"target\": function( elem ) {\n\t\t\tvar hash = window.location && window.location.hash;\n\t\t\treturn hash && hash.slice( 1 ) === elem.id;\n\t\t},\n\n\t\t\"root\": function( elem ) {\n\t\t\treturn elem === docElem;\n\t\t},\n\n\t\t\"focus\": function( elem ) {\n\t\t\treturn elem === document.activeElement && (!document.hasFocus || document.hasFocus()) && !!(elem.type || elem.href || ~elem.tabIndex);\n\t\t},\n\n\t\t// Boolean properties\n\t\t\"enabled\": function( elem ) {\n\t\t\treturn elem.disabled === false;\n\t\t},\n\n\t\t\"disabled\": function( elem ) {\n\t\t\treturn elem.disabled === true;\n\t\t},\n\n\t\t\"checked\": function( elem ) {\n\t\t\t// In CSS3, :checked should return both checked and selected elements\n\t\t\t// http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked\n\t\t\tvar nodeName = elem.nodeName.toLowerCase();\n\t\t\treturn (nodeName === \"input\" && !!elem.checked) || (nodeName === \"option\" && !!elem.selected);\n\t\t},\n\n\t\t\"selected\": function( elem ) {\n\t\t\t// Accessing this property makes selected-by-default\n\t\t\t// options in Safari work properly\n\t\t\tif ( elem.parentNode ) {\n\t\t\t\telem.parentNode.selectedIndex;\n\t\t\t}\n\n\t\t\treturn elem.selected === true;\n\t\t},\n\n\t\t// Contents\n\t\t\"empty\": function( elem ) {\n\t\t\t// http://www.w3.org/TR/selectors/#empty-pseudo\n\t\t\t// :empty is negated by element (1) or content nodes (text: 3; cdata: 4; entity ref: 5),\n\t\t\t//   but not by others (comment: 8; processing instruction: 7; etc.)\n\t\t\t// nodeType < 6 works because attributes (2) do not appear as children\n\t\t\tfor ( elem = elem.firstChild; elem; elem = elem.nextSibling ) {\n\t\t\t\tif ( elem.nodeType < 6 ) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn true;\n\t\t},\n\n\t\t\"parent\": function( elem ) {\n\t\t\treturn !Expr.pseudos[\"empty\"]( elem );\n\t\t},\n\n\t\t// Element/input types\n\t\t\"header\": function( elem ) {\n\t\t\treturn rheader.test( elem.nodeName );\n\t\t},\n\n\t\t\"input\": function( elem ) {\n\t\t\treturn rinputs.test( elem.nodeName );\n\t\t},\n\n\t\t\"button\": function( elem ) {\n\t\t\tvar name = elem.nodeName.toLowerCase();\n\t\t\treturn name === \"input\" && elem.type === \"button\" || name === \"button\";\n\t\t},\n\n\t\t\"text\": function( elem ) {\n\t\t\tvar attr;\n\t\t\treturn elem.nodeName.toLowerCase() === \"input\" &&\n\t\t\t\telem.type === \"text\" &&\n\n\t\t\t\t// Support: IE<8\n\t\t\t\t// New HTML5 attribute values (e.g., \"search\") appear with elem.type === \"text\"\n\t\t\t\t( (attr = elem.getAttribute(\"type\")) == null || attr.toLowerCase() === \"text\" );\n\t\t},\n\n\t\t// Position-in-collection\n\t\t\"first\": createPositionalPseudo(function() {\n\t\t\treturn [ 0 ];\n\t\t}),\n\n\t\t\"last\": createPositionalPseudo(function( matchIndexes, length ) {\n\t\t\treturn [ length - 1 ];\n\t\t}),\n\n\t\t\"eq\": createPositionalPseudo(function( matchIndexes, length, argument ) {\n\t\t\treturn [ argument < 0 ? argument + length : argument ];\n\t\t}),\n\n\t\t\"even\": createPositionalPseudo(function( matchIndexes, length ) {\n\t\t\tvar i = 0;\n\t\t\tfor ( ; i < length; i += 2 ) {\n\t\t\t\tmatchIndexes.push( i );\n\t\t\t}\n\t\t\treturn matchIndexes;\n\t\t}),\n\n\t\t\"odd\": createPositionalPseudo(function( matchIndexes, length ) {\n\t\t\tvar i = 1;\n\t\t\tfor ( ; i < length; i += 2 ) {\n\t\t\t\tmatchIndexes.push( i );\n\t\t\t}\n\t\t\treturn matchIndexes;\n\t\t}),\n\n\t\t\"lt\": createPositionalPseudo(function( matchIndexes, length, argument ) {\n\t\t\tvar i = argument < 0 ? argument + length : argument;\n\t\t\tfor ( ; --i >= 0; ) {\n\t\t\t\tmatchIndexes.push( i );\n\t\t\t}\n\t\t\treturn matchIndexes;\n\t\t}),\n\n\t\t\"gt\": createPositionalPseudo(function( matchIndexes, length, argument ) {\n\t\t\tvar i = argument < 0 ? argument + length : argument;\n\t\t\tfor ( ; ++i < length; ) {\n\t\t\t\tmatchIndexes.push( i );\n\t\t\t}\n\t\t\treturn matchIndexes;\n\t\t})\n\t}\n};\n\nExpr.pseudos[\"nth\"] = Expr.pseudos[\"eq\"];\n\n// Add button/input type pseudos\nfor ( i in { radio: true, checkbox: true, file: true, password: true, image: true } ) {\n\tExpr.pseudos[ i ] = createInputPseudo( i );\n}\nfor ( i in { submit: true, reset: true } ) {\n\tExpr.pseudos[ i ] = createButtonPseudo( i );\n}\n\n// Easy API for creating new setFilters\nfunction setFilters() {}\nsetFilters.prototype = Expr.filters = Expr.pseudos;\nExpr.setFilters = new setFilters();\n\ntokenize = Sizzle.tokenize = function( selector, parseOnly ) {\n\tvar matched, match, tokens, type,\n\t\tsoFar, groups, preFilters,\n\t\tcached = tokenCache[ selector + \" \" ];\n\n\tif ( cached ) {\n\t\treturn parseOnly ? 0 : cached.slice( 0 );\n\t}\n\n\tsoFar = selector;\n\tgroups = [];\n\tpreFilters = Expr.preFilter;\n\n\twhile ( soFar ) {\n\n\t\t// Comma and first run\n\t\tif ( !matched || (match = rcomma.exec( soFar )) ) {\n\t\t\tif ( match ) {\n\t\t\t\t// Don't consume trailing commas as valid\n\t\t\t\tsoFar = soFar.slice( match[0].length ) || soFar;\n\t\t\t}\n\t\t\tgroups.push( (tokens = []) );\n\t\t}\n\n\t\tmatched = false;\n\n\t\t// Combinators\n\t\tif ( (match = rcombinators.exec( soFar )) ) {\n\t\t\tmatched = match.shift();\n\t\t\ttokens.push({\n\t\t\t\tvalue: matched,\n\t\t\t\t// Cast descendant combinators to space\n\t\t\t\ttype: match[0].replace( rtrim, \" \" )\n\t\t\t});\n\t\t\tsoFar = soFar.slice( matched.length );\n\t\t}\n\n\t\t// Filters\n\t\tfor ( type in Expr.filter ) {\n\t\t\tif ( (match = matchExpr[ type ].exec( soFar )) && (!preFilters[ type ] ||\n\t\t\t\t(match = preFilters[ type ]( match ))) ) {\n\t\t\t\tmatched = match.shift();\n\t\t\t\ttokens.push({\n\t\t\t\t\tvalue: matched,\n\t\t\t\t\ttype: type,\n\t\t\t\t\tmatches: match\n\t\t\t\t});\n\t\t\t\tsoFar = soFar.slice( matched.length );\n\t\t\t}\n\t\t}\n\n\t\tif ( !matched ) {\n\t\t\tbreak;\n\t\t}\n\t}\n\n\t// Return the length of the invalid excess\n\t// if we're just parsing\n\t// Otherwise, throw an error or return tokens\n\treturn parseOnly ?\n\t\tsoFar.length :\n\t\tsoFar ?\n\t\t\tSizzle.error( selector ) :\n\t\t\t// Cache the tokens\n\t\t\ttokenCache( selector, groups ).slice( 0 );\n};\n\nfunction toSelector( tokens ) {\n\tvar i = 0,\n\t\tlen = tokens.length,\n\t\tselector = \"\";\n\tfor ( ; i < len; i++ ) {\n\t\tselector += tokens[i].value;\n\t}\n\treturn selector;\n}\n\nfunction addCombinator( matcher, combinator, base ) {\n\tvar dir = combinator.dir,\n\t\tcheckNonElements = base && dir === \"parentNode\",\n\t\tdoneName = done++;\n\n\treturn combinator.first ?\n\t\t// Check against closest ancestor/preceding element\n\t\tfunction( elem, context, xml ) {\n\t\t\twhile ( (elem = elem[ dir ]) ) {\n\t\t\t\tif ( elem.nodeType === 1 || checkNonElements ) {\n\t\t\t\t\treturn matcher( elem, context, xml );\n\t\t\t\t}\n\t\t\t}\n\t\t} :\n\n\t\t// Check against all ancestor/preceding elements\n\t\tfunction( elem, context, xml ) {\n\t\t\tvar oldCache, outerCache,\n\t\t\t\tnewCache = [ dirruns, doneName ];\n\n\t\t\t// We can't set arbitrary data on XML nodes, so they don't benefit from dir caching\n\t\t\tif ( xml ) {\n\t\t\t\twhile ( (elem = elem[ dir ]) ) {\n\t\t\t\t\tif ( elem.nodeType === 1 || checkNonElements ) {\n\t\t\t\t\t\tif ( matcher( elem, context, xml ) ) {\n\t\t\t\t\t\t\treturn true;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\twhile ( (elem = elem[ dir ]) ) {\n\t\t\t\t\tif ( elem.nodeType === 1 || checkNonElements ) {\n\t\t\t\t\t\touterCache = elem[ expando ] || (elem[ expando ] = {});\n\t\t\t\t\t\tif ( (oldCache = outerCache[ dir ]) &&\n\t\t\t\t\t\t\toldCache[ 0 ] === dirruns && oldCache[ 1 ] === doneName ) {\n\n\t\t\t\t\t\t\t// Assign to newCache so results back-propagate to previous elements\n\t\t\t\t\t\t\treturn (newCache[ 2 ] = oldCache[ 2 ]);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// Reuse newcache so results back-propagate to previous elements\n\t\t\t\t\t\t\touterCache[ dir ] = newCache;\n\n\t\t\t\t\t\t\t// A match means we're done; a fail means we have to keep checking\n\t\t\t\t\t\t\tif ( (newCache[ 2 ] = matcher( elem, context, xml )) ) {\n\t\t\t\t\t\t\t\treturn true;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t};\n}\n\nfunction elementMatcher( matchers ) {\n\treturn matchers.length > 1 ?\n\t\tfunction( elem, context, xml ) {\n\t\t\tvar i = matchers.length;\n\t\t\twhile ( i-- ) {\n\t\t\t\tif ( !matchers[i]( elem, context, xml ) ) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn true;\n\t\t} :\n\t\tmatchers[0];\n}\n\nfunction multipleContexts( selector, contexts, results ) {\n\tvar i = 0,\n\t\tlen = contexts.length;\n\tfor ( ; i < len; i++ ) {\n\t\tSizzle( selector, contexts[i], results );\n\t}\n\treturn results;\n}\n\nfunction condense( unmatched, map, filter, context, xml ) {\n\tvar elem,\n\t\tnewUnmatched = [],\n\t\ti = 0,\n\t\tlen = unmatched.length,\n\t\tmapped = map != null;\n\n\tfor ( ; i < len; i++ ) {\n\t\tif ( (elem = unmatched[i]) ) {\n\t\t\tif ( !filter || filter( elem, context, xml ) ) {\n\t\t\t\tnewUnmatched.push( elem );\n\t\t\t\tif ( mapped ) {\n\t\t\t\t\tmap.push( i );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn newUnmatched;\n}\n\nfunction setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) {\n\tif ( postFilter && !postFilter[ expando ] ) {\n\t\tpostFilter = setMatcher( postFilter );\n\t}\n\tif ( postFinder && !postFinder[ expando ] ) {\n\t\tpostFinder = setMatcher( postFinder, postSelector );\n\t}\n\treturn markFunction(function( seed, results, context, xml ) {\n\t\tvar temp, i, elem,\n\t\t\tpreMap = [],\n\t\t\tpostMap = [],\n\t\t\tpreexisting = results.length,\n\n\t\t\t// Get initial elements from seed or context\n\t\t\telems = seed || multipleContexts( selector || \"*\", context.nodeType ? [ context ] : context, [] ),\n\n\t\t\t// Prefilter to get matcher input, preserving a map for seed-results synchronization\n\t\t\tmatcherIn = preFilter && ( seed || !selector ) ?\n\t\t\t\tcondense( elems, preMap, preFilter, context, xml ) :\n\t\t\t\telems,\n\n\t\t\tmatcherOut = matcher ?\n\t\t\t\t// If we have a postFinder, or filtered seed, or non-seed postFilter or preexisting results,\n\t\t\t\tpostFinder || ( seed ? preFilter : preexisting || postFilter ) ?\n\n\t\t\t\t\t// ...intermediate processing is necessary\n\t\t\t\t\t[] :\n\n\t\t\t\t\t// ...otherwise use results directly\n\t\t\t\t\tresults :\n\t\t\t\tmatcherIn;\n\n\t\t// Find primary matches\n\t\tif ( matcher ) {\n\t\t\tmatcher( matcherIn, matcherOut, context, xml );\n\t\t}\n\n\t\t// Apply postFilter\n\t\tif ( postFilter ) {\n\t\t\ttemp = condense( matcherOut, postMap );\n\t\t\tpostFilter( temp, [], context, xml );\n\n\t\t\t// Un-match failing elements by moving them back to matcherIn\n\t\t\ti = temp.length;\n\t\t\twhile ( i-- ) {\n\t\t\t\tif ( (elem = temp[i]) ) {\n\t\t\t\t\tmatcherOut[ postMap[i] ] = !(matcherIn[ postMap[i] ] = elem);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif ( seed ) {\n\t\t\tif ( postFinder || preFilter ) {\n\t\t\t\tif ( postFinder ) {\n\t\t\t\t\t// Get the final matcherOut by condensing this intermediate into postFinder contexts\n\t\t\t\t\ttemp = [];\n\t\t\t\t\ti = matcherOut.length;\n\t\t\t\t\twhile ( i-- ) {\n\t\t\t\t\t\tif ( (elem = matcherOut[i]) ) {\n\t\t\t\t\t\t\t// Restore matcherIn since elem is not yet a final match\n\t\t\t\t\t\t\ttemp.push( (matcherIn[i] = elem) );\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tpostFinder( null, (matcherOut = []), temp, xml );\n\t\t\t\t}\n\n\t\t\t\t// Move matched elements from seed to results to keep them synchronized\n\t\t\t\ti = matcherOut.length;\n\t\t\t\twhile ( i-- ) {\n\t\t\t\t\tif ( (elem = matcherOut[i]) &&\n\t\t\t\t\t\t(temp = postFinder ? indexOf.call( seed, elem ) : preMap[i]) > -1 ) {\n\n\t\t\t\t\t\tseed[temp] = !(results[temp] = elem);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t// Add elements to results, through postFinder if defined\n\t\t} else {\n\t\t\tmatcherOut = condense(\n\t\t\t\tmatcherOut === results ?\n\t\t\t\t\tmatcherOut.splice( preexisting, matcherOut.length ) :\n\t\t\t\t\tmatcherOut\n\t\t\t);\n\t\t\tif ( postFinder ) {\n\t\t\t\tpostFinder( null, results, matcherOut, xml );\n\t\t\t} else {\n\t\t\t\tpush.apply( results, matcherOut );\n\t\t\t}\n\t\t}\n\t});\n}\n\nfunction matcherFromTokens( tokens ) {\n\tvar checkContext, matcher, j,\n\t\tlen = tokens.length,\n\t\tleadingRelative = Expr.relative[ tokens[0].type ],\n\t\timplicitRelative = leadingRelative || Expr.relative[\" \"],\n\t\ti = leadingRelative ? 1 : 0,\n\n\t\t// The foundational matcher ensures that elements are reachable from top-level context(s)\n\t\tmatchContext = addCombinator( function( elem ) {\n\t\t\treturn elem === checkContext;\n\t\t}, implicitRelative, true ),\n\t\tmatchAnyContext = addCombinator( function( elem ) {\n\t\t\treturn indexOf.call( checkContext, elem ) > -1;\n\t\t}, implicitRelative, true ),\n\t\tmatchers = [ function( elem, context, xml ) {\n\t\t\treturn ( !leadingRelative && ( xml || context !== outermostContext ) ) || (\n\t\t\t\t(checkContext = context).nodeType ?\n\t\t\t\t\tmatchContext( elem, context, xml ) :\n\t\t\t\t\tmatchAnyContext( elem, context, xml ) );\n\t\t} ];\n\n\tfor ( ; i < len; i++ ) {\n\t\tif ( (matcher = Expr.relative[ tokens[i].type ]) ) {\n\t\t\tmatchers = [ addCombinator(elementMatcher( matchers ), matcher) ];\n\t\t} else {\n\t\t\tmatcher = Expr.filter[ tokens[i].type ].apply( null, tokens[i].matches );\n\n\t\t\t// Return special upon seeing a positional matcher\n\t\t\tif ( matcher[ expando ] ) {\n\t\t\t\t// Find the next relative operator (if any) for proper handling\n\t\t\t\tj = ++i;\n\t\t\t\tfor ( ; j < len; j++ ) {\n\t\t\t\t\tif ( Expr.relative[ tokens[j].type ] ) {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn setMatcher(\n\t\t\t\t\ti > 1 && elementMatcher( matchers ),\n\t\t\t\t\ti > 1 && toSelector(\n\t\t\t\t\t\t// If the preceding token was a descendant combinator, insert an implicit any-element `*`\n\t\t\t\t\t\ttokens.slice( 0, i - 1 ).concat({ value: tokens[ i - 2 ].type === \" \" ? \"*\" : \"\" })\n\t\t\t\t\t).replace( rtrim, \"$1\" ),\n\t\t\t\t\tmatcher,\n\t\t\t\t\ti < j && matcherFromTokens( tokens.slice( i, j ) ),\n\t\t\t\t\tj < len && matcherFromTokens( (tokens = tokens.slice( j )) ),\n\t\t\t\t\tj < len && toSelector( tokens )\n\t\t\t\t);\n\t\t\t}\n\t\t\tmatchers.push( matcher );\n\t\t}\n\t}\n\n\treturn elementMatcher( matchers );\n}\n\nfunction matcherFromGroupMatchers( elementMatchers, setMatchers ) {\n\tvar bySet = setMatchers.length > 0,\n\t\tbyElement = elementMatchers.length > 0,\n\t\tsuperMatcher = function( seed, context, xml, results, outermost ) {\n\t\t\tvar elem, j, matcher,\n\t\t\t\tmatchedCount = 0,\n\t\t\t\ti = \"0\",\n\t\t\t\tunmatched = seed && [],\n\t\t\t\tsetMatched = [],\n\t\t\t\tcontextBackup = outermostContext,\n\t\t\t\t// We must always have either seed elements or outermost context\n\t\t\t\telems = seed || byElement && Expr.find[\"TAG\"]( \"*\", outermost ),\n\t\t\t\t// Use integer dirruns iff this is the outermost matcher\n\t\t\t\tdirrunsUnique = (dirruns += contextBackup == null ? 1 : Math.random() || 0.1),\n\t\t\t\tlen = elems.length;\n\n\t\t\tif ( outermost ) {\n\t\t\t\toutermostContext = context !== document && context;\n\t\t\t}\n\n\t\t\t// Add elements passing elementMatchers directly to results\n\t\t\t// Keep `i` a string if there are no elements so `matchedCount` will be \"00\" below\n\t\t\t// Support: IE<9, Safari\n\t\t\t// Tolerate NodeList properties (IE: \"length\"; Safari: <number>) matching elements by id\n\t\t\tfor ( ; i !== len && (elem = elems[i]) != null; i++ ) {\n\t\t\t\tif ( byElement && elem ) {\n\t\t\t\t\tj = 0;\n\t\t\t\t\twhile ( (matcher = elementMatchers[j++]) ) {\n\t\t\t\t\t\tif ( matcher( elem, context, xml ) ) {\n\t\t\t\t\t\t\tresults.push( elem );\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif ( outermost ) {\n\t\t\t\t\t\tdirruns = dirrunsUnique;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Track unmatched elements for set filters\n\t\t\t\tif ( bySet ) {\n\t\t\t\t\t// They will have gone through all possible matchers\n\t\t\t\t\tif ( (elem = !matcher && elem) ) {\n\t\t\t\t\t\tmatchedCount--;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Lengthen the array for every element, matched or not\n\t\t\t\t\tif ( seed ) {\n\t\t\t\t\t\tunmatched.push( elem );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Apply set filters to unmatched elements\n\t\t\tmatchedCount += i;\n\t\t\tif ( bySet && i !== matchedCount ) {\n\t\t\t\tj = 0;\n\t\t\t\twhile ( (matcher = setMatchers[j++]) ) {\n\t\t\t\t\tmatcher( unmatched, setMatched, context, xml );\n\t\t\t\t}\n\n\t\t\t\tif ( seed ) {\n\t\t\t\t\t// Reintegrate element matches to eliminate the need for sorting\n\t\t\t\t\tif ( matchedCount > 0 ) {\n\t\t\t\t\t\twhile ( i-- ) {\n\t\t\t\t\t\t\tif ( !(unmatched[i] || setMatched[i]) ) {\n\t\t\t\t\t\t\t\tsetMatched[i] = pop.call( results );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// Discard index placeholder values to get only actual matches\n\t\t\t\t\tsetMatched = condense( setMatched );\n\t\t\t\t}\n\n\t\t\t\t// Add matches to results\n\t\t\t\tpush.apply( results, setMatched );\n\n\t\t\t\t// Seedless set matches succeeding multiple successful matchers stipulate sorting\n\t\t\t\tif ( outermost && !seed && setMatched.length > 0 &&\n\t\t\t\t\t( matchedCount + setMatchers.length ) > 1 ) {\n\n\t\t\t\t\tSizzle.uniqueSort( results );\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Override manipulation of globals by nested matchers\n\t\t\tif ( outermost ) {\n\t\t\t\tdirruns = dirrunsUnique;\n\t\t\t\toutermostContext = contextBackup;\n\t\t\t}\n\n\t\t\treturn unmatched;\n\t\t};\n\n\treturn bySet ?\n\t\tmarkFunction( superMatcher ) :\n\t\tsuperMatcher;\n}\n\ncompile = Sizzle.compile = function( selector, match /* Internal Use Only */ ) {\n\tvar i,\n\t\tsetMatchers = [],\n\t\telementMatchers = [],\n\t\tcached = compilerCache[ selector + \" \" ];\n\n\tif ( !cached ) {\n\t\t// Generate a function of recursive functions that can be used to check each element\n\t\tif ( !match ) {\n\t\t\tmatch = tokenize( selector );\n\t\t}\n\t\ti = match.length;\n\t\twhile ( i-- ) {\n\t\t\tcached = matcherFromTokens( match[i] );\n\t\t\tif ( cached[ expando ] ) {\n\t\t\t\tsetMatchers.push( cached );\n\t\t\t} else {\n\t\t\t\telementMatchers.push( cached );\n\t\t\t}\n\t\t}\n\n\t\t// Cache the compiled function\n\t\tcached = compilerCache( selector, matcherFromGroupMatchers( elementMatchers, setMatchers ) );\n\n\t\t// Save selector and tokenization\n\t\tcached.selector = selector;\n\t}\n\treturn cached;\n};\n\n/**\n * A low-level selection function that works with Sizzle's compiled\n *  selector functions\n * @param {String|Function} selector A selector or a pre-compiled\n *  selector function built with Sizzle.compile\n * @param {Element} context\n * @param {Array} [results]\n * @param {Array} [seed] A set of elements to match against\n */\nselect = Sizzle.select = function( selector, context, results, seed ) {\n\tvar i, tokens, token, type, find,\n\t\tcompiled = typeof selector === \"function\" && selector,\n\t\tmatch = !seed && tokenize( (selector = compiled.selector || selector) );\n\n\tresults = results || [];\n\n\t// Try to minimize operations if there is no seed and only one group\n\tif ( match.length === 1 ) {\n\n\t\t// Take a shortcut and set the context if the root selector is an ID\n\t\ttokens = match[0] = match[0].slice( 0 );\n\t\tif ( tokens.length > 2 && (token = tokens[0]).type === \"ID\" &&\n\t\t\t\tsupport.getById && context.nodeType === 9 && documentIsHTML &&\n\t\t\t\tExpr.relative[ tokens[1].type ] ) {\n\n\t\t\tcontext = ( Expr.find[\"ID\"]( token.matches[0].replace(runescape, funescape), context ) || [] )[0];\n\t\t\tif ( !context ) {\n\t\t\t\treturn results;\n\n\t\t\t// Precompiled matchers will still verify ancestry, so step up a level\n\t\t\t} else if ( compiled ) {\n\t\t\t\tcontext = context.parentNode;\n\t\t\t}\n\n\t\t\tselector = selector.slice( tokens.shift().value.length );\n\t\t}\n\n\t\t// Fetch a seed set for right-to-left matching\n\t\ti = matchExpr[\"needsContext\"].test( selector ) ? 0 : tokens.length;\n\t\twhile ( i-- ) {\n\t\t\ttoken = tokens[i];\n\n\t\t\t// Abort if we hit a combinator\n\t\t\tif ( Expr.relative[ (type = token.type) ] ) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif ( (find = Expr.find[ type ]) ) {\n\t\t\t\t// Search, expanding context for leading sibling combinators\n\t\t\t\tif ( (seed = find(\n\t\t\t\t\ttoken.matches[0].replace( runescape, funescape ),\n\t\t\t\t\trsibling.test( tokens[0].type ) && testContext( context.parentNode ) || context\n\t\t\t\t)) ) {\n\n\t\t\t\t\t// If seed is empty or no tokens remain, we can return early\n\t\t\t\t\ttokens.splice( i, 1 );\n\t\t\t\t\tselector = seed.length && toSelector( tokens );\n\t\t\t\t\tif ( !selector ) {\n\t\t\t\t\t\tpush.apply( results, seed );\n\t\t\t\t\t\treturn results;\n\t\t\t\t\t}\n\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Compile and execute a filtering function if one is not provided\n\t// Provide `match` to avoid retokenization if we modified the selector above\n\t( compiled || compile( selector, match ) )(\n\t\tseed,\n\t\tcontext,\n\t\t!documentIsHTML,\n\t\tresults,\n\t\trsibling.test( selector ) && testContext( context.parentNode ) || context\n\t);\n\treturn results;\n};\n\n// One-time assignments\n\n// Sort stability\nsupport.sortStable = expando.split(\"\").sort( sortOrder ).join(\"\") === expando;\n\n// Support: Chrome<14\n// Always assume duplicates if they aren't passed to the comparison function\nsupport.detectDuplicates = !!hasDuplicate;\n\n// Initialize against the default document\nsetDocument();\n\n// Support: Webkit<537.32 - Safari 6.0.3/Chrome 25 (fixed in Chrome 27)\n// Detached nodes confoundingly follow *each other*\nsupport.sortDetached = assert(function( div1 ) {\n\t// Should return 1, but returns 4 (following)\n\treturn div1.compareDocumentPosition( document.createElement(\"div\") ) & 1;\n});\n\n// Support: IE<8\n// Prevent attribute/property \"interpolation\"\n// http://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx\nif ( !assert(function( div ) {\n\tdiv.innerHTML = \"<a href='#'></a>\";\n\treturn div.firstChild.getAttribute(\"href\") === \"#\" ;\n}) ) {\n\taddHandle( \"type|href|height|width\", function( elem, name, isXML ) {\n\t\tif ( !isXML ) {\n\t\t\treturn elem.getAttribute( name, name.toLowerCase() === \"type\" ? 1 : 2 );\n\t\t}\n\t});\n}\n\n// Support: IE<9\n// Use defaultValue in place of getAttribute(\"value\")\nif ( !support.attributes || !assert(function( div ) {\n\tdiv.innerHTML = \"<input/>\";\n\tdiv.firstChild.setAttribute( \"value\", \"\" );\n\treturn div.firstChild.getAttribute( \"value\" ) === \"\";\n}) ) {\n\taddHandle( \"value\", function( elem, name, isXML ) {\n\t\tif ( !isXML && elem.nodeName.toLowerCase() === \"input\" ) {\n\t\t\treturn elem.defaultValue;\n\t\t}\n\t});\n}\n\n// Support: IE<9\n// Use getAttributeNode to fetch booleans when getAttribute lies\nif ( !assert(function( div ) {\n\treturn div.getAttribute(\"disabled\") == null;\n}) ) {\n\taddHandle( booleans, function( elem, name, isXML ) {\n\t\tvar val;\n\t\tif ( !isXML ) {\n\t\t\treturn elem[ name ] === true ? name.toLowerCase() :\n\t\t\t\t\t(val = elem.getAttributeNode( name )) && val.specified ?\n\t\t\t\t\tval.value :\n\t\t\t\tnull;\n\t\t}\n\t});\n}\n\nreturn Sizzle;\n\n})( window );\n\n\n\njQuery.find = Sizzle;\njQuery.expr = Sizzle.selectors;\njQuery.expr[\":\"] = jQuery.expr.pseudos;\njQuery.unique = Sizzle.uniqueSort;\njQuery.text = Sizzle.getText;\njQuery.isXMLDoc = Sizzle.isXML;\njQuery.contains = Sizzle.contains;\n\n\n\nvar rneedsContext = jQuery.expr.match.needsContext;\n\nvar rsingleTag = (/^<(\\w+)\\s*\\/?>(?:<\\/\\1>|)$/);\n\n\n\nvar risSimple = /^.[^:#\\[\\.,]*$/;\n\n// Implement the identical functionality for filter and not\nfunction winnow( elements, qualifier, not ) {\n\tif ( jQuery.isFunction( qualifier ) ) {\n\t\treturn jQuery.grep( elements, function( elem, i ) {\n\t\t\t/* jshint -W018 */\n\t\t\treturn !!qualifier.call( elem, i, elem ) !== not;\n\t\t});\n\n\t}\n\n\tif ( qualifier.nodeType ) {\n\t\treturn jQuery.grep( elements, function( elem ) {\n\t\t\treturn ( elem === qualifier ) !== not;\n\t\t});\n\n\t}\n\n\tif ( typeof qualifier === \"string\" ) {\n\t\tif ( risSimple.test( qualifier ) ) {\n\t\t\treturn jQuery.filter( qualifier, elements, not );\n\t\t}\n\n\t\tqualifier = jQuery.filter( qualifier, elements );\n\t}\n\n\treturn jQuery.grep( elements, function( elem ) {\n\t\treturn ( indexOf.call( qualifier, elem ) >= 0 ) !== not;\n\t});\n}\n\njQuery.filter = function( expr, elems, not ) {\n\tvar elem = elems[ 0 ];\n\n\tif ( not ) {\n\t\texpr = \":not(\" + expr + \")\";\n\t}\n\n\treturn elems.length === 1 && elem.nodeType === 1 ?\n\t\tjQuery.find.matchesSelector( elem, expr ) ? [ elem ] : [] :\n\t\tjQuery.find.matches( expr, jQuery.grep( elems, function( elem ) {\n\t\t\treturn elem.nodeType === 1;\n\t\t}));\n};\n\njQuery.fn.extend({\n\tfind: function( selector ) {\n\t\tvar i,\n\t\t\tlen = this.length,\n\t\t\tret = [],\n\t\t\tself = this;\n\n\t\tif ( typeof selector !== \"string\" ) {\n\t\t\treturn this.pushStack( jQuery( selector ).filter(function() {\n\t\t\t\tfor ( i = 0; i < len; i++ ) {\n\t\t\t\t\tif ( jQuery.contains( self[ i ], this ) ) {\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}) );\n\t\t}\n\n\t\tfor ( i = 0; i < len; i++ ) {\n\t\t\tjQuery.find( selector, self[ i ], ret );\n\t\t}\n\n\t\t// Needed because $( selector, context ) becomes $( context ).find( selector )\n\t\tret = this.pushStack( len > 1 ? jQuery.unique( ret ) : ret );\n\t\tret.selector = this.selector ? this.selector + \" \" + selector : selector;\n\t\treturn ret;\n\t},\n\tfilter: function( selector ) {\n\t\treturn this.pushStack( winnow(this, selector || [], false) );\n\t},\n\tnot: function( selector ) {\n\t\treturn this.pushStack( winnow(this, selector || [], true) );\n\t},\n\tis: function( selector ) {\n\t\treturn !!winnow(\n\t\t\tthis,\n\n\t\t\t// If this is a positional/relative selector, check membership in the returned set\n\t\t\t// so $(\"p:first\").is(\"p:last\") won't return true for a doc with two \"p\".\n\t\t\ttypeof selector === \"string\" && rneedsContext.test( selector ) ?\n\t\t\t\tjQuery( selector ) :\n\t\t\t\tselector || [],\n\t\t\tfalse\n\t\t).length;\n\t}\n});\n\n\n// Initialize a jQuery object\n\n\n// A central reference to the root jQuery(document)\nvar rootjQuery,\n\n\t// A simple way to check for HTML strings\n\t// Prioritize #id over <tag> to avoid XSS via location.hash (#9521)\n\t// Strict HTML recognition (#11290: must start with <)\n\trquickExpr = /^(?:\\s*(<[\\w\\W]+>)[^>]*|#([\\w-]*))$/,\n\n\tinit = jQuery.fn.init = function( selector, context ) {\n\t\tvar match, elem;\n\n\t\t// HANDLE: $(\"\"), $(null), $(undefined), $(false)\n\t\tif ( !selector ) {\n\t\t\treturn this;\n\t\t}\n\n\t\t// Handle HTML strings\n\t\tif ( typeof selector === \"string\" ) {\n\t\t\tif ( selector[0] === \"<\" && selector[ selector.length - 1 ] === \">\" && selector.length >= 3 ) {\n\t\t\t\t// Assume that strings that start and end with <> are HTML and skip the regex check\n\t\t\t\tmatch = [ null, selector, null ];\n\n\t\t\t} else {\n\t\t\t\tmatch = rquickExpr.exec( selector );\n\t\t\t}\n\n\t\t\t// Match html or make sure no context is specified for #id\n\t\t\tif ( match && (match[1] || !context) ) {\n\n\t\t\t\t// HANDLE: $(html) -> $(array)\n\t\t\t\tif ( match[1] ) {\n\t\t\t\t\tcontext = context instanceof jQuery ? context[0] : context;\n\n\t\t\t\t\t// scripts is true for back-compat\n\t\t\t\t\t// Intentionally let the error be thrown if parseHTML is not present\n\t\t\t\t\tjQuery.merge( this, jQuery.parseHTML(\n\t\t\t\t\t\tmatch[1],\n\t\t\t\t\t\tcontext && context.nodeType ? context.ownerDocument || context : document,\n\t\t\t\t\t\ttrue\n\t\t\t\t\t) );\n\n\t\t\t\t\t// HANDLE: $(html, props)\n\t\t\t\t\tif ( rsingleTag.test( match[1] ) && jQuery.isPlainObject( context ) ) {\n\t\t\t\t\t\tfor ( match in context ) {\n\t\t\t\t\t\t\t// Properties of context are called as methods if possible\n\t\t\t\t\t\t\tif ( jQuery.isFunction( this[ match ] ) ) {\n\t\t\t\t\t\t\t\tthis[ match ]( context[ match ] );\n\n\t\t\t\t\t\t\t// ...and otherwise set as attributes\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tthis.attr( match, context[ match ] );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\treturn this;\n\n\t\t\t\t// HANDLE: $(#id)\n\t\t\t\t} else {\n\t\t\t\t\telem = document.getElementById( match[2] );\n\n\t\t\t\t\t// Check parentNode to catch when Blackberry 4.6 returns\n\t\t\t\t\t// nodes that are no longer in the document #6963\n\t\t\t\t\tif ( elem && elem.parentNode ) {\n\t\t\t\t\t\t// Inject the element directly into the jQuery object\n\t\t\t\t\t\tthis.length = 1;\n\t\t\t\t\t\tthis[0] = elem;\n\t\t\t\t\t}\n\n\t\t\t\t\tthis.context = document;\n\t\t\t\t\tthis.selector = selector;\n\t\t\t\t\treturn this;\n\t\t\t\t}\n\n\t\t\t// HANDLE: $(expr, $(...))\n\t\t\t} else if ( !context || context.jquery ) {\n\t\t\t\treturn ( context || rootjQuery ).find( selector );\n\n\t\t\t// HANDLE: $(expr, context)\n\t\t\t// (which is just equivalent to: $(context).find(expr)\n\t\t\t} else {\n\t\t\t\treturn this.constructor( context ).find( selector );\n\t\t\t}\n\n\t\t// HANDLE: $(DOMElement)\n\t\t} else if ( selector.nodeType ) {\n\t\t\tthis.context = this[0] = selector;\n\t\t\tthis.length = 1;\n\t\t\treturn this;\n\n\t\t// HANDLE: $(function)\n\t\t// Shortcut for document ready\n\t\t} else if ( jQuery.isFunction( selector ) ) {\n\t\t\treturn typeof rootjQuery.ready !== \"undefined\" ?\n\t\t\t\trootjQuery.ready( selector ) :\n\t\t\t\t// Execute immediately if ready is not present\n\t\t\t\tselector( jQuery );\n\t\t}\n\n\t\tif ( selector.selector !== undefined ) {\n\t\t\tthis.selector = selector.selector;\n\t\t\tthis.context = selector.context;\n\t\t}\n\n\t\treturn jQuery.makeArray( selector, this );\n\t};\n\n// Give the init function the jQuery prototype for later instantiation\ninit.prototype = jQuery.fn;\n\n// Initialize central reference\nrootjQuery = jQuery( document );\n\n\nvar rparentsprev = /^(?:parents|prev(?:Until|All))/,\n\t// methods guaranteed to produce a unique set when starting from a unique set\n\tguaranteedUnique = {\n\t\tchildren: true,\n\t\tcontents: true,\n\t\tnext: true,\n\t\tprev: true\n\t};\n\njQuery.extend({\n\tdir: function( elem, dir, until ) {\n\t\tvar matched = [],\n\t\t\ttruncate = until !== undefined;\n\n\t\twhile ( (elem = elem[ dir ]) && elem.nodeType !== 9 ) {\n\t\t\tif ( elem.nodeType === 1 ) {\n\t\t\t\tif ( truncate && jQuery( elem ).is( until ) ) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tmatched.push( elem );\n\t\t\t}\n\t\t}\n\t\treturn matched;\n\t},\n\n\tsibling: function( n, elem ) {\n\t\tvar matched = [];\n\n\t\tfor ( ; n; n = n.nextSibling ) {\n\t\t\tif ( n.nodeType === 1 && n !== elem ) {\n\t\t\t\tmatched.push( n );\n\t\t\t}\n\t\t}\n\n\t\treturn matched;\n\t}\n});\n\njQuery.fn.extend({\n\thas: function( target ) {\n\t\tvar targets = jQuery( target, this ),\n\t\t\tl = targets.length;\n\n\t\treturn this.filter(function() {\n\t\t\tvar i = 0;\n\t\t\tfor ( ; i < l; i++ ) {\n\t\t\t\tif ( jQuery.contains( this, targets[i] ) ) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t},\n\n\tclosest: function( selectors, context ) {\n\t\tvar cur,\n\t\t\ti = 0,\n\t\t\tl = this.length,\n\t\t\tmatched = [],\n\t\t\tpos = rneedsContext.test( selectors ) || typeof selectors !== \"string\" ?\n\t\t\t\tjQuery( selectors, context || this.context ) :\n\t\t\t\t0;\n\n\t\tfor ( ; i < l; i++ ) {\n\t\t\tfor ( cur = this[i]; cur && cur !== context; cur = cur.parentNode ) {\n\t\t\t\t// Always skip document fragments\n\t\t\t\tif ( cur.nodeType < 11 && (pos ?\n\t\t\t\t\tpos.index(cur) > -1 :\n\n\t\t\t\t\t// Don't pass non-elements to Sizzle\n\t\t\t\t\tcur.nodeType === 1 &&\n\t\t\t\t\t\tjQuery.find.matchesSelector(cur, selectors)) ) {\n\n\t\t\t\t\tmatched.push( cur );\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn this.pushStack( matched.length > 1 ? jQuery.unique( matched ) : matched );\n\t},\n\n\t// Determine the position of an element within\n\t// the matched set of elements\n\tindex: function( elem ) {\n\n\t\t// No argument, return index in parent\n\t\tif ( !elem ) {\n\t\t\treturn ( this[ 0 ] && this[ 0 ].parentNode ) ? this.first().prevAll().length : -1;\n\t\t}\n\n\t\t// index in selector\n\t\tif ( typeof elem === \"string\" ) {\n\t\t\treturn indexOf.call( jQuery( elem ), this[ 0 ] );\n\t\t}\n\n\t\t// Locate the position of the desired element\n\t\treturn indexOf.call( this,\n\n\t\t\t// If it receives a jQuery object, the first element is used\n\t\t\telem.jquery ? elem[ 0 ] : elem\n\t\t);\n\t},\n\n\tadd: function( selector, context ) {\n\t\treturn this.pushStack(\n\t\t\tjQuery.unique(\n\t\t\t\tjQuery.merge( this.get(), jQuery( selector, context ) )\n\t\t\t)\n\t\t);\n\t},\n\n\taddBack: function( selector ) {\n\t\treturn this.add( selector == null ?\n\t\t\tthis.prevObject : this.prevObject.filter(selector)\n\t\t);\n\t}\n});\n\nfunction sibling( cur, dir ) {\n\twhile ( (cur = cur[dir]) && cur.nodeType !== 1 ) {}\n\treturn cur;\n}\n\njQuery.each({\n\tparent: function( elem ) {\n\t\tvar parent = elem.parentNode;\n\t\treturn parent && parent.nodeType !== 11 ? parent : null;\n\t},\n\tparents: function( elem ) {\n\t\treturn jQuery.dir( elem, \"parentNode\" );\n\t},\n\tparentsUntil: function( elem, i, until ) {\n\t\treturn jQuery.dir( elem, \"parentNode\", until );\n\t},\n\tnext: function( elem ) {\n\t\treturn sibling( elem, \"nextSibling\" );\n\t},\n\tprev: function( elem ) {\n\t\treturn sibling( elem, \"previousSibling\" );\n\t},\n\tnextAll: function( elem ) {\n\t\treturn jQuery.dir( elem, \"nextSibling\" );\n\t},\n\tprevAll: function( elem ) {\n\t\treturn jQuery.dir( elem, \"previousSibling\" );\n\t},\n\tnextUntil: function( elem, i, until ) {\n\t\treturn jQuery.dir( elem, \"nextSibling\", until );\n\t},\n\tprevUntil: function( elem, i, until ) {\n\t\treturn jQuery.dir( elem, \"previousSibling\", until );\n\t},\n\tsiblings: function( elem ) {\n\t\treturn jQuery.sibling( ( elem.parentNode || {} ).firstChild, elem );\n\t},\n\tchildren: function( elem ) {\n\t\treturn jQuery.sibling( elem.firstChild );\n\t},\n\tcontents: function( elem ) {\n\t\treturn elem.contentDocument || jQuery.merge( [], elem.childNodes );\n\t}\n}, function( name, fn ) {\n\tjQuery.fn[ name ] = function( until, selector ) {\n\t\tvar matched = jQuery.map( this, fn, until );\n\n\t\tif ( name.slice( -5 ) !== \"Until\" ) {\n\t\t\tselector = until;\n\t\t}\n\n\t\tif ( selector && typeof selector === \"string\" ) {\n\t\t\tmatched = jQuery.filter( selector, matched );\n\t\t}\n\n\t\tif ( this.length > 1 ) {\n\t\t\t// Remove duplicates\n\t\t\tif ( !guaranteedUnique[ name ] ) {\n\t\t\t\tjQuery.unique( matched );\n\t\t\t}\n\n\t\t\t// Reverse order for parents* and prev-derivatives\n\t\t\tif ( rparentsprev.test( name ) ) {\n\t\t\t\tmatched.reverse();\n\t\t\t}\n\t\t}\n\n\t\treturn this.pushStack( matched );\n\t};\n});\nvar rnotwhite = (/\\S+/g);\n\n\n\n// String to Object options format cache\nvar optionsCache = {};\n\n// Convert String-formatted options into Object-formatted ones and store in cache\nfunction createOptions( options ) {\n\tvar object = optionsCache[ options ] = {};\n\tjQuery.each( options.match( rnotwhite ) || [], function( _, flag ) {\n\t\tobject[ flag ] = true;\n\t});\n\treturn object;\n}\n\n/*\n * Create a callback list using the following parameters:\n *\n *\toptions: an optional list of space-separated options that will change how\n *\t\t\tthe callback list behaves or a more traditional option object\n *\n * By default a callback list will act like an event callback list and can be\n * \"fired\" multiple times.\n *\n * Possible options:\n *\n *\tonce:\t\t\twill ensure the callback list can only be fired once (like a Deferred)\n *\n *\tmemory:\t\t\twill keep track of previous values and will call any callback added\n *\t\t\t\t\tafter the list has been fired right away with the latest \"memorized\"\n *\t\t\t\t\tvalues (like a Deferred)\n *\n *\tunique:\t\t\twill ensure a callback can only be added once (no duplicate in the list)\n *\n *\tstopOnFalse:\tinterrupt callings when a callback returns false\n *\n */\njQuery.Callbacks = function( options ) {\n\n\t// Convert options from String-formatted to Object-formatted if needed\n\t// (we check in cache first)\n\toptions = typeof options === \"string\" ?\n\t\t( optionsCache[ options ] || createOptions( options ) ) :\n\t\tjQuery.extend( {}, options );\n\n\tvar // Last fire value (for non-forgettable lists)\n\t\tmemory,\n\t\t// Flag to know if list was already fired\n\t\tfired,\n\t\t// Flag to know if list is currently firing\n\t\tfiring,\n\t\t// First callback to fire (used internally by add and fireWith)\n\t\tfiringStart,\n\t\t// End of the loop when firing\n\t\tfiringLength,\n\t\t// Index of currently firing callback (modified by remove if needed)\n\t\tfiringIndex,\n\t\t// Actual callback list\n\t\tlist = [],\n\t\t// Stack of fire calls for repeatable lists\n\t\tstack = !options.once && [],\n\t\t// Fire callbacks\n\t\tfire = function( data ) {\n\t\t\tmemory = options.memory && data;\n\t\t\tfired = true;\n\t\t\tfiringIndex = firingStart || 0;\n\t\t\tfiringStart = 0;\n\t\t\tfiringLength = list.length;\n\t\t\tfiring = true;\n\t\t\tfor ( ; list && firingIndex < firingLength; firingIndex++ ) {\n\t\t\t\tif ( list[ firingIndex ].apply( data[ 0 ], data[ 1 ] ) === false && options.stopOnFalse ) {\n\t\t\t\t\tmemory = false; // To prevent further calls using add\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\tfiring = false;\n\t\t\tif ( list ) {\n\t\t\t\tif ( stack ) {\n\t\t\t\t\tif ( stack.length ) {\n\t\t\t\t\t\tfire( stack.shift() );\n\t\t\t\t\t}\n\t\t\t\t} else if ( memory ) {\n\t\t\t\t\tlist = [];\n\t\t\t\t} else {\n\t\t\t\t\tself.disable();\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\t// Actual Callbacks object\n\t\tself = {\n\t\t\t// Add a callback or a collection of callbacks to the list\n\t\t\tadd: function() {\n\t\t\t\tif ( list ) {\n\t\t\t\t\t// First, we save the current length\n\t\t\t\t\tvar start = list.length;\n\t\t\t\t\t(function add( args ) {\n\t\t\t\t\t\tjQuery.each( args, function( _, arg ) {\n\t\t\t\t\t\t\tvar type = jQuery.type( arg );\n\t\t\t\t\t\t\tif ( type === \"function\" ) {\n\t\t\t\t\t\t\t\tif ( !options.unique || !self.has( arg ) ) {\n\t\t\t\t\t\t\t\t\tlist.push( arg );\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} else if ( arg && arg.length && type !== \"string\" ) {\n\t\t\t\t\t\t\t\t// Inspect recursively\n\t\t\t\t\t\t\t\tadd( arg );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t});\n\t\t\t\t\t})( arguments );\n\t\t\t\t\t// Do we need to add the callbacks to the\n\t\t\t\t\t// current firing batch?\n\t\t\t\t\tif ( firing ) {\n\t\t\t\t\t\tfiringLength = list.length;\n\t\t\t\t\t// With memory, if we're not firing then\n\t\t\t\t\t// we should call right away\n\t\t\t\t\t} else if ( memory ) {\n\t\t\t\t\t\tfiringStart = start;\n\t\t\t\t\t\tfire( memory );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn this;\n\t\t\t},\n\t\t\t// Remove a callback from the list\n\t\t\tremove: function() {\n\t\t\t\tif ( list ) {\n\t\t\t\t\tjQuery.each( arguments, function( _, arg ) {\n\t\t\t\t\t\tvar index;\n\t\t\t\t\t\twhile ( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) {\n\t\t\t\t\t\t\tlist.splice( index, 1 );\n\t\t\t\t\t\t\t// Handle firing indexes\n\t\t\t\t\t\t\tif ( firing ) {\n\t\t\t\t\t\t\t\tif ( index <= firingLength ) {\n\t\t\t\t\t\t\t\t\tfiringLength--;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tif ( index <= firingIndex ) {\n\t\t\t\t\t\t\t\t\tfiringIndex--;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\treturn this;\n\t\t\t},\n\t\t\t// Check if a given callback is in the list.\n\t\t\t// If no argument is given, return whether or not list has callbacks attached.\n\t\t\thas: function( fn ) {\n\t\t\t\treturn fn ? jQuery.inArray( fn, list ) > -1 : !!( list && list.length );\n\t\t\t},\n\t\t\t// Remove all callbacks from the list\n\t\t\tempty: function() {\n\t\t\t\tlist = [];\n\t\t\t\tfiringLength = 0;\n\t\t\t\treturn this;\n\t\t\t},\n\t\t\t// Have the list do nothing anymore\n\t\t\tdisable: function() {\n\t\t\t\tlist = stack = memory = undefined;\n\t\t\t\treturn this;\n\t\t\t},\n\t\t\t// Is it disabled?\n\t\t\tdisabled: function() {\n\t\t\t\treturn !list;\n\t\t\t},\n\t\t\t// Lock the list in its current state\n\t\t\tlock: function() {\n\t\t\t\tstack = undefined;\n\t\t\t\tif ( !memory ) {\n\t\t\t\t\tself.disable();\n\t\t\t\t}\n\t\t\t\treturn this;\n\t\t\t},\n\t\t\t// Is it locked?\n\t\t\tlocked: function() {\n\t\t\t\treturn !stack;\n\t\t\t},\n\t\t\t// Call all callbacks with the given context and arguments\n\t\t\tfireWith: function( context, args ) {\n\t\t\t\tif ( list && ( !fired || stack ) ) {\n\t\t\t\t\targs = args || [];\n\t\t\t\t\targs = [ context, args.slice ? args.slice() : args ];\n\t\t\t\t\tif ( firing ) {\n\t\t\t\t\t\tstack.push( args );\n\t\t\t\t\t} else {\n\t\t\t\t\t\tfire( args );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn this;\n\t\t\t},\n\t\t\t// Call all the callbacks with the given arguments\n\t\t\tfire: function() {\n\t\t\t\tself.fireWith( this, arguments );\n\t\t\t\treturn this;\n\t\t\t},\n\t\t\t// To know if the callbacks have already been called at least once\n\t\t\tfired: function() {\n\t\t\t\treturn !!fired;\n\t\t\t}\n\t\t};\n\n\treturn self;\n};\n\n\njQuery.extend({\n\n\tDeferred: function( func ) {\n\t\tvar tuples = [\n\t\t\t\t// action, add listener, listener list, final state\n\t\t\t\t[ \"resolve\", \"done\", jQuery.Callbacks(\"once memory\"), \"resolved\" ],\n\t\t\t\t[ \"reject\", \"fail\", jQuery.Callbacks(\"once memory\"), \"rejected\" ],\n\t\t\t\t[ \"notify\", \"progress\", jQuery.Callbacks(\"memory\") ]\n\t\t\t],\n\t\t\tstate = \"pending\",\n\t\t\tpromise = {\n\t\t\t\tstate: function() {\n\t\t\t\t\treturn state;\n\t\t\t\t},\n\t\t\t\talways: function() {\n\t\t\t\t\tdeferred.done( arguments ).fail( arguments );\n\t\t\t\t\treturn this;\n\t\t\t\t},\n\t\t\t\tthen: function( /* fnDone, fnFail, fnProgress */ ) {\n\t\t\t\t\tvar fns = arguments;\n\t\t\t\t\treturn jQuery.Deferred(function( newDefer ) {\n\t\t\t\t\t\tjQuery.each( tuples, function( i, tuple ) {\n\t\t\t\t\t\t\tvar fn = jQuery.isFunction( fns[ i ] ) && fns[ i ];\n\t\t\t\t\t\t\t// deferred[ done | fail | progress ] for forwarding actions to newDefer\n\t\t\t\t\t\t\tdeferred[ tuple[1] ](function() {\n\t\t\t\t\t\t\t\tvar returned = fn && fn.apply( this, arguments );\n\t\t\t\t\t\t\t\tif ( returned && jQuery.isFunction( returned.promise ) ) {\n\t\t\t\t\t\t\t\t\treturned.promise()\n\t\t\t\t\t\t\t\t\t\t.done( newDefer.resolve )\n\t\t\t\t\t\t\t\t\t\t.fail( newDefer.reject )\n\t\t\t\t\t\t\t\t\t\t.progress( newDefer.notify );\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tnewDefer[ tuple[ 0 ] + \"With\" ]( this === promise ? newDefer.promise() : this, fn ? [ returned ] : arguments );\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t});\n\t\t\t\t\t\tfns = null;\n\t\t\t\t\t}).promise();\n\t\t\t\t},\n\t\t\t\t// Get a promise for this deferred\n\t\t\t\t// If obj is provided, the promise aspect is added to the object\n\t\t\t\tpromise: function( obj ) {\n\t\t\t\t\treturn obj != null ? jQuery.extend( obj, promise ) : promise;\n\t\t\t\t}\n\t\t\t},\n\t\t\tdeferred = {};\n\n\t\t// Keep pipe for back-compat\n\t\tpromise.pipe = promise.then;\n\n\t\t// Add list-specific methods\n\t\tjQuery.each( tuples, function( i, tuple ) {\n\t\t\tvar list = tuple[ 2 ],\n\t\t\t\tstateString = tuple[ 3 ];\n\n\t\t\t// promise[ done | fail | progress ] = list.add\n\t\t\tpromise[ tuple[1] ] = list.add;\n\n\t\t\t// Handle state\n\t\t\tif ( stateString ) {\n\t\t\t\tlist.add(function() {\n\t\t\t\t\t// state = [ resolved | rejected ]\n\t\t\t\t\tstate = stateString;\n\n\t\t\t\t// [ reject_list | resolve_list ].disable; progress_list.lock\n\t\t\t\t}, tuples[ i ^ 1 ][ 2 ].disable, tuples[ 2 ][ 2 ].lock );\n\t\t\t}\n\n\t\t\t// deferred[ resolve | reject | notify ]\n\t\t\tdeferred[ tuple[0] ] = function() {\n\t\t\t\tdeferred[ tuple[0] + \"With\" ]( this === deferred ? promise : this, arguments );\n\t\t\t\treturn this;\n\t\t\t};\n\t\t\tdeferred[ tuple[0] + \"With\" ] = list.fireWith;\n\t\t});\n\n\t\t// Make the deferred a promise\n\t\tpromise.promise( deferred );\n\n\t\t// Call given func if any\n\t\tif ( func ) {\n\t\t\tfunc.call( deferred, deferred );\n\t\t}\n\n\t\t// All done!\n\t\treturn deferred;\n\t},\n\n\t// Deferred helper\n\twhen: function( subordinate /* , ..., subordinateN */ ) {\n\t\tvar i = 0,\n\t\t\tresolveValues = slice.call( arguments ),\n\t\t\tlength = resolveValues.length,\n\n\t\t\t// the count of uncompleted subordinates\n\t\t\tremaining = length !== 1 || ( subordinate && jQuery.isFunction( subordinate.promise ) ) ? length : 0,\n\n\t\t\t// the master Deferred. If resolveValues consist of only a single Deferred, just use that.\n\t\t\tdeferred = remaining === 1 ? subordinate : jQuery.Deferred(),\n\n\t\t\t// Update function for both resolve and progress values\n\t\t\tupdateFunc = function( i, contexts, values ) {\n\t\t\t\treturn function( value ) {\n\t\t\t\t\tcontexts[ i ] = this;\n\t\t\t\t\tvalues[ i ] = arguments.length > 1 ? slice.call( arguments ) : value;\n\t\t\t\t\tif ( values === progressValues ) {\n\t\t\t\t\t\tdeferred.notifyWith( contexts, values );\n\t\t\t\t\t} else if ( !( --remaining ) ) {\n\t\t\t\t\t\tdeferred.resolveWith( contexts, values );\n\t\t\t\t\t}\n\t\t\t\t};\n\t\t\t},\n\n\t\t\tprogressValues, progressContexts, resolveContexts;\n\n\t\t// add listeners to Deferred subordinates; treat others as resolved\n\t\tif ( length > 1 ) {\n\t\t\tprogressValues = new Array( length );\n\t\t\tprogressContexts = new Array( length );\n\t\t\tresolveContexts = new Array( length );\n\t\t\tfor ( ; i < length; i++ ) {\n\t\t\t\tif ( resolveValues[ i ] && jQuery.isFunction( resolveValues[ i ].promise ) ) {\n\t\t\t\t\tresolveValues[ i ].promise()\n\t\t\t\t\t\t.done( updateFunc( i, resolveContexts, resolveValues ) )\n\t\t\t\t\t\t.fail( deferred.reject )\n\t\t\t\t\t\t.progress( updateFunc( i, progressContexts, progressValues ) );\n\t\t\t\t} else {\n\t\t\t\t\t--remaining;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// if we're not waiting on anything, resolve the master\n\t\tif ( !remaining ) {\n\t\t\tdeferred.resolveWith( resolveContexts, resolveValues );\n\t\t}\n\n\t\treturn deferred.promise();\n\t}\n});\n\n\n// The deferred used on DOM ready\nvar readyList;\n\njQuery.fn.ready = function( fn ) {\n\t// Add the callback\n\tjQuery.ready.promise().done( fn );\n\n\treturn this;\n};\n\njQuery.extend({\n\t// Is the DOM ready to be used? Set to true once it occurs.\n\tisReady: false,\n\n\t// A counter to track how many items to wait for before\n\t// the ready event fires. See #6781\n\treadyWait: 1,\n\n\t// Hold (or release) the ready event\n\tholdReady: function( hold ) {\n\t\tif ( hold ) {\n\t\t\tjQuery.readyWait++;\n\t\t} else {\n\t\t\tjQuery.ready( true );\n\t\t}\n\t},\n\n\t// Handle when the DOM is ready\n\tready: function( wait ) {\n\n\t\t// Abort if there are pending holds or we're already ready\n\t\tif ( wait === true ? --jQuery.readyWait : jQuery.isReady ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Remember that the DOM is ready\n\t\tjQuery.isReady = true;\n\n\t\t// If a normal DOM Ready event fired, decrement, and wait if need be\n\t\tif ( wait !== true && --jQuery.readyWait > 0 ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// If there are functions bound, to execute\n\t\treadyList.resolveWith( document, [ jQuery ] );\n\n\t\t// Trigger any bound ready events\n\t\tif ( jQuery.fn.triggerHandler ) {\n\t\t\tjQuery( document ).triggerHandler( \"ready\" );\n\t\t\tjQuery( document ).off( \"ready\" );\n\t\t}\n\t}\n});\n\n/**\n * The ready event handler and self cleanup method\n */\nfunction completed() {\n\tdocument.removeEventListener( \"DOMContentLoaded\", completed, false );\n\twindow.removeEventListener( \"load\", completed, false );\n\tjQuery.ready();\n}\n\njQuery.ready.promise = function( obj ) {\n\tif ( !readyList ) {\n\n\t\treadyList = jQuery.Deferred();\n\n\t\t// Catch cases where $(document).ready() is called after the browser event has already occurred.\n\t\t// we once tried to use readyState \"interactive\" here, but it caused issues like the one\n\t\t// discovered by ChrisS here: http://bugs.jquery.com/ticket/12282#comment:15\n\t\tif ( document.readyState === \"complete\" ) {\n\t\t\t// Handle it asynchronously to allow scripts the opportunity to delay ready\n\t\t\tsetTimeout( jQuery.ready );\n\n\t\t} else {\n\n\t\t\t// Use the handy event callback\n\t\t\tdocument.addEventListener( \"DOMContentLoaded\", completed, false );\n\n\t\t\t// A fallback to window.onload, that will always work\n\t\t\twindow.addEventListener( \"load\", completed, false );\n\t\t}\n\t}\n\treturn readyList.promise( obj );\n};\n\n// Kick off the DOM ready check even if the user does not\njQuery.ready.promise();\n\n\n\n\n// Multifunctional method to get and set values of a collection\n// The value/s can optionally be executed if it's a function\nvar access = jQuery.access = function( elems, fn, key, value, chainable, emptyGet, raw ) {\n\tvar i = 0,\n\t\tlen = elems.length,\n\t\tbulk = key == null;\n\n\t// Sets many values\n\tif ( jQuery.type( key ) === \"object\" ) {\n\t\tchainable = true;\n\t\tfor ( i in key ) {\n\t\t\tjQuery.access( elems, fn, i, key[i], true, emptyGet, raw );\n\t\t}\n\n\t// Sets one value\n\t} else if ( value !== undefined ) {\n\t\tchainable = true;\n\n\t\tif ( !jQuery.isFunction( value ) ) {\n\t\t\traw = true;\n\t\t}\n\n\t\tif ( bulk ) {\n\t\t\t// Bulk operations run against the entire set\n\t\t\tif ( raw ) {\n\t\t\t\tfn.call( elems, value );\n\t\t\t\tfn = null;\n\n\t\t\t// ...except when executing function values\n\t\t\t} else {\n\t\t\t\tbulk = fn;\n\t\t\t\tfn = function( elem, key, value ) {\n\t\t\t\t\treturn bulk.call( jQuery( elem ), value );\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\tif ( fn ) {\n\t\t\tfor ( ; i < len; i++ ) {\n\t\t\t\tfn( elems[i], key, raw ? value : value.call( elems[i], i, fn( elems[i], key ) ) );\n\t\t\t}\n\t\t}\n\t}\n\n\treturn chainable ?\n\t\telems :\n\n\t\t// Gets\n\t\tbulk ?\n\t\t\tfn.call( elems ) :\n\t\t\tlen ? fn( elems[0], key ) : emptyGet;\n};\n\n\n/**\n * Determines whether an object can have data\n */\njQuery.acceptData = function( owner ) {\n\t// Accepts only:\n\t//  - Node\n\t//    - Node.ELEMENT_NODE\n\t//    - Node.DOCUMENT_NODE\n\t//  - Object\n\t//    - Any\n\t/* jshint -W018 */\n\treturn owner.nodeType === 1 || owner.nodeType === 9 || !( +owner.nodeType );\n};\n\n\nfunction Data() {\n\t// Support: Android < 4,\n\t// Old WebKit does not have Object.preventExtensions/freeze method,\n\t// return new empty object instead with no [[set]] accessor\n\tObject.defineProperty( this.cache = {}, 0, {\n\t\tget: function() {\n\t\t\treturn {};\n\t\t}\n\t});\n\n\tthis.expando = jQuery.expando + Math.random();\n}\n\nData.uid = 1;\nData.accepts = jQuery.acceptData;\n\nData.prototype = {\n\tkey: function( owner ) {\n\t\t// We can accept data for non-element nodes in modern browsers,\n\t\t// but we should not, see #8335.\n\t\t// Always return the key for a frozen object.\n\t\tif ( !Data.accepts( owner ) ) {\n\t\t\treturn 0;\n\t\t}\n\n\t\tvar descriptor = {},\n\t\t\t// Check if the owner object already has a cache key\n\t\t\tunlock = owner[ this.expando ];\n\n\t\t// If not, create one\n\t\tif ( !unlock ) {\n\t\t\tunlock = Data.uid++;\n\n\t\t\t// Secure it in a non-enumerable, non-writable property\n\t\t\ttry {\n\t\t\t\tdescriptor[ this.expando ] = { value: unlock };\n\t\t\t\tObject.defineProperties( owner, descriptor );\n\n\t\t\t// Support: Android < 4\n\t\t\t// Fallback to a less secure definition\n\t\t\t} catch ( e ) {\n\t\t\t\tdescriptor[ this.expando ] = unlock;\n\t\t\t\tjQuery.extend( owner, descriptor );\n\t\t\t}\n\t\t}\n\n\t\t// Ensure the cache object\n\t\tif ( !this.cache[ unlock ] ) {\n\t\t\tthis.cache[ unlock ] = {};\n\t\t}\n\n\t\treturn unlock;\n\t},\n\tset: function( owner, data, value ) {\n\t\tvar prop,\n\t\t\t// There may be an unlock assigned to this node,\n\t\t\t// if there is no entry for this \"owner\", create one inline\n\t\t\t// and set the unlock as though an owner entry had always existed\n\t\t\tunlock = this.key( owner ),\n\t\t\tcache = this.cache[ unlock ];\n\n\t\t// Handle: [ owner, key, value ] args\n\t\tif ( typeof data === \"string\" ) {\n\t\t\tcache[ data ] = value;\n\n\t\t// Handle: [ owner, { properties } ] args\n\t\t} else {\n\t\t\t// Fresh assignments by object are shallow copied\n\t\t\tif ( jQuery.isEmptyObject( cache ) ) {\n\t\t\t\tjQuery.extend( this.cache[ unlock ], data );\n\t\t\t// Otherwise, copy the properties one-by-one to the cache object\n\t\t\t} else {\n\t\t\t\tfor ( prop in data ) {\n\t\t\t\t\tcache[ prop ] = data[ prop ];\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn cache;\n\t},\n\tget: function( owner, key ) {\n\t\t// Either a valid cache is found, or will be created.\n\t\t// New caches will be created and the unlock returned,\n\t\t// allowing direct access to the newly created\n\t\t// empty data object. A valid owner object must be provided.\n\t\tvar cache = this.cache[ this.key( owner ) ];\n\n\t\treturn key === undefined ?\n\t\t\tcache : cache[ key ];\n\t},\n\taccess: function( owner, key, value ) {\n\t\tvar stored;\n\t\t// In cases where either:\n\t\t//\n\t\t//   1. No key was specified\n\t\t//   2. A string key was specified, but no value provided\n\t\t//\n\t\t// Take the \"read\" path and allow the get method to determine\n\t\t// which value to return, respectively either:\n\t\t//\n\t\t//   1. The entire cache object\n\t\t//   2. The data stored at the key\n\t\t//\n\t\tif ( key === undefined ||\n\t\t\t\t((key && typeof key === \"string\") && value === undefined) ) {\n\n\t\t\tstored = this.get( owner, key );\n\n\t\t\treturn stored !== undefined ?\n\t\t\t\tstored : this.get( owner, jQuery.camelCase(key) );\n\t\t}\n\n\t\t// [*]When the key is not a string, or both a key and value\n\t\t// are specified, set or extend (existing objects) with either:\n\t\t//\n\t\t//   1. An object of properties\n\t\t//   2. A key and value\n\t\t//\n\t\tthis.set( owner, key, value );\n\n\t\t// Since the \"set\" path can have two possible entry points\n\t\t// return the expected data based on which path was taken[*]\n\t\treturn value !== undefined ? value : key;\n\t},\n\tremove: function( owner, key ) {\n\t\tvar i, name, camel,\n\t\t\tunlock = this.key( owner ),\n\t\t\tcache = this.cache[ unlock ];\n\n\t\tif ( key === undefined ) {\n\t\t\tthis.cache[ unlock ] = {};\n\n\t\t} else {\n\t\t\t// Support array or space separated string of keys\n\t\t\tif ( jQuery.isArray( key ) ) {\n\t\t\t\t// If \"name\" is an array of keys...\n\t\t\t\t// When data is initially created, via (\"key\", \"val\") signature,\n\t\t\t\t// keys will be converted to camelCase.\n\t\t\t\t// Since there is no way to tell _how_ a key was added, remove\n\t\t\t\t// both plain key and camelCase key. #12786\n\t\t\t\t// This will only penalize the array argument path.\n\t\t\t\tname = key.concat( key.map( jQuery.camelCase ) );\n\t\t\t} else {\n\t\t\t\tcamel = jQuery.camelCase( key );\n\t\t\t\t// Try the string as a key before any manipulation\n\t\t\t\tif ( key in cache ) {\n\t\t\t\t\tname = [ key, camel ];\n\t\t\t\t} else {\n\t\t\t\t\t// If a key with the spaces exists, use it.\n\t\t\t\t\t// Otherwise, create an array by matching non-whitespace\n\t\t\t\t\tname = camel;\n\t\t\t\t\tname = name in cache ?\n\t\t\t\t\t\t[ name ] : ( name.match( rnotwhite ) || [] );\n\t\t\t\t}\n\t\t\t}\n\n\t\t\ti = name.length;\n\t\t\twhile ( i-- ) {\n\t\t\t\tdelete cache[ name[ i ] ];\n\t\t\t}\n\t\t}\n\t},\n\thasData: function( owner ) {\n\t\treturn !jQuery.isEmptyObject(\n\t\t\tthis.cache[ owner[ this.expando ] ] || {}\n\t\t);\n\t},\n\tdiscard: function( owner ) {\n\t\tif ( owner[ this.expando ] ) {\n\t\t\tdelete this.cache[ owner[ this.expando ] ];\n\t\t}\n\t}\n};\nvar data_priv = new Data();\n\nvar data_user = new Data();\n\n\n\n/*\n\tImplementation Summary\n\n\t1. Enforce API surface and semantic compatibility with 1.9.x branch\n\t2. Improve the module's maintainability by reducing the storage\n\t\tpaths to a single mechanism.\n\t3. Use the same single mechanism to support \"private\" and \"user\" data.\n\t4. _Never_ expose \"private\" data to user code (TODO: Drop _data, _removeData)\n\t5. Avoid exposing implementation details on user objects (eg. expando properties)\n\t6. Provide a clear path for implementation upgrade to WeakMap in 2014\n*/\nvar rbrace = /^(?:\\{[\\w\\W]*\\}|\\[[\\w\\W]*\\])$/,\n\trmultiDash = /([A-Z])/g;\n\nfunction dataAttr( elem, key, data ) {\n\tvar name;\n\n\t// If nothing was found internally, try to fetch any\n\t// data from the HTML5 data-* attribute\n\tif ( data === undefined && elem.nodeType === 1 ) {\n\t\tname = \"data-\" + key.replace( rmultiDash, \"-$1\" ).toLowerCase();\n\t\tdata = elem.getAttribute( name );\n\n\t\tif ( typeof data === \"string\" ) {\n\t\t\ttry {\n\t\t\t\tdata = data === \"true\" ? true :\n\t\t\t\t\tdata === \"false\" ? false :\n\t\t\t\t\tdata === \"null\" ? null :\n\t\t\t\t\t// Only convert to a number if it doesn't change the string\n\t\t\t\t\t+data + \"\" === data ? +data :\n\t\t\t\t\trbrace.test( data ) ? jQuery.parseJSON( data ) :\n\t\t\t\t\tdata;\n\t\t\t} catch( e ) {}\n\n\t\t\t// Make sure we set the data so it isn't changed later\n\t\t\tdata_user.set( elem, key, data );\n\t\t} else {\n\t\t\tdata = undefined;\n\t\t}\n\t}\n\treturn data;\n}\n\njQuery.extend({\n\thasData: function( elem ) {\n\t\treturn data_user.hasData( elem ) || data_priv.hasData( elem );\n\t},\n\n\tdata: function( elem, name, data ) {\n\t\treturn data_user.access( elem, name, data );\n\t},\n\n\tremoveData: function( elem, name ) {\n\t\tdata_user.remove( elem, name );\n\t},\n\n\t// TODO: Now that all calls to _data and _removeData have been replaced\n\t// with direct calls to data_priv methods, these can be deprecated.\n\t_data: function( elem, name, data ) {\n\t\treturn data_priv.access( elem, name, data );\n\t},\n\n\t_removeData: function( elem, name ) {\n\t\tdata_priv.remove( elem, name );\n\t}\n});\n\njQuery.fn.extend({\n\tdata: function( key, value ) {\n\t\tvar i, name, data,\n\t\t\telem = this[ 0 ],\n\t\t\tattrs = elem && elem.attributes;\n\n\t\t// Gets all values\n\t\tif ( key === undefined ) {\n\t\t\tif ( this.length ) {\n\t\t\t\tdata = data_user.get( elem );\n\n\t\t\t\tif ( elem.nodeType === 1 && !data_priv.get( elem, \"hasDataAttrs\" ) ) {\n\t\t\t\t\ti = attrs.length;\n\t\t\t\t\twhile ( i-- ) {\n\n\t\t\t\t\t\t// Support: IE11+\n\t\t\t\t\t\t// The attrs elements can be null (#14894)\n\t\t\t\t\t\tif ( attrs[ i ] ) {\n\t\t\t\t\t\t\tname = attrs[ i ].name;\n\t\t\t\t\t\t\tif ( name.indexOf( \"data-\" ) === 0 ) {\n\t\t\t\t\t\t\t\tname = jQuery.camelCase( name.slice(5) );\n\t\t\t\t\t\t\t\tdataAttr( elem, name, data[ name ] );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tdata_priv.set( elem, \"hasDataAttrs\", true );\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn data;\n\t\t}\n\n\t\t// Sets multiple values\n\t\tif ( typeof key === \"object\" ) {\n\t\t\treturn this.each(function() {\n\t\t\t\tdata_user.set( this, key );\n\t\t\t});\n\t\t}\n\n\t\treturn access( this, function( value ) {\n\t\t\tvar data,\n\t\t\t\tcamelKey = jQuery.camelCase( key );\n\n\t\t\t// The calling jQuery object (element matches) is not empty\n\t\t\t// (and therefore has an element appears at this[ 0 ]) and the\n\t\t\t// `value` parameter was not undefined. An empty jQuery object\n\t\t\t// will result in `undefined` for elem = this[ 0 ] which will\n\t\t\t// throw an exception if an attempt to read a data cache is made.\n\t\t\tif ( elem && value === undefined ) {\n\t\t\t\t// Attempt to get data from the cache\n\t\t\t\t// with the key as-is\n\t\t\t\tdata = data_user.get( elem, key );\n\t\t\t\tif ( data !== undefined ) {\n\t\t\t\t\treturn data;\n\t\t\t\t}\n\n\t\t\t\t// Attempt to get data from the cache\n\t\t\t\t// with the key camelized\n\t\t\t\tdata = data_user.get( elem, camelKey );\n\t\t\t\tif ( data !== undefined ) {\n\t\t\t\t\treturn data;\n\t\t\t\t}\n\n\t\t\t\t// Attempt to \"discover\" the data in\n\t\t\t\t// HTML5 custom data-* attrs\n\t\t\t\tdata = dataAttr( elem, camelKey, undefined );\n\t\t\t\tif ( data !== undefined ) {\n\t\t\t\t\treturn data;\n\t\t\t\t}\n\n\t\t\t\t// We tried really hard, but the data doesn't exist.\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Set the data...\n\t\t\tthis.each(function() {\n\t\t\t\t// First, attempt to store a copy or reference of any\n\t\t\t\t// data that might've been store with a camelCased key.\n\t\t\t\tvar data = data_user.get( this, camelKey );\n\n\t\t\t\t// For HTML5 data-* attribute interop, we have to\n\t\t\t\t// store property names with dashes in a camelCase form.\n\t\t\t\t// This might not apply to all properties...*\n\t\t\t\tdata_user.set( this, camelKey, value );\n\n\t\t\t\t// *... In the case of properties that might _actually_\n\t\t\t\t// have dashes, we need to also store a copy of that\n\t\t\t\t// unchanged property.\n\t\t\t\tif ( key.indexOf(\"-\") !== -1 && data !== undefined ) {\n\t\t\t\t\tdata_user.set( this, key, value );\n\t\t\t\t}\n\t\t\t});\n\t\t}, null, value, arguments.length > 1, null, true );\n\t},\n\n\tremoveData: function( key ) {\n\t\treturn this.each(function() {\n\t\t\tdata_user.remove( this, key );\n\t\t});\n\t}\n});\n\n\njQuery.extend({\n\tqueue: function( elem, type, data ) {\n\t\tvar queue;\n\n\t\tif ( elem ) {\n\t\t\ttype = ( type || \"fx\" ) + \"queue\";\n\t\t\tqueue = data_priv.get( elem, type );\n\n\t\t\t// Speed up dequeue by getting out quickly if this is just a lookup\n\t\t\tif ( data ) {\n\t\t\t\tif ( !queue || jQuery.isArray( data ) ) {\n\t\t\t\t\tqueue = data_priv.access( elem, type, jQuery.makeArray(data) );\n\t\t\t\t} else {\n\t\t\t\t\tqueue.push( data );\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn queue || [];\n\t\t}\n\t},\n\n\tdequeue: function( elem, type ) {\n\t\ttype = type || \"fx\";\n\n\t\tvar queue = jQuery.queue( elem, type ),\n\t\t\tstartLength = queue.length,\n\t\t\tfn = queue.shift(),\n\t\t\thooks = jQuery._queueHooks( elem, type ),\n\t\t\tnext = function() {\n\t\t\t\tjQuery.dequeue( elem, type );\n\t\t\t};\n\n\t\t// If the fx queue is dequeued, always remove the progress sentinel\n\t\tif ( fn === \"inprogress\" ) {\n\t\t\tfn = queue.shift();\n\t\t\tstartLength--;\n\t\t}\n\n\t\tif ( fn ) {\n\n\t\t\t// Add a progress sentinel to prevent the fx queue from being\n\t\t\t// automatically dequeued\n\t\t\tif ( type === \"fx\" ) {\n\t\t\t\tqueue.unshift( \"inprogress\" );\n\t\t\t}\n\n\t\t\t// clear up the last queue stop function\n\t\t\tdelete hooks.stop;\n\t\t\tfn.call( elem, next, hooks );\n\t\t}\n\n\t\tif ( !startLength && hooks ) {\n\t\t\thooks.empty.fire();\n\t\t}\n\t},\n\n\t// not intended for public consumption - generates a queueHooks object, or returns the current one\n\t_queueHooks: function( elem, type ) {\n\t\tvar key = type + \"queueHooks\";\n\t\treturn data_priv.get( elem, key ) || data_priv.access( elem, key, {\n\t\t\tempty: jQuery.Callbacks(\"once memory\").add(function() {\n\t\t\t\tdata_priv.remove( elem, [ type + \"queue\", key ] );\n\t\t\t})\n\t\t});\n\t}\n});\n\njQuery.fn.extend({\n\tqueue: function( type, data ) {\n\t\tvar setter = 2;\n\n\t\tif ( typeof type !== \"string\" ) {\n\t\t\tdata = type;\n\t\t\ttype = \"fx\";\n\t\t\tsetter--;\n\t\t}\n\n\t\tif ( arguments.length < setter ) {\n\t\t\treturn jQuery.queue( this[0], type );\n\t\t}\n\n\t\treturn data === undefined ?\n\t\t\tthis :\n\t\t\tthis.each(function() {\n\t\t\t\tvar queue = jQuery.queue( this, type, data );\n\n\t\t\t\t// ensure a hooks for this queue\n\t\t\t\tjQuery._queueHooks( this, type );\n\n\t\t\t\tif ( type === \"fx\" && queue[0] !== \"inprogress\" ) {\n\t\t\t\t\tjQuery.dequeue( this, type );\n\t\t\t\t}\n\t\t\t});\n\t},\n\tdequeue: function( type ) {\n\t\treturn this.each(function() {\n\t\t\tjQuery.dequeue( this, type );\n\t\t});\n\t},\n\tclearQueue: function( type ) {\n\t\treturn this.queue( type || \"fx\", [] );\n\t},\n\t// Get a promise resolved when queues of a certain type\n\t// are emptied (fx is the type by default)\n\tpromise: function( type, obj ) {\n\t\tvar tmp,\n\t\t\tcount = 1,\n\t\t\tdefer = jQuery.Deferred(),\n\t\t\telements = this,\n\t\t\ti = this.length,\n\t\t\tresolve = function() {\n\t\t\t\tif ( !( --count ) ) {\n\t\t\t\t\tdefer.resolveWith( elements, [ elements ] );\n\t\t\t\t}\n\t\t\t};\n\n\t\tif ( typeof type !== \"string\" ) {\n\t\t\tobj = type;\n\t\t\ttype = undefined;\n\t\t}\n\t\ttype = type || \"fx\";\n\n\t\twhile ( i-- ) {\n\t\t\ttmp = data_priv.get( elements[ i ], type + \"queueHooks\" );\n\t\t\tif ( tmp && tmp.empty ) {\n\t\t\t\tcount++;\n\t\t\t\ttmp.empty.add( resolve );\n\t\t\t}\n\t\t}\n\t\tresolve();\n\t\treturn defer.promise( obj );\n\t}\n});\nvar pnum = (/[+-]?(?:\\d*\\.|)\\d+(?:[eE][+-]?\\d+|)/).source;\n\nvar cssExpand = [ \"Top\", \"Right\", \"Bottom\", \"Left\" ];\n\nvar isHidden = function( elem, el ) {\n\t\t// isHidden might be called from jQuery#filter function;\n\t\t// in that case, element will be second argument\n\t\telem = el || elem;\n\t\treturn jQuery.css( elem, \"display\" ) === \"none\" || !jQuery.contains( elem.ownerDocument, elem );\n\t};\n\nvar rcheckableType = (/^(?:checkbox|radio)$/i);\n\n\n\n(function() {\n\tvar fragment = document.createDocumentFragment(),\n\t\tdiv = fragment.appendChild( document.createElement( \"div\" ) ),\n\t\tinput = document.createElement( \"input\" );\n\n\t// #11217 - WebKit loses check when the name is after the checked attribute\n\t// Support: Windows Web Apps (WWA)\n\t// `name` and `type` need .setAttribute for WWA\n\tinput.setAttribute( \"type\", \"radio\" );\n\tinput.setAttribute( \"checked\", \"checked\" );\n\tinput.setAttribute( \"name\", \"t\" );\n\n\tdiv.appendChild( input );\n\n\t// Support: Safari 5.1, iOS 5.1, Android 4.x, Android 2.3\n\t// old WebKit doesn't clone checked state correctly in fragments\n\tsupport.checkClone = div.cloneNode( true ).cloneNode( true ).lastChild.checked;\n\n\t// Make sure textarea (and checkbox) defaultValue is properly cloned\n\t// Support: IE9-IE11+\n\tdiv.innerHTML = \"<textarea>x</textarea>\";\n\tsupport.noCloneChecked = !!div.cloneNode( true ).lastChild.defaultValue;\n})();\nvar strundefined = typeof undefined;\n\n\n\nsupport.focusinBubbles = \"onfocusin\" in window;\n\n\nvar\n\trkeyEvent = /^key/,\n\trmouseEvent = /^(?:mouse|pointer|contextmenu)|click/,\n\trfocusMorph = /^(?:focusinfocus|focusoutblur)$/,\n\trtypenamespace = /^([^.]*)(?:\\.(.+)|)$/;\n\nfunction returnTrue() {\n\treturn true;\n}\n\nfunction returnFalse() {\n\treturn false;\n}\n\nfunction safeActiveElement() {\n\ttry {\n\t\treturn document.activeElement;\n\t} catch ( err ) { }\n}\n\n/*\n * Helper functions for managing events -- not part of the public interface.\n * Props to Dean Edwards' addEvent library for many of the ideas.\n */\njQuery.event = {\n\n\tglobal: {},\n\n\tadd: function( elem, types, handler, data, selector ) {\n\n\t\tvar handleObjIn, eventHandle, tmp,\n\t\t\tevents, t, handleObj,\n\t\t\tspecial, handlers, type, namespaces, origType,\n\t\t\telemData = data_priv.get( elem );\n\n\t\t// Don't attach events to noData or text/comment nodes (but allow plain objects)\n\t\tif ( !elemData ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Caller can pass in an object of custom data in lieu of the handler\n\t\tif ( handler.handler ) {\n\t\t\thandleObjIn = handler;\n\t\t\thandler = handleObjIn.handler;\n\t\t\tselector = handleObjIn.selector;\n\t\t}\n\n\t\t// Make sure that the handler has a unique ID, used to find/remove it later\n\t\tif ( !handler.guid ) {\n\t\t\thandler.guid = jQuery.guid++;\n\t\t}\n\n\t\t// Init the element's event structure and main handler, if this is the first\n\t\tif ( !(events = elemData.events) ) {\n\t\t\tevents = elemData.events = {};\n\t\t}\n\t\tif ( !(eventHandle = elemData.handle) ) {\n\t\t\teventHandle = elemData.handle = function( e ) {\n\t\t\t\t// Discard the second event of a jQuery.event.trigger() and\n\t\t\t\t// when an event is called after a page has unloaded\n\t\t\t\treturn typeof jQuery !== strundefined && jQuery.event.triggered !== e.type ?\n\t\t\t\t\tjQuery.event.dispatch.apply( elem, arguments ) : undefined;\n\t\t\t};\n\t\t}\n\n\t\t// Handle multiple events separated by a space\n\t\ttypes = ( types || \"\" ).match( rnotwhite ) || [ \"\" ];\n\t\tt = types.length;\n\t\twhile ( t-- ) {\n\t\t\ttmp = rtypenamespace.exec( types[t] ) || [];\n\t\t\ttype = origType = tmp[1];\n\t\t\tnamespaces = ( tmp[2] || \"\" ).split( \".\" ).sort();\n\n\t\t\t// There *must* be a type, no attaching namespace-only handlers\n\t\t\tif ( !type ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// If event changes its type, use the special event handlers for the changed type\n\t\t\tspecial = jQuery.event.special[ type ] || {};\n\n\t\t\t// If selector defined, determine special event api type, otherwise given type\n\t\t\ttype = ( selector ? special.delegateType : special.bindType ) || type;\n\n\t\t\t// Update special based on newly reset type\n\t\t\tspecial = jQuery.event.special[ type ] || {};\n\n\t\t\t// handleObj is passed to all event handlers\n\t\t\thandleObj = jQuery.extend({\n\t\t\t\ttype: type,\n\t\t\t\torigType: origType,\n\t\t\t\tdata: data,\n\t\t\t\thandler: handler,\n\t\t\t\tguid: handler.guid,\n\t\t\t\tselector: selector,\n\t\t\t\tneedsContext: selector && jQuery.expr.match.needsContext.test( selector ),\n\t\t\t\tnamespace: namespaces.join(\".\")\n\t\t\t}, handleObjIn );\n\n\t\t\t// Init the event handler queue if we're the first\n\t\t\tif ( !(handlers = events[ type ]) ) {\n\t\t\t\thandlers = events[ type ] = [];\n\t\t\t\thandlers.delegateCount = 0;\n\n\t\t\t\t// Only use addEventListener if the special events handler returns false\n\t\t\t\tif ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) {\n\t\t\t\t\tif ( elem.addEventListener ) {\n\t\t\t\t\t\telem.addEventListener( type, eventHandle, false );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif ( special.add ) {\n\t\t\t\tspecial.add.call( elem, handleObj );\n\n\t\t\t\tif ( !handleObj.handler.guid ) {\n\t\t\t\t\thandleObj.handler.guid = handler.guid;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Add to the element's handler list, delegates in front\n\t\t\tif ( selector ) {\n\t\t\t\thandlers.splice( handlers.delegateCount++, 0, handleObj );\n\t\t\t} else {\n\t\t\t\thandlers.push( handleObj );\n\t\t\t}\n\n\t\t\t// Keep track of which events have ever been used, for event optimization\n\t\t\tjQuery.event.global[ type ] = true;\n\t\t}\n\n\t},\n\n\t// Detach an event or set of events from an element\n\tremove: function( elem, types, handler, selector, mappedTypes ) {\n\n\t\tvar j, origCount, tmp,\n\t\t\tevents, t, handleObj,\n\t\t\tspecial, handlers, type, namespaces, origType,\n\t\t\telemData = data_priv.hasData( elem ) && data_priv.get( elem );\n\n\t\tif ( !elemData || !(events = elemData.events) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Once for each type.namespace in types; type may be omitted\n\t\ttypes = ( types || \"\" ).match( rnotwhite ) || [ \"\" ];\n\t\tt = types.length;\n\t\twhile ( t-- ) {\n\t\t\ttmp = rtypenamespace.exec( types[t] ) || [];\n\t\t\ttype = origType = tmp[1];\n\t\t\tnamespaces = ( tmp[2] || \"\" ).split( \".\" ).sort();\n\n\t\t\t// Unbind all events (on this namespace, if provided) for the element\n\t\t\tif ( !type ) {\n\t\t\t\tfor ( type in events ) {\n\t\t\t\t\tjQuery.event.remove( elem, type + types[ t ], handler, selector, true );\n\t\t\t\t}\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tspecial = jQuery.event.special[ type ] || {};\n\t\t\ttype = ( selector ? special.delegateType : special.bindType ) || type;\n\t\t\thandlers = events[ type ] || [];\n\t\t\ttmp = tmp[2] && new RegExp( \"(^|\\\\.)\" + namespaces.join(\"\\\\.(?:.*\\\\.|)\") + \"(\\\\.|$)\" );\n\n\t\t\t// Remove matching events\n\t\t\torigCount = j = handlers.length;\n\t\t\twhile ( j-- ) {\n\t\t\t\thandleObj = handlers[ j ];\n\n\t\t\t\tif ( ( mappedTypes || origType === handleObj.origType ) &&\n\t\t\t\t\t( !handler || handler.guid === handleObj.guid ) &&\n\t\t\t\t\t( !tmp || tmp.test( handleObj.namespace ) ) &&\n\t\t\t\t\t( !selector || selector === handleObj.selector || selector === \"**\" && handleObj.selector ) ) {\n\t\t\t\t\thandlers.splice( j, 1 );\n\n\t\t\t\t\tif ( handleObj.selector ) {\n\t\t\t\t\t\thandlers.delegateCount--;\n\t\t\t\t\t}\n\t\t\t\t\tif ( special.remove ) {\n\t\t\t\t\t\tspecial.remove.call( elem, handleObj );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Remove generic event handler if we removed something and no more handlers exist\n\t\t\t// (avoids potential for endless recursion during removal of special event handlers)\n\t\t\tif ( origCount && !handlers.length ) {\n\t\t\t\tif ( !special.teardown || special.teardown.call( elem, namespaces, elemData.handle ) === false ) {\n\t\t\t\t\tjQuery.removeEvent( elem, type, elemData.handle );\n\t\t\t\t}\n\n\t\t\t\tdelete events[ type ];\n\t\t\t}\n\t\t}\n\n\t\t// Remove the expando if it's no longer used\n\t\tif ( jQuery.isEmptyObject( events ) ) {\n\t\t\tdelete elemData.handle;\n\t\t\tdata_priv.remove( elem, \"events\" );\n\t\t}\n\t},\n\n\ttrigger: function( event, data, elem, onlyHandlers ) {\n\n\t\tvar i, cur, tmp, bubbleType, ontype, handle, special,\n\t\t\teventPath = [ elem || document ],\n\t\t\ttype = hasOwn.call( event, \"type\" ) ? event.type : event,\n\t\t\tnamespaces = hasOwn.call( event, \"namespace\" ) ? event.namespace.split(\".\") : [];\n\n\t\tcur = tmp = elem = elem || document;\n\n\t\t// Don't do events on text and comment nodes\n\t\tif ( elem.nodeType === 3 || elem.nodeType === 8 ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// focus/blur morphs to focusin/out; ensure we're not firing them right now\n\t\tif ( rfocusMorph.test( type + jQuery.event.triggered ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\tif ( type.indexOf(\".\") >= 0 ) {\n\t\t\t// Namespaced trigger; create a regexp to match event type in handle()\n\t\t\tnamespaces = type.split(\".\");\n\t\t\ttype = namespaces.shift();\n\t\t\tnamespaces.sort();\n\t\t}\n\t\tontype = type.indexOf(\":\") < 0 && \"on\" + type;\n\n\t\t// Caller can pass in a jQuery.Event object, Object, or just an event type string\n\t\tevent = event[ jQuery.expando ] ?\n\t\t\tevent :\n\t\t\tnew jQuery.Event( type, typeof event === \"object\" && event );\n\n\t\t// Trigger bitmask: & 1 for native handlers; & 2 for jQuery (always true)\n\t\tevent.isTrigger = onlyHandlers ? 2 : 3;\n\t\tevent.namespace = namespaces.join(\".\");\n\t\tevent.namespace_re = event.namespace ?\n\t\t\tnew RegExp( \"(^|\\\\.)\" + namespaces.join(\"\\\\.(?:.*\\\\.|)\") + \"(\\\\.|$)\" ) :\n\t\t\tnull;\n\n\t\t// Clean up the event in case it is being reused\n\t\tevent.result = undefined;\n\t\tif ( !event.target ) {\n\t\t\tevent.target = elem;\n\t\t}\n\n\t\t// Clone any incoming data and prepend the event, creating the handler arg list\n\t\tdata = data == null ?\n\t\t\t[ event ] :\n\t\t\tjQuery.makeArray( data, [ event ] );\n\n\t\t// Allow special events to draw outside the lines\n\t\tspecial = jQuery.event.special[ type ] || {};\n\t\tif ( !onlyHandlers && special.trigger && special.trigger.apply( elem, data ) === false ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Determine event propagation path in advance, per W3C events spec (#9951)\n\t\t// Bubble up to document, then to window; watch for a global ownerDocument var (#9724)\n\t\tif ( !onlyHandlers && !special.noBubble && !jQuery.isWindow( elem ) ) {\n\n\t\t\tbubbleType = special.delegateType || type;\n\t\t\tif ( !rfocusMorph.test( bubbleType + type ) ) {\n\t\t\t\tcur = cur.parentNode;\n\t\t\t}\n\t\t\tfor ( ; cur; cur = cur.parentNode ) {\n\t\t\t\teventPath.push( cur );\n\t\t\t\ttmp = cur;\n\t\t\t}\n\n\t\t\t// Only add window if we got to document (e.g., not plain obj or detached DOM)\n\t\t\tif ( tmp === (elem.ownerDocument || document) ) {\n\t\t\t\teventPath.push( tmp.defaultView || tmp.parentWindow || window );\n\t\t\t}\n\t\t}\n\n\t\t// Fire handlers on the event path\n\t\ti = 0;\n\t\twhile ( (cur = eventPath[i++]) && !event.isPropagationStopped() ) {\n\n\t\t\tevent.type = i > 1 ?\n\t\t\t\tbubbleType :\n\t\t\t\tspecial.bindType || type;\n\n\t\t\t// jQuery handler\n\t\t\thandle = ( data_priv.get( cur, \"events\" ) || {} )[ event.type ] && data_priv.get( cur, \"handle\" );\n\t\t\tif ( handle ) {\n\t\t\t\thandle.apply( cur, data );\n\t\t\t}\n\n\t\t\t// Native handler\n\t\t\thandle = ontype && cur[ ontype ];\n\t\t\tif ( handle && handle.apply && jQuery.acceptData( cur ) ) {\n\t\t\t\tevent.result = handle.apply( cur, data );\n\t\t\t\tif ( event.result === false ) {\n\t\t\t\t\tevent.preventDefault();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tevent.type = type;\n\n\t\t// If nobody prevented the default action, do it now\n\t\tif ( !onlyHandlers && !event.isDefaultPrevented() ) {\n\n\t\t\tif ( (!special._default || special._default.apply( eventPath.pop(), data ) === false) &&\n\t\t\t\tjQuery.acceptData( elem ) ) {\n\n\t\t\t\t// Call a native DOM method on the target with the same name name as the event.\n\t\t\t\t// Don't do default actions on window, that's where global variables be (#6170)\n\t\t\t\tif ( ontype && jQuery.isFunction( elem[ type ] ) && !jQuery.isWindow( elem ) ) {\n\n\t\t\t\t\t// Don't re-trigger an onFOO event when we call its FOO() method\n\t\t\t\t\ttmp = elem[ ontype ];\n\n\t\t\t\t\tif ( tmp ) {\n\t\t\t\t\t\telem[ ontype ] = null;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Prevent re-triggering of the same event, since we already bubbled it above\n\t\t\t\t\tjQuery.event.triggered = type;\n\t\t\t\t\telem[ type ]();\n\t\t\t\t\tjQuery.event.triggered = undefined;\n\n\t\t\t\t\tif ( tmp ) {\n\t\t\t\t\t\telem[ ontype ] = tmp;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn event.result;\n\t},\n\n\tdispatch: function( event ) {\n\n\t\t// Make a writable jQuery.Event from the native event object\n\t\tevent = jQuery.event.fix( event );\n\n\t\tvar i, j, ret, matched, handleObj,\n\t\t\thandlerQueue = [],\n\t\t\targs = slice.call( arguments ),\n\t\t\thandlers = ( data_priv.get( this, \"events\" ) || {} )[ event.type ] || [],\n\t\t\tspecial = jQuery.event.special[ event.type ] || {};\n\n\t\t// Use the fix-ed jQuery.Event rather than the (read-only) native event\n\t\targs[0] = event;\n\t\tevent.delegateTarget = this;\n\n\t\t// Call the preDispatch hook for the mapped type, and let it bail if desired\n\t\tif ( special.preDispatch && special.preDispatch.call( this, event ) === false ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Determine handlers\n\t\thandlerQueue = jQuery.event.handlers.call( this, event, handlers );\n\n\t\t// Run delegates first; they may want to stop propagation beneath us\n\t\ti = 0;\n\t\twhile ( (matched = handlerQueue[ i++ ]) && !event.isPropagationStopped() ) {\n\t\t\tevent.currentTarget = matched.elem;\n\n\t\t\tj = 0;\n\t\t\twhile ( (handleObj = matched.handlers[ j++ ]) && !event.isImmediatePropagationStopped() ) {\n\n\t\t\t\t// Triggered event must either 1) have no namespace, or\n\t\t\t\t// 2) have namespace(s) a subset or equal to those in the bound event (both can have no namespace).\n\t\t\t\tif ( !event.namespace_re || event.namespace_re.test( handleObj.namespace ) ) {\n\n\t\t\t\t\tevent.handleObj = handleObj;\n\t\t\t\t\tevent.data = handleObj.data;\n\n\t\t\t\t\tret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler )\n\t\t\t\t\t\t\t.apply( matched.elem, args );\n\n\t\t\t\t\tif ( ret !== undefined ) {\n\t\t\t\t\t\tif ( (event.result = ret) === false ) {\n\t\t\t\t\t\t\tevent.preventDefault();\n\t\t\t\t\t\t\tevent.stopPropagation();\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Call the postDispatch hook for the mapped type\n\t\tif ( special.postDispatch ) {\n\t\t\tspecial.postDispatch.call( this, event );\n\t\t}\n\n\t\treturn event.result;\n\t},\n\n\thandlers: function( event, handlers ) {\n\t\tvar i, matches, sel, handleObj,\n\t\t\thandlerQueue = [],\n\t\t\tdelegateCount = handlers.delegateCount,\n\t\t\tcur = event.target;\n\n\t\t// Find delegate handlers\n\t\t// Black-hole SVG <use> instance trees (#13180)\n\t\t// Avoid non-left-click bubbling in Firefox (#3861)\n\t\tif ( delegateCount && cur.nodeType && (!event.button || event.type !== \"click\") ) {\n\n\t\t\tfor ( ; cur !== this; cur = cur.parentNode || this ) {\n\n\t\t\t\t// Don't process clicks on disabled elements (#6911, #8165, #11382, #11764)\n\t\t\t\tif ( cur.disabled !== true || event.type !== \"click\" ) {\n\t\t\t\t\tmatches = [];\n\t\t\t\t\tfor ( i = 0; i < delegateCount; i++ ) {\n\t\t\t\t\t\thandleObj = handlers[ i ];\n\n\t\t\t\t\t\t// Don't conflict with Object.prototype properties (#13203)\n\t\t\t\t\t\tsel = handleObj.selector + \" \";\n\n\t\t\t\t\t\tif ( matches[ sel ] === undefined ) {\n\t\t\t\t\t\t\tmatches[ sel ] = handleObj.needsContext ?\n\t\t\t\t\t\t\t\tjQuery( sel, this ).index( cur ) >= 0 :\n\t\t\t\t\t\t\t\tjQuery.find( sel, this, null, [ cur ] ).length;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif ( matches[ sel ] ) {\n\t\t\t\t\t\t\tmatches.push( handleObj );\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif ( matches.length ) {\n\t\t\t\t\t\thandlerQueue.push({ elem: cur, handlers: matches });\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Add the remaining (directly-bound) handlers\n\t\tif ( delegateCount < handlers.length ) {\n\t\t\thandlerQueue.push({ elem: this, handlers: handlers.slice( delegateCount ) });\n\t\t}\n\n\t\treturn handlerQueue;\n\t},\n\n\t// Includes some event props shared by KeyEvent and MouseEvent\n\tprops: \"altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which\".split(\" \"),\n\n\tfixHooks: {},\n\n\tkeyHooks: {\n\t\tprops: \"char charCode key keyCode\".split(\" \"),\n\t\tfilter: function( event, original ) {\n\n\t\t\t// Add which for key events\n\t\t\tif ( event.which == null ) {\n\t\t\t\tevent.which = original.charCode != null ? original.charCode : original.keyCode;\n\t\t\t}\n\n\t\t\treturn event;\n\t\t}\n\t},\n\n\tmouseHooks: {\n\t\tprops: \"button buttons clientX clientY offsetX offsetY pageX pageY screenX screenY toElement\".split(\" \"),\n\t\tfilter: function( event, original ) {\n\t\t\tvar eventDoc, doc, body,\n\t\t\t\tbutton = original.button;\n\n\t\t\t// Calculate pageX/Y if missing and clientX/Y available\n\t\t\tif ( event.pageX == null && original.clientX != null ) {\n\t\t\t\teventDoc = event.target.ownerDocument || document;\n\t\t\t\tdoc = eventDoc.documentElement;\n\t\t\t\tbody = eventDoc.body;\n\n\t\t\t\tevent.pageX = original.clientX + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - ( doc && doc.clientLeft || body && body.clientLeft || 0 );\n\t\t\t\tevent.pageY = original.clientY + ( doc && doc.scrollTop  || body && body.scrollTop  || 0 ) - ( doc && doc.clientTop  || body && body.clientTop  || 0 );\n\t\t\t}\n\n\t\t\t// Add which for click: 1 === left; 2 === middle; 3 === right\n\t\t\t// Note: button is not normalized, so don't use it\n\t\t\tif ( !event.which && button !== undefined ) {\n\t\t\t\tevent.which = ( button & 1 ? 1 : ( button & 2 ? 3 : ( button & 4 ? 2 : 0 ) ) );\n\t\t\t}\n\n\t\t\treturn event;\n\t\t}\n\t},\n\n\tfix: function( event ) {\n\t\tif ( event[ jQuery.expando ] ) {\n\t\t\treturn event;\n\t\t}\n\n\t\t// Create a writable copy of the event object and normalize some properties\n\t\tvar i, prop, copy,\n\t\t\ttype = event.type,\n\t\t\toriginalEvent = event,\n\t\t\tfixHook = this.fixHooks[ type ];\n\n\t\tif ( !fixHook ) {\n\t\t\tthis.fixHooks[ type ] = fixHook =\n\t\t\t\trmouseEvent.test( type ) ? this.mouseHooks :\n\t\t\t\trkeyEvent.test( type ) ? this.keyHooks :\n\t\t\t\t{};\n\t\t}\n\t\tcopy = fixHook.props ? this.props.concat( fixHook.props ) : this.props;\n\n\t\tevent = new jQuery.Event( originalEvent );\n\n\t\ti = copy.length;\n\t\twhile ( i-- ) {\n\t\t\tprop = copy[ i ];\n\t\t\tevent[ prop ] = originalEvent[ prop ];\n\t\t}\n\n\t\t// Support: Cordova 2.5 (WebKit) (#13255)\n\t\t// All events should have a target; Cordova deviceready doesn't\n\t\tif ( !event.target ) {\n\t\t\tevent.target = document;\n\t\t}\n\n\t\t// Support: Safari 6.0+, Chrome < 28\n\t\t// Target should not be a text node (#504, #13143)\n\t\tif ( event.target.nodeType === 3 ) {\n\t\t\tevent.target = event.target.parentNode;\n\t\t}\n\n\t\treturn fixHook.filter ? fixHook.filter( event, originalEvent ) : event;\n\t},\n\n\tspecial: {\n\t\tload: {\n\t\t\t// Prevent triggered image.load events from bubbling to window.load\n\t\t\tnoBubble: true\n\t\t},\n\t\tfocus: {\n\t\t\t// Fire native event if possible so blur/focus sequence is correct\n\t\t\ttrigger: function() {\n\t\t\t\tif ( this !== safeActiveElement() && this.focus ) {\n\t\t\t\t\tthis.focus();\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t},\n\t\t\tdelegateType: \"focusin\"\n\t\t},\n\t\tblur: {\n\t\t\ttrigger: function() {\n\t\t\t\tif ( this === safeActiveElement() && this.blur ) {\n\t\t\t\t\tthis.blur();\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t},\n\t\t\tdelegateType: \"focusout\"\n\t\t},\n\t\tclick: {\n\t\t\t// For checkbox, fire native event so checked state will be right\n\t\t\ttrigger: function() {\n\t\t\t\tif ( this.type === \"checkbox\" && this.click && jQuery.nodeName( this, \"input\" ) ) {\n\t\t\t\t\tthis.click();\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t},\n\n\t\t\t// For cross-browser consistency, don't fire native .click() on links\n\t\t\t_default: function( event ) {\n\t\t\t\treturn jQuery.nodeName( event.target, \"a\" );\n\t\t\t}\n\t\t},\n\n\t\tbeforeunload: {\n\t\t\tpostDispatch: function( event ) {\n\n\t\t\t\t// Support: Firefox 20+\n\t\t\t\t// Firefox doesn't alert if the returnValue field is not set.\n\t\t\t\tif ( event.result !== undefined && event.originalEvent ) {\n\t\t\t\t\tevent.originalEvent.returnValue = event.result;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t},\n\n\tsimulate: function( type, elem, event, bubble ) {\n\t\t// Piggyback on a donor event to simulate a different one.\n\t\t// Fake originalEvent to avoid donor's stopPropagation, but if the\n\t\t// simulated event prevents default then we do the same on the donor.\n\t\tvar e = jQuery.extend(\n\t\t\tnew jQuery.Event(),\n\t\t\tevent,\n\t\t\t{\n\t\t\t\ttype: type,\n\t\t\t\tisSimulated: true,\n\t\t\t\toriginalEvent: {}\n\t\t\t}\n\t\t);\n\t\tif ( bubble ) {\n\t\t\tjQuery.event.trigger( e, null, elem );\n\t\t} else {\n\t\t\tjQuery.event.dispatch.call( elem, e );\n\t\t}\n\t\tif ( e.isDefaultPrevented() ) {\n\t\t\tevent.preventDefault();\n\t\t}\n\t}\n};\n\njQuery.removeEvent = function( elem, type, handle ) {\n\tif ( elem.removeEventListener ) {\n\t\telem.removeEventListener( type, handle, false );\n\t}\n};\n\njQuery.Event = function( src, props ) {\n\t// Allow instantiation without the 'new' keyword\n\tif ( !(this instanceof jQuery.Event) ) {\n\t\treturn new jQuery.Event( src, props );\n\t}\n\n\t// Event object\n\tif ( src && src.type ) {\n\t\tthis.originalEvent = src;\n\t\tthis.type = src.type;\n\n\t\t// Events bubbling up the document may have been marked as prevented\n\t\t// by a handler lower down the tree; reflect the correct value.\n\t\tthis.isDefaultPrevented = src.defaultPrevented ||\n\t\t\t\tsrc.defaultPrevented === undefined &&\n\t\t\t\t// Support: Android < 4.0\n\t\t\t\tsrc.returnValue === false ?\n\t\t\treturnTrue :\n\t\t\treturnFalse;\n\n\t// Event type\n\t} else {\n\t\tthis.type = src;\n\t}\n\n\t// Put explicitly provided properties onto the event object\n\tif ( props ) {\n\t\tjQuery.extend( this, props );\n\t}\n\n\t// Create a timestamp if incoming event doesn't have one\n\tthis.timeStamp = src && src.timeStamp || jQuery.now();\n\n\t// Mark it as fixed\n\tthis[ jQuery.expando ] = true;\n};\n\n// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding\n// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html\njQuery.Event.prototype = {\n\tisDefaultPrevented: returnFalse,\n\tisPropagationStopped: returnFalse,\n\tisImmediatePropagationStopped: returnFalse,\n\n\tpreventDefault: function() {\n\t\tvar e = this.originalEvent;\n\n\t\tthis.isDefaultPrevented = returnTrue;\n\n\t\tif ( e && e.preventDefault ) {\n\t\t\te.preventDefault();\n\t\t}\n\t},\n\tstopPropagation: function() {\n\t\tvar e = this.originalEvent;\n\n\t\tthis.isPropagationStopped = returnTrue;\n\n\t\tif ( e && e.stopPropagation ) {\n\t\t\te.stopPropagation();\n\t\t}\n\t},\n\tstopImmediatePropagation: function() {\n\t\tvar e = this.originalEvent;\n\n\t\tthis.isImmediatePropagationStopped = returnTrue;\n\n\t\tif ( e && e.stopImmediatePropagation ) {\n\t\t\te.stopImmediatePropagation();\n\t\t}\n\n\t\tthis.stopPropagation();\n\t}\n};\n\n// Create mouseenter/leave events using mouseover/out and event-time checks\n// Support: Chrome 15+\njQuery.each({\n\tmouseenter: \"mouseover\",\n\tmouseleave: \"mouseout\",\n\tpointerenter: \"pointerover\",\n\tpointerleave: \"pointerout\"\n}, function( orig, fix ) {\n\tjQuery.event.special[ orig ] = {\n\t\tdelegateType: fix,\n\t\tbindType: fix,\n\n\t\thandle: function( event ) {\n\t\t\tvar ret,\n\t\t\t\ttarget = this,\n\t\t\t\trelated = event.relatedTarget,\n\t\t\t\thandleObj = event.handleObj;\n\n\t\t\t// For mousenter/leave call the handler if related is outside the target.\n\t\t\t// NB: No relatedTarget if the mouse left/entered the browser window\n\t\t\tif ( !related || (related !== target && !jQuery.contains( target, related )) ) {\n\t\t\t\tevent.type = handleObj.origType;\n\t\t\t\tret = handleObj.handler.apply( this, arguments );\n\t\t\t\tevent.type = fix;\n\t\t\t}\n\t\t\treturn ret;\n\t\t}\n\t};\n});\n\n// Create \"bubbling\" focus and blur events\n// Support: Firefox, Chrome, Safari\nif ( !support.focusinBubbles ) {\n\tjQuery.each({ focus: \"focusin\", blur: \"focusout\" }, function( orig, fix ) {\n\n\t\t// Attach a single capturing handler on the document while someone wants focusin/focusout\n\t\tvar handler = function( event ) {\n\t\t\t\tjQuery.event.simulate( fix, event.target, jQuery.event.fix( event ), true );\n\t\t\t};\n\n\t\tjQuery.event.special[ fix ] = {\n\t\t\tsetup: function() {\n\t\t\t\tvar doc = this.ownerDocument || this,\n\t\t\t\t\tattaches = data_priv.access( doc, fix );\n\n\t\t\t\tif ( !attaches ) {\n\t\t\t\t\tdoc.addEventListener( orig, handler, true );\n\t\t\t\t}\n\t\t\t\tdata_priv.access( doc, fix, ( attaches || 0 ) + 1 );\n\t\t\t},\n\t\t\tteardown: function() {\n\t\t\t\tvar doc = this.ownerDocument || this,\n\t\t\t\t\tattaches = data_priv.access( doc, fix ) - 1;\n\n\t\t\t\tif ( !attaches ) {\n\t\t\t\t\tdoc.removeEventListener( orig, handler, true );\n\t\t\t\t\tdata_priv.remove( doc, fix );\n\n\t\t\t\t} else {\n\t\t\t\t\tdata_priv.access( doc, fix, attaches );\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t});\n}\n\njQuery.fn.extend({\n\n\ton: function( types, selector, data, fn, /*INTERNAL*/ one ) {\n\t\tvar origFn, type;\n\n\t\t// Types can be a map of types/handlers\n\t\tif ( typeof types === \"object\" ) {\n\t\t\t// ( types-Object, selector, data )\n\t\t\tif ( typeof selector !== \"string\" ) {\n\t\t\t\t// ( types-Object, data )\n\t\t\t\tdata = data || selector;\n\t\t\t\tselector = undefined;\n\t\t\t}\n\t\t\tfor ( type in types ) {\n\t\t\t\tthis.on( type, selector, data, types[ type ], one );\n\t\t\t}\n\t\t\treturn this;\n\t\t}\n\n\t\tif ( data == null && fn == null ) {\n\t\t\t// ( types, fn )\n\t\t\tfn = selector;\n\t\t\tdata = selector = undefined;\n\t\t} else if ( fn == null ) {\n\t\t\tif ( typeof selector === \"string\" ) {\n\t\t\t\t// ( types, selector, fn )\n\t\t\t\tfn = data;\n\t\t\t\tdata = undefined;\n\t\t\t} else {\n\t\t\t\t// ( types, data, fn )\n\t\t\t\tfn = data;\n\t\t\t\tdata = selector;\n\t\t\t\tselector = undefined;\n\t\t\t}\n\t\t}\n\t\tif ( fn === false ) {\n\t\t\tfn = returnFalse;\n\t\t} else if ( !fn ) {\n\t\t\treturn this;\n\t\t}\n\n\t\tif ( one === 1 ) {\n\t\t\torigFn = fn;\n\t\t\tfn = function( event ) {\n\t\t\t\t// Can use an empty set, since event contains the info\n\t\t\t\tjQuery().off( event );\n\t\t\t\treturn origFn.apply( this, arguments );\n\t\t\t};\n\t\t\t// Use same guid so caller can remove using origFn\n\t\t\tfn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ );\n\t\t}\n\t\treturn this.each( function() {\n\t\t\tjQuery.event.add( this, types, fn, data, selector );\n\t\t});\n\t},\n\tone: function( types, selector, data, fn ) {\n\t\treturn this.on( types, selector, data, fn, 1 );\n\t},\n\toff: function( types, selector, fn ) {\n\t\tvar handleObj, type;\n\t\tif ( types && types.preventDefault && types.handleObj ) {\n\t\t\t// ( event )  dispatched jQuery.Event\n\t\t\thandleObj = types.handleObj;\n\t\t\tjQuery( types.delegateTarget ).off(\n\t\t\t\thandleObj.namespace ? handleObj.origType + \".\" + handleObj.namespace : handleObj.origType,\n\t\t\t\thandleObj.selector,\n\t\t\t\thandleObj.handler\n\t\t\t);\n\t\t\treturn this;\n\t\t}\n\t\tif ( typeof types === \"object\" ) {\n\t\t\t// ( types-object [, selector] )\n\t\t\tfor ( type in types ) {\n\t\t\t\tthis.off( type, selector, types[ type ] );\n\t\t\t}\n\t\t\treturn this;\n\t\t}\n\t\tif ( selector === false || typeof selector === \"function\" ) {\n\t\t\t// ( types [, fn] )\n\t\t\tfn = selector;\n\t\t\tselector = undefined;\n\t\t}\n\t\tif ( fn === false ) {\n\t\t\tfn = returnFalse;\n\t\t}\n\t\treturn this.each(function() {\n\t\t\tjQuery.event.remove( this, types, fn, selector );\n\t\t});\n\t},\n\n\ttrigger: function( type, data ) {\n\t\treturn this.each(function() {\n\t\t\tjQuery.event.trigger( type, data, this );\n\t\t});\n\t},\n\ttriggerHandler: function( type, data ) {\n\t\tvar elem = this[0];\n\t\tif ( elem ) {\n\t\t\treturn jQuery.event.trigger( type, data, elem, true );\n\t\t}\n\t}\n});\n\n\nvar\n\trxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\\w:]+)[^>]*)\\/>/gi,\n\trtagName = /<([\\w:]+)/,\n\trhtml = /<|&#?\\w+;/,\n\trnoInnerhtml = /<(?:script|style|link)/i,\n\t// checked=\"checked\" or checked\n\trchecked = /checked\\s*(?:[^=]|=\\s*.checked.)/i,\n\trscriptType = /^$|\\/(?:java|ecma)script/i,\n\trscriptTypeMasked = /^true\\/(.*)/,\n\trcleanScript = /^\\s*<!(?:\\[CDATA\\[|--)|(?:\\]\\]|--)>\\s*$/g,\n\n\t// We have to close these tags to support XHTML (#13200)\n\twrapMap = {\n\n\t\t// Support: IE 9\n\t\toption: [ 1, \"<select multiple='multiple'>\", \"</select>\" ],\n\n\t\tthead: [ 1, \"<table>\", \"</table>\" ],\n\t\tcol: [ 2, \"<table><colgroup>\", \"</colgroup></table>\" ],\n\t\ttr: [ 2, \"<table><tbody>\", \"</tbody></table>\" ],\n\t\ttd: [ 3, \"<table><tbody><tr>\", \"</tr></tbody></table>\" ],\n\n\t\t_default: [ 0, \"\", \"\" ]\n\t};\n\n// Support: IE 9\nwrapMap.optgroup = wrapMap.option;\n\nwrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead;\nwrapMap.th = wrapMap.td;\n\n// Support: 1.x compatibility\n// Manipulating tables requires a tbody\nfunction manipulationTarget( elem, content ) {\n\treturn jQuery.nodeName( elem, \"table\" ) &&\n\t\tjQuery.nodeName( content.nodeType !== 11 ? content : content.firstChild, \"tr\" ) ?\n\n\t\telem.getElementsByTagName(\"tbody\")[0] ||\n\t\t\telem.appendChild( elem.ownerDocument.createElement(\"tbody\") ) :\n\t\telem;\n}\n\n// Replace/restore the type attribute of script elements for safe DOM manipulation\nfunction disableScript( elem ) {\n\telem.type = (elem.getAttribute(\"type\") !== null) + \"/\" + elem.type;\n\treturn elem;\n}\nfunction restoreScript( elem ) {\n\tvar match = rscriptTypeMasked.exec( elem.type );\n\n\tif ( match ) {\n\t\telem.type = match[ 1 ];\n\t} else {\n\t\telem.removeAttribute(\"type\");\n\t}\n\n\treturn elem;\n}\n\n// Mark scripts as having already been evaluated\nfunction setGlobalEval( elems, refElements ) {\n\tvar i = 0,\n\t\tl = elems.length;\n\n\tfor ( ; i < l; i++ ) {\n\t\tdata_priv.set(\n\t\t\telems[ i ], \"globalEval\", !refElements || data_priv.get( refElements[ i ], \"globalEval\" )\n\t\t);\n\t}\n}\n\nfunction cloneCopyEvent( src, dest ) {\n\tvar i, l, type, pdataOld, pdataCur, udataOld, udataCur, events;\n\n\tif ( dest.nodeType !== 1 ) {\n\t\treturn;\n\t}\n\n\t// 1. Copy private data: events, handlers, etc.\n\tif ( data_priv.hasData( src ) ) {\n\t\tpdataOld = data_priv.access( src );\n\t\tpdataCur = data_priv.set( dest, pdataOld );\n\t\tevents = pdataOld.events;\n\n\t\tif ( events ) {\n\t\t\tdelete pdataCur.handle;\n\t\t\tpdataCur.events = {};\n\n\t\t\tfor ( type in events ) {\n\t\t\t\tfor ( i = 0, l = events[ type ].length; i < l; i++ ) {\n\t\t\t\t\tjQuery.event.add( dest, type, events[ type ][ i ] );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// 2. Copy user data\n\tif ( data_user.hasData( src ) ) {\n\t\tudataOld = data_user.access( src );\n\t\tudataCur = jQuery.extend( {}, udataOld );\n\n\t\tdata_user.set( dest, udataCur );\n\t}\n}\n\nfunction getAll( context, tag ) {\n\tvar ret = context.getElementsByTagName ? context.getElementsByTagName( tag || \"*\" ) :\n\t\t\tcontext.querySelectorAll ? context.querySelectorAll( tag || \"*\" ) :\n\t\t\t[];\n\n\treturn tag === undefined || tag && jQuery.nodeName( context, tag ) ?\n\t\tjQuery.merge( [ context ], ret ) :\n\t\tret;\n}\n\n// Support: IE >= 9\nfunction fixInput( src, dest ) {\n\tvar nodeName = dest.nodeName.toLowerCase();\n\n\t// Fails to persist the checked state of a cloned checkbox or radio button.\n\tif ( nodeName === \"input\" && rcheckableType.test( src.type ) ) {\n\t\tdest.checked = src.checked;\n\n\t// Fails to return the selected option to the default selected state when cloning options\n\t} else if ( nodeName === \"input\" || nodeName === \"textarea\" ) {\n\t\tdest.defaultValue = src.defaultValue;\n\t}\n}\n\njQuery.extend({\n\tclone: function( elem, dataAndEvents, deepDataAndEvents ) {\n\t\tvar i, l, srcElements, destElements,\n\t\t\tclone = elem.cloneNode( true ),\n\t\t\tinPage = jQuery.contains( elem.ownerDocument, elem );\n\n\t\t// Support: IE >= 9\n\t\t// Fix Cloning issues\n\t\tif ( !support.noCloneChecked && ( elem.nodeType === 1 || elem.nodeType === 11 ) &&\n\t\t\t\t!jQuery.isXMLDoc( elem ) ) {\n\n\t\t\t// We eschew Sizzle here for performance reasons: http://jsperf.com/getall-vs-sizzle/2\n\t\t\tdestElements = getAll( clone );\n\t\t\tsrcElements = getAll( elem );\n\n\t\t\tfor ( i = 0, l = srcElements.length; i < l; i++ ) {\n\t\t\t\tfixInput( srcElements[ i ], destElements[ i ] );\n\t\t\t}\n\t\t}\n\n\t\t// Copy the events from the original to the clone\n\t\tif ( dataAndEvents ) {\n\t\t\tif ( deepDataAndEvents ) {\n\t\t\t\tsrcElements = srcElements || getAll( elem );\n\t\t\t\tdestElements = destElements || getAll( clone );\n\n\t\t\t\tfor ( i = 0, l = srcElements.length; i < l; i++ ) {\n\t\t\t\t\tcloneCopyEvent( srcElements[ i ], destElements[ i ] );\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tcloneCopyEvent( elem, clone );\n\t\t\t}\n\t\t}\n\n\t\t// Preserve script evaluation history\n\t\tdestElements = getAll( clone, \"script\" );\n\t\tif ( destElements.length > 0 ) {\n\t\t\tsetGlobalEval( destElements, !inPage && getAll( elem, \"script\" ) );\n\t\t}\n\n\t\t// Return the cloned set\n\t\treturn clone;\n\t},\n\n\tbuildFragment: function( elems, context, scripts, selection ) {\n\t\tvar elem, tmp, tag, wrap, contains, j,\n\t\t\tfragment = context.createDocumentFragment(),\n\t\t\tnodes = [],\n\t\t\ti = 0,\n\t\t\tl = elems.length;\n\n\t\tfor ( ; i < l; i++ ) {\n\t\t\telem = elems[ i ];\n\n\t\t\tif ( elem || elem === 0 ) {\n\n\t\t\t\t// Add nodes directly\n\t\t\t\tif ( jQuery.type( elem ) === \"object\" ) {\n\t\t\t\t\t// Support: QtWebKit\n\t\t\t\t\t// jQuery.merge because push.apply(_, arraylike) throws\n\t\t\t\t\tjQuery.merge( nodes, elem.nodeType ? [ elem ] : elem );\n\n\t\t\t\t// Convert non-html into a text node\n\t\t\t\t} else if ( !rhtml.test( elem ) ) {\n\t\t\t\t\tnodes.push( context.createTextNode( elem ) );\n\n\t\t\t\t// Convert html into DOM nodes\n\t\t\t\t} else {\n\t\t\t\t\ttmp = tmp || fragment.appendChild( context.createElement(\"div\") );\n\n\t\t\t\t\t// Deserialize a standard representation\n\t\t\t\t\ttag = ( rtagName.exec( elem ) || [ \"\", \"\" ] )[ 1 ].toLowerCase();\n\t\t\t\t\twrap = wrapMap[ tag ] || wrapMap._default;\n\t\t\t\t\ttmp.innerHTML = wrap[ 1 ] + elem.replace( rxhtmlTag, \"<$1></$2>\" ) + wrap[ 2 ];\n\n\t\t\t\t\t// Descend through wrappers to the right content\n\t\t\t\t\tj = wrap[ 0 ];\n\t\t\t\t\twhile ( j-- ) {\n\t\t\t\t\t\ttmp = tmp.lastChild;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Support: QtWebKit\n\t\t\t\t\t// jQuery.merge because push.apply(_, arraylike) throws\n\t\t\t\t\tjQuery.merge( nodes, tmp.childNodes );\n\n\t\t\t\t\t// Remember the top-level container\n\t\t\t\t\ttmp = fragment.firstChild;\n\n\t\t\t\t\t// Fixes #12346\n\t\t\t\t\t// Support: Webkit, IE\n\t\t\t\t\ttmp.textContent = \"\";\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Remove wrapper from fragment\n\t\tfragment.textContent = \"\";\n\n\t\ti = 0;\n\t\twhile ( (elem = nodes[ i++ ]) ) {\n\n\t\t\t// #4087 - If origin and destination elements are the same, and this is\n\t\t\t// that element, do not do anything\n\t\t\tif ( selection && jQuery.inArray( elem, selection ) !== -1 ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tcontains = jQuery.contains( elem.ownerDocument, elem );\n\n\t\t\t// Append to fragment\n\t\t\ttmp = getAll( fragment.appendChild( elem ), \"script\" );\n\n\t\t\t// Preserve script evaluation history\n\t\t\tif ( contains ) {\n\t\t\t\tsetGlobalEval( tmp );\n\t\t\t}\n\n\t\t\t// Capture executables\n\t\t\tif ( scripts ) {\n\t\t\t\tj = 0;\n\t\t\t\twhile ( (elem = tmp[ j++ ]) ) {\n\t\t\t\t\tif ( rscriptType.test( elem.type || \"\" ) ) {\n\t\t\t\t\t\tscripts.push( elem );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn fragment;\n\t},\n\n\tcleanData: function( elems ) {\n\t\tvar data, elem, type, key,\n\t\t\tspecial = jQuery.event.special,\n\t\t\ti = 0;\n\n\t\tfor ( ; (elem = elems[ i ]) !== undefined; i++ ) {\n\t\t\tif ( jQuery.acceptData( elem ) ) {\n\t\t\t\tkey = elem[ data_priv.expando ];\n\n\t\t\t\tif ( key && (data = data_priv.cache[ key ]) ) {\n\t\t\t\t\tif ( data.events ) {\n\t\t\t\t\t\tfor ( type in data.events ) {\n\t\t\t\t\t\t\tif ( special[ type ] ) {\n\t\t\t\t\t\t\t\tjQuery.event.remove( elem, type );\n\n\t\t\t\t\t\t\t// This is a shortcut to avoid jQuery.event.remove's overhead\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tjQuery.removeEvent( elem, type, data.handle );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif ( data_priv.cache[ key ] ) {\n\t\t\t\t\t\t// Discard any remaining `private` data\n\t\t\t\t\t\tdelete data_priv.cache[ key ];\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Discard any remaining `user` data\n\t\t\tdelete data_user.cache[ elem[ data_user.expando ] ];\n\t\t}\n\t}\n});\n\njQuery.fn.extend({\n\ttext: function( value ) {\n\t\treturn access( this, function( value ) {\n\t\t\treturn value === undefined ?\n\t\t\t\tjQuery.text( this ) :\n\t\t\t\tthis.empty().each(function() {\n\t\t\t\t\tif ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {\n\t\t\t\t\t\tthis.textContent = value;\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t}, null, value, arguments.length );\n\t},\n\n\tappend: function() {\n\t\treturn this.domManip( arguments, function( elem ) {\n\t\t\tif ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {\n\t\t\t\tvar target = manipulationTarget( this, elem );\n\t\t\t\ttarget.appendChild( elem );\n\t\t\t}\n\t\t});\n\t},\n\n\tprepend: function() {\n\t\treturn this.domManip( arguments, function( elem ) {\n\t\t\tif ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {\n\t\t\t\tvar target = manipulationTarget( this, elem );\n\t\t\t\ttarget.insertBefore( elem, target.firstChild );\n\t\t\t}\n\t\t});\n\t},\n\n\tbefore: function() {\n\t\treturn this.domManip( arguments, function( elem ) {\n\t\t\tif ( this.parentNode ) {\n\t\t\t\tthis.parentNode.insertBefore( elem, this );\n\t\t\t}\n\t\t});\n\t},\n\n\tafter: function() {\n\t\treturn this.domManip( arguments, function( elem ) {\n\t\t\tif ( this.parentNode ) {\n\t\t\t\tthis.parentNode.insertBefore( elem, this.nextSibling );\n\t\t\t}\n\t\t});\n\t},\n\n\tremove: function( selector, keepData /* Internal Use Only */ ) {\n\t\tvar elem,\n\t\t\telems = selector ? jQuery.filter( selector, this ) : this,\n\t\t\ti = 0;\n\n\t\tfor ( ; (elem = elems[i]) != null; i++ ) {\n\t\t\tif ( !keepData && elem.nodeType === 1 ) {\n\t\t\t\tjQuery.cleanData( getAll( elem ) );\n\t\t\t}\n\n\t\t\tif ( elem.parentNode ) {\n\t\t\t\tif ( keepData && jQuery.contains( elem.ownerDocument, elem ) ) {\n\t\t\t\t\tsetGlobalEval( getAll( elem, \"script\" ) );\n\t\t\t\t}\n\t\t\t\telem.parentNode.removeChild( elem );\n\t\t\t}\n\t\t}\n\n\t\treturn this;\n\t},\n\n\tempty: function() {\n\t\tvar elem,\n\t\t\ti = 0;\n\n\t\tfor ( ; (elem = this[i]) != null; i++ ) {\n\t\t\tif ( elem.nodeType === 1 ) {\n\n\t\t\t\t// Prevent memory leaks\n\t\t\t\tjQuery.cleanData( getAll( elem, false ) );\n\n\t\t\t\t// Remove any remaining nodes\n\t\t\t\telem.textContent = \"\";\n\t\t\t}\n\t\t}\n\n\t\treturn this;\n\t},\n\n\tclone: function( dataAndEvents, deepDataAndEvents ) {\n\t\tdataAndEvents = dataAndEvents == null ? false : dataAndEvents;\n\t\tdeepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents;\n\n\t\treturn this.map(function() {\n\t\t\treturn jQuery.clone( this, dataAndEvents, deepDataAndEvents );\n\t\t});\n\t},\n\n\thtml: function( value ) {\n\t\treturn access( this, function( value ) {\n\t\t\tvar elem = this[ 0 ] || {},\n\t\t\t\ti = 0,\n\t\t\t\tl = this.length;\n\n\t\t\tif ( value === undefined && elem.nodeType === 1 ) {\n\t\t\t\treturn elem.innerHTML;\n\t\t\t}\n\n\t\t\t// See if we can take a shortcut and just use innerHTML\n\t\t\tif ( typeof value === \"string\" && !rnoInnerhtml.test( value ) &&\n\t\t\t\t!wrapMap[ ( rtagName.exec( value ) || [ \"\", \"\" ] )[ 1 ].toLowerCase() ] ) {\n\n\t\t\t\tvalue = value.replace( rxhtmlTag, \"<$1></$2>\" );\n\n\t\t\t\ttry {\n\t\t\t\t\tfor ( ; i < l; i++ ) {\n\t\t\t\t\t\telem = this[ i ] || {};\n\n\t\t\t\t\t\t// Remove element nodes and prevent memory leaks\n\t\t\t\t\t\tif ( elem.nodeType === 1 ) {\n\t\t\t\t\t\t\tjQuery.cleanData( getAll( elem, false ) );\n\t\t\t\t\t\t\telem.innerHTML = value;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\telem = 0;\n\n\t\t\t\t// If using innerHTML throws an exception, use the fallback method\n\t\t\t\t} catch( e ) {}\n\t\t\t}\n\n\t\t\tif ( elem ) {\n\t\t\t\tthis.empty().append( value );\n\t\t\t}\n\t\t}, null, value, arguments.length );\n\t},\n\n\treplaceWith: function() {\n\t\tvar arg = arguments[ 0 ];\n\n\t\t// Make the changes, replacing each context element with the new content\n\t\tthis.domManip( arguments, function( elem ) {\n\t\t\targ = this.parentNode;\n\n\t\t\tjQuery.cleanData( getAll( this ) );\n\n\t\t\tif ( arg ) {\n\t\t\t\targ.replaceChild( elem, this );\n\t\t\t}\n\t\t});\n\n\t\t// Force removal if there was no new content (e.g., from empty arguments)\n\t\treturn arg && (arg.length || arg.nodeType) ? this : this.remove();\n\t},\n\n\tdetach: function( selector ) {\n\t\treturn this.remove( selector, true );\n\t},\n\n\tdomManip: function( args, callback ) {\n\n\t\t// Flatten any nested arrays\n\t\targs = concat.apply( [], args );\n\n\t\tvar fragment, first, scripts, hasScripts, node, doc,\n\t\t\ti = 0,\n\t\t\tl = this.length,\n\t\t\tset = this,\n\t\t\tiNoClone = l - 1,\n\t\t\tvalue = args[ 0 ],\n\t\t\tisFunction = jQuery.isFunction( value );\n\n\t\t// We can't cloneNode fragments that contain checked, in WebKit\n\t\tif ( isFunction ||\n\t\t\t\t( l > 1 && typeof value === \"string\" &&\n\t\t\t\t\t!support.checkClone && rchecked.test( value ) ) ) {\n\t\t\treturn this.each(function( index ) {\n\t\t\t\tvar self = set.eq( index );\n\t\t\t\tif ( isFunction ) {\n\t\t\t\t\targs[ 0 ] = value.call( this, index, self.html() );\n\t\t\t\t}\n\t\t\t\tself.domManip( args, callback );\n\t\t\t});\n\t\t}\n\n\t\tif ( l ) {\n\t\t\tfragment = jQuery.buildFragment( args, this[ 0 ].ownerDocument, false, this );\n\t\t\tfirst = fragment.firstChild;\n\n\t\t\tif ( fragment.childNodes.length === 1 ) {\n\t\t\t\tfragment = first;\n\t\t\t}\n\n\t\t\tif ( first ) {\n\t\t\t\tscripts = jQuery.map( getAll( fragment, \"script\" ), disableScript );\n\t\t\t\thasScripts = scripts.length;\n\n\t\t\t\t// Use the original fragment for the last item instead of the first because it can end up\n\t\t\t\t// being emptied incorrectly in certain situations (#8070).\n\t\t\t\tfor ( ; i < l; i++ ) {\n\t\t\t\t\tnode = fragment;\n\n\t\t\t\t\tif ( i !== iNoClone ) {\n\t\t\t\t\t\tnode = jQuery.clone( node, true, true );\n\n\t\t\t\t\t\t// Keep references to cloned scripts for later restoration\n\t\t\t\t\t\tif ( hasScripts ) {\n\t\t\t\t\t\t\t// Support: QtWebKit\n\t\t\t\t\t\t\t// jQuery.merge because push.apply(_, arraylike) throws\n\t\t\t\t\t\t\tjQuery.merge( scripts, getAll( node, \"script\" ) );\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tcallback.call( this[ i ], node, i );\n\t\t\t\t}\n\n\t\t\t\tif ( hasScripts ) {\n\t\t\t\t\tdoc = scripts[ scripts.length - 1 ].ownerDocument;\n\n\t\t\t\t\t// Reenable scripts\n\t\t\t\t\tjQuery.map( scripts, restoreScript );\n\n\t\t\t\t\t// Evaluate executable scripts on first document insertion\n\t\t\t\t\tfor ( i = 0; i < hasScripts; i++ ) {\n\t\t\t\t\t\tnode = scripts[ i ];\n\t\t\t\t\t\tif ( rscriptType.test( node.type || \"\" ) &&\n\t\t\t\t\t\t\t!data_priv.access( node, \"globalEval\" ) && jQuery.contains( doc, node ) ) {\n\n\t\t\t\t\t\t\tif ( node.src ) {\n\t\t\t\t\t\t\t\t// Optional AJAX dependency, but won't run scripts if not present\n\t\t\t\t\t\t\t\tif ( jQuery._evalUrl ) {\n\t\t\t\t\t\t\t\t\tjQuery._evalUrl( node.src );\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tjQuery.globalEval( node.textContent.replace( rcleanScript, \"\" ) );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn this;\n\t}\n});\n\njQuery.each({\n\tappendTo: \"append\",\n\tprependTo: \"prepend\",\n\tinsertBefore: \"before\",\n\tinsertAfter: \"after\",\n\treplaceAll: \"replaceWith\"\n}, function( name, original ) {\n\tjQuery.fn[ name ] = function( selector ) {\n\t\tvar elems,\n\t\t\tret = [],\n\t\t\tinsert = jQuery( selector ),\n\t\t\tlast = insert.length - 1,\n\t\t\ti = 0;\n\n\t\tfor ( ; i <= last; i++ ) {\n\t\t\telems = i === last ? this : this.clone( true );\n\t\t\tjQuery( insert[ i ] )[ original ]( elems );\n\n\t\t\t// Support: QtWebKit\n\t\t\t// .get() because push.apply(_, arraylike) throws\n\t\t\tpush.apply( ret, elems.get() );\n\t\t}\n\n\t\treturn this.pushStack( ret );\n\t};\n});\n\n\nvar iframe,\n\telemdisplay = {};\n\n/**\n * Retrieve the actual display of a element\n * @param {String} name nodeName of the element\n * @param {Object} doc Document object\n */\n// Called only from within defaultDisplay\nfunction actualDisplay( name, doc ) {\n\tvar style,\n\t\telem = jQuery( doc.createElement( name ) ).appendTo( doc.body ),\n\n\t\t// getDefaultComputedStyle might be reliably used only on attached element\n\t\tdisplay = window.getDefaultComputedStyle && ( style = window.getDefaultComputedStyle( elem[ 0 ] ) ) ?\n\n\t\t\t// Use of this method is a temporary fix (more like optmization) until something better comes along,\n\t\t\t// since it was removed from specification and supported only in FF\n\t\t\tstyle.display : jQuery.css( elem[ 0 ], \"display\" );\n\n\t// We don't have any data stored on the element,\n\t// so use \"detach\" method as fast way to get rid of the element\n\telem.detach();\n\n\treturn display;\n}\n\n/**\n * Try to determine the default display value of an element\n * @param {String} nodeName\n */\nfunction defaultDisplay( nodeName ) {\n\tvar doc = document,\n\t\tdisplay = elemdisplay[ nodeName ];\n\n\tif ( !display ) {\n\t\tdisplay = actualDisplay( nodeName, doc );\n\n\t\t// If the simple way fails, read from inside an iframe\n\t\tif ( display === \"none\" || !display ) {\n\n\t\t\t// Use the already-created iframe if possible\n\t\t\tiframe = (iframe || jQuery( \"<iframe frameborder='0' width='0' height='0'/>\" )).appendTo( doc.documentElement );\n\n\t\t\t// Always write a new HTML skeleton so Webkit and Firefox don't choke on reuse\n\t\t\tdoc = iframe[ 0 ].contentDocument;\n\n\t\t\t// Support: IE\n\t\t\tdoc.write();\n\t\t\tdoc.close();\n\n\t\t\tdisplay = actualDisplay( nodeName, doc );\n\t\t\tiframe.detach();\n\t\t}\n\n\t\t// Store the correct default display\n\t\telemdisplay[ nodeName ] = display;\n\t}\n\n\treturn display;\n}\nvar rmargin = (/^margin/);\n\nvar rnumnonpx = new RegExp( \"^(\" + pnum + \")(?!px)[a-z%]+$\", \"i\" );\n\nvar getStyles = function( elem ) {\n\t\treturn elem.ownerDocument.defaultView.getComputedStyle( elem, null );\n\t};\n\n\n\nfunction curCSS( elem, name, computed ) {\n\tvar width, minWidth, maxWidth, ret,\n\t\tstyle = elem.style;\n\n\tcomputed = computed || getStyles( elem );\n\n\t// Support: IE9\n\t// getPropertyValue is only needed for .css('filter') in IE9, see #12537\n\tif ( computed ) {\n\t\tret = computed.getPropertyValue( name ) || computed[ name ];\n\t}\n\n\tif ( computed ) {\n\n\t\tif ( ret === \"\" && !jQuery.contains( elem.ownerDocument, elem ) ) {\n\t\t\tret = jQuery.style( elem, name );\n\t\t}\n\n\t\t// Support: iOS < 6\n\t\t// A tribute to the \"awesome hack by Dean Edwards\"\n\t\t// iOS < 6 (at least) returns percentage for a larger set of values, but width seems to be reliably pixels\n\t\t// this is against the CSSOM draft spec: http://dev.w3.org/csswg/cssom/#resolved-values\n\t\tif ( rnumnonpx.test( ret ) && rmargin.test( name ) ) {\n\n\t\t\t// Remember the original values\n\t\t\twidth = style.width;\n\t\t\tminWidth = style.minWidth;\n\t\t\tmaxWidth = style.maxWidth;\n\n\t\t\t// Put in the new values to get a computed value out\n\t\t\tstyle.minWidth = style.maxWidth = style.width = ret;\n\t\t\tret = computed.width;\n\n\t\t\t// Revert the changed values\n\t\t\tstyle.width = width;\n\t\t\tstyle.minWidth = minWidth;\n\t\t\tstyle.maxWidth = maxWidth;\n\t\t}\n\t}\n\n\treturn ret !== undefined ?\n\t\t// Support: IE\n\t\t// IE returns zIndex value as an integer.\n\t\tret + \"\" :\n\t\tret;\n}\n\n\nfunction addGetHookIf( conditionFn, hookFn ) {\n\t// Define the hook, we'll check on the first run if it's really needed.\n\treturn {\n\t\tget: function() {\n\t\t\tif ( conditionFn() ) {\n\t\t\t\t// Hook not needed (or it's not possible to use it due to missing dependency),\n\t\t\t\t// remove it.\n\t\t\t\t// Since there are no other hooks for marginRight, remove the whole object.\n\t\t\t\tdelete this.get;\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Hook needed; redefine it so that the support test is not executed again.\n\n\t\t\treturn (this.get = hookFn).apply( this, arguments );\n\t\t}\n\t};\n}\n\n\n(function() {\n\tvar pixelPositionVal, boxSizingReliableVal,\n\t\tdocElem = document.documentElement,\n\t\tcontainer = document.createElement( \"div\" ),\n\t\tdiv = document.createElement( \"div\" );\n\n\tif ( !div.style ) {\n\t\treturn;\n\t}\n\n\tdiv.style.backgroundClip = \"content-box\";\n\tdiv.cloneNode( true ).style.backgroundClip = \"\";\n\tsupport.clearCloneStyle = div.style.backgroundClip === \"content-box\";\n\n\tcontainer.style.cssText = \"border:0;width:0;height:0;top:0;left:-9999px;margin-top:1px;\" +\n\t\t\"position:absolute\";\n\tcontainer.appendChild( div );\n\n\t// Executing both pixelPosition & boxSizingReliable tests require only one layout\n\t// so they're executed at the same time to save the second computation.\n\tfunction computePixelPositionAndBoxSizingReliable() {\n\t\tdiv.style.cssText =\n\t\t\t// Support: Firefox<29, Android 2.3\n\t\t\t// Vendor-prefix box-sizing\n\t\t\t\"-webkit-box-sizing:border-box;-moz-box-sizing:border-box;\" +\n\t\t\t\"box-sizing:border-box;display:block;margin-top:1%;top:1%;\" +\n\t\t\t\"border:1px;padding:1px;width:4px;position:absolute\";\n\t\tdiv.innerHTML = \"\";\n\t\tdocElem.appendChild( container );\n\n\t\tvar divStyle = window.getComputedStyle( div, null );\n\t\tpixelPositionVal = divStyle.top !== \"1%\";\n\t\tboxSizingReliableVal = divStyle.width === \"4px\";\n\n\t\tdocElem.removeChild( container );\n\t}\n\n\t// Support: node.js jsdom\n\t// Don't assume that getComputedStyle is a property of the global object\n\tif ( window.getComputedStyle ) {\n\t\tjQuery.extend( support, {\n\t\t\tpixelPosition: function() {\n\t\t\t\t// This test is executed only once but we still do memoizing\n\t\t\t\t// since we can use the boxSizingReliable pre-computing.\n\t\t\t\t// No need to check if the test was already performed, though.\n\t\t\t\tcomputePixelPositionAndBoxSizingReliable();\n\t\t\t\treturn pixelPositionVal;\n\t\t\t},\n\t\t\tboxSizingReliable: function() {\n\t\t\t\tif ( boxSizingReliableVal == null ) {\n\t\t\t\t\tcomputePixelPositionAndBoxSizingReliable();\n\t\t\t\t}\n\t\t\t\treturn boxSizingReliableVal;\n\t\t\t},\n\t\t\treliableMarginRight: function() {\n\t\t\t\t// Support: Android 2.3\n\t\t\t\t// Check if div with explicit width and no margin-right incorrectly\n\t\t\t\t// gets computed margin-right based on width of container. (#3333)\n\t\t\t\t// WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right\n\t\t\t\t// This support function is only executed once so no memoizing is needed.\n\t\t\t\tvar ret,\n\t\t\t\t\tmarginDiv = div.appendChild( document.createElement( \"div\" ) );\n\n\t\t\t\t// Reset CSS: box-sizing; display; margin; border; padding\n\t\t\t\tmarginDiv.style.cssText = div.style.cssText =\n\t\t\t\t\t// Support: Firefox<29, Android 2.3\n\t\t\t\t\t// Vendor-prefix box-sizing\n\t\t\t\t\t\"-webkit-box-sizing:content-box;-moz-box-sizing:content-box;\" +\n\t\t\t\t\t\"box-sizing:content-box;display:block;margin:0;border:0;padding:0\";\n\t\t\t\tmarginDiv.style.marginRight = marginDiv.style.width = \"0\";\n\t\t\t\tdiv.style.width = \"1px\";\n\t\t\t\tdocElem.appendChild( container );\n\n\t\t\t\tret = !parseFloat( window.getComputedStyle( marginDiv, null ).marginRight );\n\n\t\t\t\tdocElem.removeChild( container );\n\n\t\t\t\treturn ret;\n\t\t\t}\n\t\t});\n\t}\n})();\n\n\n// A method for quickly swapping in/out CSS properties to get correct calculations.\njQuery.swap = function( elem, options, callback, args ) {\n\tvar ret, name,\n\t\told = {};\n\n\t// Remember the old values, and insert the new ones\n\tfor ( name in options ) {\n\t\told[ name ] = elem.style[ name ];\n\t\telem.style[ name ] = options[ name ];\n\t}\n\n\tret = callback.apply( elem, args || [] );\n\n\t// Revert the old values\n\tfor ( name in options ) {\n\t\telem.style[ name ] = old[ name ];\n\t}\n\n\treturn ret;\n};\n\n\nvar\n\t// swappable if display is none or starts with table except \"table\", \"table-cell\", or \"table-caption\"\n\t// see here for display values: https://developer.mozilla.org/en-US/docs/CSS/display\n\trdisplayswap = /^(none|table(?!-c[ea]).+)/,\n\trnumsplit = new RegExp( \"^(\" + pnum + \")(.*)$\", \"i\" ),\n\trrelNum = new RegExp( \"^([+-])=(\" + pnum + \")\", \"i\" ),\n\n\tcssShow = { position: \"absolute\", visibility: \"hidden\", display: \"block\" },\n\tcssNormalTransform = {\n\t\tletterSpacing: \"0\",\n\t\tfontWeight: \"400\"\n\t},\n\n\tcssPrefixes = [ \"Webkit\", \"O\", \"Moz\", \"ms\" ];\n\n// return a css property mapped to a potentially vendor prefixed property\nfunction vendorPropName( style, name ) {\n\n\t// shortcut for names that are not vendor prefixed\n\tif ( name in style ) {\n\t\treturn name;\n\t}\n\n\t// check for vendor prefixed names\n\tvar capName = name[0].toUpperCase() + name.slice(1),\n\t\torigName = name,\n\t\ti = cssPrefixes.length;\n\n\twhile ( i-- ) {\n\t\tname = cssPrefixes[ i ] + capName;\n\t\tif ( name in style ) {\n\t\t\treturn name;\n\t\t}\n\t}\n\n\treturn origName;\n}\n\nfunction setPositiveNumber( elem, value, subtract ) {\n\tvar matches = rnumsplit.exec( value );\n\treturn matches ?\n\t\t// Guard against undefined \"subtract\", e.g., when used as in cssHooks\n\t\tMath.max( 0, matches[ 1 ] - ( subtract || 0 ) ) + ( matches[ 2 ] || \"px\" ) :\n\t\tvalue;\n}\n\nfunction augmentWidthOrHeight( elem, name, extra, isBorderBox, styles ) {\n\tvar i = extra === ( isBorderBox ? \"border\" : \"content\" ) ?\n\t\t// If we already have the right measurement, avoid augmentation\n\t\t4 :\n\t\t// Otherwise initialize for horizontal or vertical properties\n\t\tname === \"width\" ? 1 : 0,\n\n\t\tval = 0;\n\n\tfor ( ; i < 4; i += 2 ) {\n\t\t// both box models exclude margin, so add it if we want it\n\t\tif ( extra === \"margin\" ) {\n\t\t\tval += jQuery.css( elem, extra + cssExpand[ i ], true, styles );\n\t\t}\n\n\t\tif ( isBorderBox ) {\n\t\t\t// border-box includes padding, so remove it if we want content\n\t\t\tif ( extra === \"content\" ) {\n\t\t\t\tval -= jQuery.css( elem, \"padding\" + cssExpand[ i ], true, styles );\n\t\t\t}\n\n\t\t\t// at this point, extra isn't border nor margin, so remove border\n\t\t\tif ( extra !== \"margin\" ) {\n\t\t\t\tval -= jQuery.css( elem, \"border\" + cssExpand[ i ] + \"Width\", true, styles );\n\t\t\t}\n\t\t} else {\n\t\t\t// at this point, extra isn't content, so add padding\n\t\t\tval += jQuery.css( elem, \"padding\" + cssExpand[ i ], true, styles );\n\n\t\t\t// at this point, extra isn't content nor padding, so add border\n\t\t\tif ( extra !== \"padding\" ) {\n\t\t\t\tval += jQuery.css( elem, \"border\" + cssExpand[ i ] + \"Width\", true, styles );\n\t\t\t}\n\t\t}\n\t}\n\n\treturn val;\n}\n\nfunction getWidthOrHeight( elem, name, extra ) {\n\n\t// Start with offset property, which is equivalent to the border-box value\n\tvar valueIsBorderBox = true,\n\t\tval = name === \"width\" ? elem.offsetWidth : elem.offsetHeight,\n\t\tstyles = getStyles( elem ),\n\t\tisBorderBox = jQuery.css( elem, \"boxSizing\", false, styles ) === \"border-box\";\n\n\t// some non-html elements return undefined for offsetWidth, so check for null/undefined\n\t// svg - https://bugzilla.mozilla.org/show_bug.cgi?id=649285\n\t// MathML - https://bugzilla.mozilla.org/show_bug.cgi?id=491668\n\tif ( val <= 0 || val == null ) {\n\t\t// Fall back to computed then uncomputed css if necessary\n\t\tval = curCSS( elem, name, styles );\n\t\tif ( val < 0 || val == null ) {\n\t\t\tval = elem.style[ name ];\n\t\t}\n\n\t\t// Computed unit is not pixels. Stop here and return.\n\t\tif ( rnumnonpx.test(val) ) {\n\t\t\treturn val;\n\t\t}\n\n\t\t// we need the check for style in case a browser which returns unreliable values\n\t\t// for getComputedStyle silently falls back to the reliable elem.style\n\t\tvalueIsBorderBox = isBorderBox &&\n\t\t\t( support.boxSizingReliable() || val === elem.style[ name ] );\n\n\t\t// Normalize \"\", auto, and prepare for extra\n\t\tval = parseFloat( val ) || 0;\n\t}\n\n\t// use the active box-sizing model to add/subtract irrelevant styles\n\treturn ( val +\n\t\taugmentWidthOrHeight(\n\t\t\telem,\n\t\t\tname,\n\t\t\textra || ( isBorderBox ? \"border\" : \"content\" ),\n\t\t\tvalueIsBorderBox,\n\t\t\tstyles\n\t\t)\n\t) + \"px\";\n}\n\nfunction showHide( elements, show ) {\n\tvar display, elem, hidden,\n\t\tvalues = [],\n\t\tindex = 0,\n\t\tlength = elements.length;\n\n\tfor ( ; index < length; index++ ) {\n\t\telem = elements[ index ];\n\t\tif ( !elem.style ) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tvalues[ index ] = data_priv.get( elem, \"olddisplay\" );\n\t\tdisplay = elem.style.display;\n\t\tif ( show ) {\n\t\t\t// Reset the inline display of this element to learn if it is\n\t\t\t// being hidden by cascaded rules or not\n\t\t\tif ( !values[ index ] && display === \"none\" ) {\n\t\t\t\telem.style.display = \"\";\n\t\t\t}\n\n\t\t\t// Set elements which have been overridden with display: none\n\t\t\t// in a stylesheet to whatever the default browser style is\n\t\t\t// for such an element\n\t\t\tif ( elem.style.display === \"\" && isHidden( elem ) ) {\n\t\t\t\tvalues[ index ] = data_priv.access( elem, \"olddisplay\", defaultDisplay(elem.nodeName) );\n\t\t\t}\n\t\t} else {\n\t\t\thidden = isHidden( elem );\n\n\t\t\tif ( display !== \"none\" || !hidden ) {\n\t\t\t\tdata_priv.set( elem, \"olddisplay\", hidden ? display : jQuery.css( elem, \"display\" ) );\n\t\t\t}\n\t\t}\n\t}\n\n\t// Set the display of most of the elements in a second loop\n\t// to avoid the constant reflow\n\tfor ( index = 0; index < length; index++ ) {\n\t\telem = elements[ index ];\n\t\tif ( !elem.style ) {\n\t\t\tcontinue;\n\t\t}\n\t\tif ( !show || elem.style.display === \"none\" || elem.style.display === \"\" ) {\n\t\t\telem.style.display = show ? values[ index ] || \"\" : \"none\";\n\t\t}\n\t}\n\n\treturn elements;\n}\n\njQuery.extend({\n\t// Add in style property hooks for overriding the default\n\t// behavior of getting and setting a style property\n\tcssHooks: {\n\t\topacity: {\n\t\t\tget: function( elem, computed ) {\n\t\t\t\tif ( computed ) {\n\t\t\t\t\t// We should always get a number back from opacity\n\t\t\t\t\tvar ret = curCSS( elem, \"opacity\" );\n\t\t\t\t\treturn ret === \"\" ? \"1\" : ret;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t},\n\n\t// Don't automatically add \"px\" to these possibly-unitless properties\n\tcssNumber: {\n\t\t\"columnCount\": true,\n\t\t\"fillOpacity\": true,\n\t\t\"flexGrow\": true,\n\t\t\"flexShrink\": true,\n\t\t\"fontWeight\": true,\n\t\t\"lineHeight\": true,\n\t\t\"opacity\": true,\n\t\t\"order\": true,\n\t\t\"orphans\": true,\n\t\t\"widows\": true,\n\t\t\"zIndex\": true,\n\t\t\"zoom\": true\n\t},\n\n\t// Add in properties whose names you wish to fix before\n\t// setting or getting the value\n\tcssProps: {\n\t\t// normalize float css property\n\t\t\"float\": \"cssFloat\"\n\t},\n\n\t// Get and set the style property on a DOM Node\n\tstyle: function( elem, name, value, extra ) {\n\t\t// Don't set styles on text and comment nodes\n\t\tif ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Make sure that we're working with the right name\n\t\tvar ret, type, hooks,\n\t\t\torigName = jQuery.camelCase( name ),\n\t\t\tstyle = elem.style;\n\n\t\tname = jQuery.cssProps[ origName ] || ( jQuery.cssProps[ origName ] = vendorPropName( style, origName ) );\n\n\t\t// gets hook for the prefixed version\n\t\t// followed by the unprefixed version\n\t\thooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ];\n\n\t\t// Check if we're setting a value\n\t\tif ( value !== undefined ) {\n\t\t\ttype = typeof value;\n\n\t\t\t// convert relative number strings (+= or -=) to relative numbers. #7345\n\t\t\tif ( type === \"string\" && (ret = rrelNum.exec( value )) ) {\n\t\t\t\tvalue = ( ret[1] + 1 ) * ret[2] + parseFloat( jQuery.css( elem, name ) );\n\t\t\t\t// Fixes bug #9237\n\t\t\t\ttype = \"number\";\n\t\t\t}\n\n\t\t\t// Make sure that null and NaN values aren't set. See: #7116\n\t\t\tif ( value == null || value !== value ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// If a number was passed in, add 'px' to the (except for certain CSS properties)\n\t\t\tif ( type === \"number\" && !jQuery.cssNumber[ origName ] ) {\n\t\t\t\tvalue += \"px\";\n\t\t\t}\n\n\t\t\t// Fixes #8908, it can be done more correctly by specifying setters in cssHooks,\n\t\t\t// but it would mean to define eight (for every problematic property) identical functions\n\t\t\tif ( !support.clearCloneStyle && value === \"\" && name.indexOf( \"background\" ) === 0 ) {\n\t\t\t\tstyle[ name ] = \"inherit\";\n\t\t\t}\n\n\t\t\t// If a hook was provided, use that value, otherwise just set the specified value\n\t\t\tif ( !hooks || !(\"set\" in hooks) || (value = hooks.set( elem, value, extra )) !== undefined ) {\n\t\t\t\tstyle[ name ] = value;\n\t\t\t}\n\n\t\t} else {\n\t\t\t// If a hook was provided get the non-computed value from there\n\t\t\tif ( hooks && \"get\" in hooks && (ret = hooks.get( elem, false, extra )) !== undefined ) {\n\t\t\t\treturn ret;\n\t\t\t}\n\n\t\t\t// Otherwise just get the value from the style object\n\t\t\treturn style[ name ];\n\t\t}\n\t},\n\n\tcss: function( elem, name, extra, styles ) {\n\t\tvar val, num, hooks,\n\t\t\torigName = jQuery.camelCase( name );\n\n\t\t// Make sure that we're working with the right name\n\t\tname = jQuery.cssProps[ origName ] || ( jQuery.cssProps[ origName ] = vendorPropName( elem.style, origName ) );\n\n\t\t// gets hook for the prefixed version\n\t\t// followed by the unprefixed version\n\t\thooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ];\n\n\t\t// If a hook was provided get the computed value from there\n\t\tif ( hooks && \"get\" in hooks ) {\n\t\t\tval = hooks.get( elem, true, extra );\n\t\t}\n\n\t\t// Otherwise, if a way to get the computed value exists, use that\n\t\tif ( val === undefined ) {\n\t\t\tval = curCSS( elem, name, styles );\n\t\t}\n\n\t\t//convert \"normal\" to computed value\n\t\tif ( val === \"normal\" && name in cssNormalTransform ) {\n\t\t\tval = cssNormalTransform[ name ];\n\t\t}\n\n\t\t// Return, converting to number if forced or a qualifier was provided and val looks numeric\n\t\tif ( extra === \"\" || extra ) {\n\t\t\tnum = parseFloat( val );\n\t\t\treturn extra === true || jQuery.isNumeric( num ) ? num || 0 : val;\n\t\t}\n\t\treturn val;\n\t}\n});\n\njQuery.each([ \"height\", \"width\" ], function( i, name ) {\n\tjQuery.cssHooks[ name ] = {\n\t\tget: function( elem, computed, extra ) {\n\t\t\tif ( computed ) {\n\t\t\t\t// certain elements can have dimension info if we invisibly show them\n\t\t\t\t// however, it must have a current display style that would benefit from this\n\t\t\t\treturn rdisplayswap.test( jQuery.css( elem, \"display\" ) ) && elem.offsetWidth === 0 ?\n\t\t\t\t\tjQuery.swap( elem, cssShow, function() {\n\t\t\t\t\t\treturn getWidthOrHeight( elem, name, extra );\n\t\t\t\t\t}) :\n\t\t\t\t\tgetWidthOrHeight( elem, name, extra );\n\t\t\t}\n\t\t},\n\n\t\tset: function( elem, value, extra ) {\n\t\t\tvar styles = extra && getStyles( elem );\n\t\t\treturn setPositiveNumber( elem, value, extra ?\n\t\t\t\taugmentWidthOrHeight(\n\t\t\t\t\telem,\n\t\t\t\t\tname,\n\t\t\t\t\textra,\n\t\t\t\t\tjQuery.css( elem, \"boxSizing\", false, styles ) === \"border-box\",\n\t\t\t\t\tstyles\n\t\t\t\t) : 0\n\t\t\t);\n\t\t}\n\t};\n});\n\n// Support: Android 2.3\njQuery.cssHooks.marginRight = addGetHookIf( support.reliableMarginRight,\n\tfunction( elem, computed ) {\n\t\tif ( computed ) {\n\t\t\t// WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right\n\t\t\t// Work around by temporarily setting element display to inline-block\n\t\t\treturn jQuery.swap( elem, { \"display\": \"inline-block\" },\n\t\t\t\tcurCSS, [ elem, \"marginRight\" ] );\n\t\t}\n\t}\n);\n\n// These hooks are used by animate to expand properties\njQuery.each({\n\tmargin: \"\",\n\tpadding: \"\",\n\tborder: \"Width\"\n}, function( prefix, suffix ) {\n\tjQuery.cssHooks[ prefix + suffix ] = {\n\t\texpand: function( value ) {\n\t\t\tvar i = 0,\n\t\t\t\texpanded = {},\n\n\t\t\t\t// assumes a single number if not a string\n\t\t\t\tparts = typeof value === \"string\" ? value.split(\" \") : [ value ];\n\n\t\t\tfor ( ; i < 4; i++ ) {\n\t\t\t\texpanded[ prefix + cssExpand[ i ] + suffix ] =\n\t\t\t\t\tparts[ i ] || parts[ i - 2 ] || parts[ 0 ];\n\t\t\t}\n\n\t\t\treturn expanded;\n\t\t}\n\t};\n\n\tif ( !rmargin.test( prefix ) ) {\n\t\tjQuery.cssHooks[ prefix + suffix ].set = setPositiveNumber;\n\t}\n});\n\njQuery.fn.extend({\n\tcss: function( name, value ) {\n\t\treturn access( this, function( elem, name, value ) {\n\t\t\tvar styles, len,\n\t\t\t\tmap = {},\n\t\t\t\ti = 0;\n\n\t\t\tif ( jQuery.isArray( name ) ) {\n\t\t\t\tstyles = getStyles( elem );\n\t\t\t\tlen = name.length;\n\n\t\t\t\tfor ( ; i < len; i++ ) {\n\t\t\t\t\tmap[ name[ i ] ] = jQuery.css( elem, name[ i ], false, styles );\n\t\t\t\t}\n\n\t\t\t\treturn map;\n\t\t\t}\n\n\t\t\treturn value !== undefined ?\n\t\t\t\tjQuery.style( elem, name, value ) :\n\t\t\t\tjQuery.css( elem, name );\n\t\t}, name, value, arguments.length > 1 );\n\t},\n\tshow: function() {\n\t\treturn showHide( this, true );\n\t},\n\thide: function() {\n\t\treturn showHide( this );\n\t},\n\ttoggle: function( state ) {\n\t\tif ( typeof state === \"boolean\" ) {\n\t\t\treturn state ? this.show() : this.hide();\n\t\t}\n\n\t\treturn this.each(function() {\n\t\t\tif ( isHidden( this ) ) {\n\t\t\t\tjQuery( this ).show();\n\t\t\t} else {\n\t\t\t\tjQuery( this ).hide();\n\t\t\t}\n\t\t});\n\t}\n});\n\n\nfunction Tween( elem, options, prop, end, easing ) {\n\treturn new Tween.prototype.init( elem, options, prop, end, easing );\n}\njQuery.Tween = Tween;\n\nTween.prototype = {\n\tconstructor: Tween,\n\tinit: function( elem, options, prop, end, easing, unit ) {\n\t\tthis.elem = elem;\n\t\tthis.prop = prop;\n\t\tthis.easing = easing || \"swing\";\n\t\tthis.options = options;\n\t\tthis.start = this.now = this.cur();\n\t\tthis.end = end;\n\t\tthis.unit = unit || ( jQuery.cssNumber[ prop ] ? \"\" : \"px\" );\n\t},\n\tcur: function() {\n\t\tvar hooks = Tween.propHooks[ this.prop ];\n\n\t\treturn hooks && hooks.get ?\n\t\t\thooks.get( this ) :\n\t\t\tTween.propHooks._default.get( this );\n\t},\n\trun: function( percent ) {\n\t\tvar eased,\n\t\t\thooks = Tween.propHooks[ this.prop ];\n\n\t\tif ( this.options.duration ) {\n\t\t\tthis.pos = eased = jQuery.easing[ this.easing ](\n\t\t\t\tpercent, this.options.duration * percent, 0, 1, this.options.duration\n\t\t\t);\n\t\t} else {\n\t\t\tthis.pos = eased = percent;\n\t\t}\n\t\tthis.now = ( this.end - this.start ) * eased + this.start;\n\n\t\tif ( this.options.step ) {\n\t\t\tthis.options.step.call( this.elem, this.now, this );\n\t\t}\n\n\t\tif ( hooks && hooks.set ) {\n\t\t\thooks.set( this );\n\t\t} else {\n\t\t\tTween.propHooks._default.set( this );\n\t\t}\n\t\treturn this;\n\t}\n};\n\nTween.prototype.init.prototype = Tween.prototype;\n\nTween.propHooks = {\n\t_default: {\n\t\tget: function( tween ) {\n\t\t\tvar result;\n\n\t\t\tif ( tween.elem[ tween.prop ] != null &&\n\t\t\t\t(!tween.elem.style || tween.elem.style[ tween.prop ] == null) ) {\n\t\t\t\treturn tween.elem[ tween.prop ];\n\t\t\t}\n\n\t\t\t// passing an empty string as a 3rd parameter to .css will automatically\n\t\t\t// attempt a parseFloat and fallback to a string if the parse fails\n\t\t\t// so, simple values such as \"10px\" are parsed to Float.\n\t\t\t// complex values such as \"rotate(1rad)\" are returned as is.\n\t\t\tresult = jQuery.css( tween.elem, tween.prop, \"\" );\n\t\t\t// Empty strings, null, undefined and \"auto\" are converted to 0.\n\t\t\treturn !result || result === \"auto\" ? 0 : result;\n\t\t},\n\t\tset: function( tween ) {\n\t\t\t// use step hook for back compat - use cssHook if its there - use .style if its\n\t\t\t// available and use plain properties where available\n\t\t\tif ( jQuery.fx.step[ tween.prop ] ) {\n\t\t\t\tjQuery.fx.step[ tween.prop ]( tween );\n\t\t\t} else if ( tween.elem.style && ( tween.elem.style[ jQuery.cssProps[ tween.prop ] ] != null || jQuery.cssHooks[ tween.prop ] ) ) {\n\t\t\t\tjQuery.style( tween.elem, tween.prop, tween.now + tween.unit );\n\t\t\t} else {\n\t\t\t\ttween.elem[ tween.prop ] = tween.now;\n\t\t\t}\n\t\t}\n\t}\n};\n\n// Support: IE9\n// Panic based approach to setting things on disconnected nodes\n\nTween.propHooks.scrollTop = Tween.propHooks.scrollLeft = {\n\tset: function( tween ) {\n\t\tif ( tween.elem.nodeType && tween.elem.parentNode ) {\n\t\t\ttween.elem[ tween.prop ] = tween.now;\n\t\t}\n\t}\n};\n\njQuery.easing = {\n\tlinear: function( p ) {\n\t\treturn p;\n\t},\n\tswing: function( p ) {\n\t\treturn 0.5 - Math.cos( p * Math.PI ) / 2;\n\t}\n};\n\njQuery.fx = Tween.prototype.init;\n\n// Back Compat <1.8 extension point\njQuery.fx.step = {};\n\n\n\n\nvar\n\tfxNow, timerId,\n\trfxtypes = /^(?:toggle|show|hide)$/,\n\trfxnum = new RegExp( \"^(?:([+-])=|)(\" + pnum + \")([a-z%]*)$\", \"i\" ),\n\trrun = /queueHooks$/,\n\tanimationPrefilters = [ defaultPrefilter ],\n\ttweeners = {\n\t\t\"*\": [ function( prop, value ) {\n\t\t\tvar tween = this.createTween( prop, value ),\n\t\t\t\ttarget = tween.cur(),\n\t\t\t\tparts = rfxnum.exec( value ),\n\t\t\t\tunit = parts && parts[ 3 ] || ( jQuery.cssNumber[ prop ] ? \"\" : \"px\" ),\n\n\t\t\t\t// Starting value computation is required for potential unit mismatches\n\t\t\t\tstart = ( jQuery.cssNumber[ prop ] || unit !== \"px\" && +target ) &&\n\t\t\t\t\trfxnum.exec( jQuery.css( tween.elem, prop ) ),\n\t\t\t\tscale = 1,\n\t\t\t\tmaxIterations = 20;\n\n\t\t\tif ( start && start[ 3 ] !== unit ) {\n\t\t\t\t// Trust units reported by jQuery.css\n\t\t\t\tunit = unit || start[ 3 ];\n\n\t\t\t\t// Make sure we update the tween properties later on\n\t\t\t\tparts = parts || [];\n\n\t\t\t\t// Iteratively approximate from a nonzero starting point\n\t\t\t\tstart = +target || 1;\n\n\t\t\t\tdo {\n\t\t\t\t\t// If previous iteration zeroed out, double until we get *something*\n\t\t\t\t\t// Use a string for doubling factor so we don't accidentally see scale as unchanged below\n\t\t\t\t\tscale = scale || \".5\";\n\n\t\t\t\t\t// Adjust and apply\n\t\t\t\t\tstart = start / scale;\n\t\t\t\t\tjQuery.style( tween.elem, prop, start + unit );\n\n\t\t\t\t// Update scale, tolerating zero or NaN from tween.cur()\n\t\t\t\t// And breaking the loop if scale is unchanged or perfect, or if we've just had enough\n\t\t\t\t} while ( scale !== (scale = tween.cur() / target) && scale !== 1 && --maxIterations );\n\t\t\t}\n\n\t\t\t// Update tween properties\n\t\t\tif ( parts ) {\n\t\t\t\tstart = tween.start = +start || +target || 0;\n\t\t\t\ttween.unit = unit;\n\t\t\t\t// If a +=/-= token was provided, we're doing a relative animation\n\t\t\t\ttween.end = parts[ 1 ] ?\n\t\t\t\t\tstart + ( parts[ 1 ] + 1 ) * parts[ 2 ] :\n\t\t\t\t\t+parts[ 2 ];\n\t\t\t}\n\n\t\t\treturn tween;\n\t\t} ]\n\t};\n\n// Animations created synchronously will run synchronously\nfunction createFxNow() {\n\tsetTimeout(function() {\n\t\tfxNow = undefined;\n\t});\n\treturn ( fxNow = jQuery.now() );\n}\n\n// Generate parameters to create a standard animation\nfunction genFx( type, includeWidth ) {\n\tvar which,\n\t\ti = 0,\n\t\tattrs = { height: type };\n\n\t// if we include width, step value is 1 to do all cssExpand values,\n\t// if we don't include width, step value is 2 to skip over Left and Right\n\tincludeWidth = includeWidth ? 1 : 0;\n\tfor ( ; i < 4 ; i += 2 - includeWidth ) {\n\t\twhich = cssExpand[ i ];\n\t\tattrs[ \"margin\" + which ] = attrs[ \"padding\" + which ] = type;\n\t}\n\n\tif ( includeWidth ) {\n\t\tattrs.opacity = attrs.width = type;\n\t}\n\n\treturn attrs;\n}\n\nfunction createTween( value, prop, animation ) {\n\tvar tween,\n\t\tcollection = ( tweeners[ prop ] || [] ).concat( tweeners[ \"*\" ] ),\n\t\tindex = 0,\n\t\tlength = collection.length;\n\tfor ( ; index < length; index++ ) {\n\t\tif ( (tween = collection[ index ].call( animation, prop, value )) ) {\n\n\t\t\t// we're done with this property\n\t\t\treturn tween;\n\t\t}\n\t}\n}\n\nfunction defaultPrefilter( elem, props, opts ) {\n\t/* jshint validthis: true */\n\tvar prop, value, toggle, tween, hooks, oldfire, display, checkDisplay,\n\t\tanim = this,\n\t\torig = {},\n\t\tstyle = elem.style,\n\t\thidden = elem.nodeType && isHidden( elem ),\n\t\tdataShow = data_priv.get( elem, \"fxshow\" );\n\n\t// handle queue: false promises\n\tif ( !opts.queue ) {\n\t\thooks = jQuery._queueHooks( elem, \"fx\" );\n\t\tif ( hooks.unqueued == null ) {\n\t\t\thooks.unqueued = 0;\n\t\t\toldfire = hooks.empty.fire;\n\t\t\thooks.empty.fire = function() {\n\t\t\t\tif ( !hooks.unqueued ) {\n\t\t\t\t\toldfire();\n\t\t\t\t}\n\t\t\t};\n\t\t}\n\t\thooks.unqueued++;\n\n\t\tanim.always(function() {\n\t\t\t// doing this makes sure that the complete handler will be called\n\t\t\t// before this completes\n\t\t\tanim.always(function() {\n\t\t\t\thooks.unqueued--;\n\t\t\t\tif ( !jQuery.queue( elem, \"fx\" ).length ) {\n\t\t\t\t\thooks.empty.fire();\n\t\t\t\t}\n\t\t\t});\n\t\t});\n\t}\n\n\t// height/width overflow pass\n\tif ( elem.nodeType === 1 && ( \"height\" in props || \"width\" in props ) ) {\n\t\t// Make sure that nothing sneaks out\n\t\t// Record all 3 overflow attributes because IE9-10 do not\n\t\t// change the overflow attribute when overflowX and\n\t\t// overflowY are set to the same value\n\t\topts.overflow = [ style.overflow, style.overflowX, style.overflowY ];\n\n\t\t// Set display property to inline-block for height/width\n\t\t// animations on inline elements that are having width/height animated\n\t\tdisplay = jQuery.css( elem, \"display\" );\n\n\t\t// Test default display if display is currently \"none\"\n\t\tcheckDisplay = display === \"none\" ?\n\t\t\tdata_priv.get( elem, \"olddisplay\" ) || defaultDisplay( elem.nodeName ) : display;\n\n\t\tif ( checkDisplay === \"inline\" && jQuery.css( elem, \"float\" ) === \"none\" ) {\n\t\t\tstyle.display = \"inline-block\";\n\t\t}\n\t}\n\n\tif ( opts.overflow ) {\n\t\tstyle.overflow = \"hidden\";\n\t\tanim.always(function() {\n\t\t\tstyle.overflow = opts.overflow[ 0 ];\n\t\t\tstyle.overflowX = opts.overflow[ 1 ];\n\t\t\tstyle.overflowY = opts.overflow[ 2 ];\n\t\t});\n\t}\n\n\t// show/hide pass\n\tfor ( prop in props ) {\n\t\tvalue = props[ prop ];\n\t\tif ( rfxtypes.exec( value ) ) {\n\t\t\tdelete props[ prop ];\n\t\t\ttoggle = toggle || value === \"toggle\";\n\t\t\tif ( value === ( hidden ? \"hide\" : \"show\" ) ) {\n\n\t\t\t\t// If there is dataShow left over from a stopped hide or show and we are going to proceed with show, we should pretend to be hidden\n\t\t\t\tif ( value === \"show\" && dataShow && dataShow[ prop ] !== undefined ) {\n\t\t\t\t\thidden = true;\n\t\t\t\t} else {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t}\n\t\t\torig[ prop ] = dataShow && dataShow[ prop ] || jQuery.style( elem, prop );\n\n\t\t// Any non-fx value stops us from restoring the original display value\n\t\t} else {\n\t\t\tdisplay = undefined;\n\t\t}\n\t}\n\n\tif ( !jQuery.isEmptyObject( orig ) ) {\n\t\tif ( dataShow ) {\n\t\t\tif ( \"hidden\" in dataShow ) {\n\t\t\t\thidden = dataShow.hidden;\n\t\t\t}\n\t\t} else {\n\t\t\tdataShow = data_priv.access( elem, \"fxshow\", {} );\n\t\t}\n\n\t\t// store state if its toggle - enables .stop().toggle() to \"reverse\"\n\t\tif ( toggle ) {\n\t\t\tdataShow.hidden = !hidden;\n\t\t}\n\t\tif ( hidden ) {\n\t\t\tjQuery( elem ).show();\n\t\t} else {\n\t\t\tanim.done(function() {\n\t\t\t\tjQuery( elem ).hide();\n\t\t\t});\n\t\t}\n\t\tanim.done(function() {\n\t\t\tvar prop;\n\n\t\t\tdata_priv.remove( elem, \"fxshow\" );\n\t\t\tfor ( prop in orig ) {\n\t\t\t\tjQuery.style( elem, prop, orig[ prop ] );\n\t\t\t}\n\t\t});\n\t\tfor ( prop in orig ) {\n\t\t\ttween = createTween( hidden ? dataShow[ prop ] : 0, prop, anim );\n\n\t\t\tif ( !( prop in dataShow ) ) {\n\t\t\t\tdataShow[ prop ] = tween.start;\n\t\t\t\tif ( hidden ) {\n\t\t\t\t\ttween.end = tween.start;\n\t\t\t\t\ttween.start = prop === \"width\" || prop === \"height\" ? 1 : 0;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t// If this is a noop like .hide().hide(), restore an overwritten display value\n\t} else if ( (display === \"none\" ? defaultDisplay( elem.nodeName ) : display) === \"inline\" ) {\n\t\tstyle.display = display;\n\t}\n}\n\nfunction propFilter( props, specialEasing ) {\n\tvar index, name, easing, value, hooks;\n\n\t// camelCase, specialEasing and expand cssHook pass\n\tfor ( index in props ) {\n\t\tname = jQuery.camelCase( index );\n\t\teasing = specialEasing[ name ];\n\t\tvalue = props[ index ];\n\t\tif ( jQuery.isArray( value ) ) {\n\t\t\teasing = value[ 1 ];\n\t\t\tvalue = props[ index ] = value[ 0 ];\n\t\t}\n\n\t\tif ( index !== name ) {\n\t\t\tprops[ name ] = value;\n\t\t\tdelete props[ index ];\n\t\t}\n\n\t\thooks = jQuery.cssHooks[ name ];\n\t\tif ( hooks && \"expand\" in hooks ) {\n\t\t\tvalue = hooks.expand( value );\n\t\t\tdelete props[ name ];\n\n\t\t\t// not quite $.extend, this wont overwrite keys already present.\n\t\t\t// also - reusing 'index' from above because we have the correct \"name\"\n\t\t\tfor ( index in value ) {\n\t\t\t\tif ( !( index in props ) ) {\n\t\t\t\t\tprops[ index ] = value[ index ];\n\t\t\t\t\tspecialEasing[ index ] = easing;\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tspecialEasing[ name ] = easing;\n\t\t}\n\t}\n}\n\nfunction Animation( elem, properties, options ) {\n\tvar result,\n\t\tstopped,\n\t\tindex = 0,\n\t\tlength = animationPrefilters.length,\n\t\tdeferred = jQuery.Deferred().always( function() {\n\t\t\t// don't match elem in the :animated selector\n\t\t\tdelete tick.elem;\n\t\t}),\n\t\ttick = function() {\n\t\t\tif ( stopped ) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tvar currentTime = fxNow || createFxNow(),\n\t\t\t\tremaining = Math.max( 0, animation.startTime + animation.duration - currentTime ),\n\t\t\t\t// archaic crash bug won't allow us to use 1 - ( 0.5 || 0 ) (#12497)\n\t\t\t\ttemp = remaining / animation.duration || 0,\n\t\t\t\tpercent = 1 - temp,\n\t\t\t\tindex = 0,\n\t\t\t\tlength = animation.tweens.length;\n\n\t\t\tfor ( ; index < length ; index++ ) {\n\t\t\t\tanimation.tweens[ index ].run( percent );\n\t\t\t}\n\n\t\t\tdeferred.notifyWith( elem, [ animation, percent, remaining ]);\n\n\t\t\tif ( percent < 1 && length ) {\n\t\t\t\treturn remaining;\n\t\t\t} else {\n\t\t\t\tdeferred.resolveWith( elem, [ animation ] );\n\t\t\t\treturn false;\n\t\t\t}\n\t\t},\n\t\tanimation = deferred.promise({\n\t\t\telem: elem,\n\t\t\tprops: jQuery.extend( {}, properties ),\n\t\t\topts: jQuery.extend( true, { specialEasing: {} }, options ),\n\t\t\toriginalProperties: properties,\n\t\t\toriginalOptions: options,\n\t\t\tstartTime: fxNow || createFxNow(),\n\t\t\tduration: options.duration,\n\t\t\ttweens: [],\n\t\t\tcreateTween: function( prop, end ) {\n\t\t\t\tvar tween = jQuery.Tween( elem, animation.opts, prop, end,\n\t\t\t\t\t\tanimation.opts.specialEasing[ prop ] || animation.opts.easing );\n\t\t\t\tanimation.tweens.push( tween );\n\t\t\t\treturn tween;\n\t\t\t},\n\t\t\tstop: function( gotoEnd ) {\n\t\t\t\tvar index = 0,\n\t\t\t\t\t// if we are going to the end, we want to run all the tweens\n\t\t\t\t\t// otherwise we skip this part\n\t\t\t\t\tlength = gotoEnd ? animation.tweens.length : 0;\n\t\t\t\tif ( stopped ) {\n\t\t\t\t\treturn this;\n\t\t\t\t}\n\t\t\t\tstopped = true;\n\t\t\t\tfor ( ; index < length ; index++ ) {\n\t\t\t\t\tanimation.tweens[ index ].run( 1 );\n\t\t\t\t}\n\n\t\t\t\t// resolve when we played the last frame\n\t\t\t\t// otherwise, reject\n\t\t\t\tif ( gotoEnd ) {\n\t\t\t\t\tdeferred.resolveWith( elem, [ animation, gotoEnd ] );\n\t\t\t\t} else {\n\t\t\t\t\tdeferred.rejectWith( elem, [ animation, gotoEnd ] );\n\t\t\t\t}\n\t\t\t\treturn this;\n\t\t\t}\n\t\t}),\n\t\tprops = animation.props;\n\n\tpropFilter( props, animation.opts.specialEasing );\n\n\tfor ( ; index < length ; index++ ) {\n\t\tresult = animationPrefilters[ index ].call( animation, elem, props, animation.opts );\n\t\tif ( result ) {\n\t\t\treturn result;\n\t\t}\n\t}\n\n\tjQuery.map( props, createTween, animation );\n\n\tif ( jQuery.isFunction( animation.opts.start ) ) {\n\t\tanimation.opts.start.call( elem, animation );\n\t}\n\n\tjQuery.fx.timer(\n\t\tjQuery.extend( tick, {\n\t\t\telem: elem,\n\t\t\tanim: animation,\n\t\t\tqueue: animation.opts.queue\n\t\t})\n\t);\n\n\t// attach callbacks from options\n\treturn animation.progress( animation.opts.progress )\n\t\t.done( animation.opts.done, animation.opts.complete )\n\t\t.fail( animation.opts.fail )\n\t\t.always( animation.opts.always );\n}\n\njQuery.Animation = jQuery.extend( Animation, {\n\n\ttweener: function( props, callback ) {\n\t\tif ( jQuery.isFunction( props ) ) {\n\t\t\tcallback = props;\n\t\t\tprops = [ \"*\" ];\n\t\t} else {\n\t\t\tprops = props.split(\" \");\n\t\t}\n\n\t\tvar prop,\n\t\t\tindex = 0,\n\t\t\tlength = props.length;\n\n\t\tfor ( ; index < length ; index++ ) {\n\t\t\tprop = props[ index ];\n\t\t\ttweeners[ prop ] = tweeners[ prop ] || [];\n\t\t\ttweeners[ prop ].unshift( callback );\n\t\t}\n\t},\n\n\tprefilter: function( callback, prepend ) {\n\t\tif ( prepend ) {\n\t\t\tanimationPrefilters.unshift( callback );\n\t\t} else {\n\t\t\tanimationPrefilters.push( callback );\n\t\t}\n\t}\n});\n\njQuery.speed = function( speed, easing, fn ) {\n\tvar opt = speed && typeof speed === \"object\" ? jQuery.extend( {}, speed ) : {\n\t\tcomplete: fn || !fn && easing ||\n\t\t\tjQuery.isFunction( speed ) && speed,\n\t\tduration: speed,\n\t\teasing: fn && easing || easing && !jQuery.isFunction( easing ) && easing\n\t};\n\n\topt.duration = jQuery.fx.off ? 0 : typeof opt.duration === \"number\" ? opt.duration :\n\t\topt.duration in jQuery.fx.speeds ? jQuery.fx.speeds[ opt.duration ] : jQuery.fx.speeds._default;\n\n\t// normalize opt.queue - true/undefined/null -> \"fx\"\n\tif ( opt.queue == null || opt.queue === true ) {\n\t\topt.queue = \"fx\";\n\t}\n\n\t// Queueing\n\topt.old = opt.complete;\n\n\topt.complete = function() {\n\t\tif ( jQuery.isFunction( opt.old ) ) {\n\t\t\topt.old.call( this );\n\t\t}\n\n\t\tif ( opt.queue ) {\n\t\t\tjQuery.dequeue( this, opt.queue );\n\t\t}\n\t};\n\n\treturn opt;\n};\n\njQuery.fn.extend({\n\tfadeTo: function( speed, to, easing, callback ) {\n\n\t\t// show any hidden elements after setting opacity to 0\n\t\treturn this.filter( isHidden ).css( \"opacity\", 0 ).show()\n\n\t\t\t// animate to the value specified\n\t\t\t.end().animate({ opacity: to }, speed, easing, callback );\n\t},\n\tanimate: function( prop, speed, easing, callback ) {\n\t\tvar empty = jQuery.isEmptyObject( prop ),\n\t\t\toptall = jQuery.speed( speed, easing, callback ),\n\t\t\tdoAnimation = function() {\n\t\t\t\t// Operate on a copy of prop so per-property easing won't be lost\n\t\t\t\tvar anim = Animation( this, jQuery.extend( {}, prop ), optall );\n\n\t\t\t\t// Empty animations, or finishing resolves immediately\n\t\t\t\tif ( empty || data_priv.get( this, \"finish\" ) ) {\n\t\t\t\t\tanim.stop( true );\n\t\t\t\t}\n\t\t\t};\n\t\t\tdoAnimation.finish = doAnimation;\n\n\t\treturn empty || optall.queue === false ?\n\t\t\tthis.each( doAnimation ) :\n\t\t\tthis.queue( optall.queue, doAnimation );\n\t},\n\tstop: function( type, clearQueue, gotoEnd ) {\n\t\tvar stopQueue = function( hooks ) {\n\t\t\tvar stop = hooks.stop;\n\t\t\tdelete hooks.stop;\n\t\t\tstop( gotoEnd );\n\t\t};\n\n\t\tif ( typeof type !== \"string\" ) {\n\t\t\tgotoEnd = clearQueue;\n\t\t\tclearQueue = type;\n\t\t\ttype = undefined;\n\t\t}\n\t\tif ( clearQueue && type !== false ) {\n\t\t\tthis.queue( type || \"fx\", [] );\n\t\t}\n\n\t\treturn this.each(function() {\n\t\t\tvar dequeue = true,\n\t\t\t\tindex = type != null && type + \"queueHooks\",\n\t\t\t\ttimers = jQuery.timers,\n\t\t\t\tdata = data_priv.get( this );\n\n\t\t\tif ( index ) {\n\t\t\t\tif ( data[ index ] && data[ index ].stop ) {\n\t\t\t\t\tstopQueue( data[ index ] );\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tfor ( index in data ) {\n\t\t\t\t\tif ( data[ index ] && data[ index ].stop && rrun.test( index ) ) {\n\t\t\t\t\t\tstopQueue( data[ index ] );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tfor ( index = timers.length; index--; ) {\n\t\t\t\tif ( timers[ index ].elem === this && (type == null || timers[ index ].queue === type) ) {\n\t\t\t\t\ttimers[ index ].anim.stop( gotoEnd );\n\t\t\t\t\tdequeue = false;\n\t\t\t\t\ttimers.splice( index, 1 );\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// start the next in the queue if the last step wasn't forced\n\t\t\t// timers currently will call their complete callbacks, which will dequeue\n\t\t\t// but only if they were gotoEnd\n\t\t\tif ( dequeue || !gotoEnd ) {\n\t\t\t\tjQuery.dequeue( this, type );\n\t\t\t}\n\t\t});\n\t},\n\tfinish: function( type ) {\n\t\tif ( type !== false ) {\n\t\t\ttype = type || \"fx\";\n\t\t}\n\t\treturn this.each(function() {\n\t\t\tvar index,\n\t\t\t\tdata = data_priv.get( this ),\n\t\t\t\tqueue = data[ type + \"queue\" ],\n\t\t\t\thooks = data[ type + \"queueHooks\" ],\n\t\t\t\ttimers = jQuery.timers,\n\t\t\t\tlength = queue ? queue.length : 0;\n\n\t\t\t// enable finishing flag on private data\n\t\t\tdata.finish = true;\n\n\t\t\t// empty the queue first\n\t\t\tjQuery.queue( this, type, [] );\n\n\t\t\tif ( hooks && hooks.stop ) {\n\t\t\t\thooks.stop.call( this, true );\n\t\t\t}\n\n\t\t\t// look for any active animations, and finish them\n\t\t\tfor ( index = timers.length; index--; ) {\n\t\t\t\tif ( timers[ index ].elem === this && timers[ index ].queue === type ) {\n\t\t\t\t\ttimers[ index ].anim.stop( true );\n\t\t\t\t\ttimers.splice( index, 1 );\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// look for any animations in the old queue and finish them\n\t\t\tfor ( index = 0; index < length; index++ ) {\n\t\t\t\tif ( queue[ index ] && queue[ index ].finish ) {\n\t\t\t\t\tqueue[ index ].finish.call( this );\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// turn off finishing flag\n\t\t\tdelete data.finish;\n\t\t});\n\t}\n});\n\njQuery.each([ \"toggle\", \"show\", \"hide\" ], function( i, name ) {\n\tvar cssFn = jQuery.fn[ name ];\n\tjQuery.fn[ name ] = function( speed, easing, callback ) {\n\t\treturn speed == null || typeof speed === \"boolean\" ?\n\t\t\tcssFn.apply( this, arguments ) :\n\t\t\tthis.animate( genFx( name, true ), speed, easing, callback );\n\t};\n});\n\n// Generate shortcuts for custom animations\njQuery.each({\n\tslideDown: genFx(\"show\"),\n\tslideUp: genFx(\"hide\"),\n\tslideToggle: genFx(\"toggle\"),\n\tfadeIn: { opacity: \"show\" },\n\tfadeOut: { opacity: \"hide\" },\n\tfadeToggle: { opacity: \"toggle\" }\n}, function( name, props ) {\n\tjQuery.fn[ name ] = function( speed, easing, callback ) {\n\t\treturn this.animate( props, speed, easing, callback );\n\t};\n});\n\njQuery.timers = [];\njQuery.fx.tick = function() {\n\tvar timer,\n\t\ti = 0,\n\t\ttimers = jQuery.timers;\n\n\tfxNow = jQuery.now();\n\n\tfor ( ; i < timers.length; i++ ) {\n\t\ttimer = timers[ i ];\n\t\t// Checks the timer has not already been removed\n\t\tif ( !timer() && timers[ i ] === timer ) {\n\t\t\ttimers.splice( i--, 1 );\n\t\t}\n\t}\n\n\tif ( !timers.length ) {\n\t\tjQuery.fx.stop();\n\t}\n\tfxNow = undefined;\n};\n\njQuery.fx.timer = function( timer ) {\n\tjQuery.timers.push( timer );\n\tif ( timer() ) {\n\t\tjQuery.fx.start();\n\t} else {\n\t\tjQuery.timers.pop();\n\t}\n};\n\njQuery.fx.interval = 13;\n\njQuery.fx.start = function() {\n\tif ( !timerId ) {\n\t\ttimerId = setInterval( jQuery.fx.tick, jQuery.fx.interval );\n\t}\n};\n\njQuery.fx.stop = function() {\n\tclearInterval( timerId );\n\ttimerId = null;\n};\n\njQuery.fx.speeds = {\n\tslow: 600,\n\tfast: 200,\n\t// Default speed\n\t_default: 400\n};\n\n\n// Based off of the plugin by Clint Helfers, with permission.\n// http://blindsignals.com/index.php/2009/07/jquery-delay/\njQuery.fn.delay = function( time, type ) {\n\ttime = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time;\n\ttype = type || \"fx\";\n\n\treturn this.queue( type, function( next, hooks ) {\n\t\tvar timeout = setTimeout( next, time );\n\t\thooks.stop = function() {\n\t\t\tclearTimeout( timeout );\n\t\t};\n\t});\n};\n\n\n(function() {\n\tvar input = document.createElement( \"input\" ),\n\t\tselect = document.createElement( \"select\" ),\n\t\topt = select.appendChild( document.createElement( \"option\" ) );\n\n\tinput.type = \"checkbox\";\n\n\t// Support: iOS 5.1, Android 4.x, Android 2.3\n\t// Check the default checkbox/radio value (\"\" on old WebKit; \"on\" elsewhere)\n\tsupport.checkOn = input.value !== \"\";\n\n\t// Must access the parent to make an option select properly\n\t// Support: IE9, IE10\n\tsupport.optSelected = opt.selected;\n\n\t// Make sure that the options inside disabled selects aren't marked as disabled\n\t// (WebKit marks them as disabled)\n\tselect.disabled = true;\n\tsupport.optDisabled = !opt.disabled;\n\n\t// Check if an input maintains its value after becoming a radio\n\t// Support: IE9, IE10\n\tinput = document.createElement( \"input\" );\n\tinput.value = \"t\";\n\tinput.type = \"radio\";\n\tsupport.radioValue = input.value === \"t\";\n})();\n\n\nvar nodeHook, boolHook,\n\tattrHandle = jQuery.expr.attrHandle;\n\njQuery.fn.extend({\n\tattr: function( name, value ) {\n\t\treturn access( this, jQuery.attr, name, value, arguments.length > 1 );\n\t},\n\n\tremoveAttr: function( name ) {\n\t\treturn this.each(function() {\n\t\t\tjQuery.removeAttr( this, name );\n\t\t});\n\t}\n});\n\njQuery.extend({\n\tattr: function( elem, name, value ) {\n\t\tvar hooks, ret,\n\t\t\tnType = elem.nodeType;\n\n\t\t// don't get/set attributes on text, comment and attribute nodes\n\t\tif ( !elem || nType === 3 || nType === 8 || nType === 2 ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Fallback to prop when attributes are not supported\n\t\tif ( typeof elem.getAttribute === strundefined ) {\n\t\t\treturn jQuery.prop( elem, name, value );\n\t\t}\n\n\t\t// All attributes are lowercase\n\t\t// Grab necessary hook if one is defined\n\t\tif ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) {\n\t\t\tname = name.toLowerCase();\n\t\t\thooks = jQuery.attrHooks[ name ] ||\n\t\t\t\t( jQuery.expr.match.bool.test( name ) ? boolHook : nodeHook );\n\t\t}\n\n\t\tif ( value !== undefined ) {\n\n\t\t\tif ( value === null ) {\n\t\t\t\tjQuery.removeAttr( elem, name );\n\n\t\t\t} else if ( hooks && \"set\" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ) {\n\t\t\t\treturn ret;\n\n\t\t\t} else {\n\t\t\t\telem.setAttribute( name, value + \"\" );\n\t\t\t\treturn value;\n\t\t\t}\n\n\t\t} else if ( hooks && \"get\" in hooks && (ret = hooks.get( elem, name )) !== null ) {\n\t\t\treturn ret;\n\n\t\t} else {\n\t\t\tret = jQuery.find.attr( elem, name );\n\n\t\t\t// Non-existent attributes return null, we normalize to undefined\n\t\t\treturn ret == null ?\n\t\t\t\tundefined :\n\t\t\t\tret;\n\t\t}\n\t},\n\n\tremoveAttr: function( elem, value ) {\n\t\tvar name, propName,\n\t\t\ti = 0,\n\t\t\tattrNames = value && value.match( rnotwhite );\n\n\t\tif ( attrNames && elem.nodeType === 1 ) {\n\t\t\twhile ( (name = attrNames[i++]) ) {\n\t\t\t\tpropName = jQuery.propFix[ name ] || name;\n\n\t\t\t\t// Boolean attributes get special treatment (#10870)\n\t\t\t\tif ( jQuery.expr.match.bool.test( name ) ) {\n\t\t\t\t\t// Set corresponding property to false\n\t\t\t\t\telem[ propName ] = false;\n\t\t\t\t}\n\n\t\t\t\telem.removeAttribute( name );\n\t\t\t}\n\t\t}\n\t},\n\n\tattrHooks: {\n\t\ttype: {\n\t\t\tset: function( elem, value ) {\n\t\t\t\tif ( !support.radioValue && value === \"radio\" &&\n\t\t\t\t\tjQuery.nodeName( elem, \"input\" ) ) {\n\t\t\t\t\t// Setting the type on a radio button after the value resets the value in IE6-9\n\t\t\t\t\t// Reset value to default in case type is set after value during creation\n\t\t\t\t\tvar val = elem.value;\n\t\t\t\t\telem.setAttribute( \"type\", value );\n\t\t\t\t\tif ( val ) {\n\t\t\t\t\t\telem.value = val;\n\t\t\t\t\t}\n\t\t\t\t\treturn value;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n});\n\n// Hooks for boolean attributes\nboolHook = {\n\tset: function( elem, value, name ) {\n\t\tif ( value === false ) {\n\t\t\t// Remove boolean attributes when set to false\n\t\t\tjQuery.removeAttr( elem, name );\n\t\t} else {\n\t\t\telem.setAttribute( name, name );\n\t\t}\n\t\treturn name;\n\t}\n};\njQuery.each( jQuery.expr.match.bool.source.match( /\\w+/g ), function( i, name ) {\n\tvar getter = attrHandle[ name ] || jQuery.find.attr;\n\n\tattrHandle[ name ] = function( elem, name, isXML ) {\n\t\tvar ret, handle;\n\t\tif ( !isXML ) {\n\t\t\t// Avoid an infinite loop by temporarily removing this function from the getter\n\t\t\thandle = attrHandle[ name ];\n\t\t\tattrHandle[ name ] = ret;\n\t\t\tret = getter( elem, name, isXML ) != null ?\n\t\t\t\tname.toLowerCase() :\n\t\t\t\tnull;\n\t\t\tattrHandle[ name ] = handle;\n\t\t}\n\t\treturn ret;\n\t};\n});\n\n\n\n\nvar rfocusable = /^(?:input|select|textarea|button)$/i;\n\njQuery.fn.extend({\n\tprop: function( name, value ) {\n\t\treturn access( this, jQuery.prop, name, value, arguments.length > 1 );\n\t},\n\n\tremoveProp: function( name ) {\n\t\treturn this.each(function() {\n\t\t\tdelete this[ jQuery.propFix[ name ] || name ];\n\t\t});\n\t}\n});\n\njQuery.extend({\n\tpropFix: {\n\t\t\"for\": \"htmlFor\",\n\t\t\"class\": \"className\"\n\t},\n\n\tprop: function( elem, name, value ) {\n\t\tvar ret, hooks, notxml,\n\t\t\tnType = elem.nodeType;\n\n\t\t// don't get/set properties on text, comment and attribute nodes\n\t\tif ( !elem || nType === 3 || nType === 8 || nType === 2 ) {\n\t\t\treturn;\n\t\t}\n\n\t\tnotxml = nType !== 1 || !jQuery.isXMLDoc( elem );\n\n\t\tif ( notxml ) {\n\t\t\t// Fix name and attach hooks\n\t\t\tname = jQuery.propFix[ name ] || name;\n\t\t\thooks = jQuery.propHooks[ name ];\n\t\t}\n\n\t\tif ( value !== undefined ) {\n\t\t\treturn hooks && \"set\" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ?\n\t\t\t\tret :\n\t\t\t\t( elem[ name ] = value );\n\n\t\t} else {\n\t\t\treturn hooks && \"get\" in hooks && (ret = hooks.get( elem, name )) !== null ?\n\t\t\t\tret :\n\t\t\t\telem[ name ];\n\t\t}\n\t},\n\n\tpropHooks: {\n\t\ttabIndex: {\n\t\t\tget: function( elem ) {\n\t\t\t\treturn elem.hasAttribute( \"tabindex\" ) || rfocusable.test( elem.nodeName ) || elem.href ?\n\t\t\t\t\telem.tabIndex :\n\t\t\t\t\t-1;\n\t\t\t}\n\t\t}\n\t}\n});\n\n// Support: IE9+\n// Selectedness for an option in an optgroup can be inaccurate\nif ( !support.optSelected ) {\n\tjQuery.propHooks.selected = {\n\t\tget: function( elem ) {\n\t\t\tvar parent = elem.parentNode;\n\t\t\tif ( parent && parent.parentNode ) {\n\t\t\t\tparent.parentNode.selectedIndex;\n\t\t\t}\n\t\t\treturn null;\n\t\t}\n\t};\n}\n\njQuery.each([\n\t\"tabIndex\",\n\t\"readOnly\",\n\t\"maxLength\",\n\t\"cellSpacing\",\n\t\"cellPadding\",\n\t\"rowSpan\",\n\t\"colSpan\",\n\t\"useMap\",\n\t\"frameBorder\",\n\t\"contentEditable\"\n], function() {\n\tjQuery.propFix[ this.toLowerCase() ] = this;\n});\n\n\n\n\nvar rclass = /[\\t\\r\\n\\f]/g;\n\njQuery.fn.extend({\n\taddClass: function( value ) {\n\t\tvar classes, elem, cur, clazz, j, finalValue,\n\t\t\tproceed = typeof value === \"string\" && value,\n\t\t\ti = 0,\n\t\t\tlen = this.length;\n\n\t\tif ( jQuery.isFunction( value ) ) {\n\t\t\treturn this.each(function( j ) {\n\t\t\t\tjQuery( this ).addClass( value.call( this, j, this.className ) );\n\t\t\t});\n\t\t}\n\n\t\tif ( proceed ) {\n\t\t\t// The disjunction here is for better compressibility (see removeClass)\n\t\t\tclasses = ( value || \"\" ).match( rnotwhite ) || [];\n\n\t\t\tfor ( ; i < len; i++ ) {\n\t\t\t\telem = this[ i ];\n\t\t\t\tcur = elem.nodeType === 1 && ( elem.className ?\n\t\t\t\t\t( \" \" + elem.className + \" \" ).replace( rclass, \" \" ) :\n\t\t\t\t\t\" \"\n\t\t\t\t);\n\n\t\t\t\tif ( cur ) {\n\t\t\t\t\tj = 0;\n\t\t\t\t\twhile ( (clazz = classes[j++]) ) {\n\t\t\t\t\t\tif ( cur.indexOf( \" \" + clazz + \" \" ) < 0 ) {\n\t\t\t\t\t\t\tcur += clazz + \" \";\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// only assign if different to avoid unneeded rendering.\n\t\t\t\t\tfinalValue = jQuery.trim( cur );\n\t\t\t\t\tif ( elem.className !== finalValue ) {\n\t\t\t\t\t\telem.className = finalValue;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn this;\n\t},\n\n\tremoveClass: function( value ) {\n\t\tvar classes, elem, cur, clazz, j, finalValue,\n\t\t\tproceed = arguments.length === 0 || typeof value === \"string\" && value,\n\t\t\ti = 0,\n\t\t\tlen = this.length;\n\n\t\tif ( jQuery.isFunction( value ) ) {\n\t\t\treturn this.each(function( j ) {\n\t\t\t\tjQuery( this ).removeClass( value.call( this, j, this.className ) );\n\t\t\t});\n\t\t}\n\t\tif ( proceed ) {\n\t\t\tclasses = ( value || \"\" ).match( rnotwhite ) || [];\n\n\t\t\tfor ( ; i < len; i++ ) {\n\t\t\t\telem = this[ i ];\n\t\t\t\t// This expression is here for better compressibility (see addClass)\n\t\t\t\tcur = elem.nodeType === 1 && ( elem.className ?\n\t\t\t\t\t( \" \" + elem.className + \" \" ).replace( rclass, \" \" ) :\n\t\t\t\t\t\"\"\n\t\t\t\t);\n\n\t\t\t\tif ( cur ) {\n\t\t\t\t\tj = 0;\n\t\t\t\t\twhile ( (clazz = classes[j++]) ) {\n\t\t\t\t\t\t// Remove *all* instances\n\t\t\t\t\t\twhile ( cur.indexOf( \" \" + clazz + \" \" ) >= 0 ) {\n\t\t\t\t\t\t\tcur = cur.replace( \" \" + clazz + \" \", \" \" );\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// only assign if different to avoid unneeded rendering.\n\t\t\t\t\tfinalValue = value ? jQuery.trim( cur ) : \"\";\n\t\t\t\t\tif ( elem.className !== finalValue ) {\n\t\t\t\t\t\telem.className = finalValue;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn this;\n\t},\n\n\ttoggleClass: function( value, stateVal ) {\n\t\tvar type = typeof value;\n\n\t\tif ( typeof stateVal === \"boolean\" && type === \"string\" ) {\n\t\t\treturn stateVal ? this.addClass( value ) : this.removeClass( value );\n\t\t}\n\n\t\tif ( jQuery.isFunction( value ) ) {\n\t\t\treturn this.each(function( i ) {\n\t\t\t\tjQuery( this ).toggleClass( value.call(this, i, this.className, stateVal), stateVal );\n\t\t\t});\n\t\t}\n\n\t\treturn this.each(function() {\n\t\t\tif ( type === \"string\" ) {\n\t\t\t\t// toggle individual class names\n\t\t\t\tvar className,\n\t\t\t\t\ti = 0,\n\t\t\t\t\tself = jQuery( this ),\n\t\t\t\t\tclassNames = value.match( rnotwhite ) || [];\n\n\t\t\t\twhile ( (className = classNames[ i++ ]) ) {\n\t\t\t\t\t// check each className given, space separated list\n\t\t\t\t\tif ( self.hasClass( className ) ) {\n\t\t\t\t\t\tself.removeClass( className );\n\t\t\t\t\t} else {\n\t\t\t\t\t\tself.addClass( className );\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t// Toggle whole class name\n\t\t\t} else if ( type === strundefined || type === \"boolean\" ) {\n\t\t\t\tif ( this.className ) {\n\t\t\t\t\t// store className if set\n\t\t\t\t\tdata_priv.set( this, \"__className__\", this.className );\n\t\t\t\t}\n\n\t\t\t\t// If the element has a class name or if we're passed \"false\",\n\t\t\t\t// then remove the whole classname (if there was one, the above saved it).\n\t\t\t\t// Otherwise bring back whatever was previously saved (if anything),\n\t\t\t\t// falling back to the empty string if nothing was stored.\n\t\t\t\tthis.className = this.className || value === false ? \"\" : data_priv.get( this, \"__className__\" ) || \"\";\n\t\t\t}\n\t\t});\n\t},\n\n\thasClass: function( selector ) {\n\t\tvar className = \" \" + selector + \" \",\n\t\t\ti = 0,\n\t\t\tl = this.length;\n\t\tfor ( ; i < l; i++ ) {\n\t\t\tif ( this[i].nodeType === 1 && (\" \" + this[i].className + \" \").replace(rclass, \" \").indexOf( className ) >= 0 ) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\t}\n});\n\n\n\n\nvar rreturn = /\\r/g;\n\njQuery.fn.extend({\n\tval: function( value ) {\n\t\tvar hooks, ret, isFunction,\n\t\t\telem = this[0];\n\n\t\tif ( !arguments.length ) {\n\t\t\tif ( elem ) {\n\t\t\t\thooks = jQuery.valHooks[ elem.type ] || jQuery.valHooks[ elem.nodeName.toLowerCase() ];\n\n\t\t\t\tif ( hooks && \"get\" in hooks && (ret = hooks.get( elem, \"value\" )) !== undefined ) {\n\t\t\t\t\treturn ret;\n\t\t\t\t}\n\n\t\t\t\tret = elem.value;\n\n\t\t\t\treturn typeof ret === \"string\" ?\n\t\t\t\t\t// handle most common string cases\n\t\t\t\t\tret.replace(rreturn, \"\") :\n\t\t\t\t\t// handle cases where value is null/undef or number\n\t\t\t\t\tret == null ? \"\" : ret;\n\t\t\t}\n\n\t\t\treturn;\n\t\t}\n\n\t\tisFunction = jQuery.isFunction( value );\n\n\t\treturn this.each(function( i ) {\n\t\t\tvar val;\n\n\t\t\tif ( this.nodeType !== 1 ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif ( isFunction ) {\n\t\t\t\tval = value.call( this, i, jQuery( this ).val() );\n\t\t\t} else {\n\t\t\t\tval = value;\n\t\t\t}\n\n\t\t\t// Treat null/undefined as \"\"; convert numbers to string\n\t\t\tif ( val == null ) {\n\t\t\t\tval = \"\";\n\n\t\t\t} else if ( typeof val === \"number\" ) {\n\t\t\t\tval += \"\";\n\n\t\t\t} else if ( jQuery.isArray( val ) ) {\n\t\t\t\tval = jQuery.map( val, function( value ) {\n\t\t\t\t\treturn value == null ? \"\" : value + \"\";\n\t\t\t\t});\n\t\t\t}\n\n\t\t\thooks = jQuery.valHooks[ this.type ] || jQuery.valHooks[ this.nodeName.toLowerCase() ];\n\n\t\t\t// If set returns undefined, fall back to normal setting\n\t\t\tif ( !hooks || !(\"set\" in hooks) || hooks.set( this, val, \"value\" ) === undefined ) {\n\t\t\t\tthis.value = val;\n\t\t\t}\n\t\t});\n\t}\n});\n\njQuery.extend({\n\tvalHooks: {\n\t\toption: {\n\t\t\tget: function( elem ) {\n\t\t\t\tvar val = jQuery.find.attr( elem, \"value\" );\n\t\t\t\treturn val != null ?\n\t\t\t\t\tval :\n\t\t\t\t\t// Support: IE10-11+\n\t\t\t\t\t// option.text throws exceptions (#14686, #14858)\n\t\t\t\t\tjQuery.trim( jQuery.text( elem ) );\n\t\t\t}\n\t\t},\n\t\tselect: {\n\t\t\tget: function( elem ) {\n\t\t\t\tvar value, option,\n\t\t\t\t\toptions = elem.options,\n\t\t\t\t\tindex = elem.selectedIndex,\n\t\t\t\t\tone = elem.type === \"select-one\" || index < 0,\n\t\t\t\t\tvalues = one ? null : [],\n\t\t\t\t\tmax = one ? index + 1 : options.length,\n\t\t\t\t\ti = index < 0 ?\n\t\t\t\t\t\tmax :\n\t\t\t\t\t\tone ? index : 0;\n\n\t\t\t\t// Loop through all the selected options\n\t\t\t\tfor ( ; i < max; i++ ) {\n\t\t\t\t\toption = options[ i ];\n\n\t\t\t\t\t// IE6-9 doesn't update selected after form reset (#2551)\n\t\t\t\t\tif ( ( option.selected || i === index ) &&\n\t\t\t\t\t\t\t// Don't return options that are disabled or in a disabled optgroup\n\t\t\t\t\t\t\t( support.optDisabled ? !option.disabled : option.getAttribute( \"disabled\" ) === null ) &&\n\t\t\t\t\t\t\t( !option.parentNode.disabled || !jQuery.nodeName( option.parentNode, \"optgroup\" ) ) ) {\n\n\t\t\t\t\t\t// Get the specific value for the option\n\t\t\t\t\t\tvalue = jQuery( option ).val();\n\n\t\t\t\t\t\t// We don't need an array for one selects\n\t\t\t\t\t\tif ( one ) {\n\t\t\t\t\t\t\treturn value;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Multi-Selects return an array\n\t\t\t\t\t\tvalues.push( value );\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\treturn values;\n\t\t\t},\n\n\t\t\tset: function( elem, value ) {\n\t\t\t\tvar optionSet, option,\n\t\t\t\t\toptions = elem.options,\n\t\t\t\t\tvalues = jQuery.makeArray( value ),\n\t\t\t\t\ti = options.length;\n\n\t\t\t\twhile ( i-- ) {\n\t\t\t\t\toption = options[ i ];\n\t\t\t\t\tif ( (option.selected = jQuery.inArray( option.value, values ) >= 0) ) {\n\t\t\t\t\t\toptionSet = true;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// force browsers to behave consistently when non-matching value is set\n\t\t\t\tif ( !optionSet ) {\n\t\t\t\t\telem.selectedIndex = -1;\n\t\t\t\t}\n\t\t\t\treturn values;\n\t\t\t}\n\t\t}\n\t}\n});\n\n// Radios and checkboxes getter/setter\njQuery.each([ \"radio\", \"checkbox\" ], function() {\n\tjQuery.valHooks[ this ] = {\n\t\tset: function( elem, value ) {\n\t\t\tif ( jQuery.isArray( value ) ) {\n\t\t\t\treturn ( elem.checked = jQuery.inArray( jQuery(elem).val(), value ) >= 0 );\n\t\t\t}\n\t\t}\n\t};\n\tif ( !support.checkOn ) {\n\t\tjQuery.valHooks[ this ].get = function( elem ) {\n\t\t\t// Support: Webkit\n\t\t\t// \"\" is returned instead of \"on\" if a value isn't specified\n\t\t\treturn elem.getAttribute(\"value\") === null ? \"on\" : elem.value;\n\t\t};\n\t}\n});\n\n\n\n\n// Return jQuery for attributes-only inclusion\n\n\njQuery.each( (\"blur focus focusin focusout load resize scroll unload click dblclick \" +\n\t\"mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave \" +\n\t\"change select submit keydown keypress keyup error contextmenu\").split(\" \"), function( i, name ) {\n\n\t// Handle event binding\n\tjQuery.fn[ name ] = function( data, fn ) {\n\t\treturn arguments.length > 0 ?\n\t\t\tthis.on( name, null, data, fn ) :\n\t\t\tthis.trigger( name );\n\t};\n});\n\njQuery.fn.extend({\n\thover: function( fnOver, fnOut ) {\n\t\treturn this.mouseenter( fnOver ).mouseleave( fnOut || fnOver );\n\t},\n\n\tbind: function( types, data, fn ) {\n\t\treturn this.on( types, null, data, fn );\n\t},\n\tunbind: function( types, fn ) {\n\t\treturn this.off( types, null, fn );\n\t},\n\n\tdelegate: function( selector, types, data, fn ) {\n\t\treturn this.on( types, selector, data, fn );\n\t},\n\tundelegate: function( selector, types, fn ) {\n\t\t// ( namespace ) or ( selector, types [, fn] )\n\t\treturn arguments.length === 1 ? this.off( selector, \"**\" ) : this.off( types, selector || \"**\", fn );\n\t}\n});\n\n\nvar nonce = jQuery.now();\n\nvar rquery = (/\\?/);\n\n\n\n// Support: Android 2.3\n// Workaround failure to string-cast null input\njQuery.parseJSON = function( data ) {\n\treturn JSON.parse( data + \"\" );\n};\n\n\n// Cross-browser xml parsing\njQuery.parseXML = function( data ) {\n\tvar xml, tmp;\n\tif ( !data || typeof data !== \"string\" ) {\n\t\treturn null;\n\t}\n\n\t// Support: IE9\n\ttry {\n\t\ttmp = new DOMParser();\n\t\txml = tmp.parseFromString( data, \"text/xml\" );\n\t} catch ( e ) {\n\t\txml = undefined;\n\t}\n\n\tif ( !xml || xml.getElementsByTagName( \"parsererror\" ).length ) {\n\t\tjQuery.error( \"Invalid XML: \" + data );\n\t}\n\treturn xml;\n};\n\n\nvar\n\t// Document location\n\tajaxLocParts,\n\tajaxLocation,\n\n\trhash = /#.*$/,\n\trts = /([?&])_=[^&]*/,\n\trheaders = /^(.*?):[ \\t]*([^\\r\\n]*)$/mg,\n\t// #7653, #8125, #8152: local protocol detection\n\trlocalProtocol = /^(?:about|app|app-storage|.+-extension|file|res|widget):$/,\n\trnoContent = /^(?:GET|HEAD)$/,\n\trprotocol = /^\\/\\//,\n\trurl = /^([\\w.+-]+:)(?:\\/\\/(?:[^\\/?#]*@|)([^\\/?#:]*)(?::(\\d+)|)|)/,\n\n\t/* Prefilters\n\t * 1) They are useful to introduce custom dataTypes (see ajax/jsonp.js for an example)\n\t * 2) These are called:\n\t *    - BEFORE asking for a transport\n\t *    - AFTER param serialization (s.data is a string if s.processData is true)\n\t * 3) key is the dataType\n\t * 4) the catchall symbol \"*\" can be used\n\t * 5) execution will start with transport dataType and THEN continue down to \"*\" if needed\n\t */\n\tprefilters = {},\n\n\t/* Transports bindings\n\t * 1) key is the dataType\n\t * 2) the catchall symbol \"*\" can be used\n\t * 3) selection will start with transport dataType and THEN go to \"*\" if needed\n\t */\n\ttransports = {},\n\n\t// Avoid comment-prolog char sequence (#10098); must appease lint and evade compression\n\tallTypes = \"*/\".concat(\"*\");\n\n// #8138, IE may throw an exception when accessing\n// a field from window.location if document.domain has been set\ntry {\n\tajaxLocation = location.href;\n} catch( e ) {\n\t// Use the href attribute of an A element\n\t// since IE will modify it given document.location\n\tajaxLocation = document.createElement( \"a\" );\n\tajaxLocation.href = \"\";\n\tajaxLocation = ajaxLocation.href;\n}\n\n// Segment location into parts\najaxLocParts = rurl.exec( ajaxLocation.toLowerCase() ) || [];\n\n// Base \"constructor\" for jQuery.ajaxPrefilter and jQuery.ajaxTransport\nfunction addToPrefiltersOrTransports( structure ) {\n\n\t// dataTypeExpression is optional and defaults to \"*\"\n\treturn function( dataTypeExpression, func ) {\n\n\t\tif ( typeof dataTypeExpression !== \"string\" ) {\n\t\t\tfunc = dataTypeExpression;\n\t\t\tdataTypeExpression = \"*\";\n\t\t}\n\n\t\tvar dataType,\n\t\t\ti = 0,\n\t\t\tdataTypes = dataTypeExpression.toLowerCase().match( rnotwhite ) || [];\n\n\t\tif ( jQuery.isFunction( func ) ) {\n\t\t\t// For each dataType in the dataTypeExpression\n\t\t\twhile ( (dataType = dataTypes[i++]) ) {\n\t\t\t\t// Prepend if requested\n\t\t\t\tif ( dataType[0] === \"+\" ) {\n\t\t\t\t\tdataType = dataType.slice( 1 ) || \"*\";\n\t\t\t\t\t(structure[ dataType ] = structure[ dataType ] || []).unshift( func );\n\n\t\t\t\t// Otherwise append\n\t\t\t\t} else {\n\t\t\t\t\t(structure[ dataType ] = structure[ dataType ] || []).push( func );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t};\n}\n\n// Base inspection function for prefilters and transports\nfunction inspectPrefiltersOrTransports( structure, options, originalOptions, jqXHR ) {\n\n\tvar inspected = {},\n\t\tseekingTransport = ( structure === transports );\n\n\tfunction inspect( dataType ) {\n\t\tvar selected;\n\t\tinspected[ dataType ] = true;\n\t\tjQuery.each( structure[ dataType ] || [], function( _, prefilterOrFactory ) {\n\t\t\tvar dataTypeOrTransport = prefilterOrFactory( options, originalOptions, jqXHR );\n\t\t\tif ( typeof dataTypeOrTransport === \"string\" && !seekingTransport && !inspected[ dataTypeOrTransport ] ) {\n\t\t\t\toptions.dataTypes.unshift( dataTypeOrTransport );\n\t\t\t\tinspect( dataTypeOrTransport );\n\t\t\t\treturn false;\n\t\t\t} else if ( seekingTransport ) {\n\t\t\t\treturn !( selected = dataTypeOrTransport );\n\t\t\t}\n\t\t});\n\t\treturn selected;\n\t}\n\n\treturn inspect( options.dataTypes[ 0 ] ) || !inspected[ \"*\" ] && inspect( \"*\" );\n}\n\n// A special extend for ajax options\n// that takes \"flat\" options (not to be deep extended)\n// Fixes #9887\nfunction ajaxExtend( target, src ) {\n\tvar key, deep,\n\t\tflatOptions = jQuery.ajaxSettings.flatOptions || {};\n\n\tfor ( key in src ) {\n\t\tif ( src[ key ] !== undefined ) {\n\t\t\t( flatOptions[ key ] ? target : ( deep || (deep = {}) ) )[ key ] = src[ key ];\n\t\t}\n\t}\n\tif ( deep ) {\n\t\tjQuery.extend( true, target, deep );\n\t}\n\n\treturn target;\n}\n\n/* Handles responses to an ajax request:\n * - finds the right dataType (mediates between content-type and expected dataType)\n * - returns the corresponding response\n */\nfunction ajaxHandleResponses( s, jqXHR, responses ) {\n\n\tvar ct, type, finalDataType, firstDataType,\n\t\tcontents = s.contents,\n\t\tdataTypes = s.dataTypes;\n\n\t// Remove auto dataType and get content-type in the process\n\twhile ( dataTypes[ 0 ] === \"*\" ) {\n\t\tdataTypes.shift();\n\t\tif ( ct === undefined ) {\n\t\t\tct = s.mimeType || jqXHR.getResponseHeader(\"Content-Type\");\n\t\t}\n\t}\n\n\t// Check if we're dealing with a known content-type\n\tif ( ct ) {\n\t\tfor ( type in contents ) {\n\t\t\tif ( contents[ type ] && contents[ type ].test( ct ) ) {\n\t\t\t\tdataTypes.unshift( type );\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\n\t// Check to see if we have a response for the expected dataType\n\tif ( dataTypes[ 0 ] in responses ) {\n\t\tfinalDataType = dataTypes[ 0 ];\n\t} else {\n\t\t// Try convertible dataTypes\n\t\tfor ( type in responses ) {\n\t\t\tif ( !dataTypes[ 0 ] || s.converters[ type + \" \" + dataTypes[0] ] ) {\n\t\t\t\tfinalDataType = type;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif ( !firstDataType ) {\n\t\t\t\tfirstDataType = type;\n\t\t\t}\n\t\t}\n\t\t// Or just use first one\n\t\tfinalDataType = finalDataType || firstDataType;\n\t}\n\n\t// If we found a dataType\n\t// We add the dataType to the list if needed\n\t// and return the corresponding response\n\tif ( finalDataType ) {\n\t\tif ( finalDataType !== dataTypes[ 0 ] ) {\n\t\t\tdataTypes.unshift( finalDataType );\n\t\t}\n\t\treturn responses[ finalDataType ];\n\t}\n}\n\n/* Chain conversions given the request and the original response\n * Also sets the responseXXX fields on the jqXHR instance\n */\nfunction ajaxConvert( s, response, jqXHR, isSuccess ) {\n\tvar conv2, current, conv, tmp, prev,\n\t\tconverters = {},\n\t\t// Work with a copy of dataTypes in case we need to modify it for conversion\n\t\tdataTypes = s.dataTypes.slice();\n\n\t// Create converters map with lowercased keys\n\tif ( dataTypes[ 1 ] ) {\n\t\tfor ( conv in s.converters ) {\n\t\t\tconverters[ conv.toLowerCase() ] = s.converters[ conv ];\n\t\t}\n\t}\n\n\tcurrent = dataTypes.shift();\n\n\t// Convert to each sequential dataType\n\twhile ( current ) {\n\n\t\tif ( s.responseFields[ current ] ) {\n\t\t\tjqXHR[ s.responseFields[ current ] ] = response;\n\t\t}\n\n\t\t// Apply the dataFilter if provided\n\t\tif ( !prev && isSuccess && s.dataFilter ) {\n\t\t\tresponse = s.dataFilter( response, s.dataType );\n\t\t}\n\n\t\tprev = current;\n\t\tcurrent = dataTypes.shift();\n\n\t\tif ( current ) {\n\n\t\t// There's only work to do if current dataType is non-auto\n\t\t\tif ( current === \"*\" ) {\n\n\t\t\t\tcurrent = prev;\n\n\t\t\t// Convert response if prev dataType is non-auto and differs from current\n\t\t\t} else if ( prev !== \"*\" && prev !== current ) {\n\n\t\t\t\t// Seek a direct converter\n\t\t\t\tconv = converters[ prev + \" \" + current ] || converters[ \"* \" + current ];\n\n\t\t\t\t// If none found, seek a pair\n\t\t\t\tif ( !conv ) {\n\t\t\t\t\tfor ( conv2 in converters ) {\n\n\t\t\t\t\t\t// If conv2 outputs current\n\t\t\t\t\t\ttmp = conv2.split( \" \" );\n\t\t\t\t\t\tif ( tmp[ 1 ] === current ) {\n\n\t\t\t\t\t\t\t// If prev can be converted to accepted input\n\t\t\t\t\t\t\tconv = converters[ prev + \" \" + tmp[ 0 ] ] ||\n\t\t\t\t\t\t\t\tconverters[ \"* \" + tmp[ 0 ] ];\n\t\t\t\t\t\t\tif ( conv ) {\n\t\t\t\t\t\t\t\t// Condense equivalence converters\n\t\t\t\t\t\t\t\tif ( conv === true ) {\n\t\t\t\t\t\t\t\t\tconv = converters[ conv2 ];\n\n\t\t\t\t\t\t\t\t// Otherwise, insert the intermediate dataType\n\t\t\t\t\t\t\t\t} else if ( converters[ conv2 ] !== true ) {\n\t\t\t\t\t\t\t\t\tcurrent = tmp[ 0 ];\n\t\t\t\t\t\t\t\t\tdataTypes.unshift( tmp[ 1 ] );\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Apply converter (if not an equivalence)\n\t\t\t\tif ( conv !== true ) {\n\n\t\t\t\t\t// Unless errors are allowed to bubble, catch and return them\n\t\t\t\t\tif ( conv && s[ \"throws\" ] ) {\n\t\t\t\t\t\tresponse = conv( response );\n\t\t\t\t\t} else {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tresponse = conv( response );\n\t\t\t\t\t\t} catch ( e ) {\n\t\t\t\t\t\t\treturn { state: \"parsererror\", error: conv ? e : \"No conversion from \" + prev + \" to \" + current };\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn { state: \"success\", data: response };\n}\n\njQuery.extend({\n\n\t// Counter for holding the number of active queries\n\tactive: 0,\n\n\t// Last-Modified header cache for next request\n\tlastModified: {},\n\tetag: {},\n\n\tajaxSettings: {\n\t\turl: ajaxLocation,\n\t\ttype: \"GET\",\n\t\tisLocal: rlocalProtocol.test( ajaxLocParts[ 1 ] ),\n\t\tglobal: true,\n\t\tprocessData: true,\n\t\tasync: true,\n\t\tcontentType: \"application/x-www-form-urlencoded; charset=UTF-8\",\n\t\t/*\n\t\ttimeout: 0,\n\t\tdata: null,\n\t\tdataType: null,\n\t\tusername: null,\n\t\tpassword: null,\n\t\tcache: null,\n\t\tthrows: false,\n\t\ttraditional: false,\n\t\theaders: {},\n\t\t*/\n\n\t\taccepts: {\n\t\t\t\"*\": allTypes,\n\t\t\ttext: \"text/plain\",\n\t\t\thtml: \"text/html\",\n\t\t\txml: \"application/xml, text/xml\",\n\t\t\tjson: \"application/json, text/javascript\"\n\t\t},\n\n\t\tcontents: {\n\t\t\txml: /xml/,\n\t\t\thtml: /html/,\n\t\t\tjson: /json/\n\t\t},\n\n\t\tresponseFields: {\n\t\t\txml: \"responseXML\",\n\t\t\ttext: \"responseText\",\n\t\t\tjson: \"responseJSON\"\n\t\t},\n\n\t\t// Data converters\n\t\t// Keys separate source (or catchall \"*\") and destination types with a single space\n\t\tconverters: {\n\n\t\t\t// Convert anything to text\n\t\t\t\"* text\": String,\n\n\t\t\t// Text to html (true = no transformation)\n\t\t\t\"text html\": true,\n\n\t\t\t// Evaluate text as a json expression\n\t\t\t\"text json\": jQuery.parseJSON,\n\n\t\t\t// Parse text as xml\n\t\t\t\"text xml\": jQuery.parseXML\n\t\t},\n\n\t\t// For options that shouldn't be deep extended:\n\t\t// you can add your own custom options here if\n\t\t// and when you create one that shouldn't be\n\t\t// deep extended (see ajaxExtend)\n\t\tflatOptions: {\n\t\t\turl: true,\n\t\t\tcontext: true\n\t\t}\n\t},\n\n\t// Creates a full fledged settings object into target\n\t// with both ajaxSettings and settings fields.\n\t// If target is omitted, writes into ajaxSettings.\n\tajaxSetup: function( target, settings ) {\n\t\treturn settings ?\n\n\t\t\t// Building a settings object\n\t\t\tajaxExtend( ajaxExtend( target, jQuery.ajaxSettings ), settings ) :\n\n\t\t\t// Extending ajaxSettings\n\t\t\tajaxExtend( jQuery.ajaxSettings, target );\n\t},\n\n\tajaxPrefilter: addToPrefiltersOrTransports( prefilters ),\n\tajaxTransport: addToPrefiltersOrTransports( transports ),\n\n\t// Main method\n\tajax: function( url, options ) {\n\n\t\t// If url is an object, simulate pre-1.5 signature\n\t\tif ( typeof url === \"object\" ) {\n\t\t\toptions = url;\n\t\t\turl = undefined;\n\t\t}\n\n\t\t// Force options to be an object\n\t\toptions = options || {};\n\n\t\tvar transport,\n\t\t\t// URL without anti-cache param\n\t\t\tcacheURL,\n\t\t\t// Response headers\n\t\t\tresponseHeadersString,\n\t\t\tresponseHeaders,\n\t\t\t// timeout handle\n\t\t\ttimeoutTimer,\n\t\t\t// Cross-domain detection vars\n\t\t\tparts,\n\t\t\t// To know if global events are to be dispatched\n\t\t\tfireGlobals,\n\t\t\t// Loop variable\n\t\t\ti,\n\t\t\t// Create the final options object\n\t\t\ts = jQuery.ajaxSetup( {}, options ),\n\t\t\t// Callbacks context\n\t\t\tcallbackContext = s.context || s,\n\t\t\t// Context for global events is callbackContext if it is a DOM node or jQuery collection\n\t\t\tglobalEventContext = s.context && ( callbackContext.nodeType || callbackContext.jquery ) ?\n\t\t\t\tjQuery( callbackContext ) :\n\t\t\t\tjQuery.event,\n\t\t\t// Deferreds\n\t\t\tdeferred = jQuery.Deferred(),\n\t\t\tcompleteDeferred = jQuery.Callbacks(\"once memory\"),\n\t\t\t// Status-dependent callbacks\n\t\t\tstatusCode = s.statusCode || {},\n\t\t\t// Headers (they are sent all at once)\n\t\t\trequestHeaders = {},\n\t\t\trequestHeadersNames = {},\n\t\t\t// The jqXHR state\n\t\t\tstate = 0,\n\t\t\t// Default abort message\n\t\t\tstrAbort = \"canceled\",\n\t\t\t// Fake xhr\n\t\t\tjqXHR = {\n\t\t\t\treadyState: 0,\n\n\t\t\t\t// Builds headers hashtable if needed\n\t\t\t\tgetResponseHeader: function( key ) {\n\t\t\t\t\tvar match;\n\t\t\t\t\tif ( state === 2 ) {\n\t\t\t\t\t\tif ( !responseHeaders ) {\n\t\t\t\t\t\t\tresponseHeaders = {};\n\t\t\t\t\t\t\twhile ( (match = rheaders.exec( responseHeadersString )) ) {\n\t\t\t\t\t\t\t\tresponseHeaders[ match[1].toLowerCase() ] = match[ 2 ];\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tmatch = responseHeaders[ key.toLowerCase() ];\n\t\t\t\t\t}\n\t\t\t\t\treturn match == null ? null : match;\n\t\t\t\t},\n\n\t\t\t\t// Raw string\n\t\t\t\tgetAllResponseHeaders: function() {\n\t\t\t\t\treturn state === 2 ? responseHeadersString : null;\n\t\t\t\t},\n\n\t\t\t\t// Caches the header\n\t\t\t\tsetRequestHeader: function( name, value ) {\n\t\t\t\t\tvar lname = name.toLowerCase();\n\t\t\t\t\tif ( !state ) {\n\t\t\t\t\t\tname = requestHeadersNames[ lname ] = requestHeadersNames[ lname ] || name;\n\t\t\t\t\t\trequestHeaders[ name ] = value;\n\t\t\t\t\t}\n\t\t\t\t\treturn this;\n\t\t\t\t},\n\n\t\t\t\t// Overrides response content-type header\n\t\t\t\toverrideMimeType: function( type ) {\n\t\t\t\t\tif ( !state ) {\n\t\t\t\t\t\ts.mimeType = type;\n\t\t\t\t\t}\n\t\t\t\t\treturn this;\n\t\t\t\t},\n\n\t\t\t\t// Status-dependent callbacks\n\t\t\t\tstatusCode: function( map ) {\n\t\t\t\t\tvar code;\n\t\t\t\t\tif ( map ) {\n\t\t\t\t\t\tif ( state < 2 ) {\n\t\t\t\t\t\t\tfor ( code in map ) {\n\t\t\t\t\t\t\t\t// Lazy-add the new callback in a way that preserves old ones\n\t\t\t\t\t\t\t\tstatusCode[ code ] = [ statusCode[ code ], map[ code ] ];\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// Execute the appropriate callbacks\n\t\t\t\t\t\t\tjqXHR.always( map[ jqXHR.status ] );\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\treturn this;\n\t\t\t\t},\n\n\t\t\t\t// Cancel the request\n\t\t\t\tabort: function( statusText ) {\n\t\t\t\t\tvar finalText = statusText || strAbort;\n\t\t\t\t\tif ( transport ) {\n\t\t\t\t\t\ttransport.abort( finalText );\n\t\t\t\t\t}\n\t\t\t\t\tdone( 0, finalText );\n\t\t\t\t\treturn this;\n\t\t\t\t}\n\t\t\t};\n\n\t\t// Attach deferreds\n\t\tdeferred.promise( jqXHR ).complete = completeDeferred.add;\n\t\tjqXHR.success = jqXHR.done;\n\t\tjqXHR.error = jqXHR.fail;\n\n\t\t// Remove hash character (#7531: and string promotion)\n\t\t// Add protocol if not provided (prefilters might expect it)\n\t\t// Handle falsy url in the settings object (#10093: consistency with old signature)\n\t\t// We also use the url parameter if available\n\t\ts.url = ( ( url || s.url || ajaxLocation ) + \"\" ).replace( rhash, \"\" )\n\t\t\t.replace( rprotocol, ajaxLocParts[ 1 ] + \"//\" );\n\n\t\t// Alias method option to type as per ticket #12004\n\t\ts.type = options.method || options.type || s.method || s.type;\n\n\t\t// Extract dataTypes list\n\t\ts.dataTypes = jQuery.trim( s.dataType || \"*\" ).toLowerCase().match( rnotwhite ) || [ \"\" ];\n\n\t\t// A cross-domain request is in order when we have a protocol:host:port mismatch\n\t\tif ( s.crossDomain == null ) {\n\t\t\tparts = rurl.exec( s.url.toLowerCase() );\n\t\t\ts.crossDomain = !!( parts &&\n\t\t\t\t( parts[ 1 ] !== ajaxLocParts[ 1 ] || parts[ 2 ] !== ajaxLocParts[ 2 ] ||\n\t\t\t\t\t( parts[ 3 ] || ( parts[ 1 ] === \"http:\" ? \"80\" : \"443\" ) ) !==\n\t\t\t\t\t\t( ajaxLocParts[ 3 ] || ( ajaxLocParts[ 1 ] === \"http:\" ? \"80\" : \"443\" ) ) )\n\t\t\t);\n\t\t}\n\n\t\t// Convert data if not already a string\n\t\tif ( s.data && s.processData && typeof s.data !== \"string\" ) {\n\t\t\ts.data = jQuery.param( s.data, s.traditional );\n\t\t}\n\n\t\t// Apply prefilters\n\t\tinspectPrefiltersOrTransports( prefilters, s, options, jqXHR );\n\n\t\t// If request was aborted inside a prefilter, stop there\n\t\tif ( state === 2 ) {\n\t\t\treturn jqXHR;\n\t\t}\n\n\t\t// We can fire global events as of now if asked to\n\t\tfireGlobals = s.global;\n\n\t\t// Watch for a new set of requests\n\t\tif ( fireGlobals && jQuery.active++ === 0 ) {\n\t\t\tjQuery.event.trigger(\"ajaxStart\");\n\t\t}\n\n\t\t// Uppercase the type\n\t\ts.type = s.type.toUpperCase();\n\n\t\t// Determine if request has content\n\t\ts.hasContent = !rnoContent.test( s.type );\n\n\t\t// Save the URL in case we're toying with the If-Modified-Since\n\t\t// and/or If-None-Match header later on\n\t\tcacheURL = s.url;\n\n\t\t// More options handling for requests with no content\n\t\tif ( !s.hasContent ) {\n\n\t\t\t// If data is available, append data to url\n\t\t\tif ( s.data ) {\n\t\t\t\tcacheURL = ( s.url += ( rquery.test( cacheURL ) ? \"&\" : \"?\" ) + s.data );\n\t\t\t\t// #9682: remove data so that it's not used in an eventual retry\n\t\t\t\tdelete s.data;\n\t\t\t}\n\n\t\t\t// Add anti-cache in url if needed\n\t\t\tif ( s.cache === false ) {\n\t\t\t\ts.url = rts.test( cacheURL ) ?\n\n\t\t\t\t\t// If there is already a '_' parameter, set its value\n\t\t\t\t\tcacheURL.replace( rts, \"$1_=\" + nonce++ ) :\n\n\t\t\t\t\t// Otherwise add one to the end\n\t\t\t\t\tcacheURL + ( rquery.test( cacheURL ) ? \"&\" : \"?\" ) + \"_=\" + nonce++;\n\t\t\t}\n\t\t}\n\n\t\t// Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode.\n\t\tif ( s.ifModified ) {\n\t\t\tif ( jQuery.lastModified[ cacheURL ] ) {\n\t\t\t\tjqXHR.setRequestHeader( \"If-Modified-Since\", jQuery.lastModified[ cacheURL ] );\n\t\t\t}\n\t\t\tif ( jQuery.etag[ cacheURL ] ) {\n\t\t\t\tjqXHR.setRequestHeader( \"If-None-Match\", jQuery.etag[ cacheURL ] );\n\t\t\t}\n\t\t}\n\n\t\t// Set the correct header, if data is being sent\n\t\tif ( s.data && s.hasContent && s.contentType !== false || options.contentType ) {\n\t\t\tjqXHR.setRequestHeader( \"Content-Type\", s.contentType );\n\t\t}\n\n\t\t// Set the Accepts header for the server, depending on the dataType\n\t\tjqXHR.setRequestHeader(\n\t\t\t\"Accept\",\n\t\t\ts.dataTypes[ 0 ] && s.accepts[ s.dataTypes[0] ] ?\n\t\t\t\ts.accepts[ s.dataTypes[0] ] + ( s.dataTypes[ 0 ] !== \"*\" ? \", \" + allTypes + \"; q=0.01\" : \"\" ) :\n\t\t\t\ts.accepts[ \"*\" ]\n\t\t);\n\n\t\t// Check for headers option\n\t\tfor ( i in s.headers ) {\n\t\t\tjqXHR.setRequestHeader( i, s.headers[ i ] );\n\t\t}\n\n\t\t// Allow custom headers/mimetypes and early abort\n\t\tif ( s.beforeSend && ( s.beforeSend.call( callbackContext, jqXHR, s ) === false || state === 2 ) ) {\n\t\t\t// Abort if not done already and return\n\t\t\treturn jqXHR.abort();\n\t\t}\n\n\t\t// aborting is no longer a cancellation\n\t\tstrAbort = \"abort\";\n\n\t\t// Install callbacks on deferreds\n\t\tfor ( i in { success: 1, error: 1, complete: 1 } ) {\n\t\t\tjqXHR[ i ]( s[ i ] );\n\t\t}\n\n\t\t// Get transport\n\t\ttransport = inspectPrefiltersOrTransports( transports, s, options, jqXHR );\n\n\t\t// If no transport, we auto-abort\n\t\tif ( !transport ) {\n\t\t\tdone( -1, \"No Transport\" );\n\t\t} else {\n\t\t\tjqXHR.readyState = 1;\n\n\t\t\t// Send global event\n\t\t\tif ( fireGlobals ) {\n\t\t\t\tglobalEventContext.trigger( \"ajaxSend\", [ jqXHR, s ] );\n\t\t\t}\n\t\t\t// Timeout\n\t\t\tif ( s.async && s.timeout > 0 ) {\n\t\t\t\ttimeoutTimer = setTimeout(function() {\n\t\t\t\t\tjqXHR.abort(\"timeout\");\n\t\t\t\t}, s.timeout );\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\tstate = 1;\n\t\t\t\ttransport.send( requestHeaders, done );\n\t\t\t} catch ( e ) {\n\t\t\t\t// Propagate exception as error if not done\n\t\t\t\tif ( state < 2 ) {\n\t\t\t\t\tdone( -1, e );\n\t\t\t\t// Simply rethrow otherwise\n\t\t\t\t} else {\n\t\t\t\t\tthrow e;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Callback for when everything is done\n\t\tfunction done( status, nativeStatusText, responses, headers ) {\n\t\t\tvar isSuccess, success, error, response, modified,\n\t\t\t\tstatusText = nativeStatusText;\n\n\t\t\t// Called once\n\t\t\tif ( state === 2 ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// State is \"done\" now\n\t\t\tstate = 2;\n\n\t\t\t// Clear timeout if it exists\n\t\t\tif ( timeoutTimer ) {\n\t\t\t\tclearTimeout( timeoutTimer );\n\t\t\t}\n\n\t\t\t// Dereference transport for early garbage collection\n\t\t\t// (no matter how long the jqXHR object will be used)\n\t\t\ttransport = undefined;\n\n\t\t\t// Cache response headers\n\t\t\tresponseHeadersString = headers || \"\";\n\n\t\t\t// Set readyState\n\t\t\tjqXHR.readyState = status > 0 ? 4 : 0;\n\n\t\t\t// Determine if successful\n\t\t\tisSuccess = status >= 200 && status < 300 || status === 304;\n\n\t\t\t// Get response data\n\t\t\tif ( responses ) {\n\t\t\t\tresponse = ajaxHandleResponses( s, jqXHR, responses );\n\t\t\t}\n\n\t\t\t// Convert no matter what (that way responseXXX fields are always set)\n\t\t\tresponse = ajaxConvert( s, response, jqXHR, isSuccess );\n\n\t\t\t// If successful, handle type chaining\n\t\t\tif ( isSuccess ) {\n\n\t\t\t\t// Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode.\n\t\t\t\tif ( s.ifModified ) {\n\t\t\t\t\tmodified = jqXHR.getResponseHeader(\"Last-Modified\");\n\t\t\t\t\tif ( modified ) {\n\t\t\t\t\t\tjQuery.lastModified[ cacheURL ] = modified;\n\t\t\t\t\t}\n\t\t\t\t\tmodified = jqXHR.getResponseHeader(\"etag\");\n\t\t\t\t\tif ( modified ) {\n\t\t\t\t\t\tjQuery.etag[ cacheURL ] = modified;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// if no content\n\t\t\t\tif ( status === 204 || s.type === \"HEAD\" ) {\n\t\t\t\t\tstatusText = \"nocontent\";\n\n\t\t\t\t// if not modified\n\t\t\t\t} else if ( status === 304 ) {\n\t\t\t\t\tstatusText = \"notmodified\";\n\n\t\t\t\t// If we have data, let's convert it\n\t\t\t\t} else {\n\t\t\t\t\tstatusText = response.state;\n\t\t\t\t\tsuccess = response.data;\n\t\t\t\t\terror = response.error;\n\t\t\t\t\tisSuccess = !error;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// We extract error from statusText\n\t\t\t\t// then normalize statusText and status for non-aborts\n\t\t\t\terror = statusText;\n\t\t\t\tif ( status || !statusText ) {\n\t\t\t\t\tstatusText = \"error\";\n\t\t\t\t\tif ( status < 0 ) {\n\t\t\t\t\t\tstatus = 0;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Set data for the fake xhr object\n\t\t\tjqXHR.status = status;\n\t\t\tjqXHR.statusText = ( nativeStatusText || statusText ) + \"\";\n\n\t\t\t// Success/Error\n\t\t\tif ( isSuccess ) {\n\t\t\t\tdeferred.resolveWith( callbackContext, [ success, statusText, jqXHR ] );\n\t\t\t} else {\n\t\t\t\tdeferred.rejectWith( callbackContext, [ jqXHR, statusText, error ] );\n\t\t\t}\n\n\t\t\t// Status-dependent callbacks\n\t\t\tjqXHR.statusCode( statusCode );\n\t\t\tstatusCode = undefined;\n\n\t\t\tif ( fireGlobals ) {\n\t\t\t\tglobalEventContext.trigger( isSuccess ? \"ajaxSuccess\" : \"ajaxError\",\n\t\t\t\t\t[ jqXHR, s, isSuccess ? success : error ] );\n\t\t\t}\n\n\t\t\t// Complete\n\t\t\tcompleteDeferred.fireWith( callbackContext, [ jqXHR, statusText ] );\n\n\t\t\tif ( fireGlobals ) {\n\t\t\t\tglobalEventContext.trigger( \"ajaxComplete\", [ jqXHR, s ] );\n\t\t\t\t// Handle the global AJAX counter\n\t\t\t\tif ( !( --jQuery.active ) ) {\n\t\t\t\t\tjQuery.event.trigger(\"ajaxStop\");\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn jqXHR;\n\t},\n\n\tgetJSON: function( url, data, callback ) {\n\t\treturn jQuery.get( url, data, callback, \"json\" );\n\t},\n\n\tgetScript: function( url, callback ) {\n\t\treturn jQuery.get( url, undefined, callback, \"script\" );\n\t}\n});\n\njQuery.each( [ \"get\", \"post\" ], function( i, method ) {\n\tjQuery[ method ] = function( url, data, callback, type ) {\n\t\t// shift arguments if data argument was omitted\n\t\tif ( jQuery.isFunction( data ) ) {\n\t\t\ttype = type || callback;\n\t\t\tcallback = data;\n\t\t\tdata = undefined;\n\t\t}\n\n\t\treturn jQuery.ajax({\n\t\t\turl: url,\n\t\t\ttype: method,\n\t\t\tdataType: type,\n\t\t\tdata: data,\n\t\t\tsuccess: callback\n\t\t});\n\t};\n});\n\n// Attach a bunch of functions for handling common AJAX events\njQuery.each( [ \"ajaxStart\", \"ajaxStop\", \"ajaxComplete\", \"ajaxError\", \"ajaxSuccess\", \"ajaxSend\" ], function( i, type ) {\n\tjQuery.fn[ type ] = function( fn ) {\n\t\treturn this.on( type, fn );\n\t};\n});\n\n\njQuery._evalUrl = function( url ) {\n\treturn jQuery.ajax({\n\t\turl: url,\n\t\ttype: \"GET\",\n\t\tdataType: \"script\",\n\t\tasync: false,\n\t\tglobal: false,\n\t\t\"throws\": true\n\t});\n};\n\n\njQuery.fn.extend({\n\twrapAll: function( html ) {\n\t\tvar wrap;\n\n\t\tif ( jQuery.isFunction( html ) ) {\n\t\t\treturn this.each(function( i ) {\n\t\t\t\tjQuery( this ).wrapAll( html.call(this, i) );\n\t\t\t});\n\t\t}\n\n\t\tif ( this[ 0 ] ) {\n\n\t\t\t// The elements to wrap the target around\n\t\t\twrap = jQuery( html, this[ 0 ].ownerDocument ).eq( 0 ).clone( true );\n\n\t\t\tif ( this[ 0 ].parentNode ) {\n\t\t\t\twrap.insertBefore( this[ 0 ] );\n\t\t\t}\n\n\t\t\twrap.map(function() {\n\t\t\t\tvar elem = this;\n\n\t\t\t\twhile ( elem.firstElementChild ) {\n\t\t\t\t\telem = elem.firstElementChild;\n\t\t\t\t}\n\n\t\t\t\treturn elem;\n\t\t\t}).append( this );\n\t\t}\n\n\t\treturn this;\n\t},\n\n\twrapInner: function( html ) {\n\t\tif ( jQuery.isFunction( html ) ) {\n\t\t\treturn this.each(function( i ) {\n\t\t\t\tjQuery( this ).wrapInner( html.call(this, i) );\n\t\t\t});\n\t\t}\n\n\t\treturn this.each(function() {\n\t\t\tvar self = jQuery( this ),\n\t\t\t\tcontents = self.contents();\n\n\t\t\tif ( contents.length ) {\n\t\t\t\tcontents.wrapAll( html );\n\n\t\t\t} else {\n\t\t\t\tself.append( html );\n\t\t\t}\n\t\t});\n\t},\n\n\twrap: function( html ) {\n\t\tvar isFunction = jQuery.isFunction( html );\n\n\t\treturn this.each(function( i ) {\n\t\t\tjQuery( this ).wrapAll( isFunction ? html.call(this, i) : html );\n\t\t});\n\t},\n\n\tunwrap: function() {\n\t\treturn this.parent().each(function() {\n\t\t\tif ( !jQuery.nodeName( this, \"body\" ) ) {\n\t\t\t\tjQuery( this ).replaceWith( this.childNodes );\n\t\t\t}\n\t\t}).end();\n\t}\n});\n\n\njQuery.expr.filters.hidden = function( elem ) {\n\t// Support: Opera <= 12.12\n\t// Opera reports offsetWidths and offsetHeights less than zero on some elements\n\treturn elem.offsetWidth <= 0 && elem.offsetHeight <= 0;\n};\njQuery.expr.filters.visible = function( elem ) {\n\treturn !jQuery.expr.filters.hidden( elem );\n};\n\n\n\n\nvar r20 = /%20/g,\n\trbracket = /\\[\\]$/,\n\trCRLF = /\\r?\\n/g,\n\trsubmitterTypes = /^(?:submit|button|image|reset|file)$/i,\n\trsubmittable = /^(?:input|select|textarea|keygen)/i;\n\nfunction buildParams( prefix, obj, traditional, add ) {\n\tvar name;\n\n\tif ( jQuery.isArray( obj ) ) {\n\t\t// Serialize array item.\n\t\tjQuery.each( obj, function( i, v ) {\n\t\t\tif ( traditional || rbracket.test( prefix ) ) {\n\t\t\t\t// Treat each array item as a scalar.\n\t\t\t\tadd( prefix, v );\n\n\t\t\t} else {\n\t\t\t\t// Item is non-scalar (array or object), encode its numeric index.\n\t\t\t\tbuildParams( prefix + \"[\" + ( typeof v === \"object\" ? i : \"\" ) + \"]\", v, traditional, add );\n\t\t\t}\n\t\t});\n\n\t} else if ( !traditional && jQuery.type( obj ) === \"object\" ) {\n\t\t// Serialize object item.\n\t\tfor ( name in obj ) {\n\t\t\tbuildParams( prefix + \"[\" + name + \"]\", obj[ name ], traditional, add );\n\t\t}\n\n\t} else {\n\t\t// Serialize scalar item.\n\t\tadd( prefix, obj );\n\t}\n}\n\n// Serialize an array of form elements or a set of\n// key/values into a query string\njQuery.param = function( a, traditional ) {\n\tvar prefix,\n\t\ts = [],\n\t\tadd = function( key, value ) {\n\t\t\t// If value is a function, invoke it and return its value\n\t\t\tvalue = jQuery.isFunction( value ) ? value() : ( value == null ? \"\" : value );\n\t\t\ts[ s.length ] = encodeURIComponent( key ) + \"=\" + encodeURIComponent( value );\n\t\t};\n\n\t// Set traditional to true for jQuery <= 1.3.2 behavior.\n\tif ( traditional === undefined ) {\n\t\ttraditional = jQuery.ajaxSettings && jQuery.ajaxSettings.traditional;\n\t}\n\n\t// If an array was passed in, assume that it is an array of form elements.\n\tif ( jQuery.isArray( a ) || ( a.jquery && !jQuery.isPlainObject( a ) ) ) {\n\t\t// Serialize the form elements\n\t\tjQuery.each( a, function() {\n\t\t\tadd( this.name, this.value );\n\t\t});\n\n\t} else {\n\t\t// If traditional, encode the \"old\" way (the way 1.3.2 or older\n\t\t// did it), otherwise encode params recursively.\n\t\tfor ( prefix in a ) {\n\t\t\tbuildParams( prefix, a[ prefix ], traditional, add );\n\t\t}\n\t}\n\n\t// Return the resulting serialization\n\treturn s.join( \"&\" ).replace( r20, \"+\" );\n};\n\njQuery.fn.extend({\n\tserialize: function() {\n\t\treturn jQuery.param( this.serializeArray() );\n\t},\n\tserializeArray: function() {\n\t\treturn this.map(function() {\n\t\t\t// Can add propHook for \"elements\" to filter or add form elements\n\t\t\tvar elements = jQuery.prop( this, \"elements\" );\n\t\t\treturn elements ? jQuery.makeArray( elements ) : this;\n\t\t})\n\t\t.filter(function() {\n\t\t\tvar type = this.type;\n\n\t\t\t// Use .is( \":disabled\" ) so that fieldset[disabled] works\n\t\t\treturn this.name && !jQuery( this ).is( \":disabled\" ) &&\n\t\t\t\trsubmittable.test( this.nodeName ) && !rsubmitterTypes.test( type ) &&\n\t\t\t\t( this.checked || !rcheckableType.test( type ) );\n\t\t})\n\t\t.map(function( i, elem ) {\n\t\t\tvar val = jQuery( this ).val();\n\n\t\t\treturn val == null ?\n\t\t\t\tnull :\n\t\t\t\tjQuery.isArray( val ) ?\n\t\t\t\t\tjQuery.map( val, function( val ) {\n\t\t\t\t\t\treturn { name: elem.name, value: val.replace( rCRLF, \"\\r\\n\" ) };\n\t\t\t\t\t}) :\n\t\t\t\t\t{ name: elem.name, value: val.replace( rCRLF, \"\\r\\n\" ) };\n\t\t}).get();\n\t}\n});\n\n\njQuery.ajaxSettings.xhr = function() {\n\ttry {\n\t\treturn new XMLHttpRequest();\n\t} catch( e ) {}\n};\n\nvar xhrId = 0,\n\txhrCallbacks = {},\n\txhrSuccessStatus = {\n\t\t// file protocol always yields status code 0, assume 200\n\t\t0: 200,\n\t\t// Support: IE9\n\t\t// #1450: sometimes IE returns 1223 when it should be 204\n\t\t1223: 204\n\t},\n\txhrSupported = jQuery.ajaxSettings.xhr();\n\n// Support: IE9\n// Open requests must be manually aborted on unload (#5280)\nif ( window.ActiveXObject ) {\n\tjQuery( window ).on( \"unload\", function() {\n\t\tfor ( var key in xhrCallbacks ) {\n\t\t\txhrCallbacks[ key ]();\n\t\t}\n\t});\n}\n\nsupport.cors = !!xhrSupported && ( \"withCredentials\" in xhrSupported );\nsupport.ajax = xhrSupported = !!xhrSupported;\n\njQuery.ajaxTransport(function( options ) {\n\tvar callback;\n\n\t// Cross domain only allowed if supported through XMLHttpRequest\n\tif ( support.cors || xhrSupported && !options.crossDomain ) {\n\t\treturn {\n\t\t\tsend: function( headers, complete ) {\n\t\t\t\tvar i,\n\t\t\t\t\txhr = options.xhr(),\n\t\t\t\t\tid = ++xhrId;\n\n\t\t\t\txhr.open( options.type, options.url, options.async, options.username, options.password );\n\n\t\t\t\t// Apply custom fields if provided\n\t\t\t\tif ( options.xhrFields ) {\n\t\t\t\t\tfor ( i in options.xhrFields ) {\n\t\t\t\t\t\txhr[ i ] = options.xhrFields[ i ];\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Override mime type if needed\n\t\t\t\tif ( options.mimeType && xhr.overrideMimeType ) {\n\t\t\t\t\txhr.overrideMimeType( options.mimeType );\n\t\t\t\t}\n\n\t\t\t\t// X-Requested-With header\n\t\t\t\t// For cross-domain requests, seeing as conditions for a preflight are\n\t\t\t\t// akin to a jigsaw puzzle, we simply never set it to be sure.\n\t\t\t\t// (it can always be set on a per-request basis or even using ajaxSetup)\n\t\t\t\t// For same-domain requests, won't change header if already provided.\n\t\t\t\tif ( !options.crossDomain && !headers[\"X-Requested-With\"] ) {\n\t\t\t\t\theaders[\"X-Requested-With\"] = \"XMLHttpRequest\";\n\t\t\t\t}\n\n\t\t\t\t// Set headers\n\t\t\t\tfor ( i in headers ) {\n\t\t\t\t\txhr.setRequestHeader( i, headers[ i ] );\n\t\t\t\t}\n\n\t\t\t\t// Callback\n\t\t\t\tcallback = function( type ) {\n\t\t\t\t\treturn function() {\n\t\t\t\t\t\tif ( callback ) {\n\t\t\t\t\t\t\tdelete xhrCallbacks[ id ];\n\t\t\t\t\t\t\tcallback = xhr.onload = xhr.onerror = null;\n\n\t\t\t\t\t\t\tif ( type === \"abort\" ) {\n\t\t\t\t\t\t\t\txhr.abort();\n\t\t\t\t\t\t\t} else if ( type === \"error\" ) {\n\t\t\t\t\t\t\t\tcomplete(\n\t\t\t\t\t\t\t\t\t// file: protocol always yields status 0; see #8605, #14207\n\t\t\t\t\t\t\t\t\txhr.status,\n\t\t\t\t\t\t\t\t\txhr.statusText\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tcomplete(\n\t\t\t\t\t\t\t\t\txhrSuccessStatus[ xhr.status ] || xhr.status,\n\t\t\t\t\t\t\t\t\txhr.statusText,\n\t\t\t\t\t\t\t\t\t// Support: IE9\n\t\t\t\t\t\t\t\t\t// Accessing binary-data responseText throws an exception\n\t\t\t\t\t\t\t\t\t// (#11426)\n\t\t\t\t\t\t\t\t\ttypeof xhr.responseText === \"string\" ? {\n\t\t\t\t\t\t\t\t\t\ttext: xhr.responseText\n\t\t\t\t\t\t\t\t\t} : undefined,\n\t\t\t\t\t\t\t\t\txhr.getAllResponseHeaders()\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t};\n\t\t\t\t};\n\n\t\t\t\t// Listen to events\n\t\t\t\txhr.onload = callback();\n\t\t\t\txhr.onerror = callback(\"error\");\n\n\t\t\t\t// Create the abort callback\n\t\t\t\tcallback = xhrCallbacks[ id ] = callback(\"abort\");\n\n\t\t\t\ttry {\n\t\t\t\t\t// Do send the request (this may raise an exception)\n\t\t\t\t\txhr.send( options.hasContent && options.data || null );\n\t\t\t\t} catch ( e ) {\n\t\t\t\t\t// #14683: Only rethrow if this hasn't been notified as an error yet\n\t\t\t\t\tif ( callback ) {\n\t\t\t\t\t\tthrow e;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t},\n\n\t\t\tabort: function() {\n\t\t\t\tif ( callback ) {\n\t\t\t\t\tcallback();\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t}\n});\n\n\n\n\n// Install script dataType\njQuery.ajaxSetup({\n\taccepts: {\n\t\tscript: \"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript\"\n\t},\n\tcontents: {\n\t\tscript: /(?:java|ecma)script/\n\t},\n\tconverters: {\n\t\t\"text script\": function( text ) {\n\t\t\tjQuery.globalEval( text );\n\t\t\treturn text;\n\t\t}\n\t}\n});\n\n// Handle cache's special case and crossDomain\njQuery.ajaxPrefilter( \"script\", function( s ) {\n\tif ( s.cache === undefined ) {\n\t\ts.cache = false;\n\t}\n\tif ( s.crossDomain ) {\n\t\ts.type = \"GET\";\n\t}\n});\n\n// Bind script tag hack transport\njQuery.ajaxTransport( \"script\", function( s ) {\n\t// This transport only deals with cross domain requests\n\tif ( s.crossDomain ) {\n\t\tvar script, callback;\n\t\treturn {\n\t\t\tsend: function( _, complete ) {\n\t\t\t\tscript = jQuery(\"<script>\").prop({\n\t\t\t\t\tasync: true,\n\t\t\t\t\tcharset: s.scriptCharset,\n\t\t\t\t\tsrc: s.url\n\t\t\t\t}).on(\n\t\t\t\t\t\"load error\",\n\t\t\t\t\tcallback = function( evt ) {\n\t\t\t\t\t\tscript.remove();\n\t\t\t\t\t\tcallback = null;\n\t\t\t\t\t\tif ( evt ) {\n\t\t\t\t\t\t\tcomplete( evt.type === \"error\" ? 404 : 200, evt.type );\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t);\n\t\t\t\tdocument.head.appendChild( script[ 0 ] );\n\t\t\t},\n\t\t\tabort: function() {\n\t\t\t\tif ( callback ) {\n\t\t\t\t\tcallback();\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t}\n});\n\n\n\n\nvar oldCallbacks = [],\n\trjsonp = /(=)\\?(?=&|$)|\\?\\?/;\n\n// Default jsonp settings\njQuery.ajaxSetup({\n\tjsonp: \"callback\",\n\tjsonpCallback: function() {\n\t\tvar callback = oldCallbacks.pop() || ( jQuery.expando + \"_\" + ( nonce++ ) );\n\t\tthis[ callback ] = true;\n\t\treturn callback;\n\t}\n});\n\n// Detect, normalize options and install callbacks for jsonp requests\njQuery.ajaxPrefilter( \"json jsonp\", function( s, originalSettings, jqXHR ) {\n\n\tvar callbackName, overwritten, responseContainer,\n\t\tjsonProp = s.jsonp !== false && ( rjsonp.test( s.url ) ?\n\t\t\t\"url\" :\n\t\t\ttypeof s.data === \"string\" && !( s.contentType || \"\" ).indexOf(\"application/x-www-form-urlencoded\") && rjsonp.test( s.data ) && \"data\"\n\t\t);\n\n\t// Handle iff the expected data type is \"jsonp\" or we have a parameter to set\n\tif ( jsonProp || s.dataTypes[ 0 ] === \"jsonp\" ) {\n\n\t\t// Get callback name, remembering preexisting value associated with it\n\t\tcallbackName = s.jsonpCallback = jQuery.isFunction( s.jsonpCallback ) ?\n\t\t\ts.jsonpCallback() :\n\t\t\ts.jsonpCallback;\n\n\t\t// Insert callback into url or form data\n\t\tif ( jsonProp ) {\n\t\t\ts[ jsonProp ] = s[ jsonProp ].replace( rjsonp, \"$1\" + callbackName );\n\t\t} else if ( s.jsonp !== false ) {\n\t\t\ts.url += ( rquery.test( s.url ) ? \"&\" : \"?\" ) + s.jsonp + \"=\" + callbackName;\n\t\t}\n\n\t\t// Use data converter to retrieve json after script execution\n\t\ts.converters[\"script json\"] = function() {\n\t\t\tif ( !responseContainer ) {\n\t\t\t\tjQuery.error( callbackName + \" was not called\" );\n\t\t\t}\n\t\t\treturn responseContainer[ 0 ];\n\t\t};\n\n\t\t// force json dataType\n\t\ts.dataTypes[ 0 ] = \"json\";\n\n\t\t// Install callback\n\t\toverwritten = window[ callbackName ];\n\t\twindow[ callbackName ] = function() {\n\t\t\tresponseContainer = arguments;\n\t\t};\n\n\t\t// Clean-up function (fires after converters)\n\t\tjqXHR.always(function() {\n\t\t\t// Restore preexisting value\n\t\t\twindow[ callbackName ] = overwritten;\n\n\t\t\t// Save back as free\n\t\t\tif ( s[ callbackName ] ) {\n\t\t\t\t// make sure that re-using the options doesn't screw things around\n\t\t\t\ts.jsonpCallback = originalSettings.jsonpCallback;\n\n\t\t\t\t// save the callback name for future use\n\t\t\t\toldCallbacks.push( callbackName );\n\t\t\t}\n\n\t\t\t// Call if it was a function and we have a response\n\t\t\tif ( responseContainer && jQuery.isFunction( overwritten ) ) {\n\t\t\t\toverwritten( responseContainer[ 0 ] );\n\t\t\t}\n\n\t\t\tresponseContainer = overwritten = undefined;\n\t\t});\n\n\t\t// Delegate to script\n\t\treturn \"script\";\n\t}\n});\n\n\n\n\n// data: string of html\n// context (optional): If specified, the fragment will be created in this context, defaults to document\n// keepScripts (optional): If true, will include scripts passed in the html string\njQuery.parseHTML = function( data, context, keepScripts ) {\n\tif ( !data || typeof data !== \"string\" ) {\n\t\treturn null;\n\t}\n\tif ( typeof context === \"boolean\" ) {\n\t\tkeepScripts = context;\n\t\tcontext = false;\n\t}\n\tcontext = context || document;\n\n\tvar parsed = rsingleTag.exec( data ),\n\t\tscripts = !keepScripts && [];\n\n\t// Single tag\n\tif ( parsed ) {\n\t\treturn [ context.createElement( parsed[1] ) ];\n\t}\n\n\tparsed = jQuery.buildFragment( [ data ], context, scripts );\n\n\tif ( scripts && scripts.length ) {\n\t\tjQuery( scripts ).remove();\n\t}\n\n\treturn jQuery.merge( [], parsed.childNodes );\n};\n\n\n// Keep a copy of the old load method\nvar _load = jQuery.fn.load;\n\n/**\n * Load a url into a page\n */\njQuery.fn.load = function( url, params, callback ) {\n\tif ( typeof url !== \"string\" && _load ) {\n\t\treturn _load.apply( this, arguments );\n\t}\n\n\tvar selector, type, response,\n\t\tself = this,\n\t\toff = url.indexOf(\" \");\n\n\tif ( off >= 0 ) {\n\t\tselector = jQuery.trim( url.slice( off ) );\n\t\turl = url.slice( 0, off );\n\t}\n\n\t// If it's a function\n\tif ( jQuery.isFunction( params ) ) {\n\n\t\t// We assume that it's the callback\n\t\tcallback = params;\n\t\tparams = undefined;\n\n\t// Otherwise, build a param string\n\t} else if ( params && typeof params === \"object\" ) {\n\t\ttype = \"POST\";\n\t}\n\n\t// If we have elements to modify, make the request\n\tif ( self.length > 0 ) {\n\t\tjQuery.ajax({\n\t\t\turl: url,\n\n\t\t\t// if \"type\" variable is undefined, then \"GET\" method will be used\n\t\t\ttype: type,\n\t\t\tdataType: \"html\",\n\t\t\tdata: params\n\t\t}).done(function( responseText ) {\n\n\t\t\t// Save response for use in complete callback\n\t\t\tresponse = arguments;\n\n\t\t\tself.html( selector ?\n\n\t\t\t\t// If a selector was specified, locate the right elements in a dummy div\n\t\t\t\t// Exclude scripts to avoid IE 'Permission Denied' errors\n\t\t\t\tjQuery(\"<div>\").append( jQuery.parseHTML( responseText ) ).find( selector ) :\n\n\t\t\t\t// Otherwise use the full result\n\t\t\t\tresponseText );\n\n\t\t}).complete( callback && function( jqXHR, status ) {\n\t\t\tself.each( callback, response || [ jqXHR.responseText, status, jqXHR ] );\n\t\t});\n\t}\n\n\treturn this;\n};\n\n\n\n\njQuery.expr.filters.animated = function( elem ) {\n\treturn jQuery.grep(jQuery.timers, function( fn ) {\n\t\treturn elem === fn.elem;\n\t}).length;\n};\n\n\n\n\nvar docElem = window.document.documentElement;\n\n/**\n * Gets a window from an element\n */\nfunction getWindow( elem ) {\n\treturn jQuery.isWindow( elem ) ? elem : elem.nodeType === 9 && elem.defaultView;\n}\n\njQuery.offset = {\n\tsetOffset: function( elem, options, i ) {\n\t\tvar curPosition, curLeft, curCSSTop, curTop, curOffset, curCSSLeft, calculatePosition,\n\t\t\tposition = jQuery.css( elem, \"position\" ),\n\t\t\tcurElem = jQuery( elem ),\n\t\t\tprops = {};\n\n\t\t// Set position first, in-case top/left are set even on static elem\n\t\tif ( position === \"static\" ) {\n\t\t\telem.style.position = \"relative\";\n\t\t}\n\n\t\tcurOffset = curElem.offset();\n\t\tcurCSSTop = jQuery.css( elem, \"top\" );\n\t\tcurCSSLeft = jQuery.css( elem, \"left\" );\n\t\tcalculatePosition = ( position === \"absolute\" || position === \"fixed\" ) &&\n\t\t\t( curCSSTop + curCSSLeft ).indexOf(\"auto\") > -1;\n\n\t\t// Need to be able to calculate position if either top or left is auto and position is either absolute or fixed\n\t\tif ( calculatePosition ) {\n\t\t\tcurPosition = curElem.position();\n\t\t\tcurTop = curPosition.top;\n\t\t\tcurLeft = curPosition.left;\n\n\t\t} else {\n\t\t\tcurTop = parseFloat( curCSSTop ) || 0;\n\t\t\tcurLeft = parseFloat( curCSSLeft ) || 0;\n\t\t}\n\n\t\tif ( jQuery.isFunction( options ) ) {\n\t\t\toptions = options.call( elem, i, curOffset );\n\t\t}\n\n\t\tif ( options.top != null ) {\n\t\t\tprops.top = ( options.top - curOffset.top ) + curTop;\n\t\t}\n\t\tif ( options.left != null ) {\n\t\t\tprops.left = ( options.left - curOffset.left ) + curLeft;\n\t\t}\n\n\t\tif ( \"using\" in options ) {\n\t\t\toptions.using.call( elem, props );\n\n\t\t} else {\n\t\t\tcurElem.css( props );\n\t\t}\n\t}\n};\n\njQuery.fn.extend({\n\toffset: function( options ) {\n\t\tif ( arguments.length ) {\n\t\t\treturn options === undefined ?\n\t\t\t\tthis :\n\t\t\t\tthis.each(function( i ) {\n\t\t\t\t\tjQuery.offset.setOffset( this, options, i );\n\t\t\t\t});\n\t\t}\n\n\t\tvar docElem, win,\n\t\t\telem = this[ 0 ],\n\t\t\tbox = { top: 0, left: 0 },\n\t\t\tdoc = elem && elem.ownerDocument;\n\n\t\tif ( !doc ) {\n\t\t\treturn;\n\t\t}\n\n\t\tdocElem = doc.documentElement;\n\n\t\t// Make sure it's not a disconnected DOM node\n\t\tif ( !jQuery.contains( docElem, elem ) ) {\n\t\t\treturn box;\n\t\t}\n\n\t\t// If we don't have gBCR, just use 0,0 rather than error\n\t\t// BlackBerry 5, iOS 3 (original iPhone)\n\t\tif ( typeof elem.getBoundingClientRect !== strundefined ) {\n\t\t\tbox = elem.getBoundingClientRect();\n\t\t}\n\t\twin = getWindow( doc );\n\t\treturn {\n\t\t\ttop: box.top + win.pageYOffset - docElem.clientTop,\n\t\t\tleft: box.left + win.pageXOffset - docElem.clientLeft\n\t\t};\n\t},\n\n\tposition: function() {\n\t\tif ( !this[ 0 ] ) {\n\t\t\treturn;\n\t\t}\n\n\t\tvar offsetParent, offset,\n\t\t\telem = this[ 0 ],\n\t\t\tparentOffset = { top: 0, left: 0 };\n\n\t\t// Fixed elements are offset from window (parentOffset = {top:0, left: 0}, because it is its only offset parent\n\t\tif ( jQuery.css( elem, \"position\" ) === \"fixed\" ) {\n\t\t\t// We assume that getBoundingClientRect is available when computed position is fixed\n\t\t\toffset = elem.getBoundingClientRect();\n\n\t\t} else {\n\t\t\t// Get *real* offsetParent\n\t\t\toffsetParent = this.offsetParent();\n\n\t\t\t// Get correct offsets\n\t\t\toffset = this.offset();\n\t\t\tif ( !jQuery.nodeName( offsetParent[ 0 ], \"html\" ) ) {\n\t\t\t\tparentOffset = offsetParent.offset();\n\t\t\t}\n\n\t\t\t// Add offsetParent borders\n\t\t\tparentOffset.top += jQuery.css( offsetParent[ 0 ], \"borderTopWidth\", true );\n\t\t\tparentOffset.left += jQuery.css( offsetParent[ 0 ], \"borderLeftWidth\", true );\n\t\t}\n\n\t\t// Subtract parent offsets and element margins\n\t\treturn {\n\t\t\ttop: offset.top - parentOffset.top - jQuery.css( elem, \"marginTop\", true ),\n\t\t\tleft: offset.left - parentOffset.left - jQuery.css( elem, \"marginLeft\", true )\n\t\t};\n\t},\n\n\toffsetParent: function() {\n\t\treturn this.map(function() {\n\t\t\tvar offsetParent = this.offsetParent || docElem;\n\n\t\t\twhile ( offsetParent && ( !jQuery.nodeName( offsetParent, \"html\" ) && jQuery.css( offsetParent, \"position\" ) === \"static\" ) ) {\n\t\t\t\toffsetParent = offsetParent.offsetParent;\n\t\t\t}\n\n\t\t\treturn offsetParent || docElem;\n\t\t});\n\t}\n});\n\n// Create scrollLeft and scrollTop methods\njQuery.each( { scrollLeft: \"pageXOffset\", scrollTop: \"pageYOffset\" }, function( method, prop ) {\n\tvar top = \"pageYOffset\" === prop;\n\n\tjQuery.fn[ method ] = function( val ) {\n\t\treturn access( this, function( elem, method, val ) {\n\t\t\tvar win = getWindow( elem );\n\n\t\t\tif ( val === undefined ) {\n\t\t\t\treturn win ? win[ prop ] : elem[ method ];\n\t\t\t}\n\n\t\t\tif ( win ) {\n\t\t\t\twin.scrollTo(\n\t\t\t\t\t!top ? val : window.pageXOffset,\n\t\t\t\t\ttop ? val : window.pageYOffset\n\t\t\t\t);\n\n\t\t\t} else {\n\t\t\t\telem[ method ] = val;\n\t\t\t}\n\t\t}, method, val, arguments.length, null );\n\t};\n});\n\n// Add the top/left cssHooks using jQuery.fn.position\n// Webkit bug: https://bugs.webkit.org/show_bug.cgi?id=29084\n// getComputedStyle returns percent when specified for top/left/bottom/right\n// rather than make the css module depend on the offset module, we just check for it here\njQuery.each( [ \"top\", \"left\" ], function( i, prop ) {\n\tjQuery.cssHooks[ prop ] = addGetHookIf( support.pixelPosition,\n\t\tfunction( elem, computed ) {\n\t\t\tif ( computed ) {\n\t\t\t\tcomputed = curCSS( elem, prop );\n\t\t\t\t// if curCSS returns percentage, fallback to offset\n\t\t\t\treturn rnumnonpx.test( computed ) ?\n\t\t\t\t\tjQuery( elem ).position()[ prop ] + \"px\" :\n\t\t\t\t\tcomputed;\n\t\t\t}\n\t\t}\n\t);\n});\n\n\n// Create innerHeight, innerWidth, height, width, outerHeight and outerWidth methods\njQuery.each( { Height: \"height\", Width: \"width\" }, function( name, type ) {\n\tjQuery.each( { padding: \"inner\" + name, content: type, \"\": \"outer\" + name }, function( defaultExtra, funcName ) {\n\t\t// margin is only for outerHeight, outerWidth\n\t\tjQuery.fn[ funcName ] = function( margin, value ) {\n\t\t\tvar chainable = arguments.length && ( defaultExtra || typeof margin !== \"boolean\" ),\n\t\t\t\textra = defaultExtra || ( margin === true || value === true ? \"margin\" : \"border\" );\n\n\t\t\treturn access( this, function( elem, type, value ) {\n\t\t\t\tvar doc;\n\n\t\t\t\tif ( jQuery.isWindow( elem ) ) {\n\t\t\t\t\t// As of 5/8/2012 this will yield incorrect results for Mobile Safari, but there\n\t\t\t\t\t// isn't a whole lot we can do. See pull request at this URL for discussion:\n\t\t\t\t\t// https://github.com/jquery/jquery/pull/764\n\t\t\t\t\treturn elem.document.documentElement[ \"client\" + name ];\n\t\t\t\t}\n\n\t\t\t\t// Get document width or height\n\t\t\t\tif ( elem.nodeType === 9 ) {\n\t\t\t\t\tdoc = elem.documentElement;\n\n\t\t\t\t\t// Either scroll[Width/Height] or offset[Width/Height] or client[Width/Height],\n\t\t\t\t\t// whichever is greatest\n\t\t\t\t\treturn Math.max(\n\t\t\t\t\t\telem.body[ \"scroll\" + name ], doc[ \"scroll\" + name ],\n\t\t\t\t\t\telem.body[ \"offset\" + name ], doc[ \"offset\" + name ],\n\t\t\t\t\t\tdoc[ \"client\" + name ]\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\treturn value === undefined ?\n\t\t\t\t\t// Get width or height on the element, requesting but not forcing parseFloat\n\t\t\t\t\tjQuery.css( elem, type, extra ) :\n\n\t\t\t\t\t// Set width or height on the element\n\t\t\t\t\tjQuery.style( elem, type, value, extra );\n\t\t\t}, type, chainable ? margin : undefined, chainable, null );\n\t\t};\n\t});\n});\n\n\n// The number of elements contained in the matched element set\njQuery.fn.size = function() {\n\treturn this.length;\n};\n\njQuery.fn.andSelf = jQuery.fn.addBack;\n\n\n\n\n// Register as a named AMD module, since jQuery can be concatenated with other\n// files that may use define, but not via a proper concatenation script that\n// understands anonymous AMD modules. A named AMD is safest and most robust\n// way to register. Lowercase jquery is used because AMD module names are\n// derived from file names, and jQuery is normally delivered in a lowercase\n// file name. Do this after creating the global so that if an AMD module wants\n// to call noConflict to hide this version of jQuery, it will work.\n\n// Note that for maximum portability, libraries that are not jQuery should\n// declare themselves as anonymous modules, and avoid setting a global if an\n// AMD loader is present. jQuery is a special case. For more information, see\n// https://github.com/jrburke/requirejs/wiki/Updating-existing-libraries#wiki-anon\n\nif ( typeof define === \"function\" && define.amd ) {\n\tdefine( \"jquery\", [], function() {\n\t\treturn jQuery;\n\t});\n}\n\n\n\n\nvar\n\t// Map over jQuery in case of overwrite\n\t_jQuery = window.jQuery,\n\n\t// Map over the $ in case of overwrite\n\t_$ = window.$;\n\njQuery.noConflict = function( deep ) {\n\tif ( window.$ === jQuery ) {\n\t\twindow.$ = _$;\n\t}\n\n\tif ( deep && window.jQuery === jQuery ) {\n\t\twindow.jQuery = _jQuery;\n\t}\n\n\treturn jQuery;\n};\n\n// Expose jQuery and $ identifiers, even in\n// AMD (#7102#comment:10, https://github.com/jquery/jquery/pull/557)\n// and CommonJS for browser emulators (#13566)\nif ( typeof noGlobal === strundefined ) {\n\twindow.jQuery = window.$ = jQuery;\n}\n\n\n\n\nreturn jQuery;\n\n}));\n"
  },
  {
    "path": "r2/r2/public/static/js/lib/jquery.cookie.js",
    "content": "/**\n * jQuery Cookie plugin\n *\n * Copyright (c) 2010 Klaus Hartl (stilbuero.de)\n * Dual licensed under the MIT and GPL licenses:\n * http://www.opensource.org/licenses/mit-license.php\n * http://www.gnu.org/licenses/gpl.html\n *\n */\njQuery.cookie = function (key, value, options) {\n\n    // key and at least value given, set cookie...\n    if (arguments.length > 1 && String(value) !== \"[object Object]\") {\n        options = jQuery.extend({}, options);\n\n        if (value === null || value === undefined) {\n            options.expires = -1;\n        }\n\n        if (typeof options.expires === 'number') {\n            var days = options.expires, t = options.expires = new Date();\n            t.setDate(t.getDate() + days);\n        }\n\n        value = String(value);\n\n        return (document.cookie = [\n            encodeURIComponent(key), '=',\n            options.raw ? value : encodeURIComponent(value),\n            options.expires ? '; expires=' + options.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE\n            options.path ? '; path=' + options.path : '',\n            options.domain ? '; domain=' + options.domain : '',\n            options.secure ? '; secure' : ''\n        ].join(''));\n    }\n\n    // key and possibly options given, get cookie...\n    options = value || {};\n    var result, decode = options.raw ? function (s) { return s; } : decodeURIComponent;\n    return (result = new RegExp('(?:^|; )' + encodeURIComponent(key) + '=([^;]*)').exec(document.cookie)) ? decode(result[1]) : null;\n};\n"
  },
  {
    "path": "r2/r2/public/static/js/lib/jquery.flot.js",
    "content": "/* Javascript plotting library for jQuery, version 0.8.3.\n\nCopyright (c) 2007-2014 IOLA and Ole Laursen.\nLicensed under the MIT license.\n\n*/\n\n// first an inline dependency, jquery.colorhelpers.js, we inline it here\n// for convenience\n\n/* Plugin for jQuery for working with colors.\n *\n * Version 1.1.\n *\n * Inspiration from jQuery color animation plugin by John Resig.\n *\n * Released under the MIT license by Ole Laursen, October 2009.\n *\n * Examples:\n *\n *   $.color.parse(\"#fff\").scale('rgb', 0.25).add('a', -0.5).toString()\n *   var c = $.color.extract($(\"#mydiv\"), 'background-color');\n *   console.log(c.r, c.g, c.b, c.a);\n *   $.color.make(100, 50, 25, 0.4).toString() // returns \"rgba(100,50,25,0.4)\"\n *\n * Note that .scale() and .add() return the same modified object\n * instead of making a new one.\n *\n * V. 1.1: Fix error handling so e.g. parsing an empty string does\n * produce a color rather than just crashing.\n */\n(function($){$.color={};$.color.make=function(r,g,b,a){var o={};o.r=r||0;o.g=g||0;o.b=b||0;o.a=a!=null?a:1;o.add=function(c,d){for(var i=0;i<c.length;++i)o[c.charAt(i)]+=d;return o.normalize()};o.scale=function(c,f){for(var i=0;i<c.length;++i)o[c.charAt(i)]*=f;return o.normalize()};o.toString=function(){if(o.a>=1){return\"rgb(\"+[o.r,o.g,o.b].join(\",\")+\")\"}else{return\"rgba(\"+[o.r,o.g,o.b,o.a].join(\",\")+\")\"}};o.normalize=function(){function clamp(min,value,max){return value<min?min:value>max?max:value}o.r=clamp(0,parseInt(o.r),255);o.g=clamp(0,parseInt(o.g),255);o.b=clamp(0,parseInt(o.b),255);o.a=clamp(0,o.a,1);return o};o.clone=function(){return $.color.make(o.r,o.b,o.g,o.a)};return o.normalize()};$.color.extract=function(elem,css){var c;do{c=elem.css(css).toLowerCase();if(c!=\"\"&&c!=\"transparent\")break;elem=elem.parent()}while(elem.length&&!$.nodeName(elem.get(0),\"body\"));if(c==\"rgba(0, 0, 0, 0)\")c=\"transparent\";return $.color.parse(c)};$.color.parse=function(str){var res,m=$.color.make;if(res=/rgb\\(\\s*([0-9]{1,3})\\s*,\\s*([0-9]{1,3})\\s*,\\s*([0-9]{1,3})\\s*\\)/.exec(str))return m(parseInt(res[1],10),parseInt(res[2],10),parseInt(res[3],10));if(res=/rgba\\(\\s*([0-9]{1,3})\\s*,\\s*([0-9]{1,3})\\s*,\\s*([0-9]{1,3})\\s*,\\s*([0-9]+(?:\\.[0-9]+)?)\\s*\\)/.exec(str))return m(parseInt(res[1],10),parseInt(res[2],10),parseInt(res[3],10),parseFloat(res[4]));if(res=/rgb\\(\\s*([0-9]+(?:\\.[0-9]+)?)\\%\\s*,\\s*([0-9]+(?:\\.[0-9]+)?)\\%\\s*,\\s*([0-9]+(?:\\.[0-9]+)?)\\%\\s*\\)/.exec(str))return m(parseFloat(res[1])*2.55,parseFloat(res[2])*2.55,parseFloat(res[3])*2.55);if(res=/rgba\\(\\s*([0-9]+(?:\\.[0-9]+)?)\\%\\s*,\\s*([0-9]+(?:\\.[0-9]+)?)\\%\\s*,\\s*([0-9]+(?:\\.[0-9]+)?)\\%\\s*,\\s*([0-9]+(?:\\.[0-9]+)?)\\s*\\)/.exec(str))return m(parseFloat(res[1])*2.55,parseFloat(res[2])*2.55,parseFloat(res[3])*2.55,parseFloat(res[4]));if(res=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(str))return m(parseInt(res[1],16),parseInt(res[2],16),parseInt(res[3],16));if(res=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(str))return m(parseInt(res[1]+res[1],16),parseInt(res[2]+res[2],16),parseInt(res[3]+res[3],16));var name=$.trim(str).toLowerCase();if(name==\"transparent\")return m(255,255,255,0);else{res=lookupColors[name]||[0,0,0];return m(res[0],res[1],res[2])}};var lookupColors={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0]}})(jQuery);\n\n// the actual Flot code\n(function($) {\n\n\t// Cache the prototype hasOwnProperty for faster access\n\n\tvar hasOwnProperty = Object.prototype.hasOwnProperty;\n\n    // A shim to provide 'detach' to jQuery versions prior to 1.4.  Using a DOM\n    // operation produces the same effect as detach, i.e. removing the element\n    // without touching its jQuery data.\n\n    // Do not merge this into Flot 0.9, since it requires jQuery 1.4.4+.\n\n    if (!$.fn.detach) {\n        $.fn.detach = function() {\n            return this.each(function() {\n                if (this.parentNode) {\n                    this.parentNode.removeChild( this );\n                }\n            });\n        };\n    }\n\n\t///////////////////////////////////////////////////////////////////////////\n\t// The Canvas object is a wrapper around an HTML5 <canvas> tag.\n\t//\n\t// @constructor\n\t// @param {string} cls List of classes to apply to the canvas.\n\t// @param {element} container Element onto which to append the canvas.\n\t//\n\t// Requiring a container is a little iffy, but unfortunately canvas\n\t// operations don't work unless the canvas is attached to the DOM.\n\n\tfunction Canvas(cls, container) {\n\n\t\tvar element = container.children(\".\" + cls)[0];\n\n\t\tif (element == null) {\n\n\t\t\telement = document.createElement(\"canvas\");\n\t\t\telement.className = cls;\n\n\t\t\t$(element).css({ direction: \"ltr\", position: \"absolute\", left: 0, top: 0 })\n\t\t\t\t.appendTo(container);\n\n\t\t\t// If HTML5 Canvas isn't available, fall back to [Ex|Flash]canvas\n\n\t\t\tif (!element.getContext) {\n\t\t\t\tif (window.G_vmlCanvasManager) {\n\t\t\t\t\telement = window.G_vmlCanvasManager.initElement(element);\n\t\t\t\t} else {\n\t\t\t\t\tthrow new Error(\"Canvas is not available. If you're using IE with a fall-back such as Excanvas, then there's either a mistake in your conditional include, or the page has no DOCTYPE and is rendering in Quirks Mode.\");\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tthis.element = element;\n\n\t\tvar context = this.context = element.getContext(\"2d\");\n\n\t\t// Determine the screen's ratio of physical to device-independent\n\t\t// pixels.  This is the ratio between the canvas width that the browser\n\t\t// advertises and the number of pixels actually present in that space.\n\n\t\t// The iPhone 4, for example, has a device-independent width of 320px,\n\t\t// but its screen is actually 640px wide.  It therefore has a pixel\n\t\t// ratio of 2, while most normal devices have a ratio of 1.\n\n\t\tvar devicePixelRatio = window.devicePixelRatio || 1,\n\t\t\tbackingStoreRatio =\n\t\t\t\tcontext.webkitBackingStorePixelRatio ||\n\t\t\t\tcontext.mozBackingStorePixelRatio ||\n\t\t\t\tcontext.msBackingStorePixelRatio ||\n\t\t\t\tcontext.oBackingStorePixelRatio ||\n\t\t\t\tcontext.backingStorePixelRatio || 1;\n\n\t\tthis.pixelRatio = devicePixelRatio / backingStoreRatio;\n\n\t\t// Size the canvas to match the internal dimensions of its container\n\n\t\tthis.resize(container.width(), container.height());\n\n\t\t// Collection of HTML div layers for text overlaid onto the canvas\n\n\t\tthis.textContainer = null;\n\t\tthis.text = {};\n\n\t\t// Cache of text fragments and metrics, so we can avoid expensively\n\t\t// re-calculating them when the plot is re-rendered in a loop.\n\n\t\tthis._textCache = {};\n\t}\n\n\t// Resizes the canvas to the given dimensions.\n\t//\n\t// @param {number} width New width of the canvas, in pixels.\n\t// @param {number} width New height of the canvas, in pixels.\n\n\tCanvas.prototype.resize = function(width, height) {\n\n\t\tif (width <= 0 || height <= 0) {\n\t\t\tthrow new Error(\"Invalid dimensions for plot, width = \" + width + \", height = \" + height);\n\t\t}\n\n\t\tvar element = this.element,\n\t\t\tcontext = this.context,\n\t\t\tpixelRatio = this.pixelRatio;\n\n\t\t// Resize the canvas, increasing its density based on the display's\n\t\t// pixel ratio; basically giving it more pixels without increasing the\n\t\t// size of its element, to take advantage of the fact that retina\n\t\t// displays have that many more pixels in the same advertised space.\n\n\t\t// Resizing should reset the state (excanvas seems to be buggy though)\n\n\t\tif (this.width != width) {\n\t\t\telement.width = width * pixelRatio;\n\t\t\telement.style.width = width + \"px\";\n\t\t\tthis.width = width;\n\t\t}\n\n\t\tif (this.height != height) {\n\t\t\telement.height = height * pixelRatio;\n\t\t\telement.style.height = height + \"px\";\n\t\t\tthis.height = height;\n\t\t}\n\n\t\t// Save the context, so we can reset in case we get replotted.  The\n\t\t// restore ensure that we're really back at the initial state, and\n\t\t// should be safe even if we haven't saved the initial state yet.\n\n\t\tcontext.restore();\n\t\tcontext.save();\n\n\t\t// Scale the coordinate space to match the display density; so even though we\n\t\t// may have twice as many pixels, we still want lines and other drawing to\n\t\t// appear at the same size; the extra pixels will just make them crisper.\n\n\t\tcontext.scale(pixelRatio, pixelRatio);\n\t};\n\n\t// Clears the entire canvas area, not including any overlaid HTML text\n\n\tCanvas.prototype.clear = function() {\n\t\tthis.context.clearRect(0, 0, this.width, this.height);\n\t};\n\n\t// Finishes rendering the canvas, including managing the text overlay.\n\n\tCanvas.prototype.render = function() {\n\n\t\tvar cache = this._textCache;\n\n\t\t// For each text layer, add elements marked as active that haven't\n\t\t// already been rendered, and remove those that are no longer active.\n\n\t\tfor (var layerKey in cache) {\n\t\t\tif (hasOwnProperty.call(cache, layerKey)) {\n\n\t\t\t\tvar layer = this.getTextLayer(layerKey),\n\t\t\t\t\tlayerCache = cache[layerKey];\n\n\t\t\t\tlayer.hide();\n\n\t\t\t\tfor (var styleKey in layerCache) {\n\t\t\t\t\tif (hasOwnProperty.call(layerCache, styleKey)) {\n\t\t\t\t\t\tvar styleCache = layerCache[styleKey];\n\t\t\t\t\t\tfor (var key in styleCache) {\n\t\t\t\t\t\t\tif (hasOwnProperty.call(styleCache, key)) {\n\n\t\t\t\t\t\t\t\tvar positions = styleCache[key].positions;\n\n\t\t\t\t\t\t\t\tfor (var i = 0, position; position = positions[i]; i++) {\n\t\t\t\t\t\t\t\t\tif (position.active) {\n\t\t\t\t\t\t\t\t\t\tif (!position.rendered) {\n\t\t\t\t\t\t\t\t\t\t\tlayer.append(position.element);\n\t\t\t\t\t\t\t\t\t\t\tposition.rendered = true;\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\tpositions.splice(i--, 1);\n\t\t\t\t\t\t\t\t\t\tif (position.rendered) {\n\t\t\t\t\t\t\t\t\t\t\tposition.element.detach();\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\tif (positions.length == 0) {\n\t\t\t\t\t\t\t\t\tdelete styleCache[key];\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tlayer.show();\n\t\t\t}\n\t\t}\n\t};\n\n\t// Creates (if necessary) and returns the text overlay container.\n\t//\n\t// @param {string} classes String of space-separated CSS classes used to\n\t//     uniquely identify the text layer.\n\t// @return {object} The jQuery-wrapped text-layer div.\n\n\tCanvas.prototype.getTextLayer = function(classes) {\n\n\t\tvar layer = this.text[classes];\n\n\t\t// Create the text layer if it doesn't exist\n\n\t\tif (layer == null) {\n\n\t\t\t// Create the text layer container, if it doesn't exist\n\n\t\t\tif (this.textContainer == null) {\n\t\t\t\tthis.textContainer = $(\"<div class='flot-text'></div>\")\n\t\t\t\t\t.css({\n\t\t\t\t\t\tposition: \"absolute\",\n\t\t\t\t\t\ttop: 0,\n\t\t\t\t\t\tleft: 0,\n\t\t\t\t\t\tbottom: 0,\n\t\t\t\t\t\tright: 0,\n\t\t\t\t\t\t'font-size': \"smaller\",\n\t\t\t\t\t\tcolor: \"#545454\"\n\t\t\t\t\t})\n\t\t\t\t\t.insertAfter(this.element);\n\t\t\t}\n\n\t\t\tlayer = this.text[classes] = $(\"<div></div>\")\n\t\t\t\t.addClass(classes)\n\t\t\t\t.css({\n\t\t\t\t\tposition: \"absolute\",\n\t\t\t\t\ttop: 0,\n\t\t\t\t\tleft: 0,\n\t\t\t\t\tbottom: 0,\n\t\t\t\t\tright: 0\n\t\t\t\t})\n\t\t\t\t.appendTo(this.textContainer);\n\t\t}\n\n\t\treturn layer;\n\t};\n\n\t// Creates (if necessary) and returns a text info object.\n\t//\n\t// The object looks like this:\n\t//\n\t// {\n\t//     width: Width of the text's wrapper div.\n\t//     height: Height of the text's wrapper div.\n\t//     element: The jQuery-wrapped HTML div containing the text.\n\t//     positions: Array of positions at which this text is drawn.\n\t// }\n\t//\n\t// The positions array contains objects that look like this:\n\t//\n\t// {\n\t//     active: Flag indicating whether the text should be visible.\n\t//     rendered: Flag indicating whether the text is currently visible.\n\t//     element: The jQuery-wrapped HTML div containing the text.\n\t//     x: X coordinate at which to draw the text.\n\t//     y: Y coordinate at which to draw the text.\n\t// }\n\t//\n\t// Each position after the first receives a clone of the original element.\n\t//\n\t// The idea is that that the width, height, and general 'identity' of the\n\t// text is constant no matter where it is placed; the placements are a\n\t// secondary property.\n\t//\n\t// Canvas maintains a cache of recently-used text info objects; getTextInfo\n\t// either returns the cached element or creates a new entry.\n\t//\n\t// @param {string} layer A string of space-separated CSS classes uniquely\n\t//     identifying the layer containing this text.\n\t// @param {string} text Text string to retrieve info for.\n\t// @param {(string|object)=} font Either a string of space-separated CSS\n\t//     classes or a font-spec object, defining the text's font and style.\n\t// @param {number=} angle Angle at which to rotate the text, in degrees.\n\t//     Angle is currently unused, it will be implemented in the future.\n\t// @param {number=} width Maximum width of the text before it wraps.\n\t// @return {object} a text info object.\n\n\tCanvas.prototype.getTextInfo = function(layer, text, font, angle, width) {\n\n\t\tvar textStyle, layerCache, styleCache, info;\n\n\t\t// Cast the value to a string, in case we were given a number or such\n\n\t\ttext = \"\" + text;\n\n\t\t// If the font is a font-spec object, generate a CSS font definition\n\n\t\tif (typeof font === \"object\") {\n\t\t\ttextStyle = font.style + \" \" + font.variant + \" \" + font.weight + \" \" + font.size + \"px/\" + font.lineHeight + \"px \" + font.family;\n\t\t} else {\n\t\t\ttextStyle = font;\n\t\t}\n\n\t\t// Retrieve (or create) the cache for the text's layer and styles\n\n\t\tlayerCache = this._textCache[layer];\n\n\t\tif (layerCache == null) {\n\t\t\tlayerCache = this._textCache[layer] = {};\n\t\t}\n\n\t\tstyleCache = layerCache[textStyle];\n\n\t\tif (styleCache == null) {\n\t\t\tstyleCache = layerCache[textStyle] = {};\n\t\t}\n\n\t\tinfo = styleCache[text];\n\n\t\t// If we can't find a matching element in our cache, create a new one\n\n\t\tif (info == null) {\n\n\t\t\tvar element = $(\"<div></div>\").html(text)\n\t\t\t\t.css({\n\t\t\t\t\tposition: \"absolute\",\n\t\t\t\t\t'max-width': width,\n\t\t\t\t\ttop: -9999\n\t\t\t\t})\n\t\t\t\t.appendTo(this.getTextLayer(layer));\n\n\t\t\tif (typeof font === \"object\") {\n\t\t\t\telement.css({\n\t\t\t\t\tfont: textStyle,\n\t\t\t\t\tcolor: font.color\n\t\t\t\t});\n\t\t\t} else if (typeof font === \"string\") {\n\t\t\t\telement.addClass(font);\n\t\t\t}\n\n\t\t\tinfo = styleCache[text] = {\n\t\t\t\twidth: element.outerWidth(true),\n\t\t\t\theight: element.outerHeight(true),\n\t\t\t\telement: element,\n\t\t\t\tpositions: []\n\t\t\t};\n\n\t\t\telement.detach();\n\t\t}\n\n\t\treturn info;\n\t};\n\n\t// Adds a text string to the canvas text overlay.\n\t//\n\t// The text isn't drawn immediately; it is marked as rendering, which will\n\t// result in its addition to the canvas on the next render pass.\n\t//\n\t// @param {string} layer A string of space-separated CSS classes uniquely\n\t//     identifying the layer containing this text.\n\t// @param {number} x X coordinate at which to draw the text.\n\t// @param {number} y Y coordinate at which to draw the text.\n\t// @param {string} text Text string to draw.\n\t// @param {(string|object)=} font Either a string of space-separated CSS\n\t//     classes or a font-spec object, defining the text's font and style.\n\t// @param {number=} angle Angle at which to rotate the text, in degrees.\n\t//     Angle is currently unused, it will be implemented in the future.\n\t// @param {number=} width Maximum width of the text before it wraps.\n\t// @param {string=} halign Horizontal alignment of the text; either \"left\",\n\t//     \"center\" or \"right\".\n\t// @param {string=} valign Vertical alignment of the text; either \"top\",\n\t//     \"middle\" or \"bottom\".\n\n\tCanvas.prototype.addText = function(layer, x, y, text, font, angle, width, halign, valign) {\n\n\t\tvar info = this.getTextInfo(layer, text, font, angle, width),\n\t\t\tpositions = info.positions;\n\n\t\t// Tweak the div's position to match the text's alignment\n\n\t\tif (halign == \"center\") {\n\t\t\tx -= info.width / 2;\n\t\t} else if (halign == \"right\") {\n\t\t\tx -= info.width;\n\t\t}\n\n\t\tif (valign == \"middle\") {\n\t\t\ty -= info.height / 2;\n\t\t} else if (valign == \"bottom\") {\n\t\t\ty -= info.height;\n\t\t}\n\n\t\t// Determine whether this text already exists at this position.\n\t\t// If so, mark it for inclusion in the next render pass.\n\n\t\tfor (var i = 0, position; position = positions[i]; i++) {\n\t\t\tif (position.x == x && position.y == y) {\n\t\t\t\tposition.active = true;\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\n\t\t// If the text doesn't exist at this position, create a new entry\n\n\t\t// For the very first position we'll re-use the original element,\n\t\t// while for subsequent ones we'll clone it.\n\n\t\tposition = {\n\t\t\tactive: true,\n\t\t\trendered: false,\n\t\t\telement: positions.length ? info.element.clone() : info.element,\n\t\t\tx: x,\n\t\t\ty: y\n\t\t};\n\n\t\tpositions.push(position);\n\n\t\t// Move the element to its final position within the container\n\n\t\tposition.element.css({\n\t\t\ttop: Math.round(y),\n\t\t\tleft: Math.round(x),\n\t\t\t'text-align': halign\t// In case the text wraps\n\t\t});\n\t};\n\n\t// Removes one or more text strings from the canvas text overlay.\n\t//\n\t// If no parameters are given, all text within the layer is removed.\n\t//\n\t// Note that the text is not immediately removed; it is simply marked as\n\t// inactive, which will result in its removal on the next render pass.\n\t// This avoids the performance penalty for 'clear and redraw' behavior,\n\t// where we potentially get rid of all text on a layer, but will likely\n\t// add back most or all of it later, as when redrawing axes, for example.\n\t//\n\t// @param {string} layer A string of space-separated CSS classes uniquely\n\t//     identifying the layer containing this text.\n\t// @param {number=} x X coordinate of the text.\n\t// @param {number=} y Y coordinate of the text.\n\t// @param {string=} text Text string to remove.\n\t// @param {(string|object)=} font Either a string of space-separated CSS\n\t//     classes or a font-spec object, defining the text's font and style.\n\t// @param {number=} angle Angle at which the text is rotated, in degrees.\n\t//     Angle is currently unused, it will be implemented in the future.\n\n\tCanvas.prototype.removeText = function(layer, x, y, text, font, angle) {\n\t\tif (text == null) {\n\t\t\tvar layerCache = this._textCache[layer];\n\t\t\tif (layerCache != null) {\n\t\t\t\tfor (var styleKey in layerCache) {\n\t\t\t\t\tif (hasOwnProperty.call(layerCache, styleKey)) {\n\t\t\t\t\t\tvar styleCache = layerCache[styleKey];\n\t\t\t\t\t\tfor (var key in styleCache) {\n\t\t\t\t\t\t\tif (hasOwnProperty.call(styleCache, key)) {\n\t\t\t\t\t\t\t\tvar positions = styleCache[key].positions;\n\t\t\t\t\t\t\t\tfor (var i = 0, position; position = positions[i]; i++) {\n\t\t\t\t\t\t\t\t\tposition.active = false;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tvar positions = this.getTextInfo(layer, text, font, angle).positions;\n\t\t\tfor (var i = 0, position; position = positions[i]; i++) {\n\t\t\t\tif (position.x == x && position.y == y) {\n\t\t\t\t\tposition.active = false;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t};\n\n\t///////////////////////////////////////////////////////////////////////////\n\t// The top-level container for the entire plot.\n\n    function Plot(placeholder, data_, options_, plugins) {\n        // data is on the form:\n        //   [ series1, series2 ... ]\n        // where series is either just the data as [ [x1, y1], [x2, y2], ... ]\n        // or { data: [ [x1, y1], [x2, y2], ... ], label: \"some label\", ... }\n\n        var series = [],\n            options = {\n                // the color theme used for graphs\n                colors: [\"#edc240\", \"#afd8f8\", \"#cb4b4b\", \"#4da74d\", \"#9440ed\"],\n                legend: {\n                    show: true,\n                    noColumns: 1, // number of colums in legend table\n                    labelFormatter: null, // fn: string -> string\n                    labelBoxBorderColor: \"#ccc\", // border color for the little label boxes\n                    container: null, // container (as jQuery object) to put legend in, null means default on top of graph\n                    position: \"ne\", // position of default legend container within plot\n                    margin: 5, // distance from grid edge to default legend container within plot\n                    backgroundColor: null, // null means auto-detect\n                    backgroundOpacity: 0.85, // set to 0 to avoid background\n                    sorted: null    // default to no legend sorting\n                },\n                xaxis: {\n                    show: null, // null = auto-detect, true = always, false = never\n                    position: \"bottom\", // or \"top\"\n                    mode: null, // null or \"time\"\n                    font: null, // null (derived from CSS in placeholder) or object like { size: 11, lineHeight: 13, style: \"italic\", weight: \"bold\", family: \"sans-serif\", variant: \"small-caps\" }\n                    color: null, // base color, labels, ticks\n                    tickColor: null, // possibly different color of ticks, e.g. \"rgba(0,0,0,0.15)\"\n                    transform: null, // null or f: number -> number to transform axis\n                    inverseTransform: null, // if transform is set, this should be the inverse function\n                    min: null, // min. value to show, null means set automatically\n                    max: null, // max. value to show, null means set automatically\n                    autoscaleMargin: null, // margin in % to add if auto-setting min/max\n                    ticks: null, // either [1, 3] or [[1, \"a\"], 3] or (fn: axis info -> ticks) or app. number of ticks for auto-ticks\n                    tickFormatter: null, // fn: number -> string\n                    labelWidth: null, // size of tick labels in pixels\n                    labelHeight: null,\n                    reserveSpace: null, // whether to reserve space even if axis isn't shown\n                    tickLength: null, // size in pixels of ticks, or \"full\" for whole line\n                    alignTicksWithAxis: null, // axis number or null for no sync\n                    tickDecimals: null, // no. of decimals, null means auto\n                    tickSize: null, // number or [number, \"unit\"]\n                    minTickSize: null // number or [number, \"unit\"]\n                },\n                yaxis: {\n                    autoscaleMargin: 0.02,\n                    position: \"left\" // or \"right\"\n                },\n                xaxes: [],\n                yaxes: [],\n                series: {\n                    points: {\n                        show: false,\n                        radius: 3,\n                        lineWidth: 2, // in pixels\n                        fill: true,\n                        fillColor: \"#ffffff\",\n                        symbol: \"circle\" // or callback\n                    },\n                    lines: {\n                        // we don't put in show: false so we can see\n                        // whether lines were actively disabled\n                        lineWidth: 2, // in pixels\n                        fill: false,\n                        fillColor: null,\n                        steps: false\n                        // Omit 'zero', so we can later default its value to\n                        // match that of the 'fill' option.\n                    },\n                    bars: {\n                        show: false,\n                        lineWidth: 2, // in pixels\n                        barWidth: 1, // in units of the x axis\n                        fill: true,\n                        fillColor: null,\n                        align: \"left\", // \"left\", \"right\", or \"center\"\n                        horizontal: false,\n                        zero: true\n                    },\n                    shadowSize: 3,\n                    highlightColor: null\n                },\n                grid: {\n                    show: true,\n                    aboveData: false,\n                    color: \"#545454\", // primary color used for outline and labels\n                    backgroundColor: null, // null for transparent, else color\n                    borderColor: null, // set if different from the grid color\n                    tickColor: null, // color for the ticks, e.g. \"rgba(0,0,0,0.15)\"\n                    margin: 0, // distance from the canvas edge to the grid\n                    labelMargin: 5, // in pixels\n                    axisMargin: 8, // in pixels\n                    borderWidth: 2, // in pixels\n                    minBorderMargin: null, // in pixels, null means taken from points radius\n                    markings: null, // array of ranges or fn: axes -> array of ranges\n                    markingsColor: \"#f4f4f4\",\n                    markingsLineWidth: 2,\n                    // interactive stuff\n                    clickable: false,\n                    hoverable: false,\n                    autoHighlight: true, // highlight in case mouse is near\n                    mouseActiveRadius: 10 // how far the mouse can be away to activate an item\n                },\n                interaction: {\n                    redrawOverlayInterval: 1000/60 // time between updates, -1 means in same flow\n                },\n                hooks: {}\n            },\n        surface = null,     // the canvas for the plot itself\n        overlay = null,     // canvas for interactive stuff on top of plot\n        eventHolder = null, // jQuery object that events should be bound to\n        ctx = null, octx = null,\n        xaxes = [], yaxes = [],\n        plotOffset = { left: 0, right: 0, top: 0, bottom: 0},\n        plotWidth = 0, plotHeight = 0,\n        hooks = {\n            processOptions: [],\n            processRawData: [],\n            processDatapoints: [],\n            processOffset: [],\n            drawBackground: [],\n            drawSeries: [],\n            draw: [],\n            bindEvents: [],\n            drawOverlay: [],\n            shutdown: []\n        },\n        plot = this;\n\n        // public functions\n        plot.setData = setData;\n        plot.setupGrid = setupGrid;\n        plot.draw = draw;\n        plot.getPlaceholder = function() { return placeholder; };\n        plot.getCanvas = function() { return surface.element; };\n        plot.getPlotOffset = function() { return plotOffset; };\n        plot.width = function () { return plotWidth; };\n        plot.height = function () { return plotHeight; };\n        plot.offset = function () {\n            var o = eventHolder.offset();\n            o.left += plotOffset.left;\n            o.top += plotOffset.top;\n            return o;\n        };\n        plot.getData = function () { return series; };\n        plot.getAxes = function () {\n            var res = {}, i;\n            $.each(xaxes.concat(yaxes), function (_, axis) {\n                if (axis)\n                    res[axis.direction + (axis.n != 1 ? axis.n : \"\") + \"axis\"] = axis;\n            });\n            return res;\n        };\n        plot.getXAxes = function () { return xaxes; };\n        plot.getYAxes = function () { return yaxes; };\n        plot.c2p = canvasToAxisCoords;\n        plot.p2c = axisToCanvasCoords;\n        plot.getOptions = function () { return options; };\n        plot.highlight = highlight;\n        plot.unhighlight = unhighlight;\n        plot.triggerRedrawOverlay = triggerRedrawOverlay;\n        plot.pointOffset = function(point) {\n            return {\n                left: parseInt(xaxes[axisNumber(point, \"x\") - 1].p2c(+point.x) + plotOffset.left, 10),\n                top: parseInt(yaxes[axisNumber(point, \"y\") - 1].p2c(+point.y) + plotOffset.top, 10)\n            };\n        };\n        plot.shutdown = shutdown;\n        plot.destroy = function () {\n            shutdown();\n            placeholder.removeData(\"plot\").empty();\n\n            series = [];\n            options = null;\n            surface = null;\n            overlay = null;\n            eventHolder = null;\n            ctx = null;\n            octx = null;\n            xaxes = [];\n            yaxes = [];\n            hooks = null;\n            highlights = [];\n            plot = null;\n        };\n        plot.resize = function () {\n        \tvar width = placeholder.width(),\n        \t\theight = placeholder.height();\n            surface.resize(width, height);\n            overlay.resize(width, height);\n        };\n\n        // public attributes\n        plot.hooks = hooks;\n\n        // initialize\n        initPlugins(plot);\n        parseOptions(options_);\n        setupCanvases();\n        setData(data_);\n        setupGrid();\n        draw();\n        bindEvents();\n\n\n        function executeHooks(hook, args) {\n            args = [plot].concat(args);\n            for (var i = 0; i < hook.length; ++i)\n                hook[i].apply(this, args);\n        }\n\n        function initPlugins() {\n\n            // References to key classes, allowing plugins to modify them\n\n            var classes = {\n                Canvas: Canvas\n            };\n\n            for (var i = 0; i < plugins.length; ++i) {\n                var p = plugins[i];\n                p.init(plot, classes);\n                if (p.options)\n                    $.extend(true, options, p.options);\n            }\n        }\n\n        function parseOptions(opts) {\n\n            $.extend(true, options, opts);\n\n            // $.extend merges arrays, rather than replacing them.  When less\n            // colors are provided than the size of the default palette, we\n            // end up with those colors plus the remaining defaults, which is\n            // not expected behavior; avoid it by replacing them here.\n\n            if (opts && opts.colors) {\n            \toptions.colors = opts.colors;\n            }\n\n            if (options.xaxis.color == null)\n                options.xaxis.color = $.color.parse(options.grid.color).scale('a', 0.22).toString();\n            if (options.yaxis.color == null)\n                options.yaxis.color = $.color.parse(options.grid.color).scale('a', 0.22).toString();\n\n            if (options.xaxis.tickColor == null) // grid.tickColor for back-compatibility\n                options.xaxis.tickColor = options.grid.tickColor || options.xaxis.color;\n            if (options.yaxis.tickColor == null) // grid.tickColor for back-compatibility\n                options.yaxis.tickColor = options.grid.tickColor || options.yaxis.color;\n\n            if (options.grid.borderColor == null)\n                options.grid.borderColor = options.grid.color;\n            if (options.grid.tickColor == null)\n                options.grid.tickColor = $.color.parse(options.grid.color).scale('a', 0.22).toString();\n\n            // Fill in defaults for axis options, including any unspecified\n            // font-spec fields, if a font-spec was provided.\n\n            // If no x/y axis options were provided, create one of each anyway,\n            // since the rest of the code assumes that they exist.\n\n            var i, axisOptions, axisCount,\n                fontSize = placeholder.css(\"font-size\"),\n                fontSizeDefault = fontSize ? +fontSize.replace(\"px\", \"\") : 13,\n                fontDefaults = {\n                    style: placeholder.css(\"font-style\"),\n                    size: Math.round(0.8 * fontSizeDefault),\n                    variant: placeholder.css(\"font-variant\"),\n                    weight: placeholder.css(\"font-weight\"),\n                    family: placeholder.css(\"font-family\")\n                };\n\n            axisCount = options.xaxes.length || 1;\n            for (i = 0; i < axisCount; ++i) {\n\n                axisOptions = options.xaxes[i];\n                if (axisOptions && !axisOptions.tickColor) {\n                    axisOptions.tickColor = axisOptions.color;\n                }\n\n                axisOptions = $.extend(true, {}, options.xaxis, axisOptions);\n                options.xaxes[i] = axisOptions;\n\n                if (axisOptions.font) {\n                    axisOptions.font = $.extend({}, fontDefaults, axisOptions.font);\n                    if (!axisOptions.font.color) {\n                        axisOptions.font.color = axisOptions.color;\n                    }\n                    if (!axisOptions.font.lineHeight) {\n                        axisOptions.font.lineHeight = Math.round(axisOptions.font.size * 1.15);\n                    }\n                }\n            }\n\n            axisCount = options.yaxes.length || 1;\n            for (i = 0; i < axisCount; ++i) {\n\n                axisOptions = options.yaxes[i];\n                if (axisOptions && !axisOptions.tickColor) {\n                    axisOptions.tickColor = axisOptions.color;\n                }\n\n                axisOptions = $.extend(true, {}, options.yaxis, axisOptions);\n                options.yaxes[i] = axisOptions;\n\n                if (axisOptions.font) {\n                    axisOptions.font = $.extend({}, fontDefaults, axisOptions.font);\n                    if (!axisOptions.font.color) {\n                        axisOptions.font.color = axisOptions.color;\n                    }\n                    if (!axisOptions.font.lineHeight) {\n                        axisOptions.font.lineHeight = Math.round(axisOptions.font.size * 1.15);\n                    }\n                }\n            }\n\n            // backwards compatibility, to be removed in future\n            if (options.xaxis.noTicks && options.xaxis.ticks == null)\n                options.xaxis.ticks = options.xaxis.noTicks;\n            if (options.yaxis.noTicks && options.yaxis.ticks == null)\n                options.yaxis.ticks = options.yaxis.noTicks;\n            if (options.x2axis) {\n                options.xaxes[1] = $.extend(true, {}, options.xaxis, options.x2axis);\n                options.xaxes[1].position = \"top\";\n                // Override the inherit to allow the axis to auto-scale\n                if (options.x2axis.min == null) {\n                    options.xaxes[1].min = null;\n                }\n                if (options.x2axis.max == null) {\n                    options.xaxes[1].max = null;\n                }\n            }\n            if (options.y2axis) {\n                options.yaxes[1] = $.extend(true, {}, options.yaxis, options.y2axis);\n                options.yaxes[1].position = \"right\";\n                // Override the inherit to allow the axis to auto-scale\n                if (options.y2axis.min == null) {\n                    options.yaxes[1].min = null;\n                }\n                if (options.y2axis.max == null) {\n                    options.yaxes[1].max = null;\n                }\n            }\n            if (options.grid.coloredAreas)\n                options.grid.markings = options.grid.coloredAreas;\n            if (options.grid.coloredAreasColor)\n                options.grid.markingsColor = options.grid.coloredAreasColor;\n            if (options.lines)\n                $.extend(true, options.series.lines, options.lines);\n            if (options.points)\n                $.extend(true, options.series.points, options.points);\n            if (options.bars)\n                $.extend(true, options.series.bars, options.bars);\n            if (options.shadowSize != null)\n                options.series.shadowSize = options.shadowSize;\n            if (options.highlightColor != null)\n                options.series.highlightColor = options.highlightColor;\n\n            // save options on axes for future reference\n            for (i = 0; i < options.xaxes.length; ++i)\n                getOrCreateAxis(xaxes, i + 1).options = options.xaxes[i];\n            for (i = 0; i < options.yaxes.length; ++i)\n                getOrCreateAxis(yaxes, i + 1).options = options.yaxes[i];\n\n            // add hooks from options\n            for (var n in hooks)\n                if (options.hooks[n] && options.hooks[n].length)\n                    hooks[n] = hooks[n].concat(options.hooks[n]);\n\n            executeHooks(hooks.processOptions, [options]);\n        }\n\n        function setData(d) {\n            series = parseData(d);\n            fillInSeriesOptions();\n            processData();\n        }\n\n        function parseData(d) {\n            var res = [];\n            for (var i = 0; i < d.length; ++i) {\n                var s = $.extend(true, {}, options.series);\n\n                if (d[i].data != null) {\n                    s.data = d[i].data; // move the data instead of deep-copy\n                    delete d[i].data;\n\n                    $.extend(true, s, d[i]);\n\n                    d[i].data = s.data;\n                }\n                else\n                    s.data = d[i];\n                res.push(s);\n            }\n\n            return res;\n        }\n\n        function axisNumber(obj, coord) {\n            var a = obj[coord + \"axis\"];\n            if (typeof a == \"object\") // if we got a real axis, extract number\n                a = a.n;\n            if (typeof a != \"number\")\n                a = 1; // default to first axis\n            return a;\n        }\n\n        function allAxes() {\n            // return flat array without annoying null entries\n            return $.grep(xaxes.concat(yaxes), function (a) { return a; });\n        }\n\n        function canvasToAxisCoords(pos) {\n            // return an object with x/y corresponding to all used axes\n            var res = {}, i, axis;\n            for (i = 0; i < xaxes.length; ++i) {\n                axis = xaxes[i];\n                if (axis && axis.used)\n                    res[\"x\" + axis.n] = axis.c2p(pos.left);\n            }\n\n            for (i = 0; i < yaxes.length; ++i) {\n                axis = yaxes[i];\n                if (axis && axis.used)\n                    res[\"y\" + axis.n] = axis.c2p(pos.top);\n            }\n\n            if (res.x1 !== undefined)\n                res.x = res.x1;\n            if (res.y1 !== undefined)\n                res.y = res.y1;\n\n            return res;\n        }\n\n        function axisToCanvasCoords(pos) {\n            // get canvas coords from the first pair of x/y found in pos\n            var res = {}, i, axis, key;\n\n            for (i = 0; i < xaxes.length; ++i) {\n                axis = xaxes[i];\n                if (axis && axis.used) {\n                    key = \"x\" + axis.n;\n                    if (pos[key] == null && axis.n == 1)\n                        key = \"x\";\n\n                    if (pos[key] != null) {\n                        res.left = axis.p2c(pos[key]);\n                        break;\n                    }\n                }\n            }\n\n            for (i = 0; i < yaxes.length; ++i) {\n                axis = yaxes[i];\n                if (axis && axis.used) {\n                    key = \"y\" + axis.n;\n                    if (pos[key] == null && axis.n == 1)\n                        key = \"y\";\n\n                    if (pos[key] != null) {\n                        res.top = axis.p2c(pos[key]);\n                        break;\n                    }\n                }\n            }\n\n            return res;\n        }\n\n        function getOrCreateAxis(axes, number) {\n            if (!axes[number - 1])\n                axes[number - 1] = {\n                    n: number, // save the number for future reference\n                    direction: axes == xaxes ? \"x\" : \"y\",\n                    options: $.extend(true, {}, axes == xaxes ? options.xaxis : options.yaxis)\n                };\n\n            return axes[number - 1];\n        }\n\n        function fillInSeriesOptions() {\n\n            var neededColors = series.length, maxIndex = -1, i;\n\n            // Subtract the number of series that already have fixed colors or\n            // color indexes from the number that we still need to generate.\n\n            for (i = 0; i < series.length; ++i) {\n                var sc = series[i].color;\n                if (sc != null) {\n                    neededColors--;\n                    if (typeof sc == \"number\" && sc > maxIndex) {\n                        maxIndex = sc;\n                    }\n                }\n            }\n\n            // If any of the series have fixed color indexes, then we need to\n            // generate at least as many colors as the highest index.\n\n            if (neededColors <= maxIndex) {\n                neededColors = maxIndex + 1;\n            }\n\n            // Generate all the colors, using first the option colors and then\n            // variations on those colors once they're exhausted.\n\n            var c, colors = [], colorPool = options.colors,\n                colorPoolSize = colorPool.length, variation = 0;\n\n            for (i = 0; i < neededColors; i++) {\n\n                c = $.color.parse(colorPool[i % colorPoolSize] || \"#666\");\n\n                // Each time we exhaust the colors in the pool we adjust\n                // a scaling factor used to produce more variations on\n                // those colors. The factor alternates negative/positive\n                // to produce lighter/darker colors.\n\n                // Reset the variation after every few cycles, or else\n                // it will end up producing only white or black colors.\n\n                if (i % colorPoolSize == 0 && i) {\n                    if (variation >= 0) {\n                        if (variation < 0.5) {\n                            variation = -variation - 0.2;\n                        } else variation = 0;\n                    } else variation = -variation;\n                }\n\n                colors[i] = c.scale('rgb', 1 + variation);\n            }\n\n            // Finalize the series options, filling in their colors\n\n            var colori = 0, s;\n            for (i = 0; i < series.length; ++i) {\n                s = series[i];\n\n                // assign colors\n                if (s.color == null) {\n                    s.color = colors[colori].toString();\n                    ++colori;\n                }\n                else if (typeof s.color == \"number\")\n                    s.color = colors[s.color].toString();\n\n                // turn on lines automatically in case nothing is set\n                if (s.lines.show == null) {\n                    var v, show = true;\n                    for (v in s)\n                        if (s[v] && s[v].show) {\n                            show = false;\n                            break;\n                        }\n                    if (show)\n                        s.lines.show = true;\n                }\n\n                // If nothing was provided for lines.zero, default it to match\n                // lines.fill, since areas by default should extend to zero.\n\n                if (s.lines.zero == null) {\n                    s.lines.zero = !!s.lines.fill;\n                }\n\n                // setup axes\n                s.xaxis = getOrCreateAxis(xaxes, axisNumber(s, \"x\"));\n                s.yaxis = getOrCreateAxis(yaxes, axisNumber(s, \"y\"));\n            }\n        }\n\n        function processData() {\n            var topSentry = Number.POSITIVE_INFINITY,\n                bottomSentry = Number.NEGATIVE_INFINITY,\n                fakeInfinity = Number.MAX_VALUE,\n                i, j, k, m, length,\n                s, points, ps, x, y, axis, val, f, p,\n                data, format;\n\n            function updateAxis(axis, min, max) {\n                if (min < axis.datamin && min != -fakeInfinity)\n                    axis.datamin = min;\n                if (max > axis.datamax && max != fakeInfinity)\n                    axis.datamax = max;\n            }\n\n            $.each(allAxes(), function (_, axis) {\n                // init axis\n                axis.datamin = topSentry;\n                axis.datamax = bottomSentry;\n                axis.used = false;\n            });\n\n            for (i = 0; i < series.length; ++i) {\n                s = series[i];\n                s.datapoints = { points: [] };\n\n                executeHooks(hooks.processRawData, [ s, s.data, s.datapoints ]);\n            }\n\n            // first pass: clean and copy data\n            for (i = 0; i < series.length; ++i) {\n                s = series[i];\n\n                data = s.data;\n                format = s.datapoints.format;\n\n                if (!format) {\n                    format = [];\n                    // find out how to copy\n                    format.push({ x: true, number: true, required: true });\n                    format.push({ y: true, number: true, required: true });\n\n                    if (s.bars.show || (s.lines.show && s.lines.fill)) {\n                        var autoscale = !!((s.bars.show && s.bars.zero) || (s.lines.show && s.lines.zero));\n                        format.push({ y: true, number: true, required: false, defaultValue: 0, autoscale: autoscale });\n                        if (s.bars.horizontal) {\n                            delete format[format.length - 1].y;\n                            format[format.length - 1].x = true;\n                        }\n                    }\n\n                    s.datapoints.format = format;\n                }\n\n                if (s.datapoints.pointsize != null)\n                    continue; // already filled in\n\n                s.datapoints.pointsize = format.length;\n\n                ps = s.datapoints.pointsize;\n                points = s.datapoints.points;\n\n                var insertSteps = s.lines.show && s.lines.steps;\n                s.xaxis.used = s.yaxis.used = true;\n\n                for (j = k = 0; j < data.length; ++j, k += ps) {\n                    p = data[j];\n\n                    var nullify = p == null;\n                    if (!nullify) {\n                        for (m = 0; m < ps; ++m) {\n                            val = p[m];\n                            f = format[m];\n\n                            if (f) {\n                                if (f.number && val != null) {\n                                    val = +val; // convert to number\n                                    if (isNaN(val))\n                                        val = null;\n                                    else if (val == Infinity)\n                                        val = fakeInfinity;\n                                    else if (val == -Infinity)\n                                        val = -fakeInfinity;\n                                }\n\n                                if (val == null) {\n                                    if (f.required)\n                                        nullify = true;\n\n                                    if (f.defaultValue != null)\n                                        val = f.defaultValue;\n                                }\n                            }\n\n                            points[k + m] = val;\n                        }\n                    }\n\n                    if (nullify) {\n                        for (m = 0; m < ps; ++m) {\n                            val = points[k + m];\n                            if (val != null) {\n                                f = format[m];\n                                // extract min/max info\n                                if (f.autoscale !== false) {\n                                    if (f.x) {\n                                        updateAxis(s.xaxis, val, val);\n                                    }\n                                    if (f.y) {\n                                        updateAxis(s.yaxis, val, val);\n                                    }\n                                }\n                            }\n                            points[k + m] = null;\n                        }\n                    }\n                    else {\n                        // a little bit of line specific stuff that\n                        // perhaps shouldn't be here, but lacking\n                        // better means...\n                        if (insertSteps && k > 0\n                            && points[k - ps] != null\n                            && points[k - ps] != points[k]\n                            && points[k - ps + 1] != points[k + 1]) {\n                            // copy the point to make room for a middle point\n                            for (m = 0; m < ps; ++m)\n                                points[k + ps + m] = points[k + m];\n\n                            // middle point has same y\n                            points[k + 1] = points[k - ps + 1];\n\n                            // we've added a point, better reflect that\n                            k += ps;\n                        }\n                    }\n                }\n            }\n\n            // give the hooks a chance to run\n            for (i = 0; i < series.length; ++i) {\n                s = series[i];\n\n                executeHooks(hooks.processDatapoints, [ s, s.datapoints]);\n            }\n\n            // second pass: find datamax/datamin for auto-scaling\n            for (i = 0; i < series.length; ++i) {\n                s = series[i];\n                points = s.datapoints.points;\n                ps = s.datapoints.pointsize;\n                format = s.datapoints.format;\n\n                var xmin = topSentry, ymin = topSentry,\n                    xmax = bottomSentry, ymax = bottomSentry;\n\n                for (j = 0; j < points.length; j += ps) {\n                    if (points[j] == null)\n                        continue;\n\n                    for (m = 0; m < ps; ++m) {\n                        val = points[j + m];\n                        f = format[m];\n                        if (!f || f.autoscale === false || val == fakeInfinity || val == -fakeInfinity)\n                            continue;\n\n                        if (f.x) {\n                            if (val < xmin)\n                                xmin = val;\n                            if (val > xmax)\n                                xmax = val;\n                        }\n                        if (f.y) {\n                            if (val < ymin)\n                                ymin = val;\n                            if (val > ymax)\n                                ymax = val;\n                        }\n                    }\n                }\n\n                if (s.bars.show) {\n                    // make sure we got room for the bar on the dancing floor\n                    var delta;\n\n                    switch (s.bars.align) {\n                        case \"left\":\n                            delta = 0;\n                            break;\n                        case \"right\":\n                            delta = -s.bars.barWidth;\n                            break;\n                        default:\n                            delta = -s.bars.barWidth / 2;\n                    }\n\n                    if (s.bars.horizontal) {\n                        ymin += delta;\n                        ymax += delta + s.bars.barWidth;\n                    }\n                    else {\n                        xmin += delta;\n                        xmax += delta + s.bars.barWidth;\n                    }\n                }\n\n                updateAxis(s.xaxis, xmin, xmax);\n                updateAxis(s.yaxis, ymin, ymax);\n            }\n\n            $.each(allAxes(), function (_, axis) {\n                if (axis.datamin == topSentry)\n                    axis.datamin = null;\n                if (axis.datamax == bottomSentry)\n                    axis.datamax = null;\n            });\n        }\n\n        function setupCanvases() {\n\n            // Make sure the placeholder is clear of everything except canvases\n            // from a previous plot in this container that we'll try to re-use.\n\n            placeholder.css(\"padding\", 0) // padding messes up the positioning\n                .children().filter(function(){\n                    return !$(this).hasClass(\"flot-overlay\") && !$(this).hasClass('flot-base');\n                }).remove();\n\n            if (placeholder.css(\"position\") == 'static')\n                placeholder.css(\"position\", \"relative\"); // for positioning labels and overlay\n\n            surface = new Canvas(\"flot-base\", placeholder);\n            overlay = new Canvas(\"flot-overlay\", placeholder); // overlay canvas for interactive features\n\n            ctx = surface.context;\n            octx = overlay.context;\n\n            // define which element we're listening for events on\n            eventHolder = $(overlay.element).unbind();\n\n            // If we're re-using a plot object, shut down the old one\n\n            var existing = placeholder.data(\"plot\");\n\n            if (existing) {\n                existing.shutdown();\n                overlay.clear();\n            }\n\n            // save in case we get replotted\n            placeholder.data(\"plot\", plot);\n        }\n\n        function bindEvents() {\n            // bind events\n            if (options.grid.hoverable) {\n                eventHolder.mousemove(onMouseMove);\n\n                // Use bind, rather than .mouseleave, because we officially\n                // still support jQuery 1.2.6, which doesn't define a shortcut\n                // for mouseenter or mouseleave.  This was a bug/oversight that\n                // was fixed somewhere around 1.3.x.  We can return to using\n                // .mouseleave when we drop support for 1.2.6.\n\n                eventHolder.bind(\"mouseleave\", onMouseLeave);\n            }\n\n            if (options.grid.clickable)\n                eventHolder.click(onClick);\n\n            executeHooks(hooks.bindEvents, [eventHolder]);\n        }\n\n        function shutdown() {\n            if (redrawTimeout)\n                clearTimeout(redrawTimeout);\n\n            eventHolder.unbind(\"mousemove\", onMouseMove);\n            eventHolder.unbind(\"mouseleave\", onMouseLeave);\n            eventHolder.unbind(\"click\", onClick);\n\n            executeHooks(hooks.shutdown, [eventHolder]);\n        }\n\n        function setTransformationHelpers(axis) {\n            // set helper functions on the axis, assumes plot area\n            // has been computed already\n\n            function identity(x) { return x; }\n\n            var s, m, t = axis.options.transform || identity,\n                it = axis.options.inverseTransform;\n\n            // precompute how much the axis is scaling a point\n            // in canvas space\n            if (axis.direction == \"x\") {\n                s = axis.scale = plotWidth / Math.abs(t(axis.max) - t(axis.min));\n                m = Math.min(t(axis.max), t(axis.min));\n            }\n            else {\n                s = axis.scale = plotHeight / Math.abs(t(axis.max) - t(axis.min));\n                s = -s;\n                m = Math.max(t(axis.max), t(axis.min));\n            }\n\n            // data point to canvas coordinate\n            if (t == identity) // slight optimization\n                axis.p2c = function (p) { return (p - m) * s; };\n            else\n                axis.p2c = function (p) { return (t(p) - m) * s; };\n            // canvas coordinate to data point\n            if (!it)\n                axis.c2p = function (c) { return m + c / s; };\n            else\n                axis.c2p = function (c) { return it(m + c / s); };\n        }\n\n        function measureTickLabels(axis) {\n\n            var opts = axis.options,\n                ticks = axis.ticks || [],\n                labelWidth = opts.labelWidth || 0,\n                labelHeight = opts.labelHeight || 0,\n                maxWidth = labelWidth || (axis.direction == \"x\" ? Math.floor(surface.width / (ticks.length || 1)) : null),\n                legacyStyles = axis.direction + \"Axis \" + axis.direction + axis.n + \"Axis\",\n                layer = \"flot-\" + axis.direction + \"-axis flot-\" + axis.direction + axis.n + \"-axis \" + legacyStyles,\n                font = opts.font || \"flot-tick-label tickLabel\";\n\n            for (var i = 0; i < ticks.length; ++i) {\n\n                var t = ticks[i];\n\n                if (!t.label)\n                    continue;\n\n                var info = surface.getTextInfo(layer, t.label, font, null, maxWidth);\n\n                labelWidth = Math.max(labelWidth, info.width);\n                labelHeight = Math.max(labelHeight, info.height);\n            }\n\n            axis.labelWidth = opts.labelWidth || labelWidth;\n            axis.labelHeight = opts.labelHeight || labelHeight;\n        }\n\n        function allocateAxisBoxFirstPhase(axis) {\n            // find the bounding box of the axis by looking at label\n            // widths/heights and ticks, make room by diminishing the\n            // plotOffset; this first phase only looks at one\n            // dimension per axis, the other dimension depends on the\n            // other axes so will have to wait\n\n            var lw = axis.labelWidth,\n                lh = axis.labelHeight,\n                pos = axis.options.position,\n                isXAxis = axis.direction === \"x\",\n                tickLength = axis.options.tickLength,\n                axisMargin = options.grid.axisMargin,\n                padding = options.grid.labelMargin,\n                innermost = true,\n                outermost = true,\n                first = true,\n                found = false;\n\n            // Determine the axis's position in its direction and on its side\n\n            $.each(isXAxis ? xaxes : yaxes, function(i, a) {\n                if (a && (a.show || a.reserveSpace)) {\n                    if (a === axis) {\n                        found = true;\n                    } else if (a.options.position === pos) {\n                        if (found) {\n                            outermost = false;\n                        } else {\n                            innermost = false;\n                        }\n                    }\n                    if (!found) {\n                        first = false;\n                    }\n                }\n            });\n\n            // The outermost axis on each side has no margin\n\n            if (outermost) {\n                axisMargin = 0;\n            }\n\n            // The ticks for the first axis in each direction stretch across\n\n            if (tickLength == null) {\n                tickLength = first ? \"full\" : 5;\n            }\n\n            if (!isNaN(+tickLength))\n                padding += +tickLength;\n\n            if (isXAxis) {\n                lh += padding;\n\n                if (pos == \"bottom\") {\n                    plotOffset.bottom += lh + axisMargin;\n                    axis.box = { top: surface.height - plotOffset.bottom, height: lh };\n                }\n                else {\n                    axis.box = { top: plotOffset.top + axisMargin, height: lh };\n                    plotOffset.top += lh + axisMargin;\n                }\n            }\n            else {\n                lw += padding;\n\n                if (pos == \"left\") {\n                    axis.box = { left: plotOffset.left + axisMargin, width: lw };\n                    plotOffset.left += lw + axisMargin;\n                }\n                else {\n                    plotOffset.right += lw + axisMargin;\n                    axis.box = { left: surface.width - plotOffset.right, width: lw };\n                }\n            }\n\n             // save for future reference\n            axis.position = pos;\n            axis.tickLength = tickLength;\n            axis.box.padding = padding;\n            axis.innermost = innermost;\n        }\n\n        function allocateAxisBoxSecondPhase(axis) {\n            // now that all axis boxes have been placed in one\n            // dimension, we can set the remaining dimension coordinates\n            if (axis.direction == \"x\") {\n                axis.box.left = plotOffset.left - axis.labelWidth / 2;\n                axis.box.width = surface.width - plotOffset.left - plotOffset.right + axis.labelWidth;\n            }\n            else {\n                axis.box.top = plotOffset.top - axis.labelHeight / 2;\n                axis.box.height = surface.height - plotOffset.bottom - plotOffset.top + axis.labelHeight;\n            }\n        }\n\n        function adjustLayoutForThingsStickingOut() {\n            // possibly adjust plot offset to ensure everything stays\n            // inside the canvas and isn't clipped off\n\n            var minMargin = options.grid.minBorderMargin,\n                axis, i;\n\n            // check stuff from the plot (FIXME: this should just read\n            // a value from the series, otherwise it's impossible to\n            // customize)\n            if (minMargin == null) {\n                minMargin = 0;\n                for (i = 0; i < series.length; ++i)\n                    minMargin = Math.max(minMargin, 2 * (series[i].points.radius + series[i].points.lineWidth/2));\n            }\n\n            var margins = {\n                left: minMargin,\n                right: minMargin,\n                top: minMargin,\n                bottom: minMargin\n            };\n\n            // check axis labels, note we don't check the actual\n            // labels but instead use the overall width/height to not\n            // jump as much around with replots\n            $.each(allAxes(), function (_, axis) {\n                if (axis.reserveSpace && axis.ticks && axis.ticks.length) {\n                    if (axis.direction === \"x\") {\n                        margins.left = Math.max(margins.left, axis.labelWidth / 2);\n                        margins.right = Math.max(margins.right, axis.labelWidth / 2);\n                    } else {\n                        margins.bottom = Math.max(margins.bottom, axis.labelHeight / 2);\n                        margins.top = Math.max(margins.top, axis.labelHeight / 2);\n                    }\n                }\n            });\n\n            plotOffset.left = Math.ceil(Math.max(margins.left, plotOffset.left));\n            plotOffset.right = Math.ceil(Math.max(margins.right, plotOffset.right));\n            plotOffset.top = Math.ceil(Math.max(margins.top, plotOffset.top));\n            plotOffset.bottom = Math.ceil(Math.max(margins.bottom, plotOffset.bottom));\n        }\n\n        function setupGrid() {\n            var i, axes = allAxes(), showGrid = options.grid.show;\n\n            // Initialize the plot's offset from the edge of the canvas\n\n            for (var a in plotOffset) {\n                var margin = options.grid.margin || 0;\n                plotOffset[a] = typeof margin == \"number\" ? margin : margin[a] || 0;\n            }\n\n            executeHooks(hooks.processOffset, [plotOffset]);\n\n            // If the grid is visible, add its border width to the offset\n\n            for (var a in plotOffset) {\n                if(typeof(options.grid.borderWidth) == \"object\") {\n                    plotOffset[a] += showGrid ? options.grid.borderWidth[a] : 0;\n                }\n                else {\n                    plotOffset[a] += showGrid ? options.grid.borderWidth : 0;\n                }\n            }\n\n            $.each(axes, function (_, axis) {\n                var axisOpts = axis.options;\n                axis.show = axisOpts.show == null ? axis.used : axisOpts.show;\n                axis.reserveSpace = axisOpts.reserveSpace == null ? axis.show : axisOpts.reserveSpace;\n                setRange(axis);\n            });\n\n            if (showGrid) {\n\n                var allocatedAxes = $.grep(axes, function (axis) {\n                    return axis.show || axis.reserveSpace;\n                });\n\n                $.each(allocatedAxes, function (_, axis) {\n                    // make the ticks\n                    setupTickGeneration(axis);\n                    setTicks(axis);\n                    snapRangeToTicks(axis, axis.ticks);\n                    // find labelWidth/Height for axis\n                    measureTickLabels(axis);\n                });\n\n                // with all dimensions calculated, we can compute the\n                // axis bounding boxes, start from the outside\n                // (reverse order)\n                for (i = allocatedAxes.length - 1; i >= 0; --i)\n                    allocateAxisBoxFirstPhase(allocatedAxes[i]);\n\n                // make sure we've got enough space for things that\n                // might stick out\n                adjustLayoutForThingsStickingOut();\n\n                $.each(allocatedAxes, function (_, axis) {\n                    allocateAxisBoxSecondPhase(axis);\n                });\n            }\n\n            plotWidth = surface.width - plotOffset.left - plotOffset.right;\n            plotHeight = surface.height - plotOffset.bottom - plotOffset.top;\n\n            // now we got the proper plot dimensions, we can compute the scaling\n            $.each(axes, function (_, axis) {\n                setTransformationHelpers(axis);\n            });\n\n            if (showGrid) {\n                drawAxisLabels();\n            }\n\n            insertLegend();\n        }\n\n        function setRange(axis) {\n            var opts = axis.options,\n                min = +(opts.min != null ? opts.min : axis.datamin),\n                max = +(opts.max != null ? opts.max : axis.datamax),\n                delta = max - min;\n\n            if (delta == 0.0) {\n                // degenerate case\n                var widen = max == 0 ? 1 : 0.01;\n\n                if (opts.min == null)\n                    min -= widen;\n                // always widen max if we couldn't widen min to ensure we\n                // don't fall into min == max which doesn't work\n                if (opts.max == null || opts.min != null)\n                    max += widen;\n            }\n            else {\n                // consider autoscaling\n                var margin = opts.autoscaleMargin;\n                if (margin != null) {\n                    if (opts.min == null) {\n                        min -= delta * margin;\n                        // make sure we don't go below zero if all values\n                        // are positive\n                        if (min < 0 && axis.datamin != null && axis.datamin >= 0)\n                            min = 0;\n                    }\n                    if (opts.max == null) {\n                        max += delta * margin;\n                        if (max > 0 && axis.datamax != null && axis.datamax <= 0)\n                            max = 0;\n                    }\n                }\n            }\n            axis.min = min;\n            axis.max = max;\n        }\n\n        function setupTickGeneration(axis) {\n            var opts = axis.options;\n\n            // estimate number of ticks\n            var noTicks;\n            if (typeof opts.ticks == \"number\" && opts.ticks > 0)\n                noTicks = opts.ticks;\n            else\n                // heuristic based on the model a*sqrt(x) fitted to\n                // some data points that seemed reasonable\n                noTicks = 0.3 * Math.sqrt(axis.direction == \"x\" ? surface.width : surface.height);\n\n            var delta = (axis.max - axis.min) / noTicks,\n                dec = -Math.floor(Math.log(delta) / Math.LN10),\n                maxDec = opts.tickDecimals;\n\n            if (maxDec != null && dec > maxDec) {\n                dec = maxDec;\n            }\n\n            var magn = Math.pow(10, -dec),\n                norm = delta / magn, // norm is between 1.0 and 10.0\n                size;\n\n            if (norm < 1.5) {\n                size = 1;\n            } else if (norm < 3) {\n                size = 2;\n                // special case for 2.5, requires an extra decimal\n                if (norm > 2.25 && (maxDec == null || dec + 1 <= maxDec)) {\n                    size = 2.5;\n                    ++dec;\n                }\n            } else if (norm < 7.5) {\n                size = 5;\n            } else {\n                size = 10;\n            }\n\n            size *= magn;\n\n            if (opts.minTickSize != null && size < opts.minTickSize) {\n                size = opts.minTickSize;\n            }\n\n            axis.delta = delta;\n            axis.tickDecimals = Math.max(0, maxDec != null ? maxDec : dec);\n            axis.tickSize = opts.tickSize || size;\n\n            // Time mode was moved to a plug-in in 0.8, and since so many people use it\n            // we'll add an especially friendly reminder to make sure they included it.\n\n            if (opts.mode == \"time\" && !axis.tickGenerator) {\n                throw new Error(\"Time mode requires the flot.time plugin.\");\n            }\n\n            // Flot supports base-10 axes; any other mode else is handled by a plug-in,\n            // like flot.time.js.\n\n            if (!axis.tickGenerator) {\n\n                axis.tickGenerator = function (axis) {\n\n                    var ticks = [],\n                        start = floorInBase(axis.min, axis.tickSize),\n                        i = 0,\n                        v = Number.NaN,\n                        prev;\n\n                    do {\n                        prev = v;\n                        v = start + i * axis.tickSize;\n                        ticks.push(v);\n                        ++i;\n                    } while (v < axis.max && v != prev);\n                    return ticks;\n                };\n\n\t\t\t\taxis.tickFormatter = function (value, axis) {\n\n\t\t\t\t\tvar factor = axis.tickDecimals ? Math.pow(10, axis.tickDecimals) : 1;\n\t\t\t\t\tvar formatted = \"\" + Math.round(value * factor) / factor;\n\n\t\t\t\t\t// If tickDecimals was specified, ensure that we have exactly that\n\t\t\t\t\t// much precision; otherwise default to the value's own precision.\n\n\t\t\t\t\tif (axis.tickDecimals != null) {\n\t\t\t\t\t\tvar decimal = formatted.indexOf(\".\");\n\t\t\t\t\t\tvar precision = decimal == -1 ? 0 : formatted.length - decimal - 1;\n\t\t\t\t\t\tif (precision < axis.tickDecimals) {\n\t\t\t\t\t\t\treturn (precision ? formatted : formatted + \".\") + (\"\" + factor).substr(1, axis.tickDecimals - precision);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n                    return formatted;\n                };\n            }\n\n            if ($.isFunction(opts.tickFormatter))\n                axis.tickFormatter = function (v, axis) { return \"\" + opts.tickFormatter(v, axis); };\n\n            if (opts.alignTicksWithAxis != null) {\n                var otherAxis = (axis.direction == \"x\" ? xaxes : yaxes)[opts.alignTicksWithAxis - 1];\n                if (otherAxis && otherAxis.used && otherAxis != axis) {\n                    // consider snapping min/max to outermost nice ticks\n                    var niceTicks = axis.tickGenerator(axis);\n                    if (niceTicks.length > 0) {\n                        if (opts.min == null)\n                            axis.min = Math.min(axis.min, niceTicks[0]);\n                        if (opts.max == null && niceTicks.length > 1)\n                            axis.max = Math.max(axis.max, niceTicks[niceTicks.length - 1]);\n                    }\n\n                    axis.tickGenerator = function (axis) {\n                        // copy ticks, scaled to this axis\n                        var ticks = [], v, i;\n                        for (i = 0; i < otherAxis.ticks.length; ++i) {\n                            v = (otherAxis.ticks[i].v - otherAxis.min) / (otherAxis.max - otherAxis.min);\n                            v = axis.min + v * (axis.max - axis.min);\n                            ticks.push(v);\n                        }\n                        return ticks;\n                    };\n\n                    // we might need an extra decimal since forced\n                    // ticks don't necessarily fit naturally\n                    if (!axis.mode && opts.tickDecimals == null) {\n                        var extraDec = Math.max(0, -Math.floor(Math.log(axis.delta) / Math.LN10) + 1),\n                            ts = axis.tickGenerator(axis);\n\n                        // only proceed if the tick interval rounded\n                        // with an extra decimal doesn't give us a\n                        // zero at end\n                        if (!(ts.length > 1 && /\\..*0$/.test((ts[1] - ts[0]).toFixed(extraDec))))\n                            axis.tickDecimals = extraDec;\n                    }\n                }\n            }\n        }\n\n        function setTicks(axis) {\n            var oticks = axis.options.ticks, ticks = [];\n            if (oticks == null || (typeof oticks == \"number\" && oticks > 0))\n                ticks = axis.tickGenerator(axis);\n            else if (oticks) {\n                if ($.isFunction(oticks))\n                    // generate the ticks\n                    ticks = oticks(axis);\n                else\n                    ticks = oticks;\n            }\n\n            // clean up/labelify the supplied ticks, copy them over\n            var i, v;\n            axis.ticks = [];\n            for (i = 0; i < ticks.length; ++i) {\n                var label = null;\n                var t = ticks[i];\n                if (typeof t == \"object\") {\n                    v = +t[0];\n                    if (t.length > 1)\n                        label = t[1];\n                }\n                else\n                    v = +t;\n                if (label == null)\n                    label = axis.tickFormatter(v, axis);\n                if (!isNaN(v))\n                    axis.ticks.push({ v: v, label: label });\n            }\n        }\n\n        function snapRangeToTicks(axis, ticks) {\n            if (axis.options.autoscaleMargin && ticks.length > 0) {\n                // snap to ticks\n                if (axis.options.min == null)\n                    axis.min = Math.min(axis.min, ticks[0].v);\n                if (axis.options.max == null && ticks.length > 1)\n                    axis.max = Math.max(axis.max, ticks[ticks.length - 1].v);\n            }\n        }\n\n        function draw() {\n\n            surface.clear();\n\n            executeHooks(hooks.drawBackground, [ctx]);\n\n            var grid = options.grid;\n\n            // draw background, if any\n            if (grid.show && grid.backgroundColor)\n                drawBackground();\n\n            if (grid.show && !grid.aboveData) {\n                drawGrid();\n            }\n\n            for (var i = 0; i < series.length; ++i) {\n                executeHooks(hooks.drawSeries, [ctx, series[i]]);\n                drawSeries(series[i]);\n            }\n\n            executeHooks(hooks.draw, [ctx]);\n\n            if (grid.show && grid.aboveData) {\n                drawGrid();\n            }\n\n            surface.render();\n\n            // A draw implies that either the axes or data have changed, so we\n            // should probably update the overlay highlights as well.\n\n            triggerRedrawOverlay();\n        }\n\n        function extractRange(ranges, coord) {\n            var axis, from, to, key, axes = allAxes();\n\n            for (var i = 0; i < axes.length; ++i) {\n                axis = axes[i];\n                if (axis.direction == coord) {\n                    key = coord + axis.n + \"axis\";\n                    if (!ranges[key] && axis.n == 1)\n                        key = coord + \"axis\"; // support x1axis as xaxis\n                    if (ranges[key]) {\n                        from = ranges[key].from;\n                        to = ranges[key].to;\n                        break;\n                    }\n                }\n            }\n\n            // backwards-compat stuff - to be removed in future\n            if (!ranges[key]) {\n                axis = coord == \"x\" ? xaxes[0] : yaxes[0];\n                from = ranges[coord + \"1\"];\n                to = ranges[coord + \"2\"];\n            }\n\n            // auto-reverse as an added bonus\n            if (from != null && to != null && from > to) {\n                var tmp = from;\n                from = to;\n                to = tmp;\n            }\n\n            return { from: from, to: to, axis: axis };\n        }\n\n        function drawBackground() {\n            ctx.save();\n            ctx.translate(plotOffset.left, plotOffset.top);\n\n            ctx.fillStyle = getColorOrGradient(options.grid.backgroundColor, plotHeight, 0, \"rgba(255, 255, 255, 0)\");\n            ctx.fillRect(0, 0, plotWidth, plotHeight);\n            ctx.restore();\n        }\n\n        function drawGrid() {\n            var i, axes, bw, bc;\n\n            ctx.save();\n            ctx.translate(plotOffset.left, plotOffset.top);\n\n            // draw markings\n            var markings = options.grid.markings;\n            if (markings) {\n                if ($.isFunction(markings)) {\n                    axes = plot.getAxes();\n                    // xmin etc. is backwards compatibility, to be\n                    // removed in the future\n                    axes.xmin = axes.xaxis.min;\n                    axes.xmax = axes.xaxis.max;\n                    axes.ymin = axes.yaxis.min;\n                    axes.ymax = axes.yaxis.max;\n\n                    markings = markings(axes);\n                }\n\n                for (i = 0; i < markings.length; ++i) {\n                    var m = markings[i],\n                        xrange = extractRange(m, \"x\"),\n                        yrange = extractRange(m, \"y\");\n\n                    // fill in missing\n                    if (xrange.from == null)\n                        xrange.from = xrange.axis.min;\n                    if (xrange.to == null)\n                        xrange.to = xrange.axis.max;\n                    if (yrange.from == null)\n                        yrange.from = yrange.axis.min;\n                    if (yrange.to == null)\n                        yrange.to = yrange.axis.max;\n\n                    // clip\n                    if (xrange.to < xrange.axis.min || xrange.from > xrange.axis.max ||\n                        yrange.to < yrange.axis.min || yrange.from > yrange.axis.max)\n                        continue;\n\n                    xrange.from = Math.max(xrange.from, xrange.axis.min);\n                    xrange.to = Math.min(xrange.to, xrange.axis.max);\n                    yrange.from = Math.max(yrange.from, yrange.axis.min);\n                    yrange.to = Math.min(yrange.to, yrange.axis.max);\n\n                    var xequal = xrange.from === xrange.to,\n                        yequal = yrange.from === yrange.to;\n\n                    if (xequal && yequal) {\n                        continue;\n                    }\n\n                    // then draw\n                    xrange.from = Math.floor(xrange.axis.p2c(xrange.from));\n                    xrange.to = Math.floor(xrange.axis.p2c(xrange.to));\n                    yrange.from = Math.floor(yrange.axis.p2c(yrange.from));\n                    yrange.to = Math.floor(yrange.axis.p2c(yrange.to));\n\n                    if (xequal || yequal) {\n                        var lineWidth = m.lineWidth || options.grid.markingsLineWidth,\n                            subPixel = lineWidth % 2 ? 0.5 : 0;\n                        ctx.beginPath();\n                        ctx.strokeStyle = m.color || options.grid.markingsColor;\n                        ctx.lineWidth = lineWidth;\n                        if (xequal) {\n                            ctx.moveTo(xrange.to + subPixel, yrange.from);\n                            ctx.lineTo(xrange.to + subPixel, yrange.to);\n                        } else {\n                            ctx.moveTo(xrange.from, yrange.to + subPixel);\n                            ctx.lineTo(xrange.to, yrange.to + subPixel);                            \n                        }\n                        ctx.stroke();\n                    } else {\n                        ctx.fillStyle = m.color || options.grid.markingsColor;\n                        ctx.fillRect(xrange.from, yrange.to,\n                                     xrange.to - xrange.from,\n                                     yrange.from - yrange.to);\n                    }\n                }\n            }\n\n            // draw the ticks\n            axes = allAxes();\n            bw = options.grid.borderWidth;\n\n            for (var j = 0; j < axes.length; ++j) {\n                var axis = axes[j], box = axis.box,\n                    t = axis.tickLength, x, y, xoff, yoff;\n                if (!axis.show || axis.ticks.length == 0)\n                    continue;\n\n                ctx.lineWidth = 1;\n\n                // find the edges\n                if (axis.direction == \"x\") {\n                    x = 0;\n                    if (t == \"full\")\n                        y = (axis.position == \"top\" ? 0 : plotHeight);\n                    else\n                        y = box.top - plotOffset.top + (axis.position == \"top\" ? box.height : 0);\n                }\n                else {\n                    y = 0;\n                    if (t == \"full\")\n                        x = (axis.position == \"left\" ? 0 : plotWidth);\n                    else\n                        x = box.left - plotOffset.left + (axis.position == \"left\" ? box.width : 0);\n                }\n\n                // draw tick bar\n                if (!axis.innermost) {\n                    ctx.strokeStyle = axis.options.color;\n                    ctx.beginPath();\n                    xoff = yoff = 0;\n                    if (axis.direction == \"x\")\n                        xoff = plotWidth + 1;\n                    else\n                        yoff = plotHeight + 1;\n\n                    if (ctx.lineWidth == 1) {\n                        if (axis.direction == \"x\") {\n                            y = Math.floor(y) + 0.5;\n                        } else {\n                            x = Math.floor(x) + 0.5;\n                        }\n                    }\n\n                    ctx.moveTo(x, y);\n                    ctx.lineTo(x + xoff, y + yoff);\n                    ctx.stroke();\n                }\n\n                // draw ticks\n\n                ctx.strokeStyle = axis.options.tickColor;\n\n                ctx.beginPath();\n                for (i = 0; i < axis.ticks.length; ++i) {\n                    var v = axis.ticks[i].v;\n\n                    xoff = yoff = 0;\n\n                    if (isNaN(v) || v < axis.min || v > axis.max\n                        // skip those lying on the axes if we got a border\n                        || (t == \"full\"\n                            && ((typeof bw == \"object\" && bw[axis.position] > 0) || bw > 0)\n                            && (v == axis.min || v == axis.max)))\n                        continue;\n\n                    if (axis.direction == \"x\") {\n                        x = axis.p2c(v);\n                        yoff = t == \"full\" ? -plotHeight : t;\n\n                        if (axis.position == \"top\")\n                            yoff = -yoff;\n                    }\n                    else {\n                        y = axis.p2c(v);\n                        xoff = t == \"full\" ? -plotWidth : t;\n\n                        if (axis.position == \"left\")\n                            xoff = -xoff;\n                    }\n\n                    if (ctx.lineWidth == 1) {\n                        if (axis.direction == \"x\")\n                            x = Math.floor(x) + 0.5;\n                        else\n                            y = Math.floor(y) + 0.5;\n                    }\n\n                    ctx.moveTo(x, y);\n                    ctx.lineTo(x + xoff, y + yoff);\n                }\n\n                ctx.stroke();\n            }\n\n\n            // draw border\n            if (bw) {\n                // If either borderWidth or borderColor is an object, then draw the border\n                // line by line instead of as one rectangle\n                bc = options.grid.borderColor;\n                if(typeof bw == \"object\" || typeof bc == \"object\") {\n                    if (typeof bw !== \"object\") {\n                        bw = {top: bw, right: bw, bottom: bw, left: bw};\n                    }\n                    if (typeof bc !== \"object\") {\n                        bc = {top: bc, right: bc, bottom: bc, left: bc};\n                    }\n\n                    if (bw.top > 0) {\n                        ctx.strokeStyle = bc.top;\n                        ctx.lineWidth = bw.top;\n                        ctx.beginPath();\n                        ctx.moveTo(0 - bw.left, 0 - bw.top/2);\n                        ctx.lineTo(plotWidth, 0 - bw.top/2);\n                        ctx.stroke();\n                    }\n\n                    if (bw.right > 0) {\n                        ctx.strokeStyle = bc.right;\n                        ctx.lineWidth = bw.right;\n                        ctx.beginPath();\n                        ctx.moveTo(plotWidth + bw.right / 2, 0 - bw.top);\n                        ctx.lineTo(plotWidth + bw.right / 2, plotHeight);\n                        ctx.stroke();\n                    }\n\n                    if (bw.bottom > 0) {\n                        ctx.strokeStyle = bc.bottom;\n                        ctx.lineWidth = bw.bottom;\n                        ctx.beginPath();\n                        ctx.moveTo(plotWidth + bw.right, plotHeight + bw.bottom / 2);\n                        ctx.lineTo(0, plotHeight + bw.bottom / 2);\n                        ctx.stroke();\n                    }\n\n                    if (bw.left > 0) {\n                        ctx.strokeStyle = bc.left;\n                        ctx.lineWidth = bw.left;\n                        ctx.beginPath();\n                        ctx.moveTo(0 - bw.left/2, plotHeight + bw.bottom);\n                        ctx.lineTo(0- bw.left/2, 0);\n                        ctx.stroke();\n                    }\n                }\n                else {\n                    ctx.lineWidth = bw;\n                    ctx.strokeStyle = options.grid.borderColor;\n                    ctx.strokeRect(-bw/2, -bw/2, plotWidth + bw, plotHeight + bw);\n                }\n            }\n\n            ctx.restore();\n        }\n\n        function drawAxisLabels() {\n\n            $.each(allAxes(), function (_, axis) {\n                var box = axis.box,\n                    legacyStyles = axis.direction + \"Axis \" + axis.direction + axis.n + \"Axis\",\n                    layer = \"flot-\" + axis.direction + \"-axis flot-\" + axis.direction + axis.n + \"-axis \" + legacyStyles,\n                    font = axis.options.font || \"flot-tick-label tickLabel\",\n                    tick, x, y, halign, valign;\n\n                // Remove text before checking for axis.show and ticks.length;\n                // otherwise plugins, like flot-tickrotor, that draw their own\n                // tick labels will end up with both theirs and the defaults.\n\n                surface.removeText(layer);\n\n                if (!axis.show || axis.ticks.length == 0)\n                    return;\n\n                for (var i = 0; i < axis.ticks.length; ++i) {\n\n                    tick = axis.ticks[i];\n                    if (!tick.label || tick.v < axis.min || tick.v > axis.max)\n                        continue;\n\n                    if (axis.direction == \"x\") {\n                        halign = \"center\";\n                        x = plotOffset.left + axis.p2c(tick.v);\n                        if (axis.position == \"bottom\") {\n                            y = box.top + box.padding;\n                        } else {\n                            y = box.top + box.height - box.padding;\n                            valign = \"bottom\";\n                        }\n                    } else {\n                        valign = \"middle\";\n                        y = plotOffset.top + axis.p2c(tick.v);\n                        if (axis.position == \"left\") {\n                            x = box.left + box.width - box.padding;\n                            halign = \"right\";\n                        } else {\n                            x = box.left + box.padding;\n                        }\n                    }\n\n                    surface.addText(layer, x, y, tick.label, font, null, null, halign, valign);\n                }\n            });\n        }\n\n        function drawSeries(series) {\n            if (series.lines.show)\n                drawSeriesLines(series);\n            if (series.bars.show)\n                drawSeriesBars(series);\n            if (series.points.show)\n                drawSeriesPoints(series);\n        }\n\n        function drawSeriesLines(series) {\n            function plotLine(datapoints, xoffset, yoffset, axisx, axisy) {\n                var points = datapoints.points,\n                    ps = datapoints.pointsize,\n                    prevx = null, prevy = null;\n\n                ctx.beginPath();\n                for (var i = ps; i < points.length; i += ps) {\n                    var x1 = points[i - ps], y1 = points[i - ps + 1],\n                        x2 = points[i], y2 = points[i + 1];\n\n                    if (x1 == null || x2 == null)\n                        continue;\n\n                    // clip with ymin\n                    if (y1 <= y2 && y1 < axisy.min) {\n                        if (y2 < axisy.min)\n                            continue;   // line segment is outside\n                        // compute new intersection point\n                        x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;\n                        y1 = axisy.min;\n                    }\n                    else if (y2 <= y1 && y2 < axisy.min) {\n                        if (y1 < axisy.min)\n                            continue;\n                        x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;\n                        y2 = axisy.min;\n                    }\n\n                    // clip with ymax\n                    if (y1 >= y2 && y1 > axisy.max) {\n                        if (y2 > axisy.max)\n                            continue;\n                        x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;\n                        y1 = axisy.max;\n                    }\n                    else if (y2 >= y1 && y2 > axisy.max) {\n                        if (y1 > axisy.max)\n                            continue;\n                        x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;\n                        y2 = axisy.max;\n                    }\n\n                    // clip with xmin\n                    if (x1 <= x2 && x1 < axisx.min) {\n                        if (x2 < axisx.min)\n                            continue;\n                        y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;\n                        x1 = axisx.min;\n                    }\n                    else if (x2 <= x1 && x2 < axisx.min) {\n                        if (x1 < axisx.min)\n                            continue;\n                        y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;\n                        x2 = axisx.min;\n                    }\n\n                    // clip with xmax\n                    if (x1 >= x2 && x1 > axisx.max) {\n                        if (x2 > axisx.max)\n                            continue;\n                        y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;\n                        x1 = axisx.max;\n                    }\n                    else if (x2 >= x1 && x2 > axisx.max) {\n                        if (x1 > axisx.max)\n                            continue;\n                        y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;\n                        x2 = axisx.max;\n                    }\n\n                    if (x1 != prevx || y1 != prevy)\n                        ctx.moveTo(axisx.p2c(x1) + xoffset, axisy.p2c(y1) + yoffset);\n\n                    prevx = x2;\n                    prevy = y2;\n                    ctx.lineTo(axisx.p2c(x2) + xoffset, axisy.p2c(y2) + yoffset);\n                }\n                ctx.stroke();\n            }\n\n            function plotLineArea(datapoints, axisx, axisy) {\n                var points = datapoints.points,\n                    ps = datapoints.pointsize,\n                    bottom = Math.min(Math.max(0, axisy.min), axisy.max),\n                    i = 0, top, areaOpen = false,\n                    ypos = 1, segmentStart = 0, segmentEnd = 0;\n\n                // we process each segment in two turns, first forward\n                // direction to sketch out top, then once we hit the\n                // end we go backwards to sketch the bottom\n                while (true) {\n                    if (ps > 0 && i > points.length + ps)\n                        break;\n\n                    i += ps; // ps is negative if going backwards\n\n                    var x1 = points[i - ps],\n                        y1 = points[i - ps + ypos],\n                        x2 = points[i], y2 = points[i + ypos];\n\n                    if (areaOpen) {\n                        if (ps > 0 && x1 != null && x2 == null) {\n                            // at turning point\n                            segmentEnd = i;\n                            ps = -ps;\n                            ypos = 2;\n                            continue;\n                        }\n\n                        if (ps < 0 && i == segmentStart + ps) {\n                            // done with the reverse sweep\n                            ctx.fill();\n                            areaOpen = false;\n                            ps = -ps;\n                            ypos = 1;\n                            i = segmentStart = segmentEnd + ps;\n                            continue;\n                        }\n                    }\n\n                    if (x1 == null || x2 == null)\n                        continue;\n\n                    // clip x values\n\n                    // clip with xmin\n                    if (x1 <= x2 && x1 < axisx.min) {\n                        if (x2 < axisx.min)\n                            continue;\n                        y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;\n                        x1 = axisx.min;\n                    }\n                    else if (x2 <= x1 && x2 < axisx.min) {\n                        if (x1 < axisx.min)\n                            continue;\n                        y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;\n                        x2 = axisx.min;\n                    }\n\n                    // clip with xmax\n                    if (x1 >= x2 && x1 > axisx.max) {\n                        if (x2 > axisx.max)\n                            continue;\n                        y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;\n                        x1 = axisx.max;\n                    }\n                    else if (x2 >= x1 && x2 > axisx.max) {\n                        if (x1 > axisx.max)\n                            continue;\n                        y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;\n                        x2 = axisx.max;\n                    }\n\n                    if (!areaOpen) {\n                        // open area\n                        ctx.beginPath();\n                        ctx.moveTo(axisx.p2c(x1), axisy.p2c(bottom));\n                        areaOpen = true;\n                    }\n\n                    // now first check the case where both is outside\n                    if (y1 >= axisy.max && y2 >= axisy.max) {\n                        ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.max));\n                        ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.max));\n                        continue;\n                    }\n                    else if (y1 <= axisy.min && y2 <= axisy.min) {\n                        ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.min));\n                        ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.min));\n                        continue;\n                    }\n\n                    // else it's a bit more complicated, there might\n                    // be a flat maxed out rectangle first, then a\n                    // triangular cutout or reverse; to find these\n                    // keep track of the current x values\n                    var x1old = x1, x2old = x2;\n\n                    // clip the y values, without shortcutting, we\n                    // go through all cases in turn\n\n                    // clip with ymin\n                    if (y1 <= y2 && y1 < axisy.min && y2 >= axisy.min) {\n                        x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;\n                        y1 = axisy.min;\n                    }\n                    else if (y2 <= y1 && y2 < axisy.min && y1 >= axisy.min) {\n                        x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;\n                        y2 = axisy.min;\n                    }\n\n                    // clip with ymax\n                    if (y1 >= y2 && y1 > axisy.max && y2 <= axisy.max) {\n                        x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;\n                        y1 = axisy.max;\n                    }\n                    else if (y2 >= y1 && y2 > axisy.max && y1 <= axisy.max) {\n                        x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;\n                        y2 = axisy.max;\n                    }\n\n                    // if the x value was changed we got a rectangle\n                    // to fill\n                    if (x1 != x1old) {\n                        ctx.lineTo(axisx.p2c(x1old), axisy.p2c(y1));\n                        // it goes to (x1, y1), but we fill that below\n                    }\n\n                    // fill triangular section, this sometimes result\n                    // in redundant points if (x1, y1) hasn't changed\n                    // from previous line to, but we just ignore that\n                    ctx.lineTo(axisx.p2c(x1), axisy.p2c(y1));\n                    ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2));\n\n                    // fill the other rectangle if it's there\n                    if (x2 != x2old) {\n                        ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2));\n                        ctx.lineTo(axisx.p2c(x2old), axisy.p2c(y2));\n                    }\n                }\n            }\n\n            ctx.save();\n            ctx.translate(plotOffset.left, plotOffset.top);\n            ctx.lineJoin = \"round\";\n\n            var lw = series.lines.lineWidth,\n                sw = series.shadowSize;\n            // FIXME: consider another form of shadow when filling is turned on\n            if (lw > 0 && sw > 0) {\n                // draw shadow as a thick and thin line with transparency\n                ctx.lineWidth = sw;\n                ctx.strokeStyle = \"rgba(0,0,0,0.1)\";\n                // position shadow at angle from the mid of line\n                var angle = Math.PI/18;\n                plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/2), Math.cos(angle) * (lw/2 + sw/2), series.xaxis, series.yaxis);\n                ctx.lineWidth = sw/2;\n                plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/4), Math.cos(angle) * (lw/2 + sw/4), series.xaxis, series.yaxis);\n            }\n\n            ctx.lineWidth = lw;\n            ctx.strokeStyle = series.color;\n            var fillStyle = getFillStyle(series.lines, series.color, 0, plotHeight);\n            if (fillStyle) {\n                ctx.fillStyle = fillStyle;\n                plotLineArea(series.datapoints, series.xaxis, series.yaxis);\n            }\n\n            if (lw > 0)\n                plotLine(series.datapoints, 0, 0, series.xaxis, series.yaxis);\n            ctx.restore();\n        }\n\n        function drawSeriesPoints(series) {\n            function plotPoints(datapoints, radius, fillStyle, offset, shadow, axisx, axisy, symbol) {\n                var points = datapoints.points, ps = datapoints.pointsize;\n\n                for (var i = 0; i < points.length; i += ps) {\n                    var x = points[i], y = points[i + 1];\n                    if (x == null || x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max)\n                        continue;\n\n                    ctx.beginPath();\n                    x = axisx.p2c(x);\n                    y = axisy.p2c(y) + offset;\n                    if (symbol == \"circle\")\n                        ctx.arc(x, y, radius, 0, shadow ? Math.PI : Math.PI * 2, false);\n                    else\n                        symbol(ctx, x, y, radius, shadow);\n                    ctx.closePath();\n\n                    if (fillStyle) {\n                        ctx.fillStyle = fillStyle;\n                        ctx.fill();\n                    }\n                    ctx.stroke();\n                }\n            }\n\n            ctx.save();\n            ctx.translate(plotOffset.left, plotOffset.top);\n\n            var lw = series.points.lineWidth,\n                sw = series.shadowSize,\n                radius = series.points.radius,\n                symbol = series.points.symbol;\n\n            // If the user sets the line width to 0, we change it to a very \n            // small value. A line width of 0 seems to force the default of 1.\n            // Doing the conditional here allows the shadow setting to still be \n            // optional even with a lineWidth of 0.\n\n            if( lw == 0 )\n                lw = 0.0001;\n\n            if (lw > 0 && sw > 0) {\n                // draw shadow in two steps\n                var w = sw / 2;\n                ctx.lineWidth = w;\n                ctx.strokeStyle = \"rgba(0,0,0,0.1)\";\n                plotPoints(series.datapoints, radius, null, w + w/2, true,\n                           series.xaxis, series.yaxis, symbol);\n\n                ctx.strokeStyle = \"rgba(0,0,0,0.2)\";\n                plotPoints(series.datapoints, radius, null, w/2, true,\n                           series.xaxis, series.yaxis, symbol);\n            }\n\n            ctx.lineWidth = lw;\n            ctx.strokeStyle = series.color;\n            plotPoints(series.datapoints, radius,\n                       getFillStyle(series.points, series.color), 0, false,\n                       series.xaxis, series.yaxis, symbol);\n            ctx.restore();\n        }\n\n        function drawBar(x, y, b, barLeft, barRight, fillStyleCallback, axisx, axisy, c, horizontal, lineWidth) {\n            var left, right, bottom, top,\n                drawLeft, drawRight, drawTop, drawBottom,\n                tmp;\n\n            // in horizontal mode, we start the bar from the left\n            // instead of from the bottom so it appears to be\n            // horizontal rather than vertical\n            if (horizontal) {\n                drawBottom = drawRight = drawTop = true;\n                drawLeft = false;\n                left = b;\n                right = x;\n                top = y + barLeft;\n                bottom = y + barRight;\n\n                // account for negative bars\n                if (right < left) {\n                    tmp = right;\n                    right = left;\n                    left = tmp;\n                    drawLeft = true;\n                    drawRight = false;\n                }\n            }\n            else {\n                drawLeft = drawRight = drawTop = true;\n                drawBottom = false;\n                left = x + barLeft;\n                right = x + barRight;\n                bottom = b;\n                top = y;\n\n                // account for negative bars\n                if (top < bottom) {\n                    tmp = top;\n                    top = bottom;\n                    bottom = tmp;\n                    drawBottom = true;\n                    drawTop = false;\n                }\n            }\n\n            // clip\n            if (right < axisx.min || left > axisx.max ||\n                top < axisy.min || bottom > axisy.max)\n                return;\n\n            if (left < axisx.min) {\n                left = axisx.min;\n                drawLeft = false;\n            }\n\n            if (right > axisx.max) {\n                right = axisx.max;\n                drawRight = false;\n            }\n\n            if (bottom < axisy.min) {\n                bottom = axisy.min;\n                drawBottom = false;\n            }\n\n            if (top > axisy.max) {\n                top = axisy.max;\n                drawTop = false;\n            }\n\n            left = axisx.p2c(left);\n            bottom = axisy.p2c(bottom);\n            right = axisx.p2c(right);\n            top = axisy.p2c(top);\n\n            // fill the bar\n            if (fillStyleCallback) {\n                c.fillStyle = fillStyleCallback(bottom, top);\n                c.fillRect(left, top, right - left, bottom - top)\n            }\n\n            // draw outline\n            if (lineWidth > 0 && (drawLeft || drawRight || drawTop || drawBottom)) {\n                c.beginPath();\n\n                // FIXME: inline moveTo is buggy with excanvas\n                c.moveTo(left, bottom);\n                if (drawLeft)\n                    c.lineTo(left, top);\n                else\n                    c.moveTo(left, top);\n                if (drawTop)\n                    c.lineTo(right, top);\n                else\n                    c.moveTo(right, top);\n                if (drawRight)\n                    c.lineTo(right, bottom);\n                else\n                    c.moveTo(right, bottom);\n                if (drawBottom)\n                    c.lineTo(left, bottom);\n                else\n                    c.moveTo(left, bottom);\n                c.stroke();\n            }\n        }\n\n        function drawSeriesBars(series) {\n            function plotBars(datapoints, barLeft, barRight, fillStyleCallback, axisx, axisy) {\n                var points = datapoints.points, ps = datapoints.pointsize;\n\n                for (var i = 0; i < points.length; i += ps) {\n                    if (points[i] == null)\n                        continue;\n                    drawBar(points[i], points[i + 1], points[i + 2], barLeft, barRight, fillStyleCallback, axisx, axisy, ctx, series.bars.horizontal, series.bars.lineWidth);\n                }\n            }\n\n            ctx.save();\n            ctx.translate(plotOffset.left, plotOffset.top);\n\n            // FIXME: figure out a way to add shadows (for instance along the right edge)\n            ctx.lineWidth = series.bars.lineWidth;\n            ctx.strokeStyle = series.color;\n\n            var barLeft;\n\n            switch (series.bars.align) {\n                case \"left\":\n                    barLeft = 0;\n                    break;\n                case \"right\":\n                    barLeft = -series.bars.barWidth;\n                    break;\n                default:\n                    barLeft = -series.bars.barWidth / 2;\n            }\n\n            var fillStyleCallback = series.bars.fill ? function (bottom, top) { return getFillStyle(series.bars, series.color, bottom, top); } : null;\n            plotBars(series.datapoints, barLeft, barLeft + series.bars.barWidth, fillStyleCallback, series.xaxis, series.yaxis);\n            ctx.restore();\n        }\n\n        function getFillStyle(filloptions, seriesColor, bottom, top) {\n            var fill = filloptions.fill;\n            if (!fill)\n                return null;\n\n            if (filloptions.fillColor)\n                return getColorOrGradient(filloptions.fillColor, bottom, top, seriesColor);\n\n            var c = $.color.parse(seriesColor);\n            c.a = typeof fill == \"number\" ? fill : 0.4;\n            c.normalize();\n            return c.toString();\n        }\n\n        function insertLegend() {\n\n            if (options.legend.container != null) {\n                $(options.legend.container).html(\"\");\n            } else {\n                placeholder.find(\".legend\").remove();\n            }\n\n            if (!options.legend.show) {\n                return;\n            }\n\n            var fragments = [], entries = [], rowStarted = false,\n                lf = options.legend.labelFormatter, s, label;\n\n            // Build a list of legend entries, with each having a label and a color\n\n            for (var i = 0; i < series.length; ++i) {\n                s = series[i];\n                if (s.label) {\n                    label = lf ? lf(s.label, s) : s.label;\n                    if (label) {\n                        entries.push({\n                            label: label,\n                            color: s.color\n                        });\n                    }\n                }\n            }\n\n            // Sort the legend using either the default or a custom comparator\n\n            if (options.legend.sorted) {\n                if ($.isFunction(options.legend.sorted)) {\n                    entries.sort(options.legend.sorted);\n                } else if (options.legend.sorted == \"reverse\") {\n                \tentries.reverse();\n                } else {\n                    var ascending = options.legend.sorted != \"descending\";\n                    entries.sort(function(a, b) {\n                        return a.label == b.label ? 0 : (\n                            (a.label < b.label) != ascending ? 1 : -1   // Logical XOR\n                        );\n                    });\n                }\n            }\n\n            // Generate markup for the list of entries, in their final order\n\n            for (var i = 0; i < entries.length; ++i) {\n\n                var entry = entries[i];\n\n                if (i % options.legend.noColumns == 0) {\n                    if (rowStarted)\n                        fragments.push('</tr>');\n                    fragments.push('<tr>');\n                    rowStarted = true;\n                }\n\n                fragments.push(\n                    '<td class=\"legendColorBox\"><div style=\"border:1px solid ' + options.legend.labelBoxBorderColor + ';padding:1px\"><div style=\"width:4px;height:0;border:5px solid ' + entry.color + ';overflow:hidden\"></div></div></td>' +\n                    '<td class=\"legendLabel\">' + entry.label + '</td>'\n                );\n            }\n\n            if (rowStarted)\n                fragments.push('</tr>');\n\n            if (fragments.length == 0)\n                return;\n\n            var table = '<table style=\"font-size:smaller;color:' + options.grid.color + '\">' + fragments.join(\"\") + '</table>';\n            if (options.legend.container != null)\n                $(options.legend.container).html(table);\n            else {\n                var pos = \"\",\n                    p = options.legend.position,\n                    m = options.legend.margin;\n                if (m[0] == null)\n                    m = [m, m];\n                if (p.charAt(0) == \"n\")\n                    pos += 'top:' + (m[1] + plotOffset.top) + 'px;';\n                else if (p.charAt(0) == \"s\")\n                    pos += 'bottom:' + (m[1] + plotOffset.bottom) + 'px;';\n                if (p.charAt(1) == \"e\")\n                    pos += 'right:' + (m[0] + plotOffset.right) + 'px;';\n                else if (p.charAt(1) == \"w\")\n                    pos += 'left:' + (m[0] + plotOffset.left) + 'px;';\n                var legend = $('<div class=\"legend\">' + table.replace('style=\"', 'style=\"position:absolute;' + pos +';') + '</div>').appendTo(placeholder);\n                if (options.legend.backgroundOpacity != 0.0) {\n                    // put in the transparent background\n                    // separately to avoid blended labels and\n                    // label boxes\n                    var c = options.legend.backgroundColor;\n                    if (c == null) {\n                        c = options.grid.backgroundColor;\n                        if (c && typeof c == \"string\")\n                            c = $.color.parse(c);\n                        else\n                            c = $.color.extract(legend, 'background-color');\n                        c.a = 1;\n                        c = c.toString();\n                    }\n                    var div = legend.children();\n                    $('<div style=\"position:absolute;width:' + div.width() + 'px;height:' + div.height() + 'px;' + pos +'background-color:' + c + ';\"> </div>').prependTo(legend).css('opacity', options.legend.backgroundOpacity);\n                }\n            }\n        }\n\n\n        // interactive features\n\n        var highlights = [],\n            redrawTimeout = null;\n\n        // returns the data item the mouse is over, or null if none is found\n        function findNearbyItem(mouseX, mouseY, seriesFilter) {\n            var maxDistance = options.grid.mouseActiveRadius,\n                smallestDistance = maxDistance * maxDistance + 1,\n                item = null, foundPoint = false, i, j, ps;\n\n            for (i = series.length - 1; i >= 0; --i) {\n                if (!seriesFilter(series[i]))\n                    continue;\n\n                var s = series[i],\n                    axisx = s.xaxis,\n                    axisy = s.yaxis,\n                    points = s.datapoints.points,\n                    mx = axisx.c2p(mouseX), // precompute some stuff to make the loop faster\n                    my = axisy.c2p(mouseY),\n                    maxx = maxDistance / axisx.scale,\n                    maxy = maxDistance / axisy.scale;\n\n                ps = s.datapoints.pointsize;\n                // with inverse transforms, we can't use the maxx/maxy\n                // optimization, sadly\n                if (axisx.options.inverseTransform)\n                    maxx = Number.MAX_VALUE;\n                if (axisy.options.inverseTransform)\n                    maxy = Number.MAX_VALUE;\n\n                if (s.lines.show || s.points.show) {\n                    for (j = 0; j < points.length; j += ps) {\n                        var x = points[j], y = points[j + 1];\n                        if (x == null)\n                            continue;\n\n                        // For points and lines, the cursor must be within a\n                        // certain distance to the data point\n                        if (x - mx > maxx || x - mx < -maxx ||\n                            y - my > maxy || y - my < -maxy)\n                            continue;\n\n                        // We have to calculate distances in pixels, not in\n                        // data units, because the scales of the axes may be different\n                        var dx = Math.abs(axisx.p2c(x) - mouseX),\n                            dy = Math.abs(axisy.p2c(y) - mouseY),\n                            dist = dx * dx + dy * dy; // we save the sqrt\n\n                        // use <= to ensure last point takes precedence\n                        // (last generally means on top of)\n                        if (dist < smallestDistance) {\n                            smallestDistance = dist;\n                            item = [i, j / ps];\n                        }\n                    }\n                }\n\n                if (s.bars.show && !item) { // no other point can be nearby\n\n                    var barLeft, barRight;\n\n                    switch (s.bars.align) {\n                        case \"left\":\n                            barLeft = 0;\n                            break;\n                        case \"right\":\n                            barLeft = -s.bars.barWidth;\n                            break;\n                        default:\n                            barLeft = -s.bars.barWidth / 2;\n                    }\n\n                    barRight = barLeft + s.bars.barWidth;\n\n                    for (j = 0; j < points.length; j += ps) {\n                        var x = points[j], y = points[j + 1], b = points[j + 2];\n                        if (x == null)\n                            continue;\n\n                        // for a bar graph, the cursor must be inside the bar\n                        if (series[i].bars.horizontal ?\n                            (mx <= Math.max(b, x) && mx >= Math.min(b, x) &&\n                             my >= y + barLeft && my <= y + barRight) :\n                            (mx >= x + barLeft && mx <= x + barRight &&\n                             my >= Math.min(b, y) && my <= Math.max(b, y)))\n                                item = [i, j / ps];\n                    }\n                }\n            }\n\n            if (item) {\n                i = item[0];\n                j = item[1];\n                ps = series[i].datapoints.pointsize;\n\n                return { datapoint: series[i].datapoints.points.slice(j * ps, (j + 1) * ps),\n                         dataIndex: j,\n                         series: series[i],\n                         seriesIndex: i };\n            }\n\n            return null;\n        }\n\n        function onMouseMove(e) {\n            if (options.grid.hoverable)\n                triggerClickHoverEvent(\"plothover\", e,\n                                       function (s) { return s[\"hoverable\"] != false; });\n        }\n\n        function onMouseLeave(e) {\n            if (options.grid.hoverable)\n                triggerClickHoverEvent(\"plothover\", e,\n                                       function (s) { return false; });\n        }\n\n        function onClick(e) {\n            triggerClickHoverEvent(\"plotclick\", e,\n                                   function (s) { return s[\"clickable\"] != false; });\n        }\n\n        // trigger click or hover event (they send the same parameters\n        // so we share their code)\n        function triggerClickHoverEvent(eventname, event, seriesFilter) {\n            var offset = eventHolder.offset(),\n                canvasX = event.pageX - offset.left - plotOffset.left,\n                canvasY = event.pageY - offset.top - plotOffset.top,\n            pos = canvasToAxisCoords({ left: canvasX, top: canvasY });\n\n            pos.pageX = event.pageX;\n            pos.pageY = event.pageY;\n\n            var item = findNearbyItem(canvasX, canvasY, seriesFilter);\n\n            if (item) {\n                // fill in mouse pos for any listeners out there\n                item.pageX = parseInt(item.series.xaxis.p2c(item.datapoint[0]) + offset.left + plotOffset.left, 10);\n                item.pageY = parseInt(item.series.yaxis.p2c(item.datapoint[1]) + offset.top + plotOffset.top, 10);\n            }\n\n            if (options.grid.autoHighlight) {\n                // clear auto-highlights\n                for (var i = 0; i < highlights.length; ++i) {\n                    var h = highlights[i];\n                    if (h.auto == eventname &&\n                        !(item && h.series == item.series &&\n                          h.point[0] == item.datapoint[0] &&\n                          h.point[1] == item.datapoint[1]))\n                        unhighlight(h.series, h.point);\n                }\n\n                if (item)\n                    highlight(item.series, item.datapoint, eventname);\n            }\n\n            placeholder.trigger(eventname, [ pos, item ]);\n        }\n\n        function triggerRedrawOverlay() {\n            var t = options.interaction.redrawOverlayInterval;\n            if (t == -1) {      // skip event queue\n                drawOverlay();\n                return;\n            }\n\n            if (!redrawTimeout)\n                redrawTimeout = setTimeout(drawOverlay, t);\n        }\n\n        function drawOverlay() {\n            redrawTimeout = null;\n\n            // draw highlights\n            octx.save();\n            overlay.clear();\n            octx.translate(plotOffset.left, plotOffset.top);\n\n            var i, hi;\n            for (i = 0; i < highlights.length; ++i) {\n                hi = highlights[i];\n\n                if (hi.series.bars.show)\n                    drawBarHighlight(hi.series, hi.point);\n                else\n                    drawPointHighlight(hi.series, hi.point);\n            }\n            octx.restore();\n\n            executeHooks(hooks.drawOverlay, [octx]);\n        }\n\n        function highlight(s, point, auto) {\n            if (typeof s == \"number\")\n                s = series[s];\n\n            if (typeof point == \"number\") {\n                var ps = s.datapoints.pointsize;\n                point = s.datapoints.points.slice(ps * point, ps * (point + 1));\n            }\n\n            var i = indexOfHighlight(s, point);\n            if (i == -1) {\n                highlights.push({ series: s, point: point, auto: auto });\n\n                triggerRedrawOverlay();\n            }\n            else if (!auto)\n                highlights[i].auto = false;\n        }\n\n        function unhighlight(s, point) {\n            if (s == null && point == null) {\n                highlights = [];\n                triggerRedrawOverlay();\n                return;\n            }\n\n            if (typeof s == \"number\")\n                s = series[s];\n\n            if (typeof point == \"number\") {\n                var ps = s.datapoints.pointsize;\n                point = s.datapoints.points.slice(ps * point, ps * (point + 1));\n            }\n\n            var i = indexOfHighlight(s, point);\n            if (i != -1) {\n                highlights.splice(i, 1);\n\n                triggerRedrawOverlay();\n            }\n        }\n\n        function indexOfHighlight(s, p) {\n            for (var i = 0; i < highlights.length; ++i) {\n                var h = highlights[i];\n                if (h.series == s && h.point[0] == p[0]\n                    && h.point[1] == p[1])\n                    return i;\n            }\n            return -1;\n        }\n\n        function drawPointHighlight(series, point) {\n            var x = point[0], y = point[1],\n                axisx = series.xaxis, axisy = series.yaxis,\n                highlightColor = (typeof series.highlightColor === \"string\") ? series.highlightColor : $.color.parse(series.color).scale('a', 0.5).toString();\n\n            if (x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max)\n                return;\n\n            var pointRadius = series.points.radius + series.points.lineWidth / 2;\n            octx.lineWidth = pointRadius;\n            octx.strokeStyle = highlightColor;\n            var radius = 1.5 * pointRadius;\n            x = axisx.p2c(x);\n            y = axisy.p2c(y);\n\n            octx.beginPath();\n            if (series.points.symbol == \"circle\")\n                octx.arc(x, y, radius, 0, 2 * Math.PI, false);\n            else\n                series.points.symbol(octx, x, y, radius, false);\n            octx.closePath();\n            octx.stroke();\n        }\n\n        function drawBarHighlight(series, point) {\n            var highlightColor = (typeof series.highlightColor === \"string\") ? series.highlightColor : $.color.parse(series.color).scale('a', 0.5).toString(),\n                fillStyle = highlightColor,\n                barLeft;\n\n            switch (series.bars.align) {\n                case \"left\":\n                    barLeft = 0;\n                    break;\n                case \"right\":\n                    barLeft = -series.bars.barWidth;\n                    break;\n                default:\n                    barLeft = -series.bars.barWidth / 2;\n            }\n\n            octx.lineWidth = series.bars.lineWidth;\n            octx.strokeStyle = highlightColor;\n\n            drawBar(point[0], point[1], point[2] || 0, barLeft, barLeft + series.bars.barWidth,\n                    function () { return fillStyle; }, series.xaxis, series.yaxis, octx, series.bars.horizontal, series.bars.lineWidth);\n        }\n\n        function getColorOrGradient(spec, bottom, top, defaultColor) {\n            if (typeof spec == \"string\")\n                return spec;\n            else {\n                // assume this is a gradient spec; IE currently only\n                // supports a simple vertical gradient properly, so that's\n                // what we support too\n                var gradient = ctx.createLinearGradient(0, top, 0, bottom);\n\n                for (var i = 0, l = spec.colors.length; i < l; ++i) {\n                    var c = spec.colors[i];\n                    if (typeof c != \"string\") {\n                        var co = $.color.parse(defaultColor);\n                        if (c.brightness != null)\n                            co = co.scale('rgb', c.brightness);\n                        if (c.opacity != null)\n                            co.a *= c.opacity;\n                        c = co.toString();\n                    }\n                    gradient.addColorStop(i / (l - 1), c);\n                }\n\n                return gradient;\n            }\n        }\n    }\n\n    // Add the plot function to the top level of the jQuery object\n\n    $.plot = function(placeholder, data, options) {\n        //var t0 = new Date();\n        var plot = new Plot($(placeholder), data, options, $.plot.plugins);\n        //(window.console ? console.log : alert)(\"time used (msecs): \" + ((new Date()).getTime() - t0.getTime()));\n        return plot;\n    };\n\n    $.plot.version = \"0.8.3\";\n\n    $.plot.plugins = [];\n\n    // Also add the plot function as a chainable property\n\n    $.fn.plot = function(data, options) {\n        return this.each(function() {\n            $.plot(this, data, options);\n        });\n    };\n\n    // round to nearby lower multiple of base\n    function floorInBase(n, base) {\n        return base * Math.floor(n / base);\n    }\n\n})(jQuery);\n"
  },
  {
    "path": "r2/r2/public/static/js/lib/jquery.flot.time.js",
    "content": "/*\nPretty handling of time axes.\n\nSet axis.mode to \"time\" to enable. See the section \"Time series data\" in API.txt\nfor details.\n*/\n(function ($) {\n    var options = {};\n\n    // round to nearby lower multiple of base\n    function floorInBase(n, base) {\n        return base * Math.floor(n / base);\n    }\n\n    // Returns a string with the date d formatted according to fmt.\n    // A subset of the Open Group's strftime format is supported.\n    function formatDate(d, fmt, monthNames, dayNames) {\n        if (typeof d.strftime == \"function\") {\n            return d.strftime(fmt);\n        }\n        var leftPad = function(n, pad) {\n            n = \"\" + n;\n            pad = \"\" + (pad == null ? \"0\" : pad);\n            return n.length == 1 ? pad + n : n;\n        };\n        \n        var r = [];\n        var escape = false;\n        var hours = d.getHours();\n        var isAM = hours < 12;\n        if (monthNames == null)\n            monthNames = [\"Jan\", \"Feb\", \"Mar\", \"Apr\", \"May\", \"Jun\", \"Jul\", \"Aug\", \"Sep\", \"Oct\", \"Nov\", \"Dec\"];\n        if (dayNames == null)\n            dayNames = [\"Sun\", \"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\"];\n\n        var hours12;\n        if (hours > 12) {\n            hours12 = hours - 12;\n        } else if (hours == 0) {\n            hours12 = 12;\n        } else {\n            hours12 = hours;\n        }\n\n        for (var i = 0; i < fmt.length; ++i) {\n            var c = fmt.charAt(i);\n            \n            if (escape) {\n                switch (c) {\n                case 'a': c = \"\" + dayNames[d.getDay()]; break;\n                case 'b': c = \"\" + monthNames[d.getMonth()]; break;\n                case 'd': c = leftPad(d.getDate()); break;\n                case 'e': c = leftPad(d.getDate(), \" \"); break;\n                case 'H': c = leftPad(hours); break;\n                case 'I': c = leftPad(hours12); break;\n                case 'l': c = leftPad(hours12, \" \"); break;\n                case 'm': c = leftPad(d.getMonth() + 1); break;\n                case 'M': c = leftPad(d.getMinutes()); break;\n                case 'S': c = leftPad(d.getSeconds()); break;\n                case 'y': c = leftPad(d.getFullYear() % 100); break;\n                case 'Y': c = \"\" + d.getFullYear(); break;\n                case 'p': c = (isAM) ? (\"\" + \"am\") : (\"\" + \"pm\"); break;\n                case 'P': c = (isAM) ? (\"\" + \"AM\") : (\"\" + \"PM\"); break;\n                case 'w': c = \"\" + d.getDay(); break;\n                }\n                r.push(c);\n                escape = false;\n            }\n            else {\n                if (c == \"%\")\n                    escape = true;\n                else\n                    r.push(c);\n            }\n        }\n        return r.join(\"\");\n    }\n\n    // To have a consistent view of time-based data independent of which time\n    // zone the client happens to be in we need a date-like object independent\n    // of time zones.  This is done through a wrapper that only calls the UTC\n    // versions of the accessor methods.\n    function makeUtcWrapper(d) {\n        function addProxyMethod(sourceObj, sourceMethod, targetObj,\n                                targetMethod) {\n            sourceObj[sourceMethod] = function() {\n                return targetObj[targetMethod].apply(targetObj, arguments);\n            };\n        };\n        var utc = {\n            date: d\n        };\n        // support strftime, if found\n        if (d.strftime != undefined)\n            addProxyMethod(utc, \"strftime\", d, \"strftime\");\n        addProxyMethod(utc, \"getTime\", d, \"getTime\");\n        addProxyMethod(utc, \"setTime\", d, \"setTime\");\n        var props = [ \"Date\", \"Day\", \"FullYear\", \"Hours\", \"Milliseconds\", \"Minutes\", \"Month\", \"Seconds\" ];\n        for (var p = 0; p < props.length; p++) {\n            addProxyMethod(utc, \"get\" + props[p], d, \"getUTC\" + props[p]);\n            addProxyMethod(utc, \"set\" + props[p], d, \"setUTC\" + props[p]);\n        }\n        return utc;\n    };\n\n    // select time zone strategy.  This returns a date-like object tied to the\n    // desired timezone\n    function dateGenerator(ts, opts) {\n        if (opts.timezone == \"browser\") {\n            return new Date(ts);\n        } else if (!opts.timezone || opts.timezone == \"utc\") {\n            return makeUtcWrapper(new Date(ts));\n        } else if (typeof timezoneJS != \"undefined\" && typeof timezoneJS.Date != \"undefined\") {\n            var d = new timezoneJS.Date();\n            // timezone-js is fickle, so be sure to set the time zone before\n            // setting the time.\n            d.setTimezone(opts.timezone);\n            d.setTime(ts);\n            return d;\n        } else {\n            return makeUtcWrapper(new Date(ts));\n        }\n    }\n    \n    // map of app. size of time units in milliseconds\n    var timeUnitSize = {\n        \"second\": 1000,\n        \"minute\": 60 * 1000,\n        \"hour\": 60 * 60 * 1000,\n        \"day\": 24 * 60 * 60 * 1000,\n        \"month\": 30 * 24 * 60 * 60 * 1000,\n        \"year\": 365.2425 * 24 * 60 * 60 * 1000\n    };\n\n    // the allowed tick sizes, after 1 year we use\n    // an integer algorithm\n    var spec = [\n        [1, \"second\"], [2, \"second\"], [5, \"second\"], [10, \"second\"],\n        [30, \"second\"], \n        [1, \"minute\"], [2, \"minute\"], [5, \"minute\"], [10, \"minute\"],\n        [30, \"minute\"], \n        [1, \"hour\"], [2, \"hour\"], [4, \"hour\"],\n        [8, \"hour\"], [12, \"hour\"],\n        [1, \"day\"], [2, \"day\"], [3, \"day\"],\n        [0.25, \"month\"], [0.5, \"month\"], [1, \"month\"],\n        [2, \"month\"], [3, \"month\"], [6, \"month\"],\n        [1, \"year\"]\n    ];\n\n    function init(plot) {\n        plot.hooks.processDatapoints.push(function (plot, series, datapoints) {\n            $.each(plot.getAxes(), function(axisName, axis) {\n                var opts = axis.options;\n                if (opts.mode == \"time\") {\n                    axis.tickGenerator = function(axis) {\n                        var ticks = [],\n                            d = dateGenerator(axis.min, opts),\n                            minSize = 0;\n\n                        if (opts.minTickSize != null) {\n                            if (typeof opts.tickSize == \"number\")\n                                minSize = opts.tickSize;\n                            else\n                                minSize = opts.minTickSize[0] * timeUnitSize[opts.minTickSize[1]];\n                        }\n\n                        for (var i = 0; i < spec.length - 1; ++i)\n                            if (axis.delta < (spec[i][0] * timeUnitSize[spec[i][1]]\n                                              + spec[i + 1][0] * timeUnitSize[spec[i + 1][1]]) / 2\n                                && spec[i][0] * timeUnitSize[spec[i][1]] >= minSize)\n                                break;\n                        var size = spec[i][0];\n                        var unit = spec[i][1];\n                        \n                        // special-case the possibility of several years\n                        if (unit == \"year\") {\n                            // if given a minTickSize in years, just use it,\n                            // ensuring that it's an integer\n                            if (opts.minTickSize != null && opts.minTickSize[1] == \"year\") {\n                                size = Math.floor(opts.minTickSize[0]);\n                            } else {\n                                var magn = Math.pow(10, Math.floor(Math.log(axis.delta / timeUnitSize.year) / Math.LN10));\n                                var norm = (axis.delta / timeUnitSize.year) / magn;\n                                if (norm < 1.5)\n                                    size = 1;\n                                else if (norm < 3)\n                                    size = 2;\n                                else if (norm < 7.5)\n                                    size = 5;\n                                else\n                                    size = 10;\n                            \n                                size *= magn;\n                            }\n\n                            // minimum size for years is 1\n                            if (size < 1)\n                                size = 1;\n                        }\n\n                        axis.tickSize = opts.tickSize || [size, unit];\n                        var tickSize = axis.tickSize[0];\n                        unit = axis.tickSize[1];\n                        \n                        var step = tickSize * timeUnitSize[unit];\n\n                        if (unit == \"second\")\n                            d.setSeconds(floorInBase(d.getSeconds(), tickSize));\n                        if (unit == \"minute\")\n                            d.setMinutes(floorInBase(d.getMinutes(), tickSize));\n                        if (unit == \"hour\")\n                            d.setHours(floorInBase(d.getHours(), tickSize));\n                        if (unit == \"month\")\n                            d.setMonth(floorInBase(d.getMonth(), tickSize));\n                        if (unit == \"year\")\n                            d.setFullYear(floorInBase(d.getFullYear(), tickSize));\n                        \n                        // reset smaller components\n                        d.setMilliseconds(0);\n                        if (step >= timeUnitSize.minute)\n                            d.setSeconds(0);\n                        if (step >= timeUnitSize.hour)\n                            d.setMinutes(0);\n                        if (step >= timeUnitSize.day)\n                            d.setHours(0);\n                        if (step >= timeUnitSize.day * 4)\n                            d.setDate(1);\n                        if (step >= timeUnitSize.year)\n                            d.setMonth(0);\n\n\n                        var carry = 0, v = Number.NaN, prev;\n                        do {\n                            prev = v;\n                            v = d.getTime();\n                            ticks.push(v);\n                            if (unit == \"month\") {\n                                if (tickSize < 1) {\n                                    // a bit complicated - we'll divide the month\n                                    // up but we need to take care of fractions\n                                    // so we don't end up in the middle of a day\n                                    d.setDate(1);\n                                    var start = d.getTime();\n                                    d.setMonth(d.getMonth() + 1);\n                                    var end = d.getTime();\n                                    d.setTime(v + carry * timeUnitSize.hour + (end - start) * tickSize);\n                                    carry = d.getHours();\n                                    d.setHours(0);\n                                }\n                                else\n                                    d.setMonth(d.getMonth() + tickSize);\n                            }\n                            else if (unit == \"year\") {\n                                d.setFullYear(d.getFullYear() + tickSize);\n                            }\n                            else\n                                d.setTime(v + step);\n                        } while (v < axis.max && v != prev);\n\n                        return ticks;\n                    };\n\n                    axis.tickFormatter = function (v, axis) {\n                        var d = dateGenerator(v, axis.options);\n\n                        // first check global format\n                        if (opts.timeformat != null)\n                            return formatDate(d, opts.timeformat, opts.monthNames, opts.dayNames);\n                        \n                        var t = axis.tickSize[0] * timeUnitSize[axis.tickSize[1]];\n                        var span = axis.max - axis.min;\n                        var suffix = (opts.twelveHourClock) ? \" %p\" : \"\";\n                        var hourCode = (opts.twelveHourClock) ? \"%I\" : \"%H\";\n                        \n                        if (t < timeUnitSize.minute)\n                            fmt = hourCode + \":%M:%S\" + suffix;\n                        else if (t < timeUnitSize.day) {\n                            if (span < 2 * timeUnitSize.day)\n                                fmt = hourCode + \":%M\" + suffix;\n                            else\n                                fmt = \"%b %d \" + hourCode + \":%M\" + suffix;\n                        }\n                        else if (t < timeUnitSize.month)\n                            fmt = \"%b %d\";\n                        else if (t < timeUnitSize.year) {\n                            if (span < timeUnitSize.year)\n                                fmt = \"%b\";\n                            else\n                                fmt = \"%b %Y\";\n                        }\n                        else\n                            fmt = \"%Y\";\n                        \n                        var rt = formatDate(d, fmt, opts.monthNames, opts.dayNames);\n                        return rt;\n                    };\n                }\n            });\n        });\n    }\n\n    $.plot.plugins.push({\n        init: init,\n        options: options,\n        name: 'time',\n        version: '1.0'\n    });\n})(jQuery);\n"
  },
  {
    "path": "r2/r2/public/static/js/lib/jquery.lazyload.js",
    "content": "/*\n * Lazy Load - jQuery plugin for lazy loading images\n *\n * Copyright (c) 2007-2009 Mika Tuupola\n *\n * Licensed under the MIT license:\n *   http://www.opensource.org/licenses/mit-license.php\n *\n * Project home:\n *   http://www.appelsiini.net/projects/lazyload\n *\n * Version:  1.5.0\n *\n */\n(function($) {\n\n    $.fn.lazyload = function(options) {\n        var settings = {\n            threshold    : 0,\n            failurelimit : 0,\n            event        : \"scroll\",\n            effect       : \"show\",\n            container    : window\n        };\n                \n        if(options) {\n            $.extend(settings, options);\n        }\n\n        /* Fire one scroll event per scroll. Not one scroll event per image. */\n        var elements = this;\n        if (\"scroll\" == settings.event) {\n            $(settings.container).bind(\"scroll\", function(event) {\n                \n                var counter = 0;\n                elements.each(function() {\n                    if ($.abovethetop(this, settings) ||\n                        $.leftofbegin(this, settings)) {\n                            /* Nothing. */\n                    } else if (!$.belowthefold(this, settings) &&\n                        !$.rightoffold(this, settings)) {\n                            $(this).trigger(\"appear\");\n                    } else {\n                        if (counter++ > settings.failurelimit) {\n                            return false;\n                        }\n                    }\n                });\n                /* Remove image from array so it is not looped next time. */\n                var temp = $.grep(elements, function(element) {\n                    return !element.loaded;\n                });\n                elements = $(temp);\n            });\n        }\n        \n        this.each(function() {\n            var self = this;\n            \n            /* Save original only if it is not defined in HTML. */\n            if (undefined == $(self).attr(\"original\")) {\n                $(self).attr(\"original\", $(self).attr(\"src\"));     \n            }\n\n            if (\"scroll\" != settings.event || \n                    undefined == $(self).attr(\"src\") || \n                    settings.placeholder == $(self).attr(\"src\") || \n                    ($.abovethetop(self, settings) ||\n                     $.leftofbegin(self, settings) || \n                     $.belowthefold(self, settings) || \n                     $.rightoffold(self, settings) )) {\n                        \n                if (settings.placeholder) {\n                    $(self).attr(\"src\", settings.placeholder);      \n                } else {\n                    $(self).removeAttr(\"src\");\n                }\n                self.loaded = false;\n            } else {\n                self.loaded = true;\n            }\n            \n            /* When appear is triggered load original image. */\n            $(self).one(\"appear\", function() {\n                if (!this.loaded) {\n                    $(\"<img />\")\n                        .bind(\"load\", function() {\n                            $(self)\n                                .hide()\n                                .attr(\"src\", $(self).attr(\"original\"))\n                                [settings.effect](settings.effectspeed);\n                            self.loaded = true;\n                        })\n                        .attr(\"src\", $(self).attr(\"original\"));\n                };\n            });\n\n            /* When wanted event is triggered load original image */\n            /* by triggering appear.                              */\n            if (\"scroll\" != settings.event) {\n                $(self).bind(settings.event, function(event) {\n                    if (!self.loaded) {\n                        $(self).trigger(\"appear\");\n                    }\n                });\n            }\n        });\n        \n        /* Force initial check if images should appear. */\n        $(settings.container).trigger(settings.event);\n        \n        return this;\n\n    };\n\n    /* Convenience methods in jQuery namespace.           */\n    /* Use as  $.belowthefold(element, {threshold : 100, container : window}) */\n\n    $.belowthefold = function(element, settings) {\n        if (settings.container === undefined || settings.container === window) {\n            var fold = $(window).height() + $(window).scrollTop();\n        } else {\n            var fold = $(settings.container).offset().top + $(settings.container).height();\n        }\n        return fold <= $(element).offset().top - settings.threshold;\n    };\n    \n    $.rightoffold = function(element, settings) {\n        if (settings.container === undefined || settings.container === window) {\n            var fold = $(window).width() + $(window).scrollLeft();\n        } else {\n            var fold = $(settings.container).offset().left + $(settings.container).width();\n        }\n        return fold <= $(element).offset().left - settings.threshold;\n    };\n        \n    $.abovethetop = function(element, settings) {\n        if (settings.container === undefined || settings.container === window) {\n            var fold = $(window).scrollTop();\n        } else {\n            var fold = $(settings.container).offset().top;\n        }\n        return fold >= $(element).offset().top + settings.threshold  + $(element).height();\n    };\n    \n    $.leftofbegin = function(element, settings) {\n        if (settings.container === undefined || settings.container === window) {\n            var fold = $(window).scrollLeft();\n        } else {\n            var fold = $(settings.container).offset().left;\n        }\n        return fold >= $(element).offset().left + settings.threshold + $(element).width();\n    };\n    /* Custom selectors for your convenience.   */\n    /* Use as $(\"img:below-the-fold\").something() */\n\n    $.extend($.expr[':'], {\n        \"below-the-fold\" : \"$.belowthefold(a, {threshold : 0, container: window})\",\n        \"above-the-fold\" : \"!$.belowthefold(a, {threshold : 0, container: window})\",\n        \"right-of-fold\"  : \"$.rightoffold(a, {threshold : 0, container: window})\",\n        \"left-of-fold\"   : \"!$.rightoffold(a, {threshold : 0, container: window})\"\n    });\n    \n})(jQuery);\n"
  },
  {
    "path": "r2/r2/public/static/js/lib/jquery.url.js",
    "content": "// JQuery URL Parser plugin - https://github.com/allmarkedup/jQuery-URL-Parser\n// Written by Mark Perkins, mark@allmarkedup.com\n// License: http://unlicense.org/ (i.e. do what you want with it!)\n\n;(function($, undefined) {\n    \n    var tag2attr = {\n        a       : 'href',\n        img     : 'src',\n        form    : 'action',\n        base    : 'href',\n        script  : 'src',\n        iframe  : 'src',\n        link    : 'href'\n    },\n    \n\tkey = [\"source\",\"protocol\",\"authority\",\"userInfo\",\"user\",\"password\",\"host\",\"port\",\"relative\",\"path\",\"directory\",\"file\",\"query\",\"fragment\"], // keys available to query\n\t\n\taliases = { \"anchor\" : \"fragment\" }, // aliases for backwards compatability\n\n\tparser = {\n\t\tstrict  : /^(?:([^:\\/?#]+):)?(?:\\/\\/((?:(([^:@]*):?([^:@]*))?@)?([^:\\/?#]*)(?::(\\d*))?))?((((?:[^?#\\/]*\\/)*)([^?#]*))(?:\\?([^#]*))?(?:#(.*))?)/,  //less intuitive, more accurate to the specs\n\t\tloose   :  /^(?:(?![^:@]+:[^:@\\/]*@)([^:\\/?#.]+):)?(?:\\/\\/)?((?:(([^:@]*):?([^:@]*))?@)?([^:\\/?#]*)(?::(\\d*))?)(((\\/(?:[^?#](?![^?#\\/]*\\.[^?#\\/.]+(?:[?#]|$)))*\\/?)?([^?#\\/]*))(?:\\?([^#]*))?(?:#(.*))?)/ // more intuitive, fails on relative paths and deviates from specs\n\t},\n\t\n\tquerystring_parser = /(?:^|&|;)([^&=;]*)=?([^&;]*)/g, // supports both ampersand and semicolon-delimted query string key/value pairs\n\t\n\tfragment_parser = /(?:^|&|;)([^&=;]*)=?([^&;]*)/g; // supports both ampersand and semicolon-delimted fragment key/value pairs\n\t\n\tfunction parseUri( url, strictMode )\n\t{\n\t\tvar str = decodeURI( url ),\n\t\t    res   = parser[ strictMode || false ? \"strict\" : \"loose\" ].exec( str ),\n\t\t    uri = { attr : {}, param : {}, seg : {} },\n\t\t    i   = 14;\n\t\t\n\t\twhile ( i-- )\n\t\t{\n\t\t\turi.attr[ key[i] ] = res[i] || \"\";\n\t\t}\n\t\t\n\t\t// build query and fragment parameters\n\t\t\n\t\turi.param['query'] = {};\n\t\turi.param['fragment'] = {};\n\t\t\n\t\turi.attr['query'].replace( querystring_parser, function ( $0, $1, $2 ){\n\t\t\tif ($1)\n\t\t\t{\n\t\t\t\turi.param['query'][$1] = $2;\n\t\t\t}\n\t\t});\n\t\t\n\t\turi.attr['fragment'].replace( fragment_parser, function ( $0, $1, $2 ){\n\t\t\tif ($1)\n\t\t\t{\n\t\t\t\turi.param['fragment'][$1] = $2;\n\t\t\t}\n\t\t});\n\t\t\t\t\n\t\t// split path and fragement into segments\n\t\t\n        uri.seg['path'] = uri.attr.path.replace(/^\\/+|\\/+$/g,'').split('/');\n        \n        uri.seg['fragment'] = uri.attr.fragment.replace(/^\\/+|\\/+$/g,'').split('/');\n        \n        // compile a 'base' domain attribute\n        \n        uri.attr['base'] = uri.attr.host ? uri.attr.protocol+\"://\"+uri.attr.host + (uri.attr.port ? \":\"+uri.attr.port : '') : '';\n        \n\t\treturn uri;\n\t};\n\t\n\tfunction getAttrName( elm )\n\t{\n\t\tvar tn = elm.tagName;\n\t\tif ( tn !== undefined ) return tag2attr[tn.toLowerCase()];\n\t\treturn tn;\n\t}\n\t\n\t$.fn.url = function( strictMode )\n\t{\n\t    var url = '';\n\t    \n\t    if ( this.length )\n\t    {\n\t        url = $(this).attr( getAttrName(this[0]) ) || '';\n\t    }\n\t    \n        return $.url( url, strictMode );\n\t};\n\t\n\t$.url = function( url, strictMode )\n\t{\n\t    if ( arguments.length === 1 && url === true )\n        {\n            strictMode = true;\n            url = undefined;\n        }\n        \n        strictMode = strictMode || false;\n        url = url || window.location.toString();\n        \t    \t            \n        return {\n            \n            data : parseUri(url, strictMode),\n            \n            // get various attributes from the URI\n            attr : function( attr )\n            {\n                attr = aliases[attr] || attr;\n                return attr !== undefined ? this.data.attr[attr] : this.data.attr;\n            },\n            \n            // return query string parameters\n            param : function( param )\n            {\n                return param !== undefined ? this.data.param.query[param] : this.data.param.query;\n            },\n            \n            // return fragment parameters\n            fparam : function( param )\n            {\n                return param !== undefined ? this.data.param.fragment[param] : this.data.param.fragment;\n            },\n            \n            // return path segments\n            segment : function( seg )\n            {\n                if ( seg === undefined )\n                {\n                    return this.data.seg.path;                    \n                }\n                else\n                {\n                    seg = seg < 0 ? this.data.seg.path.length + seg : seg - 1; // negative segments count from the end\n                    return this.data.seg.path[seg];                    \n                }\n            },\n            \n            // return fragment segments\n            fsegment : function( seg )\n            {\n                if ( seg === undefined )\n                {\n                    return this.data.seg.fragment;                    \n                }\n                else\n                {\n                    seg = seg < 0 ? this.data.seg.fragment.length + seg : seg - 1; // negative segments count from the end\n                    return this.data.seg.fragment[seg];                    \n                }\n            }\n            \n        };\n        \n\t};\n\t\n})(jQuery);"
  },
  {
    "path": "r2/r2/public/static/js/lib/json2.js",
    "content": "/*\n    http://www.JSON.org/json2.js\n    2011-02-23\n\n    Public Domain.\n\n    NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.\n\n    See http://www.JSON.org/js.html\n\n\n    This code should be minified before deployment.\n    See http://javascript.crockford.com/jsmin.html\n\n    USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO\n    NOT CONTROL.\n\n\n    This file creates a global JSON object containing two methods: stringify\n    and parse.\n\n        JSON.stringify(value, replacer, space)\n            value       any JavaScript value, usually an object or array.\n\n            replacer    an optional parameter that determines how object\n                        values are stringified for objects. It can be a\n                        function or an array of strings.\n\n            space       an optional parameter that specifies the indentation\n                        of nested structures. If it is omitted, the text will\n                        be packed without extra whitespace. If it is a number,\n                        it will specify the number of spaces to indent at each\n                        level. If it is a string (such as '\\t' or '&nbsp;'),\n                        it contains the characters used to indent at each level.\n\n            This method produces a JSON text from a JavaScript value.\n\n            When an object value is found, if the object contains a toJSON\n            method, its toJSON method will be called and the result will be\n            stringified. A toJSON method does not serialize: it returns the\n            value represented by the name/value pair that should be serialized,\n            or undefined if nothing should be serialized. The toJSON method\n            will be passed the key associated with the value, and this will be\n            bound to the value\n\n            For example, this would serialize Dates as ISO strings.\n\n                Date.prototype.toJSON = function (key) {\n                    function f(n) {\n                        // Format integers to have at least two digits.\n                        return n < 10 ? '0' + n : n;\n                    }\n\n                    return this.getUTCFullYear()   + '-' +\n                         f(this.getUTCMonth() + 1) + '-' +\n                         f(this.getUTCDate())      + 'T' +\n                         f(this.getUTCHours())     + ':' +\n                         f(this.getUTCMinutes())   + ':' +\n                         f(this.getUTCSeconds())   + 'Z';\n                };\n\n            You can provide an optional replacer method. It will be passed the\n            key and value of each member, with this bound to the containing\n            object. The value that is returned from your method will be\n            serialized. If your method returns undefined, then the member will\n            be excluded from the serialization.\n\n            If the replacer parameter is an array of strings, then it will be\n            used to select the members to be serialized. It filters the results\n            such that only members with keys listed in the replacer array are\n            stringified.\n\n            Values that do not have JSON representations, such as undefined or\n            functions, will not be serialized. Such values in objects will be\n            dropped; in arrays they will be replaced with null. You can use\n            a replacer function to replace those with JSON values.\n            JSON.stringify(undefined) returns undefined.\n\n            The optional space parameter produces a stringification of the\n            value that is filled with line breaks and indentation to make it\n            easier to read.\n\n            If the space parameter is a non-empty string, then that string will\n            be used for indentation. If the space parameter is a number, then\n            the indentation will be that many spaces.\n\n            Example:\n\n            text = JSON.stringify(['e', {pluribus: 'unum'}]);\n            // text is '[\"e\",{\"pluribus\":\"unum\"}]'\n\n\n            text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\\t');\n            // text is '[\\n\\t\"e\",\\n\\t{\\n\\t\\t\"pluribus\": \"unum\"\\n\\t}\\n]'\n\n            text = JSON.stringify([new Date()], function (key, value) {\n                return this[key] instanceof Date ?\n                    'Date(' + this[key] + ')' : value;\n            });\n            // text is '[\"Date(---current time---)\"]'\n\n\n        JSON.parse(text, reviver)\n            This method parses a JSON text to produce an object or array.\n            It can throw a SyntaxError exception.\n\n            The optional reviver parameter is a function that can filter and\n            transform the results. It receives each of the keys and values,\n            and its return value is used instead of the original value.\n            If it returns what it received, then the structure is not modified.\n            If it returns undefined then the member is deleted.\n\n            Example:\n\n            // Parse the text. Values that look like ISO date strings will\n            // be converted to Date objects.\n\n            myData = JSON.parse(text, function (key, value) {\n                var a;\n                if (typeof value === 'string') {\n                    a =\n/^(\\d{4})-(\\d{2})-(\\d{2})T(\\d{2}):(\\d{2}):(\\d{2}(?:\\.\\d*)?)Z$/.exec(value);\n                    if (a) {\n                        return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4],\n                            +a[5], +a[6]));\n                    }\n                }\n                return value;\n            });\n\n            myData = JSON.parse('[\"Date(09/09/2001)\"]', function (key, value) {\n                var d;\n                if (typeof value === 'string' &&\n                        value.slice(0, 5) === 'Date(' &&\n                        value.slice(-1) === ')') {\n                    d = new Date(value.slice(5, -1));\n                    if (d) {\n                        return d;\n                    }\n                }\n                return value;\n            });\n\n\n    This is a reference implementation. You are free to copy, modify, or\n    redistribute.\n*/\n\n/*jslint evil: true, strict: false, regexp: false */\n\n/*members \"\", \"\\b\", \"\\t\", \"\\n\", \"\\f\", \"\\r\", \"\\\"\", JSON, \"\\\\\", apply,\n    call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours,\n    getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join,\n    lastIndex, length, parse, prototype, push, replace, slice, stringify,\n    test, toJSON, toString, valueOf\n*/\n\n\n// Create a JSON object only if one does not already exist. We create the\n// methods in a closure to avoid creating global variables.\n\nvar JSON;\nif (!JSON) {\n    JSON = {};\n}\n\n(function () {\n    \"use strict\";\n\n    function f(n) {\n        // Format integers to have at least two digits.\n        return n < 10 ? '0' + n : n;\n    }\n\n    if (typeof Date.prototype.toJSON !== 'function') {\n\n        Date.prototype.toJSON = function (key) {\n\n            return isFinite(this.valueOf()) ?\n                this.getUTCFullYear()     + '-' +\n                f(this.getUTCMonth() + 1) + '-' +\n                f(this.getUTCDate())      + 'T' +\n                f(this.getUTCHours())     + ':' +\n                f(this.getUTCMinutes())   + ':' +\n                f(this.getUTCSeconds())   + 'Z' : null;\n        };\n\n        String.prototype.toJSON      =\n            Number.prototype.toJSON  =\n            Boolean.prototype.toJSON = function (key) {\n                return this.valueOf();\n            };\n    }\n\n    var cx = /[\\u0000\\u00ad\\u0600-\\u0604\\u070f\\u17b4\\u17b5\\u200c-\\u200f\\u2028-\\u202f\\u2060-\\u206f\\ufeff\\ufff0-\\uffff]/g,\n        escapable = /[\\\\\\\"\\x00-\\x1f\\x7f-\\x9f\\u00ad\\u0600-\\u0604\\u070f\\u17b4\\u17b5\\u200c-\\u200f\\u2028-\\u202f\\u2060-\\u206f\\ufeff\\ufff0-\\uffff]/g,\n        gap,\n        indent,\n        meta = {    // table of character substitutions\n            '\\b': '\\\\b',\n            '\\t': '\\\\t',\n            '\\n': '\\\\n',\n            '\\f': '\\\\f',\n            '\\r': '\\\\r',\n            '\"' : '\\\\\"',\n            '\\\\': '\\\\\\\\'\n        },\n        rep;\n\n\n    function quote(string) {\n\n// If the string contains no control characters, no quote characters, and no\n// backslash characters, then we can safely slap some quotes around it.\n// Otherwise we must also replace the offending characters with safe escape\n// sequences.\n\n        escapable.lastIndex = 0;\n        return escapable.test(string) ? '\"' + string.replace(escapable, function (a) {\n            var c = meta[a];\n            return typeof c === 'string' ? c :\n                '\\\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);\n        }) + '\"' : '\"' + string + '\"';\n    }\n\n\n    function str(key, holder) {\n\n// Produce a string from holder[key].\n\n        var i,          // The loop counter.\n            k,          // The member key.\n            v,          // The member value.\n            length,\n            mind = gap,\n            partial,\n            value = holder[key];\n\n// If the value has a toJSON method, call it to obtain a replacement value.\n\n        if (value && typeof value === 'object' &&\n                typeof value.toJSON === 'function') {\n            value = value.toJSON(key);\n        }\n\n// If we were called with a replacer function, then call the replacer to\n// obtain a replacement value.\n\n        if (typeof rep === 'function') {\n            value = rep.call(holder, key, value);\n        }\n\n// What happens next depends on the value's type.\n\n        switch (typeof value) {\n        case 'string':\n            return quote(value);\n\n        case 'number':\n\n// JSON numbers must be finite. Encode non-finite numbers as null.\n\n            return isFinite(value) ? String(value) : 'null';\n\n        case 'boolean':\n        case 'null':\n\n// If the value is a boolean or null, convert it to a string. Note:\n// typeof null does not produce 'null'. The case is included here in\n// the remote chance that this gets fixed someday.\n\n            return String(value);\n\n// If the type is 'object', we might be dealing with an object or an array or\n// null.\n\n        case 'object':\n\n// Due to a specification blunder in ECMAScript, typeof null is 'object',\n// so watch out for that case.\n\n            if (!value) {\n                return 'null';\n            }\n\n// Make an array to hold the partial results of stringifying this object value.\n\n            gap += indent;\n            partial = [];\n\n// Is the value an array?\n\n            if (Object.prototype.toString.apply(value) === '[object Array]') {\n\n// The value is an array. Stringify every element. Use null as a placeholder\n// for non-JSON values.\n\n                length = value.length;\n                for (i = 0; i < length; i += 1) {\n                    partial[i] = str(i, value) || 'null';\n                }\n\n// Join all of the elements together, separated with commas, and wrap them in\n// brackets.\n\n                v = partial.length === 0 ? '[]' : gap ?\n                    '[\\n' + gap + partial.join(',\\n' + gap) + '\\n' + mind + ']' :\n                    '[' + partial.join(',') + ']';\n                gap = mind;\n                return v;\n            }\n\n// If the replacer is an array, use it to select the members to be stringified.\n\n            if (rep && typeof rep === 'object') {\n                length = rep.length;\n                for (i = 0; i < length; i += 1) {\n                    if (typeof rep[i] === 'string') {\n                        k = rep[i];\n                        v = str(k, value);\n                        if (v) {\n                            partial.push(quote(k) + (gap ? ': ' : ':') + v);\n                        }\n                    }\n                }\n            } else {\n\n// Otherwise, iterate through all of the keys in the object.\n\n                for (k in value) {\n                    if (Object.prototype.hasOwnProperty.call(value, k)) {\n                        v = str(k, value);\n                        if (v) {\n                            partial.push(quote(k) + (gap ? ': ' : ':') + v);\n                        }\n                    }\n                }\n            }\n\n// Join all of the member texts together, separated with commas,\n// and wrap them in braces.\n\n            v = partial.length === 0 ? '{}' : gap ?\n                '{\\n' + gap + partial.join(',\\n' + gap) + '\\n' + mind + '}' :\n                '{' + partial.join(',') + '}';\n            gap = mind;\n            return v;\n        }\n    }\n\n// If the JSON object does not yet have a stringify method, give it one.\n\n    if (typeof JSON.stringify !== 'function') {\n        JSON.stringify = function (value, replacer, space) {\n\n// The stringify method takes a value and an optional replacer, and an optional\n// space parameter, and returns a JSON text. The replacer can be a function\n// that can replace values, or an array of strings that will select the keys.\n// A default replacer method can be provided. Use of the space parameter can\n// produce text that is more easily readable.\n\n            var i;\n            gap = '';\n            indent = '';\n\n// If the space parameter is a number, make an indent string containing that\n// many spaces.\n\n            if (typeof space === 'number') {\n                for (i = 0; i < space; i += 1) {\n                    indent += ' ';\n                }\n\n// If the space parameter is a string, it will be used as the indent string.\n\n            } else if (typeof space === 'string') {\n                indent = space;\n            }\n\n// If there is a replacer, it must be a function or an array.\n// Otherwise, throw an error.\n\n            rep = replacer;\n            if (replacer && typeof replacer !== 'function' &&\n                    (typeof replacer !== 'object' ||\n                    typeof replacer.length !== 'number')) {\n                throw new Error('JSON.stringify');\n            }\n\n// Make a fake root object containing our value under the key of ''.\n// Return the result of stringifying the value.\n\n            return str('', {'': value});\n        };\n    }\n\n\n// If the JSON object does not yet have a parse method, give it one.\n\n    if (typeof JSON.parse !== 'function') {\n        JSON.parse = function (text, reviver) {\n\n// The parse method takes a text and an optional reviver function, and returns\n// a JavaScript value if the text is a valid JSON text.\n\n            var j;\n\n            function walk(holder, key) {\n\n// The walk method is used to recursively walk the resulting structure so\n// that modifications can be made.\n\n                var k, v, value = holder[key];\n                if (value && typeof value === 'object') {\n                    for (k in value) {\n                        if (Object.prototype.hasOwnProperty.call(value, k)) {\n                            v = walk(value, k);\n                            if (v !== undefined) {\n                                value[k] = v;\n                            } else {\n                                delete value[k];\n                            }\n                        }\n                    }\n                }\n                return reviver.call(holder, key, value);\n            }\n\n\n// Parsing happens in four stages. In the first stage, we replace certain\n// Unicode characters with escape sequences. JavaScript handles many characters\n// incorrectly, either silently deleting them, or treating them as line endings.\n\n            text = String(text);\n            cx.lastIndex = 0;\n            if (cx.test(text)) {\n                text = text.replace(cx, function (a) {\n                    return '\\\\u' +\n                        ('0000' + a.charCodeAt(0).toString(16)).slice(-4);\n                });\n            }\n\n// In the second stage, we run the text against regular expressions that look\n// for non-JSON patterns. We are especially concerned with '()' and 'new'\n// because they can cause invocation, and '=' because it can cause mutation.\n// But just to be safe, we want to reject all unexpected forms.\n\n// We split the second stage into 4 regexp operations in order to work around\n// crippling inefficiencies in IE's and Safari's regexp engines. First we\n// replace the JSON backslash pairs with '@' (a non-JSON character). Second, we\n// replace all simple value tokens with ']' characters. Third, we delete all\n// open brackets that follow a colon or comma or that begin the text. Finally,\n// we look to see that the remaining characters are only whitespace or ']' or\n// ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval.\n\n            if (/^[\\],:{}\\s]*$/\n                    .test(text.replace(/\\\\(?:[\"\\\\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@')\n                        .replace(/\"[^\"\\\\\\n\\r]*\"|true|false|null|-?\\d+(?:\\.\\d*)?(?:[eE][+\\-]?\\d+)?/g, ']')\n                        .replace(/(?:^|:|,)(?:\\s*\\[)+/g, ''))) {\n\n// In the third stage we use the eval function to compile the text into a\n// JavaScript structure. The '{' operator is subject to a syntactic ambiguity\n// in JavaScript: it can begin a block or an object literal. We wrap the text\n// in parens to eliminate the ambiguity.\n\n                j = eval('(' + text + ')');\n\n// In the optional fourth stage, we recursively walk the new structure, passing\n// each name/value pair to a reviver function for possible transformation.\n\n                return typeof reviver === 'function' ?\n                    walk({'': j}, '') : j;\n            }\n\n// If the text is not JSON parseable, then a SyntaxError is thrown.\n\n            throw new SyntaxError('JSON.parse');\n        };\n    }\n}());\n"
  },
  {
    "path": "r2/r2/public/static/js/lib/less-1.4.2.js",
    "content": "/*\n * LESS - Leaner CSS v1.4.2\n * http://lesscss.org\n *\n * Copyright (c) 2009-2013, Alexis Sellier\n * Licensed under the Apache 2.0 License.\n *\n * @licence\n */\n(function (window, undefined) {\n//\n// Stub out `require` in the browser\n//\nfunction require(arg) {\n    return window.less[arg.split('/')[1]];\n};\n\nvar less, tree, charset;\n\nif (typeof environment === \"object\" && ({}).toString.call(environment) === \"[object Environment]\") {\n    // Rhino\n    // Details on how to detect Rhino: https://github.com/ringo/ringojs/issues/88\n    if (typeof(window) === 'undefined') { less = {} }\n    else                                { less = window.less = {} }\n    tree = less.tree = {};\n    less.mode = 'rhino';\n} else if (typeof(window) === 'undefined') {\n    // Node.js\n    less = exports,\n    tree = require('./tree');\n    less.mode = 'node';\n} else {\n    // Browser\n    if (typeof(window.less) === 'undefined') { window.less = {} }\n    less = window.less,\n    tree = window.less.tree = {};\n    less.mode = 'browser';\n}\n//\n// less.js - parser\n//\n//    A relatively straight-forward predictive parser.\n//    There is no tokenization/lexing stage, the input is parsed\n//    in one sweep.\n//\n//    To make the parser fast enough to run in the browser, several\n//    optimization had to be made:\n//\n//    - Matching and slicing on a huge input is often cause of slowdowns.\n//      The solution is to chunkify the input into smaller strings.\n//      The chunks are stored in the `chunks` var,\n//      `j` holds the current chunk index, and `current` holds\n//      the index of the current chunk in relation to `input`.\n//      This gives us an almost 4x speed-up.\n//\n//    - In many cases, we don't need to match individual tokens;\n//      for example, if a value doesn't hold any variables, operations\n//      or dynamic references, the parser can effectively 'skip' it,\n//      treating it as a literal.\n//      An example would be '1px solid #000' - which evaluates to itself,\n//      we don't need to know what the individual components are.\n//      The drawback, of course is that you don't get the benefits of\n//      syntax-checking on the CSS. This gives us a 50% speed-up in the parser,\n//      and a smaller speed-up in the code-gen.\n//\n//\n//    Token matching is done with the `$` function, which either takes\n//    a terminal string or regexp, or a non-terminal function to call.\n//    It also takes care of moving all the indices forwards.\n//\n//\nless.Parser = function Parser(env) {\n    var input,       // LeSS input string\n        i,           // current index in `input`\n        j,           // current chunk\n        temp,        // temporarily holds a chunk's state, for backtracking\n        memo,        // temporarily holds `i`, when backtracking\n        furthest,    // furthest index the parser has gone to\n        chunks,      // chunkified input\n        current,     // index of current chunk, in `input`\n        parser;\n\n    var that = this;\n\n    // Top parser on an import tree must be sure there is one \"env\"\n    // which will then be passed around by reference.\n    if (!(env instanceof tree.parseEnv)) {\n        env = new tree.parseEnv(env);\n    }\n\n    var imports = this.imports = {\n        paths: env.paths || [],  // Search paths, when importing\n        queue: [],               // Files which haven't been imported yet\n        files: env.files,        // Holds the imported parse trees\n        contents: env.contents,  // Holds the imported file contents\n        mime:  env.mime,         // MIME type of .less files\n        error: null,             // Error in parsing/evaluating an import\n        push: function (path, currentFileInfo, callback) {\n            var parserImporter = this;\n            this.queue.push(path);\n\n            //\n            // Import a file asynchronously\n            //\n            less.Parser.importer(path, currentFileInfo, function (e, root, fullPath) {\n                parserImporter.queue.splice(parserImporter.queue.indexOf(path), 1); // Remove the path from the queue\n\n                var imported = fullPath in parserImporter.files;\n\n                parserImporter.files[fullPath] = root;                        // Store the root\n\n                if (e && !parserImporter.error) { parserImporter.error = e; }\n                \n                callback(e, root, imported);\n            }, env);\n        }\n    };\n\n    function save()    { temp = chunks[j], memo = i, current = i; }\n    function restore() { chunks[j] = temp, i = memo, current = i; }\n\n    function sync() {\n        if (i > current) {\n            chunks[j] = chunks[j].slice(i - current);\n            current = i;\n        }\n    }\n    function isWhitespace(c) {\n        // Could change to \\s?\n        var code = c.charCodeAt(0);\n        return code === 32 || code === 10 || code === 9;\n    }\n    //\n    // Parse from a token, regexp or string, and move forward if match\n    //\n    function $(tok) {\n        var match, args, length, index, k;\n\n        //\n        // Non-terminal\n        //\n        if (tok instanceof Function) {\n            return tok.call(parser.parsers);\n        //\n        // Terminal\n        //\n        //     Either match a single character in the input,\n        //     or match a regexp in the current chunk (chunk[j]).\n        //\n        } else if (typeof(tok) === 'string') {\n            match = input.charAt(i) === tok ? tok : null;\n            length = 1;\n            sync ();\n        } else {\n            sync ();\n\n            if (match = tok.exec(chunks[j])) {\n                length = match[0].length;\n            } else {\n                return null;\n            }\n        }\n\n        // The match is confirmed, add the match length to `i`,\n        // and consume any extra white-space characters (' ' || '\\n')\n        // which come after that. The reason for this is that LeSS's\n        // grammar is mostly white-space insensitive.\n        //\n        if (match) {\n            skipWhitespace(length);\n\n            if(typeof(match) === 'string') {\n                return match;\n            } else {\n                return match.length === 1 ? match[0] : match;\n            }\n        }\n    }\n\n    function skipWhitespace(length) {\n        var oldi = i, oldj = j,\n            endIndex = i + chunks[j].length,\n            mem = i += length;\n\n        while (i < endIndex) {\n            if (! isWhitespace(input.charAt(i))) { break }\n            i++;\n        }\n        chunks[j] = chunks[j].slice(length + (i - mem));\n        current = i;\n\n        if (chunks[j].length === 0 && j < chunks.length - 1) { j++ }\n\n        return oldi !== i || oldj !== j;\n    }\n\n    function expect(arg, msg) {\n        var result = $(arg);\n        if (! result) {\n            error(msg || (typeof(arg) === 'string' ? \"expected '\" + arg + \"' got '\" + input.charAt(i) + \"'\"\n                                                   : \"unexpected token\"));\n        } else {\n            return result;\n        }\n    }\n\n    function error(msg, type) {\n        var e = new Error(msg);\n        e.index = i;\n        e.type = type || 'Syntax';\n        throw e;\n    }\n\n    // Same as $(), but don't change the state of the parser,\n    // just return the match.\n    function peek(tok) {\n        if (typeof(tok) === 'string') {\n            return input.charAt(i) === tok;\n        } else {\n            if (tok.test(chunks[j])) {\n                return true;\n            } else {\n                return false;\n            }\n        }\n    }\n\n    function getInput(e, env) {\n        if (e.filename && env.currentFileInfo.filename && (e.filename !== env.currentFileInfo.filename)) {\n            return parser.imports.contents[e.filename];\n        } else {\n            return input;\n        }\n    }\n\n    function getLocation(index, input) {\n        for (var n = index, column = -1;\n                 n >= 0 && input.charAt(n) !== '\\n';\n                 n--) { column++ }\n\n        return { line:   typeof(index) === 'number' ? (input.slice(0, index).match(/\\n/g) || \"\").length : null,\n                 column: column };\n    }\n\n    function getDebugInfo(index, inputStream, env) {\n        var filename = env.currentFileInfo.filename;\n        if(less.mode !== 'browser' && less.mode !== 'rhino') {\n            filename = require('path').resolve(filename);\n        }\n\n        return {\n            lineNumber: getLocation(index, inputStream).line + 1,\n            fileName: filename\n        };\n    }\n\n    function LessError(e, env) {\n        var input = getInput(e, env),\n            loc = getLocation(e.index, input),\n            line = loc.line,\n            col  = loc.column,\n            lines = input.split('\\n');\n\n        this.type = e.type || 'Syntax';\n        this.message = e.message;\n        this.filename = e.filename || env.currentFileInfo.filename;\n        this.index = e.index;\n        this.line = typeof(line) === 'number' ? line + 1 : null;\n        this.callLine = e.call && (getLocation(e.call, input).line + 1);\n        this.callExtract = lines[getLocation(e.call, input).line];\n        this.stack = e.stack;\n        this.column = col;\n        this.extract = [\n            lines[line - 1],\n            lines[line],\n            lines[line + 1]\n        ];\n    }\n\n    LessError.prototype = new Error();\n    LessError.prototype.constructor = LessError;\n\n    this.env = env = env || {};\n\n    // The optimization level dictates the thoroughness of the parser,\n    // the lower the number, the less nodes it will create in the tree.\n    // This could matter for debugging, or if you want to access\n    // the individual nodes in the tree.\n    this.optimization = ('optimization' in this.env) ? this.env.optimization : 1;\n\n    //\n    // The Parser\n    //\n    return parser = {\n\n        imports: imports,\n        //\n        // Parse an input string into an abstract syntax tree,\n        // call `callback` when done.\n        //\n        parse: function (str, callback) {\n            var root, start, end, zone, line, lines, buff = [], c, error = null;\n\n            i = j = current = furthest = 0;\n            input = str.replace(/\\r\\n/g, '\\n');\n\n            // Remove potential UTF Byte Order Mark\n            input = input.replace(/^\\uFEFF/, '');\n\n            // Split the input into chunks.\n            chunks = (function (chunks) {\n                var j = 0,\n                    skip = /(?:@\\{[\\w-]+\\}|[^\"'`\\{\\}\\/\\(\\)\\\\])+/g,\n                    comment = /\\/\\*(?:[^*]|\\*+[^\\/*])*\\*+\\/|\\/\\/.*/g,\n                    string = /\"((?:[^\"\\\\\\r\\n]|\\\\.)*)\"|'((?:[^'\\\\\\r\\n]|\\\\.)*)'|`((?:[^`]|\\\\.)*)`/g,\n                    level = 0,\n                    match,\n                    chunk = chunks[0],\n                    inParam;\n\n                for (var i = 0, c, cc; i < input.length;) {\n                    skip.lastIndex = i;\n                    if (match = skip.exec(input)) {\n                        if (match.index === i) {\n                            i += match[0].length;\n                            chunk.push(match[0]);\n                        }\n                    }\n                    c = input.charAt(i);\n                    comment.lastIndex = string.lastIndex = i;\n\n                    if (match = string.exec(input)) {\n                        if (match.index === i) {\n                            i += match[0].length;\n                            chunk.push(match[0]);\n                            continue;\n                        }\n                    }\n\n                    if (!inParam && c === '/') {\n                        cc = input.charAt(i + 1);\n                        if (cc === '/' || cc === '*') {\n                            if (match = comment.exec(input)) {\n                                if (match.index === i) {\n                                    i += match[0].length;\n                                    chunk.push(match[0]);\n                                    continue;\n                                }\n                            }\n                        }\n                    }\n                    \n                    switch (c) {\n                        case '{': if (! inParam) { level ++;        chunk.push(c);                           break }\n                        case '}': if (! inParam) { level --;        chunk.push(c); chunks[++j] = chunk = []; break }\n                        case '(': if (! inParam) { inParam = true;  chunk.push(c);                           break }\n                        case ')': if (  inParam) { inParam = false; chunk.push(c);                           break }\n                        default:                                    chunk.push(c);\n                    }\n                    \n                    i++;\n                }\n                if (level != 0) {\n                    error = new(LessError)({\n                        index: i-1,\n                        type: 'Parse',\n                        message: (level > 0) ? \"missing closing `}`\" : \"missing opening `{`\",\n                        filename: env.currentFileInfo.filename\n                    }, env);\n                }\n\n                return chunks.map(function (c) { return c.join('') });;\n            })([[]]);\n\n            if (error) {\n                return callback(new(LessError)(error, env));\n            }\n\n            // Start with the primary rule.\n            // The whole syntax tree is held under a Ruleset node,\n            // with the `root` property set to true, so no `{}` are\n            // output. The callback is called when the input is parsed.\n            try {\n                root = new(tree.Ruleset)([], $(this.parsers.primary));\n                root.root = true;\n                root.firstRoot = true;\n            } catch (e) {\n                return callback(new(LessError)(e, env));\n            }\n\n            root.toCSS = (function (evaluate) {\n                var line, lines, column;\n\n                return function (options, variables) {\n                    options = options || {};\n                    var importError,\n                        evalEnv = new tree.evalEnv(options);\n                        \n                    //\n                    // Allows setting variables with a hash, so:\n                    //\n                    //   `{ color: new(tree.Color)('#f01') }` will become:\n                    //\n                    //   new(tree.Rule)('@color',\n                    //     new(tree.Value)([\n                    //       new(tree.Expression)([\n                    //         new(tree.Color)('#f01')\n                    //       ])\n                    //     ])\n                    //   )\n                    //\n                    if (typeof(variables) === 'object' && !Array.isArray(variables)) {\n                        variables = Object.keys(variables).map(function (k) {\n                            var value = variables[k];\n\n                            if (! (value instanceof tree.Value)) {\n                                if (! (value instanceof tree.Expression)) {\n                                    value = new(tree.Expression)([value]);\n                                }\n                                value = new(tree.Value)([value]);\n                            }\n                            return new(tree.Rule)('@' + k, value, false, 0);\n                        });\n                        evalEnv.frames = [new(tree.Ruleset)(null, variables)];\n                    }\n\n                    try {\n                        var evaldRoot = evaluate.call(this, evalEnv);\n\n                        new(tree.joinSelectorVisitor)()\n                            .run(evaldRoot);\n\n                        new(tree.processExtendsVisitor)()\n                            .run(evaldRoot);\n\n                        var css = evaldRoot.toCSS({\n                                compress: Boolean(options.compress),\n                                dumpLineNumbers: env.dumpLineNumbers,\n                                strictUnits: Boolean(options.strictUnits)});\n                    } catch (e) {\n                        throw new(LessError)(e, env);\n                    }\n\n                    if (options.yuicompress && less.mode === 'node') {\n                        return require('ycssmin').cssmin(css, options.maxLineLen);\n                    } else if (options.compress) {\n                        return css.replace(/(\\s)+/g, \"$1\");\n                    } else {\n                        return css;\n                    }\n                };\n            })(root.eval);\n\n            // If `i` is smaller than the `input.length - 1`,\n            // it means the parser wasn't able to parse the whole\n            // string, so we've got a parsing error.\n            //\n            // We try to extract a \\n delimited string,\n            // showing the line where the parse error occured.\n            // We split it up into two parts (the part which parsed,\n            // and the part which didn't), so we can color them differently.\n            if (i < input.length - 1) {\n                i = furthest;\n                lines = input.split('\\n');\n                line = (input.slice(0, i).match(/\\n/g) || \"\").length + 1;\n\n                for (var n = i, column = -1; n >= 0 && input.charAt(n) !== '\\n'; n--) { column++ }\n\n                error = {\n                    type: \"Parse\",\n                    message: \"Unrecognised input\",\n                    index: i,\n                    filename: env.currentFileInfo.filename,\n                    line: line,\n                    column: column,\n                    extract: [\n                        lines[line - 2],\n                        lines[line - 1],\n                        lines[line]\n                    ]\n                };\n            }\n\n            var finish = function (e) {\n                e = error || e || parser.imports.error;\n\n                if (e) {\n                    if (!(e instanceof LessError)) {\n                        e = new(LessError)(e, env);\n                    }\n\n                    callback(e);\n                }\n                else {\n                    callback(null, root);\n                }\n            };\n\n            if (env.processImports !== false) {\n                new tree.importVisitor(this.imports, finish)\n                    .run(root);\n            } else {\n                finish();\n            }\n        },\n\n        //\n        // Here in, the parsing rules/functions\n        //\n        // The basic structure of the syntax tree generated is as follows:\n        //\n        //   Ruleset ->  Rule -> Value -> Expression -> Entity\n        //\n        // Here's some LESS code:\n        //\n        //    .class {\n        //      color: #fff;\n        //      border: 1px solid #000;\n        //      width: @w + 4px;\n        //      > .child {...}\n        //    }\n        //\n        // And here's what the parse tree might look like:\n        //\n        //     Ruleset (Selector '.class', [\n        //         Rule (\"color\",  Value ([Expression [Color #fff]]))\n        //         Rule (\"border\", Value ([Expression [Dimension 1px][Keyword \"solid\"][Color #000]]))\n        //         Rule (\"width\",  Value ([Expression [Operation \"+\" [Variable \"@w\"][Dimension 4px]]]))\n        //         Ruleset (Selector [Element '>', '.child'], [...])\n        //     ])\n        //\n        //  In general, most rules will try to parse a token with the `$()` function, and if the return\n        //  value is truly, will return a new node, of the relevant type. Sometimes, we need to check\n        //  first, before parsing, that's when we use `peek()`.\n        //\n        parsers: {\n            //\n            // The `primary` rule is the *entry* and *exit* point of the parser.\n            // The rules here can appear at any level of the parse tree.\n            //\n            // The recursive nature of the grammar is an interplay between the `block`\n            // rule, which represents `{ ... }`, the `ruleset` rule, and this `primary` rule,\n            // as represented by this simplified grammar:\n            //\n            //     primary  →  (ruleset | rule)+\n            //     ruleset  →  selector+ block\n            //     block    →  '{' primary '}'\n            //\n            // Only at one point is the primary rule not called from the\n            // block rule: at the root level.\n            //\n            primary: function () {\n                var node, root = [];\n\n                while ((node = $(this.extendRule) || $(this.mixin.definition) || $(this.rule)    ||  $(this.ruleset) ||\n                               $(this.mixin.call)       || $(this.comment) ||  $(this.directive))\n                               || $(/^[\\s\\n]+/) || $(/^;+/)) {\n                    node && root.push(node);\n                }\n                return root;\n            },\n\n            // We create a Comment node for CSS comments `/* */`,\n            // but keep the LeSS comments `//` silent, by just skipping\n            // over them.\n            comment: function () {\n                var comment;\n\n                if (input.charAt(i) !== '/') return;\n\n                if (input.charAt(i + 1) === '/') {\n                    return new(tree.Comment)($(/^\\/\\/.*/), true);\n                } else if (comment = $(/^\\/\\*(?:[^*]|\\*+[^\\/*])*\\*+\\/\\n?/)) {\n                    return new(tree.Comment)(comment);\n                }\n            },\n\n            //\n            // Entities are tokens which can be found inside an Expression\n            //\n            entities: {\n                //\n                // A string, which supports escaping \" and '\n                //\n                //     \"milky way\" 'he\\'s the one!'\n                //\n                quoted: function () {\n                    var str, j = i, e, index = i;\n\n                    if (input.charAt(j) === '~') { j++, e = true } // Escaped strings\n                    if (input.charAt(j) !== '\"' && input.charAt(j) !== \"'\") return;\n\n                    e && $('~');\n\n                    if (str = $(/^\"((?:[^\"\\\\\\r\\n]|\\\\.)*)\"|'((?:[^'\\\\\\r\\n]|\\\\.)*)'/)) {\n                        return new(tree.Quoted)(str[0], str[1] || str[2], e, index, env.currentFileInfo);\n                    }\n                },\n\n                //\n                // A catch-all word, such as:\n                //\n                //     black border-collapse\n                //\n                keyword: function () {\n                    var k;\n\n                    if (k = $(/^[_A-Za-z-][_A-Za-z0-9-]*/)) {\n                        if (tree.colors.hasOwnProperty(k)) {\n                            // detect named color\n                            return new(tree.Color)(tree.colors[k].slice(1));\n                        } else {\n                            return new(tree.Keyword)(k);\n                        }\n                    }\n                },\n\n                //\n                // A function call\n                //\n                //     rgb(255, 0, 255)\n                //\n                // We also try to catch IE's `alpha()`, but let the `alpha` parser\n                // deal with the details.\n                //\n                // The arguments are parsed with the `entities.arguments` parser.\n                //\n                call: function () {\n                    var name, nameLC, args, alpha_ret, index = i;\n\n                    if (! (name = /^([\\w-]+|%|progid:[\\w\\.]+)\\(/.exec(chunks[j]))) return;\n\n                    name = name[1];\n                    nameLC = name.toLowerCase();\n\n                    if (nameLC === 'url') { return null }\n                    else                  { i += name.length }\n\n                    if (nameLC === 'alpha') {\n                        alpha_ret = $(this.alpha);\n                        if(typeof alpha_ret !== 'undefined') {\n                            return alpha_ret;\n                        }\n                    }\n\n                    $('('); // Parse the '(' and consume whitespace.\n\n                    args = $(this.entities.arguments);\n\n                    if (! $(')')) {\n                        return;\n                    }\n\n                    if (name) { return new(tree.Call)(name, args, index, env.currentFileInfo); }\n                },\n                arguments: function () {\n                    var args = [], arg;\n\n                    while (arg = $(this.entities.assignment) || $(this.expression)) {\n                        args.push(arg);\n                        if (! $(',')) { break }\n                    }\n                    return args;\n                },\n                literal: function () {\n                    return $(this.entities.dimension) ||\n                           $(this.entities.color) ||\n                           $(this.entities.quoted) ||\n                           $(this.entities.unicodeDescriptor);\n                },\n\n                // Assignments are argument entities for calls.\n                // They are present in ie filter properties as shown below.\n                //\n                //     filter: progid:DXImageTransform.Microsoft.Alpha( *opacity=50* )\n                //\n\n                assignment: function () {\n                    var key, value;\n                    if ((key = $(/^\\w+(?=\\s?=)/i)) && $('=') && (value = $(this.entity))) {\n                        return new(tree.Assignment)(key, value);\n                    }\n                },\n\n                //\n                // Parse url() tokens\n                //\n                // We use a specific rule for urls, because they don't really behave like\n                // standard function calls. The difference is that the argument doesn't have\n                // to be enclosed within a string, so it can't be parsed as an Expression.\n                //\n                url: function () {\n                    var value;\n\n                    if (input.charAt(i) !== 'u' || !$(/^url\\(/)) return;\n                    value = $(this.entities.quoted)  || $(this.entities.variable) ||\n                            $(/^(?:(?:\\\\[\\(\\)'\"])|[^\\(\\)'\"])+/) || \"\";\n\n                    expect(')');\n\n                    return new(tree.URL)((value.value != null || value instanceof tree.Variable)\n                                        ? value : new(tree.Anonymous)(value), env.currentFileInfo);\n                },\n\n                //\n                // A Variable entity, such as `@fink`, in\n                //\n                //     width: @fink + 2px\n                //\n                // We use a different parser for variable definitions,\n                // see `parsers.variable`.\n                //\n                variable: function () {\n                    var name, index = i;\n\n                    if (input.charAt(i) === '@' && (name = $(/^@@?[\\w-]+/))) {\n                        return new(tree.Variable)(name, index, env.currentFileInfo);\n                    }\n                },\n\n                // A variable entity useing the protective {} e.g. @{var}\n                variableCurly: function () {\n                    var name, curly, index = i;\n\n                    if (input.charAt(i) === '@' && (curly = $(/^@\\{([\\w-]+)\\}/))) {\n                        return new(tree.Variable)(\"@\" + curly[1], index, env.currentFileInfo);\n                    }\n                },\n\n                //\n                // A Hexadecimal color\n                //\n                //     #4F3C2F\n                //\n                // `rgb` and `hsl` colors are parsed through the `entities.call` parser.\n                //\n                color: function () {\n                    var rgb;\n\n                    if (input.charAt(i) === '#' && (rgb = $(/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})/))) {\n                        return new(tree.Color)(rgb[1]);\n                    }\n                },\n\n                //\n                // A Dimension, that is, a number and a unit\n                //\n                //     0.5em 95%\n                //\n                dimension: function () {\n                    var value, c = input.charCodeAt(i);\n                    //Is the first char of the dimension 0-9, '.', '+' or '-'\n                    if ((c > 57 || c < 43) || c === 47 || c == 44) return;\n\n                    if (value = $(/^([+-]?\\d*\\.?\\d+)(%|[a-z]+)?/)) {\n                        return new(tree.Dimension)(value[1], value[2]);\n                    }\n                },\n\n                //\n                // A unicode descriptor, as is used in unicode-range\n                //\n                // U+0??  or U+00A1-00A9\n                //\n                unicodeDescriptor: function () {\n                    var ud;\n                    \n                    if (ud = $(/^U\\+[0-9a-fA-F?]+(\\-[0-9a-fA-F?]+)?/)) {\n                        return new(tree.UnicodeDescriptor)(ud[0]);\n                    }\n                },\n\n                //\n                // JavaScript code to be evaluated\n                //\n                //     `window.location.href`\n                //\n                javascript: function () {\n                    var str, j = i, e;\n\n                    if (input.charAt(j) === '~') { j++, e = true } // Escaped strings\n                    if (input.charAt(j) !== '`') { return }\n\n                    e && $('~');\n\n                    if (str = $(/^`([^`]*)`/)) {\n                        return new(tree.JavaScript)(str[1], i, e);\n                    }\n                }\n            },\n\n            //\n            // The variable part of a variable definition. Used in the `rule` parser\n            //\n            //     @fink:\n            //\n            variable: function () {\n                var name;\n\n                if (input.charAt(i) === '@' && (name = $(/^(@[\\w-]+)\\s*:/))) { return name[1] }\n            },\n\n            //\n            // extend syntax - used to extend selectors\n            //\n            extend: function(isRule) {\n                var elements, e, index = i, option, extendList = [];\n\n                if (!$(isRule ? /^&:extend\\(/ : /^:extend\\(/)) { return; }\n\n                do {\n                    option = null;\n                    elements = [];\n                    while (true) {\n                        option = $(/^(all)(?=\\s*(\\)|,))/);\n                        if (option) { break; }\n                        e = $(this.element);\n                        if (!e) { break; }\n                        elements.push(e);\n                    }\n\n                    option = option && option[1];\n\n                    extendList.push(new(tree.Extend)(new(tree.Selector)(elements), option, index));\n\n                } while($(\",\"))\n                \n                expect(/^\\)/);\n\n                if (isRule) {\n                    expect(/^;/);\n                }\n\n                return extendList;\n            },\n\n            //\n            // extendRule - used in a rule to extend all the parent selectors\n            //\n            extendRule: function() {\n                return this.extend(true);\n            },\n            \n            //\n            // Mixins\n            //\n            mixin: {\n                //\n                // A Mixin call, with an optional argument list\n                //\n                //     #mixins > .square(#fff);\n                //     .rounded(4px, black);\n                //     .button;\n                //\n                // The `while` loop is there because mixins can be\n                // namespaced, but we only support the child and descendant\n                // selector for now.\n                //\n                call: function () {\n                    var elements = [], e, c, args, delim, arg, index = i, s = input.charAt(i), important = false;\n\n                    if (s !== '.' && s !== '#') { return }\n\n                    save(); // stop us absorbing part of an invalid selector\n\n                    while (e = $(/^[#.](?:[\\w-]|\\\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+/)) {\n                        elements.push(new(tree.Element)(c, e, i));\n                        c = $('>');\n                    }\n                    if ($('(')) {\n                        args = this.mixin.args.call(this, true).args;\n                        expect(')');\n                    }\n\n                    args = args || [];\n\n                    if ($(this.important)) {\n                        important = true;\n                    }\n\n                    if (elements.length > 0 && ($(';') || peek('}'))) {\n                        return new(tree.mixin.Call)(elements, args, index, env.currentFileInfo, important);\n                    }\n\n                    restore();\n                },\n                args: function (isCall) {\n                    var expressions = [], argsSemiColon = [], isSemiColonSeperated, argsComma = [], expressionContainsNamed, name, nameLoop, value, arg,\n                        returner = {args:null, variadic: false};\n                    while (true) {\n                        if (isCall) {\n                            arg = $(this.expression);\n                        } else {\n                            $(this.comment);\n                            if (input.charAt(i) === '.' && $(/^\\.{3}/)) {\n                                returner.variadic = true;\n                                if ($(\";\") && !isSemiColonSeperated) {\n                                    isSemiColonSeperated = true;\n                                }\n                                (isSemiColonSeperated ? argsSemiColon : argsComma)\n                                    .push({ variadic: true });\n                                break;\n                            }\n                            arg = $(this.entities.variable) || $(this.entities.literal)\n                                || $(this.entities.keyword);\n                        }\n\n                        if (!arg) {\n                            break;\n                        }\n\n                        nameLoop = null;\n                        if (arg.throwAwayComments) {\n                            arg.throwAwayComments();\n                        }\n                        value = arg;\n                        var val = null;\n\n                        if (isCall) {\n                            // Variable\n                            if (arg.value.length == 1) {\n                                var val = arg.value[0];\n                            }\n                        } else {\n                            val = arg;\n                        }\n\n                        if (val && val instanceof tree.Variable) {\n                            if ($(':')) {\n                                if (expressions.length > 0) {\n                                    if (isSemiColonSeperated) {\n                                        error(\"Cannot mix ; and , as delimiter types\");\n                                    }\n                                    expressionContainsNamed = true;\n                                }\n                                value = expect(this.expression);\n                                nameLoop = (name = val.name);\n                            } else if (!isCall && $(/^\\.{3}/)) {\n                                returner.variadic = true;\n                                if ($(\";\") && !isSemiColonSeperated) {\n                                    isSemiColonSeperated = true;\n                                }\n                                (isSemiColonSeperated ? argsSemiColon : argsComma)\n                                    .push({ name: arg.name, variadic: true });\n                                break;\n                            } else if (!isCall) {\n                                name = nameLoop = val.name;\n                                value = null;\n                            }\n                        }\n\n                        if (value) {\n                            expressions.push(value);\n                        }\n\n                        argsComma.push({ name:nameLoop, value:value });\n\n                        if ($(',')) {\n                            continue;\n                        }\n\n                        if ($(';') || isSemiColonSeperated) {\n\n                            if (expressionContainsNamed) {\n                                error(\"Cannot mix ; and , as delimiter types\");\n                            }\n\n                            isSemiColonSeperated = true;\n\n                            if (expressions.length > 1) {\n                                value = new (tree.Value)(expressions);\n                            }\n                            argsSemiColon.push({ name:name, value:value });\n\n                            name = null;\n                            expressions = [];\n                            expressionContainsNamed = false;\n                        }\n                    }\n\n                    returner.args = isSemiColonSeperated ? argsSemiColon : argsComma;\n                    return returner;\n                },\n                //\n                // A Mixin definition, with a list of parameters\n                //\n                //     .rounded (@radius: 2px, @color) {\n                //        ...\n                //     }\n                //\n                // Until we have a finer grained state-machine, we have to\n                // do a look-ahead, to make sure we don't have a mixin call.\n                // See the `rule` function for more information.\n                //\n                // We start by matching `.rounded (`, and then proceed on to\n                // the argument list, which has optional default values.\n                // We store the parameters in `params`, with a `value` key,\n                // if there is a value, such as in the case of `@radius`.\n                //\n                // Once we've got our params list, and a closing `)`, we parse\n                // the `{...}` block.\n                //\n                definition: function () {\n                    var name, params = [], match, ruleset, param, value, cond, variadic = false;\n                    if ((input.charAt(i) !== '.' && input.charAt(i) !== '#') ||\n                        peek(/^[^{]*\\}/)) return;\n\n                    save();\n\n                    if (match = $(/^([#.](?:[\\w-]|\\\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+)\\s*\\(/)) {\n                        name = match[1];\n\n                        var argInfo = this.mixin.args.call(this, false);\n                        params = argInfo.args;\n                        variadic = argInfo.variadic;\n\n                        // .mixincall(\"@{a}\");\n                        // looks a bit like a mixin definition.. so we have to be nice and restore\n                        if (!$(')')) {\n                            furthest = i;\n                            restore();\n                        }\n                        \n                        $(this.comment);\n\n                        if ($(/^when/)) { // Guard\n                            cond = expect(this.conditions, 'expected condition');\n                        }\n\n                        ruleset = $(this.block);\n\n                        if (ruleset) {\n                            return new(tree.mixin.Definition)(name, params, ruleset, cond, variadic);\n                        } else {\n                            restore();\n                        }\n                    }\n                }\n            },\n\n            //\n            // Entities are the smallest recognized token,\n            // and can be found inside a rule's value.\n            //\n            entity: function () {\n                return $(this.entities.literal) || $(this.entities.variable) || $(this.entities.url) ||\n                       $(this.entities.call)    || $(this.entities.keyword)  ||$(this.entities.javascript) ||\n                       $(this.comment);\n            },\n\n            //\n            // A Rule terminator. Note that we use `peek()` to check for '}',\n            // because the `block` rule will be expecting it, but we still need to make sure\n            // it's there, if ';' was ommitted.\n            //\n            end: function () {\n                return $(';') || peek('}');\n            },\n\n            //\n            // IE's alpha function\n            //\n            //     alpha(opacity=88)\n            //\n            alpha: function () {\n                var value;\n\n                if (! $(/^\\(opacity=/i)) return;\n                if (value = $(/^\\d+/) || $(this.entities.variable)) {\n                    expect(')');\n                    return new(tree.Alpha)(value);\n                }\n            },\n\n            //\n            // A Selector Element\n            //\n            //     div\n            //     + h1\n            //     #socks\n            //     input[type=\"text\"]\n            //\n            // Elements are the building blocks for Selectors,\n            // they are made out of a `Combinator` (see combinator rule),\n            // and an element name, such as a tag a class, or `*`.\n            //\n            element: function () {\n                var e, t, c, v;\n\n                c = $(this.combinator);\n\n                e = $(/^(?:\\d+\\.\\d+|\\d+)%/) || $(/^(?:[.#]?|:*)(?:[\\w-]|[^\\x00-\\x9f]|\\\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+/) ||\n                    $('*') || $('&') || $(this.attribute) || $(/^\\([^()@]+\\)/) || $(/^[\\.#](?=@)/) || $(this.entities.variableCurly);\n\n                if (! e) {\n                    if ($('(')) {\n                        if ((v = ($(this.selector))) &&\n                                $(')')) {\n                            e = new(tree.Paren)(v);\n                        }\n                    }\n                }\n\n                if (e) { return new(tree.Element)(c, e, i) }\n            },\n\n            //\n            // Combinators combine elements together, in a Selector.\n            //\n            // Because our parser isn't white-space sensitive, special care\n            // has to be taken, when parsing the descendant combinator, ` `,\n            // as it's an empty space. We have to check the previous character\n            // in the input, to see if it's a ` ` character. More info on how\n            // we deal with this in *combinator.js*.\n            //\n            combinator: function () {\n                var c = input.charAt(i);\n\n                if (c === '>' || c === '+' || c === '~' || c === '|') {\n                    i++;\n                    while (input.charAt(i).match(/\\s/)) { i++ }\n                    return new(tree.Combinator)(c);\n                } else if (input.charAt(i - 1).match(/\\s/)) {\n                    return new(tree.Combinator)(\" \");\n                } else {\n                    return new(tree.Combinator)(null);\n                }\n            },\n\n            //\n            // A CSS Selector\n            //\n            //     .class > div + h1\n            //     li a:hover\n            //\n            // Selectors are made out of one or more Elements, see above.\n            //\n            selector: function () {\n                var sel, e, elements = [], c, extend, extendList = [];\n\n                while ((extend = $(this.extend)) || (e = $(this.element))) {\n                    if (extend) {\n                        extendList.push.apply(extendList, extend);\n                    } else {\n                        if (extendList.length) {\n                            error(\"Extend can only be used at the end of selector\");\n                        }\n                        c = input.charAt(i);\n                        elements.push(e)\n                        e = null;\n                    }\n                    if (c === '{' || c === '}' || c === ';' || c === ',' || c === ')') { break }\n                }\n\n                if (elements.length > 0) { return new(tree.Selector)(elements, extendList); }\n                if (extendList.length) { error(\"Extend must be used to extend a selector, it cannot be used on its own\"); }\n            },\n            attribute: function () {\n                var attr = '', key, val, op;\n\n                if (! $('[')) return;\n\n                if (!(key = $(this.entities.variableCurly))) {\n                    key = expect(/^(?:[_A-Za-z0-9-\\*]*\\|)?(?:[_A-Za-z0-9-]|\\\\.)+/);\n                }\n\n                if ((op = $(/^[|~*$^]?=/))) {\n                    val = $(this.entities.quoted) || $(/^[\\w-]+/) || $(this.entities.variableCurly);\n                }\n\n                expect(']');\n\n                return new(tree.Attribute)(key, op, val);\n            },\n\n            //\n            // The `block` rule is used by `ruleset` and `mixin.definition`.\n            // It's a wrapper around the `primary` rule, with added `{}`.\n            //\n            block: function () {\n                var content;\n                if ($('{') && (content = $(this.primary)) && $('}')) {\n                    return content;\n                }\n            },\n\n            //\n            // div, .class, body > p {...}\n            //\n            ruleset: function () {\n                var selectors = [], s, rules, debugInfo;\n                \n                save();\n\n                if (env.dumpLineNumbers)\n                    debugInfo = getDebugInfo(i, input, env);\n\n                while (s = $(this.selector)) {\n                    selectors.push(s);\n                    $(this.comment);\n                    if (! $(',')) { break }\n                    $(this.comment);\n                }\n\n                if (selectors.length > 0 && (rules = $(this.block))) {\n                    var ruleset = new(tree.Ruleset)(selectors, rules, env.strictImports);\n                    if (env.dumpLineNumbers)\n                        ruleset.debugInfo = debugInfo;\n                    return ruleset;\n                } else {\n                    // Backtrack\n                    furthest = i;\n                    restore();\n                }\n            },\n            rule: function (tryAnonymous) {\n                var name, value, c = input.charAt(i), important;\n                save();\n\n                if (c === '.' || c === '#' || c === '&') { return }\n\n                if (name = $(this.variable) || $(this.property)) {\n                    // prefer to try to parse first if its a variable or we are compressing\n                    // but always fallback on the other one\n                    value = !tryAnonymous && (env.compress || (name.charAt(0) === '@')) ?\n                        ($(this.value) || $(this.anonymousValue)) :\n                        ($(this.anonymousValue) || $(this.value));\n\n                    important = $(this.important);\n\n                    if (value && $(this.end)) {\n                        return new(tree.Rule)(name, value, important, memo, env.currentFileInfo);\n                    } else {\n                        furthest = i;\n                        restore();\n                        if (value && !tryAnonymous) {\n                            return this.rule(true);\n                        }\n                    }\n                }\n            },\n            anonymousValue: function () {\n                var match;\n                if (match = /^([^@+\\/'\"*`(;{}-]*);/.exec(chunks[j])) {\n                    i += match[0].length - 1;\n                    return new(tree.Anonymous)(match[1]);\n                }\n            },\n\n            //\n            // An @import directive\n            //\n            //     @import \"lib\";\n            //\n            // Depending on our environemnt, importing is done differently:\n            // In the browser, it's an XHR request, in Node, it would be a\n            // file-system operation. The function used for importing is\n            // stored in `import`, which we pass to the Import constructor.\n            //\n            \"import\": function () {\n                var path, features, index = i;\n\n                save();\n\n                var dir = $(/^@import?\\s+/);\n\n                var options = (dir ? $(this.importOptions) : null) || {};\n\n                if (dir && (path = $(this.entities.quoted) || $(this.entities.url))) {\n                    features = $(this.mediaFeatures);\n                    if ($(';')) {\n                        features = features && new(tree.Value)(features);\n                        return new(tree.Import)(path, features, options, index, env.currentFileInfo);\n                    }\n                }\n\n                restore();\n            },\n\n            importOptions: function() {\n                var o, options = {}, optionName, value;\n\n                // list of options, surrounded by parens\n                if (! $('(')) { return null; }\n                do {\n                    if (o = $(this.importOption)) {\n                        optionName = o;\n                        value = true;\n                        switch(optionName) {\n                            case \"css\":\n                                optionName = \"less\";\n                                value = false;\n                            break;\n                            case \"once\":\n                                optionName = \"multiple\";\n                                value = false;\n                            break;\n                        }\n                        options[optionName] = value;\n                        if (! $(',')) { break }\n                    }\n                } while (o);\n                expect(')');\n                return options;\n            },\n\n            importOption: function() {\n                var opt = $(/^(less|css|multiple|once)/);\n                if (opt) {\n                    return opt[1];\n                }\n            },\n\n            mediaFeature: function () {\n                var e, p, nodes = [];\n\n                do {\n                    if (e = $(this.entities.keyword)) {\n                        nodes.push(e);\n                    } else if ($('(')) {\n                        p = $(this.property);\n                        e = $(this.value);\n                        if ($(')')) {\n                            if (p && e) {\n                                nodes.push(new(tree.Paren)(new(tree.Rule)(p, e, null, i, env.currentFileInfo, true)));\n                            } else if (e) {\n                                nodes.push(new(tree.Paren)(e));\n                            } else {\n                                return null;\n                            }\n                        } else { return null }\n                    }\n                } while (e);\n\n                if (nodes.length > 0) {\n                    return new(tree.Expression)(nodes);\n                }\n            },\n\n            mediaFeatures: function () {\n                var e, features = [];\n\n                do {\n                  if (e = $(this.mediaFeature)) {\n                      features.push(e);\n                      if (! $(',')) { break }\n                  } else if (e = $(this.entities.variable)) {\n                      features.push(e);\n                      if (! $(',')) { break }\n                  }\n                } while (e);\n\n                return features.length > 0 ? features : null;\n            },\n\n            media: function () {\n                var features, rules, media, debugInfo;\n\n                if (env.dumpLineNumbers)\n                    debugInfo = getDebugInfo(i, input, env);\n\n                if ($(/^@media/)) {\n                    features = $(this.mediaFeatures);\n\n                    if (rules = $(this.block)) {\n                        media = new(tree.Media)(rules, features);\n                        if(env.dumpLineNumbers)\n                            media.debugInfo = debugInfo;\n                        return media;\n                    }\n                }\n            },\n\n            //\n            // A CSS Directive\n            //\n            //     @charset \"utf-8\";\n            //\n            directive: function () {\n                var name, value, rules, identifier, e, nodes, nonVendorSpecificName,\n                    hasBlock, hasIdentifier, hasExpression;\n\n                if (input.charAt(i) !== '@') return;\n\n                if (value = $(this['import']) || $(this.media)) {\n                    return value;\n                }\n\n                save();\n\n                name = $(/^@[a-z-]+/);\n                \n                if (!name) return;\n\n                nonVendorSpecificName = name;\n                if (name.charAt(1) == '-' && name.indexOf('-', 2) > 0) {\n                    nonVendorSpecificName = \"@\" + name.slice(name.indexOf('-', 2) + 1);\n                }\n\n                switch(nonVendorSpecificName) {\n                    case \"@font-face\":\n                        hasBlock = true;\n                        break;\n                    case \"@viewport\":\n                    case \"@top-left\":\n                    case \"@top-left-corner\":\n                    case \"@top-center\":\n                    case \"@top-right\":\n                    case \"@top-right-corner\":\n                    case \"@bottom-left\":\n                    case \"@bottom-left-corner\":\n                    case \"@bottom-center\":\n                    case \"@bottom-right\":\n                    case \"@bottom-right-corner\":\n                    case \"@left-top\":\n                    case \"@left-middle\":\n                    case \"@left-bottom\":\n                    case \"@right-top\":\n                    case \"@right-middle\":\n                    case \"@right-bottom\":\n                        hasBlock = true;\n                        break;\n                    case \"@page\":\n                    case \"@document\":\n                    case \"@supports\":\n                    case \"@keyframes\":\n                        hasBlock = true;\n                        hasIdentifier = true;\n                        break;\n                    case \"@namespace\":\n                        hasExpression = true;\n                        break;\n                }\n\n                if (hasIdentifier) {\n                    name += \" \" + ($(/^[^{]+/) || '').trim();\n                }\n\n                if (hasBlock)\n                {\n                    if (rules = $(this.block)) {\n                        return new(tree.Directive)(name, rules);\n                    }\n                } else {\n                    if ((value = hasExpression ? $(this.expression) : $(this.entity)) && $(';')) {\n                        var directive = new(tree.Directive)(name, value);\n                        if (env.dumpLineNumbers) {\n                            directive.debugInfo = getDebugInfo(i, input, env);\n                        }\n                        return directive;\n                    }\n                }\n\n                restore();\n            },\n\n            //\n            // A Value is a comma-delimited list of Expressions\n            //\n            //     font-family: Baskerville, Georgia, serif;\n            //\n            // In a Rule, a Value represents everything after the `:`,\n            // and before the `;`.\n            //\n            value: function () {\n                var e, expressions = [], important;\n\n                while (e = $(this.expression)) {\n                    expressions.push(e);\n                    if (! $(',')) { break }\n                }\n\n                if (expressions.length > 0) {\n                    return new(tree.Value)(expressions);\n                }\n            },\n            important: function () {\n                if (input.charAt(i) === '!') {\n                    return $(/^! *important/);\n                }\n            },\n            sub: function () {\n                var a, e;\n\n                if ($('(')) {\n                    if (a = $(this.addition)) {\n                        e = new(tree.Expression)([a]);\n                        expect(')');\n                        e.parens = true;\n                        return e;\n                    }\n                }\n            },\n            multiplication: function () {\n                var m, a, op, operation, isSpaced, expression = [];\n                if (m = $(this.operand)) {\n                    isSpaced = isWhitespace(input.charAt(i - 1));\n                    while (!peek(/^\\/[*\\/]/) && (op = ($('/') || $('*')))) {\n                        if (a = $(this.operand)) {\n                            m.parensInOp = true;\n                            a.parensInOp = true;\n                            operation = new(tree.Operation)(op, [operation || m, a], isSpaced);\n                            isSpaced = isWhitespace(input.charAt(i - 1));\n                        } else {\n                            break;\n                        }\n                    }\n                    return operation || m;\n                }\n            },\n            addition: function () {\n                var m, a, op, operation, isSpaced;\n                if (m = $(this.multiplication)) {\n                    isSpaced = isWhitespace(input.charAt(i - 1));\n                    while ((op = $(/^[-+]\\s+/) || (!isSpaced && ($('+') || $('-')))) &&\n                           (a = $(this.multiplication))) {\n                        m.parensInOp = true;\n                        a.parensInOp = true;\n                        operation = new(tree.Operation)(op, [operation || m, a], isSpaced);\n                        isSpaced = isWhitespace(input.charAt(i - 1));\n                    }\n                    return operation || m;\n                }\n            },\n            conditions: function () {\n                var a, b, index = i, condition;\n\n                if (a = $(this.condition)) {\n                    while ($(',') && (b = $(this.condition))) {\n                        condition = new(tree.Condition)('or', condition || a, b, index);\n                    }\n                    return condition || a;\n                }\n            },\n            condition: function () {\n                var a, b, c, op, index = i, negate = false;\n\n                if ($(/^not/)) { negate = true }\n                expect('(');\n                if (a = $(this.addition) || $(this.entities.keyword) || $(this.entities.quoted)) {\n                    if (op = $(/^(?:>=|=<|[<=>])/)) {\n                        if (b = $(this.addition) || $(this.entities.keyword) || $(this.entities.quoted)) {\n                            c = new(tree.Condition)(op, a, b, index, negate);\n                        } else {\n                            error('expected expression');\n                        }\n                    } else {\n                        c = new(tree.Condition)('=', a, new(tree.Keyword)('true'), index, negate);\n                    }\n                    expect(')');\n                    return $(/^and/) ? new(tree.Condition)('and', c, $(this.condition)) : c;\n                }\n            },\n\n            //\n            // An operand is anything that can be part of an operation,\n            // such as a Color, or a Variable\n            //\n            operand: function () {\n                var negate, p = input.charAt(i + 1);\n\n                if (input.charAt(i) === '-' && (p === '@' || p === '(')) { negate = $('-') }\n                var o = $(this.sub) || $(this.entities.dimension) ||\n                        $(this.entities.color) || $(this.entities.variable) ||\n                        $(this.entities.call);\n\n                if (negate) {\n                    o.parensInOp = true;\n                    o = new(tree.Negative)(o);\n                }\n\n                return o;\n            },\n\n            //\n            // Expressions either represent mathematical operations,\n            // or white-space delimited Entities.\n            //\n            //     1px solid black\n            //     @var * 2\n            //\n            expression: function () {\n                var e, delim, entities = [], d;\n\n                while (e = $(this.addition) || $(this.entity)) {\n                    entities.push(e);\n                    // operations do not allow keyword \"/\" dimension (e.g. small/20px) so we support that here\n                    if (!peek(/^\\/[\\/*]/) && (delim = $('/'))) {\n                        entities.push(new(tree.Anonymous)(delim));\n                    }\n                }\n                if (entities.length > 0) {\n                    return new(tree.Expression)(entities);\n                }\n            },\n            property: function () {\n                var name;\n\n                if (name = $(/^(\\*?-?[_a-zA-Z0-9-]+)\\s*:/)) {\n                    return name[1];\n                }\n            }\n        }\n    };\n};\n\nif (less.mode === 'browser' || less.mode === 'rhino') {\n    //\n    // Used by `@import` directives\n    //\n    less.Parser.importer = function (path, currentFileInfo, callback, env) {\n        if (!/^([a-z-]+:)?\\//.test(path) && currentFileInfo.currentDirectory) {\n            path = currentFileInfo.currentDirectory + path;\n        }\n        var sheetEnv = env.toSheet(path);\n        sheetEnv.processImports = false;\n        sheetEnv.currentFileInfo = currentFileInfo;\n\n        // We pass `true` as 3rd argument, to force the reload of the import.\n        // This is so we can get the syntax tree as opposed to just the CSS output,\n        // as we need this to evaluate the current stylesheet.\n        loadStyleSheet(sheetEnv,\n            function (e, root, data, sheet, _, path) {\n                callback.call(null, e, root, path);\n            }, true);\n    };\n}\n\n(function (tree) {\n\ntree.functions = {\n    rgb: function (r, g, b) {\n        return this.rgba(r, g, b, 1.0);\n    },\n    rgba: function (r, g, b, a) {\n        var rgb = [r, g, b].map(function (c) { return scaled(c, 256); });\n        a = number(a);\n        return new(tree.Color)(rgb, a);\n    },\n    hsl: function (h, s, l) {\n        return this.hsla(h, s, l, 1.0);\n    },\n    hsla: function (h, s, l, a) {\n        h = (number(h) % 360) / 360;\n        s = clamp(number(s)); l = clamp(number(l)); a = clamp(number(a));\n\n        var m2 = l <= 0.5 ? l * (s + 1) : l + s - l * s;\n        var m1 = l * 2 - m2;\n\n        return this.rgba(hue(h + 1/3) * 255,\n                         hue(h)       * 255,\n                         hue(h - 1/3) * 255,\n                         a);\n\n        function hue(h) {\n            h = h < 0 ? h + 1 : (h > 1 ? h - 1 : h);\n            if      (h * 6 < 1) return m1 + (m2 - m1) * h * 6;\n            else if (h * 2 < 1) return m2;\n            else if (h * 3 < 2) return m1 + (m2 - m1) * (2/3 - h) * 6;\n            else                return m1;\n        }\n    },\n\n    hsv: function(h, s, v) {\n        return this.hsva(h, s, v, 1.0);\n    },\n\n    hsva: function(h, s, v, a) {\n        h = ((number(h) % 360) / 360) * 360;\n        s = number(s); v = number(v); a = number(a);\n\n        var i, f;\n        i = Math.floor((h / 60) % 6);\n        f = (h / 60) - i;\n\n        var vs = [v,\n                  v * (1 - s),\n                  v * (1 - f * s),\n                  v * (1 - (1 - f) * s)];\n        var perm = [[0, 3, 1],\n                    [2, 0, 1],\n                    [1, 0, 3],\n                    [1, 2, 0],\n                    [3, 1, 0],\n                    [0, 1, 2]];\n\n        return this.rgba(vs[perm[i][0]] * 255,\n                         vs[perm[i][1]] * 255,\n                         vs[perm[i][2]] * 255,\n                         a);\n    },\n\n    hue: function (color) {\n        return new(tree.Dimension)(Math.round(color.toHSL().h));\n    },\n    saturation: function (color) {\n        return new(tree.Dimension)(Math.round(color.toHSL().s * 100), '%');\n    },\n    lightness: function (color) {\n        return new(tree.Dimension)(Math.round(color.toHSL().l * 100), '%');\n    },\n    hsvhue: function(color) {\n        return new(tree.Dimension)(Math.round(color.toHSV().h));\n    },\n    hsvsaturation: function (color) {\n        return new(tree.Dimension)(Math.round(color.toHSV().s * 100), '%');\n    },\n    hsvvalue: function (color) {\n        return new(tree.Dimension)(Math.round(color.toHSV().v * 100), '%');\n    },\n    red: function (color) {\n        return new(tree.Dimension)(color.rgb[0]);\n    },\n    green: function (color) {\n        return new(tree.Dimension)(color.rgb[1]);\n    },\n    blue: function (color) {\n        return new(tree.Dimension)(color.rgb[2]);\n    },\n    alpha: function (color) {\n        return new(tree.Dimension)(color.toHSL().a);\n    },\n    luma: function (color) {\n        return new(tree.Dimension)(Math.round(color.luma() * color.alpha * 100), '%');\n    },\n    saturate: function (color, amount) {\n        var hsl = color.toHSL();\n\n        hsl.s += amount.value / 100;\n        hsl.s = clamp(hsl.s);\n        return hsla(hsl);\n    },\n    desaturate: function (color, amount) {\n        var hsl = color.toHSL();\n\n        hsl.s -= amount.value / 100;\n        hsl.s = clamp(hsl.s);\n        return hsla(hsl);\n    },\n    lighten: function (color, amount) {\n        var hsl = color.toHSL();\n\n        hsl.l += amount.value / 100;\n        hsl.l = clamp(hsl.l);\n        return hsla(hsl);\n    },\n    darken: function (color, amount) {\n        var hsl = color.toHSL();\n\n        hsl.l -= amount.value / 100;\n        hsl.l = clamp(hsl.l);\n        return hsla(hsl);\n    },\n    fadein: function (color, amount) {\n        var hsl = color.toHSL();\n\n        hsl.a += amount.value / 100;\n        hsl.a = clamp(hsl.a);\n        return hsla(hsl);\n    },\n    fadeout: function (color, amount) {\n        var hsl = color.toHSL();\n\n        hsl.a -= amount.value / 100;\n        hsl.a = clamp(hsl.a);\n        return hsla(hsl);\n    },\n    fade: function (color, amount) {\n        var hsl = color.toHSL();\n\n        hsl.a = amount.value / 100;\n        hsl.a = clamp(hsl.a);\n        return hsla(hsl);\n    },\n    spin: function (color, amount) {\n        var hsl = color.toHSL();\n        var hue = (hsl.h + amount.value) % 360;\n\n        hsl.h = hue < 0 ? 360 + hue : hue;\n\n        return hsla(hsl);\n    },\n    //\n    // Copyright (c) 2006-2009 Hampton Catlin, Nathan Weizenbaum, and Chris Eppstein\n    // http://sass-lang.com\n    //\n    mix: function (color1, color2, weight) {\n        if (!weight) {\n            weight = new(tree.Dimension)(50);\n        }\n        var p = weight.value / 100.0;\n        var w = p * 2 - 1;\n        var a = color1.toHSL().a - color2.toHSL().a;\n\n        var w1 = (((w * a == -1) ? w : (w + a) / (1 + w * a)) + 1) / 2.0;\n        var w2 = 1 - w1;\n\n        var rgb = [color1.rgb[0] * w1 + color2.rgb[0] * w2,\n                   color1.rgb[1] * w1 + color2.rgb[1] * w2,\n                   color1.rgb[2] * w1 + color2.rgb[2] * w2];\n\n        var alpha = color1.alpha * p + color2.alpha * (1 - p);\n\n        return new(tree.Color)(rgb, alpha);\n    },\n    greyscale: function (color) {\n        return this.desaturate(color, new(tree.Dimension)(100));\n    },\n    contrast: function (color, dark, light, threshold) {\n        // filter: contrast(3.2);\n        // should be kept as is, so check for color\n        if (!color.rgb) {\n            return null;\n        }\n        if (typeof light === 'undefined') {\n            light = this.rgba(255, 255, 255, 1.0);\n        }\n        if (typeof dark === 'undefined') {\n            dark = this.rgba(0, 0, 0, 1.0);\n        }\n        //Figure out which is actually light and dark!\n        if (dark.luma() > light.luma()) {\n            var t = light;\n            light = dark;\n            dark = t;\n        }\n        if (typeof threshold === 'undefined') {\n            threshold = 0.43;\n        } else {\n            threshold = number(threshold);\n        }\n        if ((color.luma() * color.alpha) < threshold) {\n            return light;\n        } else {\n            return dark;\n        }\n    },\n    e: function (str) {\n        return new(tree.Anonymous)(str instanceof tree.JavaScript ? str.evaluated : str);\n    },\n    escape: function (str) {\n        return new(tree.Anonymous)(encodeURI(str.value).replace(/=/g, \"%3D\").replace(/:/g, \"%3A\").replace(/#/g, \"%23\").replace(/;/g, \"%3B\").replace(/\\(/g, \"%28\").replace(/\\)/g, \"%29\"));\n    },\n    '%': function (quoted /* arg, arg, ...*/) {\n        var args = Array.prototype.slice.call(arguments, 1),\n            str = quoted.value;\n\n        for (var i = 0; i < args.length; i++) {\n            str = str.replace(/%[sda]/i, function(token) {\n                var value = token.match(/s/i) ? args[i].value : args[i].toCSS();\n                return token.match(/[A-Z]$/) ? encodeURIComponent(value) : value;\n            });\n        }\n        str = str.replace(/%%/g, '%');\n        return new(tree.Quoted)('\"' + str + '\"', str);\n    },\n    unit: function (val, unit) {\n        return new(tree.Dimension)(val.value, unit ? unit.toCSS() : \"\");\n    },\n    convert: function (val, unit) {\n        return val.convertTo(unit.value);\n    },\n    round: function (n, f) {\n        var fraction = typeof(f) === \"undefined\" ? 0 : f.value;\n        return this._math(function(num) { return num.toFixed(fraction); }, null, n);\n    },\n    pi: function () {\n        return new(tree.Dimension)(Math.PI);\n    },\n    mod: function(a, b) {\n        return new(tree.Dimension)(a.value % b.value, a.unit);\n    },\n    pow: function(x, y) {\n        if (typeof x === \"number\" && typeof y === \"number\") {\n            x = new(tree.Dimension)(x);\n            y = new(tree.Dimension)(y);\n        } else if (!(x instanceof tree.Dimension) || !(y instanceof tree.Dimension)) {\n            throw { type: \"Argument\", message: \"arguments must be numbers\" };\n        }\n\n        return new(tree.Dimension)(Math.pow(x.value, y.value), x.unit);\n    },\n    _math: function (fn, unit, n) {\n        if (n instanceof tree.Dimension) {\n            return new(tree.Dimension)(fn(parseFloat(n.value)), unit == null ? n.unit : unit);\n        } else if (typeof(n) === 'number') {\n            return fn(n);\n        } else {\n            throw { type: \"Argument\", message: \"argument must be a number\" };\n        }\n    },\n    argb: function (color) {\n        return new(tree.Anonymous)(color.toARGB());\n\n    },\n    percentage: function (n) {\n        return new(tree.Dimension)(n.value * 100, '%');\n    },\n    color: function (n) {\n        if (n instanceof tree.Quoted) {\n            return new(tree.Color)(n.value.slice(1));\n        } else {\n            throw { type: \"Argument\", message: \"argument must be a string\" };\n        }\n    },\n    iscolor: function (n) {\n        return this._isa(n, tree.Color);\n    },\n    isnumber: function (n) {\n        return this._isa(n, tree.Dimension);\n    },\n    isstring: function (n) {\n        return this._isa(n, tree.Quoted);\n    },\n    iskeyword: function (n) {\n        return this._isa(n, tree.Keyword);\n    },\n    isurl: function (n) {\n        return this._isa(n, tree.URL);\n    },\n    ispixel: function (n) {\n        return this.isunit(n, 'px');\n    },\n    ispercentage: function (n) {\n        return this.isunit(n, '%');\n    },\n    isem: function (n) {\n        return this.isunit(n, 'em');\n    },\n    isunit: function (n, unit) {\n        return (n instanceof tree.Dimension) && n.unit.is(unit.value || unit) ? tree.True : tree.False;\n    },\n    _isa: function (n, Type) {\n        return (n instanceof Type) ? tree.True : tree.False;\n    },\n    \n    /* Blending modes */\n    \n    multiply: function(color1, color2) {\n        var r = color1.rgb[0] * color2.rgb[0] / 255;\n        var g = color1.rgb[1] * color2.rgb[1] / 255;\n        var b = color1.rgb[2] * color2.rgb[2] / 255;\n        return this.rgb(r, g, b);\n    },\n    screen: function(color1, color2) {\n        var r = 255 - (255 - color1.rgb[0]) * (255 - color2.rgb[0]) / 255;\n        var g = 255 - (255 - color1.rgb[1]) * (255 - color2.rgb[1]) / 255;\n        var b = 255 - (255 - color1.rgb[2]) * (255 - color2.rgb[2]) / 255;\n        return this.rgb(r, g, b);\n    },\n    overlay: function(color1, color2) {\n        var r = color1.rgb[0] < 128 ? 2 * color1.rgb[0] * color2.rgb[0] / 255 : 255 - 2 * (255 - color1.rgb[0]) * (255 - color2.rgb[0]) / 255;\n        var g = color1.rgb[1] < 128 ? 2 * color1.rgb[1] * color2.rgb[1] / 255 : 255 - 2 * (255 - color1.rgb[1]) * (255 - color2.rgb[1]) / 255;\n        var b = color1.rgb[2] < 128 ? 2 * color1.rgb[2] * color2.rgb[2] / 255 : 255 - 2 * (255 - color1.rgb[2]) * (255 - color2.rgb[2]) / 255;\n        return this.rgb(r, g, b);\n    },\n    softlight: function(color1, color2) {\n        var t = color2.rgb[0] * color1.rgb[0] / 255;\n        var r = t + color1.rgb[0] * (255 - (255 - color1.rgb[0]) * (255 - color2.rgb[0]) / 255 - t) / 255;\n        t = color2.rgb[1] * color1.rgb[1] / 255;\n        var g = t + color1.rgb[1] * (255 - (255 - color1.rgb[1]) * (255 - color2.rgb[1]) / 255 - t) / 255;\n        t = color2.rgb[2] * color1.rgb[2] / 255;\n        var b = t + color1.rgb[2] * (255 - (255 - color1.rgb[2]) * (255 - color2.rgb[2]) / 255 - t) / 255;\n        return this.rgb(r, g, b);\n    },\n    hardlight: function(color1, color2) {\n        var r = color2.rgb[0] < 128 ? 2 * color2.rgb[0] * color1.rgb[0] / 255 : 255 - 2 * (255 - color2.rgb[0]) * (255 - color1.rgb[0]) / 255;\n        var g = color2.rgb[1] < 128 ? 2 * color2.rgb[1] * color1.rgb[1] / 255 : 255 - 2 * (255 - color2.rgb[1]) * (255 - color1.rgb[1]) / 255;\n        var b = color2.rgb[2] < 128 ? 2 * color2.rgb[2] * color1.rgb[2] / 255 : 255 - 2 * (255 - color2.rgb[2]) * (255 - color1.rgb[2]) / 255;\n        return this.rgb(r, g, b);\n    },\n    difference: function(color1, color2) {\n        var r = Math.abs(color1.rgb[0] - color2.rgb[0]);\n        var g = Math.abs(color1.rgb[1] - color2.rgb[1]);\n        var b = Math.abs(color1.rgb[2] - color2.rgb[2]);\n        return this.rgb(r, g, b);\n    },\n    exclusion: function(color1, color2) {\n        var r = color1.rgb[0] + color2.rgb[0] * (255 - color1.rgb[0] - color1.rgb[0]) / 255;\n        var g = color1.rgb[1] + color2.rgb[1] * (255 - color1.rgb[1] - color1.rgb[1]) / 255;\n        var b = color1.rgb[2] + color2.rgb[2] * (255 - color1.rgb[2] - color1.rgb[2]) / 255;\n        return this.rgb(r, g, b);\n    },\n    average: function(color1, color2) {\n        var r = (color1.rgb[0] + color2.rgb[0]) / 2;\n        var g = (color1.rgb[1] + color2.rgb[1]) / 2;\n        var b = (color1.rgb[2] + color2.rgb[2]) / 2;\n        return this.rgb(r, g, b);\n    },\n    negation: function(color1, color2) {\n        var r = 255 - Math.abs(255 - color2.rgb[0] - color1.rgb[0]);\n        var g = 255 - Math.abs(255 - color2.rgb[1] - color1.rgb[1]);\n        var b = 255 - Math.abs(255 - color2.rgb[2] - color1.rgb[2]);\n        return this.rgb(r, g, b);\n    },\n    tint: function(color, amount) {\n        return this.mix(this.rgb(255,255,255), color, amount);\n    },\n    shade: function(color, amount) {\n        return this.mix(this.rgb(0, 0, 0), color, amount);\n    },\n    extract: function(values, index) {\n        index = index.value - 1; // (1-based index)\n        return values.value[index];\n    },\n\n    \"data-uri\": function(mimetypeNode, filePathNode) {\n\n        if (typeof window !== 'undefined') {\n            return new tree.URL(filePathNode || mimetypeNode, this.currentFileInfo).eval(this.env);\n        }\n\n        var mimetype = mimetypeNode.value;\n        var filePath = (filePathNode && filePathNode.value);\n\n        var fs = require(\"fs\"),\n            path = require(\"path\"),\n            useBase64 = false;\n\n        if (arguments.length < 2) {\n            filePath = mimetype;\n        }\n\n        if (this.env.isPathRelative(filePath)) {\n            if (this.currentFileInfo.relativeUrls) {\n                filePath = path.join(this.currentFileInfo.currentDirectory, filePath);\n            } else {\n                filePath = path.join(this.currentFileInfo.entryPath, filePath);\n            }\n        }\n\n        // detect the mimetype if not given\n        if (arguments.length < 2) {\n            var mime;\n            try {\n                mime = require('mime');\n            } catch (ex) {\n                mime = tree._mime;\n            }\n\n            mimetype = mime.lookup(filePath);\n\n            // use base 64 unless it's an ASCII or UTF-8 format\n            var charset = mime.charsets.lookup(mimetype);\n            useBase64 = ['US-ASCII', 'UTF-8'].indexOf(charset) < 0;\n            if (useBase64) mimetype += ';base64';\n        }\n        else {\n            useBase64 = /;base64$/.test(mimetype)\n        }\n\n        var buf = fs.readFileSync(filePath);\n\n        // IE8 cannot handle a data-uri larger than 32KB. If this is exceeded\n        // and the --ieCompat flag is enabled, return a normal url() instead.\n        var DATA_URI_MAX_KB = 32,\n            fileSizeInKB = parseInt((buf.length / 1024), 10);\n        if (fileSizeInKB >= DATA_URI_MAX_KB) {\n\n            if (this.env.ieCompat !== false) {\n                if (!this.env.silent) {\n                    console.warn(\"Skipped data-uri embedding of %s because its size (%dKB) exceeds IE8-safe %dKB!\", filePath, fileSizeInKB, DATA_URI_MAX_KB);\n                }\n\n                return new tree.URL(filePathNode || mimetypeNode, this.currentFileInfo).eval(this.env);\n            } else if (!this.env.silent) {\n                // if explicitly disabled (via --no-ie-compat on CLI, or env.ieCompat === false), merely warn\n                console.warn(\"WARNING: Embedding %s (%dKB) exceeds IE8's data-uri size limit of %dKB!\", filePath, fileSizeInKB, DATA_URI_MAX_KB);\n            }\n        }\n\n        buf = useBase64 ? buf.toString('base64')\n                        : encodeURIComponent(buf);\n\n        var uri = \"'data:\" + mimetype + ',' + buf + \"'\";\n        return new(tree.URL)(new(tree.Anonymous)(uri));\n    }\n};\n\n// these static methods are used as a fallback when the optional 'mime' dependency is missing\ntree._mime = {\n    // this map is intentionally incomplete\n    // if you want more, install 'mime' dep\n    _types: {\n        '.htm' : 'text/html',\n        '.html': 'text/html',\n        '.gif' : 'image/gif',\n        '.jpg' : 'image/jpeg',\n        '.jpeg': 'image/jpeg',\n        '.png' : 'image/png'\n    },\n    lookup: function (filepath) {\n        var ext = require('path').extname(filepath),\n            type = tree._mime._types[ext];\n        if (type === undefined) {\n            throw new Error('Optional dependency \"mime\" is required for ' + ext);\n        }\n        return type;\n    },\n    charsets: {\n        lookup: function (type) {\n            // assumes all text types are UTF-8\n            return type && (/^text\\//).test(type) ? 'UTF-8' : '';\n        }\n    }\n};\n\nvar mathFunctions = [{name:\"ceil\"}, {name:\"floor\"}, {name: \"sqrt\"}, {name:\"abs\"},\n        {name:\"tan\", unit: \"\"}, {name:\"sin\", unit: \"\"}, {name:\"cos\", unit: \"\"},\n        {name:\"atan\", unit: \"rad\"}, {name:\"asin\", unit: \"rad\"}, {name:\"acos\", unit: \"rad\"}],\n    createMathFunction = function(name, unit) {\n        return function(n) {\n            if (unit != null) {\n                n = n.unify();\n            }\n            return this._math(Math[name], unit, n);\n        };\n    };\n\nfor(var i = 0; i < mathFunctions.length; i++) {\n    tree.functions[mathFunctions[i].name] = createMathFunction(mathFunctions[i].name, mathFunctions[i].unit);\n}\n\nfunction hsla(color) {\n    return tree.functions.hsla(color.h, color.s, color.l, color.a);\n}\n\nfunction scaled(n, size) {\n    if (n instanceof tree.Dimension && n.unit.is('%')) {\n        return parseFloat(n.value * size / 100);\n    } else {\n        return number(n);\n    }\n}\n\nfunction number(n) {\n    if (n instanceof tree.Dimension) {\n        return parseFloat(n.unit.is('%') ? n.value / 100 : n.value);\n    } else if (typeof(n) === 'number') {\n        return n;\n    } else {\n        throw {\n            error: \"RuntimeError\",\n            message: \"color functions take numbers as parameters\"\n        };\n    }\n}\n\nfunction clamp(val) {\n    return Math.min(1, Math.max(0, val));\n}\n\ntree.functionCall = function(env, currentFileInfo) {\n    this.env = env;\n    this.currentFileInfo = currentFileInfo;\n};\n\ntree.functionCall.prototype = tree.functions;\n\n})(require('./tree'));\n(function (tree) {\n    tree.colors = {\n        'aliceblue':'#f0f8ff',\n        'antiquewhite':'#faebd7',\n        'aqua':'#00ffff',\n        'aquamarine':'#7fffd4',\n        'azure':'#f0ffff',\n        'beige':'#f5f5dc',\n        'bisque':'#ffe4c4',\n        'black':'#000000',\n        'blanchedalmond':'#ffebcd',\n        'blue':'#0000ff',\n        'blueviolet':'#8a2be2',\n        'brown':'#a52a2a',\n        'burlywood':'#deb887',\n        'cadetblue':'#5f9ea0',\n        'chartreuse':'#7fff00',\n        'chocolate':'#d2691e',\n        'coral':'#ff7f50',\n        'cornflowerblue':'#6495ed',\n        'cornsilk':'#fff8dc',\n        'crimson':'#dc143c',\n        'cyan':'#00ffff',\n        'darkblue':'#00008b',\n        'darkcyan':'#008b8b',\n        'darkgoldenrod':'#b8860b',\n        'darkgray':'#a9a9a9',\n        'darkgrey':'#a9a9a9',\n        'darkgreen':'#006400',\n        'darkkhaki':'#bdb76b',\n        'darkmagenta':'#8b008b',\n        'darkolivegreen':'#556b2f',\n        'darkorange':'#ff8c00',\n        'darkorchid':'#9932cc',\n        'darkred':'#8b0000',\n        'darksalmon':'#e9967a',\n        'darkseagreen':'#8fbc8f',\n        'darkslateblue':'#483d8b',\n        'darkslategray':'#2f4f4f',\n        'darkslategrey':'#2f4f4f',\n        'darkturquoise':'#00ced1',\n        'darkviolet':'#9400d3',\n        'deeppink':'#ff1493',\n        'deepskyblue':'#00bfff',\n        'dimgray':'#696969',\n        'dimgrey':'#696969',\n        'dodgerblue':'#1e90ff',\n        'firebrick':'#b22222',\n        'floralwhite':'#fffaf0',\n        'forestgreen':'#228b22',\n        'fuchsia':'#ff00ff',\n        'gainsboro':'#dcdcdc',\n        'ghostwhite':'#f8f8ff',\n        'gold':'#ffd700',\n        'goldenrod':'#daa520',\n        'gray':'#808080',\n        'grey':'#808080',\n        'green':'#008000',\n        'greenyellow':'#adff2f',\n        'honeydew':'#f0fff0',\n        'hotpink':'#ff69b4',\n        'indianred':'#cd5c5c',\n        'indigo':'#4b0082',\n        'ivory':'#fffff0',\n        'khaki':'#f0e68c',\n        'lavender':'#e6e6fa',\n        'lavenderblush':'#fff0f5',\n        'lawngreen':'#7cfc00',\n        'lemonchiffon':'#fffacd',\n        'lightblue':'#add8e6',\n        'lightcoral':'#f08080',\n        'lightcyan':'#e0ffff',\n        'lightgoldenrodyellow':'#fafad2',\n        'lightgray':'#d3d3d3',\n        'lightgrey':'#d3d3d3',\n        'lightgreen':'#90ee90',\n        'lightpink':'#ffb6c1',\n        'lightsalmon':'#ffa07a',\n        'lightseagreen':'#20b2aa',\n        'lightskyblue':'#87cefa',\n        'lightslategray':'#778899',\n        'lightslategrey':'#778899',\n        'lightsteelblue':'#b0c4de',\n        'lightyellow':'#ffffe0',\n        'lime':'#00ff00',\n        'limegreen':'#32cd32',\n        'linen':'#faf0e6',\n        'magenta':'#ff00ff',\n        'maroon':'#800000',\n        'mediumaquamarine':'#66cdaa',\n        'mediumblue':'#0000cd',\n        'mediumorchid':'#ba55d3',\n        'mediumpurple':'#9370d8',\n        'mediumseagreen':'#3cb371',\n        'mediumslateblue':'#7b68ee',\n        'mediumspringgreen':'#00fa9a',\n        'mediumturquoise':'#48d1cc',\n        'mediumvioletred':'#c71585',\n        'midnightblue':'#191970',\n        'mintcream':'#f5fffa',\n        'mistyrose':'#ffe4e1',\n        'moccasin':'#ffe4b5',\n        'navajowhite':'#ffdead',\n        'navy':'#000080',\n        'oldlace':'#fdf5e6',\n        'olive':'#808000',\n        'olivedrab':'#6b8e23',\n        'orange':'#ffa500',\n        'orangered':'#ff4500',\n        'orchid':'#da70d6',\n        'palegoldenrod':'#eee8aa',\n        'palegreen':'#98fb98',\n        'paleturquoise':'#afeeee',\n        'palevioletred':'#d87093',\n        'papayawhip':'#ffefd5',\n        'peachpuff':'#ffdab9',\n        'peru':'#cd853f',\n        'pink':'#ffc0cb',\n        'plum':'#dda0dd',\n        'powderblue':'#b0e0e6',\n        'purple':'#800080',\n        'red':'#ff0000',\n        'rosybrown':'#bc8f8f',\n        'royalblue':'#4169e1',\n        'saddlebrown':'#8b4513',\n        'salmon':'#fa8072',\n        'sandybrown':'#f4a460',\n        'seagreen':'#2e8b57',\n        'seashell':'#fff5ee',\n        'sienna':'#a0522d',\n        'silver':'#c0c0c0',\n        'skyblue':'#87ceeb',\n        'slateblue':'#6a5acd',\n        'slategray':'#708090',\n        'slategrey':'#708090',\n        'snow':'#fffafa',\n        'springgreen':'#00ff7f',\n        'steelblue':'#4682b4',\n        'tan':'#d2b48c',\n        'teal':'#008080',\n        'thistle':'#d8bfd8',\n        'tomato':'#ff6347',\n        // 'transparent':'rgba(0,0,0,0)',\n        'turquoise':'#40e0d0',\n        'violet':'#ee82ee',\n        'wheat':'#f5deb3',\n        'white':'#ffffff',\n        'whitesmoke':'#f5f5f5',\n        'yellow':'#ffff00',\n        'yellowgreen':'#9acd32'\n    };\n})(require('./tree'));\n(function (tree) {\n\ntree.Alpha = function (val) {\n    this.value = val;\n};\ntree.Alpha.prototype = {\n    type: \"Alpha\",\n    accept: function (visitor) {\n        this.value = visitor.visit(this.value);\n    },\n    eval: function (env) {\n        if (this.value.eval) { this.value = this.value.eval(env) }\n        return this;\n    },\n    toCSS: function () {\n        return \"alpha(opacity=\" +\n               (this.value.toCSS ? this.value.toCSS() : this.value) + \")\";\n    }\n};\n\n})(require('../tree'));\n(function (tree) {\n\ntree.Anonymous = function (string) {\n    this.value = string.value || string;\n};\ntree.Anonymous.prototype = {\n    type: \"Anonymous\",\n    toCSS: function () {\n        return this.value;\n    },\n    eval: function () { return this },\n    compare: function (x) {\n        if (!x.toCSS) {\n            return -1;\n        }\n        \n        var left = this.toCSS(),\n            right = x.toCSS();\n        \n        if (left === right) {\n            return 0;\n        }\n        \n        return left < right ? -1 : 1;\n    }\n};\n\n})(require('../tree'));\n(function (tree) {\n\ntree.Assignment = function (key, val) {\n    this.key = key;\n    this.value = val;\n};\ntree.Assignment.prototype = {\n    type: \"Assignment\",\n    accept: function (visitor) {\n        this.value = visitor.visit(this.value);\n    },\n    toCSS: function () {\n        return this.key + '=' + (this.value.toCSS ? this.value.toCSS() : this.value);\n    },\n    eval: function (env) {\n        if (this.value.eval) {\n            return new(tree.Assignment)(this.key, this.value.eval(env));\n        }\n        return this;\n    }\n};\n\n})(require('../tree'));(function (tree) {\n\n//\n// A function call node.\n//\ntree.Call = function (name, args, index, currentFileInfo) {\n    this.name = name;\n    this.args = args;\n    this.index = index;\n    this.currentFileInfo = currentFileInfo;\n};\ntree.Call.prototype = {\n    type: \"Call\",\n    accept: function (visitor) {\n        this.args = visitor.visit(this.args);\n    },\n    //\n    // When evaluating a function call,\n    // we either find the function in `tree.functions` [1],\n    // in which case we call it, passing the  evaluated arguments,\n    // if this returns null or we cannot find the function, we \n    // simply print it out as it appeared originally [2].\n    //\n    // The *functions.js* file contains the built-in functions.\n    //\n    // The reason why we evaluate the arguments, is in the case where\n    // we try to pass a variable to a function, like: `saturate(@color)`.\n    // The function should receive the value, not the variable.\n    //\n    eval: function (env) {\n        var args = this.args.map(function (a) { return a.eval(env); }),\n            nameLC = this.name.toLowerCase(),\n            result, func;\n\n        if (nameLC in tree.functions) { // 1.\n            try {\n                func = new tree.functionCall(env, this.currentFileInfo);\n                result = func[nameLC].apply(func, args);\n                if (result != null) {\n                    return result;\n                }\n            } catch (e) {\n                throw { type: e.type || \"Runtime\",\n                        message: \"error evaluating function `\" + this.name + \"`\" +\n                                 (e.message ? ': ' + e.message : ''),\n                        index: this.index, filename: this.currentFileInfo.filename };\n            }\n        }\n        \n        // 2.\n        return new(tree.Anonymous)(this.name +\n            \"(\" + args.map(function (a) { return a.toCSS(env); }).join(', ') + \")\");\n    },\n\n    toCSS: function (env) {\n        return this.eval(env).toCSS();\n    }\n};\n\n})(require('../tree'));\n(function (tree) {\n//\n// RGB Colors - #ff0014, #eee\n//\ntree.Color = function (rgb, a) {\n    //\n    // The end goal here, is to parse the arguments\n    // into an integer triplet, such as `128, 255, 0`\n    //\n    // This facilitates operations and conversions.\n    //\n    if (Array.isArray(rgb)) {\n        this.rgb = rgb;\n    } else if (rgb.length == 6) {\n        this.rgb = rgb.match(/.{2}/g).map(function (c) {\n            return parseInt(c, 16);\n        });\n    } else {\n        this.rgb = rgb.split('').map(function (c) {\n            return parseInt(c + c, 16);\n        });\n    }\n    this.alpha = typeof(a) === 'number' ? a : 1;\n};\ntree.Color.prototype = {\n    type: \"Color\",\n    eval: function () { return this },\n    luma: function () { return (0.2126 * this.rgb[0] / 255) + (0.7152 * this.rgb[1] / 255) + (0.0722 * this.rgb[2] / 255); },\n\n    //\n    // If we have some transparency, the only way to represent it\n    // is via `rgba`. Otherwise, we use the hex representation,\n    // which has better compatibility with older browsers.\n    // Values are capped between `0` and `255`, rounded and zero-padded.\n    //\n    toCSS: function (env, doNotCompress) {\n        var compress = env && env.compress && !doNotCompress;\n        if (this.alpha < 1.0) {\n            return \"rgba(\" + this.rgb.map(function (c) {\n                return Math.round(c);\n            }).concat(this.alpha).join(',' + (compress ? '' : ' ')) + \")\";\n        } else {\n            var color = this.rgb.map(function (i) {\n                i = Math.round(i);\n                i = (i > 255 ? 255 : (i < 0 ? 0 : i)).toString(16);\n                return i.length === 1 ? '0' + i : i;\n            }).join('');\n\n            if (compress) {\n                color = color.split('');\n\n                // Convert color to short format\n                if (color[0] == color[1] && color[2] == color[3] && color[4] == color[5]) {\n                    color = color[0] + color[2] + color[4];\n                } else {\n                    color = color.join('');\n                }\n            }\n\n            return '#' + color;\n        }\n    },\n\n    //\n    // Operations have to be done per-channel, if not,\n    // channels will spill onto each other. Once we have\n    // our result, in the form of an integer triplet,\n    // we create a new Color node to hold the result.\n    //\n    operate: function (env, op, other) {\n        var result = [];\n\n        if (! (other instanceof tree.Color)) {\n            other = other.toColor();\n        }\n\n        for (var c = 0; c < 3; c++) {\n            result[c] = tree.operate(env, op, this.rgb[c], other.rgb[c]);\n        }\n        return new(tree.Color)(result, this.alpha + other.alpha);\n    },\n\n    toHSL: function () {\n        var r = this.rgb[0] / 255,\n            g = this.rgb[1] / 255,\n            b = this.rgb[2] / 255,\n            a = this.alpha;\n\n        var max = Math.max(r, g, b), min = Math.min(r, g, b);\n        var h, s, l = (max + min) / 2, d = max - min;\n\n        if (max === min) {\n            h = s = 0;\n        } else {\n            s = l > 0.5 ? d / (2 - max - min) : d / (max + min);\n\n            switch (max) {\n                case r: h = (g - b) / d + (g < b ? 6 : 0); break;\n                case g: h = (b - r) / d + 2;               break;\n                case b: h = (r - g) / d + 4;               break;\n            }\n            h /= 6;\n        }\n        return { h: h * 360, s: s, l: l, a: a };\n    },\n    //Adapted from http://mjijackson.com/2008/02/rgb-to-hsl-and-rgb-to-hsv-color-model-conversion-algorithms-in-javascript\n    toHSV: function () {\n        var r = this.rgb[0] / 255,\n            g = this.rgb[1] / 255,\n            b = this.rgb[2] / 255,\n            a = this.alpha;\n\n        var max = Math.max(r, g, b), min = Math.min(r, g, b);\n        var h, s, v = max;\n\n        var d = max - min;\n        if (max === 0) {\n            s = 0;\n        } else {\n            s = d / max;\n        }\n\n        if (max === min) {\n            h = 0;\n        } else {\n            switch(max){\n                case r: h = (g - b) / d + (g < b ? 6 : 0); break;\n                case g: h = (b - r) / d + 2; break;\n                case b: h = (r - g) / d + 4; break;\n            }\n            h /= 6;\n        }\n        return { h: h * 360, s: s, v: v, a: a };\n    },\n    toARGB: function () {\n        var argb = [Math.round(this.alpha * 255)].concat(this.rgb);\n        return '#' + argb.map(function (i) {\n            i = Math.round(i);\n            i = (i > 255 ? 255 : (i < 0 ? 0 : i)).toString(16);\n            return i.length === 1 ? '0' + i : i;\n        }).join('');\n    },\n    compare: function (x) {\n        if (!x.rgb) {\n            return -1;\n        }\n        \n        return (x.rgb[0] === this.rgb[0] &&\n            x.rgb[1] === this.rgb[1] &&\n            x.rgb[2] === this.rgb[2] &&\n            x.alpha === this.alpha) ? 0 : -1;\n    }\n};\n\n\n})(require('../tree'));\n(function (tree) {\n\ntree.Comment = function (value, silent) {\n    this.value = value;\n    this.silent = !!silent;\n};\ntree.Comment.prototype = {\n    type: \"Comment\",\n    toCSS: function (env) {\n        return env.compress ? '' : this.value;\n    },\n    eval: function () { return this }\n};\n\n})(require('../tree'));\n(function (tree) {\n\ntree.Condition = function (op, l, r, i, negate) {\n    this.op = op.trim();\n    this.lvalue = l;\n    this.rvalue = r;\n    this.index = i;\n    this.negate = negate;\n};\ntree.Condition.prototype = {\n    type: \"Condition\",\n    accept: function (visitor) {\n        this.lvalue = visitor.visit(this.lvalue);\n        this.rvalue = visitor.visit(this.rvalue);\n    },\n    eval: function (env) {\n        var a = this.lvalue.eval(env),\n            b = this.rvalue.eval(env);\n\n        var i = this.index, result;\n\n        var result = (function (op) {\n            switch (op) {\n                case 'and':\n                    return a && b;\n                case 'or':\n                    return a || b;\n                default:\n                    if (a.compare) {\n                        result = a.compare(b);\n                    } else if (b.compare) {\n                        result = b.compare(a);\n                    } else {\n                        throw { type: \"Type\",\n                                message: \"Unable to perform comparison\",\n                                index: i };\n                    }\n                    switch (result) {\n                        case -1: return op === '<' || op === '=<';\n                        case  0: return op === '=' || op === '>=' || op === '=<';\n                        case  1: return op === '>' || op === '>=';\n                    }\n            }\n        })(this.op);\n        return this.negate ? !result : result;\n    }\n};\n\n})(require('../tree'));\n(function (tree) {\n\n//\n// A number with a unit\n//\ntree.Dimension = function (value, unit) {\n    this.value = parseFloat(value);\n    this.unit = (unit && unit instanceof tree.Unit) ? unit :\n      new(tree.Unit)(unit ? [unit] : undefined);\n};\n\ntree.Dimension.prototype = {\n    type: \"Dimension\",\n    accept: function (visitor) {\n        this.unit = visitor.visit(this.unit);\n    },\n    eval: function (env) {\n        return this;\n    },\n    toColor: function () {\n        return new(tree.Color)([this.value, this.value, this.value]);\n    },\n    toCSS: function (env) {\n        if ((env && env.strictUnits) && !this.unit.isSingular()) {\n            throw new Error(\"Multiple units in dimension. Correct the units or use the unit function. Bad unit: \"+this.unit.toString());\n        }\n\n        var value = this.value,\n            strValue = String(value);\n\n        if (value !== 0 && value < 0.000001 && value > -0.000001) {\n            // would be output 1e-6 etc.\n            strValue = value.toFixed(20).replace(/0+$/, \"\");\n        }\n\n        if (env && env.compress) {\n            // Zero values doesn't need a unit\n            if (value === 0 && !this.unit.isAngle()) {\n                return strValue;\n            }\n\n            // Float values doesn't need a leading zero\n            if (value > 0 && value < 1) {\n                strValue = (strValue).substr(1);\n            }\n        }\n\n        return strValue + this.unit.toCSS(env);\n    },\n\n    // In an operation between two Dimensions,\n    // we default to the first Dimension's unit,\n    // so `1px + 2` will yield `3px`.\n    operate: function (env, op, other) {\n        var value = tree.operate(env, op, this.value, other.value),\n            unit = this.unit.clone();\n\n        if (op === '+' || op === '-') {\n            if (unit.numerator.length === 0 && unit.denominator.length === 0) {\n                unit.numerator = other.unit.numerator.slice(0);\n                unit.denominator = other.unit.denominator.slice(0);\n            } else if (other.unit.numerator.length == 0 && unit.denominator.length == 0) {\n                // do nothing\n            } else {\n                other = other.convertTo(this.unit.usedUnits());\n\n                if(env.strictUnits && other.unit.toString() !== unit.toString()) {\n                  throw new Error(\"Incompatible units. Change the units or use the unit function. Bad units: '\" + unit.toString() +\n                    \"' and '\" + other.unit.toString() + \"'.\");\n                }\n\n                value = tree.operate(env, op, this.value, other.value);\n            }\n        } else if (op === '*') {\n            unit.numerator = unit.numerator.concat(other.unit.numerator).sort();\n            unit.denominator = unit.denominator.concat(other.unit.denominator).sort();\n            unit.cancel();\n        } else if (op === '/') {\n            unit.numerator = unit.numerator.concat(other.unit.denominator).sort();\n            unit.denominator = unit.denominator.concat(other.unit.numerator).sort();\n            unit.cancel();\n        }\n        return new(tree.Dimension)(value, unit);\n    },\n\n    compare: function (other) {\n        if (other instanceof tree.Dimension) {\n            var a = this.unify(), b = other.unify(),\n                aValue = a.value, bValue = b.value;\n\n            if (bValue > aValue) {\n                return -1;\n            } else if (bValue < aValue) {\n                return 1;\n            } else {\n                if (!b.unit.isEmpty() && a.unit.compare(b.unit) !== 0) {\n                    return -1;\n                }\n                return 0;\n            }\n        } else {\n            return -1;\n        }\n    },\n\n    unify: function () {\n      return this.convertTo({ length: 'm', duration: 's', angle: 'rad' });\n    },\n\n    convertTo: function (conversions) {\n      var value = this.value, unit = this.unit.clone(),\n          i, groupName, group, conversion, targetUnit, derivedConversions = {};\n\n      if (typeof conversions === 'string') {\n          for(i in tree.UnitConversions) {\n              if (tree.UnitConversions[i].hasOwnProperty(conversions)) {\n                  derivedConversions = {};\n                  derivedConversions[i] = conversions;\n              }\n          }\n          conversions = derivedConversions;\n      }\n\n      for (groupName in conversions) {\n        if (conversions.hasOwnProperty(groupName)) {\n          targetUnit = conversions[groupName];\n          group = tree.UnitConversions[groupName];\n\n          unit.map(function (atomicUnit, denominator) {\n            if (group.hasOwnProperty(atomicUnit)) {\n              if (denominator) {\n                value = value / (group[atomicUnit] / group[targetUnit]);\n              } else {\n                value = value * (group[atomicUnit] / group[targetUnit]);\n              }\n\n              return targetUnit;\n            }\n\n            return atomicUnit;\n          });\n        }\n      }\n\n      unit.cancel();\n\n      return new(tree.Dimension)(value, unit);\n    }\n};\n\n// http://www.w3.org/TR/css3-values/#absolute-lengths\ntree.UnitConversions = {\n  length: {\n     'm': 1,\n    'cm': 0.01,\n    'mm': 0.001,\n    'in': 0.0254,\n    'pt': 0.0254 / 72,\n    'pc': 0.0254 / 72 * 12\n  },\n  duration: {\n    's': 1,\n    'ms': 0.001\n  },\n  angle: {\n    'rad': 1/(2*Math.PI),\n    'deg': 1/360,\n    'grad': 1/400,\n    'turn': 1\n  }\n};\n\ntree.Unit = function (numerator, denominator, backupUnit) {\n  this.numerator = numerator ? numerator.slice(0).sort() : [];\n  this.denominator = denominator ? denominator.slice(0).sort() : [];\n  this.backupUnit = backupUnit;\n};\n\ntree.Unit.prototype = {\n  type: \"Unit\",\n  clone: function () {\n    return new tree.Unit(this.numerator.slice(0), this.denominator.slice(0), this.backupUnit);\n  },\n\n  toCSS: function (env) {\n    if (this.numerator.length >= 1) {\n        return this.numerator[0];\n    }\n    if (this.denominator.length >= 1) {\n        return this.denominator[0];\n    }\n    if ((!env || !env.strictUnits) && this.backupUnit) {\n        return this.backupUnit;\n    }\n    return \"\";\n  },\n\n  toString: function () {\n      var i, returnStr = this.numerator.join(\"*\");\n      for (i = 0; i < this.denominator.length; i++) {\n          returnStr += \"/\" + this.denominator[i];\n      }\n      return returnStr;\n  },\n  \n  compare: function (other) {\n    return this.is(other.toString()) ? 0 : -1;\n  },\n\n  is: function (unitString) {\n    return this.toString() === unitString;\n  },\n\n  isAngle: function () {\n    return tree.UnitConversions.angle.hasOwnProperty(this.toCSS());\n  },\n\n  isEmpty: function () {\n    return this.numerator.length == 0 && this.denominator.length == 0;\n  },\n\n  isSingular: function() {\n      return this.numerator.length <= 1 && this.denominator.length == 0;\n  },\n\n  map: function(callback) {\n    var i;\n\n    for (i = 0; i < this.numerator.length; i++) {\n      this.numerator[i] = callback(this.numerator[i], false);\n    }\n\n    for (i = 0; i < this.denominator.length; i++) {\n      this.denominator[i] = callback(this.denominator[i], true);\n    }\n  },\n\n  usedUnits: function() {\n    var group, groupName, result = {};\n\n    for (groupName in tree.UnitConversions) {\n      if (tree.UnitConversions.hasOwnProperty(groupName)) {\n        group = tree.UnitConversions[groupName];\n\n        this.map(function (atomicUnit) {\n          if (group.hasOwnProperty(atomicUnit) && !result[groupName]) {\n            result[groupName] = atomicUnit;\n          }\n\n          return atomicUnit;\n        });\n      }\n    }\n\n    return result;\n  },\n\n  cancel: function () {\n    var counter = {}, atomicUnit, i, backup;\n\n    for (i = 0; i < this.numerator.length; i++) {\n        atomicUnit = this.numerator[i];\n        if (!backup) {\n            backup = atomicUnit;\n        }\n        counter[atomicUnit] = (counter[atomicUnit] || 0) + 1;\n    }\n\n    for (i = 0; i < this.denominator.length; i++) {\n        atomicUnit = this.denominator[i];\n        if (!backup) {\n            backup = atomicUnit;\n        }\n        counter[atomicUnit] = (counter[atomicUnit] || 0) - 1;\n    }\n\n    this.numerator = [];\n    this.denominator = [];\n\n    for (atomicUnit in counter) {\n      if (counter.hasOwnProperty(atomicUnit)) {\n        var count = counter[atomicUnit];\n\n        if (count > 0) {\n          for (i = 0; i < count; i++) {\n            this.numerator.push(atomicUnit);\n          }\n        } else if (count < 0) {\n          for (i = 0; i < -count; i++) {\n            this.denominator.push(atomicUnit);\n          }\n        }\n      }\n    }\n\n    if (this.numerator.length === 0 && this.denominator.length === 0 && backup) {\n        this.backupUnit = backup;\n    }\n\n    this.numerator.sort();\n    this.denominator.sort();\n  }\n};\n\n})(require('../tree'));\n(function (tree) {\n\ntree.Directive = function (name, value) {\n    this.name = name;\n\n    if (Array.isArray(value)) {\n        this.ruleset = new(tree.Ruleset)([], value);\n        this.ruleset.allowImports = true;\n    } else {\n        this.value = value;\n    }\n};\ntree.Directive.prototype = {\n    type: \"Directive\",\n    accept: function (visitor) {\n        this.ruleset = visitor.visit(this.ruleset);\n        this.value = visitor.visit(this.value);\n    },\n    toCSS: function (env) {\n        if (this.ruleset) {\n            this.ruleset.root = true;\n            return this.name + (env.compress ? '{' : ' {\\n  ') +\n                   this.ruleset.toCSS(env).trim().replace(/\\n/g, '\\n  ') +\n                               (env.compress ? '}': '\\n}\\n');\n        } else {\n            return this.name + ' ' + this.value.toCSS() + ';\\n';\n        }\n    },\n    eval: function (env) {\n        var evaldDirective = this;\n        if (this.ruleset) {\n            env.frames.unshift(this);\n            evaldDirective = new(tree.Directive)(this.name);\n            evaldDirective.ruleset = this.ruleset.eval(env);\n            env.frames.shift();\n        }\n        return evaldDirective;\n    },\n    variable: function (name) { return tree.Ruleset.prototype.variable.call(this.ruleset, name) },\n    find: function () { return tree.Ruleset.prototype.find.apply(this.ruleset, arguments) },\n    rulesets: function () { return tree.Ruleset.prototype.rulesets.apply(this.ruleset) }\n};\n\n})(require('../tree'));\n(function (tree) {\n\ntree.Element = function (combinator, value, index) {\n    this.combinator = combinator instanceof tree.Combinator ?\n                      combinator : new(tree.Combinator)(combinator);\n\n    if (typeof(value) === 'string') {\n        this.value = value.trim();\n    } else if (value) {\n        this.value = value;\n    } else {\n        this.value = \"\";\n    }\n    this.index = index;\n};\ntree.Element.prototype = {\n    type: \"Element\",\n    accept: function (visitor) {\n        this.combinator = visitor.visit(this.combinator);\n        this.value = visitor.visit(this.value);\n    },\n    eval: function (env) {\n        return new(tree.Element)(this.combinator,\n                                 this.value.eval ? this.value.eval(env) : this.value,\n                                 this.index);\n    },\n    toCSS: function (env) {\n        var value = (this.value.toCSS ? this.value.toCSS(env) : this.value);\n        if (value == '' && this.combinator.value.charAt(0) == '&') {\n            return '';\n        } else {\n            return this.combinator.toCSS(env || {}) + value;\n        }\n    }\n};\n\ntree.Attribute = function (key, op, value) {\n    this.key = key;\n    this.op = op;\n    this.value = value;\n};\ntree.Attribute.prototype = {\n    type: \"Attribute\",\n    accept: function (visitor) {\n        this.value = visitor.visit(this.value);\n    },\n    eval: function (env) {\n        return new(tree.Attribute)(this.key.eval ? this.key.eval(env) : this.key,\n            this.op, (this.value && this.value.eval) ? this.value.eval(env) : this.value);\n    },\n    toCSS: function (env) {\n        var value = this.key.toCSS ? this.key.toCSS(env) : this.key;\n\n        if (this.op) {\n            value += this.op;\n            value += (this.value.toCSS ? this.value.toCSS(env) : this.value);\n        }\n\n        return '[' + value + ']';\n    }\n};\n\ntree.Combinator = function (value) {\n    if (value === ' ') {\n        this.value = ' ';\n    } else {\n        this.value = value ? value.trim() : \"\";\n    }\n};\ntree.Combinator.prototype = {\n    type: \"Combinator\",\n    toCSS: function (env) {\n        return {\n            ''  : '',\n            ' ' : ' ',\n            ':' : ' :',\n            '+' : env.compress ? '+' : ' + ',\n            '~' : env.compress ? '~' : ' ~ ',\n            '>' : env.compress ? '>' : ' > ',\n            '|' : env.compress ? '|' : ' | '\n        }[this.value];\n    }\n};\n\n})(require('../tree'));\n(function (tree) {\n\ntree.Expression = function (value) { this.value = value; };\ntree.Expression.prototype = {\n    type: \"Expression\",\n    accept: function (visitor) {\n        this.value = visitor.visit(this.value);\n    },\n    eval: function (env) {\n        var returnValue,\n            inParenthesis = this.parens && !this.parensInOp,\n            doubleParen = false;\n        if (inParenthesis) {\n            env.inParenthesis();\n        }\n        if (this.value.length > 1) {\n            returnValue = new(tree.Expression)(this.value.map(function (e) {\n                return e.eval(env);\n            }));\n        } else if (this.value.length === 1) {\n            if (this.value[0].parens && !this.value[0].parensInOp) {\n                doubleParen = true;\n            }\n            returnValue = this.value[0].eval(env);\n        } else {\n            returnValue = this;\n        }\n        if (inParenthesis) {\n            env.outOfParenthesis();\n        }\n        if (this.parens && this.parensInOp && !(env.isMathOn()) && !doubleParen) {\n            returnValue = new(tree.Paren)(returnValue);\n        }\n        return returnValue;\n    },\n    toCSS: function (env) {\n        return this.value.map(function (e) {\n            return e.toCSS ? e.toCSS(env) : '';\n        }).join(' ');\n    },\n    throwAwayComments: function () {\n        this.value = this.value.filter(function(v) {\n            return !(v instanceof tree.Comment);\n        });\n    }\n};\n\n})(require('../tree'));\n(function (tree) {\n\ntree.Extend = function Extend(selector, option, index) {\n    this.selector = selector;\n    this.option = option;\n    this.index = index;\n\n    switch(option) {\n        case \"all\":\n            this.allowBefore = true;\n            this.allowAfter = true;\n        break;\n        default:\n            this.allowBefore = false;\n            this.allowAfter = false;\n        break;\n    }\n};\n\ntree.Extend.prototype = {\n    type: \"Extend\",\n    accept: function (visitor) {\n        this.selector = visitor.visit(this.selector);\n    },\n    eval: function (env) {\n        return new(tree.Extend)(this.selector.eval(env), this.option, this.index);\n    },\n    clone: function (env) {\n        return new(tree.Extend)(this.selector, this.option, this.index);\n    },\n    findSelfSelectors: function (selectors) {\n        var selfElements = [],\n            i;\n\n        for(i = 0; i < selectors.length; i++) {\n            selfElements = selfElements.concat(selectors[i].elements);\n        }\n\n        this.selfSelectors = [{ elements: selfElements }];\n    }\n};\n\n})(require('../tree'));\n(function (tree) {\n//\n// CSS @import node\n//\n// The general strategy here is that we don't want to wait\n// for the parsing to be completed, before we start importing\n// the file. That's because in the context of a browser,\n// most of the time will be spent waiting for the server to respond.\n//\n// On creation, we push the import path to our import queue, though\n// `import,push`, we also pass it a callback, which it'll call once\n// the file has been fetched, and parsed.\n//\ntree.Import = function (path, features, options, index, currentFileInfo) {\n    var that = this;\n\n    this.options = options;\n    this.index = index;\n    this.path = path;\n    this.features = features;\n    this.currentFileInfo = currentFileInfo;\n\n    if (this.options.less !== undefined) {\n        this.css = !this.options.less;\n    } else {\n        var pathValue = this.getPath();\n        if (pathValue && /css([\\?;].*)?$/.test(pathValue)) {\n            this.css = true;\n        }\n    }\n};\n\n//\n// The actual import node doesn't return anything, when converted to CSS.\n// The reason is that it's used at the evaluation stage, so that the rules\n// it imports can be treated like any other rules.\n//\n// In `eval`, we make sure all Import nodes get evaluated, recursively, so\n// we end up with a flat structure, which can easily be imported in the parent\n// ruleset.\n//\ntree.Import.prototype = {\n    type: \"Import\",\n    accept: function (visitor) {\n        this.features = visitor.visit(this.features);\n        this.path = visitor.visit(this.path);\n        this.root = visitor.visit(this.root);\n    },\n    toCSS: function (env) {\n        var features = this.features ? ' ' + this.features.toCSS(env) : '';\n\n        if (this.css) {\n            return \"@import \" + this.path.toCSS() + features + ';\\n';\n        } else {\n            return \"\";\n        }\n    },\n    getPath: function () {\n        if (this.path instanceof tree.Quoted) {\n            var path = this.path.value;\n            return (this.css !== undefined || /(\\.[a-z]*$)|([\\?;].*)$/.test(path)) ? path : path + '.less';\n        } else if (this.path instanceof tree.URL) {\n            return this.path.value.value;\n        }\n        return null;\n    },\n    evalForImport: function (env) {\n        return new(tree.Import)(this.path.eval(env), this.features, this.options, this.index, this.currentFileInfo);\n    },\n    evalPath: function (env) {\n        var path = this.path.eval(env);\n        var rootpath = this.currentFileInfo && this.currentFileInfo.rootpath;\n        if (rootpath && !(path instanceof tree.URL)) {\n            var pathValue = path.value;\n            // Add the base path if the import is relative\n            if (pathValue && env.isPathRelative(pathValue)) {\n                path.value =  rootpath + pathValue;\n            }\n        }\n        return path;\n    },\n    eval: function (env) {\n        var ruleset, features = this.features && this.features.eval(env);\n\n        if (this.skip) { return []; }\n\n        if (this.css) {\n            var newImport = new(tree.Import)(this.evalPath(env), features, this.options, this.index);\n            if (!newImport.css && this.error) {\n                throw this.error;\n            }\n            return newImport;\n        } else {\n            ruleset = new(tree.Ruleset)([], this.root.rules.slice(0));\n\n            ruleset.evalImports(env);\n\n            return this.features ? new(tree.Media)(ruleset.rules, this.features.value) : ruleset.rules;\n        }\n    }\n};\n\n})(require('../tree'));\n(function (tree) {\n\ntree.JavaScript = function (string, index, escaped) {\n    this.escaped = escaped;\n    this.expression = string;\n    this.index = index;\n};\ntree.JavaScript.prototype = {\n    type: \"JavaScript\",\n    eval: function (env) {\n        var result,\n            that = this,\n            context = {};\n\n        var expression = this.expression.replace(/@\\{([\\w-]+)\\}/g, function (_, name) {\n            return tree.jsify(new(tree.Variable)('@' + name, that.index).eval(env));\n        });\n\n        try {\n            expression = new(Function)('return (' + expression + ')');\n        } catch (e) {\n            throw { message: \"JavaScript evaluation error: `\" + expression + \"`\" ,\n                    index: this.index };\n        }\n\n        for (var k in env.frames[0].variables()) {\n            context[k.slice(1)] = {\n                value: env.frames[0].variables()[k].value,\n                toJS: function () {\n                    return this.value.eval(env).toCSS();\n                }\n            };\n        }\n\n        try {\n            result = expression.call(context);\n        } catch (e) {\n            throw { message: \"JavaScript evaluation error: '\" + e.name + ': ' + e.message + \"'\" ,\n                    index: this.index };\n        }\n        if (typeof(result) === 'string') {\n            return new(tree.Quoted)('\"' + result + '\"', result, this.escaped, this.index);\n        } else if (Array.isArray(result)) {\n            return new(tree.Anonymous)(result.join(', '));\n        } else {\n            return new(tree.Anonymous)(result);\n        }\n    }\n};\n\n})(require('../tree'));\n\n(function (tree) {\n\ntree.Keyword = function (value) { this.value = value };\ntree.Keyword.prototype = {\n    type: \"Keyword\",\n    eval: function () { return this; },\n    toCSS: function () { return this.value; },\n    compare: function (other) {\n        if (other instanceof tree.Keyword) {\n            return other.value === this.value ? 0 : 1;\n        } else {\n            return -1;\n        }\n    }\n};\n\ntree.True = new(tree.Keyword)('true');\ntree.False = new(tree.Keyword)('false');\n\n})(require('../tree'));\n(function (tree) {\n\ntree.Media = function (value, features) {\n    var selectors = this.emptySelectors();\n\n    this.features = new(tree.Value)(features);\n    this.ruleset = new(tree.Ruleset)(selectors, value);\n    this.ruleset.allowImports = true;\n};\ntree.Media.prototype = {\n    type: \"Media\",\n    accept: function (visitor) {\n        this.features = visitor.visit(this.features);\n        this.ruleset = visitor.visit(this.ruleset);\n    },\n    toCSS: function (env) {\n        var features = this.features.toCSS(env);\n\n        return '@media ' + features + (env.compress ? '{' : ' {\\n  ') +\n               this.ruleset.toCSS(env).trim().replace(/\\n/g, '\\n  ') +\n                           (env.compress ? '}': '\\n}\\n');\n    },\n    eval: function (env) {\n        if (!env.mediaBlocks) {\n            env.mediaBlocks = [];\n            env.mediaPath = [];\n        }\n        \n        var media = new(tree.Media)([], []);\n        if(this.debugInfo) {\n            this.ruleset.debugInfo = this.debugInfo;\n            media.debugInfo = this.debugInfo;\n        }\n        var strictMathBypass = false;\n        if (!env.strictMath) {\n            strictMathBypass = true;\n            env.strictMath = true;\n        }\n        try {\n            media.features = this.features.eval(env);\n        }\n        finally {\n            if (strictMathBypass) {\n                env.strictMath = false;\n            }\n        }\n        \n        env.mediaPath.push(media);\n        env.mediaBlocks.push(media);\n        \n        env.frames.unshift(this.ruleset);\n        media.ruleset = this.ruleset.eval(env);\n        env.frames.shift();\n        \n        env.mediaPath.pop();\n\n        return env.mediaPath.length === 0 ? media.evalTop(env) :\n                    media.evalNested(env)\n    },\n    variable: function (name) { return tree.Ruleset.prototype.variable.call(this.ruleset, name) },\n    find: function () { return tree.Ruleset.prototype.find.apply(this.ruleset, arguments) },\n    rulesets: function () { return tree.Ruleset.prototype.rulesets.apply(this.ruleset) },\n    emptySelectors: function() { \n        var el = new(tree.Element)('', '&', 0);\n        return [new(tree.Selector)([el])];\n    },\n\n    evalTop: function (env) {\n        var result = this;\n\n        // Render all dependent Media blocks.\n        if (env.mediaBlocks.length > 1) {\n            var selectors = this.emptySelectors();\n            result = new(tree.Ruleset)(selectors, env.mediaBlocks);\n            result.multiMedia = true;\n        }\n\n        delete env.mediaBlocks;\n        delete env.mediaPath;\n\n        return result;\n    },\n    evalNested: function (env) {\n        var i, value,\n            path = env.mediaPath.concat([this]);\n\n        // Extract the media-query conditions separated with `,` (OR).\n        for (i = 0; i < path.length; i++) {\n            value = path[i].features instanceof tree.Value ?\n                        path[i].features.value : path[i].features;\n            path[i] = Array.isArray(value) ? value : [value];\n        }\n\n        // Trace all permutations to generate the resulting media-query.\n        //\n        // (a, b and c) with nested (d, e) ->\n        //    a and d\n        //    a and e\n        //    b and c and d\n        //    b and c and e\n        this.features = new(tree.Value)(this.permute(path).map(function (path) {\n            path = path.map(function (fragment) {\n                return fragment.toCSS ? fragment : new(tree.Anonymous)(fragment);\n            });\n\n            for(i = path.length - 1; i > 0; i--) {\n                path.splice(i, 0, new(tree.Anonymous)(\"and\"));\n            }\n\n            return new(tree.Expression)(path);\n        }));\n\n        // Fake a tree-node that doesn't output anything.\n        return new(tree.Ruleset)([], []);\n    },\n    permute: function (arr) {\n      if (arr.length === 0) {\n          return [];\n      } else if (arr.length === 1) {\n          return arr[0];\n      } else {\n          var result = [];\n          var rest = this.permute(arr.slice(1));\n          for (var i = 0; i < rest.length; i++) {\n              for (var j = 0; j < arr[0].length; j++) {\n                  result.push([arr[0][j]].concat(rest[i]));\n              }\n          }\n          return result;\n      }\n    },\n    bubbleSelectors: function (selectors) {\n      this.ruleset = new(tree.Ruleset)(selectors.slice(0), [this.ruleset]);\n    }\n};\n\n})(require('../tree'));\n(function (tree) {\n\ntree.mixin = {};\ntree.mixin.Call = function (elements, args, index, currentFileInfo, important) {\n    this.selector = new(tree.Selector)(elements);\n    this.arguments = args;\n    this.index = index;\n    this.currentFileInfo = currentFileInfo;\n    this.important = important;\n};\ntree.mixin.Call.prototype = {\n    type: \"MixinCall\",\n    accept: function (visitor) {\n        this.selector = visitor.visit(this.selector);\n        this.arguments = visitor.visit(this.arguments);\n    },\n    eval: function (env) {\n        var mixins, mixin, args, rules = [], match = false, i, m, f, isRecursive, isOneFound;\n\n        args = this.arguments && this.arguments.map(function (a) {\n            return { name: a.name, value: a.value.eval(env) };\n        });\n\n        for (i = 0; i < env.frames.length; i++) {\n            if ((mixins = env.frames[i].find(this.selector)).length > 0) {\n                isOneFound = true;\n                for (m = 0; m < mixins.length; m++) {\n                    mixin = mixins[m];\n                    isRecursive = false;\n                    for(f = 0; f < env.frames.length; f++) {\n                        if ((!(mixin instanceof tree.mixin.Definition)) && mixin === (env.frames[f].originalRuleset || env.frames[f])) {\n                            isRecursive = true;\n                            break;\n                        }\n                    }\n                    if (isRecursive) {\n                        continue;\n                    }\n                    if (mixin.matchArgs(args, env)) {\n                        if (!mixin.matchCondition || mixin.matchCondition(args, env)) {\n                            try {\n                                Array.prototype.push.apply(\n                                      rules, mixin.eval(env, args, this.important).rules);\n                            } catch (e) {\n                                throw { message: e.message, index: this.index, filename: this.currentFileInfo.filename, stack: e.stack };\n                            }\n                        }\n                        match = true;\n                    }\n                }\n                if (match) {\n                    return rules;\n                }\n            }\n        }\n        if (isOneFound) {\n            throw { type:    'Runtime',\n                    message: 'No matching definition was found for `' +\n                              this.selector.toCSS().trim() + '('      +\n                              (args ? args.map(function (a) {\n                                  var argValue = \"\";\n                                  if (a.name) {\n                                      argValue += a.name + \":\";\n                                  }\n                                  if (a.value.toCSS) {\n                                      argValue += a.value.toCSS();\n                                  } else {\n                                      argValue += \"???\";\n                                  }\n                                  return argValue;\n                              }).join(', ') : \"\") + \")`\",\n                    index:   this.index, filename: this.currentFileInfo.filename };\n        } else {\n            throw { type: 'Name',\n                message: this.selector.toCSS().trim() + \" is undefined\",\n                index: this.index, filename: this.currentFileInfo.filename };\n        }\n    }\n};\n\ntree.mixin.Definition = function (name, params, rules, condition, variadic) {\n    this.name = name;\n    this.selectors = [new(tree.Selector)([new(tree.Element)(null, name)])];\n    this.params = params;\n    this.condition = condition;\n    this.variadic = variadic;\n    this.arity = params.length;\n    this.rules = rules;\n    this._lookups = {};\n    this.required = params.reduce(function (count, p) {\n        if (!p.name || (p.name && !p.value)) { return count + 1 }\n        else                                 { return count }\n    }, 0);\n    this.parent = tree.Ruleset.prototype;\n    this.frames = [];\n};\ntree.mixin.Definition.prototype = {\n    type: \"MixinDefinition\",\n    accept: function (visitor) {\n        this.params = visitor.visit(this.params);\n        this.rules = visitor.visit(this.rules);\n        this.condition = visitor.visit(this.condition);\n    },\n    toCSS:     function ()     { return \"\"; },\n    variable:  function (name) { return this.parent.variable.call(this, name); },\n    variables: function ()     { return this.parent.variables.call(this); },\n    find:      function ()     { return this.parent.find.apply(this, arguments); },\n    rulesets:  function ()     { return this.parent.rulesets.apply(this); },\n\n    evalParams: function (env, mixinEnv, args, evaldArguments) {\n        var frame = new(tree.Ruleset)(null, []),\n            varargs, arg,\n            params = this.params.slice(0),\n            i, j, val, name, isNamedFound, argIndex;\n\n        mixinEnv = new tree.evalEnv(mixinEnv, [frame].concat(mixinEnv.frames));\n        \n        if (args) {\n            args = args.slice(0);\n\n            for(i = 0; i < args.length; i++) {\n                arg = args[i];\n                if (name = (arg && arg.name)) {\n                    isNamedFound = false;\n                    for(j = 0; j < params.length; j++) {\n                        if (!evaldArguments[j] && name === params[j].name) {\n                            evaldArguments[j] = arg.value.eval(env);\n                            frame.rules.unshift(new(tree.Rule)(name, arg.value.eval(env)));\n                            isNamedFound = true;\n                            break;\n                        }\n                    }\n                    if (isNamedFound) {\n                        args.splice(i, 1);\n                        i--;\n                        continue;\n                    } else {\n                        throw { type: 'Runtime', message: \"Named argument for \" + this.name +\n                            ' ' + args[i].name + ' not found' };\n                    }\n                }\n            }\n        }\n        argIndex = 0;\n        for (i = 0; i < params.length; i++) {\n            if (evaldArguments[i]) continue;\n            \n            arg = args && args[argIndex];\n\n            if (name = params[i].name) {\n                if (params[i].variadic && args) {\n                    varargs = [];\n                    for (j = argIndex; j < args.length; j++) {\n                        varargs.push(args[j].value.eval(env));\n                    }\n                    frame.rules.unshift(new(tree.Rule)(name, new(tree.Expression)(varargs).eval(env)));\n                } else {\n                    val = arg && arg.value;\n                    if (val) {\n                        val = val.eval(env);\n                    } else if (params[i].value) {\n                        val = params[i].value.eval(mixinEnv);\n                        frame.resetCache();\n                    } else {\n                        throw { type: 'Runtime', message: \"wrong number of arguments for \" + this.name +\n                            ' (' + args.length + ' for ' + this.arity + ')' };\n                    }\n                    \n                    frame.rules.unshift(new(tree.Rule)(name, val));\n                    evaldArguments[i] = val;\n                }\n            }\n            \n            if (params[i].variadic && args) {\n                for (j = argIndex; j < args.length; j++) {\n                    evaldArguments[j] = args[j].value.eval(env);\n                }\n            }\n            argIndex++;\n        }\n\n        return frame;\n    },\n    eval: function (env, args, important) {\n        var _arguments = [],\n            mixinFrames = this.frames.concat(env.frames),\n            frame = this.evalParams(env, new(tree.evalEnv)(env, mixinFrames), args, _arguments),\n            context, rules, start, ruleset;\n\n        frame.rules.unshift(new(tree.Rule)('@arguments', new(tree.Expression)(_arguments).eval(env)));\n\n        rules = important ?\n            this.parent.makeImportant.apply(this).rules : this.rules.slice(0);\n\n        ruleset = new(tree.Ruleset)(null, rules).eval(new(tree.evalEnv)(env,\n                                                    [this, frame].concat(mixinFrames)));\n        ruleset.originalRuleset = this;\n        return ruleset;\n    },\n    matchCondition: function (args, env) {\n\n        if (this.condition && !this.condition.eval(\n            new(tree.evalEnv)(env,\n                [this.evalParams(env, new(tree.evalEnv)(env, this.frames.concat(env.frames)), args, [])]\n                    .concat(env.frames)))) {\n            return false;\n        }\n        return true;\n    },\n    matchArgs: function (args, env) {\n        var argsLength = (args && args.length) || 0, len, frame;\n\n        if (! this.variadic) {\n            if (argsLength < this.required)                               { return false }\n            if (argsLength > this.params.length)                          { return false }\n            if ((this.required > 0) && (argsLength > this.params.length)) { return false }\n        }\n\n        len = Math.min(argsLength, this.arity);\n\n        for (var i = 0; i < len; i++) {\n            if (!this.params[i].name && !this.params[i].variadic) {\n                if (args[i].value.eval(env).toCSS() != this.params[i].value.eval(env).toCSS()) {\n                    return false;\n                }\n            }\n        }\n        return true;\n    }\n};\n\n})(require('../tree'));\n(function (tree) {\n\ntree.Negative = function (node) {\n    this.value = node;\n};\ntree.Negative.prototype = {\n    type: \"Negative\",\n    accept: function (visitor) {\n        this.value = visitor.visit(this.value);\n    },\n    toCSS: function (env) {\n        return '-' + this.value.toCSS(env);\n    },\n    eval: function (env) {\n        if (env.isMathOn()) {\n            return (new(tree.Operation)('*', [new(tree.Dimension)(-1), this.value])).eval(env);\n        }\n        return new(tree.Negative)(this.value.eval(env));\n    }\n};\n\n})(require('../tree'));\n(function (tree) {\n\ntree.Operation = function (op, operands, isSpaced) {\n    this.op = op.trim();\n    this.operands = operands;\n    this.isSpaced = isSpaced;\n};\ntree.Operation.prototype = {\n    type: \"Operation\",\n    accept: function (visitor) {\n        this.operands = visitor.visit(this.operands);\n    },\n    eval: function (env) {\n        var a = this.operands[0].eval(env),\n            b = this.operands[1].eval(env),\n            temp;\n\n        if (env.isMathOn()) {\n            if (a instanceof tree.Dimension && b instanceof tree.Color) {\n                if (this.op === '*' || this.op === '+') {\n                    temp = b, b = a, a = temp;\n                } else {\n                    throw { type: \"Operation\",\n                            message: \"Can't substract or divide a color from a number\" };\n                }\n            }\n            if (!a.operate) {\n                throw { type: \"Operation\",\n                        message: \"Operation on an invalid type\" };\n            }\n\n            return a.operate(env, this.op, b);\n        } else {\n            return new(tree.Operation)(this.op, [a, b], this.isSpaced);\n        }\n    },\n    toCSS: function (env) {\n        var separator = this.isSpaced ? \" \" : \"\";\n        return this.operands[0].toCSS() + separator + this.op + separator + this.operands[1].toCSS();\n    }\n};\n\ntree.operate = function (env, op, a, b) {\n    switch (op) {\n        case '+': return a + b;\n        case '-': return a - b;\n        case '*': return a * b;\n        case '/': return a / b;\n    }\n};\n\n})(require('../tree'));\n\n(function (tree) {\n\ntree.Paren = function (node) {\n    this.value = node;\n};\ntree.Paren.prototype = {\n    type: \"Paren\",\n    accept: function (visitor) {\n        this.value = visitor.visit(this.value);\n    },\n    toCSS: function (env) {\n        return '(' + this.value.toCSS(env).trim() + ')';\n    },\n    eval: function (env) {\n        return new(tree.Paren)(this.value.eval(env));\n    }\n};\n\n})(require('../tree'));\n(function (tree) {\n\ntree.Quoted = function (str, content, escaped, index, currentFileInfo) {\n    this.escaped = escaped;\n    this.value = content || '';\n    this.quote = str.charAt(0);\n    this.index = index;\n    this.currentFileInfo = currentFileInfo;\n};\ntree.Quoted.prototype = {\n    type: \"Quoted\",\n    toCSS: function () {\n        if (this.escaped) {\n            return this.value;\n        } else {\n            return this.quote + this.value + this.quote;\n        }\n    },\n    eval: function (env) {\n        var that = this;\n        var value = this.value.replace(/`([^`]+)`/g, function (_, exp) {\n            return new(tree.JavaScript)(exp, that.index, true).eval(env).value;\n        }).replace(/@\\{([\\w-]+)\\}/g, function (_, name) {\n            var v = new(tree.Variable)('@' + name, that.index, that.currentFileInfo).eval(env, true);\n            return (v instanceof tree.Quoted) ? v.value : v.toCSS();\n        });\n        return new(tree.Quoted)(this.quote + value + this.quote, value, this.escaped, this.index);\n    },\n    compare: function (x) {\n        if (!x.toCSS) {\n            return -1;\n        }\n        \n        var left = this.toCSS(),\n            right = x.toCSS();\n        \n        if (left === right) {\n            return 0;\n        }\n        \n        return left < right ? -1 : 1;\n    }\n};\n\n})(require('../tree'));\n(function (tree) {\n\ntree.Rule = function (name, value, important, index, currentFileInfo, inline) {\n    this.name = name;\n    this.value = (value instanceof tree.Value) ? value : new(tree.Value)([value]);\n    this.important = important ? ' ' + important.trim() : '';\n    this.index = index;\n    this.currentFileInfo = currentFileInfo;\n    this.inline = inline || false;\n\n    if (name.charAt(0) === '@') {\n        this.variable = true;\n    } else { this.variable = false }\n};\n\ntree.Rule.prototype = {\n    type: \"Rule\",\n    accept: function (visitor) {\n        this.value = visitor.visit(this.value);\n    },\n    toCSS: function (env) {\n        if (this.variable) { return \"\" }\n        else {\n            try {\n                return this.name + (env.compress ? ':' : ': ') +\n                   this.value.toCSS(env) +\n                   this.important + (this.inline ? \"\" : \";\");\n            }\n            catch(e) {\n                e.index = this.index;\n                e.filename = this.currentFileInfo.filename;\n                throw e;\n            }\n        }\n    },\n    eval: function (env) {\n        var strictMathBypass = false;\n        if (this.name === \"font\" && !env.strictMath) {\n            strictMathBypass = true;\n            env.strictMath = true;\n        }\n        try {\n            return new(tree.Rule)(this.name,\n                              this.value.eval(env),\n                              this.important,\n                              this.index, this.currentFileInfo, this.inline);\n        }\n        finally {\n            if (strictMathBypass) {\n                env.strictMath = false;\n            }\n        }\n    },\n    makeImportant: function () {\n        return new(tree.Rule)(this.name,\n                              this.value,\n                              \"!important\",\n                              this.index, this.currentFileInfo, this.inline);\n    }\n};\n\n})(require('../tree'));\n(function (tree) {\n\ntree.Ruleset = function (selectors, rules, strictImports) {\n    this.selectors = selectors;\n    this.rules = rules;\n    this._lookups = {};\n    this.strictImports = strictImports;\n};\ntree.Ruleset.prototype = {\n    type: \"Ruleset\",\n    accept: function (visitor) {\n        this.selectors = visitor.visit(this.selectors);\n        this.rules = visitor.visit(this.rules);\n    },\n    eval: function (env) {\n        var selectors = this.selectors && this.selectors.map(function (s) { return s.eval(env) });\n        var ruleset = new(tree.Ruleset)(selectors, this.rules.slice(0), this.strictImports);\n        var rules;\n        \n        ruleset.originalRuleset = this;\n        ruleset.root = this.root;\n        ruleset.firstRoot = this.firstRoot;\n        ruleset.allowImports = this.allowImports;\n\n        if(this.debugInfo) {\n            ruleset.debugInfo = this.debugInfo;\n        }\n\n        // push the current ruleset to the frames stack\n        env.frames.unshift(ruleset);\n\n        // currrent selectors\n        if (!env.selectors) {\n            env.selectors = [];\n        }\n        env.selectors.unshift(this.selectors);\n\n        // Evaluate imports\n        if (ruleset.root || ruleset.allowImports || !ruleset.strictImports) {\n            ruleset.evalImports(env);\n        }\n\n        // Store the frames around mixin definitions,\n        // so they can be evaluated like closures when the time comes.\n        for (var i = 0; i < ruleset.rules.length; i++) {\n            if (ruleset.rules[i] instanceof tree.mixin.Definition) {\n                ruleset.rules[i].frames = env.frames.slice(0);\n            }\n        }\n        \n        var mediaBlockCount = (env.mediaBlocks && env.mediaBlocks.length) || 0;\n\n        // Evaluate mixin calls.\n        for (var i = 0; i < ruleset.rules.length; i++) {\n            if (ruleset.rules[i] instanceof tree.mixin.Call) {\n                rules = ruleset.rules[i].eval(env).filter(function(r) {\n                    if ((r instanceof tree.Rule) && r.variable) {\n                        // do not pollute the scope if the variable is\n                        // already there. consider returning false here\n                        // but we need a way to \"return\" variable from mixins\n                        return !(ruleset.variable(r.name));\n                    }\n                    return true;\n                });\n                ruleset.rules.splice.apply(ruleset.rules, [i, 1].concat(rules));\n                i += rules.length-1;\n                ruleset.resetCache();\n            }\n        }\n        \n        // Evaluate everything else\n        for (var i = 0, rule; i < ruleset.rules.length; i++) {\n            rule = ruleset.rules[i];\n\n            if (! (rule instanceof tree.mixin.Definition)) {\n                ruleset.rules[i] = rule.eval ? rule.eval(env) : rule;\n            }\n        }\n\n        // Pop the stack\n        env.frames.shift();\n        env.selectors.shift();\n        \n        if (env.mediaBlocks) {\n            for(var i = mediaBlockCount; i < env.mediaBlocks.length; i++) {\n                env.mediaBlocks[i].bubbleSelectors(selectors);\n            }\n        }\n\n        return ruleset;\n    },\n    evalImports: function(env) {\n        var i, rules;\n        for (i = 0; i < this.rules.length; i++) {\n            if (this.rules[i] instanceof tree.Import) {\n                rules = this.rules[i].eval(env);\n                if (typeof rules.length === \"number\") {\n                    this.rules.splice.apply(this.rules, [i, 1].concat(rules));\n                    i+= rules.length-1;\n                } else {\n                    this.rules.splice(i, 1, rules);\n                }\n                this.resetCache();\n            }\n        }\n    },\n    makeImportant: function() {\n        return new tree.Ruleset(this.selectors, this.rules.map(function (r) {\n                    if (r.makeImportant) {\n                        return r.makeImportant();\n                    } else {\n                        return r;\n                    }\n                }), this.strictImports);\n    },\n    matchArgs: function (args) {\n        return !args || args.length === 0;\n    },\n    resetCache: function () {\n        this._rulesets = null;\n        this._variables = null;\n        this._lookups = {};\n    },\n    variables: function () {\n        if (this._variables) { return this._variables }\n        else {\n            return this._variables = this.rules.reduce(function (hash, r) {\n                if (r instanceof tree.Rule && r.variable === true) {\n                    hash[r.name] = r;\n                }\n                return hash;\n            }, {});\n        }\n    },\n    variable: function (name) {\n        return this.variables()[name];\n    },\n    rulesets: function () {\n        return this.rules.filter(function (r) {\n            return (r instanceof tree.Ruleset) || (r instanceof tree.mixin.Definition);\n        });\n    },\n    find: function (selector, self) {\n        self = self || this;\n        var rules = [], rule, match,\n            key = selector.toCSS();\n\n        if (key in this._lookups) { return this._lookups[key] }\n\n        this.rulesets().forEach(function (rule) {\n            if (rule !== self) {\n                for (var j = 0; j < rule.selectors.length; j++) {\n                    if (match = selector.match(rule.selectors[j])) {\n                        if (selector.elements.length > rule.selectors[j].elements.length) {\n                            Array.prototype.push.apply(rules, rule.find(\n                                new(tree.Selector)(selector.elements.slice(1)), self));\n                        } else {\n                            rules.push(rule);\n                        }\n                        break;\n                    }\n                }\n            }\n        });\n        return this._lookups[key] = rules;\n    },\n    //\n    // Entry point for code generation\n    //\n    //     `context` holds an array of arrays.\n    //\n    toCSS: function (env) {\n        var css = [],      // The CSS output\n            rules = [],    // node.Rule instances\n           _rules = [],    //\n            rulesets = [], // node.Ruleset instances\n            selector,      // The fully rendered selector\n            debugInfo,     // Line number debugging\n            rule;\n\n        // Compile rules and rulesets\n        for (var i = 0; i < this.rules.length; i++) {\n            rule = this.rules[i];\n\n            if (rule.rules || (rule instanceof tree.Media)) {\n                rulesets.push(rule.toCSS(env));\n            } else if (rule instanceof tree.Directive) {\n                var cssValue = rule.toCSS(env);\n                // Output only the first @charset definition as such - convert the others\n                // to comments in case debug is enabled\n                if (rule.name === \"@charset\") {\n                    // Only output the debug info together with subsequent @charset definitions\n                    // a comment (or @media statement) before the actual @charset directive would\n                    // be considered illegal css as it has to be on the first line\n                    if (env.charset) {\n                        if (rule.debugInfo) {\n                            rulesets.push(tree.debugInfo(env, rule));\n                            rulesets.push(new tree.Comment(\"/* \"+cssValue.replace(/\\n/g, \"\")+\" */\\n\").toCSS(env));\n                        }\n                        continue;\n                    }\n                    env.charset = true;\n                }\n                rulesets.push(cssValue);\n            } else if (rule instanceof tree.Comment) {\n                if (!rule.silent) {\n                    if (this.root) {\n                        rulesets.push(rule.toCSS(env));\n                    } else {\n                        rules.push(rule.toCSS(env));\n                    }\n                }\n            } else {\n                if (rule.toCSS && !rule.variable) {\n                    if (this.firstRoot && rule instanceof tree.Rule) {\n                        throw { message: \"properties must be inside selector blocks, they cannot be in the root.\",\n                            index: rule.index, filename: rule.currentFileInfo ? rule.currentFileInfo.filename : null};\n                    }\n                    rules.push(rule.toCSS(env));\n                } else if (rule.value && !rule.variable) {\n                    rules.push(rule.value.toString());\n                }\n            }\n        } \n\n        // Remove last semicolon\n        if (env.compress && rules.length) {\n            rule = rules[rules.length - 1];\n            if (rule.charAt(rule.length - 1) === ';') {\n                rules[rules.length - 1] = rule.substring(0, rule.length - 1);\n            }\n        }\n\n        rulesets = rulesets.join('');\n\n        // If this is the root node, we don't render\n        // a selector, or {}.\n        // Otherwise, only output if this ruleset has rules.\n        if (this.root) {\n            css.push(rules.join(env.compress ? '' : '\\n'));\n        } else {\n            if (rules.length > 0) {\n                debugInfo = tree.debugInfo(env, this);\n                selector = this.paths.map(function (p) {\n                    return p.map(function (s) {\n                        return s.toCSS(env);\n                    }).join('').trim();\n                }).join(env.compress ? ',' : ',\\n');\n\n                // Remove duplicates\n                for (var i = rules.length - 1; i >= 0; i--) {\n                    if (rules[i].slice(0, 2) === \"/*\" ||  _rules.indexOf(rules[i]) === -1) {\n                        _rules.unshift(rules[i]);\n                    }\n                }\n                rules = _rules;\n\n                css.push(debugInfo + selector + \n                        (env.compress ? '{' : ' {\\n  ') +\n                        rules.join(env.compress ? '' : '\\n  ') +\n                        (env.compress ? '}' : '\\n}\\n'));\n            }\n        }\n        css.push(rulesets);\n\n        return css.join('')  + (env.compress ? '\\n' : '');\n    },\n\n    joinSelectors: function (paths, context, selectors) {\n        for (var s = 0; s < selectors.length; s++) {\n            this.joinSelector(paths, context, selectors[s]);\n        }\n    },\n\n    joinSelector: function (paths, context, selector) {\n\n        var i, j, k, \n            hasParentSelector, newSelectors, el, sel, parentSel, \n            newSelectorPath, afterParentJoin, newJoinedSelector, \n            newJoinedSelectorEmpty, lastSelector, currentElements,\n            selectorsMultiplied;\n    \n        for (i = 0; i < selector.elements.length; i++) {\n            el = selector.elements[i];\n            if (el.value === '&') {\n                hasParentSelector = true;\n            }\n        }\n    \n        if (!hasParentSelector) {\n            if (context.length > 0) {\n                for(i = 0; i < context.length; i++) {\n                    paths.push(context[i].concat(selector));\n                }\n            }\n            else {\n                paths.push([selector]);\n            }\n            return;\n        }\n\n        // The paths are [[Selector]]\n        // The first list is a list of comma seperated selectors\n        // The inner list is a list of inheritance seperated selectors\n        // e.g.\n        // .a, .b {\n        //   .c {\n        //   }\n        // }\n        // == [[.a] [.c]] [[.b] [.c]]\n        //\n\n        // the elements from the current selector so far\n        currentElements = [];\n        // the current list of new selectors to add to the path.\n        // We will build it up. We initiate it with one empty selector as we \"multiply\" the new selectors\n        // by the parents\n        newSelectors = [[]];\n\n        for (i = 0; i < selector.elements.length; i++) {\n            el = selector.elements[i];\n            // non parent reference elements just get added\n            if (el.value !== \"&\") {\n                currentElements.push(el);\n            } else {\n                // the new list of selectors to add\n                selectorsMultiplied = [];\n\n                // merge the current list of non parent selector elements\n                // on to the current list of selectors to add\n                if (currentElements.length > 0) {\n                    this.mergeElementsOnToSelectors(currentElements, newSelectors);\n                }\n\n                // loop through our current selectors\n                for(j = 0; j < newSelectors.length; j++) {\n                    sel = newSelectors[j];\n                    // if we don't have any parent paths, the & might be in a mixin so that it can be used\n                    // whether there are parents or not\n                    if (context.length == 0) {\n                        // the combinator used on el should now be applied to the next element instead so that\n                        // it is not lost\n                        if (sel.length > 0) {\n                            sel[0].elements = sel[0].elements.slice(0);\n                            sel[0].elements.push(new(tree.Element)(el.combinator, '', 0)); //new Element(el.Combinator,  \"\"));\n                        }\n                        selectorsMultiplied.push(sel);\n                    }\n                    else {\n                        // and the parent selectors\n                        for(k = 0; k < context.length; k++) {\n                            parentSel = context[k];\n                            // We need to put the current selectors\n                            // then join the last selector's elements on to the parents selectors\n\n                            // our new selector path\n                            newSelectorPath = [];\n                            // selectors from the parent after the join\n                            afterParentJoin = [];\n                            newJoinedSelectorEmpty = true;\n\n                            //construct the joined selector - if & is the first thing this will be empty,\n                            // if not newJoinedSelector will be the last set of elements in the selector\n                            if (sel.length > 0) {\n                                newSelectorPath = sel.slice(0);\n                                lastSelector = newSelectorPath.pop();\n                                newJoinedSelector = new(tree.Selector)(lastSelector.elements.slice(0), selector.extendList);\n                                newJoinedSelectorEmpty = false;\n                            }\n                            else {\n                                newJoinedSelector = new(tree.Selector)([], selector.extendList);\n                            }\n\n                            //put together the parent selectors after the join\n                            if (parentSel.length > 1) {\n                                afterParentJoin = afterParentJoin.concat(parentSel.slice(1));\n                            }\n\n                            if (parentSel.length > 0) {\n                                newJoinedSelectorEmpty = false;\n\n                                // join the elements so far with the first part of the parent\n                                newJoinedSelector.elements.push(new(tree.Element)(el.combinator, parentSel[0].elements[0].value, 0));\n                                newJoinedSelector.elements = newJoinedSelector.elements.concat(parentSel[0].elements.slice(1));\n                            }\n\n                            if (!newJoinedSelectorEmpty) {\n                                // now add the joined selector\n                                newSelectorPath.push(newJoinedSelector);\n                            }\n\n                            // and the rest of the parent\n                            newSelectorPath = newSelectorPath.concat(afterParentJoin);\n\n                            // add that to our new set of selectors\n                            selectorsMultiplied.push(newSelectorPath);\n                        }\n                    }\n                }\n\n                // our new selectors has been multiplied, so reset the state\n                newSelectors = selectorsMultiplied;\n                currentElements = [];\n            }\n        }\n\n        // if we have any elements left over (e.g. .a& .b == .b)\n        // add them on to all the current selectors\n        if (currentElements.length > 0) {\n            this.mergeElementsOnToSelectors(currentElements, newSelectors);\n        }\n\n        for(i = 0; i < newSelectors.length; i++) {\n            if (newSelectors[i].length > 0) {\n                paths.push(newSelectors[i]);\n            }\n        }\n    },\n    \n    mergeElementsOnToSelectors: function(elements, selectors) {\n        var i, sel, extendList;\n\n        if (selectors.length == 0) {\n            selectors.push([ new(tree.Selector)(elements) ]);\n            return;\n        }\n\n        for(i = 0; i < selectors.length; i++) {\n            sel = selectors[i];\n\n            // if the previous thing in sel is a parent this needs to join on to it\n            if (sel.length > 0) {\n                sel[sel.length - 1] = new(tree.Selector)(sel[sel.length - 1].elements.concat(elements), sel[sel.length - 1].extendList);\n            }\n            else {\n                sel.push(new(tree.Selector)(elements));\n            }\n        }\n    }\n};\n})(require('../tree'));\n(function (tree) {\n\ntree.Selector = function (elements, extendList) {\n    this.elements = elements;\n    this.extendList = extendList || [];\n};\ntree.Selector.prototype = {\n    type: \"Selector\",\n    accept: function (visitor) {\n        this.elements = visitor.visit(this.elements);\n        this.extendList = visitor.visit(this.extendList)\n    },\n    match: function (other) {\n        var elements = this.elements,\n            len = elements.length,\n            oelements, olen, max, i;\n\n        oelements = other.elements.slice(\n            (other.elements.length && other.elements[0].value === \"&\") ? 1 : 0);\n        olen = oelements.length;\n        max = Math.min(len, olen);\n\n        if (olen === 0 || len < olen) {\n            return false;\n        } else {\n            for (i = 0; i < max; i++) {\n                if (elements[i].value !== oelements[i].value) {\n                    return false;\n                }\n            }\n        }\n        return true;\n    },\n    eval: function (env) {\n        return new(tree.Selector)(this.elements.map(function (e) {\n            return e.eval(env);\n        }), this.extendList.map(function(extend) {\n            return extend.eval(env);\n        }));\n    },\n    toCSS: function (env) {\n        if (this._css) { return this._css }\n\n        if (this.elements[0].combinator.value === \"\") {\n            this._css = ' ';\n        } else {\n            this._css = '';\n        }\n\n        this._css += this.elements.map(function (e) {\n            if (typeof(e) === 'string') {\n                return ' ' + e.trim();\n            } else {\n                return e.toCSS(env);\n            }\n        }).join('');\n\n        return this._css;\n    }\n};\n\n})(require('../tree'));\n(function (tree) {\n\ntree.UnicodeDescriptor = function (value) {\n    this.value = value;\n};\ntree.UnicodeDescriptor.prototype = {\n    type: \"UnicodeDescriptor\",\n    toCSS: function (env) {\n        return this.value;\n    },\n    eval: function () { return this }\n};\n\n})(require('../tree'));\n(function (tree) {\n\ntree.URL = function (val, currentFileInfo) {\n    this.value = val;\n    this.currentFileInfo = currentFileInfo;\n};\ntree.URL.prototype = {\n    type: \"Url\",\n    accept: function (visitor) {\n        this.value = visitor.visit(this.value);\n    },\n    toCSS: function () {\n        return \"url(\" + this.value.toCSS() + \")\";\n    },\n    eval: function (ctx) {\n        var val = this.value.eval(ctx), rootpath;\n\n        // Add the base path if the URL is relative\n        rootpath = this.currentFileInfo && this.currentFileInfo.rootpath;\n        if (rootpath && typeof val.value === \"string\" && ctx.isPathRelative(val.value)) {\n            if (!val.quote) {\n                rootpath = rootpath.replace(/[\\(\\)'\"\\s]/g, function(match) { return \"\\\\\"+match; });\n            }\n            val.value = rootpath + val.value;\n        }\n\n        return new(tree.URL)(val, null);\n    }\n};\n\n})(require('../tree'));\n(function (tree) {\n\ntree.Value = function (value) {\n    this.value = value;\n};\ntree.Value.prototype = {\n    type: \"Value\",\n    accept: function (visitor) {\n        this.value = visitor.visit(this.value);\n    },\n    eval: function (env) {\n        if (this.value.length === 1) {\n            return this.value[0].eval(env);\n        } else {\n            return new(tree.Value)(this.value.map(function (v) {\n                return v.eval(env);\n            }));\n        }\n    },\n    toCSS: function (env) {\n        return this.value.map(function (e) {\n            return e.toCSS(env);\n        }).join(env.compress ? ',' : ', ');\n    }\n};\n\n})(require('../tree'));\n(function (tree) {\n\ntree.Variable = function (name, index, currentFileInfo) { this.name = name, this.index = index, this.currentFileInfo = currentFileInfo };\ntree.Variable.prototype = {\n    type: \"Variable\",\n    eval: function (env) {\n        var variable, v, name = this.name;\n\n        if (name.indexOf('@@') == 0) {\n            name = '@' + new(tree.Variable)(name.slice(1)).eval(env).value;\n        }\n        \n        if (this.evaluating) {\n            throw { type: 'Name',\n                    message: \"Recursive variable definition for \" + name,\n                    filename: this.currentFileInfo.file,\n                    index: this.index };\n        }\n        \n        this.evaluating = true;\n\n        if (variable = tree.find(env.frames, function (frame) {\n            if (v = frame.variable(name)) {\n                return v.value.eval(env);\n            }\n        })) { \n            this.evaluating = false;\n            return variable;\n        }\n        else {\n            throw { type: 'Name',\n                    message: \"variable \" + name + \" is undefined\",\n                    filename: this.currentFileInfo.filename,\n                    index: this.index };\n        }\n    }\n};\n\n})(require('../tree'));\n(function (tree) {\n\ntree.debugInfo = function(env, ctx) {\n    var result=\"\";\n    if (env.dumpLineNumbers && !env.compress) {\n        switch(env.dumpLineNumbers) {\n            case 'comments':\n                result = tree.debugInfo.asComment(ctx);\n                break;\n            case 'mediaquery':\n                result = tree.debugInfo.asMediaQuery(ctx);\n                break;\n            case 'all':\n                result = tree.debugInfo.asComment(ctx)+tree.debugInfo.asMediaQuery(ctx);\n                break;\n        }\n    }\n    return result;\n};\n\ntree.debugInfo.asComment = function(ctx) {\n    return '/* line ' + ctx.debugInfo.lineNumber + ', ' + ctx.debugInfo.fileName + ' */\\n';\n};\n\ntree.debugInfo.asMediaQuery = function(ctx) {\n    return '@media -sass-debug-info{filename{font-family:' +\n        ('file://' + ctx.debugInfo.fileName).replace(/([.:/\\\\])/g, function(a){if(a=='\\\\') a = '\\/'; return '\\\\' + a}) +\n        '}line{font-family:\\\\00003' + ctx.debugInfo.lineNumber + '}}\\n';\n};\n\ntree.find = function (obj, fun) {\n    for (var i = 0, r; i < obj.length; i++) {\n        if (r = fun.call(obj, obj[i])) { return r }\n    }\n    return null;\n};\ntree.jsify = function (obj) {\n    if (Array.isArray(obj.value) && (obj.value.length > 1)) {\n        return '[' + obj.value.map(function (v) { return v.toCSS(false) }).join(', ') + ']';\n    } else {\n        return obj.toCSS(false);\n    }\n};\n\n})(require('./tree'));\n(function (tree) {\n\n    var parseCopyProperties = [\n        'paths',            // option - unmodified - paths to search for imports on\n        'optimization',     // option - optimization level (for the chunker)\n        'files',            // list of files that have been imported, used for import-once\n        'contents',         // browser-only, contents of all the files\n        'relativeUrls',     // option - whether to adjust URL's to be relative\n        'strictImports',    // option -\n        'dumpLineNumbers',  // option - whether to dump line numbers\n        'compress',         // option - whether to compress\n        'processImports',   // option - whether to process imports. if false then imports will not be imported\n        'syncImport',       // option - whether to import synchronously\n        'mime',             // browser only - mime type for sheet import\n        'currentFileInfo'   // information about the current file - for error reporting and importing and making urls relative etc.\n    ];\n\n    //currentFileInfo = {\n    //  'relativeUrls' - option - whether to adjust URL's to be relative\n    //  'filename' - full resolved filename of current file\n    //  'rootpath' - path to append to normal URLs for this node\n    //  'currentDirectory' - path to the current file, absolute\n    //  'rootFilename' - filename of the base file\n    //  'entryPath' = absolute path to the entry file\n\n    tree.parseEnv = function(options) {\n        copyFromOriginal(options, this, parseCopyProperties);\n\n        if (!this.contents) { this.contents = {}; }\n        if (!this.files) { this.files = {}; }\n\n        if (!this.currentFileInfo) {\n            var filename = (options && options.filename) || \"input\";\n            var entryPath = filename.replace(/[^\\/\\\\]*$/, \"\");\n            if (options) {\n                options.filename = null;\n            }\n            this.currentFileInfo = {\n                filename: filename,\n                relativeUrls: this.relativeUrls,\n                rootpath: (options && options.rootpath) || \"\",\n                currentDirectory: entryPath,\n                entryPath: entryPath,\n                rootFilename: filename\n            };\n        }\n    };\n\n    tree.parseEnv.prototype.toSheet = function (path) {\n        var env = new tree.parseEnv(this);\n        env.href = path;\n        //env.title = path;\n        env.type = this.mime;\n        return env;\n    };\n\n    var evalCopyProperties = [\n        'silent',      // whether to swallow errors and warnings\n        'verbose',     // whether to log more activity\n        'compress',    // whether to compress\n        'yuicompress', // whether to compress with the outside tool yui compressor\n        'ieCompat',    // whether to enforce IE compatibility (IE8 data-uri)\n        'strictMath',  // whether math has to be within parenthesis\n        'strictUnits'  // whether units need to evaluate correctly\n        ];\n\n    tree.evalEnv = function(options, frames) {\n        copyFromOriginal(options, this, evalCopyProperties);\n\n        this.frames = frames || [];\n    };\n\n    tree.evalEnv.prototype.inParenthesis = function () {\n        if (!this.parensStack) {\n            this.parensStack = [];\n        }\n        this.parensStack.push(true);\n    };\n\n    tree.evalEnv.prototype.outOfParenthesis = function () {\n        this.parensStack.pop();\n    };\n\n    tree.evalEnv.prototype.isMathOn = function () {\n        return this.strictMath ? (this.parensStack && this.parensStack.length) : true;\n    };\n\n    tree.evalEnv.prototype.isPathRelative = function (path) {\n        return !/^(?:[a-z-]+:|\\/)/.test(path);\n    };\n\n    //todo - do the same for the toCSS env\n    //tree.toCSSEnv = function (options) {\n    //};\n\n    var copyFromOriginal = function(original, destination, propertiesToCopy) {\n        if (!original) { return; }\n\n        for(var i = 0; i < propertiesToCopy.length; i++) {\n            if (original.hasOwnProperty(propertiesToCopy[i])) {\n                destination[propertiesToCopy[i]] = original[propertiesToCopy[i]];\n            }\n        }\n    }\n})(require('./tree'));(function (tree) {\n\n    tree.visitor = function(implementation) {\n        this._implementation = implementation;\n    };\n\n    tree.visitor.prototype = {\n        visit: function(node) {\n\n            if (node instanceof Array) {\n                return this.visitArray(node);\n            }\n\n            if (!node || !node.type) {\n                return node;\n            }\n\n            var funcName = \"visit\" + node.type,\n                func = this._implementation[funcName],\n                visitArgs, newNode;\n            if (func) {\n                visitArgs = {visitDeeper: true};\n                newNode = func.call(this._implementation, node, visitArgs);\n                if (this._implementation.isReplacing) {\n                    node = newNode;\n                }\n            }\n            if ((!visitArgs || visitArgs.visitDeeper) && node && node.accept) {\n                node.accept(this);\n            }\n            funcName = funcName + \"Out\";\n            if (this._implementation[funcName]) {\n                this._implementation[funcName](node);\n            }\n            return node;\n        },\n        visitArray: function(nodes) {\n            var i, newNodes = [];\n            for(i = 0; i < nodes.length; i++) {\n                var evald = this.visit(nodes[i]);\n                if (evald instanceof Array) {\n                    newNodes = newNodes.concat(evald);\n                } else {\n                    newNodes.push(evald);\n                }\n            }\n            if (this._implementation.isReplacing) {\n                return newNodes;\n            }\n            return nodes;\n        }\n    };\n\n})(require('./tree'));(function (tree) {\n    tree.importVisitor = function(importer, finish, evalEnv) {\n        this._visitor = new tree.visitor(this);\n        this._importer = importer;\n        this._finish = finish;\n        this.env = evalEnv || new tree.evalEnv();\n        this.importCount = 0;\n    };\n\n    tree.importVisitor.prototype = {\n        isReplacing: true,\n        run: function (root) {\n            var error;\n            try {\n                // process the contents\n                this._visitor.visit(root);\n            }\n            catch(e) {\n                error = e;\n            }\n\n            this.isFinished = true;\n\n            if (this.importCount === 0) {\n                this._finish(error);\n            }\n        },\n        visitImport: function (importNode, visitArgs) {\n            var importVisitor = this,\n                evaldImportNode;\n\n            if (!importNode.css) {\n\n                try {\n                    evaldImportNode = importNode.evalForImport(this.env);\n                } catch(e){\n                    if (!e.filename) { e.index = importNode.index; e.filename = importNode.currentFileInfo.filename; }\n                    // attempt to eval properly and treat as css\n                    importNode.css = true;\n                    // if that fails, this error will be thrown\n                    importNode.error = e;\n                }\n\n                if (evaldImportNode && !evaldImportNode.css) {\n                    importNode = evaldImportNode;\n                    this.importCount++;\n                    var env = new tree.evalEnv(this.env, this.env.frames.slice(0));\n                    this._importer.push(importNode.getPath(), importNode.currentFileInfo, function (e, root, imported) {\n                        if (e && !e.filename) { e.index = importNode.index; e.filename = importNode.currentFileInfo.filename; }\n                        if (imported && !importNode.options.multiple) { importNode.skip = imported; }\n\n                        var subFinish = function(e) {\n                            importVisitor.importCount--;\n\n                            if (importVisitor.importCount === 0 && importVisitor.isFinished) {\n                                importVisitor._finish(e);\n                            }\n                        };\n\n                        if (root) {\n                            importNode.root = root;\n                            new(tree.importVisitor)(importVisitor._importer, subFinish, env)\n                                .run(root);\n                        } else {\n                            subFinish();\n                        }\n                    });\n                }\n            }\n            visitArgs.visitDeeper = false;\n            return importNode;\n        },\n        visitRule: function (ruleNode, visitArgs) {\n            visitArgs.visitDeeper = false;\n            return ruleNode;\n        },\n        visitDirective: function (directiveNode, visitArgs) {\n            this.env.frames.unshift(directiveNode);\n            return directiveNode;\n        },\n        visitDirectiveOut: function (directiveNode) {\n            this.env.frames.shift();\n        },\n        visitMixinDefinition: function (mixinDefinitionNode, visitArgs) {\n            this.env.frames.unshift(mixinDefinitionNode);\n            return mixinDefinitionNode;\n        },\n        visitMixinDefinitionOut: function (mixinDefinitionNode) {\n            this.env.frames.shift();\n        },\n        visitRuleset: function (rulesetNode, visitArgs) {\n            this.env.frames.unshift(rulesetNode);\n            return rulesetNode;\n        },\n        visitRulesetOut: function (rulesetNode) {\n            this.env.frames.shift();\n        },\n        visitMedia: function (mediaNode, visitArgs) {\n            this.env.frames.unshift(mediaNode.ruleset);\n            return mediaNode;\n        },\n        visitMediaOut: function (mediaNode) {\n            this.env.frames.shift();\n        }\n    };\n\n})(require('./tree'));(function (tree) {\n    tree.joinSelectorVisitor = function() {\n        this.contexts = [[]];\n        this._visitor = new tree.visitor(this);\n    };\n\n    tree.joinSelectorVisitor.prototype = {\n        run: function (root) {\n            return this._visitor.visit(root);\n        },\n        visitRule: function (ruleNode, visitArgs) {\n            visitArgs.visitDeeper = false;\n        },\n        visitMixinDefinition: function (mixinDefinitionNode, visitArgs) {\n            visitArgs.visitDeeper = false;\n        },\n\n        visitRuleset: function (rulesetNode, visitArgs) {\n            var context = this.contexts[this.contexts.length - 1];\n            var paths = [];\n            this.contexts.push(paths);\n\n            if (! rulesetNode.root) {\n                rulesetNode.joinSelectors(paths, context, rulesetNode.selectors);\n                rulesetNode.paths = paths;\n            }\n        },\n        visitRulesetOut: function (rulesetNode) {\n            this.contexts.length = this.contexts.length - 1;\n        },\n        visitMedia: function (mediaNode, visitArgs) {\n            var context = this.contexts[this.contexts.length - 1];\n            mediaNode.ruleset.root = (context.length === 0 || context[0].multiMedia);\n        }\n    };\n\n})(require('./tree'));(function (tree) {\n    tree.extendFinderVisitor = function() {\n        this._visitor = new tree.visitor(this);\n        this.contexts = [];\n        this.allExtendsStack = [[]];\n    };\n\n    tree.extendFinderVisitor.prototype = {\n        run: function (root) {\n            root = this._visitor.visit(root);\n            root.allExtends = this.allExtendsStack[0];\n            return root;\n        },\n        visitRule: function (ruleNode, visitArgs) {\n            visitArgs.visitDeeper = false;\n        },\n        visitMixinDefinition: function (mixinDefinitionNode, visitArgs) {\n            visitArgs.visitDeeper = false;\n        },\n        visitRuleset: function (rulesetNode, visitArgs) {\n\n            if (rulesetNode.root) {\n                return;\n            }\n\n            var i, j, extend, allSelectorsExtendList = [], extendList;\n\n            // get &:extend(.a); rules which apply to all selectors in this ruleset\n            for(i = 0; i < rulesetNode.rules.length; i++) {\n                if (rulesetNode.rules[i] instanceof tree.Extend) {\n                    allSelectorsExtendList.push(rulesetNode.rules[i]);\n                }\n            }\n\n            // now find every selector and apply the extends that apply to all extends\n            // and the ones which apply to an individual extend\n            for(i = 0; i < rulesetNode.paths.length; i++) {\n                var selectorPath = rulesetNode.paths[i],\n                    selector = selectorPath[selectorPath.length-1];\n                extendList = selector.extendList.slice(0).concat(allSelectorsExtendList).map(function(allSelectorsExtend) {\n                    return allSelectorsExtend.clone();\n                });\n                for(j = 0; j < extendList.length; j++) {\n                    this.foundExtends = true;\n                    extend = extendList[j];\n                    extend.findSelfSelectors(selectorPath);\n                    extend.ruleset = rulesetNode;\n                    if (j === 0) { extend.firstExtendOnThisSelectorPath = true; }\n                    this.allExtendsStack[this.allExtendsStack.length-1].push(extend);\n                }\n            }\n\n            this.contexts.push(rulesetNode.selectors);\n        },\n        visitRulesetOut: function (rulesetNode) {\n            if (!rulesetNode.root) {\n                this.contexts.length = this.contexts.length - 1;\n            }\n        },\n        visitMedia: function (mediaNode, visitArgs) {\n            mediaNode.allExtends = [];\n            this.allExtendsStack.push(mediaNode.allExtends);\n        },\n        visitMediaOut: function (mediaNode) {\n            this.allExtendsStack.length = this.allExtendsStack.length - 1;\n        },\n        visitDirective: function (directiveNode, visitArgs) {\n            directiveNode.allExtends = [];\n            this.allExtendsStack.push(directiveNode.allExtends);\n        },\n        visitDirectiveOut: function (directiveNode) {\n            this.allExtendsStack.length = this.allExtendsStack.length - 1;\n        }\n    };\n\n    tree.processExtendsVisitor = function() {\n        this._visitor = new tree.visitor(this);\n    };\n\n    tree.processExtendsVisitor.prototype = {\n        run: function(root) {\n            var extendFinder = new tree.extendFinderVisitor();\n            extendFinder.run(root);\n            if (!extendFinder.foundExtends) { return root; }\n            root.allExtends = root.allExtends.concat(this.doExtendChaining(root.allExtends, root.allExtends));\n            this.allExtendsStack = [root.allExtends];\n            return this._visitor.visit(root);\n        },\n        doExtendChaining: function (extendsList, extendsListTarget, iterationCount) {\n            //\n            // chaining is different from normal extension.. if we extend an extend then we are not just copying, altering and pasting\n            // the selector we would do normally, but we are also adding an extend with the same target selector\n            // this means this new extend can then go and alter other extends\n            //\n            // this method deals with all the chaining work - without it, extend is flat and doesn't work on other extend selectors\n            // this is also the most expensive.. and a match on one selector can cause an extension of a selector we had already processed if\n            // we look at each selector at a time, as is done in visitRuleset\n\n            var extendIndex, targetExtendIndex, matches, extendsToAdd = [], newSelector, extendVisitor = this, selectorPath, extend, targetExtend, newExtend;\n\n            iterationCount = iterationCount || 0;\n\n            //loop through comparing every extend with every target extend.\n            // a target extend is the one on the ruleset we are looking at copy/edit/pasting in place\n            // e.g.  .a:extend(.b) {}  and .b:extend(.c) {} then the first extend extends the second one\n            // and the second is the target.\n            // the seperation into two lists allows us to process a subset of chains with a bigger set, as is the\n            // case when processing media queries\n            for(extendIndex = 0; extendIndex < extendsList.length; extendIndex++){\n                for(targetExtendIndex = 0; targetExtendIndex < extendsListTarget.length; targetExtendIndex++){\n\n                    extend = extendsList[extendIndex];\n                    targetExtend = extendsListTarget[targetExtendIndex];\n\n                    // look for circular references\n                    if (this.inInheritanceChain(targetExtend, extend)) { continue; }\n\n                    // find a match in the target extends self selector (the bit before :extend)\n                    selectorPath = [targetExtend.selfSelectors[0]];\n                    matches = extendVisitor.findMatch(extend, selectorPath);\n\n                    if (matches.length) {\n\n                        // we found a match, so for each self selector..\n                        extend.selfSelectors.forEach(function(selfSelector) {\n\n                            // process the extend as usual\n                            newSelector = extendVisitor.extendSelector(matches, selectorPath, selfSelector);\n\n                            // but now we create a new extend from it\n                            newExtend = new(tree.Extend)(targetExtend.selector, targetExtend.option, 0);\n                            newExtend.selfSelectors = newSelector;\n\n                            // add the extend onto the list of extends for that selector\n                            newSelector[newSelector.length-1].extendList = [newExtend];\n\n                            // record that we need to add it.\n                            extendsToAdd.push(newExtend);\n                            newExtend.ruleset = targetExtend.ruleset;\n\n                            //remember its parents for circular references\n                            newExtend.parents = [targetExtend, extend];\n\n                            // only process the selector once.. if we have :extend(.a,.b) then multiple\n                            // extends will look at the same selector path, so when extending\n                            // we know that any others will be duplicates in terms of what is added to the css\n                            if (targetExtend.firstExtendOnThisSelectorPath) {\n                                newExtend.firstExtendOnThisSelectorPath = true;\n                                targetExtend.ruleset.paths.push(newSelector);\n                            }\n                        });\n                    }\n                }\n            }\n\n            if (extendsToAdd.length) {\n                // try to detect circular references to stop a stack overflow.\n                // may no longer be needed.\n                this.extendChainCount++;\n                if (iterationCount > 100) {\n                    var selectorOne = \"{unable to calculate}\";\n                    var selectorTwo = \"{unable to calculate}\";\n                    try\n                    {\n                        selectorOne = extendsToAdd[0].selfSelectors[0].toCSS();\n                        selectorTwo = extendsToAdd[0].selector.toCSS();\n                    }\n                    catch(e) {}\n                    throw {message: \"extend circular reference detected. One of the circular extends is currently:\"+selectorOne+\":extend(\" + selectorTwo+\")\"};\n                }\n\n                // now process the new extends on the existing rules so that we can handle a extending b extending c ectending d extending e...\n                return extendsToAdd.concat(extendVisitor.doExtendChaining(extendsToAdd, extendsListTarget, iterationCount+1));\n            } else {\n                return extendsToAdd;\n            }\n        },\n        inInheritanceChain: function (possibleParent, possibleChild) {\n            if (possibleParent === possibleChild) {\n                return true;\n            }\n            if (possibleChild.parents) {\n                if (this.inInheritanceChain(possibleParent, possibleChild.parents[0])) {\n                    return true;\n                }\n                if (this.inInheritanceChain(possibleParent, possibleChild.parents[1])) {\n                    return true;\n                }\n            }\n            return false;\n        },\n        visitRule: function (ruleNode, visitArgs) {\n            visitArgs.visitDeeper = false;\n        },\n        visitMixinDefinition: function (mixinDefinitionNode, visitArgs) {\n            visitArgs.visitDeeper = false;\n        },\n        visitSelector: function (selectorNode, visitArgs) {\n            visitArgs.visitDeeper = false;\n        },\n        visitRuleset: function (rulesetNode, visitArgs) {\n            if (rulesetNode.root) {\n                return;\n            }\n            var matches, pathIndex, extendIndex, allExtends = this.allExtendsStack[this.allExtendsStack.length-1], selectorsToAdd = [], extendVisitor = this, selectorPath;\n\n            // look at each selector path in the ruleset, find any extend matches and then copy, find and replace\n\n            for(extendIndex = 0; extendIndex < allExtends.length; extendIndex++) {\n                for(pathIndex = 0; pathIndex < rulesetNode.paths.length; pathIndex++) {\n\n                    selectorPath = rulesetNode.paths[pathIndex];\n\n                    // extending extends happens initially, before the main pass\n                    if (selectorPath[selectorPath.length-1].extendList.length) { continue; }\n\n                    matches = this.findMatch(allExtends[extendIndex], selectorPath);\n\n                    if (matches.length) {\n\n                        allExtends[extendIndex].selfSelectors.forEach(function(selfSelector) {\n                            selectorsToAdd.push(extendVisitor.extendSelector(matches, selectorPath, selfSelector));\n                        });\n                    }\n                }\n            }\n            rulesetNode.paths = rulesetNode.paths.concat(selectorsToAdd);\n        },\n        findMatch: function (extend, haystackSelectorPath) {\n            //\n            // look through the haystack selector path to try and find the needle - extend.selector\n            // returns an array of selector matches that can then be replaced\n            //\n            var haystackSelectorIndex, hackstackSelector, hackstackElementIndex, haystackElement,\n                targetCombinator, i,\n                extendVisitor = this,\n                needleElements = extend.selector.elements,\n                potentialMatches = [], potentialMatch, matches = [];\n\n            // loop through the haystack elements\n            for(haystackSelectorIndex = 0; haystackSelectorIndex < haystackSelectorPath.length; haystackSelectorIndex++) {\n                hackstackSelector = haystackSelectorPath[haystackSelectorIndex];\n\n                for(hackstackElementIndex = 0; hackstackElementIndex < hackstackSelector.elements.length; hackstackElementIndex++) {\n\n                    haystackElement = hackstackSelector.elements[hackstackElementIndex];\n\n                    // if we allow elements before our match we can add a potential match every time. otherwise only at the first element.\n                    if (extend.allowBefore || (haystackSelectorIndex == 0 && hackstackElementIndex == 0)) {\n                        potentialMatches.push({pathIndex: haystackSelectorIndex, index: hackstackElementIndex, matched: 0, initialCombinator: haystackElement.combinator});\n                    }\n\n                    for(i = 0; i < potentialMatches.length; i++) {\n                        potentialMatch = potentialMatches[i];\n\n                        // selectors add \" \" onto the first element. When we use & it joins the selectors together, but if we don't\n                        // then each selector in haystackSelectorPath has a space before it added in the toCSS phase. so we need to work out\n                        // what the resulting combinator will be\n                        targetCombinator = haystackElement.combinator.value;\n                        if (targetCombinator == '' && hackstackElementIndex === 0) {\n                            targetCombinator = ' ';\n                        }\n\n                        // if we don't match, null our match to indicate failure\n                        if (!extendVisitor.isElementValuesEqual(needleElements[potentialMatch.matched].value, haystackElement.value) ||\n                            (potentialMatch.matched > 0 && needleElements[potentialMatch.matched].combinator.value !== targetCombinator)) {\n                            potentialMatch = null;\n                        } else {\n                            potentialMatch.matched++;\n                        }\n\n                        // if we are still valid and have finished, test whether we have elements after and whether these are allowed\n                        if (potentialMatch) {\n                            potentialMatch.finished = potentialMatch.matched === needleElements.length;\n                            if (potentialMatch.finished &&\n                                (!extend.allowAfter && (hackstackElementIndex+1 < hackstackSelector.elements.length || haystackSelectorIndex+1 < haystackSelectorPath.length))) {\n                                potentialMatch = null;\n                            }\n                        }\n                        // if null we remove, if not, we are still valid, so either push as a valid match or continue\n                        if (potentialMatch) {\n                            if (potentialMatch.finished) {\n                                potentialMatch.length = needleElements.length;\n                                potentialMatch.endPathIndex = haystackSelectorIndex;\n                                potentialMatch.endPathElementIndex = hackstackElementIndex + 1; // index after end of match\n                                potentialMatches.length = 0; // we don't allow matches to overlap, so start matching again\n                                matches.push(potentialMatch);\n                            }\n                        } else {\n                            potentialMatches.splice(i, 1);\n                            i--;\n                        }\n                    }\n                }\n            }\n            return matches;\n        },\n        isElementValuesEqual: function(elementValue1, elementValue2) {\n            if (typeof elementValue1 === \"string\" || typeof elementValue2 === \"string\") {\n                return elementValue1 === elementValue2;\n            }\n            if (elementValue1 instanceof tree.Attribute) {\n                if (elementValue1.op !== elementValue2.op || elementValue1.key !== elementValue2.key) {\n                    return false;\n                }\n                if (!elementValue1.value || !elementValue2.value) {\n                    if (elementValue1.value || elementValue2.value) {\n                        return false;\n                    }\n                    return true;\n                }\n                elementValue1 = elementValue1.value.value || elementValue1.value;\n                elementValue2 = elementValue2.value.value || elementValue2.value;\n                return elementValue1 === elementValue2;\n            }\n            return false;\n        },\n        extendSelector:function (matches, selectorPath, replacementSelector) {\n\n            //for a set of matches, replace each match with the replacement selector\n\n            var currentSelectorPathIndex = 0,\n                currentSelectorPathElementIndex = 0,\n                path = [],\n                matchIndex,\n                selector,\n                firstElement,\n                match;\n\n            for (matchIndex = 0; matchIndex < matches.length; matchIndex++) {\n                match = matches[matchIndex];\n                selector = selectorPath[match.pathIndex];\n                firstElement = new tree.Element(\n                    match.initialCombinator,\n                    replacementSelector.elements[0].value,\n                    replacementSelector.elements[0].index\n                );\n\n                if (match.pathIndex > currentSelectorPathIndex && currentSelectorPathElementIndex > 0) {\n                    path[path.length - 1].elements = path[path.length - 1].elements.concat(selectorPath[currentSelectorPathIndex].elements.slice(currentSelectorPathElementIndex));\n                    currentSelectorPathElementIndex = 0;\n                    currentSelectorPathIndex++;\n                }\n\n                path = path.concat(selectorPath.slice(currentSelectorPathIndex, match.pathIndex));\n\n                path.push(new tree.Selector(\n                    selector.elements\n                        .slice(currentSelectorPathElementIndex, match.index)\n                        .concat([firstElement])\n                        .concat(replacementSelector.elements.slice(1))\n                ));\n                currentSelectorPathIndex = match.endPathIndex;\n                currentSelectorPathElementIndex = match.endPathElementIndex;\n                if (currentSelectorPathElementIndex >= selector.elements.length) {\n                    currentSelectorPathElementIndex = 0;\n                    currentSelectorPathIndex++;\n                }\n            }\n\n            if (currentSelectorPathIndex < selectorPath.length && currentSelectorPathElementIndex > 0) {\n                path[path.length - 1].elements = path[path.length - 1].elements.concat(selectorPath[currentSelectorPathIndex].elements.slice(currentSelectorPathElementIndex));\n                currentSelectorPathElementIndex = 0;\n                currentSelectorPathIndex++;\n            }\n\n            path = path.concat(selectorPath.slice(currentSelectorPathIndex, selectorPath.length));\n\n            return path;\n        },\n        visitRulesetOut: function (rulesetNode) {\n        },\n        visitMedia: function (mediaNode, visitArgs) {\n            var newAllExtends = mediaNode.allExtends.concat(this.allExtendsStack[this.allExtendsStack.length-1]);\n            newAllExtends = newAllExtends.concat(this.doExtendChaining(newAllExtends, mediaNode.allExtends));\n            this.allExtendsStack.push(newAllExtends);\n        },\n        visitMediaOut: function (mediaNode) {\n            this.allExtendsStack.length = this.allExtendsStack.length - 1;\n        },\n        visitDirective: function (directiveNode, visitArgs) {\n            var newAllExtends = directiveNode.allExtends.concat(this.allExtendsStack[this.allExtendsStack.length-1]);\n            newAllExtends = newAllExtends.concat(this.doExtendChaining(newAllExtends, directiveNode.allExtends));\n            this.allExtendsStack.push(newAllExtends);\n        },\n        visitDirectiveOut: function (directiveNode) {\n            this.allExtendsStack.length = this.allExtendsStack.length - 1;\n        }\n    };\n\n})(require('./tree'));//\n// browser.js - client-side engine\n//\n\nvar isFileProtocol = /^(file|chrome(-extension)?|resource|qrc|app):/.test(location.protocol);\n\nless.env = less.env || (location.hostname == '127.0.0.1' ||\n                        location.hostname == '0.0.0.0'   ||\n                        location.hostname == 'localhost' ||\n                        location.port.length > 0         ||\n                        isFileProtocol                   ? 'development'\n                                                         : 'production');\n\n// Load styles asynchronously (default: false)\n//\n// This is set to `false` by default, so that the body\n// doesn't start loading before the stylesheets are parsed.\n// Setting this to `true` can result in flickering.\n//\nless.async = less.async || false;\nless.fileAsync = less.fileAsync || false;\n\n// Interval between watch polls\nless.poll = less.poll || (isFileProtocol ? 1000 : 1500);\n\n//Setup user functions\nif (less.functions) {\n    for(var func in less.functions) {\n        less.tree.functions[func] = less.functions[func];\n   }\n}\n\nvar dumpLineNumbers = /!dumpLineNumbers:(comments|mediaquery|all)/.exec(location.hash);\nif (dumpLineNumbers) {\n    less.dumpLineNumbers = dumpLineNumbers[1];\n}\n\n//\n// Watch mode\n//\nless.watch   = function () {\n    if (!less.watchMode ){\n        less.env = 'development';\n         initRunningMode();\n    }\n    return this.watchMode = true \n};\n\nless.unwatch = function () {clearInterval(less.watchTimer); return this.watchMode = false; };\n\nfunction initRunningMode(){\n    if (less.env === 'development') {\n        less.optimization = 0;\n        less.watchTimer = setInterval(function () {\n            if (less.watchMode) {\n                loadStyleSheets(function (e, root, _, sheet, env) {\n                    if (e) {\n                        error(e, sheet.href);\n                    } else if (root) {\n                        createCSS(root.toCSS(less), sheet, env.lastModified);\n                    }\n                });\n            }\n        }, less.poll);\n    } else {\n        less.optimization = 3;\n    }\n}\n\nif (/!watch/.test(location.hash)) {\n    less.watch();\n}\n\nvar cache = null;\n\nif (less.env != 'development') {\n    try {\n        cache = (typeof(window.localStorage) === 'undefined') ? null : window.localStorage;\n    } catch (_) {}\n}\n\n//\n// Get all <link> tags with the 'rel' attribute set to \"stylesheet/less\"\n//\nvar links = document.getElementsByTagName('link');\nvar typePattern = /^text\\/(x-)?less$/;\n\nless.sheets = [];\n\nfor (var i = 0; i < links.length; i++) {\n    if (links[i].rel === 'stylesheet/less' || (links[i].rel.match(/stylesheet/) &&\n       (links[i].type.match(typePattern)))) {\n        less.sheets.push(links[i]);\n    }\n}\n\n//\n// With this function, it's possible to alter variables and re-render\n// CSS without reloading less-files\n//\nvar session_cache = '';\nless.modifyVars = function(record) {\n    var str = session_cache;\n    for (var name in record) {\n        str += ((name.slice(0,1) === '@')? '' : '@') + name +': '+ \n                ((record[name].slice(-1) === ';')? record[name] : record[name] +';');\n    }\n    new(less.Parser)(new less.tree.parseEnv(less)).parse(str, function (e, root) {\n        if (e) {\n            error(e, \"session_cache\");\n        } else {\n            createCSS(root.toCSS(less), less.sheets[less.sheets.length - 1]);\n        }\n    });\n};\n\nless.refresh = function (reload) {\n    var startTime, endTime;\n    startTime = endTime = new(Date);\n\n    loadStyleSheets(function (e, root, _, sheet, env) {\n        if (e) {\n            return error(e, sheet.href);\n        }\n        if (env.local) {\n            log(\"loading \" + sheet.href + \" from cache.\");\n        } else {\n            log(\"parsed \" + sheet.href + \" successfully.\");\n            createCSS(root.toCSS(less), sheet, env.lastModified);\n        }\n        log(\"css for \" + sheet.href + \" generated in \" + (new(Date) - endTime) + 'ms');\n        (env.remaining === 0) && log(\"css generated in \" + (new(Date) - startTime) + 'ms');\n        endTime = new(Date);\n    }, reload);\n\n    loadStyles();\n};\nless.refreshStyles = loadStyles;\n\nless.refresh(less.env === 'development');\n\nfunction loadStyles() {\n    var styles = document.getElementsByTagName('style');\n    for (var i = 0; i < styles.length; i++) {\n        if (styles[i].type.match(typePattern)) {\n            var env = new less.tree.parseEnv(less);\n            env.filename = document.location.href.replace(/#.*$/, '');\n\n            new(less.Parser)(env).parse(styles[i].innerHTML || '', function (e, cssAST) {\n                if (e) {\n                    return error(e, \"inline\");\n                }\n                var css = cssAST.toCSS(less);\n                var style = styles[i];\n                style.type = 'text/css';\n                if (style.styleSheet) {\n                    style.styleSheet.cssText = css;\n                } else {\n                    style.innerHTML = css;\n                }\n            });\n        }\n    }\n}\n\nfunction loadStyleSheets(callback, reload) {\n    for (var i = 0; i < less.sheets.length; i++) {\n        loadStyleSheet(less.sheets[i], callback, reload, less.sheets.length - (i + 1));\n    }\n}\n\nfunction pathDiff(url, baseUrl) {\n    // diff between two paths to create a relative path\n\n    var urlParts = extractUrlParts(url),\n        baseUrlParts = extractUrlParts(baseUrl),\n        i, max, urlDirectories, baseUrlDirectories, diff = \"\";\n    if (urlParts.hostPart !== baseUrlParts.hostPart) {\n        return \"\";\n    }\n    max = Math.max(baseUrlParts.directories.length, urlParts.directories.length);\n    for(i = 0; i < max; i++) {\n        if (baseUrlParts.directories[i] !== urlParts.directories[i]) { break; }\n    }\n    baseUrlDirectories = baseUrlParts.directories.slice(i);\n    urlDirectories = urlParts.directories.slice(i);\n    for(i = 0; i < baseUrlDirectories.length-1; i++) {\n        diff += \"../\";\n    }\n    for(i = 0; i < urlDirectories.length-1; i++) {\n        diff += urlDirectories[i] + \"/\";\n    }\n    return diff;\n}\n\nfunction extractUrlParts(url, baseUrl) {\n    // urlParts[1] = protocol&hostname || /\n    // urlParts[2] = / if path relative to host base\n    // urlParts[3] = directories\n    // urlParts[4] = filename\n    // urlParts[5] = parameters\n\n    var urlPartsRegex = /^((?:[a-z-]+:)?\\/+?(?:[^\\/\\?#]*\\/)|([\\/\\\\]))?((?:[^\\/\\\\\\?#]*[\\/\\\\])*)([^\\/\\\\\\?#]*)([#\\?].*)?$/i,\n        urlParts = url.match(urlPartsRegex),\n        returner = {}, directories = [], i, baseUrlParts;\n\n    if (!urlParts) {\n        throw new Error(\"Could not parse sheet href - '\"+url+\"'\");\n    }\n\n    // Stylesheets in IE don't always return the full path    \n    if (!urlParts[1] || urlParts[2]) {\n        baseUrlParts = baseUrl.match(urlPartsRegex);\n        if (!baseUrlParts) {\n            throw new Error(\"Could not parse page url - '\"+baseUrl+\"'\");\n        }\n        urlParts[1] = urlParts[1] || baseUrlParts[1] || \"\";\n        if (!urlParts[2]) {\n            urlParts[3] = baseUrlParts[3] + urlParts[3];\n        }\n    }\n    \n    if (urlParts[3]) {\n        directories = urlParts[3].replace(/\\\\/g, \"/\").split(\"/\");\n\n        // extract out . before .. so .. doesn't absorb a non-directory\n        for(i = 0; i < directories.length; i++) {\n            if (directories[i] === \".\") {\n                directories.splice(i, 1);\n                i -= 1;\n            }\n        }\n\n        for(i = 0; i < directories.length; i++) {\n            if (directories[i] === \"..\" && i > 0) {\n                directories.splice(i-1, 2);\n                i -= 2;\n            }\n        }\n    }\n\n    returner.hostPart = urlParts[1];\n    returner.directories = directories;\n    returner.path = urlParts[1] + directories.join(\"/\");\n    returner.fileUrl = returner.path + (urlParts[4] || \"\");\n    returner.url = returner.fileUrl + (urlParts[5] || \"\");\n    return returner;\n}\n\nfunction loadStyleSheet(sheet, callback, reload, remaining) {\n\n    // sheet may be set to the stylesheet for the initial load or a collection of properties including\n    // some env variables for imports\n    var hrefParts = extractUrlParts(sheet.href, window.location.href);\n    var href      = hrefParts.url;\n    var css       = cache && cache.getItem(href);\n    var timestamp = cache && cache.getItem(href + ':timestamp');\n    var styles    = { css: css, timestamp: timestamp };\n    var env;\n    var newFileInfo = {\n            relativeUrls: less.relativeUrls,\n            currentDirectory: hrefParts.path,\n            filename: href\n        };\n\n    if (sheet instanceof less.tree.parseEnv) {\n        env = new less.tree.parseEnv(sheet);\n        newFileInfo.entryPath = env.currentFileInfo.entryPath;\n        newFileInfo.rootpath = env.currentFileInfo.rootpath;\n        newFileInfo.rootFilename = env.currentFileInfo.rootFilename;\n    } else {\n        env = new less.tree.parseEnv(less);\n        env.mime = sheet.type;\n        newFileInfo.entryPath = hrefParts.path;\n        newFileInfo.rootpath = less.rootpath || hrefParts.path;\n        newFileInfo.rootFilename = href;\n    }\n\n    if (env.relativeUrls) {\n        //todo - this relies on option being set on less object rather than being passed in as an option\n        //     - need an originalRootpath\n        if (less.rootpath) {\n            newFileInfo.rootpath = extractUrlParts(less.rootpath + pathDiff(hrefParts.path, newFileInfo.entryPath)).path;\n        } else {\n            newFileInfo.rootpath = hrefParts.path;\n        }\n    }\n\n    xhr(href, sheet.type, function (data, lastModified) {\n        // Store data this session\n        session_cache += data.replace(/@import .+?;/ig, '');\n\n        if (!reload && styles && lastModified &&\n           (new(Date)(lastModified).valueOf() ===\n            new(Date)(styles.timestamp).valueOf())) {\n            // Use local copy\n            createCSS(styles.css, sheet);\n            callback(null, null, data, sheet, { local: true, remaining: remaining }, href);\n        } else {\n            // Use remote copy (re-parse)\n            try {\n                env.contents[href] = data;  // Updating content cache\n                env.paths = [hrefParts.path];\n                env.currentFileInfo = newFileInfo;\n\n                new(less.Parser)(env).parse(data, function (e, root) {\n                    if (e) { return callback(e, null, null, sheet); }\n                    try {\n                        callback(e, root, data, sheet, { local: false, lastModified: lastModified, remaining: remaining }, href);\n                        //TODO - there must be a better way? A generic less-to-css function that can both call error\n                        //and removeNode where appropriate\n                        //should also add tests\n                        if (env.currentFileInfo.rootFilename === href) {\n                            removeNode(document.getElementById('less-error-message:' + extractId(href)));\n                        }\n                    } catch (e) {\n                        callback(e, null, null, sheet);\n                    }\n                });\n            } catch (e) {\n                callback(e, null, null, sheet);\n            }\n        }\n    }, function (status, url) {\n        callback({ type: 'File', message: \"'\" + url + \"' wasn't found (\" + status + \")\" }, null, null, sheet);\n    });\n}\n\nfunction extractId(href) {\n    return href.replace(/^[a-z-]+:\\/+?[^\\/]+/, '' )  // Remove protocol & domain\n               .replace(/^\\//,                 '' )  // Remove root /\n               .replace(/\\.[a-zA-Z]+$/,        '' )  // Remove simple extension\n               .replace(/[^\\.\\w-]+/g,          '-')  // Replace illegal characters\n               .replace(/\\./g,                 ':'); // Replace dots with colons(for valid id)\n}\n\nfunction createCSS(styles, sheet, lastModified) {\n    // Strip the query-string\n    var href = sheet.href || '';\n\n    // If there is no title set, use the filename, minus the extension\n    var id = 'less:' + (sheet.title || extractId(href));\n\n    // If this has already been inserted into the DOM, we may need to replace it\n    var oldCss = document.getElementById(id);\n    var keepOldCss = false;\n\n    // Create a new stylesheet node for insertion or (if necessary) replacement\n    var css = document.createElement('style');\n    css.setAttribute('type', 'text/css');\n    if (sheet.media) {\n        css.setAttribute('media', sheet.media);\n    }\n    css.id = id;\n\n    if (css.styleSheet) { // IE\n        try {\n            css.styleSheet.cssText = styles;\n        } catch (e) {\n            throw new(Error)(\"Couldn't reassign styleSheet.cssText.\");\n        }\n    } else {\n        css.appendChild(document.createTextNode(styles));\n\n        // If new contents match contents of oldCss, don't replace oldCss\n        keepOldCss = (oldCss !== null && oldCss.childNodes.length > 0 && css.childNodes.length > 0 &&\n            oldCss.firstChild.nodeValue === css.firstChild.nodeValue);\n    }\n\n    var head = document.getElementsByTagName('head')[0];\n\n    // If there is no oldCss, just append; otherwise, only append if we need\n    // to replace oldCss with an updated stylesheet\n    if (oldCss == null || keepOldCss === false) {\n        var nextEl = sheet && sheet.nextSibling || null;\n        (nextEl || document.getElementsByTagName('head')[0]).parentNode.insertBefore(css, nextEl);\n    }\n    if (oldCss && keepOldCss === false) {\n        head.removeChild(oldCss);\n    }\n\n    // Don't update the local store if the file wasn't modified\n    if (lastModified && cache) {\n        log('saving ' + href + ' to cache.');\n        try {\n            cache.setItem(href, styles);\n            cache.setItem(href + ':timestamp', lastModified);\n        } catch(e) {\n            //TODO - could do with adding more robust error handling\n            log('failed to save');\n        }\n    }\n}\n\nfunction xhr(url, type, callback, errback) {\n    var xhr = getXMLHttpRequest();\n    var async = isFileProtocol ? less.fileAsync : less.async;\n\n    if (typeof(xhr.overrideMimeType) === 'function') {\n        xhr.overrideMimeType('text/css');\n    }\n    xhr.open('GET', url, async);\n    xhr.setRequestHeader('Accept', type || 'text/x-less, text/css; q=0.9, */*; q=0.5');\n    xhr.send(null);\n\n    if (isFileProtocol && !less.fileAsync) {\n        if (xhr.status === 0 || (xhr.status >= 200 && xhr.status < 300)) {\n            callback(xhr.responseText);\n        } else {\n            errback(xhr.status, url);\n        }\n    } else if (async) {\n        xhr.onreadystatechange = function () {\n            if (xhr.readyState == 4) {\n                handleResponse(xhr, callback, errback);\n            }\n        };\n    } else {\n        handleResponse(xhr, callback, errback);\n    }\n\n    function handleResponse(xhr, callback, errback) {\n        if (xhr.status >= 200 && xhr.status < 300) {\n            callback(xhr.responseText,\n                     xhr.getResponseHeader(\"Last-Modified\"));\n        } else if (typeof(errback) === 'function') {\n            errback(xhr.status, url);\n        }\n    }\n}\n\nfunction getXMLHttpRequest() {\n    if (window.XMLHttpRequest) {\n        return new(XMLHttpRequest);\n    } else {\n        try {\n            return new(ActiveXObject)(\"MSXML2.XMLHTTP.3.0\");\n        } catch (e) {\n            log(\"browser doesn't support AJAX.\");\n            return null;\n        }\n    }\n}\n\nfunction removeNode(node) {\n    return node && node.parentNode.removeChild(node);\n}\n\nfunction log(str) {\n    if (less.env == 'development' && typeof(console) !== \"undefined\") { console.log('less: ' + str) }\n}\n\nfunction error(e, rootHref) {\n    var id = 'less-error-message:' + extractId(rootHref || \"\");\n    var template = '<li><label>{line}</label><pre class=\"{class}\">{content}</pre></li>';\n    var elem = document.createElement('div'), timer, content, error = [];\n    var filename = e.filename || rootHref;\n    var filenameNoPath = filename.match(/([^\\/]+(\\?.*)?)$/)[1];\n\n    elem.id        = id;\n    elem.className = \"less-error-message\";\n\n    content = '<h3>'  + (e.type || \"Syntax\") + \"Error: \" + (e.message || 'There is an error in your .less file') +\n              '</h3>' + '<p>in <a href=\"' + filename   + '\">' + filenameNoPath + \"</a> \";\n\n    var errorline = function (e, i, classname) {\n        if (e.extract[i] != undefined) {\n            error.push(template.replace(/\\{line\\}/, (parseInt(e.line) || 0) + (i - 1))\n                               .replace(/\\{class\\}/, classname)\n                               .replace(/\\{content\\}/, e.extract[i]));\n        }\n    };\n\n    if (e.extract) {\n        errorline(e, 0, '');\n        errorline(e, 1, 'line');\n        errorline(e, 2, '');\n        content += 'on line ' + e.line + ', column ' + (e.column + 1) + ':</p>' +\n                    '<ul>' + error.join('') + '</ul>';\n    } else if (e.stack) {\n        content += '<br/>' + e.stack.split('\\n').slice(1).join('<br/>');\n    }\n    elem.innerHTML = content;\n\n    // CSS for error messages\n    createCSS([\n        '.less-error-message ul, .less-error-message li {',\n            'list-style-type: none;',\n            'margin-right: 15px;',\n            'padding: 4px 0;',\n            'margin: 0;',\n        '}',\n        '.less-error-message label {',\n            'font-size: 12px;',\n            'margin-right: 15px;',\n            'padding: 4px 0;',\n            'color: #cc7777;',\n        '}',\n        '.less-error-message pre {',\n            'color: #dd6666;',\n            'padding: 4px 0;',\n            'margin: 0;',\n            'display: inline-block;',\n        '}',\n        '.less-error-message pre.line {',\n            'color: #ff0000;',\n        '}',\n        '.less-error-message h3 {',\n            'font-size: 20px;',\n            'font-weight: bold;',\n            'padding: 15px 0 5px 0;',\n            'margin: 0;',\n        '}',\n        '.less-error-message a {',\n            'color: #10a',\n        '}',\n        '.less-error-message .error {',\n            'color: red;',\n            'font-weight: bold;',\n            'padding-bottom: 2px;',\n            'border-bottom: 1px dashed red;',\n        '}'\n    ].join('\\n'), { title: 'error-message' });\n\n    elem.style.cssText = [\n        \"font-family: Arial, sans-serif\",\n        \"border: 1px solid #e00\",\n        \"background-color: #eee\",\n        \"border-radius: 5px\",\n        \"-webkit-border-radius: 5px\",\n        \"-moz-border-radius: 5px\",\n        \"color: #e00\",\n        \"padding: 15px\",\n        \"margin-bottom: 15px\"\n    ].join(';');\n\n    if (less.env == 'development') {\n        timer = setInterval(function () {\n            if (document.body) {\n                if (document.getElementById(id)) {\n                    document.body.replaceChild(elem, document.getElementById(id));\n                } else {\n                    document.body.insertBefore(elem, document.body.firstChild);\n                }\n                clearInterval(timer);\n            }\n        }, 10);\n    }\n}\n// amd.js\n//\n// Define Less as an AMD module.\nif (typeof define === \"function\" && define.amd) {\n    define(function () { return less; } );\n}\n})(window);\n"
  },
  {
    "path": "r2/r2/public/static/js/lib/modernizr.js",
    "content": "/* Modernizr 2.8.3 (Custom Build) | MIT & BSD\n * Build: http://modernizr.com/download/#-cssanimations-csstransforms-cssclasses-testprop-testallprops-domprefixes\n */\n;\n\n\n\nwindow.Modernizr = (function( window, document, undefined ) {\n\n    var version = '2.8.3',\n\n    Modernizr = {},\n\n    enableClasses = true,\n\n    docElement = document.documentElement,\n\n    mod = 'modernizr',\n    modElem = document.createElement(mod),\n    mStyle = modElem.style,\n\n    inputElem  ,\n\n\n    toString = {}.toString,    omPrefixes = 'Webkit Moz O ms',\n\n    cssomPrefixes = omPrefixes.split(' '),\n\n    domPrefixes = omPrefixes.toLowerCase().split(' '),\n\n\n    tests = {},\n    inputs = {},\n    attrs = {},\n\n    classes = [],\n\n    slice = classes.slice,\n\n    featureName,\n\n\n\n    _hasOwnProperty = ({}).hasOwnProperty, hasOwnProp;\n\n    if ( !is(_hasOwnProperty, 'undefined') && !is(_hasOwnProperty.call, 'undefined') ) {\n      hasOwnProp = function (object, property) {\n        return _hasOwnProperty.call(object, property);\n      };\n    }\n    else {\n      hasOwnProp = function (object, property) { \n        return ((property in object) && is(object.constructor.prototype[property], 'undefined'));\n      };\n    }\n\n\n    if (!Function.prototype.bind) {\n      Function.prototype.bind = function bind(that) {\n\n        var target = this;\n\n        if (typeof target != \"function\") {\n            throw new TypeError();\n        }\n\n        var args = slice.call(arguments, 1),\n            bound = function () {\n\n            if (this instanceof bound) {\n\n              var F = function(){};\n              F.prototype = target.prototype;\n              var self = new F();\n\n              var result = target.apply(\n                  self,\n                  args.concat(slice.call(arguments))\n              );\n              if (Object(result) === result) {\n                  return result;\n              }\n              return self;\n\n            } else {\n\n              return target.apply(\n                  that,\n                  args.concat(slice.call(arguments))\n              );\n\n            }\n\n        };\n\n        return bound;\n      };\n    }\n\n    function setCss( str ) {\n        mStyle.cssText = str;\n    }\n\n    function setCssAll( str1, str2 ) {\n        return setCss(prefixes.join(str1 + ';') + ( str2 || '' ));\n    }\n\n    function is( obj, type ) {\n        return typeof obj === type;\n    }\n\n    function contains( str, substr ) {\n        return !!~('' + str).indexOf(substr);\n    }\n\n    function testProps( props, prefixed ) {\n        for ( var i in props ) {\n            var prop = props[i];\n            if ( !contains(prop, \"-\") && mStyle[prop] !== undefined ) {\n                return prefixed == 'pfx' ? prop : true;\n            }\n        }\n        return false;\n    }\n\n    function testDOMProps( props, obj, elem ) {\n        for ( var i in props ) {\n            var item = obj[props[i]];\n            if ( item !== undefined) {\n\n                            if (elem === false) return props[i];\n\n                            if (is(item, 'function')){\n                                return item.bind(elem || obj);\n                }\n\n                            return item;\n            }\n        }\n        return false;\n    }\n\n    function testPropsAll( prop, prefixed, elem ) {\n\n        var ucProp  = prop.charAt(0).toUpperCase() + prop.slice(1),\n            props   = (prop + ' ' + cssomPrefixes.join(ucProp + ' ') + ucProp).split(' ');\n\n            if(is(prefixed, \"string\") || is(prefixed, \"undefined\")) {\n          return testProps(props, prefixed);\n\n            } else {\n          props = (prop + ' ' + (domPrefixes).join(ucProp + ' ') + ucProp).split(' ');\n          return testDOMProps(props, prefixed, elem);\n        }\n    }    tests['cssanimations'] = function() {\n        return testPropsAll('animationName');\n    };\n\n\n\n    tests['csstransforms'] = function() {\n        return !!testPropsAll('transform');\n    };\n\n\n    for ( var feature in tests ) {\n        if ( hasOwnProp(tests, feature) ) {\n                                    featureName  = feature.toLowerCase();\n            Modernizr[featureName] = tests[feature]();\n\n            classes.push((Modernizr[featureName] ? '' : 'no-') + featureName);\n        }\n    }\n\n\n\n     Modernizr.addTest = function ( feature, test ) {\n       if ( typeof feature == 'object' ) {\n         for ( var key in feature ) {\n           if ( hasOwnProp( feature, key ) ) {\n             Modernizr.addTest( key, feature[ key ] );\n           }\n         }\n       } else {\n\n         feature = feature.toLowerCase();\n\n         if ( Modernizr[feature] !== undefined ) {\n                                              return Modernizr;\n         }\n\n         test = typeof test == 'function' ? test() : test;\n\n         if (typeof enableClasses !== \"undefined\" && enableClasses) {\n           docElement.className += ' ' + (test ? '' : 'no-') + feature;\n         }\n         Modernizr[feature] = test;\n\n       }\n\n       return Modernizr; \n     };\n\n\n    setCss('');\n    modElem = inputElem = null;\n\n\n    Modernizr._version      = version;\n\n    Modernizr._domPrefixes  = domPrefixes;\n    Modernizr._cssomPrefixes  = cssomPrefixes;\n\n\n\n    Modernizr.testProp      = function(prop){\n        return testProps([prop]);\n    };\n\n    Modernizr.testAllProps  = testPropsAll;\n\n    docElement.className = docElement.className.replace(/(^|\\s)no-js(\\s|$)/, '$1$2') +\n\n                                                    (enableClasses ? ' js ' + classes.join(' ') : '');\n\n    return Modernizr;\n\n})(this, this.document);\n;"
  },
  {
    "path": "r2/r2/public/static/js/lib/react-with-addons-0.11.2.js",
    "content": "/**\n * React (with addons) v0.11.2\n */\n!function(e){if(\"object\"==typeof exports&&\"undefined\"!=typeof module)module.exports=e();else if(\"function\"==typeof define&&define.amd)define([],e);else{var f;\"undefined\"!=typeof window?f=window:\"undefined\"!=typeof global?f=global:\"undefined\"!=typeof self&&(f=self),f.React=e()}}(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require==\"function\"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error(\"Cannot find module '\"+o+\"'\")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require==\"function\"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule AutoFocusMixin\n * @typechecks static-only\n */\n\n\"use strict\";\n\nvar focusNode = _dereq_(\"./focusNode\");\n\nvar AutoFocusMixin = {\n  componentDidMount: function() {\n    if (this.props.autoFocus) {\n      focusNode(this.getDOMNode());\n    }\n  }\n};\n\nmodule.exports = AutoFocusMixin;\n\n},{\"./focusNode\":120}],2:[function(_dereq_,module,exports){\n/**\n * Copyright 2013 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule BeforeInputEventPlugin\n * @typechecks static-only\n */\n\n\"use strict\";\n\nvar EventConstants = _dereq_(\"./EventConstants\");\nvar EventPropagators = _dereq_(\"./EventPropagators\");\nvar ExecutionEnvironment = _dereq_(\"./ExecutionEnvironment\");\nvar SyntheticInputEvent = _dereq_(\"./SyntheticInputEvent\");\n\nvar keyOf = _dereq_(\"./keyOf\");\n\nvar canUseTextInputEvent = (\n  ExecutionEnvironment.canUseDOM &&\n  'TextEvent' in window &&\n  !('documentMode' in document || isPresto())\n);\n\n/**\n * Opera <= 12 includes TextEvent in window, but does not fire\n * text input events. Rely on keypress instead.\n */\nfunction isPresto() {\n  var opera = window.opera;\n  return (\n    typeof opera === 'object' &&\n    typeof opera.version === 'function' &&\n    parseInt(opera.version(), 10) <= 12\n  );\n}\n\nvar SPACEBAR_CODE = 32;\nvar SPACEBAR_CHAR = String.fromCharCode(SPACEBAR_CODE);\n\nvar topLevelTypes = EventConstants.topLevelTypes;\n\n// Events and their corresponding property names.\nvar eventTypes = {\n  beforeInput: {\n    phasedRegistrationNames: {\n      bubbled: keyOf({onBeforeInput: null}),\n      captured: keyOf({onBeforeInputCapture: null})\n    },\n    dependencies: [\n      topLevelTypes.topCompositionEnd,\n      topLevelTypes.topKeyPress,\n      topLevelTypes.topTextInput,\n      topLevelTypes.topPaste\n    ]\n  }\n};\n\n// Track characters inserted via keypress and composition events.\nvar fallbackChars = null;\n\n/**\n * Return whether a native keypress event is assumed to be a command.\n * This is required because Firefox fires `keypress` events for key commands\n * (cut, copy, select-all, etc.) even though no character is inserted.\n */\nfunction isKeypressCommand(nativeEvent) {\n  return (\n    (nativeEvent.ctrlKey || nativeEvent.altKey || nativeEvent.metaKey) &&\n    // ctrlKey && altKey is equivalent to AltGr, and is not a command.\n    !(nativeEvent.ctrlKey && nativeEvent.altKey)\n  );\n}\n\n/**\n * Create an `onBeforeInput` event to match\n * http://www.w3.org/TR/2013/WD-DOM-Level-3-Events-20131105/#events-inputevents.\n *\n * This event plugin is based on the native `textInput` event\n * available in Chrome, Safari, Opera, and IE. This event fires after\n * `onKeyPress` and `onCompositionEnd`, but before `onInput`.\n *\n * `beforeInput` is spec'd but not implemented in any browsers, and\n * the `input` event does not provide any useful information about what has\n * actually been added, contrary to the spec. Thus, `textInput` is the best\n * available event to identify the characters that have actually been inserted\n * into the target node.\n */\nvar BeforeInputEventPlugin = {\n\n  eventTypes: eventTypes,\n\n  /**\n   * @param {string} topLevelType Record from `EventConstants`.\n   * @param {DOMEventTarget} topLevelTarget The listening component root node.\n   * @param {string} topLevelTargetID ID of `topLevelTarget`.\n   * @param {object} nativeEvent Native browser event.\n   * @return {*} An accumulation of synthetic events.\n   * @see {EventPluginHub.extractEvents}\n   */\n  extractEvents: function(\n      topLevelType,\n      topLevelTarget,\n      topLevelTargetID,\n      nativeEvent) {\n\n    var chars;\n\n    if (canUseTextInputEvent) {\n      switch (topLevelType) {\n        case topLevelTypes.topKeyPress:\n          /**\n           * If native `textInput` events are available, our goal is to make\n           * use of them. However, there is a special case: the spacebar key.\n           * In Webkit, preventing default on a spacebar `textInput` event\n           * cancels character insertion, but it *also* causes the browser\n           * to fall back to its default spacebar behavior of scrolling the\n           * page.\n           *\n           * Tracking at:\n           * https://code.google.com/p/chromium/issues/detail?id=355103\n           *\n           * To avoid this issue, use the keypress event as if no `textInput`\n           * event is available.\n           */\n          var which = nativeEvent.which;\n          if (which !== SPACEBAR_CODE) {\n            return;\n          }\n\n          chars = String.fromCharCode(which);\n          break;\n\n        case topLevelTypes.topTextInput:\n          // Record the characters to be added to the DOM.\n          chars = nativeEvent.data;\n\n          // If it's a spacebar character, assume that we have already handled\n          // it at the keypress level and bail immediately.\n          if (chars === SPACEBAR_CHAR) {\n            return;\n          }\n\n          // Otherwise, carry on.\n          break;\n\n        default:\n          // For other native event types, do nothing.\n          return;\n      }\n    } else {\n      switch (topLevelType) {\n        case topLevelTypes.topPaste:\n          // If a paste event occurs after a keypress, throw out the input\n          // chars. Paste events should not lead to BeforeInput events.\n          fallbackChars = null;\n          break;\n        case topLevelTypes.topKeyPress:\n          /**\n           * As of v27, Firefox may fire keypress events even when no character\n           * will be inserted. A few possibilities:\n           *\n           * - `which` is `0`. Arrow keys, Esc key, etc.\n           *\n           * - `which` is the pressed key code, but no char is available.\n           *   Ex: 'AltGr + d` in Polish. There is no modified character for\n           *   this key combination and no character is inserted into the\n           *   document, but FF fires the keypress for char code `100` anyway.\n           *   No `input` event will occur.\n           *\n           * - `which` is the pressed key code, but a command combination is\n           *   being used. Ex: `Cmd+C`. No character is inserted, and no\n           *   `input` event will occur.\n           */\n          if (nativeEvent.which && !isKeypressCommand(nativeEvent)) {\n            fallbackChars = String.fromCharCode(nativeEvent.which);\n          }\n          break;\n        case topLevelTypes.topCompositionEnd:\n          fallbackChars = nativeEvent.data;\n          break;\n      }\n\n      // If no changes have occurred to the fallback string, no relevant\n      // event has fired and we're done.\n      if (fallbackChars === null) {\n        return;\n      }\n\n      chars = fallbackChars;\n    }\n\n    // If no characters are being inserted, no BeforeInput event should\n    // be fired.\n    if (!chars) {\n      return;\n    }\n\n    var event = SyntheticInputEvent.getPooled(\n      eventTypes.beforeInput,\n      topLevelTargetID,\n      nativeEvent\n    );\n\n    event.data = chars;\n    fallbackChars = null;\n    EventPropagators.accumulateTwoPhaseDispatches(event);\n    return event;\n  }\n};\n\nmodule.exports = BeforeInputEventPlugin;\n\n},{\"./EventConstants\":16,\"./EventPropagators\":21,\"./ExecutionEnvironment\":22,\"./SyntheticInputEvent\":98,\"./keyOf\":141}],3:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule CSSCore\n * @typechecks\n */\n\nvar invariant = _dereq_(\"./invariant\");\n\n/**\n * The CSSCore module specifies the API (and implements most of the methods)\n * that should be used when dealing with the display of elements (via their\n * CSS classes and visibility on screen. It is an API focused on mutating the\n * display and not reading it as no logical state should be encoded in the\n * display of elements.\n */\n\nvar CSSCore = {\n\n  /**\n   * Adds the class passed in to the element if it doesn't already have it.\n   *\n   * @param {DOMElement} element the element to set the class on\n   * @param {string} className the CSS className\n   * @return {DOMElement} the element passed in\n   */\n  addClass: function(element, className) {\n    (\"production\" !== \"development\" ? invariant(\n      !/\\s/.test(className),\n      'CSSCore.addClass takes only a single class name. \"%s\" contains ' +\n      'multiple classes.', className\n    ) : invariant(!/\\s/.test(className)));\n\n    if (className) {\n      if (element.classList) {\n        element.classList.add(className);\n      } else if (!CSSCore.hasClass(element, className)) {\n        element.className = element.className + ' ' + className;\n      }\n    }\n    return element;\n  },\n\n  /**\n   * Removes the class passed in from the element\n   *\n   * @param {DOMElement} element the element to set the class on\n   * @param {string} className the CSS className\n   * @return {DOMElement} the element passed in\n   */\n  removeClass: function(element, className) {\n    (\"production\" !== \"development\" ? invariant(\n      !/\\s/.test(className),\n      'CSSCore.removeClass takes only a single class name. \"%s\" contains ' +\n      'multiple classes.', className\n    ) : invariant(!/\\s/.test(className)));\n\n    if (className) {\n      if (element.classList) {\n        element.classList.remove(className);\n      } else if (CSSCore.hasClass(element, className)) {\n        element.className = element.className\n          .replace(new RegExp('(^|\\\\s)' + className + '(?:\\\\s|$)', 'g'), '$1')\n          .replace(/\\s+/g, ' ') // multiple spaces to one\n          .replace(/^\\s*|\\s*$/g, ''); // trim the ends\n      }\n    }\n    return element;\n  },\n\n  /**\n   * Helper to add or remove a class from an element based on a condition.\n   *\n   * @param {DOMElement} element the element to set the class on\n   * @param {string} className the CSS className\n   * @param {*} bool condition to whether to add or remove the class\n   * @return {DOMElement} the element passed in\n   */\n  conditionClass: function(element, className, bool) {\n    return (bool ? CSSCore.addClass : CSSCore.removeClass)(element, className);\n  },\n\n  /**\n   * Tests whether the element has the class specified.\n   *\n   * @param {DOMNode|DOMWindow} element the element to set the class on\n   * @param {string} className the CSS className\n   * @returns {boolean} true if the element has the class, false if not\n   */\n  hasClass: function(element, className) {\n    (\"production\" !== \"development\" ? invariant(\n      !/\\s/.test(className),\n      'CSS.hasClass takes only a single class name.'\n    ) : invariant(!/\\s/.test(className)));\n    if (element.classList) {\n      return !!className && element.classList.contains(className);\n    }\n    return (' ' + element.className + ' ').indexOf(' ' + className + ' ') > -1;\n  }\n\n};\n\nmodule.exports = CSSCore;\n\n},{\"./invariant\":134}],4:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule CSSProperty\n */\n\n\"use strict\";\n\n/**\n * CSS properties which accept numbers but are not in units of \"px\".\n */\nvar isUnitlessNumber = {\n  columnCount: true,\n  fillOpacity: true,\n  flex: true,\n  flexGrow: true,\n  flexShrink: true,\n  fontWeight: true,\n  lineClamp: true,\n  lineHeight: true,\n  opacity: true,\n  order: true,\n  orphans: true,\n  widows: true,\n  zIndex: true,\n  zoom: true\n};\n\n/**\n * @param {string} prefix vendor-specific prefix, eg: Webkit\n * @param {string} key style name, eg: transitionDuration\n * @return {string} style name prefixed with `prefix`, properly camelCased, eg:\n * WebkitTransitionDuration\n */\nfunction prefixKey(prefix, key) {\n  return prefix + key.charAt(0).toUpperCase() + key.substring(1);\n}\n\n/**\n * Support style names that may come passed in prefixed by adding permutations\n * of vendor prefixes.\n */\nvar prefixes = ['Webkit', 'ms', 'Moz', 'O'];\n\n// Using Object.keys here, or else the vanilla for-in loop makes IE8 go into an\n// infinite loop, because it iterates over the newly added props too.\nObject.keys(isUnitlessNumber).forEach(function(prop) {\n  prefixes.forEach(function(prefix) {\n    isUnitlessNumber[prefixKey(prefix, prop)] = isUnitlessNumber[prop];\n  });\n});\n\n/**\n * Most style properties can be unset by doing .style[prop] = '' but IE8\n * doesn't like doing that with shorthand properties so for the properties that\n * IE8 breaks on, which are listed here, we instead unset each of the\n * individual properties. See http://bugs.jquery.com/ticket/12385.\n * The 4-value 'clock' properties like margin, padding, border-width seem to\n * behave without any problems. Curiously, list-style works too without any\n * special prodding.\n */\nvar shorthandPropertyExpansions = {\n  background: {\n    backgroundImage: true,\n    backgroundPosition: true,\n    backgroundRepeat: true,\n    backgroundColor: true\n  },\n  border: {\n    borderWidth: true,\n    borderStyle: true,\n    borderColor: true\n  },\n  borderBottom: {\n    borderBottomWidth: true,\n    borderBottomStyle: true,\n    borderBottomColor: true\n  },\n  borderLeft: {\n    borderLeftWidth: true,\n    borderLeftStyle: true,\n    borderLeftColor: true\n  },\n  borderRight: {\n    borderRightWidth: true,\n    borderRightStyle: true,\n    borderRightColor: true\n  },\n  borderTop: {\n    borderTopWidth: true,\n    borderTopStyle: true,\n    borderTopColor: true\n  },\n  font: {\n    fontStyle: true,\n    fontVariant: true,\n    fontWeight: true,\n    fontSize: true,\n    lineHeight: true,\n    fontFamily: true\n  }\n};\n\nvar CSSProperty = {\n  isUnitlessNumber: isUnitlessNumber,\n  shorthandPropertyExpansions: shorthandPropertyExpansions\n};\n\nmodule.exports = CSSProperty;\n\n},{}],5:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule CSSPropertyOperations\n * @typechecks static-only\n */\n\n\"use strict\";\n\nvar CSSProperty = _dereq_(\"./CSSProperty\");\n\nvar dangerousStyleValue = _dereq_(\"./dangerousStyleValue\");\nvar hyphenateStyleName = _dereq_(\"./hyphenateStyleName\");\nvar memoizeStringOnly = _dereq_(\"./memoizeStringOnly\");\n\nvar processStyleName = memoizeStringOnly(function(styleName) {\n  return hyphenateStyleName(styleName);\n});\n\n/**\n * Operations for dealing with CSS properties.\n */\nvar CSSPropertyOperations = {\n\n  /**\n   * Serializes a mapping of style properties for use as inline styles:\n   *\n   *   > createMarkupForStyles({width: '200px', height: 0})\n   *   \"width:200px;height:0;\"\n   *\n   * Undefined values are ignored so that declarative programming is easier.\n   * The result should be HTML-escaped before insertion into the DOM.\n   *\n   * @param {object} styles\n   * @return {?string}\n   */\n  createMarkupForStyles: function(styles) {\n    var serialized = '';\n    for (var styleName in styles) {\n      if (!styles.hasOwnProperty(styleName)) {\n        continue;\n      }\n      var styleValue = styles[styleName];\n      if (styleValue != null) {\n        serialized += processStyleName(styleName) + ':';\n        serialized += dangerousStyleValue(styleName, styleValue) + ';';\n      }\n    }\n    return serialized || null;\n  },\n\n  /**\n   * Sets the value for multiple styles on a node.  If a value is specified as\n   * '' (empty string), the corresponding style property will be unset.\n   *\n   * @param {DOMElement} node\n   * @param {object} styles\n   */\n  setValueForStyles: function(node, styles) {\n    var style = node.style;\n    for (var styleName in styles) {\n      if (!styles.hasOwnProperty(styleName)) {\n        continue;\n      }\n      var styleValue = dangerousStyleValue(styleName, styles[styleName]);\n      if (styleValue) {\n        style[styleName] = styleValue;\n      } else {\n        var expansion = CSSProperty.shorthandPropertyExpansions[styleName];\n        if (expansion) {\n          // Shorthand property that IE8 won't like unsetting, so unset each\n          // component to placate it\n          for (var individualStyleName in expansion) {\n            style[individualStyleName] = '';\n          }\n        } else {\n          style[styleName] = '';\n        }\n      }\n    }\n  }\n\n};\n\nmodule.exports = CSSPropertyOperations;\n\n},{\"./CSSProperty\":4,\"./dangerousStyleValue\":115,\"./hyphenateStyleName\":132,\"./memoizeStringOnly\":143}],6:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule CallbackQueue\n */\n\n\"use strict\";\n\nvar PooledClass = _dereq_(\"./PooledClass\");\n\nvar invariant = _dereq_(\"./invariant\");\nvar mixInto = _dereq_(\"./mixInto\");\n\n/**\n * A specialized pseudo-event module to help keep track of components waiting to\n * be notified when their DOM representations are available for use.\n *\n * This implements `PooledClass`, so you should never need to instantiate this.\n * Instead, use `CallbackQueue.getPooled()`.\n *\n * @class ReactMountReady\n * @implements PooledClass\n * @internal\n */\nfunction CallbackQueue() {\n  this._callbacks = null;\n  this._contexts = null;\n}\n\nmixInto(CallbackQueue, {\n\n  /**\n   * Enqueues a callback to be invoked when `notifyAll` is invoked.\n   *\n   * @param {function} callback Invoked when `notifyAll` is invoked.\n   * @param {?object} context Context to call `callback` with.\n   * @internal\n   */\n  enqueue: function(callback, context) {\n    this._callbacks = this._callbacks || [];\n    this._contexts = this._contexts || [];\n    this._callbacks.push(callback);\n    this._contexts.push(context);\n  },\n\n  /**\n   * Invokes all enqueued callbacks and clears the queue. This is invoked after\n   * the DOM representation of a component has been created or updated.\n   *\n   * @internal\n   */\n  notifyAll: function() {\n    var callbacks = this._callbacks;\n    var contexts = this._contexts;\n    if (callbacks) {\n      (\"production\" !== \"development\" ? invariant(\n        callbacks.length === contexts.length,\n        \"Mismatched list of contexts in callback queue\"\n      ) : invariant(callbacks.length === contexts.length));\n      this._callbacks = null;\n      this._contexts = null;\n      for (var i = 0, l = callbacks.length; i < l; i++) {\n        callbacks[i].call(contexts[i]);\n      }\n      callbacks.length = 0;\n      contexts.length = 0;\n    }\n  },\n\n  /**\n   * Resets the internal queue.\n   *\n   * @internal\n   */\n  reset: function() {\n    this._callbacks = null;\n    this._contexts = null;\n  },\n\n  /**\n   * `PooledClass` looks for this.\n   */\n  destructor: function() {\n    this.reset();\n  }\n\n});\n\nPooledClass.addPoolingTo(CallbackQueue);\n\nmodule.exports = CallbackQueue;\n\n},{\"./PooledClass\":28,\"./invariant\":134,\"./mixInto\":147}],7:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule ChangeEventPlugin\n */\n\n\"use strict\";\n\nvar EventConstants = _dereq_(\"./EventConstants\");\nvar EventPluginHub = _dereq_(\"./EventPluginHub\");\nvar EventPropagators = _dereq_(\"./EventPropagators\");\nvar ExecutionEnvironment = _dereq_(\"./ExecutionEnvironment\");\nvar ReactUpdates = _dereq_(\"./ReactUpdates\");\nvar SyntheticEvent = _dereq_(\"./SyntheticEvent\");\n\nvar isEventSupported = _dereq_(\"./isEventSupported\");\nvar isTextInputElement = _dereq_(\"./isTextInputElement\");\nvar keyOf = _dereq_(\"./keyOf\");\n\nvar topLevelTypes = EventConstants.topLevelTypes;\n\nvar eventTypes = {\n  change: {\n    phasedRegistrationNames: {\n      bubbled: keyOf({onChange: null}),\n      captured: keyOf({onChangeCapture: null})\n    },\n    dependencies: [\n      topLevelTypes.topBlur,\n      topLevelTypes.topChange,\n      topLevelTypes.topClick,\n      topLevelTypes.topFocus,\n      topLevelTypes.topInput,\n      topLevelTypes.topKeyDown,\n      topLevelTypes.topKeyUp,\n      topLevelTypes.topSelectionChange\n    ]\n  }\n};\n\n/**\n * For IE shims\n */\nvar activeElement = null;\nvar activeElementID = null;\nvar activeElementValue = null;\nvar activeElementValueProp = null;\n\n/**\n * SECTION: handle `change` event\n */\nfunction shouldUseChangeEvent(elem) {\n  return (\n    elem.nodeName === 'SELECT' ||\n    (elem.nodeName === 'INPUT' && elem.type === 'file')\n  );\n}\n\nvar doesChangeEventBubble = false;\nif (ExecutionEnvironment.canUseDOM) {\n  // See `handleChange` comment below\n  doesChangeEventBubble = isEventSupported('change') && (\n    !('documentMode' in document) || document.documentMode > 8\n  );\n}\n\nfunction manualDispatchChangeEvent(nativeEvent) {\n  var event = SyntheticEvent.getPooled(\n    eventTypes.change,\n    activeElementID,\n    nativeEvent\n  );\n  EventPropagators.accumulateTwoPhaseDispatches(event);\n\n  // If change and propertychange bubbled, we'd just bind to it like all the\n  // other events and have it go through ReactBrowserEventEmitter. Since it\n  // doesn't, we manually listen for the events and so we have to enqueue and\n  // process the abstract event manually.\n  //\n  // Batching is necessary here in order to ensure that all event handlers run\n  // before the next rerender (including event handlers attached to ancestor\n  // elements instead of directly on the input). Without this, controlled\n  // components don't work properly in conjunction with event bubbling because\n  // the component is rerendered and the value reverted before all the event\n  // handlers can run. See https://github.com/facebook/react/issues/708.\n  ReactUpdates.batchedUpdates(runEventInBatch, event);\n}\n\nfunction runEventInBatch(event) {\n  EventPluginHub.enqueueEvents(event);\n  EventPluginHub.processEventQueue();\n}\n\nfunction startWatchingForChangeEventIE8(target, targetID) {\n  activeElement = target;\n  activeElementID = targetID;\n  activeElement.attachEvent('onchange', manualDispatchChangeEvent);\n}\n\nfunction stopWatchingForChangeEventIE8() {\n  if (!activeElement) {\n    return;\n  }\n  activeElement.detachEvent('onchange', manualDispatchChangeEvent);\n  activeElement = null;\n  activeElementID = null;\n}\n\nfunction getTargetIDForChangeEvent(\n    topLevelType,\n    topLevelTarget,\n    topLevelTargetID) {\n  if (topLevelType === topLevelTypes.topChange) {\n    return topLevelTargetID;\n  }\n}\nfunction handleEventsForChangeEventIE8(\n    topLevelType,\n    topLevelTarget,\n    topLevelTargetID) {\n  if (topLevelType === topLevelTypes.topFocus) {\n    // stopWatching() should be a noop here but we call it just in case we\n    // missed a blur event somehow.\n    stopWatchingForChangeEventIE8();\n    startWatchingForChangeEventIE8(topLevelTarget, topLevelTargetID);\n  } else if (topLevelType === topLevelTypes.topBlur) {\n    stopWatchingForChangeEventIE8();\n  }\n}\n\n\n/**\n * SECTION: handle `input` event\n */\nvar isInputEventSupported = false;\nif (ExecutionEnvironment.canUseDOM) {\n  // IE9 claims to support the input event but fails to trigger it when\n  // deleting text, so we ignore its input events\n  isInputEventSupported = isEventSupported('input') && (\n    !('documentMode' in document) || document.documentMode > 9\n  );\n}\n\n/**\n * (For old IE.) Replacement getter/setter for the `value` property that gets\n * set on the active element.\n */\nvar newValueProp =  {\n  get: function() {\n    return activeElementValueProp.get.call(this);\n  },\n  set: function(val) {\n    // Cast to a string so we can do equality checks.\n    activeElementValue = '' + val;\n    activeElementValueProp.set.call(this, val);\n  }\n};\n\n/**\n * (For old IE.) Starts tracking propertychange events on the passed-in element\n * and override the value property so that we can distinguish user events from\n * value changes in JS.\n */\nfunction startWatchingForValueChange(target, targetID) {\n  activeElement = target;\n  activeElementID = targetID;\n  activeElementValue = target.value;\n  activeElementValueProp = Object.getOwnPropertyDescriptor(\n    target.constructor.prototype,\n    'value'\n  );\n\n  Object.defineProperty(activeElement, 'value', newValueProp);\n  activeElement.attachEvent('onpropertychange', handlePropertyChange);\n}\n\n/**\n * (For old IE.) Removes the event listeners from the currently-tracked element,\n * if any exists.\n */\nfunction stopWatchingForValueChange() {\n  if (!activeElement) {\n    return;\n  }\n\n  // delete restores the original property definition\n  delete activeElement.value;\n  activeElement.detachEvent('onpropertychange', handlePropertyChange);\n\n  activeElement = null;\n  activeElementID = null;\n  activeElementValue = null;\n  activeElementValueProp = null;\n}\n\n/**\n * (For old IE.) Handles a propertychange event, sending a `change` event if\n * the value of the active element has changed.\n */\nfunction handlePropertyChange(nativeEvent) {\n  if (nativeEvent.propertyName !== 'value') {\n    return;\n  }\n  var value = nativeEvent.srcElement.value;\n  if (value === activeElementValue) {\n    return;\n  }\n  activeElementValue = value;\n\n  manualDispatchChangeEvent(nativeEvent);\n}\n\n/**\n * If a `change` event should be fired, returns the target's ID.\n */\nfunction getTargetIDForInputEvent(\n    topLevelType,\n    topLevelTarget,\n    topLevelTargetID) {\n  if (topLevelType === topLevelTypes.topInput) {\n    // In modern browsers (i.e., not IE8 or IE9), the input event is exactly\n    // what we want so fall through here and trigger an abstract event\n    return topLevelTargetID;\n  }\n}\n\n// For IE8 and IE9.\nfunction handleEventsForInputEventIE(\n    topLevelType,\n    topLevelTarget,\n    topLevelTargetID) {\n  if (topLevelType === topLevelTypes.topFocus) {\n    // In IE8, we can capture almost all .value changes by adding a\n    // propertychange handler and looking for events with propertyName\n    // equal to 'value'\n    // In IE9, propertychange fires for most input events but is buggy and\n    // doesn't fire when text is deleted, but conveniently, selectionchange\n    // appears to fire in all of the remaining cases so we catch those and\n    // forward the event if the value has changed\n    // In either case, we don't want to call the event handler if the value\n    // is changed from JS so we redefine a setter for `.value` that updates\n    // our activeElementValue variable, allowing us to ignore those changes\n    //\n    // stopWatching() should be a noop here but we call it just in case we\n    // missed a blur event somehow.\n    stopWatchingForValueChange();\n    startWatchingForValueChange(topLevelTarget, topLevelTargetID);\n  } else if (topLevelType === topLevelTypes.topBlur) {\n    stopWatchingForValueChange();\n  }\n}\n\n// For IE8 and IE9.\nfunction getTargetIDForInputEventIE(\n    topLevelType,\n    topLevelTarget,\n    topLevelTargetID) {\n  if (topLevelType === topLevelTypes.topSelectionChange ||\n      topLevelType === topLevelTypes.topKeyUp ||\n      topLevelType === topLevelTypes.topKeyDown) {\n    // On the selectionchange event, the target is just document which isn't\n    // helpful for us so just check activeElement instead.\n    //\n    // 99% of the time, keydown and keyup aren't necessary. IE8 fails to fire\n    // propertychange on the first input event after setting `value` from a\n    // script and fires only keydown, keypress, keyup. Catching keyup usually\n    // gets it and catching keydown lets us fire an event for the first\n    // keystroke if user does a key repeat (it'll be a little delayed: right\n    // before the second keystroke). Other input methods (e.g., paste) seem to\n    // fire selectionchange normally.\n    if (activeElement && activeElement.value !== activeElementValue) {\n      activeElementValue = activeElement.value;\n      return activeElementID;\n    }\n  }\n}\n\n\n/**\n * SECTION: handle `click` event\n */\nfunction shouldUseClickEvent(elem) {\n  // Use the `click` event to detect changes to checkbox and radio inputs.\n  // This approach works across all browsers, whereas `change` does not fire\n  // until `blur` in IE8.\n  return (\n    elem.nodeName === 'INPUT' &&\n    (elem.type === 'checkbox' || elem.type === 'radio')\n  );\n}\n\nfunction getTargetIDForClickEvent(\n    topLevelType,\n    topLevelTarget,\n    topLevelTargetID) {\n  if (topLevelType === topLevelTypes.topClick) {\n    return topLevelTargetID;\n  }\n}\n\n/**\n * This plugin creates an `onChange` event that normalizes change events\n * across form elements. This event fires at a time when it's possible to\n * change the element's value without seeing a flicker.\n *\n * Supported elements are:\n * - input (see `isTextInputElement`)\n * - textarea\n * - select\n */\nvar ChangeEventPlugin = {\n\n  eventTypes: eventTypes,\n\n  /**\n   * @param {string} topLevelType Record from `EventConstants`.\n   * @param {DOMEventTarget} topLevelTarget The listening component root node.\n   * @param {string} topLevelTargetID ID of `topLevelTarget`.\n   * @param {object} nativeEvent Native browser event.\n   * @return {*} An accumulation of synthetic events.\n   * @see {EventPluginHub.extractEvents}\n   */\n  extractEvents: function(\n      topLevelType,\n      topLevelTarget,\n      topLevelTargetID,\n      nativeEvent) {\n\n    var getTargetIDFunc, handleEventFunc;\n    if (shouldUseChangeEvent(topLevelTarget)) {\n      if (doesChangeEventBubble) {\n        getTargetIDFunc = getTargetIDForChangeEvent;\n      } else {\n        handleEventFunc = handleEventsForChangeEventIE8;\n      }\n    } else if (isTextInputElement(topLevelTarget)) {\n      if (isInputEventSupported) {\n        getTargetIDFunc = getTargetIDForInputEvent;\n      } else {\n        getTargetIDFunc = getTargetIDForInputEventIE;\n        handleEventFunc = handleEventsForInputEventIE;\n      }\n    } else if (shouldUseClickEvent(topLevelTarget)) {\n      getTargetIDFunc = getTargetIDForClickEvent;\n    }\n\n    if (getTargetIDFunc) {\n      var targetID = getTargetIDFunc(\n        topLevelType,\n        topLevelTarget,\n        topLevelTargetID\n      );\n      if (targetID) {\n        var event = SyntheticEvent.getPooled(\n          eventTypes.change,\n          targetID,\n          nativeEvent\n        );\n        EventPropagators.accumulateTwoPhaseDispatches(event);\n        return event;\n      }\n    }\n\n    if (handleEventFunc) {\n      handleEventFunc(\n        topLevelType,\n        topLevelTarget,\n        topLevelTargetID\n      );\n    }\n  }\n\n};\n\nmodule.exports = ChangeEventPlugin;\n\n},{\"./EventConstants\":16,\"./EventPluginHub\":18,\"./EventPropagators\":21,\"./ExecutionEnvironment\":22,\"./ReactUpdates\":87,\"./SyntheticEvent\":96,\"./isEventSupported\":135,\"./isTextInputElement\":137,\"./keyOf\":141}],8:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule ClientReactRootIndex\n * @typechecks\n */\n\n\"use strict\";\n\nvar nextReactRootIndex = 0;\n\nvar ClientReactRootIndex = {\n  createReactRootIndex: function() {\n    return nextReactRootIndex++;\n  }\n};\n\nmodule.exports = ClientReactRootIndex;\n\n},{}],9:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule CompositionEventPlugin\n * @typechecks static-only\n */\n\n\"use strict\";\n\nvar EventConstants = _dereq_(\"./EventConstants\");\nvar EventPropagators = _dereq_(\"./EventPropagators\");\nvar ExecutionEnvironment = _dereq_(\"./ExecutionEnvironment\");\nvar ReactInputSelection = _dereq_(\"./ReactInputSelection\");\nvar SyntheticCompositionEvent = _dereq_(\"./SyntheticCompositionEvent\");\n\nvar getTextContentAccessor = _dereq_(\"./getTextContentAccessor\");\nvar keyOf = _dereq_(\"./keyOf\");\n\nvar END_KEYCODES = [9, 13, 27, 32]; // Tab, Return, Esc, Space\nvar START_KEYCODE = 229;\n\nvar useCompositionEvent = (\n  ExecutionEnvironment.canUseDOM &&\n  'CompositionEvent' in window\n);\n\n// In IE9+, we have access to composition events, but the data supplied\n// by the native compositionend event may be incorrect. In Korean, for example,\n// the compositionend event contains only one character regardless of\n// how many characters have been composed since compositionstart.\n// We therefore use the fallback data while still using the native\n// events as triggers.\nvar useFallbackData = (\n  !useCompositionEvent ||\n  (\n    'documentMode' in document &&\n    document.documentMode > 8 &&\n    document.documentMode <= 11\n  )\n);\n\nvar topLevelTypes = EventConstants.topLevelTypes;\nvar currentComposition = null;\n\n// Events and their corresponding property names.\nvar eventTypes = {\n  compositionEnd: {\n    phasedRegistrationNames: {\n      bubbled: keyOf({onCompositionEnd: null}),\n      captured: keyOf({onCompositionEndCapture: null})\n    },\n    dependencies: [\n      topLevelTypes.topBlur,\n      topLevelTypes.topCompositionEnd,\n      topLevelTypes.topKeyDown,\n      topLevelTypes.topKeyPress,\n      topLevelTypes.topKeyUp,\n      topLevelTypes.topMouseDown\n    ]\n  },\n  compositionStart: {\n    phasedRegistrationNames: {\n      bubbled: keyOf({onCompositionStart: null}),\n      captured: keyOf({onCompositionStartCapture: null})\n    },\n    dependencies: [\n      topLevelTypes.topBlur,\n      topLevelTypes.topCompositionStart,\n      topLevelTypes.topKeyDown,\n      topLevelTypes.topKeyPress,\n      topLevelTypes.topKeyUp,\n      topLevelTypes.topMouseDown\n    ]\n  },\n  compositionUpdate: {\n    phasedRegistrationNames: {\n      bubbled: keyOf({onCompositionUpdate: null}),\n      captured: keyOf({onCompositionUpdateCapture: null})\n    },\n    dependencies: [\n      topLevelTypes.topBlur,\n      topLevelTypes.topCompositionUpdate,\n      topLevelTypes.topKeyDown,\n      topLevelTypes.topKeyPress,\n      topLevelTypes.topKeyUp,\n      topLevelTypes.topMouseDown\n    ]\n  }\n};\n\n/**\n * Translate native top level events into event types.\n *\n * @param {string} topLevelType\n * @return {object}\n */\nfunction getCompositionEventType(topLevelType) {\n  switch (topLevelType) {\n    case topLevelTypes.topCompositionStart:\n      return eventTypes.compositionStart;\n    case topLevelTypes.topCompositionEnd:\n      return eventTypes.compositionEnd;\n    case topLevelTypes.topCompositionUpdate:\n      return eventTypes.compositionUpdate;\n  }\n}\n\n/**\n * Does our fallback best-guess model think this event signifies that\n * composition has begun?\n *\n * @param {string} topLevelType\n * @param {object} nativeEvent\n * @return {boolean}\n */\nfunction isFallbackStart(topLevelType, nativeEvent) {\n  return (\n    topLevelType === topLevelTypes.topKeyDown &&\n    nativeEvent.keyCode === START_KEYCODE\n  );\n}\n\n/**\n * Does our fallback mode think that this event is the end of composition?\n *\n * @param {string} topLevelType\n * @param {object} nativeEvent\n * @return {boolean}\n */\nfunction isFallbackEnd(topLevelType, nativeEvent) {\n  switch (topLevelType) {\n    case topLevelTypes.topKeyUp:\n      // Command keys insert or clear IME input.\n      return (END_KEYCODES.indexOf(nativeEvent.keyCode) !== -1);\n    case topLevelTypes.topKeyDown:\n      // Expect IME keyCode on each keydown. If we get any other\n      // code we must have exited earlier.\n      return (nativeEvent.keyCode !== START_KEYCODE);\n    case topLevelTypes.topKeyPress:\n    case topLevelTypes.topMouseDown:\n    case topLevelTypes.topBlur:\n      // Events are not possible without cancelling IME.\n      return true;\n    default:\n      return false;\n  }\n}\n\n/**\n * Helper class stores information about selection and document state\n * so we can figure out what changed at a later date.\n *\n * @param {DOMEventTarget} root\n */\nfunction FallbackCompositionState(root) {\n  this.root = root;\n  this.startSelection = ReactInputSelection.getSelection(root);\n  this.startValue = this.getText();\n}\n\n/**\n * Get current text of input.\n *\n * @return {string}\n */\nFallbackCompositionState.prototype.getText = function() {\n  return this.root.value || this.root[getTextContentAccessor()];\n};\n\n/**\n * Text that has changed since the start of composition.\n *\n * @return {string}\n */\nFallbackCompositionState.prototype.getData = function() {\n  var endValue = this.getText();\n  var prefixLength = this.startSelection.start;\n  var suffixLength = this.startValue.length - this.startSelection.end;\n\n  return endValue.substr(\n    prefixLength,\n    endValue.length - suffixLength - prefixLength\n  );\n};\n\n/**\n * This plugin creates `onCompositionStart`, `onCompositionUpdate` and\n * `onCompositionEnd` events on inputs, textareas and contentEditable\n * nodes.\n */\nvar CompositionEventPlugin = {\n\n  eventTypes: eventTypes,\n\n  /**\n   * @param {string} topLevelType Record from `EventConstants`.\n   * @param {DOMEventTarget} topLevelTarget The listening component root node.\n   * @param {string} topLevelTargetID ID of `topLevelTarget`.\n   * @param {object} nativeEvent Native browser event.\n   * @return {*} An accumulation of synthetic events.\n   * @see {EventPluginHub.extractEvents}\n   */\n  extractEvents: function(\n      topLevelType,\n      topLevelTarget,\n      topLevelTargetID,\n      nativeEvent) {\n\n    var eventType;\n    var data;\n\n    if (useCompositionEvent) {\n      eventType = getCompositionEventType(topLevelType);\n    } else if (!currentComposition) {\n      if (isFallbackStart(topLevelType, nativeEvent)) {\n        eventType = eventTypes.compositionStart;\n      }\n    } else if (isFallbackEnd(topLevelType, nativeEvent)) {\n      eventType = eventTypes.compositionEnd;\n    }\n\n    if (useFallbackData) {\n      // The current composition is stored statically and must not be\n      // overwritten while composition continues.\n      if (!currentComposition && eventType === eventTypes.compositionStart) {\n        currentComposition = new FallbackCompositionState(topLevelTarget);\n      } else if (eventType === eventTypes.compositionEnd) {\n        if (currentComposition) {\n          data = currentComposition.getData();\n          currentComposition = null;\n        }\n      }\n    }\n\n    if (eventType) {\n      var event = SyntheticCompositionEvent.getPooled(\n        eventType,\n        topLevelTargetID,\n        nativeEvent\n      );\n      if (data) {\n        // Inject data generated from fallback path into the synthetic event.\n        // This matches the property of native CompositionEventInterface.\n        event.data = data;\n      }\n      EventPropagators.accumulateTwoPhaseDispatches(event);\n      return event;\n    }\n  }\n};\n\nmodule.exports = CompositionEventPlugin;\n\n},{\"./EventConstants\":16,\"./EventPropagators\":21,\"./ExecutionEnvironment\":22,\"./ReactInputSelection\":63,\"./SyntheticCompositionEvent\":94,\"./getTextContentAccessor\":129,\"./keyOf\":141}],10:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule DOMChildrenOperations\n * @typechecks static-only\n */\n\n\"use strict\";\n\nvar Danger = _dereq_(\"./Danger\");\nvar ReactMultiChildUpdateTypes = _dereq_(\"./ReactMultiChildUpdateTypes\");\n\nvar getTextContentAccessor = _dereq_(\"./getTextContentAccessor\");\nvar invariant = _dereq_(\"./invariant\");\n\n/**\n * The DOM property to use when setting text content.\n *\n * @type {string}\n * @private\n */\nvar textContentAccessor = getTextContentAccessor();\n\n/**\n * Inserts `childNode` as a child of `parentNode` at the `index`.\n *\n * @param {DOMElement} parentNode Parent node in which to insert.\n * @param {DOMElement} childNode Child node to insert.\n * @param {number} index Index at which to insert the child.\n * @internal\n */\nfunction insertChildAt(parentNode, childNode, index) {\n  // By exploiting arrays returning `undefined` for an undefined index, we can\n  // rely exclusively on `insertBefore(node, null)` instead of also using\n  // `appendChild(node)`. However, using `undefined` is not allowed by all\n  // browsers so we must replace it with `null`.\n  parentNode.insertBefore(\n    childNode,\n    parentNode.childNodes[index] || null\n  );\n}\n\nvar updateTextContent;\nif (textContentAccessor === 'textContent') {\n  /**\n   * Sets the text content of `node` to `text`.\n   *\n   * @param {DOMElement} node Node to change\n   * @param {string} text New text content\n   */\n  updateTextContent = function(node, text) {\n    node.textContent = text;\n  };\n} else {\n  /**\n   * Sets the text content of `node` to `text`.\n   *\n   * @param {DOMElement} node Node to change\n   * @param {string} text New text content\n   */\n  updateTextContent = function(node, text) {\n    // In order to preserve newlines correctly, we can't use .innerText to set\n    // the contents (see #1080), so we empty the element then append a text node\n    while (node.firstChild) {\n      node.removeChild(node.firstChild);\n    }\n    if (text) {\n      var doc = node.ownerDocument || document;\n      node.appendChild(doc.createTextNode(text));\n    }\n  };\n}\n\n/**\n * Operations for updating with DOM children.\n */\nvar DOMChildrenOperations = {\n\n  dangerouslyReplaceNodeWithMarkup: Danger.dangerouslyReplaceNodeWithMarkup,\n\n  updateTextContent: updateTextContent,\n\n  /**\n   * Updates a component's children by processing a series of updates. The\n   * update configurations are each expected to have a `parentNode` property.\n   *\n   * @param {array<object>} updates List of update configurations.\n   * @param {array<string>} markupList List of markup strings.\n   * @internal\n   */\n  processUpdates: function(updates, markupList) {\n    var update;\n    // Mapping from parent IDs to initial child orderings.\n    var initialChildren = null;\n    // List of children that will be moved or removed.\n    var updatedChildren = null;\n\n    for (var i = 0; update = updates[i]; i++) {\n      if (update.type === ReactMultiChildUpdateTypes.MOVE_EXISTING ||\n          update.type === ReactMultiChildUpdateTypes.REMOVE_NODE) {\n        var updatedIndex = update.fromIndex;\n        var updatedChild = update.parentNode.childNodes[updatedIndex];\n        var parentID = update.parentID;\n\n        (\"production\" !== \"development\" ? invariant(\n          updatedChild,\n          'processUpdates(): Unable to find child %s of element. This ' +\n          'probably means the DOM was unexpectedly mutated (e.g., by the ' +\n          'browser), usually due to forgetting a <tbody> when using tables, ' +\n          'nesting <p> or <a> tags, or using non-SVG elements in an <svg> '+\n          'parent. Try inspecting the child nodes of the element with React ' +\n          'ID `%s`.',\n          updatedIndex,\n          parentID\n        ) : invariant(updatedChild));\n\n        initialChildren = initialChildren || {};\n        initialChildren[parentID] = initialChildren[parentID] || [];\n        initialChildren[parentID][updatedIndex] = updatedChild;\n\n        updatedChildren = updatedChildren || [];\n        updatedChildren.push(updatedChild);\n      }\n    }\n\n    var renderedMarkup = Danger.dangerouslyRenderMarkup(markupList);\n\n    // Remove updated children first so that `toIndex` is consistent.\n    if (updatedChildren) {\n      for (var j = 0; j < updatedChildren.length; j++) {\n        updatedChildren[j].parentNode.removeChild(updatedChildren[j]);\n      }\n    }\n\n    for (var k = 0; update = updates[k]; k++) {\n      switch (update.type) {\n        case ReactMultiChildUpdateTypes.INSERT_MARKUP:\n          insertChildAt(\n            update.parentNode,\n            renderedMarkup[update.markupIndex],\n            update.toIndex\n          );\n          break;\n        case ReactMultiChildUpdateTypes.MOVE_EXISTING:\n          insertChildAt(\n            update.parentNode,\n            initialChildren[update.parentID][update.fromIndex],\n            update.toIndex\n          );\n          break;\n        case ReactMultiChildUpdateTypes.TEXT_CONTENT:\n          updateTextContent(\n            update.parentNode,\n            update.textContent\n          );\n          break;\n        case ReactMultiChildUpdateTypes.REMOVE_NODE:\n          // Already removed by the for-loop above.\n          break;\n      }\n    }\n  }\n\n};\n\nmodule.exports = DOMChildrenOperations;\n\n},{\"./Danger\":13,\"./ReactMultiChildUpdateTypes\":69,\"./getTextContentAccessor\":129,\"./invariant\":134}],11:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule DOMProperty\n * @typechecks static-only\n */\n\n/*jslint bitwise: true */\n\n\"use strict\";\n\nvar invariant = _dereq_(\"./invariant\");\n\nvar DOMPropertyInjection = {\n  /**\n   * Mapping from normalized, camelcased property names to a configuration that\n   * specifies how the associated DOM property should be accessed or rendered.\n   */\n  MUST_USE_ATTRIBUTE: 0x1,\n  MUST_USE_PROPERTY: 0x2,\n  HAS_SIDE_EFFECTS: 0x4,\n  HAS_BOOLEAN_VALUE: 0x8,\n  HAS_NUMERIC_VALUE: 0x10,\n  HAS_POSITIVE_NUMERIC_VALUE: 0x20 | 0x10,\n  HAS_OVERLOADED_BOOLEAN_VALUE: 0x40,\n\n  /**\n   * Inject some specialized knowledge about the DOM. This takes a config object\n   * with the following properties:\n   *\n   * isCustomAttribute: function that given an attribute name will return true\n   * if it can be inserted into the DOM verbatim. Useful for data-* or aria-*\n   * attributes where it's impossible to enumerate all of the possible\n   * attribute names,\n   *\n   * Properties: object mapping DOM property name to one of the\n   * DOMPropertyInjection constants or null. If your attribute isn't in here,\n   * it won't get written to the DOM.\n   *\n   * DOMAttributeNames: object mapping React attribute name to the DOM\n   * attribute name. Attribute names not specified use the **lowercase**\n   * normalized name.\n   *\n   * DOMPropertyNames: similar to DOMAttributeNames but for DOM properties.\n   * Property names not specified use the normalized name.\n   *\n   * DOMMutationMethods: Properties that require special mutation methods. If\n   * `value` is undefined, the mutation method should unset the property.\n   *\n   * @param {object} domPropertyConfig the config as described above.\n   */\n  injectDOMPropertyConfig: function(domPropertyConfig) {\n    var Properties = domPropertyConfig.Properties || {};\n    var DOMAttributeNames = domPropertyConfig.DOMAttributeNames || {};\n    var DOMPropertyNames = domPropertyConfig.DOMPropertyNames || {};\n    var DOMMutationMethods = domPropertyConfig.DOMMutationMethods || {};\n\n    if (domPropertyConfig.isCustomAttribute) {\n      DOMProperty._isCustomAttributeFunctions.push(\n        domPropertyConfig.isCustomAttribute\n      );\n    }\n\n    for (var propName in Properties) {\n      (\"production\" !== \"development\" ? invariant(\n        !DOMProperty.isStandardName.hasOwnProperty(propName),\n        'injectDOMPropertyConfig(...): You\\'re trying to inject DOM property ' +\n        '\\'%s\\' which has already been injected. You may be accidentally ' +\n        'injecting the same DOM property config twice, or you may be ' +\n        'injecting two configs that have conflicting property names.',\n        propName\n      ) : invariant(!DOMProperty.isStandardName.hasOwnProperty(propName)));\n\n      DOMProperty.isStandardName[propName] = true;\n\n      var lowerCased = propName.toLowerCase();\n      DOMProperty.getPossibleStandardName[lowerCased] = propName;\n\n      if (DOMAttributeNames.hasOwnProperty(propName)) {\n        var attributeName = DOMAttributeNames[propName];\n        DOMProperty.getPossibleStandardName[attributeName] = propName;\n        DOMProperty.getAttributeName[propName] = attributeName;\n      } else {\n        DOMProperty.getAttributeName[propName] = lowerCased;\n      }\n\n      DOMProperty.getPropertyName[propName] =\n        DOMPropertyNames.hasOwnProperty(propName) ?\n          DOMPropertyNames[propName] :\n          propName;\n\n      if (DOMMutationMethods.hasOwnProperty(propName)) {\n        DOMProperty.getMutationMethod[propName] = DOMMutationMethods[propName];\n      } else {\n        DOMProperty.getMutationMethod[propName] = null;\n      }\n\n      var propConfig = Properties[propName];\n      DOMProperty.mustUseAttribute[propName] =\n        propConfig & DOMPropertyInjection.MUST_USE_ATTRIBUTE;\n      DOMProperty.mustUseProperty[propName] =\n        propConfig & DOMPropertyInjection.MUST_USE_PROPERTY;\n      DOMProperty.hasSideEffects[propName] =\n        propConfig & DOMPropertyInjection.HAS_SIDE_EFFECTS;\n      DOMProperty.hasBooleanValue[propName] =\n        propConfig & DOMPropertyInjection.HAS_BOOLEAN_VALUE;\n      DOMProperty.hasNumericValue[propName] =\n        propConfig & DOMPropertyInjection.HAS_NUMERIC_VALUE;\n      DOMProperty.hasPositiveNumericValue[propName] =\n        propConfig & DOMPropertyInjection.HAS_POSITIVE_NUMERIC_VALUE;\n      DOMProperty.hasOverloadedBooleanValue[propName] =\n        propConfig & DOMPropertyInjection.HAS_OVERLOADED_BOOLEAN_VALUE;\n\n      (\"production\" !== \"development\" ? invariant(\n        !DOMProperty.mustUseAttribute[propName] ||\n          !DOMProperty.mustUseProperty[propName],\n        'DOMProperty: Cannot require using both attribute and property: %s',\n        propName\n      ) : invariant(!DOMProperty.mustUseAttribute[propName] ||\n        !DOMProperty.mustUseProperty[propName]));\n      (\"production\" !== \"development\" ? invariant(\n        DOMProperty.mustUseProperty[propName] ||\n          !DOMProperty.hasSideEffects[propName],\n        'DOMProperty: Properties that have side effects must use property: %s',\n        propName\n      ) : invariant(DOMProperty.mustUseProperty[propName] ||\n        !DOMProperty.hasSideEffects[propName]));\n      (\"production\" !== \"development\" ? invariant(\n        !!DOMProperty.hasBooleanValue[propName] +\n          !!DOMProperty.hasNumericValue[propName] +\n          !!DOMProperty.hasOverloadedBooleanValue[propName] <= 1,\n        'DOMProperty: Value can be one of boolean, overloaded boolean, or ' +\n        'numeric value, but not a combination: %s',\n        propName\n      ) : invariant(!!DOMProperty.hasBooleanValue[propName] +\n        !!DOMProperty.hasNumericValue[propName] +\n        !!DOMProperty.hasOverloadedBooleanValue[propName] <= 1));\n    }\n  }\n};\nvar defaultValueCache = {};\n\n/**\n * DOMProperty exports lookup objects that can be used like functions:\n *\n *   > DOMProperty.isValid['id']\n *   true\n *   > DOMProperty.isValid['foobar']\n *   undefined\n *\n * Although this may be confusing, it performs better in general.\n *\n * @see http://jsperf.com/key-exists\n * @see http://jsperf.com/key-missing\n */\nvar DOMProperty = {\n\n  ID_ATTRIBUTE_NAME: 'data-reactid',\n\n  /**\n   * Checks whether a property name is a standard property.\n   * @type {Object}\n   */\n  isStandardName: {},\n\n  /**\n   * Mapping from lowercase property names to the properly cased version, used\n   * to warn in the case of missing properties.\n   * @type {Object}\n   */\n  getPossibleStandardName: {},\n\n  /**\n   * Mapping from normalized names to attribute names that differ. Attribute\n   * names are used when rendering markup or with `*Attribute()`.\n   * @type {Object}\n   */\n  getAttributeName: {},\n\n  /**\n   * Mapping from normalized names to properties on DOM node instances.\n   * (This includes properties that mutate due to external factors.)\n   * @type {Object}\n   */\n  getPropertyName: {},\n\n  /**\n   * Mapping from normalized names to mutation methods. This will only exist if\n   * mutation cannot be set simply by the property or `setAttribute()`.\n   * @type {Object}\n   */\n  getMutationMethod: {},\n\n  /**\n   * Whether the property must be accessed and mutated as an object property.\n   * @type {Object}\n   */\n  mustUseAttribute: {},\n\n  /**\n   * Whether the property must be accessed and mutated using `*Attribute()`.\n   * (This includes anything that fails `<propName> in <element>`.)\n   * @type {Object}\n   */\n  mustUseProperty: {},\n\n  /**\n   * Whether or not setting a value causes side effects such as triggering\n   * resources to be loaded or text selection changes. We must ensure that\n   * the value is only set if it has changed.\n   * @type {Object}\n   */\n  hasSideEffects: {},\n\n  /**\n   * Whether the property should be removed when set to a falsey value.\n   * @type {Object}\n   */\n  hasBooleanValue: {},\n\n  /**\n   * Whether the property must be numeric or parse as a\n   * numeric and should be removed when set to a falsey value.\n   * @type {Object}\n   */\n  hasNumericValue: {},\n\n  /**\n   * Whether the property must be positive numeric or parse as a positive\n   * numeric and should be removed when set to a falsey value.\n   * @type {Object}\n   */\n  hasPositiveNumericValue: {},\n\n  /**\n   * Whether the property can be used as a flag as well as with a value. Removed\n   * when strictly equal to false; present without a value when strictly equal\n   * to true; present with a value otherwise.\n   * @type {Object}\n   */\n  hasOverloadedBooleanValue: {},\n\n  /**\n   * All of the isCustomAttribute() functions that have been injected.\n   */\n  _isCustomAttributeFunctions: [],\n\n  /**\n   * Checks whether a property name is a custom attribute.\n   * @method\n   */\n  isCustomAttribute: function(attributeName) {\n    for (var i = 0; i < DOMProperty._isCustomAttributeFunctions.length; i++) {\n      var isCustomAttributeFn = DOMProperty._isCustomAttributeFunctions[i];\n      if (isCustomAttributeFn(attributeName)) {\n        return true;\n      }\n    }\n    return false;\n  },\n\n  /**\n   * Returns the default property value for a DOM property (i.e., not an\n   * attribute). Most default values are '' or false, but not all. Worse yet,\n   * some (in particular, `type`) vary depending on the type of element.\n   *\n   * TODO: Is it better to grab all the possible properties when creating an\n   * element to avoid having to create the same element twice?\n   */\n  getDefaultValueForProperty: function(nodeName, prop) {\n    var nodeDefaults = defaultValueCache[nodeName];\n    var testElement;\n    if (!nodeDefaults) {\n      defaultValueCache[nodeName] = nodeDefaults = {};\n    }\n    if (!(prop in nodeDefaults)) {\n      testElement = document.createElement(nodeName);\n      nodeDefaults[prop] = testElement[prop];\n    }\n    return nodeDefaults[prop];\n  },\n\n  injection: DOMPropertyInjection\n};\n\nmodule.exports = DOMProperty;\n\n},{\"./invariant\":134}],12:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule DOMPropertyOperations\n * @typechecks static-only\n */\n\n\"use strict\";\n\nvar DOMProperty = _dereq_(\"./DOMProperty\");\n\nvar escapeTextForBrowser = _dereq_(\"./escapeTextForBrowser\");\nvar memoizeStringOnly = _dereq_(\"./memoizeStringOnly\");\nvar warning = _dereq_(\"./warning\");\n\nfunction shouldIgnoreValue(name, value) {\n  return value == null ||\n    (DOMProperty.hasBooleanValue[name] && !value) ||\n    (DOMProperty.hasNumericValue[name] && isNaN(value)) ||\n    (DOMProperty.hasPositiveNumericValue[name] && (value < 1)) ||\n    (DOMProperty.hasOverloadedBooleanValue[name] && value === false);\n}\n\nvar processAttributeNameAndPrefix = memoizeStringOnly(function(name) {\n  return escapeTextForBrowser(name) + '=\"';\n});\n\nif (\"production\" !== \"development\") {\n  var reactProps = {\n    children: true,\n    dangerouslySetInnerHTML: true,\n    key: true,\n    ref: true\n  };\n  var warnedProperties = {};\n\n  var warnUnknownProperty = function(name) {\n    if (reactProps.hasOwnProperty(name) && reactProps[name] ||\n        warnedProperties.hasOwnProperty(name) && warnedProperties[name]) {\n      return;\n    }\n\n    warnedProperties[name] = true;\n    var lowerCasedName = name.toLowerCase();\n\n    // data-* attributes should be lowercase; suggest the lowercase version\n    var standardName = (\n      DOMProperty.isCustomAttribute(lowerCasedName) ?\n        lowerCasedName :\n      DOMProperty.getPossibleStandardName.hasOwnProperty(lowerCasedName) ?\n        DOMProperty.getPossibleStandardName[lowerCasedName] :\n        null\n    );\n\n    // For now, only warn when we have a suggested correction. This prevents\n    // logging too much when using transferPropsTo.\n    (\"production\" !== \"development\" ? warning(\n      standardName == null,\n      'Unknown DOM property ' + name + '. Did you mean ' + standardName + '?'\n    ) : null);\n\n  };\n}\n\n/**\n * Operations for dealing with DOM properties.\n */\nvar DOMPropertyOperations = {\n\n  /**\n   * Creates markup for the ID property.\n   *\n   * @param {string} id Unescaped ID.\n   * @return {string} Markup string.\n   */\n  createMarkupForID: function(id) {\n    return processAttributeNameAndPrefix(DOMProperty.ID_ATTRIBUTE_NAME) +\n      escapeTextForBrowser(id) + '\"';\n  },\n\n  /**\n   * Creates markup for a property.\n   *\n   * @param {string} name\n   * @param {*} value\n   * @return {?string} Markup string, or null if the property was invalid.\n   */\n  createMarkupForProperty: function(name, value) {\n    if (DOMProperty.isStandardName.hasOwnProperty(name) &&\n        DOMProperty.isStandardName[name]) {\n      if (shouldIgnoreValue(name, value)) {\n        return '';\n      }\n      var attributeName = DOMProperty.getAttributeName[name];\n      if (DOMProperty.hasBooleanValue[name] ||\n          (DOMProperty.hasOverloadedBooleanValue[name] && value === true)) {\n        return escapeTextForBrowser(attributeName);\n      }\n      return processAttributeNameAndPrefix(attributeName) +\n        escapeTextForBrowser(value) + '\"';\n    } else if (DOMProperty.isCustomAttribute(name)) {\n      if (value == null) {\n        return '';\n      }\n      return processAttributeNameAndPrefix(name) +\n        escapeTextForBrowser(value) + '\"';\n    } else if (\"production\" !== \"development\") {\n      warnUnknownProperty(name);\n    }\n    return null;\n  },\n\n  /**\n   * Sets the value for a property on a node.\n   *\n   * @param {DOMElement} node\n   * @param {string} name\n   * @param {*} value\n   */\n  setValueForProperty: function(node, name, value) {\n    if (DOMProperty.isStandardName.hasOwnProperty(name) &&\n        DOMProperty.isStandardName[name]) {\n      var mutationMethod = DOMProperty.getMutationMethod[name];\n      if (mutationMethod) {\n        mutationMethod(node, value);\n      } else if (shouldIgnoreValue(name, value)) {\n        this.deleteValueForProperty(node, name);\n      } else if (DOMProperty.mustUseAttribute[name]) {\n        node.setAttribute(DOMProperty.getAttributeName[name], '' + value);\n      } else {\n        var propName = DOMProperty.getPropertyName[name];\n        if (!DOMProperty.hasSideEffects[name] || node[propName] !== value) {\n          node[propName] = value;\n        }\n      }\n    } else if (DOMProperty.isCustomAttribute(name)) {\n      if (value == null) {\n        node.removeAttribute(name);\n      } else {\n        node.setAttribute(name, '' + value);\n      }\n    } else if (\"production\" !== \"development\") {\n      warnUnknownProperty(name);\n    }\n  },\n\n  /**\n   * Deletes the value for a property on a node.\n   *\n   * @param {DOMElement} node\n   * @param {string} name\n   */\n  deleteValueForProperty: function(node, name) {\n    if (DOMProperty.isStandardName.hasOwnProperty(name) &&\n        DOMProperty.isStandardName[name]) {\n      var mutationMethod = DOMProperty.getMutationMethod[name];\n      if (mutationMethod) {\n        mutationMethod(node, undefined);\n      } else if (DOMProperty.mustUseAttribute[name]) {\n        node.removeAttribute(DOMProperty.getAttributeName[name]);\n      } else {\n        var propName = DOMProperty.getPropertyName[name];\n        var defaultValue = DOMProperty.getDefaultValueForProperty(\n          node.nodeName,\n          propName\n        );\n        if (!DOMProperty.hasSideEffects[name] ||\n            node[propName] !== defaultValue) {\n          node[propName] = defaultValue;\n        }\n      }\n    } else if (DOMProperty.isCustomAttribute(name)) {\n      node.removeAttribute(name);\n    } else if (\"production\" !== \"development\") {\n      warnUnknownProperty(name);\n    }\n  }\n\n};\n\nmodule.exports = DOMPropertyOperations;\n\n},{\"./DOMProperty\":11,\"./escapeTextForBrowser\":118,\"./memoizeStringOnly\":143,\"./warning\":158}],13:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule Danger\n * @typechecks static-only\n */\n\n/*jslint evil: true, sub: true */\n\n\"use strict\";\n\nvar ExecutionEnvironment = _dereq_(\"./ExecutionEnvironment\");\n\nvar createNodesFromMarkup = _dereq_(\"./createNodesFromMarkup\");\nvar emptyFunction = _dereq_(\"./emptyFunction\");\nvar getMarkupWrap = _dereq_(\"./getMarkupWrap\");\nvar invariant = _dereq_(\"./invariant\");\n\nvar OPEN_TAG_NAME_EXP = /^(<[^ \\/>]+)/;\nvar RESULT_INDEX_ATTR = 'data-danger-index';\n\n/**\n * Extracts the `nodeName` from a string of markup.\n *\n * NOTE: Extracting the `nodeName` does not require a regular expression match\n * because we make assumptions about React-generated markup (i.e. there are no\n * spaces surrounding the opening tag and there is at least one attribute).\n *\n * @param {string} markup String of markup.\n * @return {string} Node name of the supplied markup.\n * @see http://jsperf.com/extract-nodename\n */\nfunction getNodeName(markup) {\n  return markup.substring(1, markup.indexOf(' '));\n}\n\nvar Danger = {\n\n  /**\n   * Renders markup into an array of nodes. The markup is expected to render\n   * into a list of root nodes. Also, the length of `resultList` and\n   * `markupList` should be the same.\n   *\n   * @param {array<string>} markupList List of markup strings to render.\n   * @return {array<DOMElement>} List of rendered nodes.\n   * @internal\n   */\n  dangerouslyRenderMarkup: function(markupList) {\n    (\"production\" !== \"development\" ? invariant(\n      ExecutionEnvironment.canUseDOM,\n      'dangerouslyRenderMarkup(...): Cannot render markup in a Worker ' +\n      'thread. This is likely a bug in the framework. Please report ' +\n      'immediately.'\n    ) : invariant(ExecutionEnvironment.canUseDOM));\n    var nodeName;\n    var markupByNodeName = {};\n    // Group markup by `nodeName` if a wrap is necessary, else by '*'.\n    for (var i = 0; i < markupList.length; i++) {\n      (\"production\" !== \"development\" ? invariant(\n        markupList[i],\n        'dangerouslyRenderMarkup(...): Missing markup.'\n      ) : invariant(markupList[i]));\n      nodeName = getNodeName(markupList[i]);\n      nodeName = getMarkupWrap(nodeName) ? nodeName : '*';\n      markupByNodeName[nodeName] = markupByNodeName[nodeName] || [];\n      markupByNodeName[nodeName][i] = markupList[i];\n    }\n    var resultList = [];\n    var resultListAssignmentCount = 0;\n    for (nodeName in markupByNodeName) {\n      if (!markupByNodeName.hasOwnProperty(nodeName)) {\n        continue;\n      }\n      var markupListByNodeName = markupByNodeName[nodeName];\n\n      // This for-in loop skips the holes of the sparse array. The order of\n      // iteration should follow the order of assignment, which happens to match\n      // numerical index order, but we don't rely on that.\n      for (var resultIndex in markupListByNodeName) {\n        if (markupListByNodeName.hasOwnProperty(resultIndex)) {\n          var markup = markupListByNodeName[resultIndex];\n\n          // Push the requested markup with an additional RESULT_INDEX_ATTR\n          // attribute.  If the markup does not start with a < character, it\n          // will be discarded below (with an appropriate console.error).\n          markupListByNodeName[resultIndex] = markup.replace(\n            OPEN_TAG_NAME_EXP,\n            // This index will be parsed back out below.\n            '$1 ' + RESULT_INDEX_ATTR + '=\"' + resultIndex + '\" '\n          );\n        }\n      }\n\n      // Render each group of markup with similar wrapping `nodeName`.\n      var renderNodes = createNodesFromMarkup(\n        markupListByNodeName.join(''),\n        emptyFunction // Do nothing special with <script> tags.\n      );\n\n      for (i = 0; i < renderNodes.length; ++i) {\n        var renderNode = renderNodes[i];\n        if (renderNode.hasAttribute &&\n            renderNode.hasAttribute(RESULT_INDEX_ATTR)) {\n\n          resultIndex = +renderNode.getAttribute(RESULT_INDEX_ATTR);\n          renderNode.removeAttribute(RESULT_INDEX_ATTR);\n\n          (\"production\" !== \"development\" ? invariant(\n            !resultList.hasOwnProperty(resultIndex),\n            'Danger: Assigning to an already-occupied result index.'\n          ) : invariant(!resultList.hasOwnProperty(resultIndex)));\n\n          resultList[resultIndex] = renderNode;\n\n          // This should match resultList.length and markupList.length when\n          // we're done.\n          resultListAssignmentCount += 1;\n\n        } else if (\"production\" !== \"development\") {\n          console.error(\n            \"Danger: Discarding unexpected node:\",\n            renderNode\n          );\n        }\n      }\n    }\n\n    // Although resultList was populated out of order, it should now be a dense\n    // array.\n    (\"production\" !== \"development\" ? invariant(\n      resultListAssignmentCount === resultList.length,\n      'Danger: Did not assign to every index of resultList.'\n    ) : invariant(resultListAssignmentCount === resultList.length));\n\n    (\"production\" !== \"development\" ? invariant(\n      resultList.length === markupList.length,\n      'Danger: Expected markup to render %s nodes, but rendered %s.',\n      markupList.length,\n      resultList.length\n    ) : invariant(resultList.length === markupList.length));\n\n    return resultList;\n  },\n\n  /**\n   * Replaces a node with a string of markup at its current position within its\n   * parent. The markup must render into a single root node.\n   *\n   * @param {DOMElement} oldChild Child node to replace.\n   * @param {string} markup Markup to render in place of the child node.\n   * @internal\n   */\n  dangerouslyReplaceNodeWithMarkup: function(oldChild, markup) {\n    (\"production\" !== \"development\" ? invariant(\n      ExecutionEnvironment.canUseDOM,\n      'dangerouslyReplaceNodeWithMarkup(...): Cannot render markup in a ' +\n      'worker thread. This is likely a bug in the framework. Please report ' +\n      'immediately.'\n    ) : invariant(ExecutionEnvironment.canUseDOM));\n    (\"production\" !== \"development\" ? invariant(markup, 'dangerouslyReplaceNodeWithMarkup(...): Missing markup.') : invariant(markup));\n    (\"production\" !== \"development\" ? invariant(\n      oldChild.tagName.toLowerCase() !== 'html',\n      'dangerouslyReplaceNodeWithMarkup(...): Cannot replace markup of the ' +\n      '<html> node. This is because browser quirks make this unreliable ' +\n      'and/or slow. If you want to render to the root you must use ' +\n      'server rendering. See renderComponentToString().'\n    ) : invariant(oldChild.tagName.toLowerCase() !== 'html'));\n\n    var newChild = createNodesFromMarkup(markup, emptyFunction)[0];\n    oldChild.parentNode.replaceChild(newChild, oldChild);\n  }\n\n};\n\nmodule.exports = Danger;\n\n},{\"./ExecutionEnvironment\":22,\"./createNodesFromMarkup\":113,\"./emptyFunction\":116,\"./getMarkupWrap\":126,\"./invariant\":134}],14:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule DefaultEventPluginOrder\n */\n\n\"use strict\";\n\n var keyOf = _dereq_(\"./keyOf\");\n\n/**\n * Module that is injectable into `EventPluginHub`, that specifies a\n * deterministic ordering of `EventPlugin`s. A convenient way to reason about\n * plugins, without having to package every one of them. This is better than\n * having plugins be ordered in the same order that they are injected because\n * that ordering would be influenced by the packaging order.\n * `ResponderEventPlugin` must occur before `SimpleEventPlugin` so that\n * preventing default on events is convenient in `SimpleEventPlugin` handlers.\n */\nvar DefaultEventPluginOrder = [\n  keyOf({ResponderEventPlugin: null}),\n  keyOf({SimpleEventPlugin: null}),\n  keyOf({TapEventPlugin: null}),\n  keyOf({EnterLeaveEventPlugin: null}),\n  keyOf({ChangeEventPlugin: null}),\n  keyOf({SelectEventPlugin: null}),\n  keyOf({CompositionEventPlugin: null}),\n  keyOf({BeforeInputEventPlugin: null}),\n  keyOf({AnalyticsEventPlugin: null}),\n  keyOf({MobileSafariClickEventPlugin: null})\n];\n\nmodule.exports = DefaultEventPluginOrder;\n\n},{\"./keyOf\":141}],15:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule EnterLeaveEventPlugin\n * @typechecks static-only\n */\n\n\"use strict\";\n\nvar EventConstants = _dereq_(\"./EventConstants\");\nvar EventPropagators = _dereq_(\"./EventPropagators\");\nvar SyntheticMouseEvent = _dereq_(\"./SyntheticMouseEvent\");\n\nvar ReactMount = _dereq_(\"./ReactMount\");\nvar keyOf = _dereq_(\"./keyOf\");\n\nvar topLevelTypes = EventConstants.topLevelTypes;\nvar getFirstReactDOM = ReactMount.getFirstReactDOM;\n\nvar eventTypes = {\n  mouseEnter: {\n    registrationName: keyOf({onMouseEnter: null}),\n    dependencies: [\n      topLevelTypes.topMouseOut,\n      topLevelTypes.topMouseOver\n    ]\n  },\n  mouseLeave: {\n    registrationName: keyOf({onMouseLeave: null}),\n    dependencies: [\n      topLevelTypes.topMouseOut,\n      topLevelTypes.topMouseOver\n    ]\n  }\n};\n\nvar extractedEvents = [null, null];\n\nvar EnterLeaveEventPlugin = {\n\n  eventTypes: eventTypes,\n\n  /**\n   * For almost every interaction we care about, there will be both a top-level\n   * `mouseover` and `mouseout` event that occurs. Only use `mouseout` so that\n   * we do not extract duplicate events. However, moving the mouse into the\n   * browser from outside will not fire a `mouseout` event. In this case, we use\n   * the `mouseover` top-level event.\n   *\n   * @param {string} topLevelType Record from `EventConstants`.\n   * @param {DOMEventTarget} topLevelTarget The listening component root node.\n   * @param {string} topLevelTargetID ID of `topLevelTarget`.\n   * @param {object} nativeEvent Native browser event.\n   * @return {*} An accumulation of synthetic events.\n   * @see {EventPluginHub.extractEvents}\n   */\n  extractEvents: function(\n      topLevelType,\n      topLevelTarget,\n      topLevelTargetID,\n      nativeEvent) {\n    if (topLevelType === topLevelTypes.topMouseOver &&\n        (nativeEvent.relatedTarget || nativeEvent.fromElement)) {\n      return null;\n    }\n    if (topLevelType !== topLevelTypes.topMouseOut &&\n        topLevelType !== topLevelTypes.topMouseOver) {\n      // Must not be a mouse in or mouse out - ignoring.\n      return null;\n    }\n\n    var win;\n    if (topLevelTarget.window === topLevelTarget) {\n      // `topLevelTarget` is probably a window object.\n      win = topLevelTarget;\n    } else {\n      // TODO: Figure out why `ownerDocument` is sometimes undefined in IE8.\n      var doc = topLevelTarget.ownerDocument;\n      if (doc) {\n        win = doc.defaultView || doc.parentWindow;\n      } else {\n        win = window;\n      }\n    }\n\n    var from, to;\n    if (topLevelType === topLevelTypes.topMouseOut) {\n      from = topLevelTarget;\n      to =\n        getFirstReactDOM(nativeEvent.relatedTarget || nativeEvent.toElement) ||\n        win;\n    } else {\n      from = win;\n      to = topLevelTarget;\n    }\n\n    if (from === to) {\n      // Nothing pertains to our managed components.\n      return null;\n    }\n\n    var fromID = from ? ReactMount.getID(from) : '';\n    var toID = to ? ReactMount.getID(to) : '';\n\n    var leave = SyntheticMouseEvent.getPooled(\n      eventTypes.mouseLeave,\n      fromID,\n      nativeEvent\n    );\n    leave.type = 'mouseleave';\n    leave.target = from;\n    leave.relatedTarget = to;\n\n    var enter = SyntheticMouseEvent.getPooled(\n      eventTypes.mouseEnter,\n      toID,\n      nativeEvent\n    );\n    enter.type = 'mouseenter';\n    enter.target = to;\n    enter.relatedTarget = from;\n\n    EventPropagators.accumulateEnterLeaveDispatches(leave, enter, fromID, toID);\n\n    extractedEvents[0] = leave;\n    extractedEvents[1] = enter;\n\n    return extractedEvents;\n  }\n\n};\n\nmodule.exports = EnterLeaveEventPlugin;\n\n},{\"./EventConstants\":16,\"./EventPropagators\":21,\"./ReactMount\":67,\"./SyntheticMouseEvent\":100,\"./keyOf\":141}],16:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule EventConstants\n */\n\n\"use strict\";\n\nvar keyMirror = _dereq_(\"./keyMirror\");\n\nvar PropagationPhases = keyMirror({bubbled: null, captured: null});\n\n/**\n * Types of raw signals from the browser caught at the top level.\n */\nvar topLevelTypes = keyMirror({\n  topBlur: null,\n  topChange: null,\n  topClick: null,\n  topCompositionEnd: null,\n  topCompositionStart: null,\n  topCompositionUpdate: null,\n  topContextMenu: null,\n  topCopy: null,\n  topCut: null,\n  topDoubleClick: null,\n  topDrag: null,\n  topDragEnd: null,\n  topDragEnter: null,\n  topDragExit: null,\n  topDragLeave: null,\n  topDragOver: null,\n  topDragStart: null,\n  topDrop: null,\n  topError: null,\n  topFocus: null,\n  topInput: null,\n  topKeyDown: null,\n  topKeyPress: null,\n  topKeyUp: null,\n  topLoad: null,\n  topMouseDown: null,\n  topMouseMove: null,\n  topMouseOut: null,\n  topMouseOver: null,\n  topMouseUp: null,\n  topPaste: null,\n  topReset: null,\n  topScroll: null,\n  topSelectionChange: null,\n  topSubmit: null,\n  topTextInput: null,\n  topTouchCancel: null,\n  topTouchEnd: null,\n  topTouchMove: null,\n  topTouchStart: null,\n  topWheel: null\n});\n\nvar EventConstants = {\n  topLevelTypes: topLevelTypes,\n  PropagationPhases: PropagationPhases\n};\n\nmodule.exports = EventConstants;\n\n},{\"./keyMirror\":140}],17:[function(_dereq_,module,exports){\n/**\n * @providesModule EventListener\n * @typechecks\n */\n\nvar emptyFunction = _dereq_(\"./emptyFunction\");\n\n/**\n * Upstream version of event listener. Does not take into account specific\n * nature of platform.\n */\nvar EventListener = {\n  /**\n   * Listen to DOM events during the bubble phase.\n   *\n   * @param {DOMEventTarget} target DOM element to register listener on.\n   * @param {string} eventType Event type, e.g. 'click' or 'mouseover'.\n   * @param {function} callback Callback function.\n   * @return {object} Object with a `remove` method.\n   */\n  listen: function(target, eventType, callback) {\n    if (target.addEventListener) {\n      target.addEventListener(eventType, callback, false);\n      return {\n        remove: function() {\n          target.removeEventListener(eventType, callback, false);\n        }\n      };\n    } else if (target.attachEvent) {\n      target.attachEvent('on' + eventType, callback);\n      return {\n        remove: function() {\n          target.detachEvent('on' + eventType, callback);\n        }\n      };\n    }\n  },\n\n  /**\n   * Listen to DOM events during the capture phase.\n   *\n   * @param {DOMEventTarget} target DOM element to register listener on.\n   * @param {string} eventType Event type, e.g. 'click' or 'mouseover'.\n   * @param {function} callback Callback function.\n   * @return {object} Object with a `remove` method.\n   */\n  capture: function(target, eventType, callback) {\n    if (!target.addEventListener) {\n      if (\"production\" !== \"development\") {\n        console.error(\n          'Attempted to listen to events during the capture phase on a ' +\n          'browser that does not support the capture phase. Your application ' +\n          'will not receive some events.'\n        );\n      }\n      return {\n        remove: emptyFunction\n      };\n    } else {\n      target.addEventListener(eventType, callback, true);\n      return {\n        remove: function() {\n          target.removeEventListener(eventType, callback, true);\n        }\n      };\n    }\n  },\n\n  registerDefault: function() {}\n};\n\nmodule.exports = EventListener;\n\n},{\"./emptyFunction\":116}],18:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule EventPluginHub\n */\n\n\"use strict\";\n\nvar EventPluginRegistry = _dereq_(\"./EventPluginRegistry\");\nvar EventPluginUtils = _dereq_(\"./EventPluginUtils\");\n\nvar accumulate = _dereq_(\"./accumulate\");\nvar forEachAccumulated = _dereq_(\"./forEachAccumulated\");\nvar invariant = _dereq_(\"./invariant\");\nvar isEventSupported = _dereq_(\"./isEventSupported\");\nvar monitorCodeUse = _dereq_(\"./monitorCodeUse\");\n\n/**\n * Internal store for event listeners\n */\nvar listenerBank = {};\n\n/**\n * Internal queue of events that have accumulated their dispatches and are\n * waiting to have their dispatches executed.\n */\nvar eventQueue = null;\n\n/**\n * Dispatches an event and releases it back into the pool, unless persistent.\n *\n * @param {?object} event Synthetic event to be dispatched.\n * @private\n */\nvar executeDispatchesAndRelease = function(event) {\n  if (event) {\n    var executeDispatch = EventPluginUtils.executeDispatch;\n    // Plugins can provide custom behavior when dispatching events.\n    var PluginModule = EventPluginRegistry.getPluginModuleForEvent(event);\n    if (PluginModule && PluginModule.executeDispatch) {\n      executeDispatch = PluginModule.executeDispatch;\n    }\n    EventPluginUtils.executeDispatchesInOrder(event, executeDispatch);\n\n    if (!event.isPersistent()) {\n      event.constructor.release(event);\n    }\n  }\n};\n\n/**\n * - `InstanceHandle`: [required] Module that performs logical traversals of DOM\n *   hierarchy given ids of the logical DOM elements involved.\n */\nvar InstanceHandle = null;\n\nfunction validateInstanceHandle() {\n  var invalid = !InstanceHandle||\n    !InstanceHandle.traverseTwoPhase ||\n    !InstanceHandle.traverseEnterLeave;\n  if (invalid) {\n    throw new Error('InstanceHandle not injected before use!');\n  }\n}\n\n/**\n * This is a unified interface for event plugins to be installed and configured.\n *\n * Event plugins can implement the following properties:\n *\n *   `extractEvents` {function(string, DOMEventTarget, string, object): *}\n *     Required. When a top-level event is fired, this method is expected to\n *     extract synthetic events that will in turn be queued and dispatched.\n *\n *   `eventTypes` {object}\n *     Optional, plugins that fire events must publish a mapping of registration\n *     names that are used to register listeners. Values of this mapping must\n *     be objects that contain `registrationName` or `phasedRegistrationNames`.\n *\n *   `executeDispatch` {function(object, function, string)}\n *     Optional, allows plugins to override how an event gets dispatched. By\n *     default, the listener is simply invoked.\n *\n * Each plugin that is injected into `EventsPluginHub` is immediately operable.\n *\n * @public\n */\nvar EventPluginHub = {\n\n  /**\n   * Methods for injecting dependencies.\n   */\n  injection: {\n\n    /**\n     * @param {object} InjectedMount\n     * @public\n     */\n    injectMount: EventPluginUtils.injection.injectMount,\n\n    /**\n     * @param {object} InjectedInstanceHandle\n     * @public\n     */\n    injectInstanceHandle: function(InjectedInstanceHandle) {\n      InstanceHandle = InjectedInstanceHandle;\n      if (\"production\" !== \"development\") {\n        validateInstanceHandle();\n      }\n    },\n\n    getInstanceHandle: function() {\n      if (\"production\" !== \"development\") {\n        validateInstanceHandle();\n      }\n      return InstanceHandle;\n    },\n\n    /**\n     * @param {array} InjectedEventPluginOrder\n     * @public\n     */\n    injectEventPluginOrder: EventPluginRegistry.injectEventPluginOrder,\n\n    /**\n     * @param {object} injectedNamesToPlugins Map from names to plugin modules.\n     */\n    injectEventPluginsByName: EventPluginRegistry.injectEventPluginsByName\n\n  },\n\n  eventNameDispatchConfigs: EventPluginRegistry.eventNameDispatchConfigs,\n\n  registrationNameModules: EventPluginRegistry.registrationNameModules,\n\n  /**\n   * Stores `listener` at `listenerBank[registrationName][id]`. Is idempotent.\n   *\n   * @param {string} id ID of the DOM element.\n   * @param {string} registrationName Name of listener (e.g. `onClick`).\n   * @param {?function} listener The callback to store.\n   */\n  putListener: function(id, registrationName, listener) {\n    (\"production\" !== \"development\" ? invariant(\n      !listener || typeof listener === 'function',\n      'Expected %s listener to be a function, instead got type %s',\n      registrationName, typeof listener\n    ) : invariant(!listener || typeof listener === 'function'));\n\n    if (\"production\" !== \"development\") {\n      // IE8 has no API for event capturing and the `onScroll` event doesn't\n      // bubble.\n      if (registrationName === 'onScroll' &&\n          !isEventSupported('scroll', true)) {\n        monitorCodeUse('react_no_scroll_event');\n        console.warn('This browser doesn\\'t support the `onScroll` event');\n      }\n    }\n    var bankForRegistrationName =\n      listenerBank[registrationName] || (listenerBank[registrationName] = {});\n    bankForRegistrationName[id] = listener;\n  },\n\n  /**\n   * @param {string} id ID of the DOM element.\n   * @param {string} registrationName Name of listener (e.g. `onClick`).\n   * @return {?function} The stored callback.\n   */\n  getListener: function(id, registrationName) {\n    var bankForRegistrationName = listenerBank[registrationName];\n    return bankForRegistrationName && bankForRegistrationName[id];\n  },\n\n  /**\n   * Deletes a listener from the registration bank.\n   *\n   * @param {string} id ID of the DOM element.\n   * @param {string} registrationName Name of listener (e.g. `onClick`).\n   */\n  deleteListener: function(id, registrationName) {\n    var bankForRegistrationName = listenerBank[registrationName];\n    if (bankForRegistrationName) {\n      delete bankForRegistrationName[id];\n    }\n  },\n\n  /**\n   * Deletes all listeners for the DOM element with the supplied ID.\n   *\n   * @param {string} id ID of the DOM element.\n   */\n  deleteAllListeners: function(id) {\n    for (var registrationName in listenerBank) {\n      delete listenerBank[registrationName][id];\n    }\n  },\n\n  /**\n   * Allows registered plugins an opportunity to extract events from top-level\n   * native browser events.\n   *\n   * @param {string} topLevelType Record from `EventConstants`.\n   * @param {DOMEventTarget} topLevelTarget The listening component root node.\n   * @param {string} topLevelTargetID ID of `topLevelTarget`.\n   * @param {object} nativeEvent Native browser event.\n   * @return {*} An accumulation of synthetic events.\n   * @internal\n   */\n  extractEvents: function(\n      topLevelType,\n      topLevelTarget,\n      topLevelTargetID,\n      nativeEvent) {\n    var events;\n    var plugins = EventPluginRegistry.plugins;\n    for (var i = 0, l = plugins.length; i < l; i++) {\n      // Not every plugin in the ordering may be loaded at runtime.\n      var possiblePlugin = plugins[i];\n      if (possiblePlugin) {\n        var extractedEvents = possiblePlugin.extractEvents(\n          topLevelType,\n          topLevelTarget,\n          topLevelTargetID,\n          nativeEvent\n        );\n        if (extractedEvents) {\n          events = accumulate(events, extractedEvents);\n        }\n      }\n    }\n    return events;\n  },\n\n  /**\n   * Enqueues a synthetic event that should be dispatched when\n   * `processEventQueue` is invoked.\n   *\n   * @param {*} events An accumulation of synthetic events.\n   * @internal\n   */\n  enqueueEvents: function(events) {\n    if (events) {\n      eventQueue = accumulate(eventQueue, events);\n    }\n  },\n\n  /**\n   * Dispatches all synthetic events on the event queue.\n   *\n   * @internal\n   */\n  processEventQueue: function() {\n    // Set `eventQueue` to null before processing it so that we can tell if more\n    // events get enqueued while processing.\n    var processingEventQueue = eventQueue;\n    eventQueue = null;\n    forEachAccumulated(processingEventQueue, executeDispatchesAndRelease);\n    (\"production\" !== \"development\" ? invariant(\n      !eventQueue,\n      'processEventQueue(): Additional events were enqueued while processing ' +\n      'an event queue. Support for this has not yet been implemented.'\n    ) : invariant(!eventQueue));\n  },\n\n  /**\n   * These are needed for tests only. Do not use!\n   */\n  __purge: function() {\n    listenerBank = {};\n  },\n\n  __getListenerBank: function() {\n    return listenerBank;\n  }\n\n};\n\nmodule.exports = EventPluginHub;\n\n},{\"./EventPluginRegistry\":19,\"./EventPluginUtils\":20,\"./accumulate\":106,\"./forEachAccumulated\":121,\"./invariant\":134,\"./isEventSupported\":135,\"./monitorCodeUse\":148}],19:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule EventPluginRegistry\n * @typechecks static-only\n */\n\n\"use strict\";\n\nvar invariant = _dereq_(\"./invariant\");\n\n/**\n * Injectable ordering of event plugins.\n */\nvar EventPluginOrder = null;\n\n/**\n * Injectable mapping from names to event plugin modules.\n */\nvar namesToPlugins = {};\n\n/**\n * Recomputes the plugin list using the injected plugins and plugin ordering.\n *\n * @private\n */\nfunction recomputePluginOrdering() {\n  if (!EventPluginOrder) {\n    // Wait until an `EventPluginOrder` is injected.\n    return;\n  }\n  for (var pluginName in namesToPlugins) {\n    var PluginModule = namesToPlugins[pluginName];\n    var pluginIndex = EventPluginOrder.indexOf(pluginName);\n    (\"production\" !== \"development\" ? invariant(\n      pluginIndex > -1,\n      'EventPluginRegistry: Cannot inject event plugins that do not exist in ' +\n      'the plugin ordering, `%s`.',\n      pluginName\n    ) : invariant(pluginIndex > -1));\n    if (EventPluginRegistry.plugins[pluginIndex]) {\n      continue;\n    }\n    (\"production\" !== \"development\" ? invariant(\n      PluginModule.extractEvents,\n      'EventPluginRegistry: Event plugins must implement an `extractEvents` ' +\n      'method, but `%s` does not.',\n      pluginName\n    ) : invariant(PluginModule.extractEvents));\n    EventPluginRegistry.plugins[pluginIndex] = PluginModule;\n    var publishedEvents = PluginModule.eventTypes;\n    for (var eventName in publishedEvents) {\n      (\"production\" !== \"development\" ? invariant(\n        publishEventForPlugin(\n          publishedEvents[eventName],\n          PluginModule,\n          eventName\n        ),\n        'EventPluginRegistry: Failed to publish event `%s` for plugin `%s`.',\n        eventName,\n        pluginName\n      ) : invariant(publishEventForPlugin(\n        publishedEvents[eventName],\n        PluginModule,\n        eventName\n      )));\n    }\n  }\n}\n\n/**\n * Publishes an event so that it can be dispatched by the supplied plugin.\n *\n * @param {object} dispatchConfig Dispatch configuration for the event.\n * @param {object} PluginModule Plugin publishing the event.\n * @return {boolean} True if the event was successfully published.\n * @private\n */\nfunction publishEventForPlugin(dispatchConfig, PluginModule, eventName) {\n  (\"production\" !== \"development\" ? invariant(\n    !EventPluginRegistry.eventNameDispatchConfigs.hasOwnProperty(eventName),\n    'EventPluginHub: More than one plugin attempted to publish the same ' +\n    'event name, `%s`.',\n    eventName\n  ) : invariant(!EventPluginRegistry.eventNameDispatchConfigs.hasOwnProperty(eventName)));\n  EventPluginRegistry.eventNameDispatchConfigs[eventName] = dispatchConfig;\n\n  var phasedRegistrationNames = dispatchConfig.phasedRegistrationNames;\n  if (phasedRegistrationNames) {\n    for (var phaseName in phasedRegistrationNames) {\n      if (phasedRegistrationNames.hasOwnProperty(phaseName)) {\n        var phasedRegistrationName = phasedRegistrationNames[phaseName];\n        publishRegistrationName(\n          phasedRegistrationName,\n          PluginModule,\n          eventName\n        );\n      }\n    }\n    return true;\n  } else if (dispatchConfig.registrationName) {\n    publishRegistrationName(\n      dispatchConfig.registrationName,\n      PluginModule,\n      eventName\n    );\n    return true;\n  }\n  return false;\n}\n\n/**\n * Publishes a registration name that is used to identify dispatched events and\n * can be used with `EventPluginHub.putListener` to register listeners.\n *\n * @param {string} registrationName Registration name to add.\n * @param {object} PluginModule Plugin publishing the event.\n * @private\n */\nfunction publishRegistrationName(registrationName, PluginModule, eventName) {\n  (\"production\" !== \"development\" ? invariant(\n    !EventPluginRegistry.registrationNameModules[registrationName],\n    'EventPluginHub: More than one plugin attempted to publish the same ' +\n    'registration name, `%s`.',\n    registrationName\n  ) : invariant(!EventPluginRegistry.registrationNameModules[registrationName]));\n  EventPluginRegistry.registrationNameModules[registrationName] = PluginModule;\n  EventPluginRegistry.registrationNameDependencies[registrationName] =\n    PluginModule.eventTypes[eventName].dependencies;\n}\n\n/**\n * Registers plugins so that they can extract and dispatch events.\n *\n * @see {EventPluginHub}\n */\nvar EventPluginRegistry = {\n\n  /**\n   * Ordered list of injected plugins.\n   */\n  plugins: [],\n\n  /**\n   * Mapping from event name to dispatch config\n   */\n  eventNameDispatchConfigs: {},\n\n  /**\n   * Mapping from registration name to plugin module\n   */\n  registrationNameModules: {},\n\n  /**\n   * Mapping from registration name to event name\n   */\n  registrationNameDependencies: {},\n\n  /**\n   * Injects an ordering of plugins (by plugin name). This allows the ordering\n   * to be decoupled from injection of the actual plugins so that ordering is\n   * always deterministic regardless of packaging, on-the-fly injection, etc.\n   *\n   * @param {array} InjectedEventPluginOrder\n   * @internal\n   * @see {EventPluginHub.injection.injectEventPluginOrder}\n   */\n  injectEventPluginOrder: function(InjectedEventPluginOrder) {\n    (\"production\" !== \"development\" ? invariant(\n      !EventPluginOrder,\n      'EventPluginRegistry: Cannot inject event plugin ordering more than ' +\n      'once. You are likely trying to load more than one copy of React.'\n    ) : invariant(!EventPluginOrder));\n    // Clone the ordering so it cannot be dynamically mutated.\n    EventPluginOrder = Array.prototype.slice.call(InjectedEventPluginOrder);\n    recomputePluginOrdering();\n  },\n\n  /**\n   * Injects plugins to be used by `EventPluginHub`. The plugin names must be\n   * in the ordering injected by `injectEventPluginOrder`.\n   *\n   * Plugins can be injected as part of page initialization or on-the-fly.\n   *\n   * @param {object} injectedNamesToPlugins Map from names to plugin modules.\n   * @internal\n   * @see {EventPluginHub.injection.injectEventPluginsByName}\n   */\n  injectEventPluginsByName: function(injectedNamesToPlugins) {\n    var isOrderingDirty = false;\n    for (var pluginName in injectedNamesToPlugins) {\n      if (!injectedNamesToPlugins.hasOwnProperty(pluginName)) {\n        continue;\n      }\n      var PluginModule = injectedNamesToPlugins[pluginName];\n      if (!namesToPlugins.hasOwnProperty(pluginName) ||\n          namesToPlugins[pluginName] !== PluginModule) {\n        (\"production\" !== \"development\" ? invariant(\n          !namesToPlugins[pluginName],\n          'EventPluginRegistry: Cannot inject two different event plugins ' +\n          'using the same name, `%s`.',\n          pluginName\n        ) : invariant(!namesToPlugins[pluginName]));\n        namesToPlugins[pluginName] = PluginModule;\n        isOrderingDirty = true;\n      }\n    }\n    if (isOrderingDirty) {\n      recomputePluginOrdering();\n    }\n  },\n\n  /**\n   * Looks up the plugin for the supplied event.\n   *\n   * @param {object} event A synthetic event.\n   * @return {?object} The plugin that created the supplied event.\n   * @internal\n   */\n  getPluginModuleForEvent: function(event) {\n    var dispatchConfig = event.dispatchConfig;\n    if (dispatchConfig.registrationName) {\n      return EventPluginRegistry.registrationNameModules[\n        dispatchConfig.registrationName\n      ] || null;\n    }\n    for (var phase in dispatchConfig.phasedRegistrationNames) {\n      if (!dispatchConfig.phasedRegistrationNames.hasOwnProperty(phase)) {\n        continue;\n      }\n      var PluginModule = EventPluginRegistry.registrationNameModules[\n        dispatchConfig.phasedRegistrationNames[phase]\n      ];\n      if (PluginModule) {\n        return PluginModule;\n      }\n    }\n    return null;\n  },\n\n  /**\n   * Exposed for unit testing.\n   * @private\n   */\n  _resetEventPlugins: function() {\n    EventPluginOrder = null;\n    for (var pluginName in namesToPlugins) {\n      if (namesToPlugins.hasOwnProperty(pluginName)) {\n        delete namesToPlugins[pluginName];\n      }\n    }\n    EventPluginRegistry.plugins.length = 0;\n\n    var eventNameDispatchConfigs = EventPluginRegistry.eventNameDispatchConfigs;\n    for (var eventName in eventNameDispatchConfigs) {\n      if (eventNameDispatchConfigs.hasOwnProperty(eventName)) {\n        delete eventNameDispatchConfigs[eventName];\n      }\n    }\n\n    var registrationNameModules = EventPluginRegistry.registrationNameModules;\n    for (var registrationName in registrationNameModules) {\n      if (registrationNameModules.hasOwnProperty(registrationName)) {\n        delete registrationNameModules[registrationName];\n      }\n    }\n  }\n\n};\n\nmodule.exports = EventPluginRegistry;\n\n},{\"./invariant\":134}],20:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule EventPluginUtils\n */\n\n\"use strict\";\n\nvar EventConstants = _dereq_(\"./EventConstants\");\n\nvar invariant = _dereq_(\"./invariant\");\n\n/**\n * Injected dependencies:\n */\n\n/**\n * - `Mount`: [required] Module that can convert between React dom IDs and\n *   actual node references.\n */\nvar injection = {\n  Mount: null,\n  injectMount: function(InjectedMount) {\n    injection.Mount = InjectedMount;\n    if (\"production\" !== \"development\") {\n      (\"production\" !== \"development\" ? invariant(\n        InjectedMount && InjectedMount.getNode,\n        'EventPluginUtils.injection.injectMount(...): Injected Mount module ' +\n        'is missing getNode.'\n      ) : invariant(InjectedMount && InjectedMount.getNode));\n    }\n  }\n};\n\nvar topLevelTypes = EventConstants.topLevelTypes;\n\nfunction isEndish(topLevelType) {\n  return topLevelType === topLevelTypes.topMouseUp ||\n         topLevelType === topLevelTypes.topTouchEnd ||\n         topLevelType === topLevelTypes.topTouchCancel;\n}\n\nfunction isMoveish(topLevelType) {\n  return topLevelType === topLevelTypes.topMouseMove ||\n         topLevelType === topLevelTypes.topTouchMove;\n}\nfunction isStartish(topLevelType) {\n  return topLevelType === topLevelTypes.topMouseDown ||\n         topLevelType === topLevelTypes.topTouchStart;\n}\n\n\nvar validateEventDispatches;\nif (\"production\" !== \"development\") {\n  validateEventDispatches = function(event) {\n    var dispatchListeners = event._dispatchListeners;\n    var dispatchIDs = event._dispatchIDs;\n\n    var listenersIsArr = Array.isArray(dispatchListeners);\n    var idsIsArr = Array.isArray(dispatchIDs);\n    var IDsLen = idsIsArr ? dispatchIDs.length : dispatchIDs ? 1 : 0;\n    var listenersLen = listenersIsArr ?\n      dispatchListeners.length :\n      dispatchListeners ? 1 : 0;\n\n    (\"production\" !== \"development\" ? invariant(\n      idsIsArr === listenersIsArr && IDsLen === listenersLen,\n      'EventPluginUtils: Invalid `event`.'\n    ) : invariant(idsIsArr === listenersIsArr && IDsLen === listenersLen));\n  };\n}\n\n/**\n * Invokes `cb(event, listener, id)`. Avoids using call if no scope is\n * provided. The `(listener,id)` pair effectively forms the \"dispatch\" but are\n * kept separate to conserve memory.\n */\nfunction forEachEventDispatch(event, cb) {\n  var dispatchListeners = event._dispatchListeners;\n  var dispatchIDs = event._dispatchIDs;\n  if (\"production\" !== \"development\") {\n    validateEventDispatches(event);\n  }\n  if (Array.isArray(dispatchListeners)) {\n    for (var i = 0; i < dispatchListeners.length; i++) {\n      if (event.isPropagationStopped()) {\n        break;\n      }\n      // Listeners and IDs are two parallel arrays that are always in sync.\n      cb(event, dispatchListeners[i], dispatchIDs[i]);\n    }\n  } else if (dispatchListeners) {\n    cb(event, dispatchListeners, dispatchIDs);\n  }\n}\n\n/**\n * Default implementation of PluginModule.executeDispatch().\n * @param {SyntheticEvent} SyntheticEvent to handle\n * @param {function} Application-level callback\n * @param {string} domID DOM id to pass to the callback.\n */\nfunction executeDispatch(event, listener, domID) {\n  event.currentTarget = injection.Mount.getNode(domID);\n  var returnValue = listener(event, domID);\n  event.currentTarget = null;\n  return returnValue;\n}\n\n/**\n * Standard/simple iteration through an event's collected dispatches.\n */\nfunction executeDispatchesInOrder(event, executeDispatch) {\n  forEachEventDispatch(event, executeDispatch);\n  event._dispatchListeners = null;\n  event._dispatchIDs = null;\n}\n\n/**\n * Standard/simple iteration through an event's collected dispatches, but stops\n * at the first dispatch execution returning true, and returns that id.\n *\n * @return id of the first dispatch execution who's listener returns true, or\n * null if no listener returned true.\n */\nfunction executeDispatchesInOrderStopAtTrueImpl(event) {\n  var dispatchListeners = event._dispatchListeners;\n  var dispatchIDs = event._dispatchIDs;\n  if (\"production\" !== \"development\") {\n    validateEventDispatches(event);\n  }\n  if (Array.isArray(dispatchListeners)) {\n    for (var i = 0; i < dispatchListeners.length; i++) {\n      if (event.isPropagationStopped()) {\n        break;\n      }\n      // Listeners and IDs are two parallel arrays that are always in sync.\n      if (dispatchListeners[i](event, dispatchIDs[i])) {\n        return dispatchIDs[i];\n      }\n    }\n  } else if (dispatchListeners) {\n    if (dispatchListeners(event, dispatchIDs)) {\n      return dispatchIDs;\n    }\n  }\n  return null;\n}\n\n/**\n * @see executeDispatchesInOrderStopAtTrueImpl\n */\nfunction executeDispatchesInOrderStopAtTrue(event) {\n  var ret = executeDispatchesInOrderStopAtTrueImpl(event);\n  event._dispatchIDs = null;\n  event._dispatchListeners = null;\n  return ret;\n}\n\n/**\n * Execution of a \"direct\" dispatch - there must be at most one dispatch\n * accumulated on the event or it is considered an error. It doesn't really make\n * sense for an event with multiple dispatches (bubbled) to keep track of the\n * return values at each dispatch execution, but it does tend to make sense when\n * dealing with \"direct\" dispatches.\n *\n * @return The return value of executing the single dispatch.\n */\nfunction executeDirectDispatch(event) {\n  if (\"production\" !== \"development\") {\n    validateEventDispatches(event);\n  }\n  var dispatchListener = event._dispatchListeners;\n  var dispatchID = event._dispatchIDs;\n  (\"production\" !== \"development\" ? invariant(\n    !Array.isArray(dispatchListener),\n    'executeDirectDispatch(...): Invalid `event`.'\n  ) : invariant(!Array.isArray(dispatchListener)));\n  var res = dispatchListener ?\n    dispatchListener(event, dispatchID) :\n    null;\n  event._dispatchListeners = null;\n  event._dispatchIDs = null;\n  return res;\n}\n\n/**\n * @param {SyntheticEvent} event\n * @return {bool} True iff number of dispatches accumulated is greater than 0.\n */\nfunction hasDispatches(event) {\n  return !!event._dispatchListeners;\n}\n\n/**\n * General utilities that are useful in creating custom Event Plugins.\n */\nvar EventPluginUtils = {\n  isEndish: isEndish,\n  isMoveish: isMoveish,\n  isStartish: isStartish,\n\n  executeDirectDispatch: executeDirectDispatch,\n  executeDispatch: executeDispatch,\n  executeDispatchesInOrder: executeDispatchesInOrder,\n  executeDispatchesInOrderStopAtTrue: executeDispatchesInOrderStopAtTrue,\n  hasDispatches: hasDispatches,\n  injection: injection,\n  useTouchEvents: false\n};\n\nmodule.exports = EventPluginUtils;\n\n},{\"./EventConstants\":16,\"./invariant\":134}],21:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule EventPropagators\n */\n\n\"use strict\";\n\nvar EventConstants = _dereq_(\"./EventConstants\");\nvar EventPluginHub = _dereq_(\"./EventPluginHub\");\n\nvar accumulate = _dereq_(\"./accumulate\");\nvar forEachAccumulated = _dereq_(\"./forEachAccumulated\");\n\nvar PropagationPhases = EventConstants.PropagationPhases;\nvar getListener = EventPluginHub.getListener;\n\n/**\n * Some event types have a notion of different registration names for different\n * \"phases\" of propagation. This finds listeners by a given phase.\n */\nfunction listenerAtPhase(id, event, propagationPhase) {\n  var registrationName =\n    event.dispatchConfig.phasedRegistrationNames[propagationPhase];\n  return getListener(id, registrationName);\n}\n\n/**\n * Tags a `SyntheticEvent` with dispatched listeners. Creating this function\n * here, allows us to not have to bind or create functions for each event.\n * Mutating the event's members allows us to not have to create a wrapping\n * \"dispatch\" object that pairs the event with the listener.\n */\nfunction accumulateDirectionalDispatches(domID, upwards, event) {\n  if (\"production\" !== \"development\") {\n    if (!domID) {\n      throw new Error('Dispatching id must not be null');\n    }\n  }\n  var phase = upwards ? PropagationPhases.bubbled : PropagationPhases.captured;\n  var listener = listenerAtPhase(domID, event, phase);\n  if (listener) {\n    event._dispatchListeners = accumulate(event._dispatchListeners, listener);\n    event._dispatchIDs = accumulate(event._dispatchIDs, domID);\n  }\n}\n\n/**\n * Collect dispatches (must be entirely collected before dispatching - see unit\n * tests). Lazily allocate the array to conserve memory.  We must loop through\n * each event and perform the traversal for each one. We can not perform a\n * single traversal for the entire collection of events because each event may\n * have a different target.\n */\nfunction accumulateTwoPhaseDispatchesSingle(event) {\n  if (event && event.dispatchConfig.phasedRegistrationNames) {\n    EventPluginHub.injection.getInstanceHandle().traverseTwoPhase(\n      event.dispatchMarker,\n      accumulateDirectionalDispatches,\n      event\n    );\n  }\n}\n\n\n/**\n * Accumulates without regard to direction, does not look for phased\n * registration names. Same as `accumulateDirectDispatchesSingle` but without\n * requiring that the `dispatchMarker` be the same as the dispatched ID.\n */\nfunction accumulateDispatches(id, ignoredDirection, event) {\n  if (event && event.dispatchConfig.registrationName) {\n    var registrationName = event.dispatchConfig.registrationName;\n    var listener = getListener(id, registrationName);\n    if (listener) {\n      event._dispatchListeners = accumulate(event._dispatchListeners, listener);\n      event._dispatchIDs = accumulate(event._dispatchIDs, id);\n    }\n  }\n}\n\n/**\n * Accumulates dispatches on an `SyntheticEvent`, but only for the\n * `dispatchMarker`.\n * @param {SyntheticEvent} event\n */\nfunction accumulateDirectDispatchesSingle(event) {\n  if (event && event.dispatchConfig.registrationName) {\n    accumulateDispatches(event.dispatchMarker, null, event);\n  }\n}\n\nfunction accumulateTwoPhaseDispatches(events) {\n  forEachAccumulated(events, accumulateTwoPhaseDispatchesSingle);\n}\n\nfunction accumulateEnterLeaveDispatches(leave, enter, fromID, toID) {\n  EventPluginHub.injection.getInstanceHandle().traverseEnterLeave(\n    fromID,\n    toID,\n    accumulateDispatches,\n    leave,\n    enter\n  );\n}\n\n\nfunction accumulateDirectDispatches(events) {\n  forEachAccumulated(events, accumulateDirectDispatchesSingle);\n}\n\n\n\n/**\n * A small set of propagation patterns, each of which will accept a small amount\n * of information, and generate a set of \"dispatch ready event objects\" - which\n * are sets of events that have already been annotated with a set of dispatched\n * listener functions/ids. The API is designed this way to discourage these\n * propagation strategies from actually executing the dispatches, since we\n * always want to collect the entire set of dispatches before executing event a\n * single one.\n *\n * @constructor EventPropagators\n */\nvar EventPropagators = {\n  accumulateTwoPhaseDispatches: accumulateTwoPhaseDispatches,\n  accumulateDirectDispatches: accumulateDirectDispatches,\n  accumulateEnterLeaveDispatches: accumulateEnterLeaveDispatches\n};\n\nmodule.exports = EventPropagators;\n\n},{\"./EventConstants\":16,\"./EventPluginHub\":18,\"./accumulate\":106,\"./forEachAccumulated\":121}],22:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule ExecutionEnvironment\n */\n\n/*jslint evil: true */\n\n\"use strict\";\n\nvar canUseDOM = !!(\n  typeof window !== 'undefined' &&\n  window.document &&\n  window.document.createElement\n);\n\n/**\n * Simple, lightweight module assisting with the detection and context of\n * Worker. Helps avoid circular dependencies and allows code to reason about\n * whether or not they are in a Worker, even if they never include the main\n * `ReactWorker` dependency.\n */\nvar ExecutionEnvironment = {\n\n  canUseDOM: canUseDOM,\n\n  canUseWorkers: typeof Worker !== 'undefined',\n\n  canUseEventListeners:\n    canUseDOM && !!(window.addEventListener || window.attachEvent),\n\n  canUseViewport: canUseDOM && !!window.screen,\n\n  isInWorker: !canUseDOM // For now, this is true - might change in the future.\n\n};\n\nmodule.exports = ExecutionEnvironment;\n\n},{}],23:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule HTMLDOMPropertyConfig\n */\n\n/*jslint bitwise: true*/\n\n\"use strict\";\n\nvar DOMProperty = _dereq_(\"./DOMProperty\");\nvar ExecutionEnvironment = _dereq_(\"./ExecutionEnvironment\");\n\nvar MUST_USE_ATTRIBUTE = DOMProperty.injection.MUST_USE_ATTRIBUTE;\nvar MUST_USE_PROPERTY = DOMProperty.injection.MUST_USE_PROPERTY;\nvar HAS_BOOLEAN_VALUE = DOMProperty.injection.HAS_BOOLEAN_VALUE;\nvar HAS_SIDE_EFFECTS = DOMProperty.injection.HAS_SIDE_EFFECTS;\nvar HAS_NUMERIC_VALUE = DOMProperty.injection.HAS_NUMERIC_VALUE;\nvar HAS_POSITIVE_NUMERIC_VALUE =\n  DOMProperty.injection.HAS_POSITIVE_NUMERIC_VALUE;\nvar HAS_OVERLOADED_BOOLEAN_VALUE =\n  DOMProperty.injection.HAS_OVERLOADED_BOOLEAN_VALUE;\n\nvar hasSVG;\nif (ExecutionEnvironment.canUseDOM) {\n  var implementation = document.implementation;\n  hasSVG = (\n    implementation &&\n    implementation.hasFeature &&\n    implementation.hasFeature(\n      'http://www.w3.org/TR/SVG11/feature#BasicStructure',\n      '1.1'\n    )\n  );\n}\n\n\nvar HTMLDOMPropertyConfig = {\n  isCustomAttribute: RegExp.prototype.test.bind(\n    /^(data|aria)-[a-z_][a-z\\d_.\\-]*$/\n  ),\n  Properties: {\n    /**\n     * Standard Properties\n     */\n    accept: null,\n    accessKey: null,\n    action: null,\n    allowFullScreen: MUST_USE_ATTRIBUTE | HAS_BOOLEAN_VALUE,\n    allowTransparency: MUST_USE_ATTRIBUTE,\n    alt: null,\n    async: HAS_BOOLEAN_VALUE,\n    autoComplete: null,\n    // autoFocus is polyfilled/normalized by AutoFocusMixin\n    // autoFocus: HAS_BOOLEAN_VALUE,\n    autoPlay: HAS_BOOLEAN_VALUE,\n    cellPadding: null,\n    cellSpacing: null,\n    charSet: MUST_USE_ATTRIBUTE,\n    checked: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE,\n    // To set className on SVG elements, it's necessary to use .setAttribute;\n    // this works on HTML elements too in all browsers except IE8. Conveniently,\n    // IE8 doesn't support SVG and so we can simply use the attribute in\n    // browsers that support SVG and the property in browsers that don't,\n    // regardless of whether the element is HTML or SVG.\n    className: hasSVG ? MUST_USE_ATTRIBUTE : MUST_USE_PROPERTY,\n    cols: MUST_USE_ATTRIBUTE | HAS_POSITIVE_NUMERIC_VALUE,\n    colSpan: null,\n    content: null,\n    contentEditable: null,\n    contextMenu: MUST_USE_ATTRIBUTE,\n    controls: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE,\n    coords: null,\n    crossOrigin: null,\n    data: null, // For `<object />` acts as `src`.\n    dateTime: MUST_USE_ATTRIBUTE,\n    defer: HAS_BOOLEAN_VALUE,\n    dir: null,\n    disabled: MUST_USE_ATTRIBUTE | HAS_BOOLEAN_VALUE,\n    download: HAS_OVERLOADED_BOOLEAN_VALUE,\n    draggable: null,\n    encType: null,\n    form: MUST_USE_ATTRIBUTE,\n    formNoValidate: HAS_BOOLEAN_VALUE,\n    frameBorder: MUST_USE_ATTRIBUTE,\n    height: MUST_USE_ATTRIBUTE,\n    hidden: MUST_USE_ATTRIBUTE | HAS_BOOLEAN_VALUE,\n    href: null,\n    hrefLang: null,\n    htmlFor: null,\n    httpEquiv: null,\n    icon: null,\n    id: MUST_USE_PROPERTY,\n    label: null,\n    lang: null,\n    list: null,\n    loop: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE,\n    max: null,\n    maxLength: MUST_USE_ATTRIBUTE,\n    media: MUST_USE_ATTRIBUTE,\n    mediaGroup: null,\n    method: null,\n    min: null,\n    multiple: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE,\n    muted: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE,\n    name: null,\n    noValidate: HAS_BOOLEAN_VALUE,\n    open: null,\n    pattern: null,\n    placeholder: null,\n    poster: null,\n    preload: null,\n    radioGroup: null,\n    readOnly: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE,\n    rel: null,\n    required: HAS_BOOLEAN_VALUE,\n    role: MUST_USE_ATTRIBUTE,\n    rows: MUST_USE_ATTRIBUTE | HAS_POSITIVE_NUMERIC_VALUE,\n    rowSpan: null,\n    sandbox: null,\n    scope: null,\n    scrollLeft: MUST_USE_PROPERTY,\n    scrolling: null,\n    scrollTop: MUST_USE_PROPERTY,\n    seamless: MUST_USE_ATTRIBUTE | HAS_BOOLEAN_VALUE,\n    selected: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE,\n    shape: null,\n    size: MUST_USE_ATTRIBUTE | HAS_POSITIVE_NUMERIC_VALUE,\n    sizes: MUST_USE_ATTRIBUTE,\n    span: HAS_POSITIVE_NUMERIC_VALUE,\n    spellCheck: null,\n    src: null,\n    srcDoc: MUST_USE_PROPERTY,\n    srcSet: MUST_USE_ATTRIBUTE,\n    start: HAS_NUMERIC_VALUE,\n    step: null,\n    style: null,\n    tabIndex: null,\n    target: null,\n    title: null,\n    type: null,\n    useMap: null,\n    value: MUST_USE_PROPERTY | HAS_SIDE_EFFECTS,\n    width: MUST_USE_ATTRIBUTE,\n    wmode: MUST_USE_ATTRIBUTE,\n\n    /**\n     * Non-standard Properties\n     */\n    autoCapitalize: null, // Supported in Mobile Safari for keyboard hints\n    autoCorrect: null, // Supported in Mobile Safari for keyboard hints\n    itemProp: MUST_USE_ATTRIBUTE, // Microdata: http://schema.org/docs/gs.html\n    itemScope: MUST_USE_ATTRIBUTE | HAS_BOOLEAN_VALUE, // Microdata: http://schema.org/docs/gs.html\n    itemType: MUST_USE_ATTRIBUTE, // Microdata: http://schema.org/docs/gs.html\n    property: null // Supports OG in meta tags\n  },\n  DOMAttributeNames: {\n    className: 'class',\n    htmlFor: 'for',\n    httpEquiv: 'http-equiv'\n  },\n  DOMPropertyNames: {\n    autoCapitalize: 'autocapitalize',\n    autoComplete: 'autocomplete',\n    autoCorrect: 'autocorrect',\n    autoFocus: 'autofocus',\n    autoPlay: 'autoplay',\n    encType: 'enctype',\n    hrefLang: 'hreflang',\n    radioGroup: 'radiogroup',\n    spellCheck: 'spellcheck',\n    srcDoc: 'srcdoc',\n    srcSet: 'srcset'\n  }\n};\n\nmodule.exports = HTMLDOMPropertyConfig;\n\n},{\"./DOMProperty\":11,\"./ExecutionEnvironment\":22}],24:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule LinkedStateMixin\n * @typechecks static-only\n */\n\n\"use strict\";\n\nvar ReactLink = _dereq_(\"./ReactLink\");\nvar ReactStateSetters = _dereq_(\"./ReactStateSetters\");\n\n/**\n * A simple mixin around ReactLink.forState().\n */\nvar LinkedStateMixin = {\n  /**\n   * Create a ReactLink that's linked to part of this component's state. The\n   * ReactLink will have the current value of this.state[key] and will call\n   * setState() when a change is requested.\n   *\n   * @param {string} key state key to update. Note: you may want to use keyOf()\n   * if you're using Google Closure Compiler advanced mode.\n   * @return {ReactLink} ReactLink instance linking to the state.\n   */\n  linkState: function(key) {\n    return new ReactLink(\n      this.state[key],\n      ReactStateSetters.createStateKeySetter(this, key)\n    );\n  }\n};\n\nmodule.exports = LinkedStateMixin;\n\n},{\"./ReactLink\":65,\"./ReactStateSetters\":81}],25:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule LinkedValueUtils\n * @typechecks static-only\n */\n\n\"use strict\";\n\nvar ReactPropTypes = _dereq_(\"./ReactPropTypes\");\n\nvar invariant = _dereq_(\"./invariant\");\n\nvar hasReadOnlyValue = {\n  'button': true,\n  'checkbox': true,\n  'image': true,\n  'hidden': true,\n  'radio': true,\n  'reset': true,\n  'submit': true\n};\n\nfunction _assertSingleLink(input) {\n  (\"production\" !== \"development\" ? invariant(\n    input.props.checkedLink == null || input.props.valueLink == null,\n    'Cannot provide a checkedLink and a valueLink. If you want to use ' +\n    'checkedLink, you probably don\\'t want to use valueLink and vice versa.'\n  ) : invariant(input.props.checkedLink == null || input.props.valueLink == null));\n}\nfunction _assertValueLink(input) {\n  _assertSingleLink(input);\n  (\"production\" !== \"development\" ? invariant(\n    input.props.value == null && input.props.onChange == null,\n    'Cannot provide a valueLink and a value or onChange event. If you want ' +\n    'to use value or onChange, you probably don\\'t want to use valueLink.'\n  ) : invariant(input.props.value == null && input.props.onChange == null));\n}\n\nfunction _assertCheckedLink(input) {\n  _assertSingleLink(input);\n  (\"production\" !== \"development\" ? invariant(\n    input.props.checked == null && input.props.onChange == null,\n    'Cannot provide a checkedLink and a checked property or onChange event. ' +\n    'If you want to use checked or onChange, you probably don\\'t want to ' +\n    'use checkedLink'\n  ) : invariant(input.props.checked == null && input.props.onChange == null));\n}\n\n/**\n * @param {SyntheticEvent} e change event to handle\n */\nfunction _handleLinkedValueChange(e) {\n  /*jshint validthis:true */\n  this.props.valueLink.requestChange(e.target.value);\n}\n\n/**\n  * @param {SyntheticEvent} e change event to handle\n  */\nfunction _handleLinkedCheckChange(e) {\n  /*jshint validthis:true */\n  this.props.checkedLink.requestChange(e.target.checked);\n}\n\n/**\n * Provide a linked `value` attribute for controlled forms. You should not use\n * this outside of the ReactDOM controlled form components.\n */\nvar LinkedValueUtils = {\n  Mixin: {\n    propTypes: {\n      value: function(props, propName, componentName) {\n        if (!props[propName] ||\n            hasReadOnlyValue[props.type] ||\n            props.onChange ||\n            props.readOnly ||\n            props.disabled) {\n          return;\n        }\n        return new Error(\n          'You provided a `value` prop to a form field without an ' +\n          '`onChange` handler. This will render a read-only field. If ' +\n          'the field should be mutable use `defaultValue`. Otherwise, ' +\n          'set either `onChange` or `readOnly`.'\n        );\n      },\n      checked: function(props, propName, componentName) {\n        if (!props[propName] ||\n            props.onChange ||\n            props.readOnly ||\n            props.disabled) {\n          return;\n        }\n        return new Error(\n          'You provided a `checked` prop to a form field without an ' +\n          '`onChange` handler. This will render a read-only field. If ' +\n          'the field should be mutable use `defaultChecked`. Otherwise, ' +\n          'set either `onChange` or `readOnly`.'\n        );\n      },\n      onChange: ReactPropTypes.func\n    }\n  },\n\n  /**\n   * @param {ReactComponent} input Form component\n   * @return {*} current value of the input either from value prop or link.\n   */\n  getValue: function(input) {\n    if (input.props.valueLink) {\n      _assertValueLink(input);\n      return input.props.valueLink.value;\n    }\n    return input.props.value;\n  },\n\n  /**\n   * @param {ReactComponent} input Form component\n   * @return {*} current checked status of the input either from checked prop\n   *             or link.\n   */\n  getChecked: function(input) {\n    if (input.props.checkedLink) {\n      _assertCheckedLink(input);\n      return input.props.checkedLink.value;\n    }\n    return input.props.checked;\n  },\n\n  /**\n   * @param {ReactComponent} input Form component\n   * @return {function} change callback either from onChange prop or link.\n   */\n  getOnChange: function(input) {\n    if (input.props.valueLink) {\n      _assertValueLink(input);\n      return _handleLinkedValueChange;\n    } else if (input.props.checkedLink) {\n      _assertCheckedLink(input);\n      return _handleLinkedCheckChange;\n    }\n    return input.props.onChange;\n  }\n};\n\nmodule.exports = LinkedValueUtils;\n\n},{\"./ReactPropTypes\":75,\"./invariant\":134}],26:[function(_dereq_,module,exports){\n/**\n * Copyright 2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule LocalEventTrapMixin\n */\n\n\"use strict\";\n\nvar ReactBrowserEventEmitter = _dereq_(\"./ReactBrowserEventEmitter\");\n\nvar accumulate = _dereq_(\"./accumulate\");\nvar forEachAccumulated = _dereq_(\"./forEachAccumulated\");\nvar invariant = _dereq_(\"./invariant\");\n\nfunction remove(event) {\n  event.remove();\n}\n\nvar LocalEventTrapMixin = {\n  trapBubbledEvent:function(topLevelType, handlerBaseName) {\n    (\"production\" !== \"development\" ? invariant(this.isMounted(), 'Must be mounted to trap events') : invariant(this.isMounted()));\n    var listener = ReactBrowserEventEmitter.trapBubbledEvent(\n      topLevelType,\n      handlerBaseName,\n      this.getDOMNode()\n    );\n    this._localEventListeners = accumulate(this._localEventListeners, listener);\n  },\n\n  // trapCapturedEvent would look nearly identical. We don't implement that\n  // method because it isn't currently needed.\n\n  componentWillUnmount:function() {\n    if (this._localEventListeners) {\n      forEachAccumulated(this._localEventListeners, remove);\n    }\n  }\n};\n\nmodule.exports = LocalEventTrapMixin;\n\n},{\"./ReactBrowserEventEmitter\":31,\"./accumulate\":106,\"./forEachAccumulated\":121,\"./invariant\":134}],27:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule MobileSafariClickEventPlugin\n * @typechecks static-only\n */\n\n\"use strict\";\n\nvar EventConstants = _dereq_(\"./EventConstants\");\n\nvar emptyFunction = _dereq_(\"./emptyFunction\");\n\nvar topLevelTypes = EventConstants.topLevelTypes;\n\n/**\n * Mobile Safari does not fire properly bubble click events on non-interactive\n * elements, which means delegated click listeners do not fire. The workaround\n * for this bug involves attaching an empty click listener on the target node.\n *\n * This particular plugin works around the bug by attaching an empty click\n * listener on `touchstart` (which does fire on every element).\n */\nvar MobileSafariClickEventPlugin = {\n\n  eventTypes: null,\n\n  /**\n   * @param {string} topLevelType Record from `EventConstants`.\n   * @param {DOMEventTarget} topLevelTarget The listening component root node.\n   * @param {string} topLevelTargetID ID of `topLevelTarget`.\n   * @param {object} nativeEvent Native browser event.\n   * @return {*} An accumulation of synthetic events.\n   * @see {EventPluginHub.extractEvents}\n   */\n  extractEvents: function(\n      topLevelType,\n      topLevelTarget,\n      topLevelTargetID,\n      nativeEvent) {\n    if (topLevelType === topLevelTypes.topTouchStart) {\n      var target = nativeEvent.target;\n      if (target && !target.onclick) {\n        target.onclick = emptyFunction;\n      }\n    }\n  }\n\n};\n\nmodule.exports = MobileSafariClickEventPlugin;\n\n},{\"./EventConstants\":16,\"./emptyFunction\":116}],28:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule PooledClass\n */\n\n\"use strict\";\n\nvar invariant = _dereq_(\"./invariant\");\n\n/**\n * Static poolers. Several custom versions for each potential number of\n * arguments. A completely generic pooler is easy to implement, but would\n * require accessing the `arguments` object. In each of these, `this` refers to\n * the Class itself, not an instance. If any others are needed, simply add them\n * here, or in their own files.\n */\nvar oneArgumentPooler = function(copyFieldsFrom) {\n  var Klass = this;\n  if (Klass.instancePool.length) {\n    var instance = Klass.instancePool.pop();\n    Klass.call(instance, copyFieldsFrom);\n    return instance;\n  } else {\n    return new Klass(copyFieldsFrom);\n  }\n};\n\nvar twoArgumentPooler = function(a1, a2) {\n  var Klass = this;\n  if (Klass.instancePool.length) {\n    var instance = Klass.instancePool.pop();\n    Klass.call(instance, a1, a2);\n    return instance;\n  } else {\n    return new Klass(a1, a2);\n  }\n};\n\nvar threeArgumentPooler = function(a1, a2, a3) {\n  var Klass = this;\n  if (Klass.instancePool.length) {\n    var instance = Klass.instancePool.pop();\n    Klass.call(instance, a1, a2, a3);\n    return instance;\n  } else {\n    return new Klass(a1, a2, a3);\n  }\n};\n\nvar fiveArgumentPooler = function(a1, a2, a3, a4, a5) {\n  var Klass = this;\n  if (Klass.instancePool.length) {\n    var instance = Klass.instancePool.pop();\n    Klass.call(instance, a1, a2, a3, a4, a5);\n    return instance;\n  } else {\n    return new Klass(a1, a2, a3, a4, a5);\n  }\n};\n\nvar standardReleaser = function(instance) {\n  var Klass = this;\n  (\"production\" !== \"development\" ? invariant(\n    instance instanceof Klass,\n    'Trying to release an instance into a pool of a different type.'\n  ) : invariant(instance instanceof Klass));\n  if (instance.destructor) {\n    instance.destructor();\n  }\n  if (Klass.instancePool.length < Klass.poolSize) {\n    Klass.instancePool.push(instance);\n  }\n};\n\nvar DEFAULT_POOL_SIZE = 10;\nvar DEFAULT_POOLER = oneArgumentPooler;\n\n/**\n * Augments `CopyConstructor` to be a poolable class, augmenting only the class\n * itself (statically) not adding any prototypical fields. Any CopyConstructor\n * you give this may have a `poolSize` property, and will look for a\n * prototypical `destructor` on instances (optional).\n *\n * @param {Function} CopyConstructor Constructor that can be used to reset.\n * @param {Function} pooler Customizable pooler.\n */\nvar addPoolingTo = function(CopyConstructor, pooler) {\n  var NewKlass = CopyConstructor;\n  NewKlass.instancePool = [];\n  NewKlass.getPooled = pooler || DEFAULT_POOLER;\n  if (!NewKlass.poolSize) {\n    NewKlass.poolSize = DEFAULT_POOL_SIZE;\n  }\n  NewKlass.release = standardReleaser;\n  return NewKlass;\n};\n\nvar PooledClass = {\n  addPoolingTo: addPoolingTo,\n  oneArgumentPooler: oneArgumentPooler,\n  twoArgumentPooler: twoArgumentPooler,\n  threeArgumentPooler: threeArgumentPooler,\n  fiveArgumentPooler: fiveArgumentPooler\n};\n\nmodule.exports = PooledClass;\n\n},{\"./invariant\":134}],29:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule React\n */\n\n\"use strict\";\n\nvar DOMPropertyOperations = _dereq_(\"./DOMPropertyOperations\");\nvar EventPluginUtils = _dereq_(\"./EventPluginUtils\");\nvar ReactChildren = _dereq_(\"./ReactChildren\");\nvar ReactComponent = _dereq_(\"./ReactComponent\");\nvar ReactCompositeComponent = _dereq_(\"./ReactCompositeComponent\");\nvar ReactContext = _dereq_(\"./ReactContext\");\nvar ReactCurrentOwner = _dereq_(\"./ReactCurrentOwner\");\nvar ReactDescriptor = _dereq_(\"./ReactDescriptor\");\nvar ReactDOM = _dereq_(\"./ReactDOM\");\nvar ReactDOMComponent = _dereq_(\"./ReactDOMComponent\");\nvar ReactDefaultInjection = _dereq_(\"./ReactDefaultInjection\");\nvar ReactInstanceHandles = _dereq_(\"./ReactInstanceHandles\");\nvar ReactMount = _dereq_(\"./ReactMount\");\nvar ReactMultiChild = _dereq_(\"./ReactMultiChild\");\nvar ReactPerf = _dereq_(\"./ReactPerf\");\nvar ReactPropTypes = _dereq_(\"./ReactPropTypes\");\nvar ReactServerRendering = _dereq_(\"./ReactServerRendering\");\nvar ReactTextComponent = _dereq_(\"./ReactTextComponent\");\n\nvar onlyChild = _dereq_(\"./onlyChild\");\nvar warning = _dereq_(\"./warning\");\n\nReactDefaultInjection.inject();\n\n// Specifying arguments isn't necessary since we just use apply anyway, but it\n// makes it clear for those actually consuming this API.\nfunction createDescriptor(type, props, children) {\n  var args = Array.prototype.slice.call(arguments, 1);\n  return type.apply(null, args);\n}\n\nif (\"production\" !== \"development\") {\n  var _warnedForDeprecation = false;\n}\n\nvar React = {\n  Children: {\n    map: ReactChildren.map,\n    forEach: ReactChildren.forEach,\n    count: ReactChildren.count,\n    only: onlyChild\n  },\n  DOM: ReactDOM,\n  PropTypes: ReactPropTypes,\n  initializeTouchEvents: function(shouldUseTouch) {\n    EventPluginUtils.useTouchEvents = shouldUseTouch;\n  },\n  createClass: ReactCompositeComponent.createClass,\n  createDescriptor: function() {\n    if (\"production\" !== \"development\") {\n      (\"production\" !== \"development\" ? warning(\n        _warnedForDeprecation,\n        'React.createDescriptor is deprecated and will be removed in the ' +\n        'next version of React. Use React.createElement instead.'\n      ) : null);\n      _warnedForDeprecation = true;\n    }\n    return createDescriptor.apply(this, arguments);\n  },\n  createElement: createDescriptor,\n  constructAndRenderComponent: ReactMount.constructAndRenderComponent,\n  constructAndRenderComponentByID: ReactMount.constructAndRenderComponentByID,\n  renderComponent: ReactPerf.measure(\n    'React',\n    'renderComponent',\n    ReactMount.renderComponent\n  ),\n  renderComponentToString: ReactServerRendering.renderComponentToString,\n  renderComponentToStaticMarkup:\n    ReactServerRendering.renderComponentToStaticMarkup,\n  unmountComponentAtNode: ReactMount.unmountComponentAtNode,\n  isValidClass: ReactDescriptor.isValidFactory,\n  isValidComponent: ReactDescriptor.isValidDescriptor,\n  withContext: ReactContext.withContext,\n  __internals: {\n    Component: ReactComponent,\n    CurrentOwner: ReactCurrentOwner,\n    DOMComponent: ReactDOMComponent,\n    DOMPropertyOperations: DOMPropertyOperations,\n    InstanceHandles: ReactInstanceHandles,\n    Mount: ReactMount,\n    MultiChild: ReactMultiChild,\n    TextComponent: ReactTextComponent\n  }\n};\n\nif (\"production\" !== \"development\") {\n  var ExecutionEnvironment = _dereq_(\"./ExecutionEnvironment\");\n  if (ExecutionEnvironment.canUseDOM &&\n      window.top === window.self &&\n      navigator.userAgent.indexOf('Chrome') > -1) {\n    console.debug(\n      'Download the React DevTools for a better development experience: ' +\n      'http://fb.me/react-devtools'\n    );\n\n    var expectedFeatures = [\n      // shims\n      Array.isArray,\n      Array.prototype.every,\n      Array.prototype.forEach,\n      Array.prototype.indexOf,\n      Array.prototype.map,\n      Date.now,\n      Function.prototype.bind,\n      Object.keys,\n      String.prototype.split,\n      String.prototype.trim,\n\n      // shams\n      Object.create,\n      Object.freeze\n    ];\n\n    for (var i in expectedFeatures) {\n      if (!expectedFeatures[i]) {\n        console.error(\n          'One or more ES5 shim/shams expected by React are not available: ' +\n          'http://fb.me/react-warning-polyfills'\n        );\n        break;\n      }\n    }\n  }\n}\n\n// Version exists only in the open-source version of React, not in Facebook's\n// internal version.\nReact.version = '0.11.2';\n\nmodule.exports = React;\n\n},{\"./DOMPropertyOperations\":12,\"./EventPluginUtils\":20,\"./ExecutionEnvironment\":22,\"./ReactChildren\":34,\"./ReactComponent\":35,\"./ReactCompositeComponent\":38,\"./ReactContext\":39,\"./ReactCurrentOwner\":40,\"./ReactDOM\":41,\"./ReactDOMComponent\":43,\"./ReactDefaultInjection\":53,\"./ReactDescriptor\":56,\"./ReactInstanceHandles\":64,\"./ReactMount\":67,\"./ReactMultiChild\":68,\"./ReactPerf\":71,\"./ReactPropTypes\":75,\"./ReactServerRendering\":79,\"./ReactTextComponent\":83,\"./onlyChild\":149,\"./warning\":158}],30:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule ReactBrowserComponentMixin\n */\n\n\"use strict\";\n\nvar ReactEmptyComponent = _dereq_(\"./ReactEmptyComponent\");\nvar ReactMount = _dereq_(\"./ReactMount\");\n\nvar invariant = _dereq_(\"./invariant\");\n\nvar ReactBrowserComponentMixin = {\n  /**\n   * Returns the DOM node rendered by this component.\n   *\n   * @return {DOMElement} The root node of this component.\n   * @final\n   * @protected\n   */\n  getDOMNode: function() {\n    (\"production\" !== \"development\" ? invariant(\n      this.isMounted(),\n      'getDOMNode(): A component must be mounted to have a DOM node.'\n    ) : invariant(this.isMounted()));\n    if (ReactEmptyComponent.isNullComponentID(this._rootNodeID)) {\n      return null;\n    }\n    return ReactMount.getNode(this._rootNodeID);\n  }\n};\n\nmodule.exports = ReactBrowserComponentMixin;\n\n},{\"./ReactEmptyComponent\":58,\"./ReactMount\":67,\"./invariant\":134}],31:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule ReactBrowserEventEmitter\n * @typechecks static-only\n */\n\n\"use strict\";\n\nvar EventConstants = _dereq_(\"./EventConstants\");\nvar EventPluginHub = _dereq_(\"./EventPluginHub\");\nvar EventPluginRegistry = _dereq_(\"./EventPluginRegistry\");\nvar ReactEventEmitterMixin = _dereq_(\"./ReactEventEmitterMixin\");\nvar ViewportMetrics = _dereq_(\"./ViewportMetrics\");\n\nvar isEventSupported = _dereq_(\"./isEventSupported\");\nvar merge = _dereq_(\"./merge\");\n\n/**\n * Summary of `ReactBrowserEventEmitter` event handling:\n *\n *  - Top-level delegation is used to trap most native browser events. This\n *    may only occur in the main thread and is the responsibility of\n *    ReactEventListener, which is injected and can therefore support pluggable\n *    event sources. This is the only work that occurs in the main thread.\n *\n *  - We normalize and de-duplicate events to account for browser quirks. This\n *    may be done in the worker thread.\n *\n *  - Forward these native events (with the associated top-level type used to\n *    trap it) to `EventPluginHub`, which in turn will ask plugins if they want\n *    to extract any synthetic events.\n *\n *  - The `EventPluginHub` will then process each event by annotating them with\n *    \"dispatches\", a sequence of listeners and IDs that care about that event.\n *\n *  - The `EventPluginHub` then dispatches the events.\n *\n * Overview of React and the event system:\n *\n * +------------+    .\n * |    DOM     |    .\n * +------------+    .\n *       |           .\n *       v           .\n * +------------+    .\n * | ReactEvent |    .\n * |  Listener  |    .\n * +------------+    .                         +-----------+\n *       |           .               +--------+|SimpleEvent|\n *       |           .               |         |Plugin     |\n * +-----|------+    .               v         +-----------+\n * |     |      |    .    +--------------+                    +------------+\n * |     +-----------.--->|EventPluginHub|                    |    Event   |\n * |            |    .    |              |     +-----------+  | Propagators|\n * | ReactEvent |    .    |              |     |TapEvent   |  |------------|\n * |  Emitter   |    .    |              |<---+|Plugin     |  |other plugin|\n * |            |    .    |              |     +-----------+  |  utilities |\n * |     +-----------.--->|              |                    +------------+\n * |     |      |    .    +--------------+\n * +-----|------+    .                ^        +-----------+\n *       |           .                |        |Enter/Leave|\n *       +           .                +-------+|Plugin     |\n * +-------------+   .                         +-----------+\n * | application |   .\n * |-------------|   .\n * |             |   .\n * |             |   .\n * +-------------+   .\n *                   .\n *    React Core     .  General Purpose Event Plugin System\n */\n\nvar alreadyListeningTo = {};\nvar isMonitoringScrollValue = false;\nvar reactTopListenersCounter = 0;\n\n// For events like 'submit' which don't consistently bubble (which we trap at a\n// lower node than `document`), binding at `document` would cause duplicate\n// events so we don't include them here\nvar topEventMapping = {\n  topBlur: 'blur',\n  topChange: 'change',\n  topClick: 'click',\n  topCompositionEnd: 'compositionend',\n  topCompositionStart: 'compositionstart',\n  topCompositionUpdate: 'compositionupdate',\n  topContextMenu: 'contextmenu',\n  topCopy: 'copy',\n  topCut: 'cut',\n  topDoubleClick: 'dblclick',\n  topDrag: 'drag',\n  topDragEnd: 'dragend',\n  topDragEnter: 'dragenter',\n  topDragExit: 'dragexit',\n  topDragLeave: 'dragleave',\n  topDragOver: 'dragover',\n  topDragStart: 'dragstart',\n  topDrop: 'drop',\n  topFocus: 'focus',\n  topInput: 'input',\n  topKeyDown: 'keydown',\n  topKeyPress: 'keypress',\n  topKeyUp: 'keyup',\n  topMouseDown: 'mousedown',\n  topMouseMove: 'mousemove',\n  topMouseOut: 'mouseout',\n  topMouseOver: 'mouseover',\n  topMouseUp: 'mouseup',\n  topPaste: 'paste',\n  topScroll: 'scroll',\n  topSelectionChange: 'selectionchange',\n  topTextInput: 'textInput',\n  topTouchCancel: 'touchcancel',\n  topTouchEnd: 'touchend',\n  topTouchMove: 'touchmove',\n  topTouchStart: 'touchstart',\n  topWheel: 'wheel'\n};\n\n/**\n * To ensure no conflicts with other potential React instances on the page\n */\nvar topListenersIDKey = \"_reactListenersID\" + String(Math.random()).slice(2);\n\nfunction getListeningForDocument(mountAt) {\n  // In IE8, `mountAt` is a host object and doesn't have `hasOwnProperty`\n  // directly.\n  if (!Object.prototype.hasOwnProperty.call(mountAt, topListenersIDKey)) {\n    mountAt[topListenersIDKey] = reactTopListenersCounter++;\n    alreadyListeningTo[mountAt[topListenersIDKey]] = {};\n  }\n  return alreadyListeningTo[mountAt[topListenersIDKey]];\n}\n\n/**\n * `ReactBrowserEventEmitter` is used to attach top-level event listeners. For\n * example:\n *\n *   ReactBrowserEventEmitter.putListener('myID', 'onClick', myFunction);\n *\n * This would allocate a \"registration\" of `('onClick', myFunction)` on 'myID'.\n *\n * @internal\n */\nvar ReactBrowserEventEmitter = merge(ReactEventEmitterMixin, {\n\n  /**\n   * Injectable event backend\n   */\n  ReactEventListener: null,\n\n  injection: {\n    /**\n     * @param {object} ReactEventListener\n     */\n    injectReactEventListener: function(ReactEventListener) {\n      ReactEventListener.setHandleTopLevel(\n        ReactBrowserEventEmitter.handleTopLevel\n      );\n      ReactBrowserEventEmitter.ReactEventListener = ReactEventListener;\n    }\n  },\n\n  /**\n   * Sets whether or not any created callbacks should be enabled.\n   *\n   * @param {boolean} enabled True if callbacks should be enabled.\n   */\n  setEnabled: function(enabled) {\n    if (ReactBrowserEventEmitter.ReactEventListener) {\n      ReactBrowserEventEmitter.ReactEventListener.setEnabled(enabled);\n    }\n  },\n\n  /**\n   * @return {boolean} True if callbacks are enabled.\n   */\n  isEnabled: function() {\n    return !!(\n      ReactBrowserEventEmitter.ReactEventListener &&\n      ReactBrowserEventEmitter.ReactEventListener.isEnabled()\n    );\n  },\n\n  /**\n   * We listen for bubbled touch events on the document object.\n   *\n   * Firefox v8.01 (and possibly others) exhibited strange behavior when\n   * mounting `onmousemove` events at some node that was not the document\n   * element. The symptoms were that if your mouse is not moving over something\n   * contained within that mount point (for example on the background) the\n   * top-level listeners for `onmousemove` won't be called. However, if you\n   * register the `mousemove` on the document object, then it will of course\n   * catch all `mousemove`s. This along with iOS quirks, justifies restricting\n   * top-level listeners to the document object only, at least for these\n   * movement types of events and possibly all events.\n   *\n   * @see http://www.quirksmode.org/blog/archives/2010/09/click_event_del.html\n   *\n   * Also, `keyup`/`keypress`/`keydown` do not bubble to the window on IE, but\n   * they bubble to document.\n   *\n   * @param {string} registrationName Name of listener (e.g. `onClick`).\n   * @param {object} contentDocumentHandle Document which owns the container\n   */\n  listenTo: function(registrationName, contentDocumentHandle) {\n    var mountAt = contentDocumentHandle;\n    var isListening = getListeningForDocument(mountAt);\n    var dependencies = EventPluginRegistry.\n      registrationNameDependencies[registrationName];\n\n    var topLevelTypes = EventConstants.topLevelTypes;\n    for (var i = 0, l = dependencies.length; i < l; i++) {\n      var dependency = dependencies[i];\n      if (!(\n            isListening.hasOwnProperty(dependency) &&\n            isListening[dependency]\n          )) {\n        if (dependency === topLevelTypes.topWheel) {\n          if (isEventSupported('wheel')) {\n            ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(\n              topLevelTypes.topWheel,\n              'wheel',\n              mountAt\n            );\n          } else if (isEventSupported('mousewheel')) {\n            ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(\n              topLevelTypes.topWheel,\n              'mousewheel',\n              mountAt\n            );\n          } else {\n            // Firefox needs to capture a different mouse scroll event.\n            // @see http://www.quirksmode.org/dom/events/tests/scroll.html\n            ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(\n              topLevelTypes.topWheel,\n              'DOMMouseScroll',\n              mountAt\n            );\n          }\n        } else if (dependency === topLevelTypes.topScroll) {\n\n          if (isEventSupported('scroll', true)) {\n            ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent(\n              topLevelTypes.topScroll,\n              'scroll',\n              mountAt\n            );\n          } else {\n            ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(\n              topLevelTypes.topScroll,\n              'scroll',\n              ReactBrowserEventEmitter.ReactEventListener.WINDOW_HANDLE\n            );\n          }\n        } else if (dependency === topLevelTypes.topFocus ||\n            dependency === topLevelTypes.topBlur) {\n\n          if (isEventSupported('focus', true)) {\n            ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent(\n              topLevelTypes.topFocus,\n              'focus',\n              mountAt\n            );\n            ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent(\n              topLevelTypes.topBlur,\n              'blur',\n              mountAt\n            );\n          } else if (isEventSupported('focusin')) {\n            // IE has `focusin` and `focusout` events which bubble.\n            // @see http://www.quirksmode.org/blog/archives/2008/04/delegating_the.html\n            ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(\n              topLevelTypes.topFocus,\n              'focusin',\n              mountAt\n            );\n            ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(\n              topLevelTypes.topBlur,\n              'focusout',\n              mountAt\n            );\n          }\n\n          // to make sure blur and focus event listeners are only attached once\n          isListening[topLevelTypes.topBlur] = true;\n          isListening[topLevelTypes.topFocus] = true;\n        } else if (topEventMapping.hasOwnProperty(dependency)) {\n          ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(\n            dependency,\n            topEventMapping[dependency],\n            mountAt\n          );\n        }\n\n        isListening[dependency] = true;\n      }\n    }\n  },\n\n  trapBubbledEvent: function(topLevelType, handlerBaseName, handle) {\n    return ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(\n      topLevelType,\n      handlerBaseName,\n      handle\n    );\n  },\n\n  trapCapturedEvent: function(topLevelType, handlerBaseName, handle) {\n    return ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent(\n      topLevelType,\n      handlerBaseName,\n      handle\n    );\n  },\n\n  /**\n   * Listens to window scroll and resize events. We cache scroll values so that\n   * application code can access them without triggering reflows.\n   *\n   * NOTE: Scroll events do not bubble.\n   *\n   * @see http://www.quirksmode.org/dom/events/scroll.html\n   */\n  ensureScrollValueMonitoring: function(){\n    if (!isMonitoringScrollValue) {\n      var refresh = ViewportMetrics.refreshScrollValues;\n      ReactBrowserEventEmitter.ReactEventListener.monitorScrollValue(refresh);\n      isMonitoringScrollValue = true;\n    }\n  },\n\n  eventNameDispatchConfigs: EventPluginHub.eventNameDispatchConfigs,\n\n  registrationNameModules: EventPluginHub.registrationNameModules,\n\n  putListener: EventPluginHub.putListener,\n\n  getListener: EventPluginHub.getListener,\n\n  deleteListener: EventPluginHub.deleteListener,\n\n  deleteAllListeners: EventPluginHub.deleteAllListeners\n\n});\n\nmodule.exports = ReactBrowserEventEmitter;\n\n},{\"./EventConstants\":16,\"./EventPluginHub\":18,\"./EventPluginRegistry\":19,\"./ReactEventEmitterMixin\":60,\"./ViewportMetrics\":105,\"./isEventSupported\":135,\"./merge\":144}],32:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @typechecks\n * @providesModule ReactCSSTransitionGroup\n */\n\n\"use strict\";\n\nvar React = _dereq_(\"./React\");\n\nvar ReactTransitionGroup = _dereq_(\"./ReactTransitionGroup\");\nvar ReactCSSTransitionGroupChild = _dereq_(\"./ReactCSSTransitionGroupChild\");\n\nvar ReactCSSTransitionGroup = React.createClass({\n  displayName: 'ReactCSSTransitionGroup',\n\n  propTypes: {\n    transitionName: React.PropTypes.string.isRequired,\n    transitionEnter: React.PropTypes.bool,\n    transitionLeave: React.PropTypes.bool\n  },\n\n  getDefaultProps: function() {\n    return {\n      transitionEnter: true,\n      transitionLeave: true\n    };\n  },\n\n  _wrapChild: function(child) {\n    // We need to provide this childFactory so that\n    // ReactCSSTransitionGroupChild can receive updates to name, enter, and\n    // leave while it is leaving.\n    return ReactCSSTransitionGroupChild(\n      {\n        name: this.props.transitionName,\n        enter: this.props.transitionEnter,\n        leave: this.props.transitionLeave\n      },\n      child\n    );\n  },\n\n  render: function() {\n    return this.transferPropsTo(\n      ReactTransitionGroup(\n        {childFactory: this._wrapChild},\n        this.props.children\n      )\n    );\n  }\n});\n\nmodule.exports = ReactCSSTransitionGroup;\n\n},{\"./React\":29,\"./ReactCSSTransitionGroupChild\":33,\"./ReactTransitionGroup\":86}],33:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @typechecks\n * @providesModule ReactCSSTransitionGroupChild\n */\n\n\"use strict\";\n\nvar React = _dereq_(\"./React\");\n\nvar CSSCore = _dereq_(\"./CSSCore\");\nvar ReactTransitionEvents = _dereq_(\"./ReactTransitionEvents\");\n\nvar onlyChild = _dereq_(\"./onlyChild\");\n\n// We don't remove the element from the DOM until we receive an animationend or\n// transitionend event. If the user screws up and forgets to add an animation\n// their node will be stuck in the DOM forever, so we detect if an animation\n// does not start and if it doesn't, we just call the end listener immediately.\nvar TICK = 17;\nvar NO_EVENT_TIMEOUT = 5000;\n\nvar noEventListener = null;\n\n\nif (\"production\" !== \"development\") {\n  noEventListener = function() {\n    console.warn(\n      'transition(): tried to perform an animation without ' +\n      'an animationend or transitionend event after timeout (' +\n      NO_EVENT_TIMEOUT + 'ms). You should either disable this ' +\n      'transition in JS or add a CSS animation/transition.'\n    );\n  };\n}\n\nvar ReactCSSTransitionGroupChild = React.createClass({\n  displayName: 'ReactCSSTransitionGroupChild',\n\n  transition: function(animationType, finishCallback) {\n    var node = this.getDOMNode();\n    var className = this.props.name + '-' + animationType;\n    var activeClassName = className + '-active';\n    var noEventTimeout = null;\n\n    var endListener = function() {\n      if (\"production\" !== \"development\") {\n        clearTimeout(noEventTimeout);\n      }\n\n      CSSCore.removeClass(node, className);\n      CSSCore.removeClass(node, activeClassName);\n\n      ReactTransitionEvents.removeEndEventListener(node, endListener);\n\n      // Usually this optional callback is used for informing an owner of\n      // a leave animation and telling it to remove the child.\n      finishCallback && finishCallback();\n    };\n\n    ReactTransitionEvents.addEndEventListener(node, endListener);\n\n    CSSCore.addClass(node, className);\n\n    // Need to do this to actually trigger a transition.\n    this.queueClass(activeClassName);\n\n    if (\"production\" !== \"development\") {\n      noEventTimeout = setTimeout(noEventListener, NO_EVENT_TIMEOUT);\n    }\n  },\n\n  queueClass: function(className) {\n    this.classNameQueue.push(className);\n\n    if (!this.timeout) {\n      this.timeout = setTimeout(this.flushClassNameQueue, TICK);\n    }\n  },\n\n  flushClassNameQueue: function() {\n    if (this.isMounted()) {\n      this.classNameQueue.forEach(\n        CSSCore.addClass.bind(CSSCore, this.getDOMNode())\n      );\n    }\n    this.classNameQueue.length = 0;\n    this.timeout = null;\n  },\n\n  componentWillMount: function() {\n    this.classNameQueue = [];\n  },\n\n  componentWillUnmount: function() {\n    if (this.timeout) {\n      clearTimeout(this.timeout);\n    }\n  },\n\n  componentWillEnter: function(done) {\n    if (this.props.enter) {\n      this.transition('enter', done);\n    } else {\n      done();\n    }\n  },\n\n  componentWillLeave: function(done) {\n    if (this.props.leave) {\n      this.transition('leave', done);\n    } else {\n      done();\n    }\n  },\n\n  render: function() {\n    return onlyChild(this.props.children);\n  }\n});\n\nmodule.exports = ReactCSSTransitionGroupChild;\n\n},{\"./CSSCore\":3,\"./React\":29,\"./ReactTransitionEvents\":85,\"./onlyChild\":149}],34:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule ReactChildren\n */\n\n\"use strict\";\n\nvar PooledClass = _dereq_(\"./PooledClass\");\n\nvar traverseAllChildren = _dereq_(\"./traverseAllChildren\");\nvar warning = _dereq_(\"./warning\");\n\nvar twoArgumentPooler = PooledClass.twoArgumentPooler;\nvar threeArgumentPooler = PooledClass.threeArgumentPooler;\n\n/**\n * PooledClass representing the bookkeeping associated with performing a child\n * traversal. Allows avoiding binding callbacks.\n *\n * @constructor ForEachBookKeeping\n * @param {!function} forEachFunction Function to perform traversal with.\n * @param {?*} forEachContext Context to perform context with.\n */\nfunction ForEachBookKeeping(forEachFunction, forEachContext) {\n  this.forEachFunction = forEachFunction;\n  this.forEachContext = forEachContext;\n}\nPooledClass.addPoolingTo(ForEachBookKeeping, twoArgumentPooler);\n\nfunction forEachSingleChild(traverseContext, child, name, i) {\n  var forEachBookKeeping = traverseContext;\n  forEachBookKeeping.forEachFunction.call(\n    forEachBookKeeping.forEachContext, child, i);\n}\n\n/**\n * Iterates through children that are typically specified as `props.children`.\n *\n * The provided forEachFunc(child, index) will be called for each\n * leaf child.\n *\n * @param {?*} children Children tree container.\n * @param {function(*, int)} forEachFunc.\n * @param {*} forEachContext Context for forEachContext.\n */\nfunction forEachChildren(children, forEachFunc, forEachContext) {\n  if (children == null) {\n    return children;\n  }\n\n  var traverseContext =\n    ForEachBookKeeping.getPooled(forEachFunc, forEachContext);\n  traverseAllChildren(children, forEachSingleChild, traverseContext);\n  ForEachBookKeeping.release(traverseContext);\n}\n\n/**\n * PooledClass representing the bookkeeping associated with performing a child\n * mapping. Allows avoiding binding callbacks.\n *\n * @constructor MapBookKeeping\n * @param {!*} mapResult Object containing the ordered map of results.\n * @param {!function} mapFunction Function to perform mapping with.\n * @param {?*} mapContext Context to perform mapping with.\n */\nfunction MapBookKeeping(mapResult, mapFunction, mapContext) {\n  this.mapResult = mapResult;\n  this.mapFunction = mapFunction;\n  this.mapContext = mapContext;\n}\nPooledClass.addPoolingTo(MapBookKeeping, threeArgumentPooler);\n\nfunction mapSingleChildIntoContext(traverseContext, child, name, i) {\n  var mapBookKeeping = traverseContext;\n  var mapResult = mapBookKeeping.mapResult;\n\n  var keyUnique = !mapResult.hasOwnProperty(name);\n  (\"production\" !== \"development\" ? warning(\n    keyUnique,\n    'ReactChildren.map(...): Encountered two children with the same key, ' +\n    '`%s`. Child keys must be unique; when two children share a key, only ' +\n    'the first child will be used.',\n    name\n  ) : null);\n\n  if (keyUnique) {\n    var mappedChild =\n      mapBookKeeping.mapFunction.call(mapBookKeeping.mapContext, child, i);\n    mapResult[name] = mappedChild;\n  }\n}\n\n/**\n * Maps children that are typically specified as `props.children`.\n *\n * The provided mapFunction(child, key, index) will be called for each\n * leaf child.\n *\n * TODO: This may likely break any calls to `ReactChildren.map` that were\n * previously relying on the fact that we guarded against null children.\n *\n * @param {?*} children Children tree container.\n * @param {function(*, int)} mapFunction.\n * @param {*} mapContext Context for mapFunction.\n * @return {object} Object containing the ordered map of results.\n */\nfunction mapChildren(children, func, context) {\n  if (children == null) {\n    return children;\n  }\n\n  var mapResult = {};\n  var traverseContext = MapBookKeeping.getPooled(mapResult, func, context);\n  traverseAllChildren(children, mapSingleChildIntoContext, traverseContext);\n  MapBookKeeping.release(traverseContext);\n  return mapResult;\n}\n\nfunction forEachSingleChildDummy(traverseContext, child, name, i) {\n  return null;\n}\n\n/**\n * Count the number of children that are typically specified as\n * `props.children`.\n *\n * @param {?*} children Children tree container.\n * @return {number} The number of children.\n */\nfunction countChildren(children, context) {\n  return traverseAllChildren(children, forEachSingleChildDummy, null);\n}\n\nvar ReactChildren = {\n  forEach: forEachChildren,\n  map: mapChildren,\n  count: countChildren\n};\n\nmodule.exports = ReactChildren;\n\n},{\"./PooledClass\":28,\"./traverseAllChildren\":156,\"./warning\":158}],35:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule ReactComponent\n */\n\n\"use strict\";\n\nvar ReactDescriptor = _dereq_(\"./ReactDescriptor\");\nvar ReactOwner = _dereq_(\"./ReactOwner\");\nvar ReactUpdates = _dereq_(\"./ReactUpdates\");\n\nvar invariant = _dereq_(\"./invariant\");\nvar keyMirror = _dereq_(\"./keyMirror\");\nvar merge = _dereq_(\"./merge\");\n\n/**\n * Every React component is in one of these life cycles.\n */\nvar ComponentLifeCycle = keyMirror({\n  /**\n   * Mounted components have a DOM node representation and are capable of\n   * receiving new props.\n   */\n  MOUNTED: null,\n  /**\n   * Unmounted components are inactive and cannot receive new props.\n   */\n  UNMOUNTED: null\n});\n\nvar injected = false;\n\n/**\n * Optionally injectable environment dependent cleanup hook. (server vs.\n * browser etc). Example: A browser system caches DOM nodes based on component\n * ID and must remove that cache entry when this instance is unmounted.\n *\n * @private\n */\nvar unmountIDFromEnvironment = null;\n\n/**\n * The \"image\" of a component tree, is the platform specific (typically\n * serialized) data that represents a tree of lower level UI building blocks.\n * On the web, this \"image\" is HTML markup which describes a construction of\n * low level `div` and `span` nodes. Other platforms may have different\n * encoding of this \"image\". This must be injected.\n *\n * @private\n */\nvar mountImageIntoNode = null;\n\n/**\n * Components are the basic units of composition in React.\n *\n * Every component accepts a set of keyed input parameters known as \"props\" that\n * are initialized by the constructor. Once a component is mounted, the props\n * can be mutated using `setProps` or `replaceProps`.\n *\n * Every component is capable of the following operations:\n *\n *   `mountComponent`\n *     Initializes the component, renders markup, and registers event listeners.\n *\n *   `receiveComponent`\n *     Updates the rendered DOM nodes to match the given component.\n *\n *   `unmountComponent`\n *     Releases any resources allocated by this component.\n *\n * Components can also be \"owned\" by other components. Being owned by another\n * component means being constructed by that component. This is different from\n * being the child of a component, which means having a DOM representation that\n * is a child of the DOM representation of that component.\n *\n * @class ReactComponent\n */\nvar ReactComponent = {\n\n  injection: {\n    injectEnvironment: function(ReactComponentEnvironment) {\n      (\"production\" !== \"development\" ? invariant(\n        !injected,\n        'ReactComponent: injectEnvironment() can only be called once.'\n      ) : invariant(!injected));\n      mountImageIntoNode = ReactComponentEnvironment.mountImageIntoNode;\n      unmountIDFromEnvironment =\n        ReactComponentEnvironment.unmountIDFromEnvironment;\n      ReactComponent.BackendIDOperations =\n        ReactComponentEnvironment.BackendIDOperations;\n      injected = true;\n    }\n  },\n\n  /**\n   * @internal\n   */\n  LifeCycle: ComponentLifeCycle,\n\n  /**\n   * Injected module that provides ability to mutate individual properties.\n   * Injected into the base class because many different subclasses need access\n   * to this.\n   *\n   * @internal\n   */\n  BackendIDOperations: null,\n\n  /**\n   * Base functionality for every ReactComponent constructor. Mixed into the\n   * `ReactComponent` prototype, but exposed statically for easy access.\n   *\n   * @lends {ReactComponent.prototype}\n   */\n  Mixin: {\n\n    /**\n     * Checks whether or not this component is mounted.\n     *\n     * @return {boolean} True if mounted, false otherwise.\n     * @final\n     * @protected\n     */\n    isMounted: function() {\n      return this._lifeCycleState === ComponentLifeCycle.MOUNTED;\n    },\n\n    /**\n     * Sets a subset of the props.\n     *\n     * @param {object} partialProps Subset of the next props.\n     * @param {?function} callback Called after props are updated.\n     * @final\n     * @public\n     */\n    setProps: function(partialProps, callback) {\n      // Merge with the pending descriptor if it exists, otherwise with existing\n      // descriptor props.\n      var descriptor = this._pendingDescriptor || this._descriptor;\n      this.replaceProps(\n        merge(descriptor.props, partialProps),\n        callback\n      );\n    },\n\n    /**\n     * Replaces all of the props.\n     *\n     * @param {object} props New props.\n     * @param {?function} callback Called after props are updated.\n     * @final\n     * @public\n     */\n    replaceProps: function(props, callback) {\n      (\"production\" !== \"development\" ? invariant(\n        this.isMounted(),\n        'replaceProps(...): Can only update a mounted component.'\n      ) : invariant(this.isMounted()));\n      (\"production\" !== \"development\" ? invariant(\n        this._mountDepth === 0,\n        'replaceProps(...): You called `setProps` or `replaceProps` on a ' +\n        'component with a parent. This is an anti-pattern since props will ' +\n        'get reactively updated when rendered. Instead, change the owner\\'s ' +\n        '`render` method to pass the correct value as props to the component ' +\n        'where it is created.'\n      ) : invariant(this._mountDepth === 0));\n      // This is a deoptimized path. We optimize for always having a descriptor.\n      // This creates an extra internal descriptor.\n      this._pendingDescriptor = ReactDescriptor.cloneAndReplaceProps(\n        this._pendingDescriptor || this._descriptor,\n        props\n      );\n      ReactUpdates.enqueueUpdate(this, callback);\n    },\n\n    /**\n     * Schedule a partial update to the props. Only used for internal testing.\n     *\n     * @param {object} partialProps Subset of the next props.\n     * @param {?function} callback Called after props are updated.\n     * @final\n     * @internal\n     */\n    _setPropsInternal: function(partialProps, callback) {\n      // This is a deoptimized path. We optimize for always having a descriptor.\n      // This creates an extra internal descriptor.\n      var descriptor = this._pendingDescriptor || this._descriptor;\n      this._pendingDescriptor = ReactDescriptor.cloneAndReplaceProps(\n        descriptor,\n        merge(descriptor.props, partialProps)\n      );\n      ReactUpdates.enqueueUpdate(this, callback);\n    },\n\n    /**\n     * Base constructor for all React components.\n     *\n     * Subclasses that override this method should make sure to invoke\n     * `ReactComponent.Mixin.construct.call(this, ...)`.\n     *\n     * @param {ReactDescriptor} descriptor\n     * @internal\n     */\n    construct: function(descriptor) {\n      // This is the public exposed props object after it has been processed\n      // with default props. The descriptor's props represents the true internal\n      // state of the props.\n      this.props = descriptor.props;\n      // Record the component responsible for creating this component.\n      // This is accessible through the descriptor but we maintain an extra\n      // field for compatibility with devtools and as a way to make an\n      // incremental update. TODO: Consider deprecating this field.\n      this._owner = descriptor._owner;\n\n      // All components start unmounted.\n      this._lifeCycleState = ComponentLifeCycle.UNMOUNTED;\n\n      // See ReactUpdates.\n      this._pendingCallbacks = null;\n\n      // We keep the old descriptor and a reference to the pending descriptor\n      // to track updates.\n      this._descriptor = descriptor;\n      this._pendingDescriptor = null;\n    },\n\n    /**\n     * Initializes the component, renders markup, and registers event listeners.\n     *\n     * NOTE: This does not insert any nodes into the DOM.\n     *\n     * Subclasses that override this method should make sure to invoke\n     * `ReactComponent.Mixin.mountComponent.call(this, ...)`.\n     *\n     * @param {string} rootID DOM ID of the root node.\n     * @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction\n     * @param {number} mountDepth number of components in the owner hierarchy.\n     * @return {?string} Rendered markup to be inserted into the DOM.\n     * @internal\n     */\n    mountComponent: function(rootID, transaction, mountDepth) {\n      (\"production\" !== \"development\" ? invariant(\n        !this.isMounted(),\n        'mountComponent(%s, ...): Can only mount an unmounted component. ' +\n        'Make sure to avoid storing components between renders or reusing a ' +\n        'single component instance in multiple places.',\n        rootID\n      ) : invariant(!this.isMounted()));\n      var props = this._descriptor.props;\n      if (props.ref != null) {\n        var owner = this._descriptor._owner;\n        ReactOwner.addComponentAsRefTo(this, props.ref, owner);\n      }\n      this._rootNodeID = rootID;\n      this._lifeCycleState = ComponentLifeCycle.MOUNTED;\n      this._mountDepth = mountDepth;\n      // Effectively: return '';\n    },\n\n    /**\n     * Releases any resources allocated by `mountComponent`.\n     *\n     * NOTE: This does not remove any nodes from the DOM.\n     *\n     * Subclasses that override this method should make sure to invoke\n     * `ReactComponent.Mixin.unmountComponent.call(this)`.\n     *\n     * @internal\n     */\n    unmountComponent: function() {\n      (\"production\" !== \"development\" ? invariant(\n        this.isMounted(),\n        'unmountComponent(): Can only unmount a mounted component.'\n      ) : invariant(this.isMounted()));\n      var props = this.props;\n      if (props.ref != null) {\n        ReactOwner.removeComponentAsRefFrom(this, props.ref, this._owner);\n      }\n      unmountIDFromEnvironment(this._rootNodeID);\n      this._rootNodeID = null;\n      this._lifeCycleState = ComponentLifeCycle.UNMOUNTED;\n    },\n\n    /**\n     * Given a new instance of this component, updates the rendered DOM nodes\n     * as if that instance was rendered instead.\n     *\n     * Subclasses that override this method should make sure to invoke\n     * `ReactComponent.Mixin.receiveComponent.call(this, ...)`.\n     *\n     * @param {object} nextComponent Next set of properties.\n     * @param {ReactReconcileTransaction} transaction\n     * @internal\n     */\n    receiveComponent: function(nextDescriptor, transaction) {\n      (\"production\" !== \"development\" ? invariant(\n        this.isMounted(),\n        'receiveComponent(...): Can only update a mounted component.'\n      ) : invariant(this.isMounted()));\n      this._pendingDescriptor = nextDescriptor;\n      this.performUpdateIfNecessary(transaction);\n    },\n\n    /**\n     * If `_pendingDescriptor` is set, update the component.\n     *\n     * @param {ReactReconcileTransaction} transaction\n     * @internal\n     */\n    performUpdateIfNecessary: function(transaction) {\n      if (this._pendingDescriptor == null) {\n        return;\n      }\n      var prevDescriptor = this._descriptor;\n      var nextDescriptor = this._pendingDescriptor;\n      this._descriptor = nextDescriptor;\n      this.props = nextDescriptor.props;\n      this._owner = nextDescriptor._owner;\n      this._pendingDescriptor = null;\n      this.updateComponent(transaction, prevDescriptor);\n    },\n\n    /**\n     * Updates the component's currently mounted representation.\n     *\n     * @param {ReactReconcileTransaction} transaction\n     * @param {object} prevDescriptor\n     * @internal\n     */\n    updateComponent: function(transaction, prevDescriptor) {\n      var nextDescriptor = this._descriptor;\n\n      // If either the owner or a `ref` has changed, make sure the newest owner\n      // has stored a reference to `this`, and the previous owner (if different)\n      // has forgotten the reference to `this`. We use the descriptor instead\n      // of the public this.props because the post processing cannot determine\n      // a ref. The ref conceptually lives on the descriptor.\n\n      // TODO: Should this even be possible? The owner cannot change because\n      // it's forbidden by shouldUpdateReactComponent. The ref can change\n      // if you swap the keys of but not the refs. Reconsider where this check\n      // is made. It probably belongs where the key checking and\n      // instantiateReactComponent is done.\n\n      if (nextDescriptor._owner !== prevDescriptor._owner ||\n          nextDescriptor.props.ref !== prevDescriptor.props.ref) {\n        if (prevDescriptor.props.ref != null) {\n          ReactOwner.removeComponentAsRefFrom(\n            this, prevDescriptor.props.ref, prevDescriptor._owner\n          );\n        }\n        // Correct, even if the owner is the same, and only the ref has changed.\n        if (nextDescriptor.props.ref != null) {\n          ReactOwner.addComponentAsRefTo(\n            this,\n            nextDescriptor.props.ref,\n            nextDescriptor._owner\n          );\n        }\n      }\n    },\n\n    /**\n     * Mounts this component and inserts it into the DOM.\n     *\n     * @param {string} rootID DOM ID of the root node.\n     * @param {DOMElement} container DOM element to mount into.\n     * @param {boolean} shouldReuseMarkup If true, do not insert markup\n     * @final\n     * @internal\n     * @see {ReactMount.renderComponent}\n     */\n    mountComponentIntoNode: function(rootID, container, shouldReuseMarkup) {\n      var transaction = ReactUpdates.ReactReconcileTransaction.getPooled();\n      transaction.perform(\n        this._mountComponentIntoNode,\n        this,\n        rootID,\n        container,\n        transaction,\n        shouldReuseMarkup\n      );\n      ReactUpdates.ReactReconcileTransaction.release(transaction);\n    },\n\n    /**\n     * @param {string} rootID DOM ID of the root node.\n     * @param {DOMElement} container DOM element to mount into.\n     * @param {ReactReconcileTransaction} transaction\n     * @param {boolean} shouldReuseMarkup If true, do not insert markup\n     * @final\n     * @private\n     */\n    _mountComponentIntoNode: function(\n        rootID,\n        container,\n        transaction,\n        shouldReuseMarkup) {\n      var markup = this.mountComponent(rootID, transaction, 0);\n      mountImageIntoNode(markup, container, shouldReuseMarkup);\n    },\n\n    /**\n     * Checks if this component is owned by the supplied `owner` component.\n     *\n     * @param {ReactComponent} owner Component to check.\n     * @return {boolean} True if `owners` owns this component.\n     * @final\n     * @internal\n     */\n    isOwnedBy: function(owner) {\n      return this._owner === owner;\n    },\n\n    /**\n     * Gets another component, that shares the same owner as this one, by ref.\n     *\n     * @param {string} ref of a sibling Component.\n     * @return {?ReactComponent} the actual sibling Component.\n     * @final\n     * @internal\n     */\n    getSiblingByRef: function(ref) {\n      var owner = this._owner;\n      if (!owner || !owner.refs) {\n        return null;\n      }\n      return owner.refs[ref];\n    }\n  }\n};\n\nmodule.exports = ReactComponent;\n\n},{\"./ReactDescriptor\":56,\"./ReactOwner\":70,\"./ReactUpdates\":87,\"./invariant\":134,\"./keyMirror\":140,\"./merge\":144}],36:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule ReactComponentBrowserEnvironment\n */\n\n/*jslint evil: true */\n\n\"use strict\";\n\nvar ReactDOMIDOperations = _dereq_(\"./ReactDOMIDOperations\");\nvar ReactMarkupChecksum = _dereq_(\"./ReactMarkupChecksum\");\nvar ReactMount = _dereq_(\"./ReactMount\");\nvar ReactPerf = _dereq_(\"./ReactPerf\");\nvar ReactReconcileTransaction = _dereq_(\"./ReactReconcileTransaction\");\n\nvar getReactRootElementInContainer = _dereq_(\"./getReactRootElementInContainer\");\nvar invariant = _dereq_(\"./invariant\");\nvar setInnerHTML = _dereq_(\"./setInnerHTML\");\n\n\nvar ELEMENT_NODE_TYPE = 1;\nvar DOC_NODE_TYPE = 9;\n\n\n/**\n * Abstracts away all functionality of `ReactComponent` requires knowledge of\n * the browser context.\n */\nvar ReactComponentBrowserEnvironment = {\n  ReactReconcileTransaction: ReactReconcileTransaction,\n\n  BackendIDOperations: ReactDOMIDOperations,\n\n  /**\n   * If a particular environment requires that some resources be cleaned up,\n   * specify this in the injected Mixin. In the DOM, we would likely want to\n   * purge any cached node ID lookups.\n   *\n   * @private\n   */\n  unmountIDFromEnvironment: function(rootNodeID) {\n    ReactMount.purgeID(rootNodeID);\n  },\n\n  /**\n   * @param {string} markup Markup string to place into the DOM Element.\n   * @param {DOMElement} container DOM Element to insert markup into.\n   * @param {boolean} shouldReuseMarkup Should reuse the existing markup in the\n   * container if possible.\n   */\n  mountImageIntoNode: ReactPerf.measure(\n    'ReactComponentBrowserEnvironment',\n    'mountImageIntoNode',\n    function(markup, container, shouldReuseMarkup) {\n      (\"production\" !== \"development\" ? invariant(\n        container && (\n          container.nodeType === ELEMENT_NODE_TYPE ||\n            container.nodeType === DOC_NODE_TYPE\n        ),\n        'mountComponentIntoNode(...): Target container is not valid.'\n      ) : invariant(container && (\n        container.nodeType === ELEMENT_NODE_TYPE ||\n          container.nodeType === DOC_NODE_TYPE\n      )));\n\n      if (shouldReuseMarkup) {\n        if (ReactMarkupChecksum.canReuseMarkup(\n          markup,\n          getReactRootElementInContainer(container))) {\n          return;\n        } else {\n          (\"production\" !== \"development\" ? invariant(\n            container.nodeType !== DOC_NODE_TYPE,\n            'You\\'re trying to render a component to the document using ' +\n            'server rendering but the checksum was invalid. This usually ' +\n            'means you rendered a different component type or props on ' +\n            'the client from the one on the server, or your render() ' +\n            'methods are impure. React cannot handle this case due to ' +\n            'cross-browser quirks by rendering at the document root. You ' +\n            'should look for environment dependent code in your components ' +\n            'and ensure the props are the same client and server side.'\n          ) : invariant(container.nodeType !== DOC_NODE_TYPE));\n\n          if (\"production\" !== \"development\") {\n            console.warn(\n              'React attempted to use reuse markup in a container but the ' +\n              'checksum was invalid. This generally means that you are ' +\n              'using server rendering and the markup generated on the ' +\n              'server was not what the client was expecting. React injected ' +\n              'new markup to compensate which works but you have lost many ' +\n              'of the benefits of server rendering. Instead, figure out ' +\n              'why the markup being generated is different on the client ' +\n              'or server.'\n            );\n          }\n        }\n      }\n\n      (\"production\" !== \"development\" ? invariant(\n        container.nodeType !== DOC_NODE_TYPE,\n        'You\\'re trying to render a component to the document but ' +\n          'you didn\\'t use server rendering. We can\\'t do this ' +\n          'without using server rendering due to cross-browser quirks. ' +\n          'See renderComponentToString() for server rendering.'\n      ) : invariant(container.nodeType !== DOC_NODE_TYPE));\n\n      setInnerHTML(container, markup);\n    }\n  )\n};\n\nmodule.exports = ReactComponentBrowserEnvironment;\n\n},{\"./ReactDOMIDOperations\":45,\"./ReactMarkupChecksum\":66,\"./ReactMount\":67,\"./ReactPerf\":71,\"./ReactReconcileTransaction\":77,\"./getReactRootElementInContainer\":128,\"./invariant\":134,\"./setInnerHTML\":152}],37:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n* @providesModule ReactComponentWithPureRenderMixin\n*/\n\n\"use strict\";\n\nvar shallowEqual = _dereq_(\"./shallowEqual\");\n\n/**\n * If your React component's render function is \"pure\", e.g. it will render the\n * same result given the same props and state, provide this Mixin for a\n * considerable performance boost.\n *\n * Most React components have pure render functions.\n *\n * Example:\n *\n *   var ReactComponentWithPureRenderMixin =\n *     require('ReactComponentWithPureRenderMixin');\n *   React.createClass({\n *     mixins: [ReactComponentWithPureRenderMixin],\n *\n *     render: function() {\n *       return <div className={this.props.className}>foo</div>;\n *     }\n *   });\n *\n * Note: This only checks shallow equality for props and state. If these contain\n * complex data structures this mixin may have false-negatives for deeper\n * differences. Only mixin to components which have simple props and state, or\n * use `forceUpdate()` when you know deep data structures have changed.\n */\nvar ReactComponentWithPureRenderMixin = {\n  shouldComponentUpdate: function(nextProps, nextState) {\n    return !shallowEqual(this.props, nextProps) ||\n           !shallowEqual(this.state, nextState);\n  }\n};\n\nmodule.exports = ReactComponentWithPureRenderMixin;\n\n},{\"./shallowEqual\":153}],38:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule ReactCompositeComponent\n */\n\n\"use strict\";\n\nvar ReactComponent = _dereq_(\"./ReactComponent\");\nvar ReactContext = _dereq_(\"./ReactContext\");\nvar ReactCurrentOwner = _dereq_(\"./ReactCurrentOwner\");\nvar ReactDescriptor = _dereq_(\"./ReactDescriptor\");\nvar ReactDescriptorValidator = _dereq_(\"./ReactDescriptorValidator\");\nvar ReactEmptyComponent = _dereq_(\"./ReactEmptyComponent\");\nvar ReactErrorUtils = _dereq_(\"./ReactErrorUtils\");\nvar ReactOwner = _dereq_(\"./ReactOwner\");\nvar ReactPerf = _dereq_(\"./ReactPerf\");\nvar ReactPropTransferer = _dereq_(\"./ReactPropTransferer\");\nvar ReactPropTypeLocations = _dereq_(\"./ReactPropTypeLocations\");\nvar ReactPropTypeLocationNames = _dereq_(\"./ReactPropTypeLocationNames\");\nvar ReactUpdates = _dereq_(\"./ReactUpdates\");\n\nvar instantiateReactComponent = _dereq_(\"./instantiateReactComponent\");\nvar invariant = _dereq_(\"./invariant\");\nvar keyMirror = _dereq_(\"./keyMirror\");\nvar merge = _dereq_(\"./merge\");\nvar mixInto = _dereq_(\"./mixInto\");\nvar monitorCodeUse = _dereq_(\"./monitorCodeUse\");\nvar mapObject = _dereq_(\"./mapObject\");\nvar shouldUpdateReactComponent = _dereq_(\"./shouldUpdateReactComponent\");\nvar warning = _dereq_(\"./warning\");\n\n/**\n * Policies that describe methods in `ReactCompositeComponentInterface`.\n */\nvar SpecPolicy = keyMirror({\n  /**\n   * These methods may be defined only once by the class specification or mixin.\n   */\n  DEFINE_ONCE: null,\n  /**\n   * These methods may be defined by both the class specification and mixins.\n   * Subsequent definitions will be chained. These methods must return void.\n   */\n  DEFINE_MANY: null,\n  /**\n   * These methods are overriding the base ReactCompositeComponent class.\n   */\n  OVERRIDE_BASE: null,\n  /**\n   * These methods are similar to DEFINE_MANY, except we assume they return\n   * objects. We try to merge the keys of the return values of all the mixed in\n   * functions. If there is a key conflict we throw.\n   */\n  DEFINE_MANY_MERGED: null\n});\n\n\nvar injectedMixins = [];\n\n/**\n * Composite components are higher-level components that compose other composite\n * or native components.\n *\n * To create a new type of `ReactCompositeComponent`, pass a specification of\n * your new class to `React.createClass`. The only requirement of your class\n * specification is that you implement a `render` method.\n *\n *   var MyComponent = React.createClass({\n *     render: function() {\n *       return <div>Hello World</div>;\n *     }\n *   });\n *\n * The class specification supports a specific protocol of methods that have\n * special meaning (e.g. `render`). See `ReactCompositeComponentInterface` for\n * more the comprehensive protocol. Any other properties and methods in the\n * class specification will available on the prototype.\n *\n * @interface ReactCompositeComponentInterface\n * @internal\n */\nvar ReactCompositeComponentInterface = {\n\n  /**\n   * An array of Mixin objects to include when defining your component.\n   *\n   * @type {array}\n   * @optional\n   */\n  mixins: SpecPolicy.DEFINE_MANY,\n\n  /**\n   * An object containing properties and methods that should be defined on\n   * the component's constructor instead of its prototype (static methods).\n   *\n   * @type {object}\n   * @optional\n   */\n  statics: SpecPolicy.DEFINE_MANY,\n\n  /**\n   * Definition of prop types for this component.\n   *\n   * @type {object}\n   * @optional\n   */\n  propTypes: SpecPolicy.DEFINE_MANY,\n\n  /**\n   * Definition of context types for this component.\n   *\n   * @type {object}\n   * @optional\n   */\n  contextTypes: SpecPolicy.DEFINE_MANY,\n\n  /**\n   * Definition of context types this component sets for its children.\n   *\n   * @type {object}\n   * @optional\n   */\n  childContextTypes: SpecPolicy.DEFINE_MANY,\n\n  // ==== Definition methods ====\n\n  /**\n   * Invoked when the component is mounted. Values in the mapping will be set on\n   * `this.props` if that prop is not specified (i.e. using an `in` check).\n   *\n   * This method is invoked before `getInitialState` and therefore cannot rely\n   * on `this.state` or use `this.setState`.\n   *\n   * @return {object}\n   * @optional\n   */\n  getDefaultProps: SpecPolicy.DEFINE_MANY_MERGED,\n\n  /**\n   * Invoked once before the component is mounted. The return value will be used\n   * as the initial value of `this.state`.\n   *\n   *   getInitialState: function() {\n   *     return {\n   *       isOn: false,\n   *       fooBaz: new BazFoo()\n   *     }\n   *   }\n   *\n   * @return {object}\n   * @optional\n   */\n  getInitialState: SpecPolicy.DEFINE_MANY_MERGED,\n\n  /**\n   * @return {object}\n   * @optional\n   */\n  getChildContext: SpecPolicy.DEFINE_MANY_MERGED,\n\n  /**\n   * Uses props from `this.props` and state from `this.state` to render the\n   * structure of the component.\n   *\n   * No guarantees are made about when or how often this method is invoked, so\n   * it must not have side effects.\n   *\n   *   render: function() {\n   *     var name = this.props.name;\n   *     return <div>Hello, {name}!</div>;\n   *   }\n   *\n   * @return {ReactComponent}\n   * @nosideeffects\n   * @required\n   */\n  render: SpecPolicy.DEFINE_ONCE,\n\n\n\n  // ==== Delegate methods ====\n\n  /**\n   * Invoked when the component is initially created and about to be mounted.\n   * This may have side effects, but any external subscriptions or data created\n   * by this method must be cleaned up in `componentWillUnmount`.\n   *\n   * @optional\n   */\n  componentWillMount: SpecPolicy.DEFINE_MANY,\n\n  /**\n   * Invoked when the component has been mounted and has a DOM representation.\n   * However, there is no guarantee that the DOM node is in the document.\n   *\n   * Use this as an opportunity to operate on the DOM when the component has\n   * been mounted (initialized and rendered) for the first time.\n   *\n   * @param {DOMElement} rootNode DOM element representing the component.\n   * @optional\n   */\n  componentDidMount: SpecPolicy.DEFINE_MANY,\n\n  /**\n   * Invoked before the component receives new props.\n   *\n   * Use this as an opportunity to react to a prop transition by updating the\n   * state using `this.setState`. Current props are accessed via `this.props`.\n   *\n   *   componentWillReceiveProps: function(nextProps, nextContext) {\n   *     this.setState({\n   *       likesIncreasing: nextProps.likeCount > this.props.likeCount\n   *     });\n   *   }\n   *\n   * NOTE: There is no equivalent `componentWillReceiveState`. An incoming prop\n   * transition may cause a state change, but the opposite is not true. If you\n   * need it, you are probably looking for `componentWillUpdate`.\n   *\n   * @param {object} nextProps\n   * @optional\n   */\n  componentWillReceiveProps: SpecPolicy.DEFINE_MANY,\n\n  /**\n   * Invoked while deciding if the component should be updated as a result of\n   * receiving new props, state and/or context.\n   *\n   * Use this as an opportunity to `return false` when you're certain that the\n   * transition to the new props/state/context will not require a component\n   * update.\n   *\n   *   shouldComponentUpdate: function(nextProps, nextState, nextContext) {\n   *     return !equal(nextProps, this.props) ||\n   *       !equal(nextState, this.state) ||\n   *       !equal(nextContext, this.context);\n   *   }\n   *\n   * @param {object} nextProps\n   * @param {?object} nextState\n   * @param {?object} nextContext\n   * @return {boolean} True if the component should update.\n   * @optional\n   */\n  shouldComponentUpdate: SpecPolicy.DEFINE_ONCE,\n\n  /**\n   * Invoked when the component is about to update due to a transition from\n   * `this.props`, `this.state` and `this.context` to `nextProps`, `nextState`\n   * and `nextContext`.\n   *\n   * Use this as an opportunity to perform preparation before an update occurs.\n   *\n   * NOTE: You **cannot** use `this.setState()` in this method.\n   *\n   * @param {object} nextProps\n   * @param {?object} nextState\n   * @param {?object} nextContext\n   * @param {ReactReconcileTransaction} transaction\n   * @optional\n   */\n  componentWillUpdate: SpecPolicy.DEFINE_MANY,\n\n  /**\n   * Invoked when the component's DOM representation has been updated.\n   *\n   * Use this as an opportunity to operate on the DOM when the component has\n   * been updated.\n   *\n   * @param {object} prevProps\n   * @param {?object} prevState\n   * @param {?object} prevContext\n   * @param {DOMElement} rootNode DOM element representing the component.\n   * @optional\n   */\n  componentDidUpdate: SpecPolicy.DEFINE_MANY,\n\n  /**\n   * Invoked when the component is about to be removed from its parent and have\n   * its DOM representation destroyed.\n   *\n   * Use this as an opportunity to deallocate any external resources.\n   *\n   * NOTE: There is no `componentDidUnmount` since your component will have been\n   * destroyed by that point.\n   *\n   * @optional\n   */\n  componentWillUnmount: SpecPolicy.DEFINE_MANY,\n\n\n\n  // ==== Advanced methods ====\n\n  /**\n   * Updates the component's currently mounted DOM representation.\n   *\n   * By default, this implements React's rendering and reconciliation algorithm.\n   * Sophisticated clients may wish to override this.\n   *\n   * @param {ReactReconcileTransaction} transaction\n   * @internal\n   * @overridable\n   */\n  updateComponent: SpecPolicy.OVERRIDE_BASE\n\n};\n\n/**\n * Mapping from class specification keys to special processing functions.\n *\n * Although these are declared like instance properties in the specification\n * when defining classes using `React.createClass`, they are actually static\n * and are accessible on the constructor instead of the prototype. Despite\n * being static, they must be defined outside of the \"statics\" key under\n * which all other static methods are defined.\n */\nvar RESERVED_SPEC_KEYS = {\n  displayName: function(Constructor, displayName) {\n    Constructor.displayName = displayName;\n  },\n  mixins: function(Constructor, mixins) {\n    if (mixins) {\n      for (var i = 0; i < mixins.length; i++) {\n        mixSpecIntoComponent(Constructor, mixins[i]);\n      }\n    }\n  },\n  childContextTypes: function(Constructor, childContextTypes) {\n    validateTypeDef(\n      Constructor,\n      childContextTypes,\n      ReactPropTypeLocations.childContext\n    );\n    Constructor.childContextTypes = merge(\n      Constructor.childContextTypes,\n      childContextTypes\n    );\n  },\n  contextTypes: function(Constructor, contextTypes) {\n    validateTypeDef(\n      Constructor,\n      contextTypes,\n      ReactPropTypeLocations.context\n    );\n    Constructor.contextTypes = merge(Constructor.contextTypes, contextTypes);\n  },\n  /**\n   * Special case getDefaultProps which should move into statics but requires\n   * automatic merging.\n   */\n  getDefaultProps: function(Constructor, getDefaultProps) {\n    if (Constructor.getDefaultProps) {\n      Constructor.getDefaultProps = createMergedResultFunction(\n        Constructor.getDefaultProps,\n        getDefaultProps\n      );\n    } else {\n      Constructor.getDefaultProps = getDefaultProps;\n    }\n  },\n  propTypes: function(Constructor, propTypes) {\n    validateTypeDef(\n      Constructor,\n      propTypes,\n      ReactPropTypeLocations.prop\n    );\n    Constructor.propTypes = merge(Constructor.propTypes, propTypes);\n  },\n  statics: function(Constructor, statics) {\n    mixStaticSpecIntoComponent(Constructor, statics);\n  }\n};\n\nfunction getDeclarationErrorAddendum(component) {\n  var owner = component._owner || null;\n  if (owner && owner.constructor && owner.constructor.displayName) {\n    return ' Check the render method of `' + owner.constructor.displayName +\n      '`.';\n  }\n  return '';\n}\n\nfunction validateTypeDef(Constructor, typeDef, location) {\n  for (var propName in typeDef) {\n    if (typeDef.hasOwnProperty(propName)) {\n      (\"production\" !== \"development\" ? invariant(\n        typeof typeDef[propName] == 'function',\n        '%s: %s type `%s` is invalid; it must be a function, usually from ' +\n        'React.PropTypes.',\n        Constructor.displayName || 'ReactCompositeComponent',\n        ReactPropTypeLocationNames[location],\n        propName\n      ) : invariant(typeof typeDef[propName] == 'function'));\n    }\n  }\n}\n\nfunction validateMethodOverride(proto, name) {\n  var specPolicy = ReactCompositeComponentInterface.hasOwnProperty(name) ?\n    ReactCompositeComponentInterface[name] :\n    null;\n\n  // Disallow overriding of base class methods unless explicitly allowed.\n  if (ReactCompositeComponentMixin.hasOwnProperty(name)) {\n    (\"production\" !== \"development\" ? invariant(\n      specPolicy === SpecPolicy.OVERRIDE_BASE,\n      'ReactCompositeComponentInterface: You are attempting to override ' +\n      '`%s` from your class specification. Ensure that your method names ' +\n      'do not overlap with React methods.',\n      name\n    ) : invariant(specPolicy === SpecPolicy.OVERRIDE_BASE));\n  }\n\n  // Disallow defining methods more than once unless explicitly allowed.\n  if (proto.hasOwnProperty(name)) {\n    (\"production\" !== \"development\" ? invariant(\n      specPolicy === SpecPolicy.DEFINE_MANY ||\n      specPolicy === SpecPolicy.DEFINE_MANY_MERGED,\n      'ReactCompositeComponentInterface: You are attempting to define ' +\n      '`%s` on your component more than once. This conflict may be due ' +\n      'to a mixin.',\n      name\n    ) : invariant(specPolicy === SpecPolicy.DEFINE_MANY ||\n    specPolicy === SpecPolicy.DEFINE_MANY_MERGED));\n  }\n}\n\nfunction validateLifeCycleOnReplaceState(instance) {\n  var compositeLifeCycleState = instance._compositeLifeCycleState;\n  (\"production\" !== \"development\" ? invariant(\n    instance.isMounted() ||\n      compositeLifeCycleState === CompositeLifeCycle.MOUNTING,\n    'replaceState(...): Can only update a mounted or mounting component.'\n  ) : invariant(instance.isMounted() ||\n    compositeLifeCycleState === CompositeLifeCycle.MOUNTING));\n  (\"production\" !== \"development\" ? invariant(compositeLifeCycleState !== CompositeLifeCycle.RECEIVING_STATE,\n    'replaceState(...): Cannot update during an existing state transition ' +\n    '(such as within `render`). This could potentially cause an infinite ' +\n    'loop so it is forbidden.'\n  ) : invariant(compositeLifeCycleState !== CompositeLifeCycle.RECEIVING_STATE));\n  (\"production\" !== \"development\" ? invariant(compositeLifeCycleState !== CompositeLifeCycle.UNMOUNTING,\n    'replaceState(...): Cannot update while unmounting component. This ' +\n    'usually means you called setState() on an unmounted component.'\n  ) : invariant(compositeLifeCycleState !== CompositeLifeCycle.UNMOUNTING));\n}\n\n/**\n * Custom version of `mixInto` which handles policy validation and reserved\n * specification keys when building `ReactCompositeComponent` classses.\n */\nfunction mixSpecIntoComponent(Constructor, spec) {\n  (\"production\" !== \"development\" ? invariant(\n    !ReactDescriptor.isValidFactory(spec),\n    'ReactCompositeComponent: You\\'re attempting to ' +\n    'use a component class as a mixin. Instead, just use a regular object.'\n  ) : invariant(!ReactDescriptor.isValidFactory(spec)));\n  (\"production\" !== \"development\" ? invariant(\n    !ReactDescriptor.isValidDescriptor(spec),\n    'ReactCompositeComponent: You\\'re attempting to ' +\n    'use a component as a mixin. Instead, just use a regular object.'\n  ) : invariant(!ReactDescriptor.isValidDescriptor(spec)));\n\n  var proto = Constructor.prototype;\n  for (var name in spec) {\n    var property = spec[name];\n    if (!spec.hasOwnProperty(name)) {\n      continue;\n    }\n\n    validateMethodOverride(proto, name);\n\n    if (RESERVED_SPEC_KEYS.hasOwnProperty(name)) {\n      RESERVED_SPEC_KEYS[name](Constructor, property);\n    } else {\n      // Setup methods on prototype:\n      // The following member methods should not be automatically bound:\n      // 1. Expected ReactCompositeComponent methods (in the \"interface\").\n      // 2. Overridden methods (that were mixed in).\n      var isCompositeComponentMethod =\n        ReactCompositeComponentInterface.hasOwnProperty(name);\n      var isAlreadyDefined = proto.hasOwnProperty(name);\n      var markedDontBind = property && property.__reactDontBind;\n      var isFunction = typeof property === 'function';\n      var shouldAutoBind =\n        isFunction &&\n        !isCompositeComponentMethod &&\n        !isAlreadyDefined &&\n        !markedDontBind;\n\n      if (shouldAutoBind) {\n        if (!proto.__reactAutoBindMap) {\n          proto.__reactAutoBindMap = {};\n        }\n        proto.__reactAutoBindMap[name] = property;\n        proto[name] = property;\n      } else {\n        if (isAlreadyDefined) {\n          var specPolicy = ReactCompositeComponentInterface[name];\n\n          // These cases should already be caught by validateMethodOverride\n          (\"production\" !== \"development\" ? invariant(\n            isCompositeComponentMethod && (\n              specPolicy === SpecPolicy.DEFINE_MANY_MERGED ||\n              specPolicy === SpecPolicy.DEFINE_MANY\n            ),\n            'ReactCompositeComponent: Unexpected spec policy %s for key %s ' +\n            'when mixing in component specs.',\n            specPolicy,\n            name\n          ) : invariant(isCompositeComponentMethod && (\n            specPolicy === SpecPolicy.DEFINE_MANY_MERGED ||\n            specPolicy === SpecPolicy.DEFINE_MANY\n          )));\n\n          // For methods which are defined more than once, call the existing\n          // methods before calling the new property, merging if appropriate.\n          if (specPolicy === SpecPolicy.DEFINE_MANY_MERGED) {\n            proto[name] = createMergedResultFunction(proto[name], property);\n          } else if (specPolicy === SpecPolicy.DEFINE_MANY) {\n            proto[name] = createChainedFunction(proto[name], property);\n          }\n        } else {\n          proto[name] = property;\n          if (\"production\" !== \"development\") {\n            // Add verbose displayName to the function, which helps when looking\n            // at profiling tools.\n            if (typeof property === 'function' && spec.displayName) {\n              proto[name].displayName = spec.displayName + '_' + name;\n            }\n          }\n        }\n      }\n    }\n  }\n}\n\nfunction mixStaticSpecIntoComponent(Constructor, statics) {\n  if (!statics) {\n    return;\n  }\n  for (var name in statics) {\n    var property = statics[name];\n    if (!statics.hasOwnProperty(name)) {\n      continue;\n    }\n\n    var isInherited = name in Constructor;\n    var result = property;\n    if (isInherited) {\n      var existingProperty = Constructor[name];\n      var existingType = typeof existingProperty;\n      var propertyType = typeof property;\n      (\"production\" !== \"development\" ? invariant(\n        existingType === 'function' && propertyType === 'function',\n        'ReactCompositeComponent: You are attempting to define ' +\n        '`%s` on your component more than once, but that is only supported ' +\n        'for functions, which are chained together. This conflict may be ' +\n        'due to a mixin.',\n        name\n      ) : invariant(existingType === 'function' && propertyType === 'function'));\n      result = createChainedFunction(existingProperty, property);\n    }\n    Constructor[name] = result;\n  }\n}\n\n/**\n * Merge two objects, but throw if both contain the same key.\n *\n * @param {object} one The first object, which is mutated.\n * @param {object} two The second object\n * @return {object} one after it has been mutated to contain everything in two.\n */\nfunction mergeObjectsWithNoDuplicateKeys(one, two) {\n  (\"production\" !== \"development\" ? invariant(\n    one && two && typeof one === 'object' && typeof two === 'object',\n    'mergeObjectsWithNoDuplicateKeys(): Cannot merge non-objects'\n  ) : invariant(one && two && typeof one === 'object' && typeof two === 'object'));\n\n  mapObject(two, function(value, key) {\n    (\"production\" !== \"development\" ? invariant(\n      one[key] === undefined,\n      'mergeObjectsWithNoDuplicateKeys(): ' +\n      'Tried to merge two objects with the same key: %s',\n      key\n    ) : invariant(one[key] === undefined));\n    one[key] = value;\n  });\n  return one;\n}\n\n/**\n * Creates a function that invokes two functions and merges their return values.\n *\n * @param {function} one Function to invoke first.\n * @param {function} two Function to invoke second.\n * @return {function} Function that invokes the two argument functions.\n * @private\n */\nfunction createMergedResultFunction(one, two) {\n  return function mergedResult() {\n    var a = one.apply(this, arguments);\n    var b = two.apply(this, arguments);\n    if (a == null) {\n      return b;\n    } else if (b == null) {\n      return a;\n    }\n    return mergeObjectsWithNoDuplicateKeys(a, b);\n  };\n}\n\n/**\n * Creates a function that invokes two functions and ignores their return vales.\n *\n * @param {function} one Function to invoke first.\n * @param {function} two Function to invoke second.\n * @return {function} Function that invokes the two argument functions.\n * @private\n */\nfunction createChainedFunction(one, two) {\n  return function chainedFunction() {\n    one.apply(this, arguments);\n    two.apply(this, arguments);\n  };\n}\n\n/**\n * `ReactCompositeComponent` maintains an auxiliary life cycle state in\n * `this._compositeLifeCycleState` (which can be null).\n *\n * This is different from the life cycle state maintained by `ReactComponent` in\n * `this._lifeCycleState`. The following diagram shows how the states overlap in\n * time. There are times when the CompositeLifeCycle is null - at those times it\n * is only meaningful to look at ComponentLifeCycle alone.\n *\n * Top Row: ReactComponent.ComponentLifeCycle\n * Low Row: ReactComponent.CompositeLifeCycle\n *\n * +-------+------------------------------------------------------+--------+\n * |  UN   |                    MOUNTED                           |   UN   |\n * |MOUNTED|                                                      | MOUNTED|\n * +-------+------------------------------------------------------+--------+\n * |       ^--------+   +------+   +------+   +------+   +--------^        |\n * |       |        |   |      |   |      |   |      |   |        |        |\n * |    0--|MOUNTING|-0-|RECEIV|-0-|RECEIV|-0-|RECEIV|-0-|   UN   |--->0   |\n * |       |        |   |PROPS |   | PROPS|   | STATE|   |MOUNTING|        |\n * |       |        |   |      |   |      |   |      |   |        |        |\n * |       |        |   |      |   |      |   |      |   |        |        |\n * |       +--------+   +------+   +------+   +------+   +--------+        |\n * |       |                                                      |        |\n * +-------+------------------------------------------------------+--------+\n */\nvar CompositeLifeCycle = keyMirror({\n  /**\n   * Components in the process of being mounted respond to state changes\n   * differently.\n   */\n  MOUNTING: null,\n  /**\n   * Components in the process of being unmounted are guarded against state\n   * changes.\n   */\n  UNMOUNTING: null,\n  /**\n   * Components that are mounted and receiving new props respond to state\n   * changes differently.\n   */\n  RECEIVING_PROPS: null,\n  /**\n   * Components that are mounted and receiving new state are guarded against\n   * additional state changes.\n   */\n  RECEIVING_STATE: null\n});\n\n/**\n * @lends {ReactCompositeComponent.prototype}\n */\nvar ReactCompositeComponentMixin = {\n\n  /**\n   * Base constructor for all composite component.\n   *\n   * @param {ReactDescriptor} descriptor\n   * @final\n   * @internal\n   */\n  construct: function(descriptor) {\n    // Children can be either an array or more than one argument\n    ReactComponent.Mixin.construct.apply(this, arguments);\n    ReactOwner.Mixin.construct.apply(this, arguments);\n\n    this.state = null;\n    this._pendingState = null;\n\n    // This is the public post-processed context. The real context and pending\n    // context lives on the descriptor.\n    this.context = null;\n\n    this._compositeLifeCycleState = null;\n  },\n\n  /**\n   * Checks whether or not this composite component is mounted.\n   * @return {boolean} True if mounted, false otherwise.\n   * @protected\n   * @final\n   */\n  isMounted: function() {\n    return ReactComponent.Mixin.isMounted.call(this) &&\n      this._compositeLifeCycleState !== CompositeLifeCycle.MOUNTING;\n  },\n\n  /**\n   * Initializes the component, renders markup, and registers event listeners.\n   *\n   * @param {string} rootID DOM ID of the root node.\n   * @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction\n   * @param {number} mountDepth number of components in the owner hierarchy\n   * @return {?string} Rendered markup to be inserted into the DOM.\n   * @final\n   * @internal\n   */\n  mountComponent: ReactPerf.measure(\n    'ReactCompositeComponent',\n    'mountComponent',\n    function(rootID, transaction, mountDepth) {\n      ReactComponent.Mixin.mountComponent.call(\n        this,\n        rootID,\n        transaction,\n        mountDepth\n      );\n      this._compositeLifeCycleState = CompositeLifeCycle.MOUNTING;\n\n      if (this.__reactAutoBindMap) {\n        this._bindAutoBindMethods();\n      }\n\n      this.context = this._processContext(this._descriptor._context);\n      this.props = this._processProps(this.props);\n\n      this.state = this.getInitialState ? this.getInitialState() : null;\n      (\"production\" !== \"development\" ? invariant(\n        typeof this.state === 'object' && !Array.isArray(this.state),\n        '%s.getInitialState(): must return an object or null',\n        this.constructor.displayName || 'ReactCompositeComponent'\n      ) : invariant(typeof this.state === 'object' && !Array.isArray(this.state)));\n\n      this._pendingState = null;\n      this._pendingForceUpdate = false;\n\n      if (this.componentWillMount) {\n        this.componentWillMount();\n        // When mounting, calls to `setState` by `componentWillMount` will set\n        // `this._pendingState` without triggering a re-render.\n        if (this._pendingState) {\n          this.state = this._pendingState;\n          this._pendingState = null;\n        }\n      }\n\n      this._renderedComponent = instantiateReactComponent(\n        this._renderValidatedComponent()\n      );\n\n      // Done with mounting, `setState` will now trigger UI changes.\n      this._compositeLifeCycleState = null;\n      var markup = this._renderedComponent.mountComponent(\n        rootID,\n        transaction,\n        mountDepth + 1\n      );\n      if (this.componentDidMount) {\n        transaction.getReactMountReady().enqueue(this.componentDidMount, this);\n      }\n      return markup;\n    }\n  ),\n\n  /**\n   * Releases any resources allocated by `mountComponent`.\n   *\n   * @final\n   * @internal\n   */\n  unmountComponent: function() {\n    this._compositeLifeCycleState = CompositeLifeCycle.UNMOUNTING;\n    if (this.componentWillUnmount) {\n      this.componentWillUnmount();\n    }\n    this._compositeLifeCycleState = null;\n\n    this._renderedComponent.unmountComponent();\n    this._renderedComponent = null;\n\n    ReactComponent.Mixin.unmountComponent.call(this);\n\n    // Some existing components rely on this.props even after they've been\n    // destroyed (in event handlers).\n    // TODO: this.props = null;\n    // TODO: this.state = null;\n  },\n\n  /**\n   * Sets a subset of the state. Always use this or `replaceState` to mutate\n   * state. You should treat `this.state` as immutable.\n   *\n   * There is no guarantee that `this.state` will be immediately updated, so\n   * accessing `this.state` after calling this method may return the old value.\n   *\n   * There is no guarantee that calls to `setState` will run synchronously,\n   * as they may eventually be batched together.  You can provide an optional\n   * callback that will be executed when the call to setState is actually\n   * completed.\n   *\n   * @param {object} partialState Next partial state to be merged with state.\n   * @param {?function} callback Called after state is updated.\n   * @final\n   * @protected\n   */\n  setState: function(partialState, callback) {\n    (\"production\" !== \"development\" ? invariant(\n      typeof partialState === 'object' || partialState == null,\n      'setState(...): takes an object of state variables to update.'\n    ) : invariant(typeof partialState === 'object' || partialState == null));\n    if (\"production\" !== \"development\"){\n      (\"production\" !== \"development\" ? warning(\n        partialState != null,\n        'setState(...): You passed an undefined or null state object; ' +\n        'instead, use forceUpdate().'\n      ) : null);\n    }\n    // Merge with `_pendingState` if it exists, otherwise with existing state.\n    this.replaceState(\n      merge(this._pendingState || this.state, partialState),\n      callback\n    );\n  },\n\n  /**\n   * Replaces all of the state. Always use this or `setState` to mutate state.\n   * You should treat `this.state` as immutable.\n   *\n   * There is no guarantee that `this.state` will be immediately updated, so\n   * accessing `this.state` after calling this method may return the old value.\n   *\n   * @param {object} completeState Next state.\n   * @param {?function} callback Called after state is updated.\n   * @final\n   * @protected\n   */\n  replaceState: function(completeState, callback) {\n    validateLifeCycleOnReplaceState(this);\n    this._pendingState = completeState;\n    if (this._compositeLifeCycleState !== CompositeLifeCycle.MOUNTING) {\n      // If we're in a componentWillMount handler, don't enqueue a rerender\n      // because ReactUpdates assumes we're in a browser context (which is wrong\n      // for server rendering) and we're about to do a render anyway.\n      // TODO: The callback here is ignored when setState is called from\n      // componentWillMount. Either fix it or disallow doing so completely in\n      // favor of getInitialState.\n      ReactUpdates.enqueueUpdate(this, callback);\n    }\n  },\n\n  /**\n   * Filters the context object to only contain keys specified in\n   * `contextTypes`, and asserts that they are valid.\n   *\n   * @param {object} context\n   * @return {?object}\n   * @private\n   */\n  _processContext: function(context) {\n    var maskedContext = null;\n    var contextTypes = this.constructor.contextTypes;\n    if (contextTypes) {\n      maskedContext = {};\n      for (var contextName in contextTypes) {\n        maskedContext[contextName] = context[contextName];\n      }\n      if (\"production\" !== \"development\") {\n        this._checkPropTypes(\n          contextTypes,\n          maskedContext,\n          ReactPropTypeLocations.context\n        );\n      }\n    }\n    return maskedContext;\n  },\n\n  /**\n   * @param {object} currentContext\n   * @return {object}\n   * @private\n   */\n  _processChildContext: function(currentContext) {\n    var childContext = this.getChildContext && this.getChildContext();\n    var displayName = this.constructor.displayName || 'ReactCompositeComponent';\n    if (childContext) {\n      (\"production\" !== \"development\" ? invariant(\n        typeof this.constructor.childContextTypes === 'object',\n        '%s.getChildContext(): childContextTypes must be defined in order to ' +\n        'use getChildContext().',\n        displayName\n      ) : invariant(typeof this.constructor.childContextTypes === 'object'));\n      if (\"production\" !== \"development\") {\n        this._checkPropTypes(\n          this.constructor.childContextTypes,\n          childContext,\n          ReactPropTypeLocations.childContext\n        );\n      }\n      for (var name in childContext) {\n        (\"production\" !== \"development\" ? invariant(\n          name in this.constructor.childContextTypes,\n          '%s.getChildContext(): key \"%s\" is not defined in childContextTypes.',\n          displayName,\n          name\n        ) : invariant(name in this.constructor.childContextTypes));\n      }\n      return merge(currentContext, childContext);\n    }\n    return currentContext;\n  },\n\n  /**\n   * Processes props by setting default values for unspecified props and\n   * asserting that the props are valid. Does not mutate its argument; returns\n   * a new props object with defaults merged in.\n   *\n   * @param {object} newProps\n   * @return {object}\n   * @private\n   */\n  _processProps: function(newProps) {\n    var defaultProps = this.constructor.defaultProps;\n    var props;\n    if (defaultProps) {\n      props = merge(newProps);\n      for (var propName in defaultProps) {\n        if (typeof props[propName] === 'undefined') {\n          props[propName] = defaultProps[propName];\n        }\n      }\n    } else {\n      props = newProps;\n    }\n    if (\"production\" !== \"development\") {\n      var propTypes = this.constructor.propTypes;\n      if (propTypes) {\n        this._checkPropTypes(propTypes, props, ReactPropTypeLocations.prop);\n      }\n    }\n    return props;\n  },\n\n  /**\n   * Assert that the props are valid\n   *\n   * @param {object} propTypes Map of prop name to a ReactPropType\n   * @param {object} props\n   * @param {string} location e.g. \"prop\", \"context\", \"child context\"\n   * @private\n   */\n  _checkPropTypes: function(propTypes, props, location) {\n    // TODO: Stop validating prop types here and only use the descriptor\n    // validation.\n    var componentName = this.constructor.displayName;\n    for (var propName in propTypes) {\n      if (propTypes.hasOwnProperty(propName)) {\n        var error =\n          propTypes[propName](props, propName, componentName, location);\n        if (error instanceof Error) {\n          // We may want to extend this logic for similar errors in\n          // renderComponent calls, so I'm abstracting it away into\n          // a function to minimize refactoring in the future\n          var addendum = getDeclarationErrorAddendum(this);\n          (\"production\" !== \"development\" ? warning(false, error.message + addendum) : null);\n        }\n      }\n    }\n  },\n\n  /**\n   * If any of `_pendingDescriptor`, `_pendingState`, or `_pendingForceUpdate`\n   * is set, update the component.\n   *\n   * @param {ReactReconcileTransaction} transaction\n   * @internal\n   */\n  performUpdateIfNecessary: function(transaction) {\n    var compositeLifeCycleState = this._compositeLifeCycleState;\n    // Do not trigger a state transition if we are in the middle of mounting or\n    // receiving props because both of those will already be doing this.\n    if (compositeLifeCycleState === CompositeLifeCycle.MOUNTING ||\n        compositeLifeCycleState === CompositeLifeCycle.RECEIVING_PROPS) {\n      return;\n    }\n\n    if (this._pendingDescriptor == null &&\n        this._pendingState == null &&\n        !this._pendingForceUpdate) {\n      return;\n    }\n\n    var nextContext = this.context;\n    var nextProps = this.props;\n    var nextDescriptor = this._descriptor;\n    if (this._pendingDescriptor != null) {\n      nextDescriptor = this._pendingDescriptor;\n      nextContext = this._processContext(nextDescriptor._context);\n      nextProps = this._processProps(nextDescriptor.props);\n      this._pendingDescriptor = null;\n\n      this._compositeLifeCycleState = CompositeLifeCycle.RECEIVING_PROPS;\n      if (this.componentWillReceiveProps) {\n        this.componentWillReceiveProps(nextProps, nextContext);\n      }\n    }\n\n    this._compositeLifeCycleState = CompositeLifeCycle.RECEIVING_STATE;\n\n    var nextState = this._pendingState || this.state;\n    this._pendingState = null;\n\n    try {\n      var shouldUpdate =\n        this._pendingForceUpdate ||\n        !this.shouldComponentUpdate ||\n        this.shouldComponentUpdate(nextProps, nextState, nextContext);\n\n      if (\"production\" !== \"development\") {\n        if (typeof shouldUpdate === \"undefined\") {\n          console.warn(\n            (this.constructor.displayName || 'ReactCompositeComponent') +\n            '.shouldComponentUpdate(): Returned undefined instead of a ' +\n            'boolean value. Make sure to return true or false.'\n          );\n        }\n      }\n\n      if (shouldUpdate) {\n        this._pendingForceUpdate = false;\n        // Will set `this.props`, `this.state` and `this.context`.\n        this._performComponentUpdate(\n          nextDescriptor,\n          nextProps,\n          nextState,\n          nextContext,\n          transaction\n        );\n      } else {\n        // If it's determined that a component should not update, we still want\n        // to set props and state.\n        this._descriptor = nextDescriptor;\n        this.props = nextProps;\n        this.state = nextState;\n        this.context = nextContext;\n\n        // Owner cannot change because shouldUpdateReactComponent doesn't allow\n        // it. TODO: Remove this._owner completely.\n        this._owner = nextDescriptor._owner;\n      }\n    } finally {\n      this._compositeLifeCycleState = null;\n    }\n  },\n\n  /**\n   * Merges new props and state, notifies delegate methods of update and\n   * performs update.\n   *\n   * @param {ReactDescriptor} nextDescriptor Next descriptor\n   * @param {object} nextProps Next public object to set as properties.\n   * @param {?object} nextState Next object to set as state.\n   * @param {?object} nextContext Next public object to set as context.\n   * @param {ReactReconcileTransaction} transaction\n   * @private\n   */\n  _performComponentUpdate: function(\n    nextDescriptor,\n    nextProps,\n    nextState,\n    nextContext,\n    transaction\n  ) {\n    var prevDescriptor = this._descriptor;\n    var prevProps = this.props;\n    var prevState = this.state;\n    var prevContext = this.context;\n\n    if (this.componentWillUpdate) {\n      this.componentWillUpdate(nextProps, nextState, nextContext);\n    }\n\n    this._descriptor = nextDescriptor;\n    this.props = nextProps;\n    this.state = nextState;\n    this.context = nextContext;\n\n    // Owner cannot change because shouldUpdateReactComponent doesn't allow\n    // it. TODO: Remove this._owner completely.\n    this._owner = nextDescriptor._owner;\n\n    this.updateComponent(\n      transaction,\n      prevDescriptor\n    );\n\n    if (this.componentDidUpdate) {\n      transaction.getReactMountReady().enqueue(\n        this.componentDidUpdate.bind(this, prevProps, prevState, prevContext),\n        this\n      );\n    }\n  },\n\n  receiveComponent: function(nextDescriptor, transaction) {\n    if (nextDescriptor === this._descriptor &&\n        nextDescriptor._owner != null) {\n      // Since descriptors are immutable after the owner is rendered,\n      // we can do a cheap identity compare here to determine if this is a\n      // superfluous reconcile. It's possible for state to be mutable but such\n      // change should trigger an update of the owner which would recreate\n      // the descriptor. We explicitly check for the existence of an owner since\n      // it's possible for a descriptor created outside a composite to be\n      // deeply mutated and reused.\n      return;\n    }\n\n    ReactComponent.Mixin.receiveComponent.call(\n      this,\n      nextDescriptor,\n      transaction\n    );\n  },\n\n  /**\n   * Updates the component's currently mounted DOM representation.\n   *\n   * By default, this implements React's rendering and reconciliation algorithm.\n   * Sophisticated clients may wish to override this.\n   *\n   * @param {ReactReconcileTransaction} transaction\n   * @param {ReactDescriptor} prevDescriptor\n   * @internal\n   * @overridable\n   */\n  updateComponent: ReactPerf.measure(\n    'ReactCompositeComponent',\n    'updateComponent',\n    function(transaction, prevParentDescriptor) {\n      ReactComponent.Mixin.updateComponent.call(\n        this,\n        transaction,\n        prevParentDescriptor\n      );\n\n      var prevComponentInstance = this._renderedComponent;\n      var prevDescriptor = prevComponentInstance._descriptor;\n      var nextDescriptor = this._renderValidatedComponent();\n      if (shouldUpdateReactComponent(prevDescriptor, nextDescriptor)) {\n        prevComponentInstance.receiveComponent(nextDescriptor, transaction);\n      } else {\n        // These two IDs are actually the same! But nothing should rely on that.\n        var thisID = this._rootNodeID;\n        var prevComponentID = prevComponentInstance._rootNodeID;\n        prevComponentInstance.unmountComponent();\n        this._renderedComponent = instantiateReactComponent(nextDescriptor);\n        var nextMarkup = this._renderedComponent.mountComponent(\n          thisID,\n          transaction,\n          this._mountDepth + 1\n        );\n        ReactComponent.BackendIDOperations.dangerouslyReplaceNodeWithMarkupByID(\n          prevComponentID,\n          nextMarkup\n        );\n      }\n    }\n  ),\n\n  /**\n   * Forces an update. This should only be invoked when it is known with\n   * certainty that we are **not** in a DOM transaction.\n   *\n   * You may want to call this when you know that some deeper aspect of the\n   * component's state has changed but `setState` was not called.\n   *\n   * This will not invoke `shouldUpdateComponent`, but it will invoke\n   * `componentWillUpdate` and `componentDidUpdate`.\n   *\n   * @param {?function} callback Called after update is complete.\n   * @final\n   * @protected\n   */\n  forceUpdate: function(callback) {\n    var compositeLifeCycleState = this._compositeLifeCycleState;\n    (\"production\" !== \"development\" ? invariant(\n      this.isMounted() ||\n        compositeLifeCycleState === CompositeLifeCycle.MOUNTING,\n      'forceUpdate(...): Can only force an update on mounted or mounting ' +\n        'components.'\n    ) : invariant(this.isMounted() ||\n      compositeLifeCycleState === CompositeLifeCycle.MOUNTING));\n    (\"production\" !== \"development\" ? invariant(\n      compositeLifeCycleState !== CompositeLifeCycle.RECEIVING_STATE &&\n      compositeLifeCycleState !== CompositeLifeCycle.UNMOUNTING,\n      'forceUpdate(...): Cannot force an update while unmounting component ' +\n      'or during an existing state transition (such as within `render`).'\n    ) : invariant(compositeLifeCycleState !== CompositeLifeCycle.RECEIVING_STATE &&\n    compositeLifeCycleState !== CompositeLifeCycle.UNMOUNTING));\n    this._pendingForceUpdate = true;\n    ReactUpdates.enqueueUpdate(this, callback);\n  },\n\n  /**\n   * @private\n   */\n  _renderValidatedComponent: ReactPerf.measure(\n    'ReactCompositeComponent',\n    '_renderValidatedComponent',\n    function() {\n      var renderedComponent;\n      var previousContext = ReactContext.current;\n      ReactContext.current = this._processChildContext(\n        this._descriptor._context\n      );\n      ReactCurrentOwner.current = this;\n      try {\n        renderedComponent = this.render();\n        if (renderedComponent === null || renderedComponent === false) {\n          renderedComponent = ReactEmptyComponent.getEmptyComponent();\n          ReactEmptyComponent.registerNullComponentID(this._rootNodeID);\n        } else {\n          ReactEmptyComponent.deregisterNullComponentID(this._rootNodeID);\n        }\n      } finally {\n        ReactContext.current = previousContext;\n        ReactCurrentOwner.current = null;\n      }\n      (\"production\" !== \"development\" ? invariant(\n        ReactDescriptor.isValidDescriptor(renderedComponent),\n        '%s.render(): A valid ReactComponent must be returned. You may have ' +\n          'returned undefined, an array or some other invalid object.',\n        this.constructor.displayName || 'ReactCompositeComponent'\n      ) : invariant(ReactDescriptor.isValidDescriptor(renderedComponent)));\n      return renderedComponent;\n    }\n  ),\n\n  /**\n   * @private\n   */\n  _bindAutoBindMethods: function() {\n    for (var autoBindKey in this.__reactAutoBindMap) {\n      if (!this.__reactAutoBindMap.hasOwnProperty(autoBindKey)) {\n        continue;\n      }\n      var method = this.__reactAutoBindMap[autoBindKey];\n      this[autoBindKey] = this._bindAutoBindMethod(ReactErrorUtils.guard(\n        method,\n        this.constructor.displayName + '.' + autoBindKey\n      ));\n    }\n  },\n\n  /**\n   * Binds a method to the component.\n   *\n   * @param {function} method Method to be bound.\n   * @private\n   */\n  _bindAutoBindMethod: function(method) {\n    var component = this;\n    var boundMethod = function() {\n      return method.apply(component, arguments);\n    };\n    if (\"production\" !== \"development\") {\n      boundMethod.__reactBoundContext = component;\n      boundMethod.__reactBoundMethod = method;\n      boundMethod.__reactBoundArguments = null;\n      var componentName = component.constructor.displayName;\n      var _bind = boundMethod.bind;\n      boundMethod.bind = function(newThis ) {var args=Array.prototype.slice.call(arguments,1);\n        // User is trying to bind() an autobound method; we effectively will\n        // ignore the value of \"this\" that the user is trying to use, so\n        // let's warn.\n        if (newThis !== component && newThis !== null) {\n          monitorCodeUse('react_bind_warning', { component: componentName });\n          console.warn(\n            'bind(): React component methods may only be bound to the ' +\n            'component instance. See ' + componentName\n          );\n        } else if (!args.length) {\n          monitorCodeUse('react_bind_warning', { component: componentName });\n          console.warn(\n            'bind(): You are binding a component method to the component. ' +\n            'React does this for you automatically in a high-performance ' +\n            'way, so you can safely remove this call. See ' + componentName\n          );\n          return boundMethod;\n        }\n        var reboundMethod = _bind.apply(boundMethod, arguments);\n        reboundMethod.__reactBoundContext = component;\n        reboundMethod.__reactBoundMethod = method;\n        reboundMethod.__reactBoundArguments = args;\n        return reboundMethod;\n      };\n    }\n    return boundMethod;\n  }\n};\n\nvar ReactCompositeComponentBase = function() {};\nmixInto(ReactCompositeComponentBase, ReactComponent.Mixin);\nmixInto(ReactCompositeComponentBase, ReactOwner.Mixin);\nmixInto(ReactCompositeComponentBase, ReactPropTransferer.Mixin);\nmixInto(ReactCompositeComponentBase, ReactCompositeComponentMixin);\n\n/**\n * Module for creating composite components.\n *\n * @class ReactCompositeComponent\n * @extends ReactComponent\n * @extends ReactOwner\n * @extends ReactPropTransferer\n */\nvar ReactCompositeComponent = {\n\n  LifeCycle: CompositeLifeCycle,\n\n  Base: ReactCompositeComponentBase,\n\n  /**\n   * Creates a composite component class given a class specification.\n   *\n   * @param {object} spec Class specification (which must define `render`).\n   * @return {function} Component constructor function.\n   * @public\n   */\n  createClass: function(spec) {\n    var Constructor = function(props, owner) {\n      this.construct(props, owner);\n    };\n    Constructor.prototype = new ReactCompositeComponentBase();\n    Constructor.prototype.constructor = Constructor;\n\n    injectedMixins.forEach(\n      mixSpecIntoComponent.bind(null, Constructor)\n    );\n\n    mixSpecIntoComponent(Constructor, spec);\n\n    // Initialize the defaultProps property after all mixins have been merged\n    if (Constructor.getDefaultProps) {\n      Constructor.defaultProps = Constructor.getDefaultProps();\n    }\n\n    (\"production\" !== \"development\" ? invariant(\n      Constructor.prototype.render,\n      'createClass(...): Class specification must implement a `render` method.'\n    ) : invariant(Constructor.prototype.render));\n\n    if (\"production\" !== \"development\") {\n      if (Constructor.prototype.componentShouldUpdate) {\n        monitorCodeUse(\n          'react_component_should_update_warning',\n          { component: spec.displayName }\n        );\n        console.warn(\n          (spec.displayName || 'A component') + ' has a method called ' +\n          'componentShouldUpdate(). Did you mean shouldComponentUpdate()? ' +\n          'The name is phrased as a question because the function is ' +\n          'expected to return a value.'\n         );\n      }\n    }\n\n    // Reduce time spent doing lookups by setting these on the prototype.\n    for (var methodName in ReactCompositeComponentInterface) {\n      if (!Constructor.prototype[methodName]) {\n        Constructor.prototype[methodName] = null;\n      }\n    }\n\n    var descriptorFactory = ReactDescriptor.createFactory(Constructor);\n\n    if (\"production\" !== \"development\") {\n      return ReactDescriptorValidator.createFactory(\n        descriptorFactory,\n        Constructor.propTypes,\n        Constructor.contextTypes\n      );\n    }\n\n    return descriptorFactory;\n  },\n\n  injection: {\n    injectMixin: function(mixin) {\n      injectedMixins.push(mixin);\n    }\n  }\n};\n\nmodule.exports = ReactCompositeComponent;\n\n},{\"./ReactComponent\":35,\"./ReactContext\":39,\"./ReactCurrentOwner\":40,\"./ReactDescriptor\":56,\"./ReactDescriptorValidator\":57,\"./ReactEmptyComponent\":58,\"./ReactErrorUtils\":59,\"./ReactOwner\":70,\"./ReactPerf\":71,\"./ReactPropTransferer\":72,\"./ReactPropTypeLocationNames\":73,\"./ReactPropTypeLocations\":74,\"./ReactUpdates\":87,\"./instantiateReactComponent\":133,\"./invariant\":134,\"./keyMirror\":140,\"./mapObject\":142,\"./merge\":144,\"./mixInto\":147,\"./monitorCodeUse\":148,\"./shouldUpdateReactComponent\":154,\"./warning\":158}],39:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule ReactContext\n */\n\n\"use strict\";\n\nvar merge = _dereq_(\"./merge\");\n\n/**\n * Keeps track of the current context.\n *\n * The context is automatically passed down the component ownership hierarchy\n * and is accessible via `this.context` on ReactCompositeComponents.\n */\nvar ReactContext = {\n\n  /**\n   * @internal\n   * @type {object}\n   */\n  current: {},\n\n  /**\n   * Temporarily extends the current context while executing scopedCallback.\n   *\n   * A typical use case might look like\n   *\n   *  render: function() {\n   *    var children = ReactContext.withContext({foo: 'foo'} () => (\n   *\n   *    ));\n   *    return <div>{children}</div>;\n   *  }\n   *\n   * @param {object} newContext New context to merge into the existing context\n   * @param {function} scopedCallback Callback to run with the new context\n   * @return {ReactComponent|array<ReactComponent>}\n   */\n  withContext: function(newContext, scopedCallback) {\n    var result;\n    var previousContext = ReactContext.current;\n    ReactContext.current = merge(previousContext, newContext);\n    try {\n      result = scopedCallback();\n    } finally {\n      ReactContext.current = previousContext;\n    }\n    return result;\n  }\n\n};\n\nmodule.exports = ReactContext;\n\n},{\"./merge\":144}],40:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule ReactCurrentOwner\n */\n\n\"use strict\";\n\n/**\n * Keeps track of the current owner.\n *\n * The current owner is the component who should own any components that are\n * currently being constructed.\n *\n * The depth indicate how many composite components are above this render level.\n */\nvar ReactCurrentOwner = {\n\n  /**\n   * @internal\n   * @type {ReactComponent}\n   */\n  current: null\n\n};\n\nmodule.exports = ReactCurrentOwner;\n\n},{}],41:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule ReactDOM\n * @typechecks static-only\n */\n\n\"use strict\";\n\nvar ReactDescriptor = _dereq_(\"./ReactDescriptor\");\nvar ReactDescriptorValidator = _dereq_(\"./ReactDescriptorValidator\");\nvar ReactDOMComponent = _dereq_(\"./ReactDOMComponent\");\n\nvar mergeInto = _dereq_(\"./mergeInto\");\nvar mapObject = _dereq_(\"./mapObject\");\n\n/**\n * Creates a new React class that is idempotent and capable of containing other\n * React components. It accepts event listeners and DOM properties that are\n * valid according to `DOMProperty`.\n *\n *  - Event listeners: `onClick`, `onMouseDown`, etc.\n *  - DOM properties: `className`, `name`, `title`, etc.\n *\n * The `style` property functions differently from the DOM API. It accepts an\n * object mapping of style properties to values.\n *\n * @param {boolean} omitClose True if the close tag should be omitted.\n * @param {string} tag Tag name (e.g. `div`).\n * @private\n */\nfunction createDOMComponentClass(omitClose, tag) {\n  var Constructor = function(descriptor) {\n    this.construct(descriptor);\n  };\n  Constructor.prototype = new ReactDOMComponent(tag, omitClose);\n  Constructor.prototype.constructor = Constructor;\n  Constructor.displayName = tag;\n\n  var ConvenienceConstructor = ReactDescriptor.createFactory(Constructor);\n\n  if (\"production\" !== \"development\") {\n    return ReactDescriptorValidator.createFactory(\n      ConvenienceConstructor\n    );\n  }\n\n  return ConvenienceConstructor;\n}\n\n/**\n * Creates a mapping from supported HTML tags to `ReactDOMComponent` classes.\n * This is also accessible via `React.DOM`.\n *\n * @public\n */\nvar ReactDOM = mapObject({\n  a: false,\n  abbr: false,\n  address: false,\n  area: true,\n  article: false,\n  aside: false,\n  audio: false,\n  b: false,\n  base: true,\n  bdi: false,\n  bdo: false,\n  big: false,\n  blockquote: false,\n  body: false,\n  br: true,\n  button: false,\n  canvas: false,\n  caption: false,\n  cite: false,\n  code: false,\n  col: true,\n  colgroup: false,\n  data: false,\n  datalist: false,\n  dd: false,\n  del: false,\n  details: false,\n  dfn: false,\n  dialog: false,\n  div: false,\n  dl: false,\n  dt: false,\n  em: false,\n  embed: true,\n  fieldset: false,\n  figcaption: false,\n  figure: false,\n  footer: false,\n  form: false, // NOTE: Injected, see `ReactDOMForm`.\n  h1: false,\n  h2: false,\n  h3: false,\n  h4: false,\n  h5: false,\n  h6: false,\n  head: false,\n  header: false,\n  hr: true,\n  html: false,\n  i: false,\n  iframe: false,\n  img: true,\n  input: true,\n  ins: false,\n  kbd: false,\n  keygen: true,\n  label: false,\n  legend: false,\n  li: false,\n  link: true,\n  main: false,\n  map: false,\n  mark: false,\n  menu: false,\n  menuitem: false, // NOTE: Close tag should be omitted, but causes problems.\n  meta: true,\n  meter: false,\n  nav: false,\n  noscript: false,\n  object: false,\n  ol: false,\n  optgroup: false,\n  option: false,\n  output: false,\n  p: false,\n  param: true,\n  picture: false,\n  pre: false,\n  progress: false,\n  q: false,\n  rp: false,\n  rt: false,\n  ruby: false,\n  s: false,\n  samp: false,\n  script: false,\n  section: false,\n  select: false,\n  small: false,\n  source: true,\n  span: false,\n  strong: false,\n  style: false,\n  sub: false,\n  summary: false,\n  sup: false,\n  table: false,\n  tbody: false,\n  td: false,\n  textarea: false, // NOTE: Injected, see `ReactDOMTextarea`.\n  tfoot: false,\n  th: false,\n  thead: false,\n  time: false,\n  title: false,\n  tr: false,\n  track: true,\n  u: false,\n  ul: false,\n  'var': false,\n  video: false,\n  wbr: true,\n\n  // SVG\n  circle: false,\n  defs: false,\n  ellipse: false,\n  g: false,\n  line: false,\n  linearGradient: false,\n  mask: false,\n  path: false,\n  pattern: false,\n  polygon: false,\n  polyline: false,\n  radialGradient: false,\n  rect: false,\n  stop: false,\n  svg: false,\n  text: false,\n  tspan: false\n}, createDOMComponentClass);\n\nvar injection = {\n  injectComponentClasses: function(componentClasses) {\n    mergeInto(ReactDOM, componentClasses);\n  }\n};\n\nReactDOM.injection = injection;\n\nmodule.exports = ReactDOM;\n\n},{\"./ReactDOMComponent\":43,\"./ReactDescriptor\":56,\"./ReactDescriptorValidator\":57,\"./mapObject\":142,\"./mergeInto\":146}],42:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule ReactDOMButton\n */\n\n\"use strict\";\n\nvar AutoFocusMixin = _dereq_(\"./AutoFocusMixin\");\nvar ReactBrowserComponentMixin = _dereq_(\"./ReactBrowserComponentMixin\");\nvar ReactCompositeComponent = _dereq_(\"./ReactCompositeComponent\");\nvar ReactDOM = _dereq_(\"./ReactDOM\");\n\nvar keyMirror = _dereq_(\"./keyMirror\");\n\n// Store a reference to the <button> `ReactDOMComponent`.\nvar button = ReactDOM.button;\n\nvar mouseListenerNames = keyMirror({\n  onClick: true,\n  onDoubleClick: true,\n  onMouseDown: true,\n  onMouseMove: true,\n  onMouseUp: true,\n  onClickCapture: true,\n  onDoubleClickCapture: true,\n  onMouseDownCapture: true,\n  onMouseMoveCapture: true,\n  onMouseUpCapture: true\n});\n\n/**\n * Implements a <button> native component that does not receive mouse events\n * when `disabled` is set.\n */\nvar ReactDOMButton = ReactCompositeComponent.createClass({\n  displayName: 'ReactDOMButton',\n\n  mixins: [AutoFocusMixin, ReactBrowserComponentMixin],\n\n  render: function() {\n    var props = {};\n\n    // Copy the props; except the mouse listeners if we're disabled\n    for (var key in this.props) {\n      if (this.props.hasOwnProperty(key) &&\n          (!this.props.disabled || !mouseListenerNames[key])) {\n        props[key] = this.props[key];\n      }\n    }\n\n    return button(props, this.props.children);\n  }\n\n});\n\nmodule.exports = ReactDOMButton;\n\n},{\"./AutoFocusMixin\":1,\"./ReactBrowserComponentMixin\":30,\"./ReactCompositeComponent\":38,\"./ReactDOM\":41,\"./keyMirror\":140}],43:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule ReactDOMComponent\n * @typechecks static-only\n */\n\n\"use strict\";\n\nvar CSSPropertyOperations = _dereq_(\"./CSSPropertyOperations\");\nvar DOMProperty = _dereq_(\"./DOMProperty\");\nvar DOMPropertyOperations = _dereq_(\"./DOMPropertyOperations\");\nvar ReactBrowserComponentMixin = _dereq_(\"./ReactBrowserComponentMixin\");\nvar ReactComponent = _dereq_(\"./ReactComponent\");\nvar ReactBrowserEventEmitter = _dereq_(\"./ReactBrowserEventEmitter\");\nvar ReactMount = _dereq_(\"./ReactMount\");\nvar ReactMultiChild = _dereq_(\"./ReactMultiChild\");\nvar ReactPerf = _dereq_(\"./ReactPerf\");\n\nvar escapeTextForBrowser = _dereq_(\"./escapeTextForBrowser\");\nvar invariant = _dereq_(\"./invariant\");\nvar keyOf = _dereq_(\"./keyOf\");\nvar merge = _dereq_(\"./merge\");\nvar mixInto = _dereq_(\"./mixInto\");\n\nvar deleteListener = ReactBrowserEventEmitter.deleteListener;\nvar listenTo = ReactBrowserEventEmitter.listenTo;\nvar registrationNameModules = ReactBrowserEventEmitter.registrationNameModules;\n\n// For quickly matching children type, to test if can be treated as content.\nvar CONTENT_TYPES = {'string': true, 'number': true};\n\nvar STYLE = keyOf({style: null});\n\nvar ELEMENT_NODE_TYPE = 1;\n\n/**\n * @param {?object} props\n */\nfunction assertValidProps(props) {\n  if (!props) {\n    return;\n  }\n  // Note the use of `==` which checks for null or undefined.\n  (\"production\" !== \"development\" ? invariant(\n    props.children == null || props.dangerouslySetInnerHTML == null,\n    'Can only set one of `children` or `props.dangerouslySetInnerHTML`.'\n  ) : invariant(props.children == null || props.dangerouslySetInnerHTML == null));\n  (\"production\" !== \"development\" ? invariant(\n    props.style == null || typeof props.style === 'object',\n    'The `style` prop expects a mapping from style properties to values, ' +\n    'not a string.'\n  ) : invariant(props.style == null || typeof props.style === 'object'));\n}\n\nfunction putListener(id, registrationName, listener, transaction) {\n  var container = ReactMount.findReactContainerForID(id);\n  if (container) {\n    var doc = container.nodeType === ELEMENT_NODE_TYPE ?\n      container.ownerDocument :\n      container;\n    listenTo(registrationName, doc);\n  }\n  transaction.getPutListenerQueue().enqueuePutListener(\n    id,\n    registrationName,\n    listener\n  );\n}\n\n\n/**\n * @constructor ReactDOMComponent\n * @extends ReactComponent\n * @extends ReactMultiChild\n */\nfunction ReactDOMComponent(tag, omitClose) {\n  this._tagOpen = '<' + tag;\n  this._tagClose = omitClose ? '' : '</' + tag + '>';\n  this.tagName = tag.toUpperCase();\n}\n\nReactDOMComponent.Mixin = {\n\n  /**\n   * Generates root tag markup then recurses. This method has side effects and\n   * is not idempotent.\n   *\n   * @internal\n   * @param {string} rootID The root DOM ID for this node.\n   * @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction\n   * @param {number} mountDepth number of components in the owner hierarchy\n   * @return {string} The computed markup.\n   */\n  mountComponent: ReactPerf.measure(\n    'ReactDOMComponent',\n    'mountComponent',\n    function(rootID, transaction, mountDepth) {\n      ReactComponent.Mixin.mountComponent.call(\n        this,\n        rootID,\n        transaction,\n        mountDepth\n      );\n      assertValidProps(this.props);\n      return (\n        this._createOpenTagMarkupAndPutListeners(transaction) +\n        this._createContentMarkup(transaction) +\n        this._tagClose\n      );\n    }\n  ),\n\n  /**\n   * Creates markup for the open tag and all attributes.\n   *\n   * This method has side effects because events get registered.\n   *\n   * Iterating over object properties is faster than iterating over arrays.\n   * @see http://jsperf.com/obj-vs-arr-iteration\n   *\n   * @private\n   * @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction\n   * @return {string} Markup of opening tag.\n   */\n  _createOpenTagMarkupAndPutListeners: function(transaction) {\n    var props = this.props;\n    var ret = this._tagOpen;\n\n    for (var propKey in props) {\n      if (!props.hasOwnProperty(propKey)) {\n        continue;\n      }\n      var propValue = props[propKey];\n      if (propValue == null) {\n        continue;\n      }\n      if (registrationNameModules.hasOwnProperty(propKey)) {\n        putListener(this._rootNodeID, propKey, propValue, transaction);\n      } else {\n        if (propKey === STYLE) {\n          if (propValue) {\n            propValue = props.style = merge(props.style);\n          }\n          propValue = CSSPropertyOperations.createMarkupForStyles(propValue);\n        }\n        var markup =\n          DOMPropertyOperations.createMarkupForProperty(propKey, propValue);\n        if (markup) {\n          ret += ' ' + markup;\n        }\n      }\n    }\n\n    // For static pages, no need to put React ID and checksum. Saves lots of\n    // bytes.\n    if (transaction.renderToStaticMarkup) {\n      return ret + '>';\n    }\n\n    var markupForID = DOMPropertyOperations.createMarkupForID(this._rootNodeID);\n    return ret + ' ' + markupForID + '>';\n  },\n\n  /**\n   * Creates markup for the content between the tags.\n   *\n   * @private\n   * @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction\n   * @return {string} Content markup.\n   */\n  _createContentMarkup: function(transaction) {\n    // Intentional use of != to avoid catching zero/false.\n    var innerHTML = this.props.dangerouslySetInnerHTML;\n    if (innerHTML != null) {\n      if (innerHTML.__html != null) {\n        return innerHTML.__html;\n      }\n    } else {\n      var contentToUse =\n        CONTENT_TYPES[typeof this.props.children] ? this.props.children : null;\n      var childrenToUse = contentToUse != null ? null : this.props.children;\n      if (contentToUse != null) {\n        return escapeTextForBrowser(contentToUse);\n      } else if (childrenToUse != null) {\n        var mountImages = this.mountChildren(\n          childrenToUse,\n          transaction\n        );\n        return mountImages.join('');\n      }\n    }\n    return '';\n  },\n\n  receiveComponent: function(nextDescriptor, transaction) {\n    if (nextDescriptor === this._descriptor &&\n        nextDescriptor._owner != null) {\n      // Since descriptors are immutable after the owner is rendered,\n      // we can do a cheap identity compare here to determine if this is a\n      // superfluous reconcile. It's possible for state to be mutable but such\n      // change should trigger an update of the owner which would recreate\n      // the descriptor. We explicitly check for the existence of an owner since\n      // it's possible for a descriptor created outside a composite to be\n      // deeply mutated and reused.\n      return;\n    }\n\n    ReactComponent.Mixin.receiveComponent.call(\n      this,\n      nextDescriptor,\n      transaction\n    );\n  },\n\n  /**\n   * Updates a native DOM component after it has already been allocated and\n   * attached to the DOM. Reconciles the root DOM node, then recurses.\n   *\n   * @param {ReactReconcileTransaction} transaction\n   * @param {ReactDescriptor} prevDescriptor\n   * @internal\n   * @overridable\n   */\n  updateComponent: ReactPerf.measure(\n    'ReactDOMComponent',\n    'updateComponent',\n    function(transaction, prevDescriptor) {\n      assertValidProps(this._descriptor.props);\n      ReactComponent.Mixin.updateComponent.call(\n        this,\n        transaction,\n        prevDescriptor\n      );\n      this._updateDOMProperties(prevDescriptor.props, transaction);\n      this._updateDOMChildren(prevDescriptor.props, transaction);\n    }\n  ),\n\n  /**\n   * Reconciles the properties by detecting differences in property values and\n   * updating the DOM as necessary. This function is probably the single most\n   * critical path for performance optimization.\n   *\n   * TODO: Benchmark whether checking for changed values in memory actually\n   *       improves performance (especially statically positioned elements).\n   * TODO: Benchmark the effects of putting this at the top since 99% of props\n   *       do not change for a given reconciliation.\n   * TODO: Benchmark areas that can be improved with caching.\n   *\n   * @private\n   * @param {object} lastProps\n   * @param {ReactReconcileTransaction} transaction\n   */\n  _updateDOMProperties: function(lastProps, transaction) {\n    var nextProps = this.props;\n    var propKey;\n    var styleName;\n    var styleUpdates;\n    for (propKey in lastProps) {\n      if (nextProps.hasOwnProperty(propKey) ||\n         !lastProps.hasOwnProperty(propKey)) {\n        continue;\n      }\n      if (propKey === STYLE) {\n        var lastStyle = lastProps[propKey];\n        for (styleName in lastStyle) {\n          if (lastStyle.hasOwnProperty(styleName)) {\n            styleUpdates = styleUpdates || {};\n            styleUpdates[styleName] = '';\n          }\n        }\n      } else if (registrationNameModules.hasOwnProperty(propKey)) {\n        deleteListener(this._rootNodeID, propKey);\n      } else if (\n          DOMProperty.isStandardName[propKey] ||\n          DOMProperty.isCustomAttribute(propKey)) {\n        ReactComponent.BackendIDOperations.deletePropertyByID(\n          this._rootNodeID,\n          propKey\n        );\n      }\n    }\n    for (propKey in nextProps) {\n      var nextProp = nextProps[propKey];\n      var lastProp = lastProps[propKey];\n      if (!nextProps.hasOwnProperty(propKey) || nextProp === lastProp) {\n        continue;\n      }\n      if (propKey === STYLE) {\n        if (nextProp) {\n          nextProp = nextProps.style = merge(nextProp);\n        }\n        if (lastProp) {\n          // Unset styles on `lastProp` but not on `nextProp`.\n          for (styleName in lastProp) {\n            if (lastProp.hasOwnProperty(styleName) &&\n                (!nextProp || !nextProp.hasOwnProperty(styleName))) {\n              styleUpdates = styleUpdates || {};\n              styleUpdates[styleName] = '';\n            }\n          }\n          // Update styles that changed since `lastProp`.\n          for (styleName in nextProp) {\n            if (nextProp.hasOwnProperty(styleName) &&\n                lastProp[styleName] !== nextProp[styleName]) {\n              styleUpdates = styleUpdates || {};\n              styleUpdates[styleName] = nextProp[styleName];\n            }\n          }\n        } else {\n          // Relies on `updateStylesByID` not mutating `styleUpdates`.\n          styleUpdates = nextProp;\n        }\n      } else if (registrationNameModules.hasOwnProperty(propKey)) {\n        putListener(this._rootNodeID, propKey, nextProp, transaction);\n      } else if (\n          DOMProperty.isStandardName[propKey] ||\n          DOMProperty.isCustomAttribute(propKey)) {\n        ReactComponent.BackendIDOperations.updatePropertyByID(\n          this._rootNodeID,\n          propKey,\n          nextProp\n        );\n      }\n    }\n    if (styleUpdates) {\n      ReactComponent.BackendIDOperations.updateStylesByID(\n        this._rootNodeID,\n        styleUpdates\n      );\n    }\n  },\n\n  /**\n   * Reconciles the children with the various properties that affect the\n   * children content.\n   *\n   * @param {object} lastProps\n   * @param {ReactReconcileTransaction} transaction\n   */\n  _updateDOMChildren: function(lastProps, transaction) {\n    var nextProps = this.props;\n\n    var lastContent =\n      CONTENT_TYPES[typeof lastProps.children] ? lastProps.children : null;\n    var nextContent =\n      CONTENT_TYPES[typeof nextProps.children] ? nextProps.children : null;\n\n    var lastHtml =\n      lastProps.dangerouslySetInnerHTML &&\n      lastProps.dangerouslySetInnerHTML.__html;\n    var nextHtml =\n      nextProps.dangerouslySetInnerHTML &&\n      nextProps.dangerouslySetInnerHTML.__html;\n\n    // Note the use of `!=` which checks for null or undefined.\n    var lastChildren = lastContent != null ? null : lastProps.children;\n    var nextChildren = nextContent != null ? null : nextProps.children;\n\n    // If we're switching from children to content/html or vice versa, remove\n    // the old content\n    var lastHasContentOrHtml = lastContent != null || lastHtml != null;\n    var nextHasContentOrHtml = nextContent != null || nextHtml != null;\n    if (lastChildren != null && nextChildren == null) {\n      this.updateChildren(null, transaction);\n    } else if (lastHasContentOrHtml && !nextHasContentOrHtml) {\n      this.updateTextContent('');\n    }\n\n    if (nextContent != null) {\n      if (lastContent !== nextContent) {\n        this.updateTextContent('' + nextContent);\n      }\n    } else if (nextHtml != null) {\n      if (lastHtml !== nextHtml) {\n        ReactComponent.BackendIDOperations.updateInnerHTMLByID(\n          this._rootNodeID,\n          nextHtml\n        );\n      }\n    } else if (nextChildren != null) {\n      this.updateChildren(nextChildren, transaction);\n    }\n  },\n\n  /**\n   * Destroys all event registrations for this instance. Does not remove from\n   * the DOM. That must be done by the parent.\n   *\n   * @internal\n   */\n  unmountComponent: function() {\n    this.unmountChildren();\n    ReactBrowserEventEmitter.deleteAllListeners(this._rootNodeID);\n    ReactComponent.Mixin.unmountComponent.call(this);\n  }\n\n};\n\nmixInto(ReactDOMComponent, ReactComponent.Mixin);\nmixInto(ReactDOMComponent, ReactDOMComponent.Mixin);\nmixInto(ReactDOMComponent, ReactMultiChild.Mixin);\nmixInto(ReactDOMComponent, ReactBrowserComponentMixin);\n\nmodule.exports = ReactDOMComponent;\n\n},{\"./CSSPropertyOperations\":5,\"./DOMProperty\":11,\"./DOMPropertyOperations\":12,\"./ReactBrowserComponentMixin\":30,\"./ReactBrowserEventEmitter\":31,\"./ReactComponent\":35,\"./ReactMount\":67,\"./ReactMultiChild\":68,\"./ReactPerf\":71,\"./escapeTextForBrowser\":118,\"./invariant\":134,\"./keyOf\":141,\"./merge\":144,\"./mixInto\":147}],44:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule ReactDOMForm\n */\n\n\"use strict\";\n\nvar EventConstants = _dereq_(\"./EventConstants\");\nvar LocalEventTrapMixin = _dereq_(\"./LocalEventTrapMixin\");\nvar ReactBrowserComponentMixin = _dereq_(\"./ReactBrowserComponentMixin\");\nvar ReactCompositeComponent = _dereq_(\"./ReactCompositeComponent\");\nvar ReactDOM = _dereq_(\"./ReactDOM\");\n\n// Store a reference to the <form> `ReactDOMComponent`.\nvar form = ReactDOM.form;\n\n/**\n * Since onSubmit doesn't bubble OR capture on the top level in IE8, we need\n * to capture it on the <form> element itself. There are lots of hacks we could\n * do to accomplish this, but the most reliable is to make <form> a\n * composite component and use `componentDidMount` to attach the event handlers.\n */\nvar ReactDOMForm = ReactCompositeComponent.createClass({\n  displayName: 'ReactDOMForm',\n\n  mixins: [ReactBrowserComponentMixin, LocalEventTrapMixin],\n\n  render: function() {\n    // TODO: Instead of using `ReactDOM` directly, we should use JSX. However,\n    // `jshint` fails to parse JSX so in order for linting to work in the open\n    // source repo, we need to just use `ReactDOM.form`.\n    return this.transferPropsTo(form(null, this.props.children));\n  },\n\n  componentDidMount: function() {\n    this.trapBubbledEvent(EventConstants.topLevelTypes.topReset, 'reset');\n    this.trapBubbledEvent(EventConstants.topLevelTypes.topSubmit, 'submit');\n  }\n});\n\nmodule.exports = ReactDOMForm;\n\n},{\"./EventConstants\":16,\"./LocalEventTrapMixin\":26,\"./ReactBrowserComponentMixin\":30,\"./ReactCompositeComponent\":38,\"./ReactDOM\":41}],45:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule ReactDOMIDOperations\n * @typechecks static-only\n */\n\n/*jslint evil: true */\n\n\"use strict\";\n\nvar CSSPropertyOperations = _dereq_(\"./CSSPropertyOperations\");\nvar DOMChildrenOperations = _dereq_(\"./DOMChildrenOperations\");\nvar DOMPropertyOperations = _dereq_(\"./DOMPropertyOperations\");\nvar ReactMount = _dereq_(\"./ReactMount\");\nvar ReactPerf = _dereq_(\"./ReactPerf\");\n\nvar invariant = _dereq_(\"./invariant\");\nvar setInnerHTML = _dereq_(\"./setInnerHTML\");\n\n/**\n * Errors for properties that should not be updated with `updatePropertyById()`.\n *\n * @type {object}\n * @private\n */\nvar INVALID_PROPERTY_ERRORS = {\n  dangerouslySetInnerHTML:\n    '`dangerouslySetInnerHTML` must be set using `updateInnerHTMLByID()`.',\n  style: '`style` must be set using `updateStylesByID()`.'\n};\n\n/**\n * Operations used to process updates to DOM nodes. This is made injectable via\n * `ReactComponent.BackendIDOperations`.\n */\nvar ReactDOMIDOperations = {\n\n  /**\n   * Updates a DOM node with new property values. This should only be used to\n   * update DOM properties in `DOMProperty`.\n   *\n   * @param {string} id ID of the node to update.\n   * @param {string} name A valid property name, see `DOMProperty`.\n   * @param {*} value New value of the property.\n   * @internal\n   */\n  updatePropertyByID: ReactPerf.measure(\n    'ReactDOMIDOperations',\n    'updatePropertyByID',\n    function(id, name, value) {\n      var node = ReactMount.getNode(id);\n      (\"production\" !== \"development\" ? invariant(\n        !INVALID_PROPERTY_ERRORS.hasOwnProperty(name),\n        'updatePropertyByID(...): %s',\n        INVALID_PROPERTY_ERRORS[name]\n      ) : invariant(!INVALID_PROPERTY_ERRORS.hasOwnProperty(name)));\n\n      // If we're updating to null or undefined, we should remove the property\n      // from the DOM node instead of inadvertantly setting to a string. This\n      // brings us in line with the same behavior we have on initial render.\n      if (value != null) {\n        DOMPropertyOperations.setValueForProperty(node, name, value);\n      } else {\n        DOMPropertyOperations.deleteValueForProperty(node, name);\n      }\n    }\n  ),\n\n  /**\n   * Updates a DOM node to remove a property. This should only be used to remove\n   * DOM properties in `DOMProperty`.\n   *\n   * @param {string} id ID of the node to update.\n   * @param {string} name A property name to remove, see `DOMProperty`.\n   * @internal\n   */\n  deletePropertyByID: ReactPerf.measure(\n    'ReactDOMIDOperations',\n    'deletePropertyByID',\n    function(id, name, value) {\n      var node = ReactMount.getNode(id);\n      (\"production\" !== \"development\" ? invariant(\n        !INVALID_PROPERTY_ERRORS.hasOwnProperty(name),\n        'updatePropertyByID(...): %s',\n        INVALID_PROPERTY_ERRORS[name]\n      ) : invariant(!INVALID_PROPERTY_ERRORS.hasOwnProperty(name)));\n      DOMPropertyOperations.deleteValueForProperty(node, name, value);\n    }\n  ),\n\n  /**\n   * Updates a DOM node with new style values. If a value is specified as '',\n   * the corresponding style property will be unset.\n   *\n   * @param {string} id ID of the node to update.\n   * @param {object} styles Mapping from styles to values.\n   * @internal\n   */\n  updateStylesByID: ReactPerf.measure(\n    'ReactDOMIDOperations',\n    'updateStylesByID',\n    function(id, styles) {\n      var node = ReactMount.getNode(id);\n      CSSPropertyOperations.setValueForStyles(node, styles);\n    }\n  ),\n\n  /**\n   * Updates a DOM node's innerHTML.\n   *\n   * @param {string} id ID of the node to update.\n   * @param {string} html An HTML string.\n   * @internal\n   */\n  updateInnerHTMLByID: ReactPerf.measure(\n    'ReactDOMIDOperations',\n    'updateInnerHTMLByID',\n    function(id, html) {\n      var node = ReactMount.getNode(id);\n      setInnerHTML(node, html);\n    }\n  ),\n\n  /**\n   * Updates a DOM node's text content set by `props.content`.\n   *\n   * @param {string} id ID of the node to update.\n   * @param {string} content Text content.\n   * @internal\n   */\n  updateTextContentByID: ReactPerf.measure(\n    'ReactDOMIDOperations',\n    'updateTextContentByID',\n    function(id, content) {\n      var node = ReactMount.getNode(id);\n      DOMChildrenOperations.updateTextContent(node, content);\n    }\n  ),\n\n  /**\n   * Replaces a DOM node that exists in the document with markup.\n   *\n   * @param {string} id ID of child to be replaced.\n   * @param {string} markup Dangerous markup to inject in place of child.\n   * @internal\n   * @see {Danger.dangerouslyReplaceNodeWithMarkup}\n   */\n  dangerouslyReplaceNodeWithMarkupByID: ReactPerf.measure(\n    'ReactDOMIDOperations',\n    'dangerouslyReplaceNodeWithMarkupByID',\n    function(id, markup) {\n      var node = ReactMount.getNode(id);\n      DOMChildrenOperations.dangerouslyReplaceNodeWithMarkup(node, markup);\n    }\n  ),\n\n  /**\n   * Updates a component's children by processing a series of updates.\n   *\n   * @param {array<object>} updates List of update configurations.\n   * @param {array<string>} markup List of markup strings.\n   * @internal\n   */\n  dangerouslyProcessChildrenUpdates: ReactPerf.measure(\n    'ReactDOMIDOperations',\n    'dangerouslyProcessChildrenUpdates',\n    function(updates, markup) {\n      for (var i = 0; i < updates.length; i++) {\n        updates[i].parentNode = ReactMount.getNode(updates[i].parentID);\n      }\n      DOMChildrenOperations.processUpdates(updates, markup);\n    }\n  )\n};\n\nmodule.exports = ReactDOMIDOperations;\n\n},{\"./CSSPropertyOperations\":5,\"./DOMChildrenOperations\":10,\"./DOMPropertyOperations\":12,\"./ReactMount\":67,\"./ReactPerf\":71,\"./invariant\":134,\"./setInnerHTML\":152}],46:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule ReactDOMImg\n */\n\n\"use strict\";\n\nvar EventConstants = _dereq_(\"./EventConstants\");\nvar LocalEventTrapMixin = _dereq_(\"./LocalEventTrapMixin\");\nvar ReactBrowserComponentMixin = _dereq_(\"./ReactBrowserComponentMixin\");\nvar ReactCompositeComponent = _dereq_(\"./ReactCompositeComponent\");\nvar ReactDOM = _dereq_(\"./ReactDOM\");\n\n// Store a reference to the <img> `ReactDOMComponent`.\nvar img = ReactDOM.img;\n\n/**\n * Since onLoad doesn't bubble OR capture on the top level in IE8, we need to\n * capture it on the <img> element itself. There are lots of hacks we could do\n * to accomplish this, but the most reliable is to make <img> a composite\n * component and use `componentDidMount` to attach the event handlers.\n */\nvar ReactDOMImg = ReactCompositeComponent.createClass({\n  displayName: 'ReactDOMImg',\n  tagName: 'IMG',\n\n  mixins: [ReactBrowserComponentMixin, LocalEventTrapMixin],\n\n  render: function() {\n    return img(this.props);\n  },\n\n  componentDidMount: function() {\n    this.trapBubbledEvent(EventConstants.topLevelTypes.topLoad, 'load');\n    this.trapBubbledEvent(EventConstants.topLevelTypes.topError, 'error');\n  }\n});\n\nmodule.exports = ReactDOMImg;\n\n},{\"./EventConstants\":16,\"./LocalEventTrapMixin\":26,\"./ReactBrowserComponentMixin\":30,\"./ReactCompositeComponent\":38,\"./ReactDOM\":41}],47:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule ReactDOMInput\n */\n\n\"use strict\";\n\nvar AutoFocusMixin = _dereq_(\"./AutoFocusMixin\");\nvar DOMPropertyOperations = _dereq_(\"./DOMPropertyOperations\");\nvar LinkedValueUtils = _dereq_(\"./LinkedValueUtils\");\nvar ReactBrowserComponentMixin = _dereq_(\"./ReactBrowserComponentMixin\");\nvar ReactCompositeComponent = _dereq_(\"./ReactCompositeComponent\");\nvar ReactDOM = _dereq_(\"./ReactDOM\");\nvar ReactMount = _dereq_(\"./ReactMount\");\n\nvar invariant = _dereq_(\"./invariant\");\nvar merge = _dereq_(\"./merge\");\n\n// Store a reference to the <input> `ReactDOMComponent`.\nvar input = ReactDOM.input;\n\nvar instancesByReactID = {};\n\n/**\n * Implements an <input> native component that allows setting these optional\n * props: `checked`, `value`, `defaultChecked`, and `defaultValue`.\n *\n * If `checked` or `value` are not supplied (or null/undefined), user actions\n * that affect the checked state or value will trigger updates to the element.\n *\n * If they are supplied (and not null/undefined), the rendered element will not\n * trigger updates to the element. Instead, the props must change in order for\n * the rendered element to be updated.\n *\n * The rendered element will be initialized as unchecked (or `defaultChecked`)\n * with an empty value (or `defaultValue`).\n *\n * @see http://www.w3.org/TR/2012/WD-html5-20121025/the-input-element.html\n */\nvar ReactDOMInput = ReactCompositeComponent.createClass({\n  displayName: 'ReactDOMInput',\n\n  mixins: [AutoFocusMixin, LinkedValueUtils.Mixin, ReactBrowserComponentMixin],\n\n  getInitialState: function() {\n    var defaultValue = this.props.defaultValue;\n    return {\n      checked: this.props.defaultChecked || false,\n      value: defaultValue != null ? defaultValue : null\n    };\n  },\n\n  shouldComponentUpdate: function() {\n    // Defer any updates to this component during the `onChange` handler.\n    return !this._isChanging;\n  },\n\n  render: function() {\n    // Clone `this.props` so we don't mutate the input.\n    var props = merge(this.props);\n\n    props.defaultChecked = null;\n    props.defaultValue = null;\n\n    var value = LinkedValueUtils.getValue(this);\n    props.value = value != null ? value : this.state.value;\n\n    var checked = LinkedValueUtils.getChecked(this);\n    props.checked = checked != null ? checked : this.state.checked;\n\n    props.onChange = this._handleChange;\n\n    return input(props, this.props.children);\n  },\n\n  componentDidMount: function() {\n    var id = ReactMount.getID(this.getDOMNode());\n    instancesByReactID[id] = this;\n  },\n\n  componentWillUnmount: function() {\n    var rootNode = this.getDOMNode();\n    var id = ReactMount.getID(rootNode);\n    delete instancesByReactID[id];\n  },\n\n  componentDidUpdate: function(prevProps, prevState, prevContext) {\n    var rootNode = this.getDOMNode();\n    if (this.props.checked != null) {\n      DOMPropertyOperations.setValueForProperty(\n        rootNode,\n        'checked',\n        this.props.checked || false\n      );\n    }\n\n    var value = LinkedValueUtils.getValue(this);\n    if (value != null) {\n      // Cast `value` to a string to ensure the value is set correctly. While\n      // browsers typically do this as necessary, jsdom doesn't.\n      DOMPropertyOperations.setValueForProperty(rootNode, 'value', '' + value);\n    }\n  },\n\n  _handleChange: function(event) {\n    var returnValue;\n    var onChange = LinkedValueUtils.getOnChange(this);\n    if (onChange) {\n      this._isChanging = true;\n      returnValue = onChange.call(this, event);\n      this._isChanging = false;\n    }\n    this.setState({\n      checked: event.target.checked,\n      value: event.target.value\n    });\n\n    var name = this.props.name;\n    if (this.props.type === 'radio' && name != null) {\n      var rootNode = this.getDOMNode();\n      var queryRoot = rootNode;\n\n      while (queryRoot.parentNode) {\n        queryRoot = queryRoot.parentNode;\n      }\n\n      // If `rootNode.form` was non-null, then we could try `form.elements`,\n      // but that sometimes behaves strangely in IE8. We could also try using\n      // `form.getElementsByName`, but that will only return direct children\n      // and won't include inputs that use the HTML5 `form=` attribute. Since\n      // the input might not even be in a form, let's just use the global\n      // `querySelectorAll` to ensure we don't miss anything.\n      var group = queryRoot.querySelectorAll(\n        'input[name=' + JSON.stringify('' + name) + '][type=\"radio\"]');\n\n      for (var i = 0, groupLen = group.length; i < groupLen; i++) {\n        var otherNode = group[i];\n        if (otherNode === rootNode ||\n            otherNode.form !== rootNode.form) {\n          continue;\n        }\n        var otherID = ReactMount.getID(otherNode);\n        (\"production\" !== \"development\" ? invariant(\n          otherID,\n          'ReactDOMInput: Mixing React and non-React radio inputs with the ' +\n          'same `name` is not supported.'\n        ) : invariant(otherID));\n        var otherInstance = instancesByReactID[otherID];\n        (\"production\" !== \"development\" ? invariant(\n          otherInstance,\n          'ReactDOMInput: Unknown radio button ID %s.',\n          otherID\n        ) : invariant(otherInstance));\n        // In some cases, this will actually change the `checked` state value.\n        // In other cases, there's no change but this forces a reconcile upon\n        // which componentDidUpdate will reset the DOM property to whatever it\n        // should be.\n        otherInstance.setState({\n          checked: false\n        });\n      }\n    }\n\n    return returnValue;\n  }\n\n});\n\nmodule.exports = ReactDOMInput;\n\n},{\"./AutoFocusMixin\":1,\"./DOMPropertyOperations\":12,\"./LinkedValueUtils\":25,\"./ReactBrowserComponentMixin\":30,\"./ReactCompositeComponent\":38,\"./ReactDOM\":41,\"./ReactMount\":67,\"./invariant\":134,\"./merge\":144}],48:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule ReactDOMOption\n */\n\n\"use strict\";\n\nvar ReactBrowserComponentMixin = _dereq_(\"./ReactBrowserComponentMixin\");\nvar ReactCompositeComponent = _dereq_(\"./ReactCompositeComponent\");\nvar ReactDOM = _dereq_(\"./ReactDOM\");\n\nvar warning = _dereq_(\"./warning\");\n\n// Store a reference to the <option> `ReactDOMComponent`.\nvar option = ReactDOM.option;\n\n/**\n * Implements an <option> native component that warns when `selected` is set.\n */\nvar ReactDOMOption = ReactCompositeComponent.createClass({\n  displayName: 'ReactDOMOption',\n\n  mixins: [ReactBrowserComponentMixin],\n\n  componentWillMount: function() {\n    // TODO (yungsters): Remove support for `selected` in <option>.\n    if (\"production\" !== \"development\") {\n      (\"production\" !== \"development\" ? warning(\n        this.props.selected == null,\n        'Use the `defaultValue` or `value` props on <select> instead of ' +\n        'setting `selected` on <option>.'\n      ) : null);\n    }\n  },\n\n  render: function() {\n    return option(this.props, this.props.children);\n  }\n\n});\n\nmodule.exports = ReactDOMOption;\n\n},{\"./ReactBrowserComponentMixin\":30,\"./ReactCompositeComponent\":38,\"./ReactDOM\":41,\"./warning\":158}],49:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule ReactDOMSelect\n */\n\n\"use strict\";\n\nvar AutoFocusMixin = _dereq_(\"./AutoFocusMixin\");\nvar LinkedValueUtils = _dereq_(\"./LinkedValueUtils\");\nvar ReactBrowserComponentMixin = _dereq_(\"./ReactBrowserComponentMixin\");\nvar ReactCompositeComponent = _dereq_(\"./ReactCompositeComponent\");\nvar ReactDOM = _dereq_(\"./ReactDOM\");\n\nvar merge = _dereq_(\"./merge\");\n\n// Store a reference to the <select> `ReactDOMComponent`.\nvar select = ReactDOM.select;\n\n/**\n * Validation function for `value` and `defaultValue`.\n * @private\n */\nfunction selectValueType(props, propName, componentName) {\n  if (props[propName] == null) {\n    return;\n  }\n  if (props.multiple) {\n    if (!Array.isArray(props[propName])) {\n      return new Error(\n        (\"The `\" + propName + \"` prop supplied to <select> must be an array if \") +\n        (\"`multiple` is true.\")\n      );\n    }\n  } else {\n    if (Array.isArray(props[propName])) {\n      return new Error(\n        (\"The `\" + propName + \"` prop supplied to <select> must be a scalar \") +\n        (\"value if `multiple` is false.\")\n      );\n    }\n  }\n}\n\n/**\n * If `value` is supplied, updates <option> elements on mount and update.\n * @param {ReactComponent} component Instance of ReactDOMSelect\n * @param {?*} propValue For uncontrolled components, null/undefined. For\n * controlled components, a string (or with `multiple`, a list of strings).\n * @private\n */\nfunction updateOptions(component, propValue) {\n  var multiple = component.props.multiple;\n  var value = propValue != null ? propValue : component.state.value;\n  var options = component.getDOMNode().options;\n  var selectedValue, i, l;\n  if (multiple) {\n    selectedValue = {};\n    for (i = 0, l = value.length; i < l; ++i) {\n      selectedValue['' + value[i]] = true;\n    }\n  } else {\n    selectedValue = '' + value;\n  }\n  for (i = 0, l = options.length; i < l; i++) {\n    var selected = multiple ?\n      selectedValue.hasOwnProperty(options[i].value) :\n      options[i].value === selectedValue;\n\n    if (selected !== options[i].selected) {\n      options[i].selected = selected;\n    }\n  }\n}\n\n/**\n * Implements a <select> native component that allows optionally setting the\n * props `value` and `defaultValue`. If `multiple` is false, the prop must be a\n * string. If `multiple` is true, the prop must be an array of strings.\n *\n * If `value` is not supplied (or null/undefined), user actions that change the\n * selected option will trigger updates to the rendered options.\n *\n * If it is supplied (and not null/undefined), the rendered options will not\n * update in response to user actions. Instead, the `value` prop must change in\n * order for the rendered options to update.\n *\n * If `defaultValue` is provided, any options with the supplied values will be\n * selected.\n */\nvar ReactDOMSelect = ReactCompositeComponent.createClass({\n  displayName: 'ReactDOMSelect',\n\n  mixins: [AutoFocusMixin, LinkedValueUtils.Mixin, ReactBrowserComponentMixin],\n\n  propTypes: {\n    defaultValue: selectValueType,\n    value: selectValueType\n  },\n\n  getInitialState: function() {\n    return {value: this.props.defaultValue || (this.props.multiple ? [] : '')};\n  },\n\n  componentWillReceiveProps: function(nextProps) {\n    if (!this.props.multiple && nextProps.multiple) {\n      this.setState({value: [this.state.value]});\n    } else if (this.props.multiple && !nextProps.multiple) {\n      this.setState({value: this.state.value[0]});\n    }\n  },\n\n  shouldComponentUpdate: function() {\n    // Defer any updates to this component during the `onChange` handler.\n    return !this._isChanging;\n  },\n\n  render: function() {\n    // Clone `this.props` so we don't mutate the input.\n    var props = merge(this.props);\n\n    props.onChange = this._handleChange;\n    props.value = null;\n\n    return select(props, this.props.children);\n  },\n\n  componentDidMount: function() {\n    updateOptions(this, LinkedValueUtils.getValue(this));\n  },\n\n  componentDidUpdate: function(prevProps) {\n    var value = LinkedValueUtils.getValue(this);\n    var prevMultiple = !!prevProps.multiple;\n    var multiple = !!this.props.multiple;\n    if (value != null || prevMultiple !== multiple) {\n      updateOptions(this, value);\n    }\n  },\n\n  _handleChange: function(event) {\n    var returnValue;\n    var onChange = LinkedValueUtils.getOnChange(this);\n    if (onChange) {\n      this._isChanging = true;\n      returnValue = onChange.call(this, event);\n      this._isChanging = false;\n    }\n\n    var selectedValue;\n    if (this.props.multiple) {\n      selectedValue = [];\n      var options = event.target.options;\n      for (var i = 0, l = options.length; i < l; i++) {\n        if (options[i].selected) {\n          selectedValue.push(options[i].value);\n        }\n      }\n    } else {\n      selectedValue = event.target.value;\n    }\n\n    this.setState({value: selectedValue});\n    return returnValue;\n  }\n\n});\n\nmodule.exports = ReactDOMSelect;\n\n},{\"./AutoFocusMixin\":1,\"./LinkedValueUtils\":25,\"./ReactBrowserComponentMixin\":30,\"./ReactCompositeComponent\":38,\"./ReactDOM\":41,\"./merge\":144}],50:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule ReactDOMSelection\n */\n\n\"use strict\";\n\nvar ExecutionEnvironment = _dereq_(\"./ExecutionEnvironment\");\n\nvar getNodeForCharacterOffset = _dereq_(\"./getNodeForCharacterOffset\");\nvar getTextContentAccessor = _dereq_(\"./getTextContentAccessor\");\n\n/**\n * While `isCollapsed` is available on the Selection object and `collapsed`\n * is available on the Range object, IE11 sometimes gets them wrong.\n * If the anchor/focus nodes and offsets are the same, the range is collapsed.\n */\nfunction isCollapsed(anchorNode, anchorOffset, focusNode, focusOffset) {\n  return anchorNode === focusNode && anchorOffset === focusOffset;\n}\n\n/**\n * Get the appropriate anchor and focus node/offset pairs for IE.\n *\n * The catch here is that IE's selection API doesn't provide information\n * about whether the selection is forward or backward, so we have to\n * behave as though it's always forward.\n *\n * IE text differs from modern selection in that it behaves as though\n * block elements end with a new line. This means character offsets will\n * differ between the two APIs.\n *\n * @param {DOMElement} node\n * @return {object}\n */\nfunction getIEOffsets(node) {\n  var selection = document.selection;\n  var selectedRange = selection.createRange();\n  var selectedLength = selectedRange.text.length;\n\n  // Duplicate selection so we can move range without breaking user selection.\n  var fromStart = selectedRange.duplicate();\n  fromStart.moveToElementText(node);\n  fromStart.setEndPoint('EndToStart', selectedRange);\n\n  var startOffset = fromStart.text.length;\n  var endOffset = startOffset + selectedLength;\n\n  return {\n    start: startOffset,\n    end: endOffset\n  };\n}\n\n/**\n * @param {DOMElement} node\n * @return {?object}\n */\nfunction getModernOffsets(node) {\n  var selection = window.getSelection();\n\n  if (selection.rangeCount === 0) {\n    return null;\n  }\n\n  var anchorNode = selection.anchorNode;\n  var anchorOffset = selection.anchorOffset;\n  var focusNode = selection.focusNode;\n  var focusOffset = selection.focusOffset;\n\n  var currentRange = selection.getRangeAt(0);\n\n  // If the node and offset values are the same, the selection is collapsed.\n  // `Selection.isCollapsed` is available natively, but IE sometimes gets\n  // this value wrong.\n  var isSelectionCollapsed = isCollapsed(\n    selection.anchorNode,\n    selection.anchorOffset,\n    selection.focusNode,\n    selection.focusOffset\n  );\n\n  var rangeLength = isSelectionCollapsed ? 0 : currentRange.toString().length;\n\n  var tempRange = currentRange.cloneRange();\n  tempRange.selectNodeContents(node);\n  tempRange.setEnd(currentRange.startContainer, currentRange.startOffset);\n\n  var isTempRangeCollapsed = isCollapsed(\n    tempRange.startContainer,\n    tempRange.startOffset,\n    tempRange.endContainer,\n    tempRange.endOffset\n  );\n\n  var start = isTempRangeCollapsed ? 0 : tempRange.toString().length;\n  var end = start + rangeLength;\n\n  // Detect whether the selection is backward.\n  var detectionRange = document.createRange();\n  detectionRange.setStart(anchorNode, anchorOffset);\n  detectionRange.setEnd(focusNode, focusOffset);\n  var isBackward = detectionRange.collapsed;\n  detectionRange.detach();\n\n  return {\n    start: isBackward ? end : start,\n    end: isBackward ? start : end\n  };\n}\n\n/**\n * @param {DOMElement|DOMTextNode} node\n * @param {object} offsets\n */\nfunction setIEOffsets(node, offsets) {\n  var range = document.selection.createRange().duplicate();\n  var start, end;\n\n  if (typeof offsets.end === 'undefined') {\n    start = offsets.start;\n    end = start;\n  } else if (offsets.start > offsets.end) {\n    start = offsets.end;\n    end = offsets.start;\n  } else {\n    start = offsets.start;\n    end = offsets.end;\n  }\n\n  range.moveToElementText(node);\n  range.moveStart('character', start);\n  range.setEndPoint('EndToStart', range);\n  range.moveEnd('character', end - start);\n  range.select();\n}\n\n/**\n * In modern non-IE browsers, we can support both forward and backward\n * selections.\n *\n * Note: IE10+ supports the Selection object, but it does not support\n * the `extend` method, which means that even in modern IE, it's not possible\n * to programatically create a backward selection. Thus, for all IE\n * versions, we use the old IE API to create our selections.\n *\n * @param {DOMElement|DOMTextNode} node\n * @param {object} offsets\n */\nfunction setModernOffsets(node, offsets) {\n  var selection = window.getSelection();\n\n  var length = node[getTextContentAccessor()].length;\n  var start = Math.min(offsets.start, length);\n  var end = typeof offsets.end === 'undefined' ?\n            start : Math.min(offsets.end, length);\n\n  // IE 11 uses modern selection, but doesn't support the extend method.\n  // Flip backward selections, so we can set with a single range.\n  if (!selection.extend && start > end) {\n    var temp = end;\n    end = start;\n    start = temp;\n  }\n\n  var startMarker = getNodeForCharacterOffset(node, start);\n  var endMarker = getNodeForCharacterOffset(node, end);\n\n  if (startMarker && endMarker) {\n    var range = document.createRange();\n    range.setStart(startMarker.node, startMarker.offset);\n    selection.removeAllRanges();\n\n    if (start > end) {\n      selection.addRange(range);\n      selection.extend(endMarker.node, endMarker.offset);\n    } else {\n      range.setEnd(endMarker.node, endMarker.offset);\n      selection.addRange(range);\n    }\n\n    range.detach();\n  }\n}\n\nvar useIEOffsets = ExecutionEnvironment.canUseDOM && document.selection;\n\nvar ReactDOMSelection = {\n  /**\n   * @param {DOMElement} node\n   */\n  getOffsets: useIEOffsets ? getIEOffsets : getModernOffsets,\n\n  /**\n   * @param {DOMElement|DOMTextNode} node\n   * @param {object} offsets\n   */\n  setOffsets: useIEOffsets ? setIEOffsets : setModernOffsets\n};\n\nmodule.exports = ReactDOMSelection;\n\n},{\"./ExecutionEnvironment\":22,\"./getNodeForCharacterOffset\":127,\"./getTextContentAccessor\":129}],51:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule ReactDOMTextarea\n */\n\n\"use strict\";\n\nvar AutoFocusMixin = _dereq_(\"./AutoFocusMixin\");\nvar DOMPropertyOperations = _dereq_(\"./DOMPropertyOperations\");\nvar LinkedValueUtils = _dereq_(\"./LinkedValueUtils\");\nvar ReactBrowserComponentMixin = _dereq_(\"./ReactBrowserComponentMixin\");\nvar ReactCompositeComponent = _dereq_(\"./ReactCompositeComponent\");\nvar ReactDOM = _dereq_(\"./ReactDOM\");\n\nvar invariant = _dereq_(\"./invariant\");\nvar merge = _dereq_(\"./merge\");\n\nvar warning = _dereq_(\"./warning\");\n\n// Store a reference to the <textarea> `ReactDOMComponent`.\nvar textarea = ReactDOM.textarea;\n\n/**\n * Implements a <textarea> native component that allows setting `value`, and\n * `defaultValue`. This differs from the traditional DOM API because value is\n * usually set as PCDATA children.\n *\n * If `value` is not supplied (or null/undefined), user actions that affect the\n * value will trigger updates to the element.\n *\n * If `value` is supplied (and not null/undefined), the rendered element will\n * not trigger updates to the element. Instead, the `value` prop must change in\n * order for the rendered element to be updated.\n *\n * The rendered element will be initialized with an empty value, the prop\n * `defaultValue` if specified, or the children content (deprecated).\n */\nvar ReactDOMTextarea = ReactCompositeComponent.createClass({\n  displayName: 'ReactDOMTextarea',\n\n  mixins: [AutoFocusMixin, LinkedValueUtils.Mixin, ReactBrowserComponentMixin],\n\n  getInitialState: function() {\n    var defaultValue = this.props.defaultValue;\n    // TODO (yungsters): Remove support for children content in <textarea>.\n    var children = this.props.children;\n    if (children != null) {\n      if (\"production\" !== \"development\") {\n        (\"production\" !== \"development\" ? warning(\n          false,\n          'Use the `defaultValue` or `value` props instead of setting ' +\n          'children on <textarea>.'\n        ) : null);\n      }\n      (\"production\" !== \"development\" ? invariant(\n        defaultValue == null,\n        'If you supply `defaultValue` on a <textarea>, do not pass children.'\n      ) : invariant(defaultValue == null));\n      if (Array.isArray(children)) {\n        (\"production\" !== \"development\" ? invariant(\n          children.length <= 1,\n          '<textarea> can only have at most one child.'\n        ) : invariant(children.length <= 1));\n        children = children[0];\n      }\n\n      defaultValue = '' + children;\n    }\n    if (defaultValue == null) {\n      defaultValue = '';\n    }\n    var value = LinkedValueUtils.getValue(this);\n    return {\n      // We save the initial value so that `ReactDOMComponent` doesn't update\n      // `textContent` (unnecessary since we update value).\n      // The initial value can be a boolean or object so that's why it's\n      // forced to be a string.\n      initialValue: '' + (value != null ? value : defaultValue)\n    };\n  },\n\n  shouldComponentUpdate: function() {\n    // Defer any updates to this component during the `onChange` handler.\n    return !this._isChanging;\n  },\n\n  render: function() {\n    // Clone `this.props` so we don't mutate the input.\n    var props = merge(this.props);\n\n    (\"production\" !== \"development\" ? invariant(\n      props.dangerouslySetInnerHTML == null,\n      '`dangerouslySetInnerHTML` does not make sense on <textarea>.'\n    ) : invariant(props.dangerouslySetInnerHTML == null));\n\n    props.defaultValue = null;\n    props.value = null;\n    props.onChange = this._handleChange;\n\n    // Always set children to the same thing. In IE9, the selection range will\n    // get reset if `textContent` is mutated.\n    return textarea(props, this.state.initialValue);\n  },\n\n  componentDidUpdate: function(prevProps, prevState, prevContext) {\n    var value = LinkedValueUtils.getValue(this);\n    if (value != null) {\n      var rootNode = this.getDOMNode();\n      // Cast `value` to a string to ensure the value is set correctly. While\n      // browsers typically do this as necessary, jsdom doesn't.\n      DOMPropertyOperations.setValueForProperty(rootNode, 'value', '' + value);\n    }\n  },\n\n  _handleChange: function(event) {\n    var returnValue;\n    var onChange = LinkedValueUtils.getOnChange(this);\n    if (onChange) {\n      this._isChanging = true;\n      returnValue = onChange.call(this, event);\n      this._isChanging = false;\n    }\n    this.setState({value: event.target.value});\n    return returnValue;\n  }\n\n});\n\nmodule.exports = ReactDOMTextarea;\n\n},{\"./AutoFocusMixin\":1,\"./DOMPropertyOperations\":12,\"./LinkedValueUtils\":25,\"./ReactBrowserComponentMixin\":30,\"./ReactCompositeComponent\":38,\"./ReactDOM\":41,\"./invariant\":134,\"./merge\":144,\"./warning\":158}],52:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule ReactDefaultBatchingStrategy\n */\n\n\"use strict\";\n\nvar ReactUpdates = _dereq_(\"./ReactUpdates\");\nvar Transaction = _dereq_(\"./Transaction\");\n\nvar emptyFunction = _dereq_(\"./emptyFunction\");\nvar mixInto = _dereq_(\"./mixInto\");\n\nvar RESET_BATCHED_UPDATES = {\n  initialize: emptyFunction,\n  close: function() {\n    ReactDefaultBatchingStrategy.isBatchingUpdates = false;\n  }\n};\n\nvar FLUSH_BATCHED_UPDATES = {\n  initialize: emptyFunction,\n  close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates)\n};\n\nvar TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES];\n\nfunction ReactDefaultBatchingStrategyTransaction() {\n  this.reinitializeTransaction();\n}\n\nmixInto(ReactDefaultBatchingStrategyTransaction, Transaction.Mixin);\nmixInto(ReactDefaultBatchingStrategyTransaction, {\n  getTransactionWrappers: function() {\n    return TRANSACTION_WRAPPERS;\n  }\n});\n\nvar transaction = new ReactDefaultBatchingStrategyTransaction();\n\nvar ReactDefaultBatchingStrategy = {\n  isBatchingUpdates: false,\n\n  /**\n   * Call the provided function in a context within which calls to `setState`\n   * and friends are batched such that components aren't updated unnecessarily.\n   */\n  batchedUpdates: function(callback, a, b) {\n    var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;\n\n    ReactDefaultBatchingStrategy.isBatchingUpdates = true;\n\n    // The code is written this way to avoid extra allocations\n    if (alreadyBatchingUpdates) {\n      callback(a, b);\n    } else {\n      transaction.perform(callback, null, a, b);\n    }\n  }\n};\n\nmodule.exports = ReactDefaultBatchingStrategy;\n\n},{\"./ReactUpdates\":87,\"./Transaction\":104,\"./emptyFunction\":116,\"./mixInto\":147}],53:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule ReactDefaultInjection\n */\n\n\"use strict\";\n\nvar BeforeInputEventPlugin = _dereq_(\"./BeforeInputEventPlugin\");\nvar ChangeEventPlugin = _dereq_(\"./ChangeEventPlugin\");\nvar ClientReactRootIndex = _dereq_(\"./ClientReactRootIndex\");\nvar CompositionEventPlugin = _dereq_(\"./CompositionEventPlugin\");\nvar DefaultEventPluginOrder = _dereq_(\"./DefaultEventPluginOrder\");\nvar EnterLeaveEventPlugin = _dereq_(\"./EnterLeaveEventPlugin\");\nvar ExecutionEnvironment = _dereq_(\"./ExecutionEnvironment\");\nvar HTMLDOMPropertyConfig = _dereq_(\"./HTMLDOMPropertyConfig\");\nvar MobileSafariClickEventPlugin = _dereq_(\"./MobileSafariClickEventPlugin\");\nvar ReactBrowserComponentMixin = _dereq_(\"./ReactBrowserComponentMixin\");\nvar ReactComponentBrowserEnvironment =\n  _dereq_(\"./ReactComponentBrowserEnvironment\");\nvar ReactDefaultBatchingStrategy = _dereq_(\"./ReactDefaultBatchingStrategy\");\nvar ReactDOM = _dereq_(\"./ReactDOM\");\nvar ReactDOMButton = _dereq_(\"./ReactDOMButton\");\nvar ReactDOMForm = _dereq_(\"./ReactDOMForm\");\nvar ReactDOMImg = _dereq_(\"./ReactDOMImg\");\nvar ReactDOMInput = _dereq_(\"./ReactDOMInput\");\nvar ReactDOMOption = _dereq_(\"./ReactDOMOption\");\nvar ReactDOMSelect = _dereq_(\"./ReactDOMSelect\");\nvar ReactDOMTextarea = _dereq_(\"./ReactDOMTextarea\");\nvar ReactEventListener = _dereq_(\"./ReactEventListener\");\nvar ReactInjection = _dereq_(\"./ReactInjection\");\nvar ReactInstanceHandles = _dereq_(\"./ReactInstanceHandles\");\nvar ReactMount = _dereq_(\"./ReactMount\");\nvar SelectEventPlugin = _dereq_(\"./SelectEventPlugin\");\nvar ServerReactRootIndex = _dereq_(\"./ServerReactRootIndex\");\nvar SimpleEventPlugin = _dereq_(\"./SimpleEventPlugin\");\nvar SVGDOMPropertyConfig = _dereq_(\"./SVGDOMPropertyConfig\");\n\nvar createFullPageComponent = _dereq_(\"./createFullPageComponent\");\n\nfunction inject() {\n  ReactInjection.EventEmitter.injectReactEventListener(\n    ReactEventListener\n  );\n\n  /**\n   * Inject modules for resolving DOM hierarchy and plugin ordering.\n   */\n  ReactInjection.EventPluginHub.injectEventPluginOrder(DefaultEventPluginOrder);\n  ReactInjection.EventPluginHub.injectInstanceHandle(ReactInstanceHandles);\n  ReactInjection.EventPluginHub.injectMount(ReactMount);\n\n  /**\n   * Some important event plugins included by default (without having to require\n   * them).\n   */\n  ReactInjection.EventPluginHub.injectEventPluginsByName({\n    SimpleEventPlugin: SimpleEventPlugin,\n    EnterLeaveEventPlugin: EnterLeaveEventPlugin,\n    ChangeEventPlugin: ChangeEventPlugin,\n    CompositionEventPlugin: CompositionEventPlugin,\n    MobileSafariClickEventPlugin: MobileSafariClickEventPlugin,\n    SelectEventPlugin: SelectEventPlugin,\n    BeforeInputEventPlugin: BeforeInputEventPlugin\n  });\n\n  ReactInjection.DOM.injectComponentClasses({\n    button: ReactDOMButton,\n    form: ReactDOMForm,\n    img: ReactDOMImg,\n    input: ReactDOMInput,\n    option: ReactDOMOption,\n    select: ReactDOMSelect,\n    textarea: ReactDOMTextarea,\n\n    html: createFullPageComponent(ReactDOM.html),\n    head: createFullPageComponent(ReactDOM.head),\n    body: createFullPageComponent(ReactDOM.body)\n  });\n\n  // This needs to happen after createFullPageComponent() otherwise the mixin\n  // gets double injected.\n  ReactInjection.CompositeComponent.injectMixin(ReactBrowserComponentMixin);\n\n  ReactInjection.DOMProperty.injectDOMPropertyConfig(HTMLDOMPropertyConfig);\n  ReactInjection.DOMProperty.injectDOMPropertyConfig(SVGDOMPropertyConfig);\n\n  ReactInjection.EmptyComponent.injectEmptyComponent(ReactDOM.noscript);\n\n  ReactInjection.Updates.injectReconcileTransaction(\n    ReactComponentBrowserEnvironment.ReactReconcileTransaction\n  );\n  ReactInjection.Updates.injectBatchingStrategy(\n    ReactDefaultBatchingStrategy\n  );\n\n  ReactInjection.RootIndex.injectCreateReactRootIndex(\n    ExecutionEnvironment.canUseDOM ?\n      ClientReactRootIndex.createReactRootIndex :\n      ServerReactRootIndex.createReactRootIndex\n  );\n\n  ReactInjection.Component.injectEnvironment(ReactComponentBrowserEnvironment);\n\n  if (\"production\" !== \"development\") {\n    var url = (ExecutionEnvironment.canUseDOM && window.location.href) || '';\n    if ((/[?&]react_perf\\b/).test(url)) {\n      var ReactDefaultPerf = _dereq_(\"./ReactDefaultPerf\");\n      ReactDefaultPerf.start();\n    }\n  }\n}\n\nmodule.exports = {\n  inject: inject\n};\n\n},{\"./BeforeInputEventPlugin\":2,\"./ChangeEventPlugin\":7,\"./ClientReactRootIndex\":8,\"./CompositionEventPlugin\":9,\"./DefaultEventPluginOrder\":14,\"./EnterLeaveEventPlugin\":15,\"./ExecutionEnvironment\":22,\"./HTMLDOMPropertyConfig\":23,\"./MobileSafariClickEventPlugin\":27,\"./ReactBrowserComponentMixin\":30,\"./ReactComponentBrowserEnvironment\":36,\"./ReactDOM\":41,\"./ReactDOMButton\":42,\"./ReactDOMForm\":44,\"./ReactDOMImg\":46,\"./ReactDOMInput\":47,\"./ReactDOMOption\":48,\"./ReactDOMSelect\":49,\"./ReactDOMTextarea\":51,\"./ReactDefaultBatchingStrategy\":52,\"./ReactDefaultPerf\":54,\"./ReactEventListener\":61,\"./ReactInjection\":62,\"./ReactInstanceHandles\":64,\"./ReactMount\":67,\"./SVGDOMPropertyConfig\":89,\"./SelectEventPlugin\":90,\"./ServerReactRootIndex\":91,\"./SimpleEventPlugin\":92,\"./createFullPageComponent\":112}],54:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule ReactDefaultPerf\n * @typechecks static-only\n */\n\n\"use strict\";\n\nvar DOMProperty = _dereq_(\"./DOMProperty\");\nvar ReactDefaultPerfAnalysis = _dereq_(\"./ReactDefaultPerfAnalysis\");\nvar ReactMount = _dereq_(\"./ReactMount\");\nvar ReactPerf = _dereq_(\"./ReactPerf\");\n\nvar performanceNow = _dereq_(\"./performanceNow\");\n\nfunction roundFloat(val) {\n  return Math.floor(val * 100) / 100;\n}\n\nfunction addValue(obj, key, val) {\n  obj[key] = (obj[key] || 0) + val;\n}\n\nvar ReactDefaultPerf = {\n  _allMeasurements: [], // last item in the list is the current one\n  _mountStack: [0],\n  _injected: false,\n\n  start: function() {\n    if (!ReactDefaultPerf._injected) {\n      ReactPerf.injection.injectMeasure(ReactDefaultPerf.measure);\n    }\n\n    ReactDefaultPerf._allMeasurements.length = 0;\n    ReactPerf.enableMeasure = true;\n  },\n\n  stop: function() {\n    ReactPerf.enableMeasure = false;\n  },\n\n  getLastMeasurements: function() {\n    return ReactDefaultPerf._allMeasurements;\n  },\n\n  printExclusive: function(measurements) {\n    measurements = measurements || ReactDefaultPerf._allMeasurements;\n    var summary = ReactDefaultPerfAnalysis.getExclusiveSummary(measurements);\n    console.table(summary.map(function(item) {\n      return {\n        'Component class name': item.componentName,\n        'Total inclusive time (ms)': roundFloat(item.inclusive),\n        'Exclusive mount time (ms)': roundFloat(item.exclusive),\n        'Exclusive render time (ms)': roundFloat(item.render),\n        'Mount time per instance (ms)': roundFloat(item.exclusive / item.count),\n        'Render time per instance (ms)': roundFloat(item.render / item.count),\n        'Instances': item.count\n      };\n    }));\n    // TODO: ReactDefaultPerfAnalysis.getTotalTime() does not return the correct\n    // number.\n  },\n\n  printInclusive: function(measurements) {\n    measurements = measurements || ReactDefaultPerf._allMeasurements;\n    var summary = ReactDefaultPerfAnalysis.getInclusiveSummary(measurements);\n    console.table(summary.map(function(item) {\n      return {\n        'Owner > component': item.componentName,\n        'Inclusive time (ms)': roundFloat(item.time),\n        'Instances': item.count\n      };\n    }));\n    console.log(\n      'Total time:',\n      ReactDefaultPerfAnalysis.getTotalTime(measurements).toFixed(2) + ' ms'\n    );\n  },\n\n  printWasted: function(measurements) {\n    measurements = measurements || ReactDefaultPerf._allMeasurements;\n    var summary = ReactDefaultPerfAnalysis.getInclusiveSummary(\n      measurements,\n      true\n    );\n    console.table(summary.map(function(item) {\n      return {\n        'Owner > component': item.componentName,\n        'Wasted time (ms)': item.time,\n        'Instances': item.count\n      };\n    }));\n    console.log(\n      'Total time:',\n      ReactDefaultPerfAnalysis.getTotalTime(measurements).toFixed(2) + ' ms'\n    );\n  },\n\n  printDOM: function(measurements) {\n    measurements = measurements || ReactDefaultPerf._allMeasurements;\n    var summary = ReactDefaultPerfAnalysis.getDOMSummary(measurements);\n    console.table(summary.map(function(item) {\n      var result = {};\n      result[DOMProperty.ID_ATTRIBUTE_NAME] = item.id;\n      result['type'] = item.type;\n      result['args'] = JSON.stringify(item.args);\n      return result;\n    }));\n    console.log(\n      'Total time:',\n      ReactDefaultPerfAnalysis.getTotalTime(measurements).toFixed(2) + ' ms'\n    );\n  },\n\n  _recordWrite: function(id, fnName, totalTime, args) {\n    // TODO: totalTime isn't that useful since it doesn't count paints/reflows\n    var writes =\n      ReactDefaultPerf\n        ._allMeasurements[ReactDefaultPerf._allMeasurements.length - 1]\n        .writes;\n    writes[id] = writes[id] || [];\n    writes[id].push({\n      type: fnName,\n      time: totalTime,\n      args: args\n    });\n  },\n\n  measure: function(moduleName, fnName, func) {\n    return function() {var args=Array.prototype.slice.call(arguments,0);\n      var totalTime;\n      var rv;\n      var start;\n\n      if (fnName === '_renderNewRootComponent' ||\n          fnName === 'flushBatchedUpdates') {\n        // A \"measurement\" is a set of metrics recorded for each flush. We want\n        // to group the metrics for a given flush together so we can look at the\n        // components that rendered and the DOM operations that actually\n        // happened to determine the amount of \"wasted work\" performed.\n        ReactDefaultPerf._allMeasurements.push({\n          exclusive: {},\n          inclusive: {},\n          render: {},\n          counts: {},\n          writes: {},\n          displayNames: {},\n          totalTime: 0\n        });\n        start = performanceNow();\n        rv = func.apply(this, args);\n        ReactDefaultPerf._allMeasurements[\n          ReactDefaultPerf._allMeasurements.length - 1\n        ].totalTime = performanceNow() - start;\n        return rv;\n      } else if (moduleName === 'ReactDOMIDOperations' ||\n        moduleName === 'ReactComponentBrowserEnvironment') {\n        start = performanceNow();\n        rv = func.apply(this, args);\n        totalTime = performanceNow() - start;\n\n        if (fnName === 'mountImageIntoNode') {\n          var mountID = ReactMount.getID(args[1]);\n          ReactDefaultPerf._recordWrite(mountID, fnName, totalTime, args[0]);\n        } else if (fnName === 'dangerouslyProcessChildrenUpdates') {\n          // special format\n          args[0].forEach(function(update) {\n            var writeArgs = {};\n            if (update.fromIndex !== null) {\n              writeArgs.fromIndex = update.fromIndex;\n            }\n            if (update.toIndex !== null) {\n              writeArgs.toIndex = update.toIndex;\n            }\n            if (update.textContent !== null) {\n              writeArgs.textContent = update.textContent;\n            }\n            if (update.markupIndex !== null) {\n              writeArgs.markup = args[1][update.markupIndex];\n            }\n            ReactDefaultPerf._recordWrite(\n              update.parentID,\n              update.type,\n              totalTime,\n              writeArgs\n            );\n          });\n        } else {\n          // basic format\n          ReactDefaultPerf._recordWrite(\n            args[0],\n            fnName,\n            totalTime,\n            Array.prototype.slice.call(args, 1)\n          );\n        }\n        return rv;\n      } else if (moduleName === 'ReactCompositeComponent' && (\n        fnName === 'mountComponent' ||\n        fnName === 'updateComponent' || // TODO: receiveComponent()?\n        fnName === '_renderValidatedComponent')) {\n\n        var rootNodeID = fnName === 'mountComponent' ?\n          args[0] :\n          this._rootNodeID;\n        var isRender = fnName === '_renderValidatedComponent';\n        var isMount = fnName === 'mountComponent';\n\n        var mountStack = ReactDefaultPerf._mountStack;\n        var entry = ReactDefaultPerf._allMeasurements[\n          ReactDefaultPerf._allMeasurements.length - 1\n        ];\n\n        if (isRender) {\n          addValue(entry.counts, rootNodeID, 1);\n        } else if (isMount) {\n          mountStack.push(0);\n        }\n\n        start = performanceNow();\n        rv = func.apply(this, args);\n        totalTime = performanceNow() - start;\n\n        if (isRender) {\n          addValue(entry.render, rootNodeID, totalTime);\n        } else if (isMount) {\n          var subMountTime = mountStack.pop();\n          mountStack[mountStack.length - 1] += totalTime;\n          addValue(entry.exclusive, rootNodeID, totalTime - subMountTime);\n          addValue(entry.inclusive, rootNodeID, totalTime);\n        } else {\n          addValue(entry.inclusive, rootNodeID, totalTime);\n        }\n\n        entry.displayNames[rootNodeID] = {\n          current: this.constructor.displayName,\n          owner: this._owner ? this._owner.constructor.displayName : '<root>'\n        };\n\n        return rv;\n      } else {\n        return func.apply(this, args);\n      }\n    };\n  }\n};\n\nmodule.exports = ReactDefaultPerf;\n\n},{\"./DOMProperty\":11,\"./ReactDefaultPerfAnalysis\":55,\"./ReactMount\":67,\"./ReactPerf\":71,\"./performanceNow\":151}],55:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule ReactDefaultPerfAnalysis\n */\n\nvar merge = _dereq_(\"./merge\");\n\n// Don't try to save users less than 1.2ms (a number I made up)\nvar DONT_CARE_THRESHOLD = 1.2;\nvar DOM_OPERATION_TYPES = {\n  'mountImageIntoNode': 'set innerHTML',\n  INSERT_MARKUP: 'set innerHTML',\n  MOVE_EXISTING: 'move',\n  REMOVE_NODE: 'remove',\n  TEXT_CONTENT: 'set textContent',\n  'updatePropertyByID': 'update attribute',\n  'deletePropertyByID': 'delete attribute',\n  'updateStylesByID': 'update styles',\n  'updateInnerHTMLByID': 'set innerHTML',\n  'dangerouslyReplaceNodeWithMarkupByID': 'replace'\n};\n\nfunction getTotalTime(measurements) {\n  // TODO: return number of DOM ops? could be misleading.\n  // TODO: measure dropped frames after reconcile?\n  // TODO: log total time of each reconcile and the top-level component\n  // class that triggered it.\n  var totalTime = 0;\n  for (var i = 0; i < measurements.length; i++) {\n    var measurement = measurements[i];\n    totalTime += measurement.totalTime;\n  }\n  return totalTime;\n}\n\nfunction getDOMSummary(measurements) {\n  var items = [];\n  for (var i = 0; i < measurements.length; i++) {\n    var measurement = measurements[i];\n    var id;\n\n    for (id in measurement.writes) {\n      measurement.writes[id].forEach(function(write) {\n        items.push({\n          id: id,\n          type: DOM_OPERATION_TYPES[write.type] || write.type,\n          args: write.args\n        });\n      });\n    }\n  }\n  return items;\n}\n\nfunction getExclusiveSummary(measurements) {\n  var candidates = {};\n  var displayName;\n\n  for (var i = 0; i < measurements.length; i++) {\n    var measurement = measurements[i];\n    var allIDs = merge(measurement.exclusive, measurement.inclusive);\n\n    for (var id in allIDs) {\n      displayName = measurement.displayNames[id].current;\n\n      candidates[displayName] = candidates[displayName] || {\n        componentName: displayName,\n        inclusive: 0,\n        exclusive: 0,\n        render: 0,\n        count: 0\n      };\n      if (measurement.render[id]) {\n        candidates[displayName].render += measurement.render[id];\n      }\n      if (measurement.exclusive[id]) {\n        candidates[displayName].exclusive += measurement.exclusive[id];\n      }\n      if (measurement.inclusive[id]) {\n        candidates[displayName].inclusive += measurement.inclusive[id];\n      }\n      if (measurement.counts[id]) {\n        candidates[displayName].count += measurement.counts[id];\n      }\n    }\n  }\n\n  // Now make a sorted array with the results.\n  var arr = [];\n  for (displayName in candidates) {\n    if (candidates[displayName].exclusive >= DONT_CARE_THRESHOLD) {\n      arr.push(candidates[displayName]);\n    }\n  }\n\n  arr.sort(function(a, b) {\n    return b.exclusive - a.exclusive;\n  });\n\n  return arr;\n}\n\nfunction getInclusiveSummary(measurements, onlyClean) {\n  var candidates = {};\n  var inclusiveKey;\n\n  for (var i = 0; i < measurements.length; i++) {\n    var measurement = measurements[i];\n    var allIDs = merge(measurement.exclusive, measurement.inclusive);\n    var cleanComponents;\n\n    if (onlyClean) {\n      cleanComponents = getUnchangedComponents(measurement);\n    }\n\n    for (var id in allIDs) {\n      if (onlyClean && !cleanComponents[id]) {\n        continue;\n      }\n\n      var displayName = measurement.displayNames[id];\n\n      // Inclusive time is not useful for many components without knowing where\n      // they are instantiated. So we aggregate inclusive time with both the\n      // owner and current displayName as the key.\n      inclusiveKey = displayName.owner + ' > ' + displayName.current;\n\n      candidates[inclusiveKey] = candidates[inclusiveKey] || {\n        componentName: inclusiveKey,\n        time: 0,\n        count: 0\n      };\n\n      if (measurement.inclusive[id]) {\n        candidates[inclusiveKey].time += measurement.inclusive[id];\n      }\n      if (measurement.counts[id]) {\n        candidates[inclusiveKey].count += measurement.counts[id];\n      }\n    }\n  }\n\n  // Now make a sorted array with the results.\n  var arr = [];\n  for (inclusiveKey in candidates) {\n    if (candidates[inclusiveKey].time >= DONT_CARE_THRESHOLD) {\n      arr.push(candidates[inclusiveKey]);\n    }\n  }\n\n  arr.sort(function(a, b) {\n    return b.time - a.time;\n  });\n\n  return arr;\n}\n\nfunction getUnchangedComponents(measurement) {\n  // For a given reconcile, look at which components did not actually\n  // render anything to the DOM and return a mapping of their ID to\n  // the amount of time it took to render the entire subtree.\n  var cleanComponents = {};\n  var dirtyLeafIDs = Object.keys(measurement.writes);\n  var allIDs = merge(measurement.exclusive, measurement.inclusive);\n\n  for (var id in allIDs) {\n    var isDirty = false;\n    // For each component that rendered, see if a component that triggerd\n    // a DOM op is in its subtree.\n    for (var i = 0; i < dirtyLeafIDs.length; i++) {\n      if (dirtyLeafIDs[i].indexOf(id) === 0) {\n        isDirty = true;\n        break;\n      }\n    }\n    if (!isDirty && measurement.counts[id] > 0) {\n      cleanComponents[id] = true;\n    }\n  }\n  return cleanComponents;\n}\n\nvar ReactDefaultPerfAnalysis = {\n  getExclusiveSummary: getExclusiveSummary,\n  getInclusiveSummary: getInclusiveSummary,\n  getDOMSummary: getDOMSummary,\n  getTotalTime: getTotalTime\n};\n\nmodule.exports = ReactDefaultPerfAnalysis;\n\n},{\"./merge\":144}],56:[function(_dereq_,module,exports){\n/**\n * Copyright 2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule ReactDescriptor\n */\n\n\"use strict\";\n\nvar ReactContext = _dereq_(\"./ReactContext\");\nvar ReactCurrentOwner = _dereq_(\"./ReactCurrentOwner\");\n\nvar merge = _dereq_(\"./merge\");\nvar warning = _dereq_(\"./warning\");\n\n/**\n * Warn for mutations.\n *\n * @internal\n * @param {object} object\n * @param {string} key\n */\nfunction defineWarningProperty(object, key) {\n  Object.defineProperty(object, key, {\n\n    configurable: false,\n    enumerable: true,\n\n    get: function() {\n      if (!this._store) {\n        return null;\n      }\n      return this._store[key];\n    },\n\n    set: function(value) {\n      (\"production\" !== \"development\" ? warning(\n        false,\n        'Don\\'t set the ' + key + ' property of the component. ' +\n        'Mutate the existing props object instead.'\n      ) : null);\n      this._store[key] = value;\n    }\n\n  });\n}\n\n/**\n * This is updated to true if the membrane is successfully created.\n */\nvar useMutationMembrane = false;\n\n/**\n * Warn for mutations.\n *\n * @internal\n * @param {object} descriptor\n */\nfunction defineMutationMembrane(prototype) {\n  try {\n    var pseudoFrozenProperties = {\n      props: true\n    };\n    for (var key in pseudoFrozenProperties) {\n      defineWarningProperty(prototype, key);\n    }\n    useMutationMembrane = true;\n  } catch (x) {\n    // IE will fail on defineProperty\n  }\n}\n\n/**\n * Transfer static properties from the source to the target. Functions are\n * rebound to have this reflect the original source.\n */\nfunction proxyStaticMethods(target, source) {\n  if (typeof source !== 'function') {\n    return;\n  }\n  for (var key in source) {\n    if (source.hasOwnProperty(key)) {\n      var value = source[key];\n      if (typeof value === 'function') {\n        var bound = value.bind(source);\n        // Copy any properties defined on the function, such as `isRequired` on\n        // a PropTypes validator. (mergeInto refuses to work on functions.)\n        for (var k in value) {\n          if (value.hasOwnProperty(k)) {\n            bound[k] = value[k];\n          }\n        }\n        target[key] = bound;\n      } else {\n        target[key] = value;\n      }\n    }\n  }\n}\n\n/**\n * Base constructor for all React descriptors. This is only used to make this\n * work with a dynamic instanceof check. Nothing should live on this prototype.\n *\n * @param {*} type\n * @internal\n */\nvar ReactDescriptor = function() {};\n\nif (\"production\" !== \"development\") {\n  defineMutationMembrane(ReactDescriptor.prototype);\n}\n\nReactDescriptor.createFactory = function(type) {\n\n  var descriptorPrototype = Object.create(ReactDescriptor.prototype);\n\n  var factory = function(props, children) {\n    // For consistency we currently allocate a new object for every descriptor.\n    // This protects the descriptor from being mutated by the original props\n    // object being mutated. It also protects the original props object from\n    // being mutated by children arguments and default props. This behavior\n    // comes with a performance cost and could be deprecated in the future.\n    // It could also be optimized with a smarter JSX transform.\n    if (props == null) {\n      props = {};\n    } else if (typeof props === 'object') {\n      props = merge(props);\n    }\n\n    // Children can be more than one argument, and those are transferred onto\n    // the newly allocated props object.\n    var childrenLength = arguments.length - 1;\n    if (childrenLength === 1) {\n      props.children = children;\n    } else if (childrenLength > 1) {\n      var childArray = Array(childrenLength);\n      for (var i = 0; i < childrenLength; i++) {\n        childArray[i] = arguments[i + 1];\n      }\n      props.children = childArray;\n    }\n\n    // Initialize the descriptor object\n    var descriptor = Object.create(descriptorPrototype);\n\n    // Record the component responsible for creating this descriptor.\n    descriptor._owner = ReactCurrentOwner.current;\n\n    // TODO: Deprecate withContext, and then the context becomes accessible\n    // through the owner.\n    descriptor._context = ReactContext.current;\n\n    if (\"production\" !== \"development\") {\n      // The validation flag and props are currently mutative. We put them on\n      // an external backing store so that we can freeze the whole object.\n      // This can be replaced with a WeakMap once they are implemented in\n      // commonly used development environments.\n      descriptor._store = { validated: false, props: props };\n\n      // We're not allowed to set props directly on the object so we early\n      // return and rely on the prototype membrane to forward to the backing\n      // store.\n      if (useMutationMembrane) {\n        Object.freeze(descriptor);\n        return descriptor;\n      }\n    }\n\n    descriptor.props = props;\n    return descriptor;\n  };\n\n  // Currently we expose the prototype of the descriptor so that\n  // <Foo /> instanceof Foo works. This is controversial pattern.\n  factory.prototype = descriptorPrototype;\n\n  // Expose the type on the factory and the prototype so that it can be\n  // easily accessed on descriptors. E.g. <Foo />.type === Foo.type and for\n  // static methods like <Foo />.type.staticMethod();\n  // This should not be named constructor since this may not be the function\n  // that created the descriptor, and it may not even be a constructor.\n  factory.type = type;\n  descriptorPrototype.type = type;\n\n  proxyStaticMethods(factory, type);\n\n  // Expose a unique constructor on the prototype is that this works with type\n  // systems that compare constructor properties: <Foo />.constructor === Foo\n  // This may be controversial since it requires a known factory function.\n  descriptorPrototype.constructor = factory;\n\n  return factory;\n\n};\n\nReactDescriptor.cloneAndReplaceProps = function(oldDescriptor, newProps) {\n  var newDescriptor = Object.create(oldDescriptor.constructor.prototype);\n  // It's important that this property order matches the hidden class of the\n  // original descriptor to maintain perf.\n  newDescriptor._owner = oldDescriptor._owner;\n  newDescriptor._context = oldDescriptor._context;\n\n  if (\"production\" !== \"development\") {\n    newDescriptor._store = {\n      validated: oldDescriptor._store.validated,\n      props: newProps\n    };\n    if (useMutationMembrane) {\n      Object.freeze(newDescriptor);\n      return newDescriptor;\n    }\n  }\n\n  newDescriptor.props = newProps;\n  return newDescriptor;\n};\n\n/**\n * Checks if a value is a valid descriptor constructor.\n *\n * @param {*}\n * @return {boolean}\n * @public\n */\nReactDescriptor.isValidFactory = function(factory) {\n  return typeof factory === 'function' &&\n         factory.prototype instanceof ReactDescriptor;\n};\n\n/**\n * @param {?object} object\n * @return {boolean} True if `object` is a valid component.\n * @final\n */\nReactDescriptor.isValidDescriptor = function(object) {\n  return object instanceof ReactDescriptor;\n};\n\nmodule.exports = ReactDescriptor;\n\n},{\"./ReactContext\":39,\"./ReactCurrentOwner\":40,\"./merge\":144,\"./warning\":158}],57:[function(_dereq_,module,exports){\n/**\n * Copyright 2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule ReactDescriptorValidator\n */\n\n/**\n * ReactDescriptorValidator provides a wrapper around a descriptor factory\n * which validates the props passed to the descriptor. This is intended to be\n * used only in DEV and could be replaced by a static type checker for languages\n * that support it.\n */\n\n\"use strict\";\n\nvar ReactDescriptor = _dereq_(\"./ReactDescriptor\");\nvar ReactPropTypeLocations = _dereq_(\"./ReactPropTypeLocations\");\nvar ReactCurrentOwner = _dereq_(\"./ReactCurrentOwner\");\n\nvar monitorCodeUse = _dereq_(\"./monitorCodeUse\");\n\n/**\n * Warn if there's no key explicitly set on dynamic arrays of children or\n * object keys are not valid. This allows us to keep track of children between\n * updates.\n */\nvar ownerHasKeyUseWarning = {\n  'react_key_warning': {},\n  'react_numeric_key_warning': {}\n};\nvar ownerHasMonitoredObjectMap = {};\n\nvar loggedTypeFailures = {};\n\nvar NUMERIC_PROPERTY_REGEX = /^\\d+$/;\n\n/**\n * Gets the current owner's displayName for use in warnings.\n *\n * @internal\n * @return {?string} Display name or undefined\n */\nfunction getCurrentOwnerDisplayName() {\n  var current = ReactCurrentOwner.current;\n  return current && current.constructor.displayName || undefined;\n}\n\n/**\n * Warn if the component doesn't have an explicit key assigned to it.\n * This component is in an array. The array could grow and shrink or be\n * reordered. All children that haven't already been validated are required to\n * have a \"key\" property assigned to it.\n *\n * @internal\n * @param {ReactComponent} component Component that requires a key.\n * @param {*} parentType component's parent's type.\n */\nfunction validateExplicitKey(component, parentType) {\n  if (component._store.validated || component.props.key != null) {\n    return;\n  }\n  component._store.validated = true;\n\n  warnAndMonitorForKeyUse(\n    'react_key_warning',\n    'Each child in an array should have a unique \"key\" prop.',\n    component,\n    parentType\n  );\n}\n\n/**\n * Warn if the key is being defined as an object property but has an incorrect\n * value.\n *\n * @internal\n * @param {string} name Property name of the key.\n * @param {ReactComponent} component Component that requires a key.\n * @param {*} parentType component's parent's type.\n */\nfunction validatePropertyKey(name, component, parentType) {\n  if (!NUMERIC_PROPERTY_REGEX.test(name)) {\n    return;\n  }\n  warnAndMonitorForKeyUse(\n    'react_numeric_key_warning',\n    'Child objects should have non-numeric keys so ordering is preserved.',\n    component,\n    parentType\n  );\n}\n\n/**\n * Shared warning and monitoring code for the key warnings.\n *\n * @internal\n * @param {string} warningID The id used when logging.\n * @param {string} message The base warning that gets output.\n * @param {ReactComponent} component Component that requires a key.\n * @param {*} parentType component's parent's type.\n */\nfunction warnAndMonitorForKeyUse(warningID, message, component, parentType) {\n  var ownerName = getCurrentOwnerDisplayName();\n  var parentName = parentType.displayName;\n\n  var useName = ownerName || parentName;\n  var memoizer = ownerHasKeyUseWarning[warningID];\n  if (memoizer.hasOwnProperty(useName)) {\n    return;\n  }\n  memoizer[useName] = true;\n\n  message += ownerName ?\n    (\" Check the render method of \" + ownerName + \".\") :\n    (\" Check the renderComponent call using <\" + parentName + \">.\");\n\n  // Usually the current owner is the offender, but if it accepts children as a\n  // property, it may be the creator of the child that's responsible for\n  // assigning it a key.\n  var childOwnerName = null;\n  if (component._owner && component._owner !== ReactCurrentOwner.current) {\n    // Name of the component that originally created this child.\n    childOwnerName = component._owner.constructor.displayName;\n\n    message += (\" It was passed a child from \" + childOwnerName + \".\");\n  }\n\n  message += ' See http://fb.me/react-warning-keys for more information.';\n  monitorCodeUse(warningID, {\n    component: useName,\n    componentOwner: childOwnerName\n  });\n  console.warn(message);\n}\n\n/**\n * Log that we're using an object map. We're considering deprecating this\n * feature and replace it with proper Map and ImmutableMap data structures.\n *\n * @internal\n */\nfunction monitorUseOfObjectMap() {\n  var currentName = getCurrentOwnerDisplayName() || '';\n  if (ownerHasMonitoredObjectMap.hasOwnProperty(currentName)) {\n    return;\n  }\n  ownerHasMonitoredObjectMap[currentName] = true;\n  monitorCodeUse('react_object_map_children');\n}\n\n/**\n * Ensure that every component either is passed in a static location, in an\n * array with an explicit keys property defined, or in an object literal\n * with valid key property.\n *\n * @internal\n * @param {*} component Statically passed child of any type.\n * @param {*} parentType component's parent's type.\n * @return {boolean}\n */\nfunction validateChildKeys(component, parentType) {\n  if (Array.isArray(component)) {\n    for (var i = 0; i < component.length; i++) {\n      var child = component[i];\n      if (ReactDescriptor.isValidDescriptor(child)) {\n        validateExplicitKey(child, parentType);\n      }\n    }\n  } else if (ReactDescriptor.isValidDescriptor(component)) {\n    // This component was passed in a valid location.\n    component._store.validated = true;\n  } else if (component && typeof component === 'object') {\n    monitorUseOfObjectMap();\n    for (var name in component) {\n      validatePropertyKey(name, component[name], parentType);\n    }\n  }\n}\n\n/**\n * Assert that the props are valid\n *\n * @param {string} componentName Name of the component for error messages.\n * @param {object} propTypes Map of prop name to a ReactPropType\n * @param {object} props\n * @param {string} location e.g. \"prop\", \"context\", \"child context\"\n * @private\n */\nfunction checkPropTypes(componentName, propTypes, props, location) {\n  for (var propName in propTypes) {\n    if (propTypes.hasOwnProperty(propName)) {\n      var error;\n      // Prop type validation may throw. In case they do, we don't want to\n      // fail the render phase where it didn't fail before. So we log it.\n      // After these have been cleaned up, we'll let them throw.\n      try {\n        error = propTypes[propName](props, propName, componentName, location);\n      } catch (ex) {\n        error = ex;\n      }\n      if (error instanceof Error && !(error.message in loggedTypeFailures)) {\n        // Only monitor this failure once because there tends to be a lot of the\n        // same error.\n        loggedTypeFailures[error.message] = true;\n        // This will soon use the warning module\n        monitorCodeUse(\n          'react_failed_descriptor_type_check',\n          { message: error.message }\n        );\n      }\n    }\n  }\n}\n\nvar ReactDescriptorValidator = {\n\n  /**\n   * Wraps a descriptor factory function in another function which validates\n   * the props and context of the descriptor and warns about any failed type\n   * checks.\n   *\n   * @param {function} factory The original descriptor factory\n   * @param {object?} propTypes A prop type definition set\n   * @param {object?} contextTypes A context type definition set\n   * @return {object} The component descriptor, which may be invalid.\n   * @private\n   */\n  createFactory: function(factory, propTypes, contextTypes) {\n    var validatedFactory = function(props, children) {\n      var descriptor = factory.apply(this, arguments);\n\n      for (var i = 1; i < arguments.length; i++) {\n        validateChildKeys(arguments[i], descriptor.type);\n      }\n\n      var name = descriptor.type.displayName;\n      if (propTypes) {\n        checkPropTypes(\n          name,\n          propTypes,\n          descriptor.props,\n          ReactPropTypeLocations.prop\n        );\n      }\n      if (contextTypes) {\n        checkPropTypes(\n          name,\n          contextTypes,\n          descriptor._context,\n          ReactPropTypeLocations.context\n        );\n      }\n      return descriptor;\n    };\n\n    validatedFactory.prototype = factory.prototype;\n    validatedFactory.type = factory.type;\n\n    // Copy static properties\n    for (var key in factory) {\n      if (factory.hasOwnProperty(key)) {\n        validatedFactory[key] = factory[key];\n      }\n    }\n\n    return validatedFactory;\n  }\n\n};\n\nmodule.exports = ReactDescriptorValidator;\n\n},{\"./ReactCurrentOwner\":40,\"./ReactDescriptor\":56,\"./ReactPropTypeLocations\":74,\"./monitorCodeUse\":148}],58:[function(_dereq_,module,exports){\n/**\n * Copyright 2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule ReactEmptyComponent\n */\n\n\"use strict\";\n\nvar invariant = _dereq_(\"./invariant\");\n\nvar component;\n// This registry keeps track of the React IDs of the components that rendered to\n// `null` (in reality a placeholder such as `noscript`)\nvar nullComponentIdsRegistry = {};\n\nvar ReactEmptyComponentInjection = {\n  injectEmptyComponent: function(emptyComponent) {\n    component = emptyComponent;\n  }\n};\n\n/**\n * @return {ReactComponent} component The injected empty component.\n */\nfunction getEmptyComponent() {\n  (\"production\" !== \"development\" ? invariant(\n    component,\n    'Trying to return null from a render, but no null placeholder component ' +\n    'was injected.'\n  ) : invariant(component));\n  return component();\n}\n\n/**\n * Mark the component as having rendered to null.\n * @param {string} id Component's `_rootNodeID`.\n */\nfunction registerNullComponentID(id) {\n  nullComponentIdsRegistry[id] = true;\n}\n\n/**\n * Unmark the component as having rendered to null: it renders to something now.\n * @param {string} id Component's `_rootNodeID`.\n */\nfunction deregisterNullComponentID(id) {\n  delete nullComponentIdsRegistry[id];\n}\n\n/**\n * @param {string} id Component's `_rootNodeID`.\n * @return {boolean} True if the component is rendered to null.\n */\nfunction isNullComponentID(id) {\n  return nullComponentIdsRegistry[id];\n}\n\nvar ReactEmptyComponent = {\n  deregisterNullComponentID: deregisterNullComponentID,\n  getEmptyComponent: getEmptyComponent,\n  injection: ReactEmptyComponentInjection,\n  isNullComponentID: isNullComponentID,\n  registerNullComponentID: registerNullComponentID\n};\n\nmodule.exports = ReactEmptyComponent;\n\n},{\"./invariant\":134}],59:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule ReactErrorUtils\n * @typechecks\n */\n\n\"use strict\";\n\nvar ReactErrorUtils = {\n  /**\n   * Creates a guarded version of a function. This is supposed to make debugging\n   * of event handlers easier. To aid debugging with the browser's debugger,\n   * this currently simply returns the original function.\n   *\n   * @param {function} func Function to be executed\n   * @param {string} name The name of the guard\n   * @return {function}\n   */\n  guard: function(func, name) {\n    return func;\n  }\n};\n\nmodule.exports = ReactErrorUtils;\n\n},{}],60:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule ReactEventEmitterMixin\n */\n\n\"use strict\";\n\nvar EventPluginHub = _dereq_(\"./EventPluginHub\");\n\nfunction runEventQueueInBatch(events) {\n  EventPluginHub.enqueueEvents(events);\n  EventPluginHub.processEventQueue();\n}\n\nvar ReactEventEmitterMixin = {\n\n  /**\n   * Streams a fired top-level event to `EventPluginHub` where plugins have the\n   * opportunity to create `ReactEvent`s to be dispatched.\n   *\n   * @param {string} topLevelType Record from `EventConstants`.\n   * @param {object} topLevelTarget The listening component root node.\n   * @param {string} topLevelTargetID ID of `topLevelTarget`.\n   * @param {object} nativeEvent Native environment event.\n   */\n  handleTopLevel: function(\n      topLevelType,\n      topLevelTarget,\n      topLevelTargetID,\n      nativeEvent) {\n    var events = EventPluginHub.extractEvents(\n      topLevelType,\n      topLevelTarget,\n      topLevelTargetID,\n      nativeEvent\n    );\n\n    runEventQueueInBatch(events);\n  }\n};\n\nmodule.exports = ReactEventEmitterMixin;\n\n},{\"./EventPluginHub\":18}],61:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule ReactEventListener\n * @typechecks static-only\n */\n\n\"use strict\";\n\nvar EventListener = _dereq_(\"./EventListener\");\nvar ExecutionEnvironment = _dereq_(\"./ExecutionEnvironment\");\nvar PooledClass = _dereq_(\"./PooledClass\");\nvar ReactInstanceHandles = _dereq_(\"./ReactInstanceHandles\");\nvar ReactMount = _dereq_(\"./ReactMount\");\nvar ReactUpdates = _dereq_(\"./ReactUpdates\");\n\nvar getEventTarget = _dereq_(\"./getEventTarget\");\nvar getUnboundedScrollPosition = _dereq_(\"./getUnboundedScrollPosition\");\nvar mixInto = _dereq_(\"./mixInto\");\n\n/**\n * Finds the parent React component of `node`.\n *\n * @param {*} node\n * @return {?DOMEventTarget} Parent container, or `null` if the specified node\n *                           is not nested.\n */\nfunction findParent(node) {\n  // TODO: It may be a good idea to cache this to prevent unnecessary DOM\n  // traversal, but caching is difficult to do correctly without using a\n  // mutation observer to listen for all DOM changes.\n  var nodeID = ReactMount.getID(node);\n  var rootID = ReactInstanceHandles.getReactRootIDFromNodeID(nodeID);\n  var container = ReactMount.findReactContainerForID(rootID);\n  var parent = ReactMount.getFirstReactDOM(container);\n  return parent;\n}\n\n// Used to store ancestor hierarchy in top level callback\nfunction TopLevelCallbackBookKeeping(topLevelType, nativeEvent) {\n  this.topLevelType = topLevelType;\n  this.nativeEvent = nativeEvent;\n  this.ancestors = [];\n}\nmixInto(TopLevelCallbackBookKeeping, {\n  destructor: function() {\n    this.topLevelType = null;\n    this.nativeEvent = null;\n    this.ancestors.length = 0;\n  }\n});\nPooledClass.addPoolingTo(\n  TopLevelCallbackBookKeeping,\n  PooledClass.twoArgumentPooler\n);\n\nfunction handleTopLevelImpl(bookKeeping) {\n  var topLevelTarget = ReactMount.getFirstReactDOM(\n    getEventTarget(bookKeeping.nativeEvent)\n  ) || window;\n\n  // Loop through the hierarchy, in case there's any nested components.\n  // It's important that we build the array of ancestors before calling any\n  // event handlers, because event handlers can modify the DOM, leading to\n  // inconsistencies with ReactMount's node cache. See #1105.\n  var ancestor = topLevelTarget;\n  while (ancestor) {\n    bookKeeping.ancestors.push(ancestor);\n    ancestor = findParent(ancestor);\n  }\n\n  for (var i = 0, l = bookKeeping.ancestors.length; i < l; i++) {\n    topLevelTarget = bookKeeping.ancestors[i];\n    var topLevelTargetID = ReactMount.getID(topLevelTarget) || '';\n    ReactEventListener._handleTopLevel(\n      bookKeeping.topLevelType,\n      topLevelTarget,\n      topLevelTargetID,\n      bookKeeping.nativeEvent\n    );\n  }\n}\n\nfunction scrollValueMonitor(cb) {\n  var scrollPosition = getUnboundedScrollPosition(window);\n  cb(scrollPosition);\n}\n\nvar ReactEventListener = {\n  _enabled: true,\n  _handleTopLevel: null,\n\n  WINDOW_HANDLE: ExecutionEnvironment.canUseDOM ? window : null,\n\n  setHandleTopLevel: function(handleTopLevel) {\n    ReactEventListener._handleTopLevel = handleTopLevel;\n  },\n\n  setEnabled: function(enabled) {\n    ReactEventListener._enabled = !!enabled;\n  },\n\n  isEnabled: function() {\n    return ReactEventListener._enabled;\n  },\n\n\n  /**\n   * Traps top-level events by using event bubbling.\n   *\n   * @param {string} topLevelType Record from `EventConstants`.\n   * @param {string} handlerBaseName Event name (e.g. \"click\").\n   * @param {object} handle Element on which to attach listener.\n   * @return {object} An object with a remove function which will forcefully\n   *                  remove the listener.\n   * @internal\n   */\n  trapBubbledEvent: function(topLevelType, handlerBaseName, handle) {\n    var element = handle;\n    if (!element) {\n      return;\n    }\n    return EventListener.listen(\n      element,\n      handlerBaseName,\n      ReactEventListener.dispatchEvent.bind(null, topLevelType)\n    );\n  },\n\n  /**\n   * Traps a top-level event by using event capturing.\n   *\n   * @param {string} topLevelType Record from `EventConstants`.\n   * @param {string} handlerBaseName Event name (e.g. \"click\").\n   * @param {object} handle Element on which to attach listener.\n   * @return {object} An object with a remove function which will forcefully\n   *                  remove the listener.\n   * @internal\n   */\n  trapCapturedEvent: function(topLevelType, handlerBaseName, handle) {\n    var element = handle;\n    if (!element) {\n      return;\n    }\n    return EventListener.capture(\n      element,\n      handlerBaseName,\n      ReactEventListener.dispatchEvent.bind(null, topLevelType)\n    );\n  },\n\n  monitorScrollValue: function(refresh) {\n    var callback = scrollValueMonitor.bind(null, refresh);\n    EventListener.listen(window, 'scroll', callback);\n    EventListener.listen(window, 'resize', callback);\n  },\n\n  dispatchEvent: function(topLevelType, nativeEvent) {\n    if (!ReactEventListener._enabled) {\n      return;\n    }\n\n    var bookKeeping = TopLevelCallbackBookKeeping.getPooled(\n      topLevelType,\n      nativeEvent\n    );\n    try {\n      // Event queue being processed in the same cycle allows\n      // `preventDefault`.\n      ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping);\n    } finally {\n      TopLevelCallbackBookKeeping.release(bookKeeping);\n    }\n  }\n};\n\nmodule.exports = ReactEventListener;\n\n},{\"./EventListener\":17,\"./ExecutionEnvironment\":22,\"./PooledClass\":28,\"./ReactInstanceHandles\":64,\"./ReactMount\":67,\"./ReactUpdates\":87,\"./getEventTarget\":125,\"./getUnboundedScrollPosition\":130,\"./mixInto\":147}],62:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule ReactInjection\n */\n\n\"use strict\";\n\nvar DOMProperty = _dereq_(\"./DOMProperty\");\nvar EventPluginHub = _dereq_(\"./EventPluginHub\");\nvar ReactComponent = _dereq_(\"./ReactComponent\");\nvar ReactCompositeComponent = _dereq_(\"./ReactCompositeComponent\");\nvar ReactDOM = _dereq_(\"./ReactDOM\");\nvar ReactEmptyComponent = _dereq_(\"./ReactEmptyComponent\");\nvar ReactBrowserEventEmitter = _dereq_(\"./ReactBrowserEventEmitter\");\nvar ReactPerf = _dereq_(\"./ReactPerf\");\nvar ReactRootIndex = _dereq_(\"./ReactRootIndex\");\nvar ReactUpdates = _dereq_(\"./ReactUpdates\");\n\nvar ReactInjection = {\n  Component: ReactComponent.injection,\n  CompositeComponent: ReactCompositeComponent.injection,\n  DOMProperty: DOMProperty.injection,\n  EmptyComponent: ReactEmptyComponent.injection,\n  EventPluginHub: EventPluginHub.injection,\n  DOM: ReactDOM.injection,\n  EventEmitter: ReactBrowserEventEmitter.injection,\n  Perf: ReactPerf.injection,\n  RootIndex: ReactRootIndex.injection,\n  Updates: ReactUpdates.injection\n};\n\nmodule.exports = ReactInjection;\n\n},{\"./DOMProperty\":11,\"./EventPluginHub\":18,\"./ReactBrowserEventEmitter\":31,\"./ReactComponent\":35,\"./ReactCompositeComponent\":38,\"./ReactDOM\":41,\"./ReactEmptyComponent\":58,\"./ReactPerf\":71,\"./ReactRootIndex\":78,\"./ReactUpdates\":87}],63:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule ReactInputSelection\n */\n\n\"use strict\";\n\nvar ReactDOMSelection = _dereq_(\"./ReactDOMSelection\");\n\nvar containsNode = _dereq_(\"./containsNode\");\nvar focusNode = _dereq_(\"./focusNode\");\nvar getActiveElement = _dereq_(\"./getActiveElement\");\n\nfunction isInDocument(node) {\n  return containsNode(document.documentElement, node);\n}\n\n/**\n * @ReactInputSelection: React input selection module. Based on Selection.js,\n * but modified to be suitable for react and has a couple of bug fixes (doesn't\n * assume buttons have range selections allowed).\n * Input selection module for React.\n */\nvar ReactInputSelection = {\n\n  hasSelectionCapabilities: function(elem) {\n    return elem && (\n      (elem.nodeName === 'INPUT' && elem.type === 'text') ||\n      elem.nodeName === 'TEXTAREA' ||\n      elem.contentEditable === 'true'\n    );\n  },\n\n  getSelectionInformation: function() {\n    var focusedElem = getActiveElement();\n    return {\n      focusedElem: focusedElem,\n      selectionRange:\n          ReactInputSelection.hasSelectionCapabilities(focusedElem) ?\n          ReactInputSelection.getSelection(focusedElem) :\n          null\n    };\n  },\n\n  /**\n   * @restoreSelection: If any selection information was potentially lost,\n   * restore it. This is useful when performing operations that could remove dom\n   * nodes and place them back in, resulting in focus being lost.\n   */\n  restoreSelection: function(priorSelectionInformation) {\n    var curFocusedElem = getActiveElement();\n    var priorFocusedElem = priorSelectionInformation.focusedElem;\n    var priorSelectionRange = priorSelectionInformation.selectionRange;\n    if (curFocusedElem !== priorFocusedElem &&\n        isInDocument(priorFocusedElem)) {\n      if (ReactInputSelection.hasSelectionCapabilities(priorFocusedElem)) {\n        ReactInputSelection.setSelection(\n          priorFocusedElem,\n          priorSelectionRange\n        );\n      }\n      focusNode(priorFocusedElem);\n    }\n  },\n\n  /**\n   * @getSelection: Gets the selection bounds of a focused textarea, input or\n   * contentEditable node.\n   * -@input: Look up selection bounds of this input\n   * -@return {start: selectionStart, end: selectionEnd}\n   */\n  getSelection: function(input) {\n    var selection;\n\n    if ('selectionStart' in input) {\n      // Modern browser with input or textarea.\n      selection = {\n        start: input.selectionStart,\n        end: input.selectionEnd\n      };\n    } else if (document.selection && input.nodeName === 'INPUT') {\n      // IE8 input.\n      var range = document.selection.createRange();\n      // There can only be one selection per document in IE, so it must\n      // be in our element.\n      if (range.parentElement() === input) {\n        selection = {\n          start: -range.moveStart('character', -input.value.length),\n          end: -range.moveEnd('character', -input.value.length)\n        };\n      }\n    } else {\n      // Content editable or old IE textarea.\n      selection = ReactDOMSelection.getOffsets(input);\n    }\n\n    return selection || {start: 0, end: 0};\n  },\n\n  /**\n   * @setSelection: Sets the selection bounds of a textarea or input and focuses\n   * the input.\n   * -@input     Set selection bounds of this input or textarea\n   * -@offsets   Object of same form that is returned from get*\n   */\n  setSelection: function(input, offsets) {\n    var start = offsets.start;\n    var end = offsets.end;\n    if (typeof end === 'undefined') {\n      end = start;\n    }\n\n    if ('selectionStart' in input) {\n      input.selectionStart = start;\n      input.selectionEnd = Math.min(end, input.value.length);\n    } else if (document.selection && input.nodeName === 'INPUT') {\n      var range = input.createTextRange();\n      range.collapse(true);\n      range.moveStart('character', start);\n      range.moveEnd('character', end - start);\n      range.select();\n    } else {\n      ReactDOMSelection.setOffsets(input, offsets);\n    }\n  }\n};\n\nmodule.exports = ReactInputSelection;\n\n},{\"./ReactDOMSelection\":50,\"./containsNode\":109,\"./focusNode\":120,\"./getActiveElement\":122}],64:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule ReactInstanceHandles\n * @typechecks static-only\n */\n\n\"use strict\";\n\nvar ReactRootIndex = _dereq_(\"./ReactRootIndex\");\n\nvar invariant = _dereq_(\"./invariant\");\n\nvar SEPARATOR = '.';\nvar SEPARATOR_LENGTH = SEPARATOR.length;\n\n/**\n * Maximum depth of traversals before we consider the possibility of a bad ID.\n */\nvar MAX_TREE_DEPTH = 100;\n\n/**\n * Creates a DOM ID prefix to use when mounting React components.\n *\n * @param {number} index A unique integer\n * @return {string} React root ID.\n * @internal\n */\nfunction getReactRootIDString(index) {\n  return SEPARATOR + index.toString(36);\n}\n\n/**\n * Checks if a character in the supplied ID is a separator or the end.\n *\n * @param {string} id A React DOM ID.\n * @param {number} index Index of the character to check.\n * @return {boolean} True if the character is a separator or end of the ID.\n * @private\n */\nfunction isBoundary(id, index) {\n  return id.charAt(index) === SEPARATOR || index === id.length;\n}\n\n/**\n * Checks if the supplied string is a valid React DOM ID.\n *\n * @param {string} id A React DOM ID, maybe.\n * @return {boolean} True if the string is a valid React DOM ID.\n * @private\n */\nfunction isValidID(id) {\n  return id === '' || (\n    id.charAt(0) === SEPARATOR && id.charAt(id.length - 1) !== SEPARATOR\n  );\n}\n\n/**\n * Checks if the first ID is an ancestor of or equal to the second ID.\n *\n * @param {string} ancestorID\n * @param {string} descendantID\n * @return {boolean} True if `ancestorID` is an ancestor of `descendantID`.\n * @internal\n */\nfunction isAncestorIDOf(ancestorID, descendantID) {\n  return (\n    descendantID.indexOf(ancestorID) === 0 &&\n    isBoundary(descendantID, ancestorID.length)\n  );\n}\n\n/**\n * Gets the parent ID of the supplied React DOM ID, `id`.\n *\n * @param {string} id ID of a component.\n * @return {string} ID of the parent, or an empty string.\n * @private\n */\nfunction getParentID(id) {\n  return id ? id.substr(0, id.lastIndexOf(SEPARATOR)) : '';\n}\n\n/**\n * Gets the next DOM ID on the tree path from the supplied `ancestorID` to the\n * supplied `destinationID`. If they are equal, the ID is returned.\n *\n * @param {string} ancestorID ID of an ancestor node of `destinationID`.\n * @param {string} destinationID ID of the destination node.\n * @return {string} Next ID on the path from `ancestorID` to `destinationID`.\n * @private\n */\nfunction getNextDescendantID(ancestorID, destinationID) {\n  (\"production\" !== \"development\" ? invariant(\n    isValidID(ancestorID) && isValidID(destinationID),\n    'getNextDescendantID(%s, %s): Received an invalid React DOM ID.',\n    ancestorID,\n    destinationID\n  ) : invariant(isValidID(ancestorID) && isValidID(destinationID)));\n  (\"production\" !== \"development\" ? invariant(\n    isAncestorIDOf(ancestorID, destinationID),\n    'getNextDescendantID(...): React has made an invalid assumption about ' +\n    'the DOM hierarchy. Expected `%s` to be an ancestor of `%s`.',\n    ancestorID,\n    destinationID\n  ) : invariant(isAncestorIDOf(ancestorID, destinationID)));\n  if (ancestorID === destinationID) {\n    return ancestorID;\n  }\n  // Skip over the ancestor and the immediate separator. Traverse until we hit\n  // another separator or we reach the end of `destinationID`.\n  var start = ancestorID.length + SEPARATOR_LENGTH;\n  for (var i = start; i < destinationID.length; i++) {\n    if (isBoundary(destinationID, i)) {\n      break;\n    }\n  }\n  return destinationID.substr(0, i);\n}\n\n/**\n * Gets the nearest common ancestor ID of two IDs.\n *\n * Using this ID scheme, the nearest common ancestor ID is the longest common\n * prefix of the two IDs that immediately preceded a \"marker\" in both strings.\n *\n * @param {string} oneID\n * @param {string} twoID\n * @return {string} Nearest common ancestor ID, or the empty string if none.\n * @private\n */\nfunction getFirstCommonAncestorID(oneID, twoID) {\n  var minLength = Math.min(oneID.length, twoID.length);\n  if (minLength === 0) {\n    return '';\n  }\n  var lastCommonMarkerIndex = 0;\n  // Use `<=` to traverse until the \"EOL\" of the shorter string.\n  for (var i = 0; i <= minLength; i++) {\n    if (isBoundary(oneID, i) && isBoundary(twoID, i)) {\n      lastCommonMarkerIndex = i;\n    } else if (oneID.charAt(i) !== twoID.charAt(i)) {\n      break;\n    }\n  }\n  var longestCommonID = oneID.substr(0, lastCommonMarkerIndex);\n  (\"production\" !== \"development\" ? invariant(\n    isValidID(longestCommonID),\n    'getFirstCommonAncestorID(%s, %s): Expected a valid React DOM ID: %s',\n    oneID,\n    twoID,\n    longestCommonID\n  ) : invariant(isValidID(longestCommonID)));\n  return longestCommonID;\n}\n\n/**\n * Traverses the parent path between two IDs (either up or down). The IDs must\n * not be the same, and there must exist a parent path between them. If the\n * callback returns `false`, traversal is stopped.\n *\n * @param {?string} start ID at which to start traversal.\n * @param {?string} stop ID at which to end traversal.\n * @param {function} cb Callback to invoke each ID with.\n * @param {?boolean} skipFirst Whether or not to skip the first node.\n * @param {?boolean} skipLast Whether or not to skip the last node.\n * @private\n */\nfunction traverseParentPath(start, stop, cb, arg, skipFirst, skipLast) {\n  start = start || '';\n  stop = stop || '';\n  (\"production\" !== \"development\" ? invariant(\n    start !== stop,\n    'traverseParentPath(...): Cannot traverse from and to the same ID, `%s`.',\n    start\n  ) : invariant(start !== stop));\n  var traverseUp = isAncestorIDOf(stop, start);\n  (\"production\" !== \"development\" ? invariant(\n    traverseUp || isAncestorIDOf(start, stop),\n    'traverseParentPath(%s, %s, ...): Cannot traverse from two IDs that do ' +\n    'not have a parent path.',\n    start,\n    stop\n  ) : invariant(traverseUp || isAncestorIDOf(start, stop)));\n  // Traverse from `start` to `stop` one depth at a time.\n  var depth = 0;\n  var traverse = traverseUp ? getParentID : getNextDescendantID;\n  for (var id = start; /* until break */; id = traverse(id, stop)) {\n    var ret;\n    if ((!skipFirst || id !== start) && (!skipLast || id !== stop)) {\n      ret = cb(id, traverseUp, arg);\n    }\n    if (ret === false || id === stop) {\n      // Only break //after// visiting `stop`.\n      break;\n    }\n    (\"production\" !== \"development\" ? invariant(\n      depth++ < MAX_TREE_DEPTH,\n      'traverseParentPath(%s, %s, ...): Detected an infinite loop while ' +\n      'traversing the React DOM ID tree. This may be due to malformed IDs: %s',\n      start, stop\n    ) : invariant(depth++ < MAX_TREE_DEPTH));\n  }\n}\n\n/**\n * Manages the IDs assigned to DOM representations of React components. This\n * uses a specific scheme in order to traverse the DOM efficiently (e.g. in\n * order to simulate events).\n *\n * @internal\n */\nvar ReactInstanceHandles = {\n\n  /**\n   * Constructs a React root ID\n   * @return {string} A React root ID.\n   */\n  createReactRootID: function() {\n    return getReactRootIDString(ReactRootIndex.createReactRootIndex());\n  },\n\n  /**\n   * Constructs a React ID by joining a root ID with a name.\n   *\n   * @param {string} rootID Root ID of a parent component.\n   * @param {string} name A component's name (as flattened children).\n   * @return {string} A React ID.\n   * @internal\n   */\n  createReactID: function(rootID, name) {\n    return rootID + name;\n  },\n\n  /**\n   * Gets the DOM ID of the React component that is the root of the tree that\n   * contains the React component with the supplied DOM ID.\n   *\n   * @param {string} id DOM ID of a React component.\n   * @return {?string} DOM ID of the React component that is the root.\n   * @internal\n   */\n  getReactRootIDFromNodeID: function(id) {\n    if (id && id.charAt(0) === SEPARATOR && id.length > 1) {\n      var index = id.indexOf(SEPARATOR, 1);\n      return index > -1 ? id.substr(0, index) : id;\n    }\n    return null;\n  },\n\n  /**\n   * Traverses the ID hierarchy and invokes the supplied `cb` on any IDs that\n   * should would receive a `mouseEnter` or `mouseLeave` event.\n   *\n   * NOTE: Does not invoke the callback on the nearest common ancestor because\n   * nothing \"entered\" or \"left\" that element.\n   *\n   * @param {string} leaveID ID being left.\n   * @param {string} enterID ID being entered.\n   * @param {function} cb Callback to invoke on each entered/left ID.\n   * @param {*} upArg Argument to invoke the callback with on left IDs.\n   * @param {*} downArg Argument to invoke the callback with on entered IDs.\n   * @internal\n   */\n  traverseEnterLeave: function(leaveID, enterID, cb, upArg, downArg) {\n    var ancestorID = getFirstCommonAncestorID(leaveID, enterID);\n    if (ancestorID !== leaveID) {\n      traverseParentPath(leaveID, ancestorID, cb, upArg, false, true);\n    }\n    if (ancestorID !== enterID) {\n      traverseParentPath(ancestorID, enterID, cb, downArg, true, false);\n    }\n  },\n\n  /**\n   * Simulates the traversal of a two-phase, capture/bubble event dispatch.\n   *\n   * NOTE: This traversal happens on IDs without touching the DOM.\n   *\n   * @param {string} targetID ID of the target node.\n   * @param {function} cb Callback to invoke.\n   * @param {*} arg Argument to invoke the callback with.\n   * @internal\n   */\n  traverseTwoPhase: function(targetID, cb, arg) {\n    if (targetID) {\n      traverseParentPath('', targetID, cb, arg, true, false);\n      traverseParentPath(targetID, '', cb, arg, false, true);\n    }\n  },\n\n  /**\n   * Traverse a node ID, calling the supplied `cb` for each ancestor ID. For\n   * example, passing `.0.$row-0.1` would result in `cb` getting called\n   * with `.0`, `.0.$row-0`, and `.0.$row-0.1`.\n   *\n   * NOTE: This traversal happens on IDs without touching the DOM.\n   *\n   * @param {string} targetID ID of the target node.\n   * @param {function} cb Callback to invoke.\n   * @param {*} arg Argument to invoke the callback with.\n   * @internal\n   */\n  traverseAncestors: function(targetID, cb, arg) {\n    traverseParentPath('', targetID, cb, arg, true, false);\n  },\n\n  /**\n   * Exposed for unit testing.\n   * @private\n   */\n  _getFirstCommonAncestorID: getFirstCommonAncestorID,\n\n  /**\n   * Exposed for unit testing.\n   * @private\n   */\n  _getNextDescendantID: getNextDescendantID,\n\n  isAncestorIDOf: isAncestorIDOf,\n\n  SEPARATOR: SEPARATOR\n\n};\n\nmodule.exports = ReactInstanceHandles;\n\n},{\"./ReactRootIndex\":78,\"./invariant\":134}],65:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule ReactLink\n * @typechecks static-only\n */\n\n\"use strict\";\n\n/**\n * ReactLink encapsulates a common pattern in which a component wants to modify\n * a prop received from its parent. ReactLink allows the parent to pass down a\n * value coupled with a callback that, when invoked, expresses an intent to\n * modify that value. For example:\n *\n * React.createClass({\n *   getInitialState: function() {\n *     return {value: ''};\n *   },\n *   render: function() {\n *     var valueLink = new ReactLink(this.state.value, this._handleValueChange);\n *     return <input valueLink={valueLink} />;\n *   },\n *   this._handleValueChange: function(newValue) {\n *     this.setState({value: newValue});\n *   }\n * });\n *\n * We have provided some sugary mixins to make the creation and\n * consumption of ReactLink easier; see LinkedValueUtils and LinkedStateMixin.\n */\n\nvar React = _dereq_(\"./React\");\n\n/**\n * @param {*} value current value of the link\n * @param {function} requestChange callback to request a change\n */\nfunction ReactLink(value, requestChange) {\n  this.value = value;\n  this.requestChange = requestChange;\n}\n\n/**\n * Creates a PropType that enforces the ReactLink API and optionally checks the\n * type of the value being passed inside the link. Example:\n *\n * MyComponent.propTypes = {\n *   tabIndexLink: ReactLink.PropTypes.link(React.PropTypes.number)\n * }\n */\nfunction createLinkTypeChecker(linkType) {\n  var shapes = {\n    value: typeof linkType === 'undefined' ?\n      React.PropTypes.any.isRequired :\n      linkType.isRequired,\n    requestChange: React.PropTypes.func.isRequired\n  };\n  return React.PropTypes.shape(shapes);\n}\n\nReactLink.PropTypes = {\n  link: createLinkTypeChecker\n};\n\nmodule.exports = ReactLink;\n\n},{\"./React\":29}],66:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule ReactMarkupChecksum\n */\n\n\"use strict\";\n\nvar adler32 = _dereq_(\"./adler32\");\n\nvar ReactMarkupChecksum = {\n  CHECKSUM_ATTR_NAME: 'data-react-checksum',\n\n  /**\n   * @param {string} markup Markup string\n   * @return {string} Markup string with checksum attribute attached\n   */\n  addChecksumToMarkup: function(markup) {\n    var checksum = adler32(markup);\n    return markup.replace(\n      '>',\n      ' ' + ReactMarkupChecksum.CHECKSUM_ATTR_NAME + '=\"' + checksum + '\">'\n    );\n  },\n\n  /**\n   * @param {string} markup to use\n   * @param {DOMElement} element root React element\n   * @returns {boolean} whether or not the markup is the same\n   */\n  canReuseMarkup: function(markup, element) {\n    var existingChecksum = element.getAttribute(\n      ReactMarkupChecksum.CHECKSUM_ATTR_NAME\n    );\n    existingChecksum = existingChecksum && parseInt(existingChecksum, 10);\n    var markupChecksum = adler32(markup);\n    return markupChecksum === existingChecksum;\n  }\n};\n\nmodule.exports = ReactMarkupChecksum;\n\n},{\"./adler32\":107}],67:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule ReactMount\n */\n\n\"use strict\";\n\nvar DOMProperty = _dereq_(\"./DOMProperty\");\nvar ReactBrowserEventEmitter = _dereq_(\"./ReactBrowserEventEmitter\");\nvar ReactCurrentOwner = _dereq_(\"./ReactCurrentOwner\");\nvar ReactDescriptor = _dereq_(\"./ReactDescriptor\");\nvar ReactInstanceHandles = _dereq_(\"./ReactInstanceHandles\");\nvar ReactPerf = _dereq_(\"./ReactPerf\");\n\nvar containsNode = _dereq_(\"./containsNode\");\nvar getReactRootElementInContainer = _dereq_(\"./getReactRootElementInContainer\");\nvar instantiateReactComponent = _dereq_(\"./instantiateReactComponent\");\nvar invariant = _dereq_(\"./invariant\");\nvar shouldUpdateReactComponent = _dereq_(\"./shouldUpdateReactComponent\");\nvar warning = _dereq_(\"./warning\");\n\nvar SEPARATOR = ReactInstanceHandles.SEPARATOR;\n\nvar ATTR_NAME = DOMProperty.ID_ATTRIBUTE_NAME;\nvar nodeCache = {};\n\nvar ELEMENT_NODE_TYPE = 1;\nvar DOC_NODE_TYPE = 9;\n\n/** Mapping from reactRootID to React component instance. */\nvar instancesByReactRootID = {};\n\n/** Mapping from reactRootID to `container` nodes. */\nvar containersByReactRootID = {};\n\nif (\"production\" !== \"development\") {\n  /** __DEV__-only mapping from reactRootID to root elements. */\n  var rootElementsByReactRootID = {};\n}\n\n// Used to store breadth-first search state in findComponentRoot.\nvar findComponentRootReusableArray = [];\n\n/**\n * @param {DOMElement} container DOM element that may contain a React component.\n * @return {?string} A \"reactRoot\" ID, if a React component is rendered.\n */\nfunction getReactRootID(container) {\n  var rootElement = getReactRootElementInContainer(container);\n  return rootElement && ReactMount.getID(rootElement);\n}\n\n/**\n * Accessing node[ATTR_NAME] or calling getAttribute(ATTR_NAME) on a form\n * element can return its control whose name or ID equals ATTR_NAME. All\n * DOM nodes support `getAttributeNode` but this can also get called on\n * other objects so just return '' if we're given something other than a\n * DOM node (such as window).\n *\n * @param {?DOMElement|DOMWindow|DOMDocument|DOMTextNode} node DOM node.\n * @return {string} ID of the supplied `domNode`.\n */\nfunction getID(node) {\n  var id = internalGetID(node);\n  if (id) {\n    if (nodeCache.hasOwnProperty(id)) {\n      var cached = nodeCache[id];\n      if (cached !== node) {\n        (\"production\" !== \"development\" ? invariant(\n          !isValid(cached, id),\n          'ReactMount: Two valid but unequal nodes with the same `%s`: %s',\n          ATTR_NAME, id\n        ) : invariant(!isValid(cached, id)));\n\n        nodeCache[id] = node;\n      }\n    } else {\n      nodeCache[id] = node;\n    }\n  }\n\n  return id;\n}\n\nfunction internalGetID(node) {\n  // If node is something like a window, document, or text node, none of\n  // which support attributes or a .getAttribute method, gracefully return\n  // the empty string, as if the attribute were missing.\n  return node && node.getAttribute && node.getAttribute(ATTR_NAME) || '';\n}\n\n/**\n * Sets the React-specific ID of the given node.\n *\n * @param {DOMElement} node The DOM node whose ID will be set.\n * @param {string} id The value of the ID attribute.\n */\nfunction setID(node, id) {\n  var oldID = internalGetID(node);\n  if (oldID !== id) {\n    delete nodeCache[oldID];\n  }\n  node.setAttribute(ATTR_NAME, id);\n  nodeCache[id] = node;\n}\n\n/**\n * Finds the node with the supplied React-generated DOM ID.\n *\n * @param {string} id A React-generated DOM ID.\n * @return {DOMElement} DOM node with the suppled `id`.\n * @internal\n */\nfunction getNode(id) {\n  if (!nodeCache.hasOwnProperty(id) || !isValid(nodeCache[id], id)) {\n    nodeCache[id] = ReactMount.findReactNodeByID(id);\n  }\n  return nodeCache[id];\n}\n\n/**\n * A node is \"valid\" if it is contained by a currently mounted container.\n *\n * This means that the node does not have to be contained by a document in\n * order to be considered valid.\n *\n * @param {?DOMElement} node The candidate DOM node.\n * @param {string} id The expected ID of the node.\n * @return {boolean} Whether the node is contained by a mounted container.\n */\nfunction isValid(node, id) {\n  if (node) {\n    (\"production\" !== \"development\" ? invariant(\n      internalGetID(node) === id,\n      'ReactMount: Unexpected modification of `%s`',\n      ATTR_NAME\n    ) : invariant(internalGetID(node) === id));\n\n    var container = ReactMount.findReactContainerForID(id);\n    if (container && containsNode(container, node)) {\n      return true;\n    }\n  }\n\n  return false;\n}\n\n/**\n * Causes the cache to forget about one React-specific ID.\n *\n * @param {string} id The ID to forget.\n */\nfunction purgeID(id) {\n  delete nodeCache[id];\n}\n\nvar deepestNodeSoFar = null;\nfunction findDeepestCachedAncestorImpl(ancestorID) {\n  var ancestor = nodeCache[ancestorID];\n  if (ancestor && isValid(ancestor, ancestorID)) {\n    deepestNodeSoFar = ancestor;\n  } else {\n    // This node isn't populated in the cache, so presumably none of its\n    // descendants are. Break out of the loop.\n    return false;\n  }\n}\n\n/**\n * Return the deepest cached node whose ID is a prefix of `targetID`.\n */\nfunction findDeepestCachedAncestor(targetID) {\n  deepestNodeSoFar = null;\n  ReactInstanceHandles.traverseAncestors(\n    targetID,\n    findDeepestCachedAncestorImpl\n  );\n\n  var foundNode = deepestNodeSoFar;\n  deepestNodeSoFar = null;\n  return foundNode;\n}\n\n/**\n * Mounting is the process of initializing a React component by creatings its\n * representative DOM elements and inserting them into a supplied `container`.\n * Any prior content inside `container` is destroyed in the process.\n *\n *   ReactMount.renderComponent(\n *     component,\n *     document.getElementById('container')\n *   );\n *\n *   <div id=\"container\">                   <-- Supplied `container`.\n *     <div data-reactid=\".3\">              <-- Rendered reactRoot of React\n *       // ...                                 component.\n *     </div>\n *   </div>\n *\n * Inside of `container`, the first element rendered is the \"reactRoot\".\n */\nvar ReactMount = {\n  /** Exposed for debugging purposes **/\n  _instancesByReactRootID: instancesByReactRootID,\n\n  /**\n   * This is a hook provided to support rendering React components while\n   * ensuring that the apparent scroll position of its `container` does not\n   * change.\n   *\n   * @param {DOMElement} container The `container` being rendered into.\n   * @param {function} renderCallback This must be called once to do the render.\n   */\n  scrollMonitor: function(container, renderCallback) {\n    renderCallback();\n  },\n\n  /**\n   * Take a component that's already mounted into the DOM and replace its props\n   * @param {ReactComponent} prevComponent component instance already in the DOM\n   * @param {ReactComponent} nextComponent component instance to render\n   * @param {DOMElement} container container to render into\n   * @param {?function} callback function triggered on completion\n   */\n  _updateRootComponent: function(\n      prevComponent,\n      nextComponent,\n      container,\n      callback) {\n    var nextProps = nextComponent.props;\n    ReactMount.scrollMonitor(container, function() {\n      prevComponent.replaceProps(nextProps, callback);\n    });\n\n    if (\"production\" !== \"development\") {\n      // Record the root element in case it later gets transplanted.\n      rootElementsByReactRootID[getReactRootID(container)] =\n        getReactRootElementInContainer(container);\n    }\n\n    return prevComponent;\n  },\n\n  /**\n   * Register a component into the instance map and starts scroll value\n   * monitoring\n   * @param {ReactComponent} nextComponent component instance to render\n   * @param {DOMElement} container container to render into\n   * @return {string} reactRoot ID prefix\n   */\n  _registerComponent: function(nextComponent, container) {\n    (\"production\" !== \"development\" ? invariant(\n      container && (\n        container.nodeType === ELEMENT_NODE_TYPE ||\n        container.nodeType === DOC_NODE_TYPE\n      ),\n      '_registerComponent(...): Target container is not a DOM element.'\n    ) : invariant(container && (\n      container.nodeType === ELEMENT_NODE_TYPE ||\n      container.nodeType === DOC_NODE_TYPE\n    )));\n\n    ReactBrowserEventEmitter.ensureScrollValueMonitoring();\n\n    var reactRootID = ReactMount.registerContainer(container);\n    instancesByReactRootID[reactRootID] = nextComponent;\n    return reactRootID;\n  },\n\n  /**\n   * Render a new component into the DOM.\n   * @param {ReactComponent} nextComponent component instance to render\n   * @param {DOMElement} container container to render into\n   * @param {boolean} shouldReuseMarkup if we should skip the markup insertion\n   * @return {ReactComponent} nextComponent\n   */\n  _renderNewRootComponent: ReactPerf.measure(\n    'ReactMount',\n    '_renderNewRootComponent',\n    function(\n        nextComponent,\n        container,\n        shouldReuseMarkup) {\n      // Various parts of our code (such as ReactCompositeComponent's\n      // _renderValidatedComponent) assume that calls to render aren't nested;\n      // verify that that's the case.\n      (\"production\" !== \"development\" ? warning(\n        ReactCurrentOwner.current == null,\n        '_renderNewRootComponent(): Render methods should be a pure function ' +\n        'of props and state; triggering nested component updates from ' +\n        'render is not allowed. If necessary, trigger nested updates in ' +\n        'componentDidUpdate.'\n      ) : null);\n\n      var componentInstance = instantiateReactComponent(nextComponent);\n      var reactRootID = ReactMount._registerComponent(\n        componentInstance,\n        container\n      );\n      componentInstance.mountComponentIntoNode(\n        reactRootID,\n        container,\n        shouldReuseMarkup\n      );\n\n      if (\"production\" !== \"development\") {\n        // Record the root element in case it later gets transplanted.\n        rootElementsByReactRootID[reactRootID] =\n          getReactRootElementInContainer(container);\n      }\n\n      return componentInstance;\n    }\n  ),\n\n  /**\n   * Renders a React component into the DOM in the supplied `container`.\n   *\n   * If the React component was previously rendered into `container`, this will\n   * perform an update on it and only mutate the DOM as necessary to reflect the\n   * latest React component.\n   *\n   * @param {ReactDescriptor} nextDescriptor Component descriptor to render.\n   * @param {DOMElement} container DOM element to render into.\n   * @param {?function} callback function triggered on completion\n   * @return {ReactComponent} Component instance rendered in `container`.\n   */\n  renderComponent: function(nextDescriptor, container, callback) {\n    (\"production\" !== \"development\" ? invariant(\n      ReactDescriptor.isValidDescriptor(nextDescriptor),\n      'renderComponent(): Invalid component descriptor.%s',\n      (\n        ReactDescriptor.isValidFactory(nextDescriptor) ?\n          ' Instead of passing a component class, make sure to instantiate ' +\n          'it first by calling it with props.' :\n        // Check if it quacks like a descriptor\n        typeof nextDescriptor.props !== \"undefined\" ?\n          ' This may be caused by unintentionally loading two independent ' +\n          'copies of React.' :\n          ''\n      )\n    ) : invariant(ReactDescriptor.isValidDescriptor(nextDescriptor)));\n\n    var prevComponent = instancesByReactRootID[getReactRootID(container)];\n\n    if (prevComponent) {\n      var prevDescriptor = prevComponent._descriptor;\n      if (shouldUpdateReactComponent(prevDescriptor, nextDescriptor)) {\n        return ReactMount._updateRootComponent(\n          prevComponent,\n          nextDescriptor,\n          container,\n          callback\n        );\n      } else {\n        ReactMount.unmountComponentAtNode(container);\n      }\n    }\n\n    var reactRootElement = getReactRootElementInContainer(container);\n    var containerHasReactMarkup =\n      reactRootElement && ReactMount.isRenderedByReact(reactRootElement);\n\n    var shouldReuseMarkup = containerHasReactMarkup && !prevComponent;\n\n    var component = ReactMount._renderNewRootComponent(\n      nextDescriptor,\n      container,\n      shouldReuseMarkup\n    );\n    callback && callback.call(component);\n    return component;\n  },\n\n  /**\n   * Constructs a component instance of `constructor` with `initialProps` and\n   * renders it into the supplied `container`.\n   *\n   * @param {function} constructor React component constructor.\n   * @param {?object} props Initial props of the component instance.\n   * @param {DOMElement} container DOM element to render into.\n   * @return {ReactComponent} Component instance rendered in `container`.\n   */\n  constructAndRenderComponent: function(constructor, props, container) {\n    return ReactMount.renderComponent(constructor(props), container);\n  },\n\n  /**\n   * Constructs a component instance of `constructor` with `initialProps` and\n   * renders it into a container node identified by supplied `id`.\n   *\n   * @param {function} componentConstructor React component constructor\n   * @param {?object} props Initial props of the component instance.\n   * @param {string} id ID of the DOM element to render into.\n   * @return {ReactComponent} Component instance rendered in the container node.\n   */\n  constructAndRenderComponentByID: function(constructor, props, id) {\n    var domNode = document.getElementById(id);\n    (\"production\" !== \"development\" ? invariant(\n      domNode,\n      'Tried to get element with id of \"%s\" but it is not present on the page.',\n      id\n    ) : invariant(domNode));\n    return ReactMount.constructAndRenderComponent(constructor, props, domNode);\n  },\n\n  /**\n   * Registers a container node into which React components will be rendered.\n   * This also creates the \"reactRoot\" ID that will be assigned to the element\n   * rendered within.\n   *\n   * @param {DOMElement} container DOM element to register as a container.\n   * @return {string} The \"reactRoot\" ID of elements rendered within.\n   */\n  registerContainer: function(container) {\n    var reactRootID = getReactRootID(container);\n    if (reactRootID) {\n      // If one exists, make sure it is a valid \"reactRoot\" ID.\n      reactRootID = ReactInstanceHandles.getReactRootIDFromNodeID(reactRootID);\n    }\n    if (!reactRootID) {\n      // No valid \"reactRoot\" ID found, create one.\n      reactRootID = ReactInstanceHandles.createReactRootID();\n    }\n    containersByReactRootID[reactRootID] = container;\n    return reactRootID;\n  },\n\n  /**\n   * Unmounts and destroys the React component rendered in the `container`.\n   *\n   * @param {DOMElement} container DOM element containing a React component.\n   * @return {boolean} True if a component was found in and unmounted from\n   *                   `container`\n   */\n  unmountComponentAtNode: function(container) {\n    // Various parts of our code (such as ReactCompositeComponent's\n    // _renderValidatedComponent) assume that calls to render aren't nested;\n    // verify that that's the case. (Strictly speaking, unmounting won't cause a\n    // render but we still don't expect to be in a render call here.)\n    (\"production\" !== \"development\" ? warning(\n      ReactCurrentOwner.current == null,\n      'unmountComponentAtNode(): Render methods should be a pure function of ' +\n      'props and state; triggering nested component updates from render is ' +\n      'not allowed. If necessary, trigger nested updates in ' +\n      'componentDidUpdate.'\n    ) : null);\n\n    var reactRootID = getReactRootID(container);\n    var component = instancesByReactRootID[reactRootID];\n    if (!component) {\n      return false;\n    }\n    ReactMount.unmountComponentFromNode(component, container);\n    delete instancesByReactRootID[reactRootID];\n    delete containersByReactRootID[reactRootID];\n    if (\"production\" !== \"development\") {\n      delete rootElementsByReactRootID[reactRootID];\n    }\n    return true;\n  },\n\n  /**\n   * Unmounts a component and removes it from the DOM.\n   *\n   * @param {ReactComponent} instance React component instance.\n   * @param {DOMElement} container DOM element to unmount from.\n   * @final\n   * @internal\n   * @see {ReactMount.unmountComponentAtNode}\n   */\n  unmountComponentFromNode: function(instance, container) {\n    instance.unmountComponent();\n\n    if (container.nodeType === DOC_NODE_TYPE) {\n      container = container.documentElement;\n    }\n\n    // http://jsperf.com/emptying-a-node\n    while (container.lastChild) {\n      container.removeChild(container.lastChild);\n    }\n  },\n\n  /**\n   * Finds the container DOM element that contains React component to which the\n   * supplied DOM `id` belongs.\n   *\n   * @param {string} id The ID of an element rendered by a React component.\n   * @return {?DOMElement} DOM element that contains the `id`.\n   */\n  findReactContainerForID: function(id) {\n    var reactRootID = ReactInstanceHandles.getReactRootIDFromNodeID(id);\n    var container = containersByReactRootID[reactRootID];\n\n    if (\"production\" !== \"development\") {\n      var rootElement = rootElementsByReactRootID[reactRootID];\n      if (rootElement && rootElement.parentNode !== container) {\n        (\"production\" !== \"development\" ? invariant(\n          // Call internalGetID here because getID calls isValid which calls\n          // findReactContainerForID (this function).\n          internalGetID(rootElement) === reactRootID,\n          'ReactMount: Root element ID differed from reactRootID.'\n        ) : invariant(// Call internalGetID here because getID calls isValid which calls\n        // findReactContainerForID (this function).\n        internalGetID(rootElement) === reactRootID));\n\n        var containerChild = container.firstChild;\n        if (containerChild &&\n            reactRootID === internalGetID(containerChild)) {\n          // If the container has a new child with the same ID as the old\n          // root element, then rootElementsByReactRootID[reactRootID] is\n          // just stale and needs to be updated. The case that deserves a\n          // warning is when the container is empty.\n          rootElementsByReactRootID[reactRootID] = containerChild;\n        } else {\n          console.warn(\n            'ReactMount: Root element has been removed from its original ' +\n            'container. New container:', rootElement.parentNode\n          );\n        }\n      }\n    }\n\n    return container;\n  },\n\n  /**\n   * Finds an element rendered by React with the supplied ID.\n   *\n   * @param {string} id ID of a DOM node in the React component.\n   * @return {DOMElement} Root DOM node of the React component.\n   */\n  findReactNodeByID: function(id) {\n    var reactRoot = ReactMount.findReactContainerForID(id);\n    return ReactMount.findComponentRoot(reactRoot, id);\n  },\n\n  /**\n   * True if the supplied `node` is rendered by React.\n   *\n   * @param {*} node DOM Element to check.\n   * @return {boolean} True if the DOM Element appears to be rendered by React.\n   * @internal\n   */\n  isRenderedByReact: function(node) {\n    if (node.nodeType !== 1) {\n      // Not a DOMElement, therefore not a React component\n      return false;\n    }\n    var id = ReactMount.getID(node);\n    return id ? id.charAt(0) === SEPARATOR : false;\n  },\n\n  /**\n   * Traverses up the ancestors of the supplied node to find a node that is a\n   * DOM representation of a React component.\n   *\n   * @param {*} node\n   * @return {?DOMEventTarget}\n   * @internal\n   */\n  getFirstReactDOM: function(node) {\n    var current = node;\n    while (current && current.parentNode !== current) {\n      if (ReactMount.isRenderedByReact(current)) {\n        return current;\n      }\n      current = current.parentNode;\n    }\n    return null;\n  },\n\n  /**\n   * Finds a node with the supplied `targetID` inside of the supplied\n   * `ancestorNode`.  Exploits the ID naming scheme to perform the search\n   * quickly.\n   *\n   * @param {DOMEventTarget} ancestorNode Search from this root.\n   * @pararm {string} targetID ID of the DOM representation of the component.\n   * @return {DOMEventTarget} DOM node with the supplied `targetID`.\n   * @internal\n   */\n  findComponentRoot: function(ancestorNode, targetID) {\n    var firstChildren = findComponentRootReusableArray;\n    var childIndex = 0;\n\n    var deepestAncestor = findDeepestCachedAncestor(targetID) || ancestorNode;\n\n    firstChildren[0] = deepestAncestor.firstChild;\n    firstChildren.length = 1;\n\n    while (childIndex < firstChildren.length) {\n      var child = firstChildren[childIndex++];\n      var targetChild;\n\n      while (child) {\n        var childID = ReactMount.getID(child);\n        if (childID) {\n          // Even if we find the node we're looking for, we finish looping\n          // through its siblings to ensure they're cached so that we don't have\n          // to revisit this node again. Otherwise, we make n^2 calls to getID\n          // when visiting the many children of a single node in order.\n\n          if (targetID === childID) {\n            targetChild = child;\n          } else if (ReactInstanceHandles.isAncestorIDOf(childID, targetID)) {\n            // If we find a child whose ID is an ancestor of the given ID,\n            // then we can be sure that we only want to search the subtree\n            // rooted at this child, so we can throw out the rest of the\n            // search state.\n            firstChildren.length = childIndex = 0;\n            firstChildren.push(child.firstChild);\n          }\n\n        } else {\n          // If this child had no ID, then there's a chance that it was\n          // injected automatically by the browser, as when a `<table>`\n          // element sprouts an extra `<tbody>` child as a side effect of\n          // `.innerHTML` parsing. Optimistically continue down this\n          // branch, but not before examining the other siblings.\n          firstChildren.push(child.firstChild);\n        }\n\n        child = child.nextSibling;\n      }\n\n      if (targetChild) {\n        // Emptying firstChildren/findComponentRootReusableArray is\n        // not necessary for correctness, but it helps the GC reclaim\n        // any nodes that were left at the end of the search.\n        firstChildren.length = 0;\n\n        return targetChild;\n      }\n    }\n\n    firstChildren.length = 0;\n\n    (\"production\" !== \"development\" ? invariant(\n      false,\n      'findComponentRoot(..., %s): Unable to find element. This probably ' +\n      'means the DOM was unexpectedly mutated (e.g., by the browser), ' +\n      'usually due to forgetting a <tbody> when using tables, nesting <p> ' +\n      'or <a> tags, or using non-SVG elements in an <svg> parent. Try ' +\n      'inspecting the child nodes of the element with React ID `%s`.',\n      targetID,\n      ReactMount.getID(ancestorNode)\n    ) : invariant(false));\n  },\n\n\n  /**\n   * React ID utilities.\n   */\n\n  getReactRootID: getReactRootID,\n\n  getID: getID,\n\n  setID: setID,\n\n  getNode: getNode,\n\n  purgeID: purgeID\n};\n\nmodule.exports = ReactMount;\n\n},{\"./DOMProperty\":11,\"./ReactBrowserEventEmitter\":31,\"./ReactCurrentOwner\":40,\"./ReactDescriptor\":56,\"./ReactInstanceHandles\":64,\"./ReactPerf\":71,\"./containsNode\":109,\"./getReactRootElementInContainer\":128,\"./instantiateReactComponent\":133,\"./invariant\":134,\"./shouldUpdateReactComponent\":154,\"./warning\":158}],68:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule ReactMultiChild\n * @typechecks static-only\n */\n\n\"use strict\";\n\nvar ReactComponent = _dereq_(\"./ReactComponent\");\nvar ReactMultiChildUpdateTypes = _dereq_(\"./ReactMultiChildUpdateTypes\");\n\nvar flattenChildren = _dereq_(\"./flattenChildren\");\nvar instantiateReactComponent = _dereq_(\"./instantiateReactComponent\");\nvar shouldUpdateReactComponent = _dereq_(\"./shouldUpdateReactComponent\");\n\n/**\n * Updating children of a component may trigger recursive updates. The depth is\n * used to batch recursive updates to render markup more efficiently.\n *\n * @type {number}\n * @private\n */\nvar updateDepth = 0;\n\n/**\n * Queue of update configuration objects.\n *\n * Each object has a `type` property that is in `ReactMultiChildUpdateTypes`.\n *\n * @type {array<object>}\n * @private\n */\nvar updateQueue = [];\n\n/**\n * Queue of markup to be rendered.\n *\n * @type {array<string>}\n * @private\n */\nvar markupQueue = [];\n\n/**\n * Enqueues markup to be rendered and inserted at a supplied index.\n *\n * @param {string} parentID ID of the parent component.\n * @param {string} markup Markup that renders into an element.\n * @param {number} toIndex Destination index.\n * @private\n */\nfunction enqueueMarkup(parentID, markup, toIndex) {\n  // NOTE: Null values reduce hidden classes.\n  updateQueue.push({\n    parentID: parentID,\n    parentNode: null,\n    type: ReactMultiChildUpdateTypes.INSERT_MARKUP,\n    markupIndex: markupQueue.push(markup) - 1,\n    textContent: null,\n    fromIndex: null,\n    toIndex: toIndex\n  });\n}\n\n/**\n * Enqueues moving an existing element to another index.\n *\n * @param {string} parentID ID of the parent component.\n * @param {number} fromIndex Source index of the existing element.\n * @param {number} toIndex Destination index of the element.\n * @private\n */\nfunction enqueueMove(parentID, fromIndex, toIndex) {\n  // NOTE: Null values reduce hidden classes.\n  updateQueue.push({\n    parentID: parentID,\n    parentNode: null,\n    type: ReactMultiChildUpdateTypes.MOVE_EXISTING,\n    markupIndex: null,\n    textContent: null,\n    fromIndex: fromIndex,\n    toIndex: toIndex\n  });\n}\n\n/**\n * Enqueues removing an element at an index.\n *\n * @param {string} parentID ID of the parent component.\n * @param {number} fromIndex Index of the element to remove.\n * @private\n */\nfunction enqueueRemove(parentID, fromIndex) {\n  // NOTE: Null values reduce hidden classes.\n  updateQueue.push({\n    parentID: parentID,\n    parentNode: null,\n    type: ReactMultiChildUpdateTypes.REMOVE_NODE,\n    markupIndex: null,\n    textContent: null,\n    fromIndex: fromIndex,\n    toIndex: null\n  });\n}\n\n/**\n * Enqueues setting the text content.\n *\n * @param {string} parentID ID of the parent component.\n * @param {string} textContent Text content to set.\n * @private\n */\nfunction enqueueTextContent(parentID, textContent) {\n  // NOTE: Null values reduce hidden classes.\n  updateQueue.push({\n    parentID: parentID,\n    parentNode: null,\n    type: ReactMultiChildUpdateTypes.TEXT_CONTENT,\n    markupIndex: null,\n    textContent: textContent,\n    fromIndex: null,\n    toIndex: null\n  });\n}\n\n/**\n * Processes any enqueued updates.\n *\n * @private\n */\nfunction processQueue() {\n  if (updateQueue.length) {\n    ReactComponent.BackendIDOperations.dangerouslyProcessChildrenUpdates(\n      updateQueue,\n      markupQueue\n    );\n    clearQueue();\n  }\n}\n\n/**\n * Clears any enqueued updates.\n *\n * @private\n */\nfunction clearQueue() {\n  updateQueue.length = 0;\n  markupQueue.length = 0;\n}\n\n/**\n * ReactMultiChild are capable of reconciling multiple children.\n *\n * @class ReactMultiChild\n * @internal\n */\nvar ReactMultiChild = {\n\n  /**\n   * Provides common functionality for components that must reconcile multiple\n   * children. This is used by `ReactDOMComponent` to mount, update, and\n   * unmount child components.\n   *\n   * @lends {ReactMultiChild.prototype}\n   */\n  Mixin: {\n\n    /**\n     * Generates a \"mount image\" for each of the supplied children. In the case\n     * of `ReactDOMComponent`, a mount image is a string of markup.\n     *\n     * @param {?object} nestedChildren Nested child maps.\n     * @return {array} An array of mounted representations.\n     * @internal\n     */\n    mountChildren: function(nestedChildren, transaction) {\n      var children = flattenChildren(nestedChildren);\n      var mountImages = [];\n      var index = 0;\n      this._renderedChildren = children;\n      for (var name in children) {\n        var child = children[name];\n        if (children.hasOwnProperty(name)) {\n          // The rendered children must be turned into instances as they're\n          // mounted.\n          var childInstance = instantiateReactComponent(child);\n          children[name] = childInstance;\n          // Inlined for performance, see `ReactInstanceHandles.createReactID`.\n          var rootID = this._rootNodeID + name;\n          var mountImage = childInstance.mountComponent(\n            rootID,\n            transaction,\n            this._mountDepth + 1\n          );\n          childInstance._mountIndex = index;\n          mountImages.push(mountImage);\n          index++;\n        }\n      }\n      return mountImages;\n    },\n\n    /**\n     * Replaces any rendered children with a text content string.\n     *\n     * @param {string} nextContent String of content.\n     * @internal\n     */\n    updateTextContent: function(nextContent) {\n      updateDepth++;\n      var errorThrown = true;\n      try {\n        var prevChildren = this._renderedChildren;\n        // Remove any rendered children.\n        for (var name in prevChildren) {\n          if (prevChildren.hasOwnProperty(name)) {\n            this._unmountChildByName(prevChildren[name], name);\n          }\n        }\n        // Set new text content.\n        this.setTextContent(nextContent);\n        errorThrown = false;\n      } finally {\n        updateDepth--;\n        if (!updateDepth) {\n          errorThrown ? clearQueue() : processQueue();\n        }\n      }\n    },\n\n    /**\n     * Updates the rendered children with new children.\n     *\n     * @param {?object} nextNestedChildren Nested child maps.\n     * @param {ReactReconcileTransaction} transaction\n     * @internal\n     */\n    updateChildren: function(nextNestedChildren, transaction) {\n      updateDepth++;\n      var errorThrown = true;\n      try {\n        this._updateChildren(nextNestedChildren, transaction);\n        errorThrown = false;\n      } finally {\n        updateDepth--;\n        if (!updateDepth) {\n          errorThrown ? clearQueue() : processQueue();\n        }\n      }\n    },\n\n    /**\n     * Improve performance by isolating this hot code path from the try/catch\n     * block in `updateChildren`.\n     *\n     * @param {?object} nextNestedChildren Nested child maps.\n     * @param {ReactReconcileTransaction} transaction\n     * @final\n     * @protected\n     */\n    _updateChildren: function(nextNestedChildren, transaction) {\n      var nextChildren = flattenChildren(nextNestedChildren);\n      var prevChildren = this._renderedChildren;\n      if (!nextChildren && !prevChildren) {\n        return;\n      }\n      var name;\n      // `nextIndex` will increment for each child in `nextChildren`, but\n      // `lastIndex` will be the last index visited in `prevChildren`.\n      var lastIndex = 0;\n      var nextIndex = 0;\n      for (name in nextChildren) {\n        if (!nextChildren.hasOwnProperty(name)) {\n          continue;\n        }\n        var prevChild = prevChildren && prevChildren[name];\n        var prevDescriptor = prevChild && prevChild._descriptor;\n        var nextDescriptor = nextChildren[name];\n        if (shouldUpdateReactComponent(prevDescriptor, nextDescriptor)) {\n          this.moveChild(prevChild, nextIndex, lastIndex);\n          lastIndex = Math.max(prevChild._mountIndex, lastIndex);\n          prevChild.receiveComponent(nextDescriptor, transaction);\n          prevChild._mountIndex = nextIndex;\n        } else {\n          if (prevChild) {\n            // Update `lastIndex` before `_mountIndex` gets unset by unmounting.\n            lastIndex = Math.max(prevChild._mountIndex, lastIndex);\n            this._unmountChildByName(prevChild, name);\n          }\n          // The child must be instantiated before it's mounted.\n          var nextChildInstance = instantiateReactComponent(nextDescriptor);\n          this._mountChildByNameAtIndex(\n            nextChildInstance, name, nextIndex, transaction\n          );\n        }\n        nextIndex++;\n      }\n      // Remove children that are no longer present.\n      for (name in prevChildren) {\n        if (prevChildren.hasOwnProperty(name) &&\n            !(nextChildren && nextChildren[name])) {\n          this._unmountChildByName(prevChildren[name], name);\n        }\n      }\n    },\n\n    /**\n     * Unmounts all rendered children. This should be used to clean up children\n     * when this component is unmounted.\n     *\n     * @internal\n     */\n    unmountChildren: function() {\n      var renderedChildren = this._renderedChildren;\n      for (var name in renderedChildren) {\n        var renderedChild = renderedChildren[name];\n        // TODO: When is this not true?\n        if (renderedChild.unmountComponent) {\n          renderedChild.unmountComponent();\n        }\n      }\n      this._renderedChildren = null;\n    },\n\n    /**\n     * Moves a child component to the supplied index.\n     *\n     * @param {ReactComponent} child Component to move.\n     * @param {number} toIndex Destination index of the element.\n     * @param {number} lastIndex Last index visited of the siblings of `child`.\n     * @protected\n     */\n    moveChild: function(child, toIndex, lastIndex) {\n      // If the index of `child` is less than `lastIndex`, then it needs to\n      // be moved. Otherwise, we do not need to move it because a child will be\n      // inserted or moved before `child`.\n      if (child._mountIndex < lastIndex) {\n        enqueueMove(this._rootNodeID, child._mountIndex, toIndex);\n      }\n    },\n\n    /**\n     * Creates a child component.\n     *\n     * @param {ReactComponent} child Component to create.\n     * @param {string} mountImage Markup to insert.\n     * @protected\n     */\n    createChild: function(child, mountImage) {\n      enqueueMarkup(this._rootNodeID, mountImage, child._mountIndex);\n    },\n\n    /**\n     * Removes a child component.\n     *\n     * @param {ReactComponent} child Child to remove.\n     * @protected\n     */\n    removeChild: function(child) {\n      enqueueRemove(this._rootNodeID, child._mountIndex);\n    },\n\n    /**\n     * Sets this text content string.\n     *\n     * @param {string} textContent Text content to set.\n     * @protected\n     */\n    setTextContent: function(textContent) {\n      enqueueTextContent(this._rootNodeID, textContent);\n    },\n\n    /**\n     * Mounts a child with the supplied name.\n     *\n     * NOTE: This is part of `updateChildren` and is here for readability.\n     *\n     * @param {ReactComponent} child Component to mount.\n     * @param {string} name Name of the child.\n     * @param {number} index Index at which to insert the child.\n     * @param {ReactReconcileTransaction} transaction\n     * @private\n     */\n    _mountChildByNameAtIndex: function(child, name, index, transaction) {\n      // Inlined for performance, see `ReactInstanceHandles.createReactID`.\n      var rootID = this._rootNodeID + name;\n      var mountImage = child.mountComponent(\n        rootID,\n        transaction,\n        this._mountDepth + 1\n      );\n      child._mountIndex = index;\n      this.createChild(child, mountImage);\n      this._renderedChildren = this._renderedChildren || {};\n      this._renderedChildren[name] = child;\n    },\n\n    /**\n     * Unmounts a rendered child by name.\n     *\n     * NOTE: This is part of `updateChildren` and is here for readability.\n     *\n     * @param {ReactComponent} child Component to unmount.\n     * @param {string} name Name of the child in `this._renderedChildren`.\n     * @private\n     */\n    _unmountChildByName: function(child, name) {\n      this.removeChild(child);\n      child._mountIndex = null;\n      child.unmountComponent();\n      delete this._renderedChildren[name];\n    }\n\n  }\n\n};\n\nmodule.exports = ReactMultiChild;\n\n},{\"./ReactComponent\":35,\"./ReactMultiChildUpdateTypes\":69,\"./flattenChildren\":119,\"./instantiateReactComponent\":133,\"./shouldUpdateReactComponent\":154}],69:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule ReactMultiChildUpdateTypes\n */\n\n\"use strict\";\n\nvar keyMirror = _dereq_(\"./keyMirror\");\n\n/**\n * When a component's children are updated, a series of update configuration\n * objects are created in order to batch and serialize the required changes.\n *\n * Enumerates all the possible types of update configurations.\n *\n * @internal\n */\nvar ReactMultiChildUpdateTypes = keyMirror({\n  INSERT_MARKUP: null,\n  MOVE_EXISTING: null,\n  REMOVE_NODE: null,\n  TEXT_CONTENT: null\n});\n\nmodule.exports = ReactMultiChildUpdateTypes;\n\n},{\"./keyMirror\":140}],70:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule ReactOwner\n */\n\n\"use strict\";\n\nvar emptyObject = _dereq_(\"./emptyObject\");\nvar invariant = _dereq_(\"./invariant\");\n\n/**\n * ReactOwners are capable of storing references to owned components.\n *\n * All components are capable of //being// referenced by owner components, but\n * only ReactOwner components are capable of //referencing// owned components.\n * The named reference is known as a \"ref\".\n *\n * Refs are available when mounted and updated during reconciliation.\n *\n *   var MyComponent = React.createClass({\n *     render: function() {\n *       return (\n *         <div onClick={this.handleClick}>\n *           <CustomComponent ref=\"custom\" />\n *         </div>\n *       );\n *     },\n *     handleClick: function() {\n *       this.refs.custom.handleClick();\n *     },\n *     componentDidMount: function() {\n *       this.refs.custom.initialize();\n *     }\n *   });\n *\n * Refs should rarely be used. When refs are used, they should only be done to\n * control data that is not handled by React's data flow.\n *\n * @class ReactOwner\n */\nvar ReactOwner = {\n\n  /**\n   * @param {?object} object\n   * @return {boolean} True if `object` is a valid owner.\n   * @final\n   */\n  isValidOwner: function(object) {\n    return !!(\n      object &&\n      typeof object.attachRef === 'function' &&\n      typeof object.detachRef === 'function'\n    );\n  },\n\n  /**\n   * Adds a component by ref to an owner component.\n   *\n   * @param {ReactComponent} component Component to reference.\n   * @param {string} ref Name by which to refer to the component.\n   * @param {ReactOwner} owner Component on which to record the ref.\n   * @final\n   * @internal\n   */\n  addComponentAsRefTo: function(component, ref, owner) {\n    (\"production\" !== \"development\" ? invariant(\n      ReactOwner.isValidOwner(owner),\n      'addComponentAsRefTo(...): Only a ReactOwner can have refs. This ' +\n      'usually means that you\\'re trying to add a ref to a component that ' +\n      'doesn\\'t have an owner (that is, was not created inside of another ' +\n      'component\\'s `render` method). Try rendering this component inside of ' +\n      'a new top-level component which will hold the ref.'\n    ) : invariant(ReactOwner.isValidOwner(owner)));\n    owner.attachRef(ref, component);\n  },\n\n  /**\n   * Removes a component by ref from an owner component.\n   *\n   * @param {ReactComponent} component Component to dereference.\n   * @param {string} ref Name of the ref to remove.\n   * @param {ReactOwner} owner Component on which the ref is recorded.\n   * @final\n   * @internal\n   */\n  removeComponentAsRefFrom: function(component, ref, owner) {\n    (\"production\" !== \"development\" ? invariant(\n      ReactOwner.isValidOwner(owner),\n      'removeComponentAsRefFrom(...): Only a ReactOwner can have refs. This ' +\n      'usually means that you\\'re trying to remove a ref to a component that ' +\n      'doesn\\'t have an owner (that is, was not created inside of another ' +\n      'component\\'s `render` method). Try rendering this component inside of ' +\n      'a new top-level component which will hold the ref.'\n    ) : invariant(ReactOwner.isValidOwner(owner)));\n    // Check that `component` is still the current ref because we do not want to\n    // detach the ref if another component stole it.\n    if (owner.refs[ref] === component) {\n      owner.detachRef(ref);\n    }\n  },\n\n  /**\n   * A ReactComponent must mix this in to have refs.\n   *\n   * @lends {ReactOwner.prototype}\n   */\n  Mixin: {\n\n    construct: function() {\n      this.refs = emptyObject;\n    },\n\n    /**\n     * Lazily allocates the refs object and stores `component` as `ref`.\n     *\n     * @param {string} ref Reference name.\n     * @param {component} component Component to store as `ref`.\n     * @final\n     * @private\n     */\n    attachRef: function(ref, component) {\n      (\"production\" !== \"development\" ? invariant(\n        component.isOwnedBy(this),\n        'attachRef(%s, ...): Only a component\\'s owner can store a ref to it.',\n        ref\n      ) : invariant(component.isOwnedBy(this)));\n      var refs = this.refs === emptyObject ? (this.refs = {}) : this.refs;\n      refs[ref] = component;\n    },\n\n    /**\n     * Detaches a reference name.\n     *\n     * @param {string} ref Name to dereference.\n     * @final\n     * @private\n     */\n    detachRef: function(ref) {\n      delete this.refs[ref];\n    }\n\n  }\n\n};\n\nmodule.exports = ReactOwner;\n\n},{\"./emptyObject\":117,\"./invariant\":134}],71:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule ReactPerf\n * @typechecks static-only\n */\n\n\"use strict\";\n\n/**\n * ReactPerf is a general AOP system designed to measure performance. This\n * module only has the hooks: see ReactDefaultPerf for the analysis tool.\n */\nvar ReactPerf = {\n  /**\n   * Boolean to enable/disable measurement. Set to false by default to prevent\n   * accidental logging and perf loss.\n   */\n  enableMeasure: false,\n\n  /**\n   * Holds onto the measure function in use. By default, don't measure\n   * anything, but we'll override this if we inject a measure function.\n   */\n  storedMeasure: _noMeasure,\n\n  /**\n   * Use this to wrap methods you want to measure. Zero overhead in production.\n   *\n   * @param {string} objName\n   * @param {string} fnName\n   * @param {function} func\n   * @return {function}\n   */\n  measure: function(objName, fnName, func) {\n    if (\"production\" !== \"development\") {\n      var measuredFunc = null;\n      return function() {\n        if (ReactPerf.enableMeasure) {\n          if (!measuredFunc) {\n            measuredFunc = ReactPerf.storedMeasure(objName, fnName, func);\n          }\n          return measuredFunc.apply(this, arguments);\n        }\n        return func.apply(this, arguments);\n      };\n    }\n    return func;\n  },\n\n  injection: {\n    /**\n     * @param {function} measure\n     */\n    injectMeasure: function(measure) {\n      ReactPerf.storedMeasure = measure;\n    }\n  }\n};\n\n/**\n * Simply passes through the measured function, without measuring it.\n *\n * @param {string} objName\n * @param {string} fnName\n * @param {function} func\n * @return {function}\n */\nfunction _noMeasure(objName, fnName, func) {\n  return func;\n}\n\nmodule.exports = ReactPerf;\n\n},{}],72:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule ReactPropTransferer\n */\n\n\"use strict\";\n\nvar emptyFunction = _dereq_(\"./emptyFunction\");\nvar invariant = _dereq_(\"./invariant\");\nvar joinClasses = _dereq_(\"./joinClasses\");\nvar merge = _dereq_(\"./merge\");\n\n/**\n * Creates a transfer strategy that will merge prop values using the supplied\n * `mergeStrategy`. If a prop was previously unset, this just sets it.\n *\n * @param {function} mergeStrategy\n * @return {function}\n */\nfunction createTransferStrategy(mergeStrategy) {\n  return function(props, key, value) {\n    if (!props.hasOwnProperty(key)) {\n      props[key] = value;\n    } else {\n      props[key] = mergeStrategy(props[key], value);\n    }\n  };\n}\n\nvar transferStrategyMerge = createTransferStrategy(function(a, b) {\n  // `merge` overrides the first object's (`props[key]` above) keys using the\n  // second object's (`value`) keys. An object's style's existing `propA` would\n  // get overridden. Flip the order here.\n  return merge(b, a);\n});\n\n/**\n * Transfer strategies dictate how props are transferred by `transferPropsTo`.\n * NOTE: if you add any more exceptions to this list you should be sure to\n * update `cloneWithProps()` accordingly.\n */\nvar TransferStrategies = {\n  /**\n   * Never transfer `children`.\n   */\n  children: emptyFunction,\n  /**\n   * Transfer the `className` prop by merging them.\n   */\n  className: createTransferStrategy(joinClasses),\n  /**\n   * Never transfer the `key` prop.\n   */\n  key: emptyFunction,\n  /**\n   * Never transfer the `ref` prop.\n   */\n  ref: emptyFunction,\n  /**\n   * Transfer the `style` prop (which is an object) by merging them.\n   */\n  style: transferStrategyMerge\n};\n\n/**\n * Mutates the first argument by transferring the properties from the second\n * argument.\n *\n * @param {object} props\n * @param {object} newProps\n * @return {object}\n */\nfunction transferInto(props, newProps) {\n  for (var thisKey in newProps) {\n    if (!newProps.hasOwnProperty(thisKey)) {\n      continue;\n    }\n\n    var transferStrategy = TransferStrategies[thisKey];\n\n    if (transferStrategy && TransferStrategies.hasOwnProperty(thisKey)) {\n      transferStrategy(props, thisKey, newProps[thisKey]);\n    } else if (!props.hasOwnProperty(thisKey)) {\n      props[thisKey] = newProps[thisKey];\n    }\n  }\n  return props;\n}\n\n/**\n * ReactPropTransferer are capable of transferring props to another component\n * using a `transferPropsTo` method.\n *\n * @class ReactPropTransferer\n */\nvar ReactPropTransferer = {\n\n  TransferStrategies: TransferStrategies,\n\n  /**\n   * Merge two props objects using TransferStrategies.\n   *\n   * @param {object} oldProps original props (they take precedence)\n   * @param {object} newProps new props to merge in\n   * @return {object} a new object containing both sets of props merged.\n   */\n  mergeProps: function(oldProps, newProps) {\n    return transferInto(merge(oldProps), newProps);\n  },\n\n  /**\n   * @lends {ReactPropTransferer.prototype}\n   */\n  Mixin: {\n\n    /**\n     * Transfer props from this component to a target component.\n     *\n     * Props that do not have an explicit transfer strategy will be transferred\n     * only if the target component does not already have the prop set.\n     *\n     * This is usually used to pass down props to a returned root component.\n     *\n     * @param {ReactDescriptor} descriptor Component receiving the properties.\n     * @return {ReactDescriptor} The supplied `component`.\n     * @final\n     * @protected\n     */\n    transferPropsTo: function(descriptor) {\n      (\"production\" !== \"development\" ? invariant(\n        descriptor._owner === this,\n        '%s: You can\\'t call transferPropsTo() on a component that you ' +\n        'don\\'t own, %s. This usually means you are calling ' +\n        'transferPropsTo() on a component passed in as props or children.',\n        this.constructor.displayName,\n        descriptor.type.displayName\n      ) : invariant(descriptor._owner === this));\n\n      // Because descriptors are immutable we have to merge into the existing\n      // props object rather than clone it.\n      transferInto(descriptor.props, this.props);\n\n      return descriptor;\n    }\n\n  }\n};\n\nmodule.exports = ReactPropTransferer;\n\n},{\"./emptyFunction\":116,\"./invariant\":134,\"./joinClasses\":139,\"./merge\":144}],73:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule ReactPropTypeLocationNames\n */\n\n\"use strict\";\n\nvar ReactPropTypeLocationNames = {};\n\nif (\"production\" !== \"development\") {\n  ReactPropTypeLocationNames = {\n    prop: 'prop',\n    context: 'context',\n    childContext: 'child context'\n  };\n}\n\nmodule.exports = ReactPropTypeLocationNames;\n\n},{}],74:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule ReactPropTypeLocations\n */\n\n\"use strict\";\n\nvar keyMirror = _dereq_(\"./keyMirror\");\n\nvar ReactPropTypeLocations = keyMirror({\n  prop: null,\n  context: null,\n  childContext: null\n});\n\nmodule.exports = ReactPropTypeLocations;\n\n},{\"./keyMirror\":140}],75:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule ReactPropTypes\n */\n\n\"use strict\";\n\nvar ReactDescriptor = _dereq_(\"./ReactDescriptor\");\nvar ReactPropTypeLocationNames = _dereq_(\"./ReactPropTypeLocationNames\");\n\nvar emptyFunction = _dereq_(\"./emptyFunction\");\n\n/**\n * Collection of methods that allow declaration and validation of props that are\n * supplied to React components. Example usage:\n *\n *   var Props = require('ReactPropTypes');\n *   var MyArticle = React.createClass({\n *     propTypes: {\n *       // An optional string prop named \"description\".\n *       description: Props.string,\n *\n *       // A required enum prop named \"category\".\n *       category: Props.oneOf(['News','Photos']).isRequired,\n *\n *       // A prop named \"dialog\" that requires an instance of Dialog.\n *       dialog: Props.instanceOf(Dialog).isRequired\n *     },\n *     render: function() { ... }\n *   });\n *\n * A more formal specification of how these methods are used:\n *\n *   type := array|bool|func|object|number|string|oneOf([...])|instanceOf(...)\n *   decl := ReactPropTypes.{type}(.isRequired)?\n *\n * Each and every declaration produces a function with the same signature. This\n * allows the creation of custom validation functions. For example:\n *\n *  var MyLink = React.createClass({\n *    propTypes: {\n *      // An optional string or URI prop named \"href\".\n *      href: function(props, propName, componentName) {\n *        var propValue = props[propName];\n *        if (propValue != null && typeof propValue !== 'string' &&\n *            !(propValue instanceof URI)) {\n *          return new Error(\n *            'Expected a string or an URI for ' + propName + ' in ' +\n *            componentName\n *          );\n *        }\n *      }\n *    },\n *    render: function() {...}\n *  });\n *\n * @internal\n */\n\nvar ANONYMOUS = '<<anonymous>>';\n\nvar ReactPropTypes = {\n  array: createPrimitiveTypeChecker('array'),\n  bool: createPrimitiveTypeChecker('boolean'),\n  func: createPrimitiveTypeChecker('function'),\n  number: createPrimitiveTypeChecker('number'),\n  object: createPrimitiveTypeChecker('object'),\n  string: createPrimitiveTypeChecker('string'),\n\n  any: createAnyTypeChecker(),\n  arrayOf: createArrayOfTypeChecker,\n  component: createComponentTypeChecker(),\n  instanceOf: createInstanceTypeChecker,\n  objectOf: createObjectOfTypeChecker,\n  oneOf: createEnumTypeChecker,\n  oneOfType: createUnionTypeChecker,\n  renderable: createRenderableTypeChecker(),\n  shape: createShapeTypeChecker\n};\n\nfunction createChainableTypeChecker(validate) {\n  function checkType(isRequired, props, propName, componentName, location) {\n    componentName = componentName || ANONYMOUS;\n    if (props[propName] == null) {\n      var locationName = ReactPropTypeLocationNames[location];\n      if (isRequired) {\n        return new Error(\n          (\"Required \" + locationName + \" `\" + propName + \"` was not specified in \")+\n          (\"`\" + componentName + \"`.\")\n        );\n      }\n    } else {\n      return validate(props, propName, componentName, location);\n    }\n  }\n\n  var chainedCheckType = checkType.bind(null, false);\n  chainedCheckType.isRequired = checkType.bind(null, true);\n\n  return chainedCheckType;\n}\n\nfunction createPrimitiveTypeChecker(expectedType) {\n  function validate(props, propName, componentName, location) {\n    var propValue = props[propName];\n    var propType = getPropType(propValue);\n    if (propType !== expectedType) {\n      var locationName = ReactPropTypeLocationNames[location];\n      // `propValue` being instance of, say, date/regexp, pass the 'object'\n      // check, but we can offer a more precise error message here rather than\n      // 'of type `object`'.\n      var preciseType = getPreciseType(propValue);\n\n      return new Error(\n        (\"Invalid \" + locationName + \" `\" + propName + \"` of type `\" + preciseType + \"` \") +\n        (\"supplied to `\" + componentName + \"`, expected `\" + expectedType + \"`.\")\n      );\n    }\n  }\n  return createChainableTypeChecker(validate);\n}\n\nfunction createAnyTypeChecker() {\n  return createChainableTypeChecker(emptyFunction.thatReturns());\n}\n\nfunction createArrayOfTypeChecker(typeChecker) {\n  function validate(props, propName, componentName, location) {\n    var propValue = props[propName];\n    if (!Array.isArray(propValue)) {\n      var locationName = ReactPropTypeLocationNames[location];\n      var propType = getPropType(propValue);\n      return new Error(\n        (\"Invalid \" + locationName + \" `\" + propName + \"` of type \") +\n        (\"`\" + propType + \"` supplied to `\" + componentName + \"`, expected an array.\")\n      );\n    }\n    for (var i = 0; i < propValue.length; i++) {\n      var error = typeChecker(propValue, i, componentName, location);\n      if (error instanceof Error) {\n        return error;\n      }\n    }\n  }\n  return createChainableTypeChecker(validate);\n}\n\nfunction createComponentTypeChecker() {\n  function validate(props, propName, componentName, location) {\n    if (!ReactDescriptor.isValidDescriptor(props[propName])) {\n      var locationName = ReactPropTypeLocationNames[location];\n      return new Error(\n        (\"Invalid \" + locationName + \" `\" + propName + \"` supplied to \") +\n        (\"`\" + componentName + \"`, expected a React component.\")\n      );\n    }\n  }\n  return createChainableTypeChecker(validate);\n}\n\nfunction createInstanceTypeChecker(expectedClass) {\n  function validate(props, propName, componentName, location) {\n    if (!(props[propName] instanceof expectedClass)) {\n      var locationName = ReactPropTypeLocationNames[location];\n      var expectedClassName = expectedClass.name || ANONYMOUS;\n      return new Error(\n        (\"Invalid \" + locationName + \" `\" + propName + \"` supplied to \") +\n        (\"`\" + componentName + \"`, expected instance of `\" + expectedClassName + \"`.\")\n      );\n    }\n  }\n  return createChainableTypeChecker(validate);\n}\n\nfunction createEnumTypeChecker(expectedValues) {\n  function validate(props, propName, componentName, location) {\n    var propValue = props[propName];\n    for (var i = 0; i < expectedValues.length; i++) {\n      if (propValue === expectedValues[i]) {\n        return;\n      }\n    }\n\n    var locationName = ReactPropTypeLocationNames[location];\n    var valuesString = JSON.stringify(expectedValues);\n    return new Error(\n      (\"Invalid \" + locationName + \" `\" + propName + \"` of value `\" + propValue + \"` \") +\n      (\"supplied to `\" + componentName + \"`, expected one of \" + valuesString + \".\")\n    );\n  }\n  return createChainableTypeChecker(validate);\n}\n\nfunction createObjectOfTypeChecker(typeChecker) {\n  function validate(props, propName, componentName, location) {\n    var propValue = props[propName];\n    var propType = getPropType(propValue);\n    if (propType !== 'object') {\n      var locationName = ReactPropTypeLocationNames[location];\n      return new Error(\n        (\"Invalid \" + locationName + \" `\" + propName + \"` of type \") +\n        (\"`\" + propType + \"` supplied to `\" + componentName + \"`, expected an object.\")\n      );\n    }\n    for (var key in propValue) {\n      if (propValue.hasOwnProperty(key)) {\n        var error = typeChecker(propValue, key, componentName, location);\n        if (error instanceof Error) {\n          return error;\n        }\n      }\n    }\n  }\n  return createChainableTypeChecker(validate);\n}\n\nfunction createUnionTypeChecker(arrayOfTypeCheckers) {\n  function validate(props, propName, componentName, location) {\n    for (var i = 0; i < arrayOfTypeCheckers.length; i++) {\n      var checker = arrayOfTypeCheckers[i];\n      if (checker(props, propName, componentName, location) == null) {\n        return;\n      }\n    }\n\n    var locationName = ReactPropTypeLocationNames[location];\n    return new Error(\n      (\"Invalid \" + locationName + \" `\" + propName + \"` supplied to \") +\n      (\"`\" + componentName + \"`.\")\n    );\n  }\n  return createChainableTypeChecker(validate);\n}\n\nfunction createRenderableTypeChecker() {\n  function validate(props, propName, componentName, location) {\n    if (!isRenderable(props[propName])) {\n      var locationName = ReactPropTypeLocationNames[location];\n      return new Error(\n        (\"Invalid \" + locationName + \" `\" + propName + \"` supplied to \") +\n        (\"`\" + componentName + \"`, expected a renderable prop.\")\n      );\n    }\n  }\n  return createChainableTypeChecker(validate);\n}\n\nfunction createShapeTypeChecker(shapeTypes) {\n  function validate(props, propName, componentName, location) {\n    var propValue = props[propName];\n    var propType = getPropType(propValue);\n    if (propType !== 'object') {\n      var locationName = ReactPropTypeLocationNames[location];\n      return new Error(\n        (\"Invalid \" + locationName + \" `\" + propName + \"` of type `\" + propType + \"` \") +\n        (\"supplied to `\" + componentName + \"`, expected `object`.\")\n      );\n    }\n    for (var key in shapeTypes) {\n      var checker = shapeTypes[key];\n      if (!checker) {\n        continue;\n      }\n      var error = checker(propValue, key, componentName, location);\n      if (error) {\n        return error;\n      }\n    }\n  }\n  return createChainableTypeChecker(validate, 'expected `object`');\n}\n\nfunction isRenderable(propValue) {\n  switch(typeof propValue) {\n    // TODO: this was probably written with the assumption that we're not\n    // returning `this.props.component` directly from `render`. This is\n    // currently not supported but we should, to make it consistent.\n    case 'number':\n    case 'string':\n      return true;\n    case 'boolean':\n      return !propValue;\n    case 'object':\n      if (Array.isArray(propValue)) {\n        return propValue.every(isRenderable);\n      }\n      if (ReactDescriptor.isValidDescriptor(propValue)) {\n        return true;\n      }\n      for (var k in propValue) {\n        if (!isRenderable(propValue[k])) {\n          return false;\n        }\n      }\n      return true;\n    default:\n      return false;\n  }\n}\n\n// Equivalent of `typeof` but with special handling for array and regexp.\nfunction getPropType(propValue) {\n  var propType = typeof propValue;\n  if (Array.isArray(propValue)) {\n    return 'array';\n  }\n  if (propValue instanceof RegExp) {\n    // Old webkits (at least until Android 4.0) return 'function' rather than\n    // 'object' for typeof a RegExp. We'll normalize this here so that /bla/\n    // passes PropTypes.object.\n    return 'object';\n  }\n  return propType;\n}\n\n// This handles more types than `getPropType`. Only used for error messages.\n// See `createPrimitiveTypeChecker`.\nfunction getPreciseType(propValue) {\n  var propType = getPropType(propValue);\n  if (propType === 'object') {\n    if (propValue instanceof Date) {\n      return 'date';\n    } else if (propValue instanceof RegExp) {\n      return 'regexp';\n    }\n  }\n  return propType;\n}\n\nmodule.exports = ReactPropTypes;\n\n},{\"./ReactDescriptor\":56,\"./ReactPropTypeLocationNames\":73,\"./emptyFunction\":116}],76:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule ReactPutListenerQueue\n */\n\n\"use strict\";\n\nvar PooledClass = _dereq_(\"./PooledClass\");\nvar ReactBrowserEventEmitter = _dereq_(\"./ReactBrowserEventEmitter\");\n\nvar mixInto = _dereq_(\"./mixInto\");\n\nfunction ReactPutListenerQueue() {\n  this.listenersToPut = [];\n}\n\nmixInto(ReactPutListenerQueue, {\n  enqueuePutListener: function(rootNodeID, propKey, propValue) {\n    this.listenersToPut.push({\n      rootNodeID: rootNodeID,\n      propKey: propKey,\n      propValue: propValue\n    });\n  },\n\n  putListeners: function() {\n    for (var i = 0; i < this.listenersToPut.length; i++) {\n      var listenerToPut = this.listenersToPut[i];\n      ReactBrowserEventEmitter.putListener(\n        listenerToPut.rootNodeID,\n        listenerToPut.propKey,\n        listenerToPut.propValue\n      );\n    }\n  },\n\n  reset: function() {\n    this.listenersToPut.length = 0;\n  },\n\n  destructor: function() {\n    this.reset();\n  }\n});\n\nPooledClass.addPoolingTo(ReactPutListenerQueue);\n\nmodule.exports = ReactPutListenerQueue;\n\n},{\"./PooledClass\":28,\"./ReactBrowserEventEmitter\":31,\"./mixInto\":147}],77:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule ReactReconcileTransaction\n * @typechecks static-only\n */\n\n\"use strict\";\n\nvar CallbackQueue = _dereq_(\"./CallbackQueue\");\nvar PooledClass = _dereq_(\"./PooledClass\");\nvar ReactBrowserEventEmitter = _dereq_(\"./ReactBrowserEventEmitter\");\nvar ReactInputSelection = _dereq_(\"./ReactInputSelection\");\nvar ReactPutListenerQueue = _dereq_(\"./ReactPutListenerQueue\");\nvar Transaction = _dereq_(\"./Transaction\");\n\nvar mixInto = _dereq_(\"./mixInto\");\n\n/**\n * Ensures that, when possible, the selection range (currently selected text\n * input) is not disturbed by performing the transaction.\n */\nvar SELECTION_RESTORATION = {\n  /**\n   * @return {Selection} Selection information.\n   */\n  initialize: ReactInputSelection.getSelectionInformation,\n  /**\n   * @param {Selection} sel Selection information returned from `initialize`.\n   */\n  close: ReactInputSelection.restoreSelection\n};\n\n/**\n * Suppresses events (blur/focus) that could be inadvertently dispatched due to\n * high level DOM manipulations (like temporarily removing a text input from the\n * DOM).\n */\nvar EVENT_SUPPRESSION = {\n  /**\n   * @return {boolean} The enabled status of `ReactBrowserEventEmitter` before\n   * the reconciliation.\n   */\n  initialize: function() {\n    var currentlyEnabled = ReactBrowserEventEmitter.isEnabled();\n    ReactBrowserEventEmitter.setEnabled(false);\n    return currentlyEnabled;\n  },\n\n  /**\n   * @param {boolean} previouslyEnabled Enabled status of\n   *   `ReactBrowserEventEmitter` before the reconciliation occured. `close`\n   *   restores the previous value.\n   */\n  close: function(previouslyEnabled) {\n    ReactBrowserEventEmitter.setEnabled(previouslyEnabled);\n  }\n};\n\n/**\n * Provides a queue for collecting `componentDidMount` and\n * `componentDidUpdate` callbacks during the the transaction.\n */\nvar ON_DOM_READY_QUEUEING = {\n  /**\n   * Initializes the internal `onDOMReady` queue.\n   */\n  initialize: function() {\n    this.reactMountReady.reset();\n  },\n\n  /**\n   * After DOM is flushed, invoke all registered `onDOMReady` callbacks.\n   */\n  close: function() {\n    this.reactMountReady.notifyAll();\n  }\n};\n\nvar PUT_LISTENER_QUEUEING = {\n  initialize: function() {\n    this.putListenerQueue.reset();\n  },\n\n  close: function() {\n    this.putListenerQueue.putListeners();\n  }\n};\n\n/**\n * Executed within the scope of the `Transaction` instance. Consider these as\n * being member methods, but with an implied ordering while being isolated from\n * each other.\n */\nvar TRANSACTION_WRAPPERS = [\n  PUT_LISTENER_QUEUEING,\n  SELECTION_RESTORATION,\n  EVENT_SUPPRESSION,\n  ON_DOM_READY_QUEUEING\n];\n\n/**\n * Currently:\n * - The order that these are listed in the transaction is critical:\n * - Suppresses events.\n * - Restores selection range.\n *\n * Future:\n * - Restore document/overflow scroll positions that were unintentionally\n *   modified via DOM insertions above the top viewport boundary.\n * - Implement/integrate with customized constraint based layout system and keep\n *   track of which dimensions must be remeasured.\n *\n * @class ReactReconcileTransaction\n */\nfunction ReactReconcileTransaction() {\n  this.reinitializeTransaction();\n  // Only server-side rendering really needs this option (see\n  // `ReactServerRendering`), but server-side uses\n  // `ReactServerRenderingTransaction` instead. This option is here so that it's\n  // accessible and defaults to false when `ReactDOMComponent` and\n  // `ReactTextComponent` checks it in `mountComponent`.`\n  this.renderToStaticMarkup = false;\n  this.reactMountReady = CallbackQueue.getPooled(null);\n  this.putListenerQueue = ReactPutListenerQueue.getPooled();\n}\n\nvar Mixin = {\n  /**\n   * @see Transaction\n   * @abstract\n   * @final\n   * @return {array<object>} List of operation wrap proceedures.\n   *   TODO: convert to array<TransactionWrapper>\n   */\n  getTransactionWrappers: function() {\n    return TRANSACTION_WRAPPERS;\n  },\n\n  /**\n   * @return {object} The queue to collect `onDOMReady` callbacks with.\n   */\n  getReactMountReady: function() {\n    return this.reactMountReady;\n  },\n\n  getPutListenerQueue: function() {\n    return this.putListenerQueue;\n  },\n\n  /**\n   * `PooledClass` looks for this, and will invoke this before allowing this\n   * instance to be resused.\n   */\n  destructor: function() {\n    CallbackQueue.release(this.reactMountReady);\n    this.reactMountReady = null;\n\n    ReactPutListenerQueue.release(this.putListenerQueue);\n    this.putListenerQueue = null;\n  }\n};\n\n\nmixInto(ReactReconcileTransaction, Transaction.Mixin);\nmixInto(ReactReconcileTransaction, Mixin);\n\nPooledClass.addPoolingTo(ReactReconcileTransaction);\n\nmodule.exports = ReactReconcileTransaction;\n\n},{\"./CallbackQueue\":6,\"./PooledClass\":28,\"./ReactBrowserEventEmitter\":31,\"./ReactInputSelection\":63,\"./ReactPutListenerQueue\":76,\"./Transaction\":104,\"./mixInto\":147}],78:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule ReactRootIndex\n * @typechecks\n */\n\n\"use strict\";\n\nvar ReactRootIndexInjection = {\n  /**\n   * @param {function} _createReactRootIndex\n   */\n  injectCreateReactRootIndex: function(_createReactRootIndex) {\n    ReactRootIndex.createReactRootIndex = _createReactRootIndex;\n  }\n};\n\nvar ReactRootIndex = {\n  createReactRootIndex: null,\n  injection: ReactRootIndexInjection\n};\n\nmodule.exports = ReactRootIndex;\n\n},{}],79:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @typechecks static-only\n * @providesModule ReactServerRendering\n */\n\"use strict\";\n\nvar ReactDescriptor = _dereq_(\"./ReactDescriptor\");\nvar ReactInstanceHandles = _dereq_(\"./ReactInstanceHandles\");\nvar ReactMarkupChecksum = _dereq_(\"./ReactMarkupChecksum\");\nvar ReactServerRenderingTransaction =\n  _dereq_(\"./ReactServerRenderingTransaction\");\n\nvar instantiateReactComponent = _dereq_(\"./instantiateReactComponent\");\nvar invariant = _dereq_(\"./invariant\");\n\n/**\n * @param {ReactComponent} component\n * @return {string} the HTML markup\n */\nfunction renderComponentToString(component) {\n  (\"production\" !== \"development\" ? invariant(\n    ReactDescriptor.isValidDescriptor(component),\n    'renderComponentToString(): You must pass a valid ReactComponent.'\n  ) : invariant(ReactDescriptor.isValidDescriptor(component)));\n\n  (\"production\" !== \"development\" ? invariant(\n    !(arguments.length === 2 && typeof arguments[1] === 'function'),\n    'renderComponentToString(): This function became synchronous and now ' +\n    'returns the generated markup. Please remove the second parameter.'\n  ) : invariant(!(arguments.length === 2 && typeof arguments[1] === 'function')));\n\n  var transaction;\n  try {\n    var id = ReactInstanceHandles.createReactRootID();\n    transaction = ReactServerRenderingTransaction.getPooled(false);\n\n    return transaction.perform(function() {\n      var componentInstance = instantiateReactComponent(component);\n      var markup = componentInstance.mountComponent(id, transaction, 0);\n      return ReactMarkupChecksum.addChecksumToMarkup(markup);\n    }, null);\n  } finally {\n    ReactServerRenderingTransaction.release(transaction);\n  }\n}\n\n/**\n * @param {ReactComponent} component\n * @return {string} the HTML markup, without the extra React ID and checksum\n* (for generating static pages)\n */\nfunction renderComponentToStaticMarkup(component) {\n  (\"production\" !== \"development\" ? invariant(\n    ReactDescriptor.isValidDescriptor(component),\n    'renderComponentToStaticMarkup(): You must pass a valid ReactComponent.'\n  ) : invariant(ReactDescriptor.isValidDescriptor(component)));\n\n  var transaction;\n  try {\n    var id = ReactInstanceHandles.createReactRootID();\n    transaction = ReactServerRenderingTransaction.getPooled(true);\n\n    return transaction.perform(function() {\n      var componentInstance = instantiateReactComponent(component);\n      return componentInstance.mountComponent(id, transaction, 0);\n    }, null);\n  } finally {\n    ReactServerRenderingTransaction.release(transaction);\n  }\n}\n\nmodule.exports = {\n  renderComponentToString: renderComponentToString,\n  renderComponentToStaticMarkup: renderComponentToStaticMarkup\n};\n\n},{\"./ReactDescriptor\":56,\"./ReactInstanceHandles\":64,\"./ReactMarkupChecksum\":66,\"./ReactServerRenderingTransaction\":80,\"./instantiateReactComponent\":133,\"./invariant\":134}],80:[function(_dereq_,module,exports){\n/**\n * Copyright 2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule ReactServerRenderingTransaction\n * @typechecks\n */\n\n\"use strict\";\n\nvar PooledClass = _dereq_(\"./PooledClass\");\nvar CallbackQueue = _dereq_(\"./CallbackQueue\");\nvar ReactPutListenerQueue = _dereq_(\"./ReactPutListenerQueue\");\nvar Transaction = _dereq_(\"./Transaction\");\n\nvar emptyFunction = _dereq_(\"./emptyFunction\");\nvar mixInto = _dereq_(\"./mixInto\");\n\n/**\n * Provides a `CallbackQueue` queue for collecting `onDOMReady` callbacks\n * during the performing of the transaction.\n */\nvar ON_DOM_READY_QUEUEING = {\n  /**\n   * Initializes the internal `onDOMReady` queue.\n   */\n  initialize: function() {\n    this.reactMountReady.reset();\n  },\n\n  close: emptyFunction\n};\n\nvar PUT_LISTENER_QUEUEING = {\n  initialize: function() {\n    this.putListenerQueue.reset();\n  },\n\n  close: emptyFunction\n};\n\n/**\n * Executed within the scope of the `Transaction` instance. Consider these as\n * being member methods, but with an implied ordering while being isolated from\n * each other.\n */\nvar TRANSACTION_WRAPPERS = [\n  PUT_LISTENER_QUEUEING,\n  ON_DOM_READY_QUEUEING\n];\n\n/**\n * @class ReactServerRenderingTransaction\n * @param {boolean} renderToStaticMarkup\n */\nfunction ReactServerRenderingTransaction(renderToStaticMarkup) {\n  this.reinitializeTransaction();\n  this.renderToStaticMarkup = renderToStaticMarkup;\n  this.reactMountReady = CallbackQueue.getPooled(null);\n  this.putListenerQueue = ReactPutListenerQueue.getPooled();\n}\n\nvar Mixin = {\n  /**\n   * @see Transaction\n   * @abstract\n   * @final\n   * @return {array} Empty list of operation wrap proceedures.\n   */\n  getTransactionWrappers: function() {\n    return TRANSACTION_WRAPPERS;\n  },\n\n  /**\n   * @return {object} The queue to collect `onDOMReady` callbacks with.\n   */\n  getReactMountReady: function() {\n    return this.reactMountReady;\n  },\n\n  getPutListenerQueue: function() {\n    return this.putListenerQueue;\n  },\n\n  /**\n   * `PooledClass` looks for this, and will invoke this before allowing this\n   * instance to be resused.\n   */\n  destructor: function() {\n    CallbackQueue.release(this.reactMountReady);\n    this.reactMountReady = null;\n\n    ReactPutListenerQueue.release(this.putListenerQueue);\n    this.putListenerQueue = null;\n  }\n};\n\n\nmixInto(ReactServerRenderingTransaction, Transaction.Mixin);\nmixInto(ReactServerRenderingTransaction, Mixin);\n\nPooledClass.addPoolingTo(ReactServerRenderingTransaction);\n\nmodule.exports = ReactServerRenderingTransaction;\n\n},{\"./CallbackQueue\":6,\"./PooledClass\":28,\"./ReactPutListenerQueue\":76,\"./Transaction\":104,\"./emptyFunction\":116,\"./mixInto\":147}],81:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule ReactStateSetters\n */\n\n\"use strict\";\n\nvar ReactStateSetters = {\n  /**\n   * Returns a function that calls the provided function, and uses the result\n   * of that to set the component's state.\n   *\n   * @param {ReactCompositeComponent} component\n   * @param {function} funcReturningState Returned callback uses this to\n   *                                      determine how to update state.\n   * @return {function} callback that when invoked uses funcReturningState to\n   *                    determined the object literal to setState.\n   */\n  createStateSetter: function(component, funcReturningState) {\n    return function(a, b, c, d, e, f) {\n      var partialState = funcReturningState.call(component, a, b, c, d, e, f);\n      if (partialState) {\n        component.setState(partialState);\n      }\n    };\n  },\n\n  /**\n   * Returns a single-argument callback that can be used to update a single\n   * key in the component's state.\n   *\n   * Note: this is memoized function, which makes it inexpensive to call.\n   *\n   * @param {ReactCompositeComponent} component\n   * @param {string} key The key in the state that you should update.\n   * @return {function} callback of 1 argument which calls setState() with\n   *                    the provided keyName and callback argument.\n   */\n  createStateKeySetter: function(component, key) {\n    // Memoize the setters.\n    var cache = component.__keySetters || (component.__keySetters = {});\n    return cache[key] || (cache[key] = createStateKeySetter(component, key));\n  }\n};\n\nfunction createStateKeySetter(component, key) {\n  // Partial state is allocated outside of the function closure so it can be\n  // reused with every call, avoiding memory allocation when this function\n  // is called.\n  var partialState = {};\n  return function stateKeySetter(value) {\n    partialState[key] = value;\n    component.setState(partialState);\n  };\n}\n\nReactStateSetters.Mixin = {\n  /**\n   * Returns a function that calls the provided function, and uses the result\n   * of that to set the component's state.\n   *\n   * For example, these statements are equivalent:\n   *\n   *   this.setState({x: 1});\n   *   this.createStateSetter(function(xValue) {\n   *     return {x: xValue};\n   *   })(1);\n   *\n   * @param {function} funcReturningState Returned callback uses this to\n   *                                      determine how to update state.\n   * @return {function} callback that when invoked uses funcReturningState to\n   *                    determined the object literal to setState.\n   */\n  createStateSetter: function(funcReturningState) {\n    return ReactStateSetters.createStateSetter(this, funcReturningState);\n  },\n\n  /**\n   * Returns a single-argument callback that can be used to update a single\n   * key in the component's state.\n   *\n   * For example, these statements are equivalent:\n   *\n   *   this.setState({x: 1});\n   *   this.createStateKeySetter('x')(1);\n   *\n   * Note: this is memoized function, which makes it inexpensive to call.\n   *\n   * @param {string} key The key in the state that you should update.\n   * @return {function} callback of 1 argument which calls setState() with\n   *                    the provided keyName and callback argument.\n   */\n  createStateKeySetter: function(key) {\n    return ReactStateSetters.createStateKeySetter(this, key);\n  }\n};\n\nmodule.exports = ReactStateSetters;\n\n},{}],82:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule ReactTestUtils\n */\n\n\"use strict\";\n\nvar EventConstants = _dereq_(\"./EventConstants\");\nvar EventPluginHub = _dereq_(\"./EventPluginHub\");\nvar EventPropagators = _dereq_(\"./EventPropagators\");\nvar React = _dereq_(\"./React\");\nvar ReactDescriptor = _dereq_(\"./ReactDescriptor\");\nvar ReactDOM = _dereq_(\"./ReactDOM\");\nvar ReactBrowserEventEmitter = _dereq_(\"./ReactBrowserEventEmitter\");\nvar ReactMount = _dereq_(\"./ReactMount\");\nvar ReactTextComponent = _dereq_(\"./ReactTextComponent\");\nvar ReactUpdates = _dereq_(\"./ReactUpdates\");\nvar SyntheticEvent = _dereq_(\"./SyntheticEvent\");\n\nvar mergeInto = _dereq_(\"./mergeInto\");\nvar copyProperties = _dereq_(\"./copyProperties\");\n\nvar topLevelTypes = EventConstants.topLevelTypes;\n\nfunction Event(suffix) {}\n\n/**\n * @class ReactTestUtils\n */\n\n/**\n * Todo: Support the entire DOM.scry query syntax. For now, these simple\n * utilities will suffice for testing purposes.\n * @lends ReactTestUtils\n */\nvar ReactTestUtils = {\n  renderIntoDocument: function(instance) {\n    var div = document.createElement('div');\n    // None of our tests actually require attaching the container to the\n    // DOM, and doing so creates a mess that we rely on test isolation to\n    // clean up, so we're going to stop honoring the name of this method\n    // (and probably rename it eventually) if no problems arise.\n    // document.documentElement.appendChild(div);\n    return React.renderComponent(instance, div);\n  },\n\n  isDescriptor: function(descriptor) {\n    return ReactDescriptor.isValidDescriptor(descriptor);\n  },\n\n  isDescriptorOfType: function(inst, convenienceConstructor) {\n    return (\n      ReactDescriptor.isValidDescriptor(inst) &&\n      inst.type === convenienceConstructor.type\n    );\n  },\n\n  isDOMComponent: function(inst) {\n    return !!(inst && inst.mountComponent && inst.tagName);\n  },\n\n  isDOMComponentDescriptor: function(inst) {\n    return !!(inst &&\n              ReactDescriptor.isValidDescriptor(inst) &&\n              !!inst.tagName);\n  },\n\n  isCompositeComponent: function(inst) {\n    return typeof inst.render === 'function' &&\n           typeof inst.setState === 'function';\n  },\n\n  isCompositeComponentWithType: function(inst, type) {\n    return !!(ReactTestUtils.isCompositeComponent(inst) &&\n             (inst.constructor === type.type));\n  },\n\n  isCompositeComponentDescriptor: function(inst) {\n    if (!ReactDescriptor.isValidDescriptor(inst)) {\n      return false;\n    }\n    // We check the prototype of the type that will get mounted, not the\n    // instance itself. This is a future proof way of duck typing.\n    var prototype = inst.type.prototype;\n    return (\n      typeof prototype.render === 'function' &&\n      typeof prototype.setState === 'function'\n    );\n  },\n\n  isCompositeComponentDescriptorWithType: function(inst, type) {\n    return !!(ReactTestUtils.isCompositeComponentDescriptor(inst) &&\n             (inst.constructor === type));\n  },\n\n  isTextComponent: function(inst) {\n    return inst instanceof ReactTextComponent.type;\n  },\n\n  findAllInRenderedTree: function(inst, test) {\n    if (!inst) {\n      return [];\n    }\n    var ret = test(inst) ? [inst] : [];\n    if (ReactTestUtils.isDOMComponent(inst)) {\n      var renderedChildren = inst._renderedChildren;\n      var key;\n      for (key in renderedChildren) {\n        if (!renderedChildren.hasOwnProperty(key)) {\n          continue;\n        }\n        ret = ret.concat(\n          ReactTestUtils.findAllInRenderedTree(renderedChildren[key], test)\n        );\n      }\n    } else if (ReactTestUtils.isCompositeComponent(inst)) {\n      ret = ret.concat(\n        ReactTestUtils.findAllInRenderedTree(inst._renderedComponent, test)\n      );\n    }\n    return ret;\n  },\n\n  /**\n   * Finds all instance of components in the rendered tree that are DOM\n   * components with the class name matching `className`.\n   * @return an array of all the matches.\n   */\n  scryRenderedDOMComponentsWithClass: function(root, className) {\n    return ReactTestUtils.findAllInRenderedTree(root, function(inst) {\n      var instClassName = inst.props.className;\n      return ReactTestUtils.isDOMComponent(inst) && (\n        instClassName &&\n        (' ' + instClassName + ' ').indexOf(' ' + className + ' ') !== -1\n      );\n    });\n  },\n\n  /**\n   * Like scryRenderedDOMComponentsWithClass but expects there to be one result,\n   * and returns that one result, or throws exception if there is any other\n   * number of matches besides one.\n   * @return {!ReactDOMComponent} The one match.\n   */\n  findRenderedDOMComponentWithClass: function(root, className) {\n    var all =\n      ReactTestUtils.scryRenderedDOMComponentsWithClass(root, className);\n    if (all.length !== 1) {\n      throw new Error('Did not find exactly one match for class:' + className);\n    }\n    return all[0];\n  },\n\n\n  /**\n   * Finds all instance of components in the rendered tree that are DOM\n   * components with the tag name matching `tagName`.\n   * @return an array of all the matches.\n   */\n  scryRenderedDOMComponentsWithTag: function(root, tagName) {\n    return ReactTestUtils.findAllInRenderedTree(root, function(inst) {\n      return ReactTestUtils.isDOMComponent(inst) &&\n            inst.tagName === tagName.toUpperCase();\n    });\n  },\n\n  /**\n   * Like scryRenderedDOMComponentsWithTag but expects there to be one result,\n   * and returns that one result, or throws exception if there is any other\n   * number of matches besides one.\n   * @return {!ReactDOMComponent} The one match.\n   */\n  findRenderedDOMComponentWithTag: function(root, tagName) {\n    var all = ReactTestUtils.scryRenderedDOMComponentsWithTag(root, tagName);\n    if (all.length !== 1) {\n      throw new Error('Did not find exactly one match for tag:' + tagName);\n    }\n    return all[0];\n  },\n\n\n  /**\n   * Finds all instances of components with type equal to `componentType`.\n   * @return an array of all the matches.\n   */\n  scryRenderedComponentsWithType: function(root, componentType) {\n    return ReactTestUtils.findAllInRenderedTree(root, function(inst) {\n      return ReactTestUtils.isCompositeComponentWithType(\n        inst,\n        componentType\n      );\n    });\n  },\n\n  /**\n   * Same as `scryRenderedComponentsWithType` but expects there to be one result\n   * and returns that one result, or throws exception if there is any other\n   * number of matches besides one.\n   * @return {!ReactComponent} The one match.\n   */\n  findRenderedComponentWithType: function(root, componentType) {\n    var all = ReactTestUtils.scryRenderedComponentsWithType(\n      root,\n      componentType\n    );\n    if (all.length !== 1) {\n      throw new Error(\n        'Did not find exactly one match for componentType:' + componentType\n      );\n    }\n    return all[0];\n  },\n\n  /**\n   * Pass a mocked component module to this method to augment it with\n   * useful methods that allow it to be used as a dummy React component.\n   * Instead of rendering as usual, the component will become a simple\n   * <div> containing any provided children.\n   *\n   * @param {object} module the mock function object exported from a\n   *                        module that defines the component to be mocked\n   * @param {?string} mockTagName optional dummy root tag name to return\n   *                              from render method (overrides\n   *                              module.mockTagName if provided)\n   * @return {object} the ReactTestUtils object (for chaining)\n   */\n  mockComponent: function(module, mockTagName) {\n    var ConvenienceConstructor = React.createClass({\n      render: function() {\n        var mockTagName = mockTagName || module.mockTagName || \"div\";\n        return ReactDOM[mockTagName](null, this.props.children);\n      }\n    });\n\n    copyProperties(module, ConvenienceConstructor);\n    module.mockImplementation(ConvenienceConstructor);\n\n    return this;\n  },\n\n  /**\n   * Simulates a top level event being dispatched from a raw event that occured\n   * on an `Element` node.\n   * @param topLevelType {Object} A type from `EventConstants.topLevelTypes`\n   * @param {!Element} node The dom to simulate an event occurring on.\n   * @param {?Event} fakeNativeEvent Fake native event to use in SyntheticEvent.\n   */\n  simulateNativeEventOnNode: function(topLevelType, node, fakeNativeEvent) {\n    fakeNativeEvent.target = node;\n    ReactBrowserEventEmitter.ReactEventListener.dispatchEvent(\n      topLevelType,\n      fakeNativeEvent\n    );\n  },\n\n  /**\n   * Simulates a top level event being dispatched from a raw event that occured\n   * on the `ReactDOMComponent` `comp`.\n   * @param topLevelType {Object} A type from `EventConstants.topLevelTypes`.\n   * @param comp {!ReactDOMComponent}\n   * @param {?Event} fakeNativeEvent Fake native event to use in SyntheticEvent.\n   */\n  simulateNativeEventOnDOMComponent: function(\n      topLevelType,\n      comp,\n      fakeNativeEvent) {\n    ReactTestUtils.simulateNativeEventOnNode(\n      topLevelType,\n      comp.getDOMNode(),\n      fakeNativeEvent\n    );\n  },\n\n  nativeTouchData: function(x, y) {\n    return {\n      touches: [\n        {pageX: x, pageY: y}\n      ]\n    };\n  },\n\n  Simulate: null,\n  SimulateNative: {}\n};\n\n/**\n * Exports:\n *\n * - `ReactTestUtils.Simulate.click(Element/ReactDOMComponent)`\n * - `ReactTestUtils.Simulate.mouseMove(Element/ReactDOMComponent)`\n * - `ReactTestUtils.Simulate.change(Element/ReactDOMComponent)`\n * - ... (All keys from event plugin `eventTypes` objects)\n */\nfunction makeSimulator(eventType) {\n  return function(domComponentOrNode, eventData) {\n    var node;\n    if (ReactTestUtils.isDOMComponent(domComponentOrNode)) {\n      node = domComponentOrNode.getDOMNode();\n    } else if (domComponentOrNode.tagName) {\n      node = domComponentOrNode;\n    }\n\n    var fakeNativeEvent = new Event();\n    fakeNativeEvent.target = node;\n    // We don't use SyntheticEvent.getPooled in order to not have to worry about\n    // properly destroying any properties assigned from `eventData` upon release\n    var event = new SyntheticEvent(\n      ReactBrowserEventEmitter.eventNameDispatchConfigs[eventType],\n      ReactMount.getID(node),\n      fakeNativeEvent\n    );\n    mergeInto(event, eventData);\n    EventPropagators.accumulateTwoPhaseDispatches(event);\n\n    ReactUpdates.batchedUpdates(function() {\n      EventPluginHub.enqueueEvents(event);\n      EventPluginHub.processEventQueue();\n    });\n  };\n}\n\nfunction buildSimulators() {\n  ReactTestUtils.Simulate = {};\n\n  var eventType;\n  for (eventType in ReactBrowserEventEmitter.eventNameDispatchConfigs) {\n    /**\n     * @param {!Element || ReactDOMComponent} domComponentOrNode\n     * @param {?object} eventData Fake event data to use in SyntheticEvent.\n     */\n    ReactTestUtils.Simulate[eventType] = makeSimulator(eventType);\n  }\n}\n\n// Rebuild ReactTestUtils.Simulate whenever event plugins are injected\nvar oldInjectEventPluginOrder = EventPluginHub.injection.injectEventPluginOrder;\nEventPluginHub.injection.injectEventPluginOrder = function() {\n  oldInjectEventPluginOrder.apply(this, arguments);\n  buildSimulators();\n};\nvar oldInjectEventPlugins = EventPluginHub.injection.injectEventPluginsByName;\nEventPluginHub.injection.injectEventPluginsByName = function() {\n  oldInjectEventPlugins.apply(this, arguments);\n  buildSimulators();\n};\n\nbuildSimulators();\n\n/**\n * Exports:\n *\n * - `ReactTestUtils.SimulateNative.click(Element/ReactDOMComponent)`\n * - `ReactTestUtils.SimulateNative.mouseMove(Element/ReactDOMComponent)`\n * - `ReactTestUtils.SimulateNative.mouseIn/ReactDOMComponent)`\n * - `ReactTestUtils.SimulateNative.mouseOut(Element/ReactDOMComponent)`\n * - ... (All keys from `EventConstants.topLevelTypes`)\n *\n * Note: Top level event types are a subset of the entire set of handler types\n * (which include a broader set of \"synthetic\" events). For example, onDragDone\n * is a synthetic event. Except when testing an event plugin or React's event\n * handling code specifically, you probably want to use ReactTestUtils.Simulate\n * to dispatch synthetic events.\n */\n\nfunction makeNativeSimulator(eventType) {\n  return function(domComponentOrNode, nativeEventData) {\n    var fakeNativeEvent = new Event(eventType);\n    mergeInto(fakeNativeEvent, nativeEventData);\n    if (ReactTestUtils.isDOMComponent(domComponentOrNode)) {\n      ReactTestUtils.simulateNativeEventOnDOMComponent(\n        eventType,\n        domComponentOrNode,\n        fakeNativeEvent\n      );\n    } else if (!!domComponentOrNode.tagName) {\n      // Will allow on actual dom nodes.\n      ReactTestUtils.simulateNativeEventOnNode(\n        eventType,\n        domComponentOrNode,\n        fakeNativeEvent\n      );\n    }\n  };\n}\n\nvar eventType;\nfor (eventType in topLevelTypes) {\n  // Event type is stored as 'topClick' - we transform that to 'click'\n  var convenienceName = eventType.indexOf('top') === 0 ?\n    eventType.charAt(3).toLowerCase() + eventType.substr(4) : eventType;\n  /**\n   * @param {!Element || ReactDOMComponent} domComponentOrNode\n   * @param {?Event} nativeEventData Fake native event to use in SyntheticEvent.\n   */\n  ReactTestUtils.SimulateNative[convenienceName] =\n    makeNativeSimulator(eventType);\n}\n\nmodule.exports = ReactTestUtils;\n\n},{\"./EventConstants\":16,\"./EventPluginHub\":18,\"./EventPropagators\":21,\"./React\":29,\"./ReactBrowserEventEmitter\":31,\"./ReactDOM\":41,\"./ReactDescriptor\":56,\"./ReactMount\":67,\"./ReactTextComponent\":83,\"./ReactUpdates\":87,\"./SyntheticEvent\":96,\"./copyProperties\":110,\"./mergeInto\":146}],83:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule ReactTextComponent\n * @typechecks static-only\n */\n\n\"use strict\";\n\nvar DOMPropertyOperations = _dereq_(\"./DOMPropertyOperations\");\nvar ReactBrowserComponentMixin = _dereq_(\"./ReactBrowserComponentMixin\");\nvar ReactComponent = _dereq_(\"./ReactComponent\");\nvar ReactDescriptor = _dereq_(\"./ReactDescriptor\");\n\nvar escapeTextForBrowser = _dereq_(\"./escapeTextForBrowser\");\nvar mixInto = _dereq_(\"./mixInto\");\n\n/**\n * Text nodes violate a couple assumptions that React makes about components:\n *\n *  - When mounting text into the DOM, adjacent text nodes are merged.\n *  - Text nodes cannot be assigned a React root ID.\n *\n * This component is used to wrap strings in elements so that they can undergo\n * the same reconciliation that is applied to elements.\n *\n * TODO: Investigate representing React components in the DOM with text nodes.\n *\n * @class ReactTextComponent\n * @extends ReactComponent\n * @internal\n */\nvar ReactTextComponent = function(descriptor) {\n  this.construct(descriptor);\n};\n\nmixInto(ReactTextComponent, ReactComponent.Mixin);\nmixInto(ReactTextComponent, ReactBrowserComponentMixin);\nmixInto(ReactTextComponent, {\n\n  /**\n   * Creates the markup for this text node. This node is not intended to have\n   * any features besides containing text content.\n   *\n   * @param {string} rootID DOM ID of the root node.\n   * @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction\n   * @param {number} mountDepth number of components in the owner hierarchy\n   * @return {string} Markup for this text node.\n   * @internal\n   */\n  mountComponent: function(rootID, transaction, mountDepth) {\n    ReactComponent.Mixin.mountComponent.call(\n      this,\n      rootID,\n      transaction,\n      mountDepth\n    );\n\n    var escapedText = escapeTextForBrowser(this.props);\n\n    if (transaction.renderToStaticMarkup) {\n      // Normally we'd wrap this in a `span` for the reasons stated above, but\n      // since this is a situation where React won't take over (static pages),\n      // we can simply return the text as it is.\n      return escapedText;\n    }\n\n    return (\n      '<span ' + DOMPropertyOperations.createMarkupForID(rootID) + '>' +\n        escapedText +\n      '</span>'\n    );\n  },\n\n  /**\n   * Updates this component by updating the text content.\n   *\n   * @param {object} nextComponent Contains the next text content.\n   * @param {ReactReconcileTransaction} transaction\n   * @internal\n   */\n  receiveComponent: function(nextComponent, transaction) {\n    var nextProps = nextComponent.props;\n    if (nextProps !== this.props) {\n      this.props = nextProps;\n      ReactComponent.BackendIDOperations.updateTextContentByID(\n        this._rootNodeID,\n        nextProps\n      );\n    }\n  }\n\n});\n\nmodule.exports = ReactDescriptor.createFactory(ReactTextComponent);\n\n},{\"./DOMPropertyOperations\":12,\"./ReactBrowserComponentMixin\":30,\"./ReactComponent\":35,\"./ReactDescriptor\":56,\"./escapeTextForBrowser\":118,\"./mixInto\":147}],84:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @typechecks static-only\n * @providesModule ReactTransitionChildMapping\n */\n\n\"use strict\";\n\nvar ReactChildren = _dereq_(\"./ReactChildren\");\n\nvar ReactTransitionChildMapping = {\n  /**\n   * Given `this.props.children`, return an object mapping key to child. Just\n   * simple syntactic sugar around ReactChildren.map().\n   *\n   * @param {*} children `this.props.children`\n   * @return {object} Mapping of key to child\n   */\n  getChildMapping: function(children) {\n    return ReactChildren.map(children, function(child) {\n      return child;\n    });\n  },\n\n  /**\n   * When you're adding or removing children some may be added or removed in the\n   * same render pass. We want ot show *both* since we want to simultaneously\n   * animate elements in and out. This function takes a previous set of keys\n   * and a new set of keys and merges them with its best guess of the correct\n   * ordering. In the future we may expose some of the utilities in\n   * ReactMultiChild to make this easy, but for now React itself does not\n   * directly have this concept of the union of prevChildren and nextChildren\n   * so we implement it here.\n   *\n   * @param {object} prev prev children as returned from\n   * `ReactTransitionChildMapping.getChildMapping()`.\n   * @param {object} next next children as returned from\n   * `ReactTransitionChildMapping.getChildMapping()`.\n   * @return {object} a key set that contains all keys in `prev` and all keys\n   * in `next` in a reasonable order.\n   */\n  mergeChildMappings: function(prev, next) {\n    prev = prev || {};\n    next = next || {};\n\n    function getValueForKey(key) {\n      if (next.hasOwnProperty(key)) {\n        return next[key];\n      } else {\n        return prev[key];\n      }\n    }\n\n    // For each key of `next`, the list of keys to insert before that key in\n    // the combined list\n    var nextKeysPending = {};\n\n    var pendingKeys = [];\n    for (var prevKey in prev) {\n      if (next.hasOwnProperty(prevKey)) {\n        if (pendingKeys.length) {\n          nextKeysPending[prevKey] = pendingKeys;\n          pendingKeys = [];\n        }\n      } else {\n        pendingKeys.push(prevKey);\n      }\n    }\n\n    var i;\n    var childMapping = {};\n    for (var nextKey in next) {\n      if (nextKeysPending.hasOwnProperty(nextKey)) {\n        for (i = 0; i < nextKeysPending[nextKey].length; i++) {\n          var pendingNextKey = nextKeysPending[nextKey][i];\n          childMapping[nextKeysPending[nextKey][i]] = getValueForKey(\n            pendingNextKey\n          );\n        }\n      }\n      childMapping[nextKey] = getValueForKey(nextKey);\n    }\n\n    // Finally, add the keys which didn't appear before any key in `next`\n    for (i = 0; i < pendingKeys.length; i++) {\n      childMapping[pendingKeys[i]] = getValueForKey(pendingKeys[i]);\n    }\n\n    return childMapping;\n  }\n};\n\nmodule.exports = ReactTransitionChildMapping;\n\n},{\"./ReactChildren\":34}],85:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule ReactTransitionEvents\n */\n\n\"use strict\";\n\nvar ExecutionEnvironment = _dereq_(\"./ExecutionEnvironment\");\n\n/**\n * EVENT_NAME_MAP is used to determine which event fired when a\n * transition/animation ends, based on the style property used to\n * define that event.\n */\nvar EVENT_NAME_MAP = {\n  transitionend: {\n    'transition': 'transitionend',\n    'WebkitTransition': 'webkitTransitionEnd',\n    'MozTransition': 'mozTransitionEnd',\n    'OTransition': 'oTransitionEnd',\n    'msTransition': 'MSTransitionEnd'\n  },\n\n  animationend: {\n    'animation': 'animationend',\n    'WebkitAnimation': 'webkitAnimationEnd',\n    'MozAnimation': 'mozAnimationEnd',\n    'OAnimation': 'oAnimationEnd',\n    'msAnimation': 'MSAnimationEnd'\n  }\n};\n\nvar endEvents = [];\n\nfunction detectEvents() {\n  var testEl = document.createElement('div');\n  var style = testEl.style;\n\n  // On some platforms, in particular some releases of Android 4.x,\n  // the un-prefixed \"animation\" and \"transition\" properties are defined on the\n  // style object but the events that fire will still be prefixed, so we need\n  // to check if the un-prefixed events are useable, and if not remove them\n  // from the map\n  if (!('AnimationEvent' in window)) {\n    delete EVENT_NAME_MAP.animationend.animation;\n  }\n\n  if (!('TransitionEvent' in window)) {\n    delete EVENT_NAME_MAP.transitionend.transition;\n  }\n\n  for (var baseEventName in EVENT_NAME_MAP) {\n    var baseEvents = EVENT_NAME_MAP[baseEventName];\n    for (var styleName in baseEvents) {\n      if (styleName in style) {\n        endEvents.push(baseEvents[styleName]);\n        break;\n      }\n    }\n  }\n}\n\nif (ExecutionEnvironment.canUseDOM) {\n  detectEvents();\n}\n\n// We use the raw {add|remove}EventListener() call because EventListener\n// does not know how to remove event listeners and we really should\n// clean up. Also, these events are not triggered in older browsers\n// so we should be A-OK here.\n\nfunction addEventListener(node, eventName, eventListener) {\n  node.addEventListener(eventName, eventListener, false);\n}\n\nfunction removeEventListener(node, eventName, eventListener) {\n  node.removeEventListener(eventName, eventListener, false);\n}\n\nvar ReactTransitionEvents = {\n  addEndEventListener: function(node, eventListener) {\n    if (endEvents.length === 0) {\n      // If CSS transitions are not supported, trigger an \"end animation\"\n      // event immediately.\n      window.setTimeout(eventListener, 0);\n      return;\n    }\n    endEvents.forEach(function(endEvent) {\n      addEventListener(node, endEvent, eventListener);\n    });\n  },\n\n  removeEndEventListener: function(node, eventListener) {\n    if (endEvents.length === 0) {\n      return;\n    }\n    endEvents.forEach(function(endEvent) {\n      removeEventListener(node, endEvent, eventListener);\n    });\n  }\n};\n\nmodule.exports = ReactTransitionEvents;\n\n},{\"./ExecutionEnvironment\":22}],86:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule ReactTransitionGroup\n */\n\n\"use strict\";\n\nvar React = _dereq_(\"./React\");\nvar ReactTransitionChildMapping = _dereq_(\"./ReactTransitionChildMapping\");\n\nvar cloneWithProps = _dereq_(\"./cloneWithProps\");\nvar emptyFunction = _dereq_(\"./emptyFunction\");\nvar merge = _dereq_(\"./merge\");\n\nvar ReactTransitionGroup = React.createClass({\n  displayName: 'ReactTransitionGroup',\n\n  propTypes: {\n    component: React.PropTypes.func,\n    childFactory: React.PropTypes.func\n  },\n\n  getDefaultProps: function() {\n    return {\n      component: React.DOM.span,\n      childFactory: emptyFunction.thatReturnsArgument\n    };\n  },\n\n  getInitialState: function() {\n    return {\n      children: ReactTransitionChildMapping.getChildMapping(this.props.children)\n    };\n  },\n\n  componentWillReceiveProps: function(nextProps) {\n    var nextChildMapping = ReactTransitionChildMapping.getChildMapping(\n      nextProps.children\n    );\n    var prevChildMapping = this.state.children;\n\n    this.setState({\n      children: ReactTransitionChildMapping.mergeChildMappings(\n        prevChildMapping,\n        nextChildMapping\n      )\n    });\n\n    var key;\n\n    for (key in nextChildMapping) {\n      var hasPrev = prevChildMapping && prevChildMapping.hasOwnProperty(key);\n      if (nextChildMapping[key] && !hasPrev &&\n          !this.currentlyTransitioningKeys[key]) {\n        this.keysToEnter.push(key);\n      }\n    }\n\n    for (key in prevChildMapping) {\n      var hasNext = nextChildMapping && nextChildMapping.hasOwnProperty(key);\n      if (prevChildMapping[key] && !hasNext &&\n          !this.currentlyTransitioningKeys[key]) {\n        this.keysToLeave.push(key);\n      }\n    }\n\n    // If we want to someday check for reordering, we could do it here.\n  },\n\n  componentWillMount: function() {\n    this.currentlyTransitioningKeys = {};\n    this.keysToEnter = [];\n    this.keysToLeave = [];\n  },\n\n  componentDidUpdate: function() {\n    var keysToEnter = this.keysToEnter;\n    this.keysToEnter = [];\n    keysToEnter.forEach(this.performEnter);\n\n    var keysToLeave = this.keysToLeave;\n    this.keysToLeave = [];\n    keysToLeave.forEach(this.performLeave);\n  },\n\n  performEnter: function(key) {\n    this.currentlyTransitioningKeys[key] = true;\n\n    var component = this.refs[key];\n\n    if (component.componentWillEnter) {\n      component.componentWillEnter(\n        this._handleDoneEntering.bind(this, key)\n      );\n    } else {\n      this._handleDoneEntering(key);\n    }\n  },\n\n  _handleDoneEntering: function(key) {\n    var component = this.refs[key];\n    if (component.componentDidEnter) {\n      component.componentDidEnter();\n    }\n\n    delete this.currentlyTransitioningKeys[key];\n\n    var currentChildMapping = ReactTransitionChildMapping.getChildMapping(\n      this.props.children\n    );\n\n    if (!currentChildMapping || !currentChildMapping.hasOwnProperty(key)) {\n      // This was removed before it had fully entered. Remove it.\n      this.performLeave(key);\n    }\n  },\n\n  performLeave: function(key) {\n    this.currentlyTransitioningKeys[key] = true;\n\n    var component = this.refs[key];\n    if (component.componentWillLeave) {\n      component.componentWillLeave(this._handleDoneLeaving.bind(this, key));\n    } else {\n      // Note that this is somewhat dangerous b/c it calls setState()\n      // again, effectively mutating the component before all the work\n      // is done.\n      this._handleDoneLeaving(key);\n    }\n  },\n\n  _handleDoneLeaving: function(key) {\n    var component = this.refs[key];\n\n    if (component.componentDidLeave) {\n      component.componentDidLeave();\n    }\n\n    delete this.currentlyTransitioningKeys[key];\n\n    var currentChildMapping = ReactTransitionChildMapping.getChildMapping(\n      this.props.children\n    );\n\n    if (currentChildMapping && currentChildMapping.hasOwnProperty(key)) {\n      // This entered again before it fully left. Add it again.\n      this.performEnter(key);\n    } else {\n      var newChildren = merge(this.state.children);\n      delete newChildren[key];\n      this.setState({children: newChildren});\n    }\n  },\n\n  render: function() {\n    // TODO: we could get rid of the need for the wrapper node\n    // by cloning a single child\n    var childrenToRender = {};\n    for (var key in this.state.children) {\n      var child = this.state.children[key];\n      if (child) {\n        // You may need to apply reactive updates to a child as it is leaving.\n        // The normal React way to do it won't work since the child will have\n        // already been removed. In case you need this behavior you can provide\n        // a childFactory function to wrap every child, even the ones that are\n        // leaving.\n        childrenToRender[key] = cloneWithProps(\n          this.props.childFactory(child),\n          {ref: key}\n        );\n      }\n    }\n    return this.transferPropsTo(this.props.component(null, childrenToRender));\n  }\n});\n\nmodule.exports = ReactTransitionGroup;\n\n},{\"./React\":29,\"./ReactTransitionChildMapping\":84,\"./cloneWithProps\":108,\"./emptyFunction\":116,\"./merge\":144}],87:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule ReactUpdates\n */\n\n\"use strict\";\n\nvar CallbackQueue = _dereq_(\"./CallbackQueue\");\nvar PooledClass = _dereq_(\"./PooledClass\");\nvar ReactCurrentOwner = _dereq_(\"./ReactCurrentOwner\");\nvar ReactPerf = _dereq_(\"./ReactPerf\");\nvar Transaction = _dereq_(\"./Transaction\");\n\nvar invariant = _dereq_(\"./invariant\");\nvar mixInto = _dereq_(\"./mixInto\");\nvar warning = _dereq_(\"./warning\");\n\nvar dirtyComponents = [];\n\nvar batchingStrategy = null;\n\nfunction ensureInjected() {\n  (\"production\" !== \"development\" ? invariant(\n    ReactUpdates.ReactReconcileTransaction && batchingStrategy,\n    'ReactUpdates: must inject a reconcile transaction class and batching ' +\n    'strategy'\n  ) : invariant(ReactUpdates.ReactReconcileTransaction && batchingStrategy));\n}\n\nvar NESTED_UPDATES = {\n  initialize: function() {\n    this.dirtyComponentsLength = dirtyComponents.length;\n  },\n  close: function() {\n    if (this.dirtyComponentsLength !== dirtyComponents.length) {\n      // Additional updates were enqueued by componentDidUpdate handlers or\n      // similar; before our own UPDATE_QUEUEING wrapper closes, we want to run\n      // these new updates so that if A's componentDidUpdate calls setState on\n      // B, B will update before the callback A's updater provided when calling\n      // setState.\n      dirtyComponents.splice(0, this.dirtyComponentsLength);\n      flushBatchedUpdates();\n    } else {\n      dirtyComponents.length = 0;\n    }\n  }\n};\n\nvar UPDATE_QUEUEING = {\n  initialize: function() {\n    this.callbackQueue.reset();\n  },\n  close: function() {\n    this.callbackQueue.notifyAll();\n  }\n};\n\nvar TRANSACTION_WRAPPERS = [NESTED_UPDATES, UPDATE_QUEUEING];\n\nfunction ReactUpdatesFlushTransaction() {\n  this.reinitializeTransaction();\n  this.dirtyComponentsLength = null;\n  this.callbackQueue = CallbackQueue.getPooled(null);\n  this.reconcileTransaction =\n    ReactUpdates.ReactReconcileTransaction.getPooled();\n}\n\nmixInto(ReactUpdatesFlushTransaction, Transaction.Mixin);\nmixInto(ReactUpdatesFlushTransaction, {\n  getTransactionWrappers: function() {\n    return TRANSACTION_WRAPPERS;\n  },\n\n  destructor: function() {\n    this.dirtyComponentsLength = null;\n    CallbackQueue.release(this.callbackQueue);\n    this.callbackQueue = null;\n    ReactUpdates.ReactReconcileTransaction.release(this.reconcileTransaction);\n    this.reconcileTransaction = null;\n  },\n\n  perform: function(method, scope, a) {\n    // Essentially calls `this.reconcileTransaction.perform(method, scope, a)`\n    // with this transaction's wrappers around it.\n    return Transaction.Mixin.perform.call(\n      this,\n      this.reconcileTransaction.perform,\n      this.reconcileTransaction,\n      method,\n      scope,\n      a\n    );\n  }\n});\n\nPooledClass.addPoolingTo(ReactUpdatesFlushTransaction);\n\nfunction batchedUpdates(callback, a, b) {\n  ensureInjected();\n  batchingStrategy.batchedUpdates(callback, a, b);\n}\n\n/**\n * Array comparator for ReactComponents by owner depth\n *\n * @param {ReactComponent} c1 first component you're comparing\n * @param {ReactComponent} c2 second component you're comparing\n * @return {number} Return value usable by Array.prototype.sort().\n */\nfunction mountDepthComparator(c1, c2) {\n  return c1._mountDepth - c2._mountDepth;\n}\n\nfunction runBatchedUpdates(transaction) {\n  var len = transaction.dirtyComponentsLength;\n  (\"production\" !== \"development\" ? invariant(\n    len === dirtyComponents.length,\n    'Expected flush transaction\\'s stored dirty-components length (%s) to ' +\n    'match dirty-components array length (%s).',\n    len,\n    dirtyComponents.length\n  ) : invariant(len === dirtyComponents.length));\n\n  // Since reconciling a component higher in the owner hierarchy usually (not\n  // always -- see shouldComponentUpdate()) will reconcile children, reconcile\n  // them before their children by sorting the array.\n  dirtyComponents.sort(mountDepthComparator);\n\n  for (var i = 0; i < len; i++) {\n    // If a component is unmounted before pending changes apply, ignore them\n    // TODO: Queue unmounts in the same list to avoid this happening at all\n    var component = dirtyComponents[i];\n    if (component.isMounted()) {\n      // If performUpdateIfNecessary happens to enqueue any new updates, we\n      // shouldn't execute the callbacks until the next render happens, so\n      // stash the callbacks first\n      var callbacks = component._pendingCallbacks;\n      component._pendingCallbacks = null;\n      component.performUpdateIfNecessary(transaction.reconcileTransaction);\n\n      if (callbacks) {\n        for (var j = 0; j < callbacks.length; j++) {\n          transaction.callbackQueue.enqueue(\n            callbacks[j],\n            component\n          );\n        }\n      }\n    }\n  }\n}\n\nvar flushBatchedUpdates = ReactPerf.measure(\n  'ReactUpdates',\n  'flushBatchedUpdates',\n  function() {\n    // ReactUpdatesFlushTransaction's wrappers will clear the dirtyComponents\n    // array and perform any updates enqueued by mount-ready handlers (i.e.,\n    // componentDidUpdate) but we need to check here too in order to catch\n    // updates enqueued by setState callbacks.\n    while (dirtyComponents.length) {\n      var transaction = ReactUpdatesFlushTransaction.getPooled();\n      transaction.perform(runBatchedUpdates, null, transaction);\n      ReactUpdatesFlushTransaction.release(transaction);\n    }\n  }\n);\n\n/**\n * Mark a component as needing a rerender, adding an optional callback to a\n * list of functions which will be executed once the rerender occurs.\n */\nfunction enqueueUpdate(component, callback) {\n  (\"production\" !== \"development\" ? invariant(\n    !callback || typeof callback === \"function\",\n    'enqueueUpdate(...): You called `setProps`, `replaceProps`, ' +\n    '`setState`, `replaceState`, or `forceUpdate` with a callback that ' +\n    'isn\\'t callable.'\n  ) : invariant(!callback || typeof callback === \"function\"));\n  ensureInjected();\n\n  // Various parts of our code (such as ReactCompositeComponent's\n  // _renderValidatedComponent) assume that calls to render aren't nested;\n  // verify that that's the case. (This is called by each top-level update\n  // function, like setProps, setState, forceUpdate, etc.; creation and\n  // destruction of top-level components is guarded in ReactMount.)\n  (\"production\" !== \"development\" ? warning(\n    ReactCurrentOwner.current == null,\n    'enqueueUpdate(): Render methods should be a pure function of props ' +\n    'and state; triggering nested component updates from render is not ' +\n    'allowed. If necessary, trigger nested updates in ' +\n    'componentDidUpdate.'\n  ) : null);\n\n  if (!batchingStrategy.isBatchingUpdates) {\n    batchingStrategy.batchedUpdates(enqueueUpdate, component, callback);\n    return;\n  }\n\n  dirtyComponents.push(component);\n\n  if (callback) {\n    if (component._pendingCallbacks) {\n      component._pendingCallbacks.push(callback);\n    } else {\n      component._pendingCallbacks = [callback];\n    }\n  }\n}\n\nvar ReactUpdatesInjection = {\n  injectReconcileTransaction: function(ReconcileTransaction) {\n    (\"production\" !== \"development\" ? invariant(\n      ReconcileTransaction,\n      'ReactUpdates: must provide a reconcile transaction class'\n    ) : invariant(ReconcileTransaction));\n    ReactUpdates.ReactReconcileTransaction = ReconcileTransaction;\n  },\n\n  injectBatchingStrategy: function(_batchingStrategy) {\n    (\"production\" !== \"development\" ? invariant(\n      _batchingStrategy,\n      'ReactUpdates: must provide a batching strategy'\n    ) : invariant(_batchingStrategy));\n    (\"production\" !== \"development\" ? invariant(\n      typeof _batchingStrategy.batchedUpdates === 'function',\n      'ReactUpdates: must provide a batchedUpdates() function'\n    ) : invariant(typeof _batchingStrategy.batchedUpdates === 'function'));\n    (\"production\" !== \"development\" ? invariant(\n      typeof _batchingStrategy.isBatchingUpdates === 'boolean',\n      'ReactUpdates: must provide an isBatchingUpdates boolean attribute'\n    ) : invariant(typeof _batchingStrategy.isBatchingUpdates === 'boolean'));\n    batchingStrategy = _batchingStrategy;\n  }\n};\n\nvar ReactUpdates = {\n  /**\n   * React references `ReactReconcileTransaction` using this property in order\n   * to allow dependency injection.\n   *\n   * @internal\n   */\n  ReactReconcileTransaction: null,\n\n  batchedUpdates: batchedUpdates,\n  enqueueUpdate: enqueueUpdate,\n  flushBatchedUpdates: flushBatchedUpdates,\n  injection: ReactUpdatesInjection\n};\n\nmodule.exports = ReactUpdates;\n\n},{\"./CallbackQueue\":6,\"./PooledClass\":28,\"./ReactCurrentOwner\":40,\"./ReactPerf\":71,\"./Transaction\":104,\"./invariant\":134,\"./mixInto\":147,\"./warning\":158}],88:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule ReactWithAddons\n */\n\n/**\n * This module exists purely in the open source project, and is meant as a way\n * to create a separate standalone build of React. This build has \"addons\", or\n * functionality we've built and think might be useful but doesn't have a good\n * place to live inside React core.\n */\n\n\"use strict\";\n\nvar LinkedStateMixin = _dereq_(\"./LinkedStateMixin\");\nvar React = _dereq_(\"./React\");\nvar ReactComponentWithPureRenderMixin =\n  _dereq_(\"./ReactComponentWithPureRenderMixin\");\nvar ReactCSSTransitionGroup = _dereq_(\"./ReactCSSTransitionGroup\");\nvar ReactTransitionGroup = _dereq_(\"./ReactTransitionGroup\");\n\nvar cx = _dereq_(\"./cx\");\nvar cloneWithProps = _dereq_(\"./cloneWithProps\");\nvar update = _dereq_(\"./update\");\n\nReact.addons = {\n  CSSTransitionGroup: ReactCSSTransitionGroup,\n  LinkedStateMixin: LinkedStateMixin,\n  PureRenderMixin: ReactComponentWithPureRenderMixin,\n  TransitionGroup: ReactTransitionGroup,\n\n  classSet: cx,\n  cloneWithProps: cloneWithProps,\n  update: update\n};\n\nif (\"production\" !== \"development\") {\n  React.addons.Perf = _dereq_(\"./ReactDefaultPerf\");\n  React.addons.TestUtils = _dereq_(\"./ReactTestUtils\");\n}\n\nmodule.exports = React;\n\n\n},{\"./LinkedStateMixin\":24,\"./React\":29,\"./ReactCSSTransitionGroup\":32,\"./ReactComponentWithPureRenderMixin\":37,\"./ReactDefaultPerf\":54,\"./ReactTestUtils\":82,\"./ReactTransitionGroup\":86,\"./cloneWithProps\":108,\"./cx\":114,\"./update\":157}],89:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule SVGDOMPropertyConfig\n */\n\n/*jslint bitwise: true*/\n\n\"use strict\";\n\nvar DOMProperty = _dereq_(\"./DOMProperty\");\n\nvar MUST_USE_ATTRIBUTE = DOMProperty.injection.MUST_USE_ATTRIBUTE;\n\nvar SVGDOMPropertyConfig = {\n  Properties: {\n    cx: MUST_USE_ATTRIBUTE,\n    cy: MUST_USE_ATTRIBUTE,\n    d: MUST_USE_ATTRIBUTE,\n    dx: MUST_USE_ATTRIBUTE,\n    dy: MUST_USE_ATTRIBUTE,\n    fill: MUST_USE_ATTRIBUTE,\n    fillOpacity: MUST_USE_ATTRIBUTE,\n    fontFamily: MUST_USE_ATTRIBUTE,\n    fontSize: MUST_USE_ATTRIBUTE,\n    fx: MUST_USE_ATTRIBUTE,\n    fy: MUST_USE_ATTRIBUTE,\n    gradientTransform: MUST_USE_ATTRIBUTE,\n    gradientUnits: MUST_USE_ATTRIBUTE,\n    markerEnd: MUST_USE_ATTRIBUTE,\n    markerMid: MUST_USE_ATTRIBUTE,\n    markerStart: MUST_USE_ATTRIBUTE,\n    offset: MUST_USE_ATTRIBUTE,\n    opacity: MUST_USE_ATTRIBUTE,\n    patternContentUnits: MUST_USE_ATTRIBUTE,\n    patternUnits: MUST_USE_ATTRIBUTE,\n    points: MUST_USE_ATTRIBUTE,\n    preserveAspectRatio: MUST_USE_ATTRIBUTE,\n    r: MUST_USE_ATTRIBUTE,\n    rx: MUST_USE_ATTRIBUTE,\n    ry: MUST_USE_ATTRIBUTE,\n    spreadMethod: MUST_USE_ATTRIBUTE,\n    stopColor: MUST_USE_ATTRIBUTE,\n    stopOpacity: MUST_USE_ATTRIBUTE,\n    stroke: MUST_USE_ATTRIBUTE,\n    strokeDasharray: MUST_USE_ATTRIBUTE,\n    strokeLinecap: MUST_USE_ATTRIBUTE,\n    strokeOpacity: MUST_USE_ATTRIBUTE,\n    strokeWidth: MUST_USE_ATTRIBUTE,\n    textAnchor: MUST_USE_ATTRIBUTE,\n    transform: MUST_USE_ATTRIBUTE,\n    version: MUST_USE_ATTRIBUTE,\n    viewBox: MUST_USE_ATTRIBUTE,\n    x1: MUST_USE_ATTRIBUTE,\n    x2: MUST_USE_ATTRIBUTE,\n    x: MUST_USE_ATTRIBUTE,\n    y1: MUST_USE_ATTRIBUTE,\n    y2: MUST_USE_ATTRIBUTE,\n    y: MUST_USE_ATTRIBUTE\n  },\n  DOMAttributeNames: {\n    fillOpacity: 'fill-opacity',\n    fontFamily: 'font-family',\n    fontSize: 'font-size',\n    gradientTransform: 'gradientTransform',\n    gradientUnits: 'gradientUnits',\n    markerEnd: 'marker-end',\n    markerMid: 'marker-mid',\n    markerStart: 'marker-start',\n    patternContentUnits: 'patternContentUnits',\n    patternUnits: 'patternUnits',\n    preserveAspectRatio: 'preserveAspectRatio',\n    spreadMethod: 'spreadMethod',\n    stopColor: 'stop-color',\n    stopOpacity: 'stop-opacity',\n    strokeDasharray: 'stroke-dasharray',\n    strokeLinecap: 'stroke-linecap',\n    strokeOpacity: 'stroke-opacity',\n    strokeWidth: 'stroke-width',\n    textAnchor: 'text-anchor',\n    viewBox: 'viewBox'\n  }\n};\n\nmodule.exports = SVGDOMPropertyConfig;\n\n},{\"./DOMProperty\":11}],90:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule SelectEventPlugin\n */\n\n\"use strict\";\n\nvar EventConstants = _dereq_(\"./EventConstants\");\nvar EventPropagators = _dereq_(\"./EventPropagators\");\nvar ReactInputSelection = _dereq_(\"./ReactInputSelection\");\nvar SyntheticEvent = _dereq_(\"./SyntheticEvent\");\n\nvar getActiveElement = _dereq_(\"./getActiveElement\");\nvar isTextInputElement = _dereq_(\"./isTextInputElement\");\nvar keyOf = _dereq_(\"./keyOf\");\nvar shallowEqual = _dereq_(\"./shallowEqual\");\n\nvar topLevelTypes = EventConstants.topLevelTypes;\n\nvar eventTypes = {\n  select: {\n    phasedRegistrationNames: {\n      bubbled: keyOf({onSelect: null}),\n      captured: keyOf({onSelectCapture: null})\n    },\n    dependencies: [\n      topLevelTypes.topBlur,\n      topLevelTypes.topContextMenu,\n      topLevelTypes.topFocus,\n      topLevelTypes.topKeyDown,\n      topLevelTypes.topMouseDown,\n      topLevelTypes.topMouseUp,\n      topLevelTypes.topSelectionChange\n    ]\n  }\n};\n\nvar activeElement = null;\nvar activeElementID = null;\nvar lastSelection = null;\nvar mouseDown = false;\n\n/**\n * Get an object which is a unique representation of the current selection.\n *\n * The return value will not be consistent across nodes or browsers, but\n * two identical selections on the same node will return identical objects.\n *\n * @param {DOMElement} node\n * @param {object}\n */\nfunction getSelection(node) {\n  if ('selectionStart' in node &&\n      ReactInputSelection.hasSelectionCapabilities(node)) {\n    return {\n      start: node.selectionStart,\n      end: node.selectionEnd\n    };\n  } else if (document.selection) {\n    var range = document.selection.createRange();\n    return {\n      parentElement: range.parentElement(),\n      text: range.text,\n      top: range.boundingTop,\n      left: range.boundingLeft\n    };\n  } else {\n    var selection = window.getSelection();\n    return {\n      anchorNode: selection.anchorNode,\n      anchorOffset: selection.anchorOffset,\n      focusNode: selection.focusNode,\n      focusOffset: selection.focusOffset\n    };\n  }\n}\n\n/**\n * Poll selection to see whether it's changed.\n *\n * @param {object} nativeEvent\n * @return {?SyntheticEvent}\n */\nfunction constructSelectEvent(nativeEvent) {\n  // Ensure we have the right element, and that the user is not dragging a\n  // selection (this matches native `select` event behavior). In HTML5, select\n  // fires only on input and textarea thus if there's no focused element we\n  // won't dispatch.\n  if (mouseDown ||\n      activeElement == null ||\n      activeElement != getActiveElement()) {\n    return;\n  }\n\n  // Only fire when selection has actually changed.\n  var currentSelection = getSelection(activeElement);\n  if (!lastSelection || !shallowEqual(lastSelection, currentSelection)) {\n    lastSelection = currentSelection;\n\n    var syntheticEvent = SyntheticEvent.getPooled(\n      eventTypes.select,\n      activeElementID,\n      nativeEvent\n    );\n\n    syntheticEvent.type = 'select';\n    syntheticEvent.target = activeElement;\n\n    EventPropagators.accumulateTwoPhaseDispatches(syntheticEvent);\n\n    return syntheticEvent;\n  }\n}\n\n/**\n * This plugin creates an `onSelect` event that normalizes select events\n * across form elements.\n *\n * Supported elements are:\n * - input (see `isTextInputElement`)\n * - textarea\n * - contentEditable\n *\n * This differs from native browser implementations in the following ways:\n * - Fires on contentEditable fields as well as inputs.\n * - Fires for collapsed selection.\n * - Fires after user input.\n */\nvar SelectEventPlugin = {\n\n  eventTypes: eventTypes,\n\n  /**\n   * @param {string} topLevelType Record from `EventConstants`.\n   * @param {DOMEventTarget} topLevelTarget The listening component root node.\n   * @param {string} topLevelTargetID ID of `topLevelTarget`.\n   * @param {object} nativeEvent Native browser event.\n   * @return {*} An accumulation of synthetic events.\n   * @see {EventPluginHub.extractEvents}\n   */\n  extractEvents: function(\n      topLevelType,\n      topLevelTarget,\n      topLevelTargetID,\n      nativeEvent) {\n\n    switch (topLevelType) {\n      // Track the input node that has focus.\n      case topLevelTypes.topFocus:\n        if (isTextInputElement(topLevelTarget) ||\n            topLevelTarget.contentEditable === 'true') {\n          activeElement = topLevelTarget;\n          activeElementID = topLevelTargetID;\n          lastSelection = null;\n        }\n        break;\n      case topLevelTypes.topBlur:\n        activeElement = null;\n        activeElementID = null;\n        lastSelection = null;\n        break;\n\n      // Don't fire the event while the user is dragging. This matches the\n      // semantics of the native select event.\n      case topLevelTypes.topMouseDown:\n        mouseDown = true;\n        break;\n      case topLevelTypes.topContextMenu:\n      case topLevelTypes.topMouseUp:\n        mouseDown = false;\n        return constructSelectEvent(nativeEvent);\n\n      // Chrome and IE fire non-standard event when selection is changed (and\n      // sometimes when it hasn't).\n      // Firefox doesn't support selectionchange, so check selection status\n      // after each key entry. The selection changes after keydown and before\n      // keyup, but we check on keydown as well in the case of holding down a\n      // key, when multiple keydown events are fired but only one keyup is.\n      case topLevelTypes.topSelectionChange:\n      case topLevelTypes.topKeyDown:\n      case topLevelTypes.topKeyUp:\n        return constructSelectEvent(nativeEvent);\n    }\n  }\n};\n\nmodule.exports = SelectEventPlugin;\n\n},{\"./EventConstants\":16,\"./EventPropagators\":21,\"./ReactInputSelection\":63,\"./SyntheticEvent\":96,\"./getActiveElement\":122,\"./isTextInputElement\":137,\"./keyOf\":141,\"./shallowEqual\":153}],91:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule ServerReactRootIndex\n * @typechecks\n */\n\n\"use strict\";\n\n/**\n * Size of the reactRoot ID space. We generate random numbers for React root\n * IDs and if there's a collision the events and DOM update system will\n * get confused. In the future we need a way to generate GUIDs but for\n * now this will work on a smaller scale.\n */\nvar GLOBAL_MOUNT_POINT_MAX = Math.pow(2, 53);\n\nvar ServerReactRootIndex = {\n  createReactRootIndex: function() {\n    return Math.ceil(Math.random() * GLOBAL_MOUNT_POINT_MAX);\n  }\n};\n\nmodule.exports = ServerReactRootIndex;\n\n},{}],92:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule SimpleEventPlugin\n */\n\n\"use strict\";\n\nvar EventConstants = _dereq_(\"./EventConstants\");\nvar EventPluginUtils = _dereq_(\"./EventPluginUtils\");\nvar EventPropagators = _dereq_(\"./EventPropagators\");\nvar SyntheticClipboardEvent = _dereq_(\"./SyntheticClipboardEvent\");\nvar SyntheticEvent = _dereq_(\"./SyntheticEvent\");\nvar SyntheticFocusEvent = _dereq_(\"./SyntheticFocusEvent\");\nvar SyntheticKeyboardEvent = _dereq_(\"./SyntheticKeyboardEvent\");\nvar SyntheticMouseEvent = _dereq_(\"./SyntheticMouseEvent\");\nvar SyntheticDragEvent = _dereq_(\"./SyntheticDragEvent\");\nvar SyntheticTouchEvent = _dereq_(\"./SyntheticTouchEvent\");\nvar SyntheticUIEvent = _dereq_(\"./SyntheticUIEvent\");\nvar SyntheticWheelEvent = _dereq_(\"./SyntheticWheelEvent\");\n\nvar invariant = _dereq_(\"./invariant\");\nvar keyOf = _dereq_(\"./keyOf\");\n\nvar topLevelTypes = EventConstants.topLevelTypes;\n\nvar eventTypes = {\n  blur: {\n    phasedRegistrationNames: {\n      bubbled: keyOf({onBlur: true}),\n      captured: keyOf({onBlurCapture: true})\n    }\n  },\n  click: {\n    phasedRegistrationNames: {\n      bubbled: keyOf({onClick: true}),\n      captured: keyOf({onClickCapture: true})\n    }\n  },\n  contextMenu: {\n    phasedRegistrationNames: {\n      bubbled: keyOf({onContextMenu: true}),\n      captured: keyOf({onContextMenuCapture: true})\n    }\n  },\n  copy: {\n    phasedRegistrationNames: {\n      bubbled: keyOf({onCopy: true}),\n      captured: keyOf({onCopyCapture: true})\n    }\n  },\n  cut: {\n    phasedRegistrationNames: {\n      bubbled: keyOf({onCut: true}),\n      captured: keyOf({onCutCapture: true})\n    }\n  },\n  doubleClick: {\n    phasedRegistrationNames: {\n      bubbled: keyOf({onDoubleClick: true}),\n      captured: keyOf({onDoubleClickCapture: true})\n    }\n  },\n  drag: {\n    phasedRegistrationNames: {\n      bubbled: keyOf({onDrag: true}),\n      captured: keyOf({onDragCapture: true})\n    }\n  },\n  dragEnd: {\n    phasedRegistrationNames: {\n      bubbled: keyOf({onDragEnd: true}),\n      captured: keyOf({onDragEndCapture: true})\n    }\n  },\n  dragEnter: {\n    phasedRegistrationNames: {\n      bubbled: keyOf({onDragEnter: true}),\n      captured: keyOf({onDragEnterCapture: true})\n    }\n  },\n  dragExit: {\n    phasedRegistrationNames: {\n      bubbled: keyOf({onDragExit: true}),\n      captured: keyOf({onDragExitCapture: true})\n    }\n  },\n  dragLeave: {\n    phasedRegistrationNames: {\n      bubbled: keyOf({onDragLeave: true}),\n      captured: keyOf({onDragLeaveCapture: true})\n    }\n  },\n  dragOver: {\n    phasedRegistrationNames: {\n      bubbled: keyOf({onDragOver: true}),\n      captured: keyOf({onDragOverCapture: true})\n    }\n  },\n  dragStart: {\n    phasedRegistrationNames: {\n      bubbled: keyOf({onDragStart: true}),\n      captured: keyOf({onDragStartCapture: true})\n    }\n  },\n  drop: {\n    phasedRegistrationNames: {\n      bubbled: keyOf({onDrop: true}),\n      captured: keyOf({onDropCapture: true})\n    }\n  },\n  focus: {\n    phasedRegistrationNames: {\n      bubbled: keyOf({onFocus: true}),\n      captured: keyOf({onFocusCapture: true})\n    }\n  },\n  input: {\n    phasedRegistrationNames: {\n      bubbled: keyOf({onInput: true}),\n      captured: keyOf({onInputCapture: true})\n    }\n  },\n  keyDown: {\n    phasedRegistrationNames: {\n      bubbled: keyOf({onKeyDown: true}),\n      captured: keyOf({onKeyDownCapture: true})\n    }\n  },\n  keyPress: {\n    phasedRegistrationNames: {\n      bubbled: keyOf({onKeyPress: true}),\n      captured: keyOf({onKeyPressCapture: true})\n    }\n  },\n  keyUp: {\n    phasedRegistrationNames: {\n      bubbled: keyOf({onKeyUp: true}),\n      captured: keyOf({onKeyUpCapture: true})\n    }\n  },\n  load: {\n    phasedRegistrationNames: {\n      bubbled: keyOf({onLoad: true}),\n      captured: keyOf({onLoadCapture: true})\n    }\n  },\n  error: {\n    phasedRegistrationNames: {\n      bubbled: keyOf({onError: true}),\n      captured: keyOf({onErrorCapture: true})\n    }\n  },\n  // Note: We do not allow listening to mouseOver events. Instead, use the\n  // onMouseEnter/onMouseLeave created by `EnterLeaveEventPlugin`.\n  mouseDown: {\n    phasedRegistrationNames: {\n      bubbled: keyOf({onMouseDown: true}),\n      captured: keyOf({onMouseDownCapture: true})\n    }\n  },\n  mouseMove: {\n    phasedRegistrationNames: {\n      bubbled: keyOf({onMouseMove: true}),\n      captured: keyOf({onMouseMoveCapture: true})\n    }\n  },\n  mouseOut: {\n    phasedRegistrationNames: {\n      bubbled: keyOf({onMouseOut: true}),\n      captured: keyOf({onMouseOutCapture: true})\n    }\n  },\n  mouseOver: {\n    phasedRegistrationNames: {\n      bubbled: keyOf({onMouseOver: true}),\n      captured: keyOf({onMouseOverCapture: true})\n    }\n  },\n  mouseUp: {\n    phasedRegistrationNames: {\n      bubbled: keyOf({onMouseUp: true}),\n      captured: keyOf({onMouseUpCapture: true})\n    }\n  },\n  paste: {\n    phasedRegistrationNames: {\n      bubbled: keyOf({onPaste: true}),\n      captured: keyOf({onPasteCapture: true})\n    }\n  },\n  reset: {\n    phasedRegistrationNames: {\n      bubbled: keyOf({onReset: true}),\n      captured: keyOf({onResetCapture: true})\n    }\n  },\n  scroll: {\n    phasedRegistrationNames: {\n      bubbled: keyOf({onScroll: true}),\n      captured: keyOf({onScrollCapture: true})\n    }\n  },\n  submit: {\n    phasedRegistrationNames: {\n      bubbled: keyOf({onSubmit: true}),\n      captured: keyOf({onSubmitCapture: true})\n    }\n  },\n  touchCancel: {\n    phasedRegistrationNames: {\n      bubbled: keyOf({onTouchCancel: true}),\n      captured: keyOf({onTouchCancelCapture: true})\n    }\n  },\n  touchEnd: {\n    phasedRegistrationNames: {\n      bubbled: keyOf({onTouchEnd: true}),\n      captured: keyOf({onTouchEndCapture: true})\n    }\n  },\n  touchMove: {\n    phasedRegistrationNames: {\n      bubbled: keyOf({onTouchMove: true}),\n      captured: keyOf({onTouchMoveCapture: true})\n    }\n  },\n  touchStart: {\n    phasedRegistrationNames: {\n      bubbled: keyOf({onTouchStart: true}),\n      captured: keyOf({onTouchStartCapture: true})\n    }\n  },\n  wheel: {\n    phasedRegistrationNames: {\n      bubbled: keyOf({onWheel: true}),\n      captured: keyOf({onWheelCapture: true})\n    }\n  }\n};\n\nvar topLevelEventsToDispatchConfig = {\n  topBlur:        eventTypes.blur,\n  topClick:       eventTypes.click,\n  topContextMenu: eventTypes.contextMenu,\n  topCopy:        eventTypes.copy,\n  topCut:         eventTypes.cut,\n  topDoubleClick: eventTypes.doubleClick,\n  topDrag:        eventTypes.drag,\n  topDragEnd:     eventTypes.dragEnd,\n  topDragEnter:   eventTypes.dragEnter,\n  topDragExit:    eventTypes.dragExit,\n  topDragLeave:   eventTypes.dragLeave,\n  topDragOver:    eventTypes.dragOver,\n  topDragStart:   eventTypes.dragStart,\n  topDrop:        eventTypes.drop,\n  topError:       eventTypes.error,\n  topFocus:       eventTypes.focus,\n  topInput:       eventTypes.input,\n  topKeyDown:     eventTypes.keyDown,\n  topKeyPress:    eventTypes.keyPress,\n  topKeyUp:       eventTypes.keyUp,\n  topLoad:        eventTypes.load,\n  topMouseDown:   eventTypes.mouseDown,\n  topMouseMove:   eventTypes.mouseMove,\n  topMouseOut:    eventTypes.mouseOut,\n  topMouseOver:   eventTypes.mouseOver,\n  topMouseUp:     eventTypes.mouseUp,\n  topPaste:       eventTypes.paste,\n  topReset:       eventTypes.reset,\n  topScroll:      eventTypes.scroll,\n  topSubmit:      eventTypes.submit,\n  topTouchCancel: eventTypes.touchCancel,\n  topTouchEnd:    eventTypes.touchEnd,\n  topTouchMove:   eventTypes.touchMove,\n  topTouchStart:  eventTypes.touchStart,\n  topWheel:       eventTypes.wheel\n};\n\nfor (var topLevelType in topLevelEventsToDispatchConfig) {\n  topLevelEventsToDispatchConfig[topLevelType].dependencies = [topLevelType];\n}\n\nvar SimpleEventPlugin = {\n\n  eventTypes: eventTypes,\n\n  /**\n   * Same as the default implementation, except cancels the event when return\n   * value is false.\n   *\n   * @param {object} Event to be dispatched.\n   * @param {function} Application-level callback.\n   * @param {string} domID DOM ID to pass to the callback.\n   */\n  executeDispatch: function(event, listener, domID) {\n    var returnValue = EventPluginUtils.executeDispatch(event, listener, domID);\n    if (returnValue === false) {\n      event.stopPropagation();\n      event.preventDefault();\n    }\n  },\n\n  /**\n   * @param {string} topLevelType Record from `EventConstants`.\n   * @param {DOMEventTarget} topLevelTarget The listening component root node.\n   * @param {string} topLevelTargetID ID of `topLevelTarget`.\n   * @param {object} nativeEvent Native browser event.\n   * @return {*} An accumulation of synthetic events.\n   * @see {EventPluginHub.extractEvents}\n   */\n  extractEvents: function(\n      topLevelType,\n      topLevelTarget,\n      topLevelTargetID,\n      nativeEvent) {\n    var dispatchConfig = topLevelEventsToDispatchConfig[topLevelType];\n    if (!dispatchConfig) {\n      return null;\n    }\n    var EventConstructor;\n    switch (topLevelType) {\n      case topLevelTypes.topInput:\n      case topLevelTypes.topLoad:\n      case topLevelTypes.topError:\n      case topLevelTypes.topReset:\n      case topLevelTypes.topSubmit:\n        // HTML Events\n        // @see http://www.w3.org/TR/html5/index.html#events-0\n        EventConstructor = SyntheticEvent;\n        break;\n      case topLevelTypes.topKeyPress:\n        // FireFox creates a keypress event for function keys too. This removes\n        // the unwanted keypress events.\n        if (nativeEvent.charCode === 0) {\n          return null;\n        }\n        /* falls through */\n      case topLevelTypes.topKeyDown:\n      case topLevelTypes.topKeyUp:\n        EventConstructor = SyntheticKeyboardEvent;\n        break;\n      case topLevelTypes.topBlur:\n      case topLevelTypes.topFocus:\n        EventConstructor = SyntheticFocusEvent;\n        break;\n      case topLevelTypes.topClick:\n        // Firefox creates a click event on right mouse clicks. This removes the\n        // unwanted click events.\n        if (nativeEvent.button === 2) {\n          return null;\n        }\n        /* falls through */\n      case topLevelTypes.topContextMenu:\n      case topLevelTypes.topDoubleClick:\n      case topLevelTypes.topMouseDown:\n      case topLevelTypes.topMouseMove:\n      case topLevelTypes.topMouseOut:\n      case topLevelTypes.topMouseOver:\n      case topLevelTypes.topMouseUp:\n        EventConstructor = SyntheticMouseEvent;\n        break;\n      case topLevelTypes.topDrag:\n      case topLevelTypes.topDragEnd:\n      case topLevelTypes.topDragEnter:\n      case topLevelTypes.topDragExit:\n      case topLevelTypes.topDragLeave:\n      case topLevelTypes.topDragOver:\n      case topLevelTypes.topDragStart:\n      case topLevelTypes.topDrop:\n        EventConstructor = SyntheticDragEvent;\n        break;\n      case topLevelTypes.topTouchCancel:\n      case topLevelTypes.topTouchEnd:\n      case topLevelTypes.topTouchMove:\n      case topLevelTypes.topTouchStart:\n        EventConstructor = SyntheticTouchEvent;\n        break;\n      case topLevelTypes.topScroll:\n        EventConstructor = SyntheticUIEvent;\n        break;\n      case topLevelTypes.topWheel:\n        EventConstructor = SyntheticWheelEvent;\n        break;\n      case topLevelTypes.topCopy:\n      case topLevelTypes.topCut:\n      case topLevelTypes.topPaste:\n        EventConstructor = SyntheticClipboardEvent;\n        break;\n    }\n    (\"production\" !== \"development\" ? invariant(\n      EventConstructor,\n      'SimpleEventPlugin: Unhandled event type, `%s`.',\n      topLevelType\n    ) : invariant(EventConstructor));\n    var event = EventConstructor.getPooled(\n      dispatchConfig,\n      topLevelTargetID,\n      nativeEvent\n    );\n    EventPropagators.accumulateTwoPhaseDispatches(event);\n    return event;\n  }\n\n};\n\nmodule.exports = SimpleEventPlugin;\n\n},{\"./EventConstants\":16,\"./EventPluginUtils\":20,\"./EventPropagators\":21,\"./SyntheticClipboardEvent\":93,\"./SyntheticDragEvent\":95,\"./SyntheticEvent\":96,\"./SyntheticFocusEvent\":97,\"./SyntheticKeyboardEvent\":99,\"./SyntheticMouseEvent\":100,\"./SyntheticTouchEvent\":101,\"./SyntheticUIEvent\":102,\"./SyntheticWheelEvent\":103,\"./invariant\":134,\"./keyOf\":141}],93:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule SyntheticClipboardEvent\n * @typechecks static-only\n */\n\n\"use strict\";\n\nvar SyntheticEvent = _dereq_(\"./SyntheticEvent\");\n\n/**\n * @interface Event\n * @see http://www.w3.org/TR/clipboard-apis/\n */\nvar ClipboardEventInterface = {\n  clipboardData: function(event) {\n    return (\n      'clipboardData' in event ?\n        event.clipboardData :\n        window.clipboardData\n    );\n  }\n};\n\n/**\n * @param {object} dispatchConfig Configuration used to dispatch this event.\n * @param {string} dispatchMarker Marker identifying the event target.\n * @param {object} nativeEvent Native browser event.\n * @extends {SyntheticUIEvent}\n */\nfunction SyntheticClipboardEvent(dispatchConfig, dispatchMarker, nativeEvent) {\n  SyntheticEvent.call(this, dispatchConfig, dispatchMarker, nativeEvent);\n}\n\nSyntheticEvent.augmentClass(SyntheticClipboardEvent, ClipboardEventInterface);\n\nmodule.exports = SyntheticClipboardEvent;\n\n\n},{\"./SyntheticEvent\":96}],94:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule SyntheticCompositionEvent\n * @typechecks static-only\n */\n\n\"use strict\";\n\nvar SyntheticEvent = _dereq_(\"./SyntheticEvent\");\n\n/**\n * @interface Event\n * @see http://www.w3.org/TR/DOM-Level-3-Events/#events-compositionevents\n */\nvar CompositionEventInterface = {\n  data: null\n};\n\n/**\n * @param {object} dispatchConfig Configuration used to dispatch this event.\n * @param {string} dispatchMarker Marker identifying the event target.\n * @param {object} nativeEvent Native browser event.\n * @extends {SyntheticUIEvent}\n */\nfunction SyntheticCompositionEvent(\n  dispatchConfig,\n  dispatchMarker,\n  nativeEvent) {\n  SyntheticEvent.call(this, dispatchConfig, dispatchMarker, nativeEvent);\n}\n\nSyntheticEvent.augmentClass(\n  SyntheticCompositionEvent,\n  CompositionEventInterface\n);\n\nmodule.exports = SyntheticCompositionEvent;\n\n\n},{\"./SyntheticEvent\":96}],95:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule SyntheticDragEvent\n * @typechecks static-only\n */\n\n\"use strict\";\n\nvar SyntheticMouseEvent = _dereq_(\"./SyntheticMouseEvent\");\n\n/**\n * @interface DragEvent\n * @see http://www.w3.org/TR/DOM-Level-3-Events/\n */\nvar DragEventInterface = {\n  dataTransfer: null\n};\n\n/**\n * @param {object} dispatchConfig Configuration used to dispatch this event.\n * @param {string} dispatchMarker Marker identifying the event target.\n * @param {object} nativeEvent Native browser event.\n * @extends {SyntheticUIEvent}\n */\nfunction SyntheticDragEvent(dispatchConfig, dispatchMarker, nativeEvent) {\n  SyntheticMouseEvent.call(this, dispatchConfig, dispatchMarker, nativeEvent);\n}\n\nSyntheticMouseEvent.augmentClass(SyntheticDragEvent, DragEventInterface);\n\nmodule.exports = SyntheticDragEvent;\n\n},{\"./SyntheticMouseEvent\":100}],96:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule SyntheticEvent\n * @typechecks static-only\n */\n\n\"use strict\";\n\nvar PooledClass = _dereq_(\"./PooledClass\");\n\nvar emptyFunction = _dereq_(\"./emptyFunction\");\nvar getEventTarget = _dereq_(\"./getEventTarget\");\nvar merge = _dereq_(\"./merge\");\nvar mergeInto = _dereq_(\"./mergeInto\");\n\n/**\n * @interface Event\n * @see http://www.w3.org/TR/DOM-Level-3-Events/\n */\nvar EventInterface = {\n  type: null,\n  target: getEventTarget,\n  // currentTarget is set when dispatching; no use in copying it here\n  currentTarget: emptyFunction.thatReturnsNull,\n  eventPhase: null,\n  bubbles: null,\n  cancelable: null,\n  timeStamp: function(event) {\n    return event.timeStamp || Date.now();\n  },\n  defaultPrevented: null,\n  isTrusted: null\n};\n\n/**\n * Synthetic events are dispatched by event plugins, typically in response to a\n * top-level event delegation handler.\n *\n * These systems should generally use pooling to reduce the frequency of garbage\n * collection. The system should check `isPersistent` to determine whether the\n * event should be released into the pool after being dispatched. Users that\n * need a persisted event should invoke `persist`.\n *\n * Synthetic events (and subclasses) implement the DOM Level 3 Events API by\n * normalizing browser quirks. Subclasses do not necessarily have to implement a\n * DOM interface; custom application-specific events can also subclass this.\n *\n * @param {object} dispatchConfig Configuration used to dispatch this event.\n * @param {string} dispatchMarker Marker identifying the event target.\n * @param {object} nativeEvent Native browser event.\n */\nfunction SyntheticEvent(dispatchConfig, dispatchMarker, nativeEvent) {\n  this.dispatchConfig = dispatchConfig;\n  this.dispatchMarker = dispatchMarker;\n  this.nativeEvent = nativeEvent;\n\n  var Interface = this.constructor.Interface;\n  for (var propName in Interface) {\n    if (!Interface.hasOwnProperty(propName)) {\n      continue;\n    }\n    var normalize = Interface[propName];\n    if (normalize) {\n      this[propName] = normalize(nativeEvent);\n    } else {\n      this[propName] = nativeEvent[propName];\n    }\n  }\n\n  var defaultPrevented = nativeEvent.defaultPrevented != null ?\n    nativeEvent.defaultPrevented :\n    nativeEvent.returnValue === false;\n  if (defaultPrevented) {\n    this.isDefaultPrevented = emptyFunction.thatReturnsTrue;\n  } else {\n    this.isDefaultPrevented = emptyFunction.thatReturnsFalse;\n  }\n  this.isPropagationStopped = emptyFunction.thatReturnsFalse;\n}\n\nmergeInto(SyntheticEvent.prototype, {\n\n  preventDefault: function() {\n    this.defaultPrevented = true;\n    var event = this.nativeEvent;\n    event.preventDefault ? event.preventDefault() : event.returnValue = false;\n    this.isDefaultPrevented = emptyFunction.thatReturnsTrue;\n  },\n\n  stopPropagation: function() {\n    var event = this.nativeEvent;\n    event.stopPropagation ? event.stopPropagation() : event.cancelBubble = true;\n    this.isPropagationStopped = emptyFunction.thatReturnsTrue;\n  },\n\n  /**\n   * We release all dispatched `SyntheticEvent`s after each event loop, adding\n   * them back into the pool. This allows a way to hold onto a reference that\n   * won't be added back into the pool.\n   */\n  persist: function() {\n    this.isPersistent = emptyFunction.thatReturnsTrue;\n  },\n\n  /**\n   * Checks if this event should be released back into the pool.\n   *\n   * @return {boolean} True if this should not be released, false otherwise.\n   */\n  isPersistent: emptyFunction.thatReturnsFalse,\n\n  /**\n   * `PooledClass` looks for `destructor` on each instance it releases.\n   */\n  destructor: function() {\n    var Interface = this.constructor.Interface;\n    for (var propName in Interface) {\n      this[propName] = null;\n    }\n    this.dispatchConfig = null;\n    this.dispatchMarker = null;\n    this.nativeEvent = null;\n  }\n\n});\n\nSyntheticEvent.Interface = EventInterface;\n\n/**\n * Helper to reduce boilerplate when creating subclasses.\n *\n * @param {function} Class\n * @param {?object} Interface\n */\nSyntheticEvent.augmentClass = function(Class, Interface) {\n  var Super = this;\n\n  var prototype = Object.create(Super.prototype);\n  mergeInto(prototype, Class.prototype);\n  Class.prototype = prototype;\n  Class.prototype.constructor = Class;\n\n  Class.Interface = merge(Super.Interface, Interface);\n  Class.augmentClass = Super.augmentClass;\n\n  PooledClass.addPoolingTo(Class, PooledClass.threeArgumentPooler);\n};\n\nPooledClass.addPoolingTo(SyntheticEvent, PooledClass.threeArgumentPooler);\n\nmodule.exports = SyntheticEvent;\n\n},{\"./PooledClass\":28,\"./emptyFunction\":116,\"./getEventTarget\":125,\"./merge\":144,\"./mergeInto\":146}],97:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule SyntheticFocusEvent\n * @typechecks static-only\n */\n\n\"use strict\";\n\nvar SyntheticUIEvent = _dereq_(\"./SyntheticUIEvent\");\n\n/**\n * @interface FocusEvent\n * @see http://www.w3.org/TR/DOM-Level-3-Events/\n */\nvar FocusEventInterface = {\n  relatedTarget: null\n};\n\n/**\n * @param {object} dispatchConfig Configuration used to dispatch this event.\n * @param {string} dispatchMarker Marker identifying the event target.\n * @param {object} nativeEvent Native browser event.\n * @extends {SyntheticUIEvent}\n */\nfunction SyntheticFocusEvent(dispatchConfig, dispatchMarker, nativeEvent) {\n  SyntheticUIEvent.call(this, dispatchConfig, dispatchMarker, nativeEvent);\n}\n\nSyntheticUIEvent.augmentClass(SyntheticFocusEvent, FocusEventInterface);\n\nmodule.exports = SyntheticFocusEvent;\n\n},{\"./SyntheticUIEvent\":102}],98:[function(_dereq_,module,exports){\n/**\n * Copyright 2013 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule SyntheticInputEvent\n * @typechecks static-only\n */\n\n\"use strict\";\n\nvar SyntheticEvent = _dereq_(\"./SyntheticEvent\");\n\n/**\n * @interface Event\n * @see http://www.w3.org/TR/2013/WD-DOM-Level-3-Events-20131105\n *      /#events-inputevents\n */\nvar InputEventInterface = {\n  data: null\n};\n\n/**\n * @param {object} dispatchConfig Configuration used to dispatch this event.\n * @param {string} dispatchMarker Marker identifying the event target.\n * @param {object} nativeEvent Native browser event.\n * @extends {SyntheticUIEvent}\n */\nfunction SyntheticInputEvent(\n  dispatchConfig,\n  dispatchMarker,\n  nativeEvent) {\n  SyntheticEvent.call(this, dispatchConfig, dispatchMarker, nativeEvent);\n}\n\nSyntheticEvent.augmentClass(\n  SyntheticInputEvent,\n  InputEventInterface\n);\n\nmodule.exports = SyntheticInputEvent;\n\n\n},{\"./SyntheticEvent\":96}],99:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule SyntheticKeyboardEvent\n * @typechecks static-only\n */\n\n\"use strict\";\n\nvar SyntheticUIEvent = _dereq_(\"./SyntheticUIEvent\");\n\nvar getEventKey = _dereq_(\"./getEventKey\");\nvar getEventModifierState = _dereq_(\"./getEventModifierState\");\n\n/**\n * @interface KeyboardEvent\n * @see http://www.w3.org/TR/DOM-Level-3-Events/\n */\nvar KeyboardEventInterface = {\n  key: getEventKey,\n  location: null,\n  ctrlKey: null,\n  shiftKey: null,\n  altKey: null,\n  metaKey: null,\n  repeat: null,\n  locale: null,\n  getModifierState: getEventModifierState,\n  // Legacy Interface\n  charCode: function(event) {\n    // `charCode` is the result of a KeyPress event and represents the value of\n    // the actual printable character.\n\n    // KeyPress is deprecated but its replacement is not yet final and not\n    // implemented in any major browser.\n    if (event.type === 'keypress') {\n      // IE8 does not implement \"charCode\", but \"keyCode\" has the correct value.\n      return 'charCode' in event ? event.charCode : event.keyCode;\n    }\n    return 0;\n  },\n  keyCode: function(event) {\n    // `keyCode` is the result of a KeyDown/Up event and represents the value of\n    // physical keyboard key.\n\n    // The actual meaning of the value depends on the users' keyboard layout\n    // which cannot be detected. Assuming that it is a US keyboard layout\n    // provides a surprisingly accurate mapping for US and European users.\n    // Due to this, it is left to the user to implement at this time.\n    if (event.type === 'keydown' || event.type === 'keyup') {\n      return event.keyCode;\n    }\n    return 0;\n  },\n  which: function(event) {\n    // `which` is an alias for either `keyCode` or `charCode` depending on the\n    // type of the event. There is no need to determine the type of the event\n    // as `keyCode` and `charCode` are either aliased or default to zero.\n    return event.keyCode || event.charCode;\n  }\n};\n\n/**\n * @param {object} dispatchConfig Configuration used to dispatch this event.\n * @param {string} dispatchMarker Marker identifying the event target.\n * @param {object} nativeEvent Native browser event.\n * @extends {SyntheticUIEvent}\n */\nfunction SyntheticKeyboardEvent(dispatchConfig, dispatchMarker, nativeEvent) {\n  SyntheticUIEvent.call(this, dispatchConfig, dispatchMarker, nativeEvent);\n}\n\nSyntheticUIEvent.augmentClass(SyntheticKeyboardEvent, KeyboardEventInterface);\n\nmodule.exports = SyntheticKeyboardEvent;\n\n},{\"./SyntheticUIEvent\":102,\"./getEventKey\":123,\"./getEventModifierState\":124}],100:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule SyntheticMouseEvent\n * @typechecks static-only\n */\n\n\"use strict\";\n\nvar SyntheticUIEvent = _dereq_(\"./SyntheticUIEvent\");\nvar ViewportMetrics = _dereq_(\"./ViewportMetrics\");\n\nvar getEventModifierState = _dereq_(\"./getEventModifierState\");\n\n/**\n * @interface MouseEvent\n * @see http://www.w3.org/TR/DOM-Level-3-Events/\n */\nvar MouseEventInterface = {\n  screenX: null,\n  screenY: null,\n  clientX: null,\n  clientY: null,\n  ctrlKey: null,\n  shiftKey: null,\n  altKey: null,\n  metaKey: null,\n  getModifierState: getEventModifierState,\n  button: function(event) {\n    // Webkit, Firefox, IE9+\n    // which:  1 2 3\n    // button: 0 1 2 (standard)\n    var button = event.button;\n    if ('which' in event) {\n      return button;\n    }\n    // IE<9\n    // which:  undefined\n    // button: 0 0 0\n    // button: 1 4 2 (onmouseup)\n    return button === 2 ? 2 : button === 4 ? 1 : 0;\n  },\n  buttons: null,\n  relatedTarget: function(event) {\n    return event.relatedTarget || (\n      event.fromElement === event.srcElement ?\n        event.toElement :\n        event.fromElement\n    );\n  },\n  // \"Proprietary\" Interface.\n  pageX: function(event) {\n    return 'pageX' in event ?\n      event.pageX :\n      event.clientX + ViewportMetrics.currentScrollLeft;\n  },\n  pageY: function(event) {\n    return 'pageY' in event ?\n      event.pageY :\n      event.clientY + ViewportMetrics.currentScrollTop;\n  }\n};\n\n/**\n * @param {object} dispatchConfig Configuration used to dispatch this event.\n * @param {string} dispatchMarker Marker identifying the event target.\n * @param {object} nativeEvent Native browser event.\n * @extends {SyntheticUIEvent}\n */\nfunction SyntheticMouseEvent(dispatchConfig, dispatchMarker, nativeEvent) {\n  SyntheticUIEvent.call(this, dispatchConfig, dispatchMarker, nativeEvent);\n}\n\nSyntheticUIEvent.augmentClass(SyntheticMouseEvent, MouseEventInterface);\n\nmodule.exports = SyntheticMouseEvent;\n\n},{\"./SyntheticUIEvent\":102,\"./ViewportMetrics\":105,\"./getEventModifierState\":124}],101:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule SyntheticTouchEvent\n * @typechecks static-only\n */\n\n\"use strict\";\n\nvar SyntheticUIEvent = _dereq_(\"./SyntheticUIEvent\");\n\nvar getEventModifierState = _dereq_(\"./getEventModifierState\");\n\n/**\n * @interface TouchEvent\n * @see http://www.w3.org/TR/touch-events/\n */\nvar TouchEventInterface = {\n  touches: null,\n  targetTouches: null,\n  changedTouches: null,\n  altKey: null,\n  metaKey: null,\n  ctrlKey: null,\n  shiftKey: null,\n  getModifierState: getEventModifierState\n};\n\n/**\n * @param {object} dispatchConfig Configuration used to dispatch this event.\n * @param {string} dispatchMarker Marker identifying the event target.\n * @param {object} nativeEvent Native browser event.\n * @extends {SyntheticUIEvent}\n */\nfunction SyntheticTouchEvent(dispatchConfig, dispatchMarker, nativeEvent) {\n  SyntheticUIEvent.call(this, dispatchConfig, dispatchMarker, nativeEvent);\n}\n\nSyntheticUIEvent.augmentClass(SyntheticTouchEvent, TouchEventInterface);\n\nmodule.exports = SyntheticTouchEvent;\n\n},{\"./SyntheticUIEvent\":102,\"./getEventModifierState\":124}],102:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule SyntheticUIEvent\n * @typechecks static-only\n */\n\n\"use strict\";\n\nvar SyntheticEvent = _dereq_(\"./SyntheticEvent\");\n\nvar getEventTarget = _dereq_(\"./getEventTarget\");\n\n/**\n * @interface UIEvent\n * @see http://www.w3.org/TR/DOM-Level-3-Events/\n */\nvar UIEventInterface = {\n  view: function(event) {\n    if (event.view) {\n      return event.view;\n    }\n\n    var target = getEventTarget(event);\n    if (target != null && target.window === target) {\n      // target is a window object\n      return target;\n    }\n\n    var doc = target.ownerDocument;\n    // TODO: Figure out why `ownerDocument` is sometimes undefined in IE8.\n    if (doc) {\n      return doc.defaultView || doc.parentWindow;\n    } else {\n      return window;\n    }\n  },\n  detail: function(event) {\n    return event.detail || 0;\n  }\n};\n\n/**\n * @param {object} dispatchConfig Configuration used to dispatch this event.\n * @param {string} dispatchMarker Marker identifying the event target.\n * @param {object} nativeEvent Native browser event.\n * @extends {SyntheticEvent}\n */\nfunction SyntheticUIEvent(dispatchConfig, dispatchMarker, nativeEvent) {\n  SyntheticEvent.call(this, dispatchConfig, dispatchMarker, nativeEvent);\n}\n\nSyntheticEvent.augmentClass(SyntheticUIEvent, UIEventInterface);\n\nmodule.exports = SyntheticUIEvent;\n\n},{\"./SyntheticEvent\":96,\"./getEventTarget\":125}],103:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule SyntheticWheelEvent\n * @typechecks static-only\n */\n\n\"use strict\";\n\nvar SyntheticMouseEvent = _dereq_(\"./SyntheticMouseEvent\");\n\n/**\n * @interface WheelEvent\n * @see http://www.w3.org/TR/DOM-Level-3-Events/\n */\nvar WheelEventInterface = {\n  deltaX: function(event) {\n    return (\n      'deltaX' in event ? event.deltaX :\n      // Fallback to `wheelDeltaX` for Webkit and normalize (right is positive).\n      'wheelDeltaX' in event ? -event.wheelDeltaX : 0\n    );\n  },\n  deltaY: function(event) {\n    return (\n      'deltaY' in event ? event.deltaY :\n      // Fallback to `wheelDeltaY` for Webkit and normalize (down is positive).\n      'wheelDeltaY' in event ? -event.wheelDeltaY :\n      // Fallback to `wheelDelta` for IE<9 and normalize (down is positive).\n      'wheelDelta' in event ? -event.wheelDelta : 0\n    );\n  },\n  deltaZ: null,\n\n  // Browsers without \"deltaMode\" is reporting in raw wheel delta where one\n  // notch on the scroll is always +/- 120, roughly equivalent to pixels.\n  // A good approximation of DOM_DELTA_LINE (1) is 5% of viewport size or\n  // ~40 pixels, for DOM_DELTA_SCREEN (2) it is 87.5% of viewport size.\n  deltaMode: null\n};\n\n/**\n * @param {object} dispatchConfig Configuration used to dispatch this event.\n * @param {string} dispatchMarker Marker identifying the event target.\n * @param {object} nativeEvent Native browser event.\n * @extends {SyntheticMouseEvent}\n */\nfunction SyntheticWheelEvent(dispatchConfig, dispatchMarker, nativeEvent) {\n  SyntheticMouseEvent.call(this, dispatchConfig, dispatchMarker, nativeEvent);\n}\n\nSyntheticMouseEvent.augmentClass(SyntheticWheelEvent, WheelEventInterface);\n\nmodule.exports = SyntheticWheelEvent;\n\n},{\"./SyntheticMouseEvent\":100}],104:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule Transaction\n */\n\n\"use strict\";\n\nvar invariant = _dereq_(\"./invariant\");\n\n/**\n * `Transaction` creates a black box that is able to wrap any method such that\n * certain invariants are maintained before and after the method is invoked\n * (Even if an exception is thrown while invoking the wrapped method). Whoever\n * instantiates a transaction can provide enforcers of the invariants at\n * creation time. The `Transaction` class itself will supply one additional\n * automatic invariant for you - the invariant that any transaction instance\n * should not be run while it is already being run. You would typically create a\n * single instance of a `Transaction` for reuse multiple times, that potentially\n * is used to wrap several different methods. Wrappers are extremely simple -\n * they only require implementing two methods.\n *\n * <pre>\n *                       wrappers (injected at creation time)\n *                                      +        +\n *                                      |        |\n *                    +-----------------|--------|--------------+\n *                    |                 v        |              |\n *                    |      +---------------+   |              |\n *                    |   +--|    wrapper1   |---|----+         |\n *                    |   |  +---------------+   v    |         |\n *                    |   |          +-------------+  |         |\n *                    |   |     +----|   wrapper2  |--------+   |\n *                    |   |     |    +-------------+  |     |   |\n *                    |   |     |                     |     |   |\n *                    |   v     v                     v     v   | wrapper\n *                    | +---+ +---+   +---------+   +---+ +---+ | invariants\n * perform(anyMethod) | |   | |   |   |         |   |   | |   | | maintained\n * +----------------->|-|---|-|---|-->|anyMethod|---|---|-|---|-|-------->\n *                    | |   | |   |   |         |   |   | |   | |\n *                    | |   | |   |   |         |   |   | |   | |\n *                    | |   | |   |   |         |   |   | |   | |\n *                    | +---+ +---+   +---------+   +---+ +---+ |\n *                    |  initialize                    close    |\n *                    +-----------------------------------------+\n * </pre>\n *\n * Use cases:\n * - Preserving the input selection ranges before/after reconciliation.\n *   Restoring selection even in the event of an unexpected error.\n * - Deactivating events while rearranging the DOM, preventing blurs/focuses,\n *   while guaranteeing that afterwards, the event system is reactivated.\n * - Flushing a queue of collected DOM mutations to the main UI thread after a\n *   reconciliation takes place in a worker thread.\n * - Invoking any collected `componentDidUpdate` callbacks after rendering new\n *   content.\n * - (Future use case): Wrapping particular flushes of the `ReactWorker` queue\n *   to preserve the `scrollTop` (an automatic scroll aware DOM).\n * - (Future use case): Layout calculations before and after DOM upates.\n *\n * Transactional plugin API:\n * - A module that has an `initialize` method that returns any precomputation.\n * - and a `close` method that accepts the precomputation. `close` is invoked\n *   when the wrapped process is completed, or has failed.\n *\n * @param {Array<TransactionalWrapper>} transactionWrapper Wrapper modules\n * that implement `initialize` and `close`.\n * @return {Transaction} Single transaction for reuse in thread.\n *\n * @class Transaction\n */\nvar Mixin = {\n  /**\n   * Sets up this instance so that it is prepared for collecting metrics. Does\n   * so such that this setup method may be used on an instance that is already\n   * initialized, in a way that does not consume additional memory upon reuse.\n   * That can be useful if you decide to make your subclass of this mixin a\n   * \"PooledClass\".\n   */\n  reinitializeTransaction: function() {\n    this.transactionWrappers = this.getTransactionWrappers();\n    if (!this.wrapperInitData) {\n      this.wrapperInitData = [];\n    } else {\n      this.wrapperInitData.length = 0;\n    }\n    this._isInTransaction = false;\n  },\n\n  _isInTransaction: false,\n\n  /**\n   * @abstract\n   * @return {Array<TransactionWrapper>} Array of transaction wrappers.\n   */\n  getTransactionWrappers: null,\n\n  isInTransaction: function() {\n    return !!this._isInTransaction;\n  },\n\n  /**\n   * Executes the function within a safety window. Use this for the top level\n   * methods that result in large amounts of computation/mutations that would\n   * need to be safety checked.\n   *\n   * @param {function} method Member of scope to call.\n   * @param {Object} scope Scope to invoke from.\n   * @param {Object?=} args... Arguments to pass to the method (optional).\n   *                           Helps prevent need to bind in many cases.\n   * @return Return value from `method`.\n   */\n  perform: function(method, scope, a, b, c, d, e, f) {\n    (\"production\" !== \"development\" ? invariant(\n      !this.isInTransaction(),\n      'Transaction.perform(...): Cannot initialize a transaction when there ' +\n      'is already an outstanding transaction.'\n    ) : invariant(!this.isInTransaction()));\n    var errorThrown;\n    var ret;\n    try {\n      this._isInTransaction = true;\n      // Catching errors makes debugging more difficult, so we start with\n      // errorThrown set to true before setting it to false after calling\n      // close -- if it's still set to true in the finally block, it means\n      // one of these calls threw.\n      errorThrown = true;\n      this.initializeAll(0);\n      ret = method.call(scope, a, b, c, d, e, f);\n      errorThrown = false;\n    } finally {\n      try {\n        if (errorThrown) {\n          // If `method` throws, prefer to show that stack trace over any thrown\n          // by invoking `closeAll`.\n          try {\n            this.closeAll(0);\n          } catch (err) {\n          }\n        } else {\n          // Since `method` didn't throw, we don't want to silence the exception\n          // here.\n          this.closeAll(0);\n        }\n      } finally {\n        this._isInTransaction = false;\n      }\n    }\n    return ret;\n  },\n\n  initializeAll: function(startIndex) {\n    var transactionWrappers = this.transactionWrappers;\n    for (var i = startIndex; i < transactionWrappers.length; i++) {\n      var wrapper = transactionWrappers[i];\n      try {\n        // Catching errors makes debugging more difficult, so we start with the\n        // OBSERVED_ERROR state before overwriting it with the real return value\n        // of initialize -- if it's still set to OBSERVED_ERROR in the finally\n        // block, it means wrapper.initialize threw.\n        this.wrapperInitData[i] = Transaction.OBSERVED_ERROR;\n        this.wrapperInitData[i] = wrapper.initialize ?\n          wrapper.initialize.call(this) :\n          null;\n      } finally {\n        if (this.wrapperInitData[i] === Transaction.OBSERVED_ERROR) {\n          // The initializer for wrapper i threw an error; initialize the\n          // remaining wrappers but silence any exceptions from them to ensure\n          // that the first error is the one to bubble up.\n          try {\n            this.initializeAll(i + 1);\n          } catch (err) {\n          }\n        }\n      }\n    }\n  },\n\n  /**\n   * Invokes each of `this.transactionWrappers.close[i]` functions, passing into\n   * them the respective return values of `this.transactionWrappers.init[i]`\n   * (`close`rs that correspond to initializers that failed will not be\n   * invoked).\n   */\n  closeAll: function(startIndex) {\n    (\"production\" !== \"development\" ? invariant(\n      this.isInTransaction(),\n      'Transaction.closeAll(): Cannot close transaction when none are open.'\n    ) : invariant(this.isInTransaction()));\n    var transactionWrappers = this.transactionWrappers;\n    for (var i = startIndex; i < transactionWrappers.length; i++) {\n      var wrapper = transactionWrappers[i];\n      var initData = this.wrapperInitData[i];\n      var errorThrown;\n      try {\n        // Catching errors makes debugging more difficult, so we start with\n        // errorThrown set to true before setting it to false after calling\n        // close -- if it's still set to true in the finally block, it means\n        // wrapper.close threw.\n        errorThrown = true;\n        if (initData !== Transaction.OBSERVED_ERROR) {\n          wrapper.close && wrapper.close.call(this, initData);\n        }\n        errorThrown = false;\n      } finally {\n        if (errorThrown) {\n          // The closer for wrapper i threw an error; close the remaining\n          // wrappers but silence any exceptions from them to ensure that the\n          // first error is the one to bubble up.\n          try {\n            this.closeAll(i + 1);\n          } catch (e) {\n          }\n        }\n      }\n    }\n    this.wrapperInitData.length = 0;\n  }\n};\n\nvar Transaction = {\n\n  Mixin: Mixin,\n\n  /**\n   * Token to look for to determine if an error occured.\n   */\n  OBSERVED_ERROR: {}\n\n};\n\nmodule.exports = Transaction;\n\n},{\"./invariant\":134}],105:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule ViewportMetrics\n */\n\n\"use strict\";\n\nvar getUnboundedScrollPosition = _dereq_(\"./getUnboundedScrollPosition\");\n\nvar ViewportMetrics = {\n\n  currentScrollLeft: 0,\n\n  currentScrollTop: 0,\n\n  refreshScrollValues: function() {\n    var scrollPosition = getUnboundedScrollPosition(window);\n    ViewportMetrics.currentScrollLeft = scrollPosition.x;\n    ViewportMetrics.currentScrollTop = scrollPosition.y;\n  }\n\n};\n\nmodule.exports = ViewportMetrics;\n\n},{\"./getUnboundedScrollPosition\":130}],106:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule accumulate\n */\n\n\"use strict\";\n\nvar invariant = _dereq_(\"./invariant\");\n\n/**\n * Accumulates items that must not be null or undefined.\n *\n * This is used to conserve memory by avoiding array allocations.\n *\n * @return {*|array<*>} An accumulation of items.\n */\nfunction accumulate(current, next) {\n  (\"production\" !== \"development\" ? invariant(\n    next != null,\n    'accumulate(...): Accumulated items must be not be null or undefined.'\n  ) : invariant(next != null));\n  if (current == null) {\n    return next;\n  } else {\n    // Both are not empty. Warning: Never call x.concat(y) when you are not\n    // certain that x is an Array (x could be a string with concat method).\n    var currentIsArray = Array.isArray(current);\n    var nextIsArray = Array.isArray(next);\n    if (currentIsArray) {\n      return current.concat(next);\n    } else {\n      if (nextIsArray) {\n        return [current].concat(next);\n      } else {\n        return [current, next];\n      }\n    }\n  }\n}\n\nmodule.exports = accumulate;\n\n},{\"./invariant\":134}],107:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule adler32\n */\n\n/* jslint bitwise:true */\n\n\"use strict\";\n\nvar MOD = 65521;\n\n// This is a clean-room implementation of adler32 designed for detecting\n// if markup is not what we expect it to be. It does not need to be\n// cryptographically strong, only reasonable good at detecting if markup\n// generated on the server is different than that on the client.\nfunction adler32(data) {\n  var a = 1;\n  var b = 0;\n  for (var i = 0; i < data.length; i++) {\n    a = (a + data.charCodeAt(i)) % MOD;\n    b = (b + a) % MOD;\n  }\n  return a | (b << 16);\n}\n\nmodule.exports = adler32;\n\n},{}],108:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @typechecks\n * @providesModule cloneWithProps\n */\n\n\"use strict\";\n\nvar ReactPropTransferer = _dereq_(\"./ReactPropTransferer\");\n\nvar keyOf = _dereq_(\"./keyOf\");\nvar warning = _dereq_(\"./warning\");\n\nvar CHILDREN_PROP = keyOf({children: null});\n\n/**\n * Sometimes you want to change the props of a child passed to you. Usually\n * this is to add a CSS class.\n *\n * @param {object} child child component you'd like to clone\n * @param {object} props props you'd like to modify. They will be merged\n * as if you used `transferPropsTo()`.\n * @return {object} a clone of child with props merged in.\n */\nfunction cloneWithProps(child, props) {\n  if (\"production\" !== \"development\") {\n    (\"production\" !== \"development\" ? warning(\n      !child.props.ref,\n      'You are calling cloneWithProps() on a child with a ref. This is ' +\n      'dangerous because you\\'re creating a new child which will not be ' +\n      'added as a ref to its parent.'\n    ) : null);\n  }\n\n  var newProps = ReactPropTransferer.mergeProps(props, child.props);\n\n  // Use `child.props.children` if it is provided.\n  if (!newProps.hasOwnProperty(CHILDREN_PROP) &&\n      child.props.hasOwnProperty(CHILDREN_PROP)) {\n    newProps.children = child.props.children;\n  }\n\n  // The current API doesn't retain _owner and _context, which is why this\n  // doesn't use ReactDescriptor.cloneAndReplaceProps.\n  return child.constructor(newProps);\n}\n\nmodule.exports = cloneWithProps;\n\n},{\"./ReactPropTransferer\":72,\"./keyOf\":141,\"./warning\":158}],109:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule containsNode\n * @typechecks\n */\n\nvar isTextNode = _dereq_(\"./isTextNode\");\n\n/*jslint bitwise:true */\n\n/**\n * Checks if a given DOM node contains or is another DOM node.\n *\n * @param {?DOMNode} outerNode Outer DOM node.\n * @param {?DOMNode} innerNode Inner DOM node.\n * @return {boolean} True if `outerNode` contains or is `innerNode`.\n */\nfunction containsNode(outerNode, innerNode) {\n  if (!outerNode || !innerNode) {\n    return false;\n  } else if (outerNode === innerNode) {\n    return true;\n  } else if (isTextNode(outerNode)) {\n    return false;\n  } else if (isTextNode(innerNode)) {\n    return containsNode(outerNode, innerNode.parentNode);\n  } else if (outerNode.contains) {\n    return outerNode.contains(innerNode);\n  } else if (outerNode.compareDocumentPosition) {\n    return !!(outerNode.compareDocumentPosition(innerNode) & 16);\n  } else {\n    return false;\n  }\n}\n\nmodule.exports = containsNode;\n\n},{\"./isTextNode\":138}],110:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule copyProperties\n */\n\n/**\n * Copy properties from one or more objects (up to 5) into the first object.\n * This is a shallow copy. It mutates the first object and also returns it.\n *\n * NOTE: `arguments` has a very significant performance penalty, which is why\n * we don't support unlimited arguments.\n */\nfunction copyProperties(obj, a, b, c, d, e, f) {\n  obj = obj || {};\n\n  if (\"production\" !== \"development\") {\n    if (f) {\n      throw new Error('Too many arguments passed to copyProperties');\n    }\n  }\n\n  var args = [a, b, c, d, e];\n  var ii = 0, v;\n  while (args[ii]) {\n    v = args[ii++];\n    for (var k in v) {\n      obj[k] = v[k];\n    }\n\n    // IE ignores toString in object iteration.. See:\n    // webreflection.blogspot.com/2007/07/quick-fix-internet-explorer-and.html\n    if (v.hasOwnProperty && v.hasOwnProperty('toString') &&\n        (typeof v.toString != 'undefined') && (obj.toString !== v.toString)) {\n      obj.toString = v.toString;\n    }\n  }\n\n  return obj;\n}\n\nmodule.exports = copyProperties;\n\n},{}],111:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule createArrayFrom\n * @typechecks\n */\n\nvar toArray = _dereq_(\"./toArray\");\n\n/**\n * Perform a heuristic test to determine if an object is \"array-like\".\n *\n *   A monk asked Joshu, a Zen master, \"Has a dog Buddha nature?\"\n *   Joshu replied: \"Mu.\"\n *\n * This function determines if its argument has \"array nature\": it returns\n * true if the argument is an actual array, an `arguments' object, or an\n * HTMLCollection (e.g. node.childNodes or node.getElementsByTagName()).\n *\n * It will return false for other array-like objects like Filelist.\n *\n * @param {*} obj\n * @return {boolean}\n */\nfunction hasArrayNature(obj) {\n  return (\n    // not null/false\n    !!obj &&\n    // arrays are objects, NodeLists are functions in Safari\n    (typeof obj == 'object' || typeof obj == 'function') &&\n    // quacks like an array\n    ('length' in obj) &&\n    // not window\n    !('setInterval' in obj) &&\n    // no DOM node should be considered an array-like\n    // a 'select' element has 'length' and 'item' properties on IE8\n    (typeof obj.nodeType != 'number') &&\n    (\n      // a real array\n      (// HTMLCollection/NodeList\n      (Array.isArray(obj) ||\n      // arguments\n      ('callee' in obj) || 'item' in obj))\n    )\n  );\n}\n\n/**\n * Ensure that the argument is an array by wrapping it in an array if it is not.\n * Creates a copy of the argument if it is already an array.\n *\n * This is mostly useful idiomatically:\n *\n *   var createArrayFrom = require('createArrayFrom');\n *\n *   function takesOneOrMoreThings(things) {\n *     things = createArrayFrom(things);\n *     ...\n *   }\n *\n * This allows you to treat `things' as an array, but accept scalars in the API.\n *\n * If you need to convert an array-like object, like `arguments`, into an array\n * use toArray instead.\n *\n * @param {*} obj\n * @return {array}\n */\nfunction createArrayFrom(obj) {\n  if (!hasArrayNature(obj)) {\n    return [obj];\n  } else if (Array.isArray(obj)) {\n    return obj.slice();\n  } else {\n    return toArray(obj);\n  }\n}\n\nmodule.exports = createArrayFrom;\n\n},{\"./toArray\":155}],112:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule createFullPageComponent\n * @typechecks\n */\n\n\"use strict\";\n\n// Defeat circular references by requiring this directly.\nvar ReactCompositeComponent = _dereq_(\"./ReactCompositeComponent\");\n\nvar invariant = _dereq_(\"./invariant\");\n\n/**\n * Create a component that will throw an exception when unmounted.\n *\n * Components like <html> <head> and <body> can't be removed or added\n * easily in a cross-browser way, however it's valuable to be able to\n * take advantage of React's reconciliation for styling and <title>\n * management. So we just document it and throw in dangerous cases.\n *\n * @param {function} componentClass convenience constructor to wrap\n * @return {function} convenience constructor of new component\n */\nfunction createFullPageComponent(componentClass) {\n  var FullPageComponent = ReactCompositeComponent.createClass({\n    displayName: 'ReactFullPageComponent' + (\n      componentClass.type.displayName || ''\n    ),\n\n    componentWillUnmount: function() {\n      (\"production\" !== \"development\" ? invariant(\n        false,\n        '%s tried to unmount. Because of cross-browser quirks it is ' +\n        'impossible to unmount some top-level components (eg <html>, <head>, ' +\n        'and <body>) reliably and efficiently. To fix this, have a single ' +\n        'top-level component that never unmounts render these elements.',\n        this.constructor.displayName\n      ) : invariant(false));\n    },\n\n    render: function() {\n      return this.transferPropsTo(componentClass(null, this.props.children));\n    }\n  });\n\n  return FullPageComponent;\n}\n\nmodule.exports = createFullPageComponent;\n\n},{\"./ReactCompositeComponent\":38,\"./invariant\":134}],113:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule createNodesFromMarkup\n * @typechecks\n */\n\n/*jslint evil: true, sub: true */\n\nvar ExecutionEnvironment = _dereq_(\"./ExecutionEnvironment\");\n\nvar createArrayFrom = _dereq_(\"./createArrayFrom\");\nvar getMarkupWrap = _dereq_(\"./getMarkupWrap\");\nvar invariant = _dereq_(\"./invariant\");\n\n/**\n * Dummy container used to render all markup.\n */\nvar dummyNode =\n  ExecutionEnvironment.canUseDOM ? document.createElement('div') : null;\n\n/**\n * Pattern used by `getNodeName`.\n */\nvar nodeNamePattern = /^\\s*<(\\w+)/;\n\n/**\n * Extracts the `nodeName` of the first element in a string of markup.\n *\n * @param {string} markup String of markup.\n * @return {?string} Node name of the supplied markup.\n */\nfunction getNodeName(markup) {\n  var nodeNameMatch = markup.match(nodeNamePattern);\n  return nodeNameMatch && nodeNameMatch[1].toLowerCase();\n}\n\n/**\n * Creates an array containing the nodes rendered from the supplied markup. The\n * optionally supplied `handleScript` function will be invoked once for each\n * <script> element that is rendered. If no `handleScript` function is supplied,\n * an exception is thrown if any <script> elements are rendered.\n *\n * @param {string} markup A string of valid HTML markup.\n * @param {?function} handleScript Invoked once for each rendered <script>.\n * @return {array<DOMElement|DOMTextNode>} An array of rendered nodes.\n */\nfunction createNodesFromMarkup(markup, handleScript) {\n  var node = dummyNode;\n  (\"production\" !== \"development\" ? invariant(!!dummyNode, 'createNodesFromMarkup dummy not initialized') : invariant(!!dummyNode));\n  var nodeName = getNodeName(markup);\n\n  var wrap = nodeName && getMarkupWrap(nodeName);\n  if (wrap) {\n    node.innerHTML = wrap[1] + markup + wrap[2];\n\n    var wrapDepth = wrap[0];\n    while (wrapDepth--) {\n      node = node.lastChild;\n    }\n  } else {\n    node.innerHTML = markup;\n  }\n\n  var scripts = node.getElementsByTagName('script');\n  if (scripts.length) {\n    (\"production\" !== \"development\" ? invariant(\n      handleScript,\n      'createNodesFromMarkup(...): Unexpected <script> element rendered.'\n    ) : invariant(handleScript));\n    createArrayFrom(scripts).forEach(handleScript);\n  }\n\n  var nodes = createArrayFrom(node.childNodes);\n  while (node.lastChild) {\n    node.removeChild(node.lastChild);\n  }\n  return nodes;\n}\n\nmodule.exports = createNodesFromMarkup;\n\n},{\"./ExecutionEnvironment\":22,\"./createArrayFrom\":111,\"./getMarkupWrap\":126,\"./invariant\":134}],114:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule cx\n */\n\n/**\n * This function is used to mark string literals representing CSS class names\n * so that they can be transformed statically. This allows for modularization\n * and minification of CSS class names.\n *\n * In static_upstream, this function is actually implemented, but it should\n * eventually be replaced with something more descriptive, and the transform\n * that is used in the main stack should be ported for use elsewhere.\n *\n * @param string|object className to modularize, or an object of key/values.\n *                      In the object case, the values are conditions that\n *                      determine if the className keys should be included.\n * @param [string ...]  Variable list of classNames in the string case.\n * @return string       Renderable space-separated CSS className.\n */\nfunction cx(classNames) {\n  if (typeof classNames == 'object') {\n    return Object.keys(classNames).filter(function(className) {\n      return classNames[className];\n    }).join(' ');\n  } else {\n    return Array.prototype.join.call(arguments, ' ');\n  }\n}\n\nmodule.exports = cx;\n\n},{}],115:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule dangerousStyleValue\n * @typechecks static-only\n */\n\n\"use strict\";\n\nvar CSSProperty = _dereq_(\"./CSSProperty\");\n\nvar isUnitlessNumber = CSSProperty.isUnitlessNumber;\n\n/**\n * Convert a value into the proper css writable value. The style name `name`\n * should be logical (no hyphens), as specified\n * in `CSSProperty.isUnitlessNumber`.\n *\n * @param {string} name CSS property name such as `topMargin`.\n * @param {*} value CSS property value such as `10px`.\n * @return {string} Normalized style value with dimensions applied.\n */\nfunction dangerousStyleValue(name, value) {\n  // Note that we've removed escapeTextForBrowser() calls here since the\n  // whole string will be escaped when the attribute is injected into\n  // the markup. If you provide unsafe user data here they can inject\n  // arbitrary CSS which may be problematic (I couldn't repro this):\n  // https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet\n  // http://www.thespanner.co.uk/2007/11/26/ultimate-xss-css-injection/\n  // This is not an XSS hole but instead a potential CSS injection issue\n  // which has lead to a greater discussion about how we're going to\n  // trust URLs moving forward. See #2115901\n\n  var isEmpty = value == null || typeof value === 'boolean' || value === '';\n  if (isEmpty) {\n    return '';\n  }\n\n  var isNonNumeric = isNaN(value);\n  if (isNonNumeric || value === 0 ||\n      isUnitlessNumber.hasOwnProperty(name) && isUnitlessNumber[name]) {\n    return '' + value; // cast to string\n  }\n\n  if (typeof value === 'string') {\n    value = value.trim();\n  }\n  return value + 'px';\n}\n\nmodule.exports = dangerousStyleValue;\n\n},{\"./CSSProperty\":4}],116:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule emptyFunction\n */\n\nvar copyProperties = _dereq_(\"./copyProperties\");\n\nfunction makeEmptyFunction(arg) {\n  return function() {\n    return arg;\n  };\n}\n\n/**\n * This function accepts and discards inputs; it has no side effects. This is\n * primarily useful idiomatically for overridable function endpoints which\n * always need to be callable, since JS lacks a null-call idiom ala Cocoa.\n */\nfunction emptyFunction() {}\n\ncopyProperties(emptyFunction, {\n  thatReturns: makeEmptyFunction,\n  thatReturnsFalse: makeEmptyFunction(false),\n  thatReturnsTrue: makeEmptyFunction(true),\n  thatReturnsNull: makeEmptyFunction(null),\n  thatReturnsThis: function() { return this; },\n  thatReturnsArgument: function(arg) { return arg; }\n});\n\nmodule.exports = emptyFunction;\n\n},{\"./copyProperties\":110}],117:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule emptyObject\n */\n\n\"use strict\";\n\nvar emptyObject = {};\n\nif (\"production\" !== \"development\") {\n  Object.freeze(emptyObject);\n}\n\nmodule.exports = emptyObject;\n\n},{}],118:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule escapeTextForBrowser\n * @typechecks static-only\n */\n\n\"use strict\";\n\nvar ESCAPE_LOOKUP = {\n  \"&\": \"&amp;\",\n  \">\": \"&gt;\",\n  \"<\": \"&lt;\",\n  \"\\\"\": \"&quot;\",\n  \"'\": \"&#x27;\"\n};\n\nvar ESCAPE_REGEX = /[&><\"']/g;\n\nfunction escaper(match) {\n  return ESCAPE_LOOKUP[match];\n}\n\n/**\n * Escapes text to prevent scripting attacks.\n *\n * @param {*} text Text value to escape.\n * @return {string} An escaped string.\n */\nfunction escapeTextForBrowser(text) {\n  return ('' + text).replace(ESCAPE_REGEX, escaper);\n}\n\nmodule.exports = escapeTextForBrowser;\n\n},{}],119:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule flattenChildren\n */\n\n\"use strict\";\n\nvar traverseAllChildren = _dereq_(\"./traverseAllChildren\");\nvar warning = _dereq_(\"./warning\");\n\n/**\n * @param {function} traverseContext Context passed through traversal.\n * @param {?ReactComponent} child React child component.\n * @param {!string} name String name of key path to child.\n */\nfunction flattenSingleChildIntoContext(traverseContext, child, name) {\n  // We found a component instance.\n  var result = traverseContext;\n  var keyUnique = !result.hasOwnProperty(name);\n  (\"production\" !== \"development\" ? warning(\n    keyUnique,\n    'flattenChildren(...): Encountered two children with the same key, ' +\n    '`%s`. Child keys must be unique; when two children share a key, only ' +\n    'the first child will be used.',\n    name\n  ) : null);\n  if (keyUnique && child != null) {\n    result[name] = child;\n  }\n}\n\n/**\n * Flattens children that are typically specified as `props.children`. Any null\n * children will not be included in the resulting object.\n * @return {!object} flattened children keyed by name.\n */\nfunction flattenChildren(children) {\n  if (children == null) {\n    return children;\n  }\n  var result = {};\n  traverseAllChildren(children, flattenSingleChildIntoContext, result);\n  return result;\n}\n\nmodule.exports = flattenChildren;\n\n},{\"./traverseAllChildren\":156,\"./warning\":158}],120:[function(_dereq_,module,exports){\n/**\n * Copyright 2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule focusNode\n */\n\n\"use strict\";\n\n/**\n * IE8 throws if an input/textarea is disabled and we try to focus it.\n * Focus only when necessary.\n *\n * @param {DOMElement} node input/textarea to focus\n */\nfunction focusNode(node) {\n  if (!node.disabled) {\n    node.focus();\n  }\n}\n\nmodule.exports = focusNode;\n\n},{}],121:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule forEachAccumulated\n */\n\n\"use strict\";\n\n/**\n * @param {array} an \"accumulation\" of items which is either an Array or\n * a single item. Useful when paired with the `accumulate` module. This is a\n * simple utility that allows us to reason about a collection of items, but\n * handling the case when there is exactly one item (and we do not need to\n * allocate an array).\n */\nvar forEachAccumulated = function(arr, cb, scope) {\n  if (Array.isArray(arr)) {\n    arr.forEach(cb, scope);\n  } else if (arr) {\n    cb.call(scope, arr);\n  }\n};\n\nmodule.exports = forEachAccumulated;\n\n},{}],122:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule getActiveElement\n * @typechecks\n */\n\n/**\n * Same as document.activeElement but wraps in a try-catch block. In IE it is\n * not safe to call document.activeElement if there is nothing focused.\n *\n * The activeElement will be null only if the document body is not yet defined.\n */\nfunction getActiveElement() /*?DOMElement*/ {\n  try {\n    return document.activeElement || document.body;\n  } catch (e) {\n    return document.body;\n  }\n}\n\nmodule.exports = getActiveElement;\n\n},{}],123:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule getEventKey\n * @typechecks static-only\n */\n\n\"use strict\";\n\nvar invariant = _dereq_(\"./invariant\");\n\n/**\n * Normalization of deprecated HTML5 `key` values\n * @see https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent#Key_names\n */\nvar normalizeKey = {\n  'Esc': 'Escape',\n  'Spacebar': ' ',\n  'Left': 'ArrowLeft',\n  'Up': 'ArrowUp',\n  'Right': 'ArrowRight',\n  'Down': 'ArrowDown',\n  'Del': 'Delete',\n  'Win': 'OS',\n  'Menu': 'ContextMenu',\n  'Apps': 'ContextMenu',\n  'Scroll': 'ScrollLock',\n  'MozPrintableKey': 'Unidentified'\n};\n\n/**\n * Translation from legacy `which`/`keyCode` to HTML5 `key`\n * Only special keys supported, all others depend on keyboard layout or browser\n * @see https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent#Key_names\n */\nvar translateToKey = {\n  8: 'Backspace',\n  9: 'Tab',\n  12: 'Clear',\n  13: 'Enter',\n  16: 'Shift',\n  17: 'Control',\n  18: 'Alt',\n  19: 'Pause',\n  20: 'CapsLock',\n  27: 'Escape',\n  32: ' ',\n  33: 'PageUp',\n  34: 'PageDown',\n  35: 'End',\n  36: 'Home',\n  37: 'ArrowLeft',\n  38: 'ArrowUp',\n  39: 'ArrowRight',\n  40: 'ArrowDown',\n  45: 'Insert',\n  46: 'Delete',\n  112: 'F1', 113: 'F2', 114: 'F3', 115: 'F4', 116: 'F5', 117: 'F6',\n  118: 'F7', 119: 'F8', 120: 'F9', 121: 'F10', 122: 'F11', 123: 'F12',\n  144: 'NumLock',\n  145: 'ScrollLock',\n  224: 'Meta'\n};\n\n/**\n * @param {object} nativeEvent Native browser event.\n * @return {string} Normalized `key` property.\n */\nfunction getEventKey(nativeEvent) {\n  if (nativeEvent.key) {\n    // Normalize inconsistent values reported by browsers due to\n    // implementations of a working draft specification.\n\n    // FireFox implements `key` but returns `MozPrintableKey` for all\n    // printable characters (normalized to `Unidentified`), ignore it.\n    var key = normalizeKey[nativeEvent.key] || nativeEvent.key;\n    if (key !== 'Unidentified') {\n      return key;\n    }\n  }\n\n  // Browser does not implement `key`, polyfill as much of it as we can.\n  if (nativeEvent.type === 'keypress') {\n    // Create the character from the `charCode` ourselves and use as an almost\n    // perfect replacement.\n    var charCode = 'charCode' in nativeEvent ?\n      nativeEvent.charCode :\n      nativeEvent.keyCode;\n\n    // The enter-key is technically both printable and non-printable and can\n    // thus be captured by `keypress`, no other non-printable key should.\n    return charCode === 13 ? 'Enter' : String.fromCharCode(charCode);\n  }\n  if (nativeEvent.type === 'keydown' || nativeEvent.type === 'keyup') {\n    // While user keyboard layout determines the actual meaning of each\n    // `keyCode` value, almost all function keys have a universal value.\n    return translateToKey[nativeEvent.keyCode] || 'Unidentified';\n  }\n\n  (\"production\" !== \"development\" ? invariant(false, \"Unexpected keyboard event type: %s\", nativeEvent.type) : invariant(false));\n}\n\nmodule.exports = getEventKey;\n\n},{\"./invariant\":134}],124:[function(_dereq_,module,exports){\n/**\n * Copyright 2013 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule getEventModifierState\n * @typechecks static-only\n */\n\n\"use strict\";\n\n/**\n * Translation from modifier key to the associated property in the event.\n * @see http://www.w3.org/TR/DOM-Level-3-Events/#keys-Modifiers\n */\n\nvar modifierKeyToProp = {\n  'Alt': 'altKey',\n  'Control': 'ctrlKey',\n  'Meta': 'metaKey',\n  'Shift': 'shiftKey'\n};\n\n// IE8 does not implement getModifierState so we simply map it to the only\n// modifier keys exposed by the event itself, does not support Lock-keys.\n// Currently, all major browsers except Chrome seems to support Lock-keys.\nfunction modifierStateGetter(keyArg) {\n  /*jshint validthis:true */\n  var syntheticEvent = this;\n  var nativeEvent = syntheticEvent.nativeEvent;\n  if (nativeEvent.getModifierState) {\n    return nativeEvent.getModifierState(keyArg);\n  }\n  var keyProp = modifierKeyToProp[keyArg];\n  return keyProp ? !!nativeEvent[keyProp] : false;\n}\n\nfunction getEventModifierState(nativeEvent) {\n  return modifierStateGetter;\n}\n\nmodule.exports = getEventModifierState;\n\n},{}],125:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule getEventTarget\n * @typechecks static-only\n */\n\n\"use strict\";\n\n/**\n * Gets the target node from a native browser event by accounting for\n * inconsistencies in browser DOM APIs.\n *\n * @param {object} nativeEvent Native browser event.\n * @return {DOMEventTarget} Target node.\n */\nfunction getEventTarget(nativeEvent) {\n  var target = nativeEvent.target || nativeEvent.srcElement || window;\n  // Safari may fire events on text nodes (Node.TEXT_NODE is 3).\n  // @see http://www.quirksmode.org/js/events_properties.html\n  return target.nodeType === 3 ? target.parentNode : target;\n}\n\nmodule.exports = getEventTarget;\n\n},{}],126:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule getMarkupWrap\n */\n\nvar ExecutionEnvironment = _dereq_(\"./ExecutionEnvironment\");\n\nvar invariant = _dereq_(\"./invariant\");\n\n/**\n * Dummy container used to detect which wraps are necessary.\n */\nvar dummyNode =\n  ExecutionEnvironment.canUseDOM ? document.createElement('div') : null;\n\n/**\n * Some browsers cannot use `innerHTML` to render certain elements standalone,\n * so we wrap them, render the wrapped nodes, then extract the desired node.\n *\n * In IE8, certain elements cannot render alone, so wrap all elements ('*').\n */\nvar shouldWrap = {\n  // Force wrapping for SVG elements because if they get created inside a <div>,\n  // they will be initialized in the wrong namespace (and will not display).\n  'circle': true,\n  'defs': true,\n  'ellipse': true,\n  'g': true,\n  'line': true,\n  'linearGradient': true,\n  'path': true,\n  'polygon': true,\n  'polyline': true,\n  'radialGradient': true,\n  'rect': true,\n  'stop': true,\n  'text': true\n};\n\nvar selectWrap = [1, '<select multiple=\"true\">', '</select>'];\nvar tableWrap = [1, '<table>', '</table>'];\nvar trWrap = [3, '<table><tbody><tr>', '</tr></tbody></table>'];\n\nvar svgWrap = [1, '<svg>', '</svg>'];\n\nvar markupWrap = {\n  '*': [1, '?<div>', '</div>'],\n\n  'area': [1, '<map>', '</map>'],\n  'col': [2, '<table><tbody></tbody><colgroup>', '</colgroup></table>'],\n  'legend': [1, '<fieldset>', '</fieldset>'],\n  'param': [1, '<object>', '</object>'],\n  'tr': [2, '<table><tbody>', '</tbody></table>'],\n\n  'optgroup': selectWrap,\n  'option': selectWrap,\n\n  'caption': tableWrap,\n  'colgroup': tableWrap,\n  'tbody': tableWrap,\n  'tfoot': tableWrap,\n  'thead': tableWrap,\n\n  'td': trWrap,\n  'th': trWrap,\n\n  'circle': svgWrap,\n  'defs': svgWrap,\n  'ellipse': svgWrap,\n  'g': svgWrap,\n  'line': svgWrap,\n  'linearGradient': svgWrap,\n  'path': svgWrap,\n  'polygon': svgWrap,\n  'polyline': svgWrap,\n  'radialGradient': svgWrap,\n  'rect': svgWrap,\n  'stop': svgWrap,\n  'text': svgWrap\n};\n\n/**\n * Gets the markup wrap configuration for the supplied `nodeName`.\n *\n * NOTE: This lazily detects which wraps are necessary for the current browser.\n *\n * @param {string} nodeName Lowercase `nodeName`.\n * @return {?array} Markup wrap configuration, if applicable.\n */\nfunction getMarkupWrap(nodeName) {\n  (\"production\" !== \"development\" ? invariant(!!dummyNode, 'Markup wrapping node not initialized') : invariant(!!dummyNode));\n  if (!markupWrap.hasOwnProperty(nodeName)) {\n    nodeName = '*';\n  }\n  if (!shouldWrap.hasOwnProperty(nodeName)) {\n    if (nodeName === '*') {\n      dummyNode.innerHTML = '<link />';\n    } else {\n      dummyNode.innerHTML = '<' + nodeName + '></' + nodeName + '>';\n    }\n    shouldWrap[nodeName] = !dummyNode.firstChild;\n  }\n  return shouldWrap[nodeName] ? markupWrap[nodeName] : null;\n}\n\n\nmodule.exports = getMarkupWrap;\n\n},{\"./ExecutionEnvironment\":22,\"./invariant\":134}],127:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule getNodeForCharacterOffset\n */\n\n\"use strict\";\n\n/**\n * Given any node return the first leaf node without children.\n *\n * @param {DOMElement|DOMTextNode} node\n * @return {DOMElement|DOMTextNode}\n */\nfunction getLeafNode(node) {\n  while (node && node.firstChild) {\n    node = node.firstChild;\n  }\n  return node;\n}\n\n/**\n * Get the next sibling within a container. This will walk up the\n * DOM if a node's siblings have been exhausted.\n *\n * @param {DOMElement|DOMTextNode} node\n * @return {?DOMElement|DOMTextNode}\n */\nfunction getSiblingNode(node) {\n  while (node) {\n    if (node.nextSibling) {\n      return node.nextSibling;\n    }\n    node = node.parentNode;\n  }\n}\n\n/**\n * Get object describing the nodes which contain characters at offset.\n *\n * @param {DOMElement|DOMTextNode} root\n * @param {number} offset\n * @return {?object}\n */\nfunction getNodeForCharacterOffset(root, offset) {\n  var node = getLeafNode(root);\n  var nodeStart = 0;\n  var nodeEnd = 0;\n\n  while (node) {\n    if (node.nodeType == 3) {\n      nodeEnd = nodeStart + node.textContent.length;\n\n      if (nodeStart <= offset && nodeEnd >= offset) {\n        return {\n          node: node,\n          offset: offset - nodeStart\n        };\n      }\n\n      nodeStart = nodeEnd;\n    }\n\n    node = getLeafNode(getSiblingNode(node));\n  }\n}\n\nmodule.exports = getNodeForCharacterOffset;\n\n},{}],128:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule getReactRootElementInContainer\n */\n\n\"use strict\";\n\nvar DOC_NODE_TYPE = 9;\n\n/**\n * @param {DOMElement|DOMDocument} container DOM element that may contain\n *                                           a React component\n * @return {?*} DOM element that may have the reactRoot ID, or null.\n */\nfunction getReactRootElementInContainer(container) {\n  if (!container) {\n    return null;\n  }\n\n  if (container.nodeType === DOC_NODE_TYPE) {\n    return container.documentElement;\n  } else {\n    return container.firstChild;\n  }\n}\n\nmodule.exports = getReactRootElementInContainer;\n\n},{}],129:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule getTextContentAccessor\n */\n\n\"use strict\";\n\nvar ExecutionEnvironment = _dereq_(\"./ExecutionEnvironment\");\n\nvar contentKey = null;\n\n/**\n * Gets the key used to access text content on a DOM node.\n *\n * @return {?string} Key used to access text content.\n * @internal\n */\nfunction getTextContentAccessor() {\n  if (!contentKey && ExecutionEnvironment.canUseDOM) {\n    // Prefer textContent to innerText because many browsers support both but\n    // SVG <text> elements don't support innerText even when <div> does.\n    contentKey = 'textContent' in document.documentElement ?\n      'textContent' :\n      'innerText';\n  }\n  return contentKey;\n}\n\nmodule.exports = getTextContentAccessor;\n\n},{\"./ExecutionEnvironment\":22}],130:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule getUnboundedScrollPosition\n * @typechecks\n */\n\n\"use strict\";\n\n/**\n * Gets the scroll position of the supplied element or window.\n *\n * The return values are unbounded, unlike `getScrollPosition`. This means they\n * may be negative or exceed the element boundaries (which is possible using\n * inertial scrolling).\n *\n * @param {DOMWindow|DOMElement} scrollable\n * @return {object} Map with `x` and `y` keys.\n */\nfunction getUnboundedScrollPosition(scrollable) {\n  if (scrollable === window) {\n    return {\n      x: window.pageXOffset || document.documentElement.scrollLeft,\n      y: window.pageYOffset || document.documentElement.scrollTop\n    };\n  }\n  return {\n    x: scrollable.scrollLeft,\n    y: scrollable.scrollTop\n  };\n}\n\nmodule.exports = getUnboundedScrollPosition;\n\n},{}],131:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule hyphenate\n * @typechecks\n */\n\nvar _uppercasePattern = /([A-Z])/g;\n\n/**\n * Hyphenates a camelcased string, for example:\n *\n *   > hyphenate('backgroundColor')\n *   < \"background-color\"\n *\n * For CSS style names, use `hyphenateStyleName` instead which works properly\n * with all vendor prefixes, including `ms`.\n *\n * @param {string} string\n * @return {string}\n */\nfunction hyphenate(string) {\n  return string.replace(_uppercasePattern, '-$1').toLowerCase();\n}\n\nmodule.exports = hyphenate;\n\n},{}],132:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule hyphenateStyleName\n * @typechecks\n */\n\n\"use strict\";\n\nvar hyphenate = _dereq_(\"./hyphenate\");\n\nvar msPattern = /^ms-/;\n\n/**\n * Hyphenates a camelcased CSS property name, for example:\n *\n *   > hyphenate('backgroundColor')\n *   < \"background-color\"\n *   > hyphenate('MozTransition')\n *   < \"-moz-transition\"\n *   > hyphenate('msTransition')\n *   < \"-ms-transition\"\n *\n * As Modernizr suggests (http://modernizr.com/docs/#prefixed), an `ms` prefix\n * is converted to `-ms-`.\n *\n * @param {string} string\n * @return {string}\n */\nfunction hyphenateStyleName(string) {\n  return hyphenate(string).replace(msPattern, '-ms-');\n}\n\nmodule.exports = hyphenateStyleName;\n\n},{\"./hyphenate\":131}],133:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule instantiateReactComponent\n * @typechecks static-only\n */\n\n\"use strict\";\n\nvar invariant = _dereq_(\"./invariant\");\n\n/**\n * Validate a `componentDescriptor`. This should be exposed publicly in a follow\n * up diff.\n *\n * @param {object} descriptor\n * @return {boolean} Returns true if this is a valid descriptor of a Component.\n */\nfunction isValidComponentDescriptor(descriptor) {\n  return (\n    descriptor &&\n    typeof descriptor.type === 'function' &&\n    typeof descriptor.type.prototype.mountComponent === 'function' &&\n    typeof descriptor.type.prototype.receiveComponent === 'function'\n  );\n}\n\n/**\n * Given a `componentDescriptor` create an instance that will actually be\n * mounted. Currently it just extracts an existing clone from composite\n * components but this is an implementation detail which will change.\n *\n * @param {object} descriptor\n * @return {object} A new instance of componentDescriptor's constructor.\n * @protected\n */\nfunction instantiateReactComponent(descriptor) {\n\n  // TODO: Make warning\n  // if (__DEV__) {\n    (\"production\" !== \"development\" ? invariant(\n      isValidComponentDescriptor(descriptor),\n      'Only React Components are valid for mounting.'\n    ) : invariant(isValidComponentDescriptor(descriptor)));\n  // }\n\n  return new descriptor.type(descriptor);\n}\n\nmodule.exports = instantiateReactComponent;\n\n},{\"./invariant\":134}],134:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule invariant\n */\n\n\"use strict\";\n\n/**\n * Use invariant() to assert state which your program assumes to be true.\n *\n * Provide sprintf-style format (only %s is supported) and arguments\n * to provide information about what broke and what you were\n * expecting.\n *\n * The invariant message will be stripped in production, but the invariant\n * will remain to ensure logic does not differ in production.\n */\n\nvar invariant = function(condition, format, a, b, c, d, e, f) {\n  if (\"production\" !== \"development\") {\n    if (format === undefined) {\n      throw new Error('invariant requires an error message argument');\n    }\n  }\n\n  if (!condition) {\n    var error;\n    if (format === undefined) {\n      error = new Error(\n        'Minified exception occurred; use the non-minified dev environment ' +\n        'for the full error message and additional helpful warnings.'\n      );\n    } else {\n      var args = [a, b, c, d, e, f];\n      var argIndex = 0;\n      error = new Error(\n        'Invariant Violation: ' +\n        format.replace(/%s/g, function() { return args[argIndex++]; })\n      );\n    }\n\n    error.framesToPop = 1; // we don't care about invariant's own frame\n    throw error;\n  }\n};\n\nmodule.exports = invariant;\n\n},{}],135:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule isEventSupported\n */\n\n\"use strict\";\n\nvar ExecutionEnvironment = _dereq_(\"./ExecutionEnvironment\");\n\nvar useHasFeature;\nif (ExecutionEnvironment.canUseDOM) {\n  useHasFeature =\n    document.implementation &&\n    document.implementation.hasFeature &&\n    // always returns true in newer browsers as per the standard.\n    // @see http://dom.spec.whatwg.org/#dom-domimplementation-hasfeature\n    document.implementation.hasFeature('', '') !== true;\n}\n\n/**\n * Checks if an event is supported in the current execution environment.\n *\n * NOTE: This will not work correctly for non-generic events such as `change`,\n * `reset`, `load`, `error`, and `select`.\n *\n * Borrows from Modernizr.\n *\n * @param {string} eventNameSuffix Event name, e.g. \"click\".\n * @param {?boolean} capture Check if the capture phase is supported.\n * @return {boolean} True if the event is supported.\n * @internal\n * @license Modernizr 3.0.0pre (Custom Build) | MIT\n */\nfunction isEventSupported(eventNameSuffix, capture) {\n  if (!ExecutionEnvironment.canUseDOM ||\n      capture && !('addEventListener' in document)) {\n    return false;\n  }\n\n  var eventName = 'on' + eventNameSuffix;\n  var isSupported = eventName in document;\n\n  if (!isSupported) {\n    var element = document.createElement('div');\n    element.setAttribute(eventName, 'return;');\n    isSupported = typeof element[eventName] === 'function';\n  }\n\n  if (!isSupported && useHasFeature && eventNameSuffix === 'wheel') {\n    // This is the only way to test support for the `wheel` event in IE9+.\n    isSupported = document.implementation.hasFeature('Events.wheel', '3.0');\n  }\n\n  return isSupported;\n}\n\nmodule.exports = isEventSupported;\n\n},{\"./ExecutionEnvironment\":22}],136:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule isNode\n * @typechecks\n */\n\n/**\n * @param {*} object The object to check.\n * @return {boolean} Whether or not the object is a DOM node.\n */\nfunction isNode(object) {\n  return !!(object && (\n    typeof Node === 'function' ? object instanceof Node :\n      typeof object === 'object' &&\n      typeof object.nodeType === 'number' &&\n      typeof object.nodeName === 'string'\n  ));\n}\n\nmodule.exports = isNode;\n\n},{}],137:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule isTextInputElement\n */\n\n\"use strict\";\n\n/**\n * @see http://www.whatwg.org/specs/web-apps/current-work/multipage/the-input-element.html#input-type-attr-summary\n */\nvar supportedInputTypes = {\n  'color': true,\n  'date': true,\n  'datetime': true,\n  'datetime-local': true,\n  'email': true,\n  'month': true,\n  'number': true,\n  'password': true,\n  'range': true,\n  'search': true,\n  'tel': true,\n  'text': true,\n  'time': true,\n  'url': true,\n  'week': true\n};\n\nfunction isTextInputElement(elem) {\n  return elem && (\n    (elem.nodeName === 'INPUT' && supportedInputTypes[elem.type]) ||\n    elem.nodeName === 'TEXTAREA'\n  );\n}\n\nmodule.exports = isTextInputElement;\n\n},{}],138:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule isTextNode\n * @typechecks\n */\n\nvar isNode = _dereq_(\"./isNode\");\n\n/**\n * @param {*} object The object to check.\n * @return {boolean} Whether or not the object is a DOM text node.\n */\nfunction isTextNode(object) {\n  return isNode(object) && object.nodeType == 3;\n}\n\nmodule.exports = isTextNode;\n\n},{\"./isNode\":136}],139:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule joinClasses\n * @typechecks static-only\n */\n\n\"use strict\";\n\n/**\n * Combines multiple className strings into one.\n * http://jsperf.com/joinclasses-args-vs-array\n *\n * @param {...?string} classes\n * @return {string}\n */\nfunction joinClasses(className/*, ... */) {\n  if (!className) {\n    className = '';\n  }\n  var nextClass;\n  var argLength = arguments.length;\n  if (argLength > 1) {\n    for (var ii = 1; ii < argLength; ii++) {\n      nextClass = arguments[ii];\n      nextClass && (className += ' ' + nextClass);\n    }\n  }\n  return className;\n}\n\nmodule.exports = joinClasses;\n\n},{}],140:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule keyMirror\n * @typechecks static-only\n */\n\n\"use strict\";\n\nvar invariant = _dereq_(\"./invariant\");\n\n/**\n * Constructs an enumeration with keys equal to their value.\n *\n * For example:\n *\n *   var COLORS = keyMirror({blue: null, red: null});\n *   var myColor = COLORS.blue;\n *   var isColorValid = !!COLORS[myColor];\n *\n * The last line could not be performed if the values of the generated enum were\n * not equal to their keys.\n *\n *   Input:  {key1: val1, key2: val2}\n *   Output: {key1: key1, key2: key2}\n *\n * @param {object} obj\n * @return {object}\n */\nvar keyMirror = function(obj) {\n  var ret = {};\n  var key;\n  (\"production\" !== \"development\" ? invariant(\n    obj instanceof Object && !Array.isArray(obj),\n    'keyMirror(...): Argument must be an object.'\n  ) : invariant(obj instanceof Object && !Array.isArray(obj)));\n  for (key in obj) {\n    if (!obj.hasOwnProperty(key)) {\n      continue;\n    }\n    ret[key] = key;\n  }\n  return ret;\n};\n\nmodule.exports = keyMirror;\n\n},{\"./invariant\":134}],141:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule keyOf\n */\n\n/**\n * Allows extraction of a minified key. Let's the build system minify keys\n * without loosing the ability to dynamically use key strings as values\n * themselves. Pass in an object with a single key/val pair and it will return\n * you the string key of that single record. Suppose you want to grab the\n * value for a key 'className' inside of an object. Key/val minification may\n * have aliased that key to be 'xa12'. keyOf({className: null}) will return\n * 'xa12' in that case. Resolve keys you want to use once at startup time, then\n * reuse those resolutions.\n */\nvar keyOf = function(oneKeyObj) {\n  var key;\n  for (key in oneKeyObj) {\n    if (!oneKeyObj.hasOwnProperty(key)) {\n      continue;\n    }\n    return key;\n  }\n  return null;\n};\n\n\nmodule.exports = keyOf;\n\n},{}],142:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule mapObject\n */\n\n\"use strict\";\n\n/**\n * For each key/value pair, invokes callback func and constructs a resulting\n * object which contains, for every key in obj, values that are the result of\n * of invoking the function:\n *\n *   func(value, key, iteration)\n *\n * Grepable names:\n *\n *   function objectMap()\n *   function objMap()\n *\n * @param {?object} obj Object to map keys over\n * @param {function} func Invoked for each key/val pair.\n * @param {?*} context\n * @return {?object} Result of mapping or null if obj is falsey\n */\nfunction mapObject(obj, func, context) {\n  if (!obj) {\n    return null;\n  }\n  var i = 0;\n  var ret = {};\n  for (var key in obj) {\n    if (obj.hasOwnProperty(key)) {\n      ret[key] = func.call(context, obj[key], key, i++);\n    }\n  }\n  return ret;\n}\n\nmodule.exports = mapObject;\n\n},{}],143:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule memoizeStringOnly\n * @typechecks static-only\n */\n\n\"use strict\";\n\n/**\n * Memoizes the return value of a function that accepts one string argument.\n *\n * @param {function} callback\n * @return {function}\n */\nfunction memoizeStringOnly(callback) {\n  var cache = {};\n  return function(string) {\n    if (cache.hasOwnProperty(string)) {\n      return cache[string];\n    } else {\n      return cache[string] = callback.call(this, string);\n    }\n  };\n}\n\nmodule.exports = memoizeStringOnly;\n\n},{}],144:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule merge\n */\n\n\"use strict\";\n\nvar mergeInto = _dereq_(\"./mergeInto\");\n\n/**\n * Shallow merges two structures into a return value, without mutating either.\n *\n * @param {?object} one Optional object with properties to merge from.\n * @param {?object} two Optional object with properties to merge from.\n * @return {object} The shallow extension of one by two.\n */\nvar merge = function(one, two) {\n  var result = {};\n  mergeInto(result, one);\n  mergeInto(result, two);\n  return result;\n};\n\nmodule.exports = merge;\n\n},{\"./mergeInto\":146}],145:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule mergeHelpers\n *\n * requiresPolyfills: Array.isArray\n */\n\n\"use strict\";\n\nvar invariant = _dereq_(\"./invariant\");\nvar keyMirror = _dereq_(\"./keyMirror\");\n\n/**\n * Maximum number of levels to traverse. Will catch circular structures.\n * @const\n */\nvar MAX_MERGE_DEPTH = 36;\n\n/**\n * We won't worry about edge cases like new String('x') or new Boolean(true).\n * Functions are considered terminals, and arrays are not.\n * @param {*} o The item/object/value to test.\n * @return {boolean} true iff the argument is a terminal.\n */\nvar isTerminal = function(o) {\n  return typeof o !== 'object' || o === null;\n};\n\nvar mergeHelpers = {\n\n  MAX_MERGE_DEPTH: MAX_MERGE_DEPTH,\n\n  isTerminal: isTerminal,\n\n  /**\n   * Converts null/undefined values into empty object.\n   *\n   * @param {?Object=} arg Argument to be normalized (nullable optional)\n   * @return {!Object}\n   */\n  normalizeMergeArg: function(arg) {\n    return arg === undefined || arg === null ? {} : arg;\n  },\n\n  /**\n   * If merging Arrays, a merge strategy *must* be supplied. If not, it is\n   * likely the caller's fault. If this function is ever called with anything\n   * but `one` and `two` being `Array`s, it is the fault of the merge utilities.\n   *\n   * @param {*} one Array to merge into.\n   * @param {*} two Array to merge from.\n   */\n  checkMergeArrayArgs: function(one, two) {\n    (\"production\" !== \"development\" ? invariant(\n      Array.isArray(one) && Array.isArray(two),\n      'Tried to merge arrays, instead got %s and %s.',\n      one,\n      two\n    ) : invariant(Array.isArray(one) && Array.isArray(two)));\n  },\n\n  /**\n   * @param {*} one Object to merge into.\n   * @param {*} two Object to merge from.\n   */\n  checkMergeObjectArgs: function(one, two) {\n    mergeHelpers.checkMergeObjectArg(one);\n    mergeHelpers.checkMergeObjectArg(two);\n  },\n\n  /**\n   * @param {*} arg\n   */\n  checkMergeObjectArg: function(arg) {\n    (\"production\" !== \"development\" ? invariant(\n      !isTerminal(arg) && !Array.isArray(arg),\n      'Tried to merge an object, instead got %s.',\n      arg\n    ) : invariant(!isTerminal(arg) && !Array.isArray(arg)));\n  },\n\n  /**\n   * @param {*} arg\n   */\n  checkMergeIntoObjectArg: function(arg) {\n    (\"production\" !== \"development\" ? invariant(\n      (!isTerminal(arg) || typeof arg === 'function') && !Array.isArray(arg),\n      'Tried to merge into an object, instead got %s.',\n      arg\n    ) : invariant((!isTerminal(arg) || typeof arg === 'function') && !Array.isArray(arg)));\n  },\n\n  /**\n   * Checks that a merge was not given a circular object or an object that had\n   * too great of depth.\n   *\n   * @param {number} Level of recursion to validate against maximum.\n   */\n  checkMergeLevel: function(level) {\n    (\"production\" !== \"development\" ? invariant(\n      level < MAX_MERGE_DEPTH,\n      'Maximum deep merge depth exceeded. You may be attempting to merge ' +\n      'circular structures in an unsupported way.'\n    ) : invariant(level < MAX_MERGE_DEPTH));\n  },\n\n  /**\n   * Checks that the supplied merge strategy is valid.\n   *\n   * @param {string} Array merge strategy.\n   */\n  checkArrayStrategy: function(strategy) {\n    (\"production\" !== \"development\" ? invariant(\n      strategy === undefined || strategy in mergeHelpers.ArrayStrategies,\n      'You must provide an array strategy to deep merge functions to ' +\n      'instruct the deep merge how to resolve merging two arrays.'\n    ) : invariant(strategy === undefined || strategy in mergeHelpers.ArrayStrategies));\n  },\n\n  /**\n   * Set of possible behaviors of merge algorithms when encountering two Arrays\n   * that must be merged together.\n   * - `clobber`: The left `Array` is ignored.\n   * - `indexByIndex`: The result is achieved by recursively deep merging at\n   *   each index. (not yet supported.)\n   */\n  ArrayStrategies: keyMirror({\n    Clobber: true,\n    IndexByIndex: true\n  })\n\n};\n\nmodule.exports = mergeHelpers;\n\n},{\"./invariant\":134,\"./keyMirror\":140}],146:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule mergeInto\n * @typechecks static-only\n */\n\n\"use strict\";\n\nvar mergeHelpers = _dereq_(\"./mergeHelpers\");\n\nvar checkMergeObjectArg = mergeHelpers.checkMergeObjectArg;\nvar checkMergeIntoObjectArg = mergeHelpers.checkMergeIntoObjectArg;\n\n/**\n * Shallow merges two structures by mutating the first parameter.\n *\n * @param {object|function} one Object to be merged into.\n * @param {?object} two Optional object with properties to merge from.\n */\nfunction mergeInto(one, two) {\n  checkMergeIntoObjectArg(one);\n  if (two != null) {\n    checkMergeObjectArg(two);\n    for (var key in two) {\n      if (!two.hasOwnProperty(key)) {\n        continue;\n      }\n      one[key] = two[key];\n    }\n  }\n}\n\nmodule.exports = mergeInto;\n\n},{\"./mergeHelpers\":145}],147:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule mixInto\n */\n\n\"use strict\";\n\n/**\n * Simply copies properties to the prototype.\n */\nvar mixInto = function(constructor, methodBag) {\n  var methodName;\n  for (methodName in methodBag) {\n    if (!methodBag.hasOwnProperty(methodName)) {\n      continue;\n    }\n    constructor.prototype[methodName] = methodBag[methodName];\n  }\n};\n\nmodule.exports = mixInto;\n\n},{}],148:[function(_dereq_,module,exports){\n/**\n * Copyright 2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule monitorCodeUse\n */\n\n\"use strict\";\n\nvar invariant = _dereq_(\"./invariant\");\n\n/**\n * Provides open-source compatible instrumentation for monitoring certain API\n * uses before we're ready to issue a warning or refactor. It accepts an event\n * name which may only contain the characters [a-z0-9_] and an optional data\n * object with further information.\n */\n\nfunction monitorCodeUse(eventName, data) {\n  (\"production\" !== \"development\" ? invariant(\n    eventName && !/[^a-z0-9_]/.test(eventName),\n    'You must provide an eventName using only the characters [a-z0-9_]'\n  ) : invariant(eventName && !/[^a-z0-9_]/.test(eventName)));\n}\n\nmodule.exports = monitorCodeUse;\n\n},{\"./invariant\":134}],149:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule onlyChild\n */\n\"use strict\";\n\nvar ReactDescriptor = _dereq_(\"./ReactDescriptor\");\n\nvar invariant = _dereq_(\"./invariant\");\n\n/**\n * Returns the first child in a collection of children and verifies that there\n * is only one child in the collection. The current implementation of this\n * function assumes that a single child gets passed without a wrapper, but the\n * purpose of this helper function is to abstract away the particular structure\n * of children.\n *\n * @param {?object} children Child collection structure.\n * @return {ReactComponent} The first and only `ReactComponent` contained in the\n * structure.\n */\nfunction onlyChild(children) {\n  (\"production\" !== \"development\" ? invariant(\n    ReactDescriptor.isValidDescriptor(children),\n    'onlyChild must be passed a children with exactly one child.'\n  ) : invariant(ReactDescriptor.isValidDescriptor(children)));\n  return children;\n}\n\nmodule.exports = onlyChild;\n\n},{\"./ReactDescriptor\":56,\"./invariant\":134}],150:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule performance\n * @typechecks\n */\n\n\"use strict\";\n\nvar ExecutionEnvironment = _dereq_(\"./ExecutionEnvironment\");\n\nvar performance;\n\nif (ExecutionEnvironment.canUseDOM) {\n  performance =\n    window.performance ||\n    window.msPerformance ||\n    window.webkitPerformance;\n}\n\nmodule.exports = performance || {};\n\n},{\"./ExecutionEnvironment\":22}],151:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule performanceNow\n * @typechecks\n */\n\nvar performance = _dereq_(\"./performance\");\n\n/**\n * Detect if we can use `window.performance.now()` and gracefully fallback to\n * `Date.now()` if it doesn't exist. We need to support Firefox < 15 for now\n * because of Facebook's testing infrastructure.\n */\nif (!performance || !performance.now) {\n  performance = Date;\n}\n\nvar performanceNow = performance.now.bind(performance);\n\nmodule.exports = performanceNow;\n\n},{\"./performance\":150}],152:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule setInnerHTML\n */\n\n\"use strict\";\n\nvar ExecutionEnvironment = _dereq_(\"./ExecutionEnvironment\");\n\n/**\n * Set the innerHTML property of a node, ensuring that whitespace is preserved\n * even in IE8.\n *\n * @param {DOMElement} node\n * @param {string} html\n * @internal\n */\nvar setInnerHTML = function(node, html) {\n  node.innerHTML = html;\n};\n\nif (ExecutionEnvironment.canUseDOM) {\n  // IE8: When updating a just created node with innerHTML only leading\n  // whitespace is removed. When updating an existing node with innerHTML\n  // whitespace in root TextNodes is also collapsed.\n  // @see quirksmode.org/bugreports/archives/2004/11/innerhtml_and_t.html\n\n  // Feature detection; only IE8 is known to behave improperly like this.\n  var testElement = document.createElement('div');\n  testElement.innerHTML = ' ';\n  if (testElement.innerHTML === '') {\n    setInnerHTML = function(node, html) {\n      // Magic theory: IE8 supposedly differentiates between added and updated\n      // nodes when processing innerHTML, innerHTML on updated nodes suffers\n      // from worse whitespace behavior. Re-adding a node like this triggers\n      // the initial and more favorable whitespace behavior.\n      // TODO: What to do on a detached node?\n      if (node.parentNode) {\n        node.parentNode.replaceChild(node, node);\n      }\n\n      // We also implement a workaround for non-visible tags disappearing into\n      // thin air on IE8, this only happens if there is no visible text\n      // in-front of the non-visible tags. Piggyback on the whitespace fix\n      // and simply check if any non-visible tags appear in the source.\n      if (html.match(/^[ \\r\\n\\t\\f]/) ||\n          html[0] === '<' && (\n            html.indexOf('<noscript') !== -1 ||\n            html.indexOf('<script') !== -1 ||\n            html.indexOf('<style') !== -1 ||\n            html.indexOf('<meta') !== -1 ||\n            html.indexOf('<link') !== -1)) {\n        // Recover leading whitespace by temporarily prepending any character.\n        // \\uFEFF has the potential advantage of being zero-width/invisible.\n        node.innerHTML = '\\uFEFF' + html;\n\n        // deleteData leaves an empty `TextNode` which offsets the index of all\n        // children. Definitely want to avoid this.\n        var textNode = node.firstChild;\n        if (textNode.data.length === 1) {\n          node.removeChild(textNode);\n        } else {\n          textNode.deleteData(0, 1);\n        }\n      } else {\n        node.innerHTML = html;\n      }\n    };\n  }\n}\n\nmodule.exports = setInnerHTML;\n\n},{\"./ExecutionEnvironment\":22}],153:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule shallowEqual\n */\n\n\"use strict\";\n\n/**\n * Performs equality by iterating through keys on an object and returning\n * false when any key has values which are not strictly equal between\n * objA and objB. Returns true when the values of all keys are strictly equal.\n *\n * @return {boolean}\n */\nfunction shallowEqual(objA, objB) {\n  if (objA === objB) {\n    return true;\n  }\n  var key;\n  // Test for A's keys different from B.\n  for (key in objA) {\n    if (objA.hasOwnProperty(key) &&\n        (!objB.hasOwnProperty(key) || objA[key] !== objB[key])) {\n      return false;\n    }\n  }\n  // Test for B'a keys missing from A.\n  for (key in objB) {\n    if (objB.hasOwnProperty(key) && !objA.hasOwnProperty(key)) {\n      return false;\n    }\n  }\n  return true;\n}\n\nmodule.exports = shallowEqual;\n\n},{}],154:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule shouldUpdateReactComponent\n * @typechecks static-only\n */\n\n\"use strict\";\n\n/**\n * Given a `prevDescriptor` and `nextDescriptor`, determines if the existing\n * instance should be updated as opposed to being destroyed or replaced by a new\n * instance. Both arguments are descriptors. This ensures that this logic can\n * operate on stateless trees without any backing instance.\n *\n * @param {?object} prevDescriptor\n * @param {?object} nextDescriptor\n * @return {boolean} True if the existing instance should be updated.\n * @protected\n */\nfunction shouldUpdateReactComponent(prevDescriptor, nextDescriptor) {\n  if (prevDescriptor && nextDescriptor &&\n      prevDescriptor.type === nextDescriptor.type && (\n        (prevDescriptor.props && prevDescriptor.props.key) ===\n        (nextDescriptor.props && nextDescriptor.props.key)\n      ) && prevDescriptor._owner === nextDescriptor._owner) {\n    return true;\n  }\n  return false;\n}\n\nmodule.exports = shouldUpdateReactComponent;\n\n},{}],155:[function(_dereq_,module,exports){\n/**\n * Copyright 2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule toArray\n * @typechecks\n */\n\nvar invariant = _dereq_(\"./invariant\");\n\n/**\n * Convert array-like objects to arrays.\n *\n * This API assumes the caller knows the contents of the data type. For less\n * well defined inputs use createArrayFrom.\n *\n * @param {object|function|filelist} obj\n * @return {array}\n */\nfunction toArray(obj) {\n  var length = obj.length;\n\n  // Some browse builtin objects can report typeof 'function' (e.g. NodeList in\n  // old versions of Safari).\n  (\"production\" !== \"development\" ? invariant(\n    !Array.isArray(obj) &&\n    (typeof obj === 'object' || typeof obj === 'function'),\n    'toArray: Array-like object expected'\n  ) : invariant(!Array.isArray(obj) &&\n  (typeof obj === 'object' || typeof obj === 'function')));\n\n  (\"production\" !== \"development\" ? invariant(\n    typeof length === 'number',\n    'toArray: Object needs a length property'\n  ) : invariant(typeof length === 'number'));\n\n  (\"production\" !== \"development\" ? invariant(\n    length === 0 ||\n    (length - 1) in obj,\n    'toArray: Object should have keys for indices'\n  ) : invariant(length === 0 ||\n  (length - 1) in obj));\n\n  // Old IE doesn't give collections access to hasOwnProperty. Assume inputs\n  // without method will throw during the slice call and skip straight to the\n  // fallback.\n  if (obj.hasOwnProperty) {\n    try {\n      return Array.prototype.slice.call(obj);\n    } catch (e) {\n      // IE < 9 does not support Array#slice on collections objects\n    }\n  }\n\n  // Fall back to copying key by key. This assumes all keys have a value,\n  // so will not preserve sparsely populated inputs.\n  var ret = Array(length);\n  for (var ii = 0; ii < length; ii++) {\n    ret[ii] = obj[ii];\n  }\n  return ret;\n}\n\nmodule.exports = toArray;\n\n},{\"./invariant\":134}],156:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule traverseAllChildren\n */\n\n\"use strict\";\n\nvar ReactInstanceHandles = _dereq_(\"./ReactInstanceHandles\");\nvar ReactTextComponent = _dereq_(\"./ReactTextComponent\");\n\nvar invariant = _dereq_(\"./invariant\");\n\nvar SEPARATOR = ReactInstanceHandles.SEPARATOR;\nvar SUBSEPARATOR = ':';\n\n/**\n * TODO: Test that:\n * 1. `mapChildren` transforms strings and numbers into `ReactTextComponent`.\n * 2. it('should fail when supplied duplicate key', function() {\n * 3. That a single child and an array with one item have the same key pattern.\n * });\n */\n\nvar userProvidedKeyEscaperLookup = {\n  '=': '=0',\n  '.': '=1',\n  ':': '=2'\n};\n\nvar userProvidedKeyEscapeRegex = /[=.:]/g;\n\nfunction userProvidedKeyEscaper(match) {\n  return userProvidedKeyEscaperLookup[match];\n}\n\n/**\n * Generate a key string that identifies a component within a set.\n *\n * @param {*} component A component that could contain a manual key.\n * @param {number} index Index that is used if a manual key is not provided.\n * @return {string}\n */\nfunction getComponentKey(component, index) {\n  if (component && component.props && component.props.key != null) {\n    // Explicit key\n    return wrapUserProvidedKey(component.props.key);\n  }\n  // Implicit key determined by the index in the set\n  return index.toString(36);\n}\n\n/**\n * Escape a component key so that it is safe to use in a reactid.\n *\n * @param {*} key Component key to be escaped.\n * @return {string} An escaped string.\n */\nfunction escapeUserProvidedKey(text) {\n  return ('' + text).replace(\n    userProvidedKeyEscapeRegex,\n    userProvidedKeyEscaper\n  );\n}\n\n/**\n * Wrap a `key` value explicitly provided by the user to distinguish it from\n * implicitly-generated keys generated by a component's index in its parent.\n *\n * @param {string} key Value of a user-provided `key` attribute\n * @return {string}\n */\nfunction wrapUserProvidedKey(key) {\n  return '$' + escapeUserProvidedKey(key);\n}\n\n/**\n * @param {?*} children Children tree container.\n * @param {!string} nameSoFar Name of the key path so far.\n * @param {!number} indexSoFar Number of children encountered until this point.\n * @param {!function} callback Callback to invoke with each child found.\n * @param {?*} traverseContext Used to pass information throughout the traversal\n * process.\n * @return {!number} The number of children in this subtree.\n */\nvar traverseAllChildrenImpl =\n  function(children, nameSoFar, indexSoFar, callback, traverseContext) {\n    var subtreeCount = 0;  // Count of children found in the current subtree.\n    if (Array.isArray(children)) {\n      for (var i = 0; i < children.length; i++) {\n        var child = children[i];\n        var nextName = (\n          nameSoFar +\n          (nameSoFar ? SUBSEPARATOR : SEPARATOR) +\n          getComponentKey(child, i)\n        );\n        var nextIndex = indexSoFar + subtreeCount;\n        subtreeCount += traverseAllChildrenImpl(\n          child,\n          nextName,\n          nextIndex,\n          callback,\n          traverseContext\n        );\n      }\n    } else {\n      var type = typeof children;\n      var isOnlyChild = nameSoFar === '';\n      // If it's the only child, treat the name as if it was wrapped in an array\n      // so that it's consistent if the number of children grows\n      var storageName =\n        isOnlyChild ? SEPARATOR + getComponentKey(children, 0) : nameSoFar;\n      if (children == null || type === 'boolean') {\n        // All of the above are perceived as null.\n        callback(traverseContext, null, storageName, indexSoFar);\n        subtreeCount = 1;\n      } else if (children.type && children.type.prototype &&\n                 children.type.prototype.mountComponentIntoNode) {\n        callback(traverseContext, children, storageName, indexSoFar);\n        subtreeCount = 1;\n      } else {\n        if (type === 'object') {\n          (\"production\" !== \"development\" ? invariant(\n            !children || children.nodeType !== 1,\n            'traverseAllChildren(...): Encountered an invalid child; DOM ' +\n            'elements are not valid children of React components.'\n          ) : invariant(!children || children.nodeType !== 1));\n          for (var key in children) {\n            if (children.hasOwnProperty(key)) {\n              subtreeCount += traverseAllChildrenImpl(\n                children[key],\n                (\n                  nameSoFar + (nameSoFar ? SUBSEPARATOR : SEPARATOR) +\n                  wrapUserProvidedKey(key) + SUBSEPARATOR +\n                  getComponentKey(children[key], 0)\n                ),\n                indexSoFar + subtreeCount,\n                callback,\n                traverseContext\n              );\n            }\n          }\n        } else if (type === 'string') {\n          var normalizedText = ReactTextComponent(children);\n          callback(traverseContext, normalizedText, storageName, indexSoFar);\n          subtreeCount += 1;\n        } else if (type === 'number') {\n          var normalizedNumber = ReactTextComponent('' + children);\n          callback(traverseContext, normalizedNumber, storageName, indexSoFar);\n          subtreeCount += 1;\n        }\n      }\n    }\n    return subtreeCount;\n  };\n\n/**\n * Traverses children that are typically specified as `props.children`, but\n * might also be specified through attributes:\n *\n * - `traverseAllChildren(this.props.children, ...)`\n * - `traverseAllChildren(this.props.leftPanelChildren, ...)`\n *\n * The `traverseContext` is an optional argument that is passed through the\n * entire traversal. It can be used to store accumulations or anything else that\n * the callback might find relevant.\n *\n * @param {?*} children Children tree object.\n * @param {!function} callback To invoke upon traversing each child.\n * @param {?*} traverseContext Context for traversal.\n * @return {!number} The number of children in this subtree.\n */\nfunction traverseAllChildren(children, callback, traverseContext) {\n  if (children == null) {\n    return 0;\n  }\n\n  return traverseAllChildrenImpl(children, '', 0, callback, traverseContext);\n}\n\nmodule.exports = traverseAllChildren;\n\n},{\"./ReactInstanceHandles\":64,\"./ReactTextComponent\":83,\"./invariant\":134}],157:[function(_dereq_,module,exports){\n/**\n * Copyright 2013-2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule update\n */\n\n\"use strict\";\n\nvar copyProperties = _dereq_(\"./copyProperties\");\nvar keyOf = _dereq_(\"./keyOf\");\nvar invariant = _dereq_(\"./invariant\");\n\nfunction shallowCopy(x) {\n  if (Array.isArray(x)) {\n    return x.concat();\n  } else if (x && typeof x === 'object') {\n    return copyProperties(new x.constructor(), x);\n  } else {\n    return x;\n  }\n}\n\nvar COMMAND_PUSH = keyOf({$push: null});\nvar COMMAND_UNSHIFT = keyOf({$unshift: null});\nvar COMMAND_SPLICE = keyOf({$splice: null});\nvar COMMAND_SET = keyOf({$set: null});\nvar COMMAND_MERGE = keyOf({$merge: null});\nvar COMMAND_APPLY = keyOf({$apply: null});\n\nvar ALL_COMMANDS_LIST = [\n  COMMAND_PUSH,\n  COMMAND_UNSHIFT,\n  COMMAND_SPLICE,\n  COMMAND_SET,\n  COMMAND_MERGE,\n  COMMAND_APPLY\n];\n\nvar ALL_COMMANDS_SET = {};\n\nALL_COMMANDS_LIST.forEach(function(command) {\n  ALL_COMMANDS_SET[command] = true;\n});\n\nfunction invariantArrayCase(value, spec, command) {\n  (\"production\" !== \"development\" ? invariant(\n    Array.isArray(value),\n    'update(): expected target of %s to be an array; got %s.',\n    command,\n    value\n  ) : invariant(Array.isArray(value)));\n  var specValue = spec[command];\n  (\"production\" !== \"development\" ? invariant(\n    Array.isArray(specValue),\n    'update(): expected spec of %s to be an array; got %s. ' +\n    'Did you forget to wrap your parameter in an array?',\n    command,\n    specValue\n  ) : invariant(Array.isArray(specValue)));\n}\n\nfunction update(value, spec) {\n  (\"production\" !== \"development\" ? invariant(\n    typeof spec === 'object',\n    'update(): You provided a key path to update() that did not contain one ' +\n    'of %s. Did you forget to include {%s: ...}?',\n    ALL_COMMANDS_LIST.join(', '),\n    COMMAND_SET\n  ) : invariant(typeof spec === 'object'));\n\n  if (spec.hasOwnProperty(COMMAND_SET)) {\n    (\"production\" !== \"development\" ? invariant(\n      Object.keys(spec).length === 1,\n      'Cannot have more than one key in an object with %s',\n      COMMAND_SET\n    ) : invariant(Object.keys(spec).length === 1));\n\n    return spec[COMMAND_SET];\n  }\n\n  var nextValue = shallowCopy(value);\n\n  if (spec.hasOwnProperty(COMMAND_MERGE)) {\n    var mergeObj = spec[COMMAND_MERGE];\n    (\"production\" !== \"development\" ? invariant(\n      mergeObj && typeof mergeObj === 'object',\n      'update(): %s expects a spec of type \\'object\\'; got %s',\n      COMMAND_MERGE,\n      mergeObj\n    ) : invariant(mergeObj && typeof mergeObj === 'object'));\n    (\"production\" !== \"development\" ? invariant(\n      nextValue && typeof nextValue === 'object',\n      'update(): %s expects a target of type \\'object\\'; got %s',\n      COMMAND_MERGE,\n      nextValue\n    ) : invariant(nextValue && typeof nextValue === 'object'));\n    copyProperties(nextValue, spec[COMMAND_MERGE]);\n  }\n\n  if (spec.hasOwnProperty(COMMAND_PUSH)) {\n    invariantArrayCase(value, spec, COMMAND_PUSH);\n    spec[COMMAND_PUSH].forEach(function(item) {\n      nextValue.push(item);\n    });\n  }\n\n  if (spec.hasOwnProperty(COMMAND_UNSHIFT)) {\n    invariantArrayCase(value, spec, COMMAND_UNSHIFT);\n    spec[COMMAND_UNSHIFT].forEach(function(item) {\n      nextValue.unshift(item);\n    });\n  }\n\n  if (spec.hasOwnProperty(COMMAND_SPLICE)) {\n    (\"production\" !== \"development\" ? invariant(\n      Array.isArray(value),\n      'Expected %s target to be an array; got %s',\n      COMMAND_SPLICE,\n      value\n    ) : invariant(Array.isArray(value)));\n    (\"production\" !== \"development\" ? invariant(\n      Array.isArray(spec[COMMAND_SPLICE]),\n      'update(): expected spec of %s to be an array of arrays; got %s. ' +\n      'Did you forget to wrap your parameters in an array?',\n      COMMAND_SPLICE,\n      spec[COMMAND_SPLICE]\n    ) : invariant(Array.isArray(spec[COMMAND_SPLICE])));\n    spec[COMMAND_SPLICE].forEach(function(args) {\n      (\"production\" !== \"development\" ? invariant(\n        Array.isArray(args),\n        'update(): expected spec of %s to be an array of arrays; got %s. ' +\n        'Did you forget to wrap your parameters in an array?',\n        COMMAND_SPLICE,\n        spec[COMMAND_SPLICE]\n      ) : invariant(Array.isArray(args)));\n      nextValue.splice.apply(nextValue, args);\n    });\n  }\n\n  if (spec.hasOwnProperty(COMMAND_APPLY)) {\n    (\"production\" !== \"development\" ? invariant(\n      typeof spec[COMMAND_APPLY] === 'function',\n      'update(): expected spec of %s to be a function; got %s.',\n      COMMAND_APPLY,\n      spec[COMMAND_APPLY]\n    ) : invariant(typeof spec[COMMAND_APPLY] === 'function'));\n    nextValue = spec[COMMAND_APPLY](nextValue);\n  }\n\n  for (var k in spec) {\n    if (!(ALL_COMMANDS_SET.hasOwnProperty(k) && ALL_COMMANDS_SET[k])) {\n      nextValue[k] = update(value[k], spec[k]);\n    }\n  }\n\n  return nextValue;\n}\n\nmodule.exports = update;\n\n},{\"./copyProperties\":110,\"./invariant\":134,\"./keyOf\":141}],158:[function(_dereq_,module,exports){\n/**\n * Copyright 2014 Facebook, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @providesModule warning\n */\n\n\"use strict\";\n\nvar emptyFunction = _dereq_(\"./emptyFunction\");\n\n/**\n * Similar to invariant but only logs a warning if the condition is not met.\n * This can be used to log issues in development environments in critical\n * paths. Removing the logging code for production environments will keep the\n * same logic and follow the same code paths.\n */\n\nvar warning = emptyFunction;\n\nif (\"production\" !== \"development\") {\n  warning = function(condition, format ) {var args=Array.prototype.slice.call(arguments,2);\n    if (format === undefined) {\n      throw new Error(\n        '`warning(condition, format, ...args)` requires a warning ' +\n        'message argument'\n      );\n    }\n\n    if (!condition) {\n      var argIndex = 0;\n      console.warn('Warning: ' + format.replace(/%s/g, function()  {return args[argIndex++];}));\n    }\n  };\n}\n\nmodule.exports = warning;\n\n},{\"./emptyFunction\":116}]},{},[88])\n(88)\n});"
  },
  {
    "path": "r2/r2/public/static/js/lib/reddit-client-lib.js",
    "content": "/**\n * reddit-client-lib\n * @version v0.0.0\n * DO NOT EDIT THIS FILE DIRECTLY! Edit the source at:\n * @source https://github.com/reddit/reddit-client-lib\n */\n(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require==\"function\"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error(\"Cannot find module '\"+o+\"'\");throw f.code=\"MODULE_NOT_FOUND\",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require==\"function\"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){\n'use strict';\n\nvar Tracker = module.exports = function(options) {\n};\n\nfunction randomString(len) {\n  var id = [];\n  var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';\n\n  for (var i = 0; i < len; i++) {\n    id.push(chars.charAt(Math.floor(Math.random() * chars.length)));\n  }\n\n  return id.join('');\n}\n\nfunction getCookies(/* names */) {\n  var names = Array.prototype.slice.call(arguments);\n  var ret = {};\n\n  var cookies = document.cookie.split(';');\n  for (var i = 0; i < cookies.length; i++) {\n    var cData = cookies[i].split('=', 2);\n    var cName = cData[0].replace(/^\\s+/, '');\n\n    if (names.indexOf(cName) !== -1) {\n      ret[cName] = cData[1];\n    }\n  }\n\n  return ret;\n}\n\nfunction setCookie(name, value, expires) {\n  document.cookie = name + '=' + value +\n    '; expires=' + expires.toGMTString() + ';';\n}\n\n// Retrieve (or set and return) an ID for this user's logged out session.\nfunction getLoggedOutData() {\n  // Do not return a logged out ID if the user is logged in, for privacy purpose\n  if (window.r && window.r.config && window.r.config.logged) {\n    return {};\n  }\n\n  var cookies = getCookies('loid', 'loidcreated');\n  if (cookies.loid) {\n    return cookies;\n  }\n\n  var loggedOutId = randomString(18);\n  var created = (new Date()).toISOString();\n  var expires = new Date();\n  expires.setFullYear(expires.getFullYear() + 2);\n  setCookie('loid', loggedOutId, expires);\n  setCookie('loidcreated', created, expires);\n\n  return getCookies('loid', 'loidcreated');\n}\n\nTracker.prototype.getTrackingData = function() {\n  return getLoggedOutData();\n};\n\n// Export to `window`, for browser wo/browserify.\nif (typeof window !== 'undefined') {\n  var redditlib = (window.redditlib = window.redditlib || {});\n\n  redditlib.Tracker = Tracker;\n}\n\n},{}]},{},[1])\n//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIm5vZGVfbW9kdWxlcy9icm93c2VyaWZ5L25vZGVfbW9kdWxlcy9icm93c2VyLXBhY2svX3ByZWx1ZGUuanMiLCJzcmMvdHJhY2tpbmcuanMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7QUNBQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBIiwiZmlsZSI6ImdlbmVyYXRlZC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzQ29udGVudCI6WyIoZnVuY3Rpb24gZSh0LG4scil7ZnVuY3Rpb24gcyhvLHUpe2lmKCFuW29dKXtpZighdFtvXSl7dmFyIGE9dHlwZW9mIHJlcXVpcmU9PVwiZnVuY3Rpb25cIiYmcmVxdWlyZTtpZighdSYmYSlyZXR1cm4gYShvLCEwKTtpZihpKXJldHVybiBpKG8sITApO3ZhciBmPW5ldyBFcnJvcihcIkNhbm5vdCBmaW5kIG1vZHVsZSAnXCIrbytcIidcIik7dGhyb3cgZi5jb2RlPVwiTU9EVUxFX05PVF9GT1VORFwiLGZ9dmFyIGw9bltvXT17ZXhwb3J0czp7fX07dFtvXVswXS5jYWxsKGwuZXhwb3J0cyxmdW5jdGlvbihlKXt2YXIgbj10W29dWzFdW2VdO3JldHVybiBzKG4/bjplKX0sbCxsLmV4cG9ydHMsZSx0LG4scil9cmV0dXJuIG5bb10uZXhwb3J0c312YXIgaT10eXBlb2YgcmVxdWlyZT09XCJmdW5jdGlvblwiJiZyZXF1aXJlO2Zvcih2YXIgbz0wO288ci5sZW5ndGg7bysrKXMocltvXSk7cmV0dXJuIHN9KSIsIid1c2Ugc3RyaWN0JztcblxudmFyIFRyYWNrZXIgPSBtb2R1bGUuZXhwb3J0cyA9IGZ1bmN0aW9uKG9wdGlvbnMpIHtcbn07XG5cbmZ1bmN0aW9uIHJhbmRvbVN0cmluZyhsZW4pIHtcbiAgdmFyIGlkID0gW107XG4gIHZhciBjaGFycyA9ICdBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6MDEyMzQ1Njc4OSc7XG5cbiAgZm9yICh2YXIgaSA9IDA7IGkgPCBsZW47IGkrKykge1xuICAgIGlkLnB1c2goY2hhcnMuY2hhckF0KE1hdGguZmxvb3IoTWF0aC5yYW5kb20oKSAqIGNoYXJzLmxlbmd0aCkpKTtcbiAgfVxuXG4gIHJldHVybiBpZC5qb2luKCcnKTtcbn1cblxuZnVuY3Rpb24gZ2V0Q29va2llcygvKiBuYW1lcyAqLykge1xuICB2YXIgbmFtZXMgPSBBcnJheS5wcm90b3R5cGUuc2xpY2UuY2FsbChhcmd1bWVudHMpO1xuICB2YXIgcmV0ID0ge307XG5cbiAgdmFyIGNvb2tpZXMgPSBkb2N1bWVudC5jb29raWUuc3BsaXQoJzsnKTtcbiAgZm9yICh2YXIgaSA9IDA7IGkgPCBjb29raWVzLmxlbmd0aDsgaSsrKSB7XG4gICAgdmFyIGNEYXRhID0gY29va2llc1tpXS5zcGxpdCgnPScsIDIpO1xuICAgIHZhciBjTmFtZSA9IGNEYXRhWzBdLnJlcGxhY2UoL15cXHMrLywgJycpO1xuXG4gICAgaWYgKG5hbWVzLmluZGV4T2YoY05hbWUpICE9PSAtMSkge1xuICAgICAgcmV0W2NOYW1lXSA9IGNEYXRhWzFdO1xuICAgIH1cbiAgfVxuXG4gIHJldHVybiByZXQ7XG59XG5cbmZ1bmN0aW9uIHNldENvb2tpZShuYW1lLCB2YWx1ZSwgZXhwaXJlcykge1xuICBkb2N1bWVudC5jb29raWUgPSBuYW1lICsgJz0nICsgdmFsdWUgK1xuICAgICc7IGV4cGlyZXM9JyArIGV4cGlyZXMudG9HTVRTdHJpbmcoKSArICc7Jztcbn1cblxuLy8gUmV0cmlldmUgKG9yIHNldCBhbmQgcmV0dXJuKSBhbiBJRCBmb3IgdGhpcyB1c2VyJ3MgbG9nZ2VkIG91dCBzZXNzaW9uLlxuZnVuY3Rpb24gZ2V0TG9nZ2VkT3V0RGF0YSgpIHtcbiAgLy8gRG8gbm90IHJldHVybiBhIGxvZ2dlZCBvdXQgSUQgaWYgdGhlIHVzZXIgaXMgbG9nZ2VkIGluLCBmb3IgcHJpdmFjeSBwdXJwb3NlXG4gIGlmICh3aW5kb3cucmVkZGl0ICYmIHdpbmRvdy5yZWRkaXQubG9nZ2VkKSB7XG4gICAgcmV0dXJuIHt9O1xuICB9XG5cbiAgdmFyIGNvb2tpZXMgPSBnZXRDb29raWVzKCdsb2lkJywgJ2xvaWRjcmVhdGVkJyk7XG4gIGlmIChjb29raWVzLmxvaWQpIHtcbiAgICByZXR1cm4gY29va2llcztcbiAgfVxuXG4gIHZhciBsb2dnZWRPdXRJZCA9IHJhbmRvbVN0cmluZygxOCk7XG4gIHZhciBjcmVhdGVkID0gKG5ldyBEYXRlKCkpLnRvSVNPU3RyaW5nKCk7XG4gIHZhciBleHBpcmVzID0gbmV3IERhdGUoKTtcbiAgZXhwaXJlcy5zZXRGdWxsWWVhcihleHBpcmVzLmdldEZ1bGxZZWFyKCkgKyAyKTtcbiAgc2V0Q29va2llKCdsb2lkJywgbG9nZ2VkT3V0SWQsIGV4cGlyZXMpO1xuICBzZXRDb29raWUoJ2xvaWRjcmVhdGVkJywgY3JlYXRlZCwgZXhwaXJlcyk7XG5cbiAgcmV0dXJuIGdldENvb2tpZXMoJ2xvaWQnLCAnbG9pZGNyZWF0ZWQnKTtcbn1cblxuVHJhY2tlci5wcm90b3R5cGUuZ2V0VHJhY2tpbmdEYXRhID0gZnVuY3Rpb24oKSB7XG4gIHJldHVybiBnZXRMb2dnZWRPdXREYXRhKCk7XG59O1xuXG4vLyBFeHBvcnQgdG8gYHdpbmRvd2AsIGZvciBicm93c2VyIHdvL2Jyb3dzZXJpZnkuXG5pZiAodHlwZW9mIHdpbmRvdyAhPT0gJ3VuZGVmaW5lZCcpIHtcbiAgdmFyIHJlZGRpdGxpYiA9ICh3aW5kb3cucmVkZGl0bGliID0gd2luZG93LnJlZGRpdGxpYiB8fCB7fSk7XG5cbiAgcmVkZGl0bGliLlRyYWNrZXIgPSBUcmFja2VyO1xufVxuIl19\n"
  },
  {
    "path": "r2/r2/public/static/js/lib/store.js",
    "content": "/* Copyright (c) 2010-2012 Marcus Westin\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n * THE SOFTWARE.\n */\n\n;(function(){\n\tvar store = {},\n\t\twin = window,\n\t\tdoc = win.document,\n\t\tlocalStorageName = 'localStorage',\n\t\tglobalStorageName = 'globalStorage',\n\t\tnamespace = '__storejs__',\n\t\tstorage\n\n\tstore.disabled = false\n\tstore.set = function(key, value) {}\n\tstore.get = function(key) {}\n\tstore.remove = function(key) {}\n\tstore.clear = function() {}\n\tstore.transact = function(key, defaultVal, transactionFn) {\n\t\tvar val = store.get(key)\n\t\tif (transactionFn == null) {\n\t\t\ttransactionFn = defaultVal\n\t\t\tdefaultVal = null\n\t\t}\n\t\tif (typeof val == 'undefined') { val = defaultVal || {} }\n\t\ttransactionFn(val)\n\t\tstore.set(key, val)\n\t}\n\tstore.getAll = function() {}\n\n\tstore.serialize = function(value) {\n\t\treturn JSON.stringify(value)\n\t}\n\tstore.deserialize = function(value) {\n\t\tif (typeof value != 'string') { return undefined }\n\t\treturn JSON.parse(value)\n\t}\n\n\t// Functions to encapsulate questionable FireFox 3.6.13 behavior\n\t// when about.config::dom.storage.enabled === false\n\t// See https://github.com/marcuswestin/store.js/issues#issue/13\n\tfunction isLocalStorageNameSupported() {\n\t\ttry { return (localStorageName in win && win[localStorageName]) }\n\t\tcatch(err) { return false }\n\t}\n\n\tfunction isGlobalStorageNameSupported() {\n\t\ttry { return (globalStorageName in win && win[globalStorageName] && win[globalStorageName][win.location.hostname]) }\n\t\tcatch(err) { return false }\n\t}\n\n\tif (isLocalStorageNameSupported()) {\n\t\tstorage = win[localStorageName]\n\t\tstore.set = function(key, val) {\n\t\t\tif (val === undefined) { return store.remove(key) }\n\t\t\tstorage.setItem(key, store.serialize(val))\n\t\t}\n\t\tstore.get = function(key) { return store.deserialize(storage.getItem(key)) }\n\t\tstore.remove = function(key) { storage.removeItem(key) }\n\t\tstore.clear = function() { storage.clear() }\n\t\tstore.getAll = function() {\n\t\t\tvar ret = {}\n\t\t\tfor (var i=0; i<storage.length; ++i) {\n\t\t\t\tvar key = storage.key(i)\n\t\t\t\tret[key] = store.get(key)\n\t\t\t}\n\t\t\treturn ret\n\t\t}\n\t} else if (isGlobalStorageNameSupported()) {\n\t\tstorage = win[globalStorageName][win.location.hostname]\n\t\tstore.set = function(key, val) {\n\t\t\tif (val === undefined) { return store.remove(key) }\n\t\t\tstorage[key] = store.serialize(val)\n\t\t}\n\t\tstore.get = function(key) { return store.deserialize(storage[key] && storage[key].value) }\n\t\tstore.remove = function(key) { delete storage[key] }\n\t\tstore.clear = function() { for (var key in storage ) { delete storage[key] } }\n\t\tstore.getAll = function() {\n\t\t\tvar ret = {}\n\t\t\tfor (var i=0; i<storage.length; ++i) {\n\t\t\t\tvar key = storage.key(i)\n\t\t\t\tret[key] = store.get(key)\n\t\t\t}\n\t\t\treturn ret\n\t\t}\n\n\t} else if (doc.documentElement.addBehavior) {\n\t\tvar storageOwner,\n\t\t\tstorageContainer\n\t\t// Since #userData storage applies only to specific paths, we need to\n\t\t// somehow link our data to a specific path.  We choose /favicon.ico\n\t\t// as a pretty safe option, since all browsers already make a request to\n\t\t// this URL anyway and being a 404 will not hurt us here.  We wrap an\n\t\t// iframe pointing to the favicon in an ActiveXObject(htmlfile) object\n\t\t// (see: http://msdn.microsoft.com/en-us/library/aa752574(v=VS.85).aspx)\n\t\t// since the iframe access rules appear to allow direct access and\n\t\t// manipulation of the document element, even for a 404 page.  This\n\t\t// document can be used instead of the current document (which would\n\t\t// have been limited to the current path) to perform #userData storage.\n\t\ttry {\n\t\t\tstorageContainer = new ActiveXObject('htmlfile')\n\t\t\tstorageContainer.open()\n\t\t\tstorageContainer.write('<s' + 'cript>document.w=window</s' + 'cript><iframe src=\"/favicon.ico\"></iframe>')\n\t\t\tstorageContainer.close()\n\t\t\tstorageOwner = storageContainer.w.frames[0].document\n\t\t\tstorage = storageOwner.createElement('div')\n\t\t} catch(e) {\n\t\t\t// somehow ActiveXObject instantiation failed (perhaps some special\n\t\t\t// security settings or otherwse), fall back to per-path storage\n\t\t\tstorage = doc.createElement('div')\n\t\t\tstorageOwner = doc.body\n\t\t}\n\t\tfunction withIEStorage(storeFunction) {\n\t\t\treturn function() {\n\t\t\t\tvar args = Array.prototype.slice.call(arguments, 0)\n\t\t\t\targs.unshift(storage)\n\t\t\t\t// See http://msdn.microsoft.com/en-us/library/ms531081(v=VS.85).aspx\n\t\t\t\t// and http://msdn.microsoft.com/en-us/library/ms531424(v=VS.85).aspx\n\t\t\t\tstorageOwner.appendChild(storage)\n\t\t\t\tstorage.addBehavior('#default#userData')\n\t\t\t\tstorage.load(localStorageName)\n\t\t\t\tvar result = storeFunction.apply(store, args)\n\t\t\t\tstorageOwner.removeChild(storage)\n\t\t\t\treturn result\n\t\t\t}\n\t\t}\n\t\tfunction ieKeyFix(key) {\n\t\t\t// In IE7, keys may not begin with numbers.\n\t\t\t// See https://github.com/marcuswestin/store.js/issues/40#issuecomment-4617842\n\t\t\treturn '_'+key\n\t\t}\n\t\tstore.set = withIEStorage(function(storage, key, val) {\n\t\t\tkey = ieKeyFix(key)\n\t\t\tif (val === undefined) { return store.remove(key) }\n\t\t\tstorage.setAttribute(key, store.serialize(val))\n\t\t\tstorage.save(localStorageName)\n\t\t})\n\t\tstore.get = withIEStorage(function(storage, key) {\n\t\t\tkey = ieKeyFix(key)\n\t\t\treturn store.deserialize(storage.getAttribute(key))\n\t\t})\n\t\tstore.remove = withIEStorage(function(storage, key) {\n\t\t\tkey = ieKeyFix(key)\n\t\t\tstorage.removeAttribute(key)\n\t\t\tstorage.save(localStorageName)\n\t\t})\n\t\tstore.clear = withIEStorage(function(storage) {\n\t\t\tvar attributes = storage.XMLDocument.documentElement.attributes\n\t\t\tstorage.load(localStorageName)\n\t\t\tfor (var i=0, attr; attr=attributes[i]; i++) {\n\t\t\t\tstorage.removeAttribute(attr.name)\n\t\t\t}\n\t\t\tstorage.save(localStorageName)\n\t\t})\n\t\tstore.getAll = withIEStorage(function(storage) {\n\t\t\tvar attributes = storage.XMLDocument.documentElement.attributes\n\t\t\tstorage.load(localStorageName)\n\t\t\tvar ret = {}\n\t\t\tfor (var i=0, attr; attr=attributes[i]; ++i) {\n\t\t\t\tret[attr] = store.get(attr)\n\t\t\t}\n\t\t\treturn ret\n\t\t})\n\t}\n\n\ttry {\n\t\tstore.set(namespace, namespace)\n\t\tif (store.get(namespace) != namespace) { store.disabled = true }\n\t\tstore.remove(namespace)\n\t} catch(e) {\n\t\tstore.disabled = true\n\t}\n\t\n\tif (typeof module != 'undefined' && typeof module != 'function') { module.exports = store }\n\telse if (typeof define === 'function' && define.amd) { define(store) }\n\telse { this.store = store }\n})()\n"
  },
  {
    "path": "r2/r2/public/static/js/lib/ui.core.js",
    "content": "/*!\n * jQuery UI Core 1.10.4\n * http://jqueryui.com\n *\n * Copyright 2014 jQuery Foundation and other contributors\n * Released under the MIT license.\n * http://jquery.org/license\n *\n * http://api.jqueryui.com/category/ui-core/\n */\n(function( $, undefined ) {\n\nvar uuid = 0,\n\truniqueId = /^ui-id-\\d+$/;\n\n// $.ui might exist from components with no dependencies, e.g., $.ui.position\n$.ui = $.ui || {};\n\n$.extend( $.ui, {\n\tversion: \"1.10.4\",\n\n\tkeyCode: {\n\t\tBACKSPACE: 8,\n\t\tCOMMA: 188,\n\t\tDELETE: 46,\n\t\tDOWN: 40,\n\t\tEND: 35,\n\t\tENTER: 13,\n\t\tESCAPE: 27,\n\t\tHOME: 36,\n\t\tLEFT: 37,\n\t\tNUMPAD_ADD: 107,\n\t\tNUMPAD_DECIMAL: 110,\n\t\tNUMPAD_DIVIDE: 111,\n\t\tNUMPAD_ENTER: 108,\n\t\tNUMPAD_MULTIPLY: 106,\n\t\tNUMPAD_SUBTRACT: 109,\n\t\tPAGE_DOWN: 34,\n\t\tPAGE_UP: 33,\n\t\tPERIOD: 190,\n\t\tRIGHT: 39,\n\t\tSPACE: 32,\n\t\tTAB: 9,\n\t\tUP: 38\n\t}\n});\n\n// plugins\n$.fn.extend({\n\tfocus: (function( orig ) {\n\t\treturn function( delay, fn ) {\n\t\t\treturn typeof delay === \"number\" ?\n\t\t\t\tthis.each(function() {\n\t\t\t\t\tvar elem = this;\n\t\t\t\t\tsetTimeout(function() {\n\t\t\t\t\t\t$( elem ).focus();\n\t\t\t\t\t\tif ( fn ) {\n\t\t\t\t\t\t\tfn.call( elem );\n\t\t\t\t\t\t}\n\t\t\t\t\t}, delay );\n\t\t\t\t}) :\n\t\t\t\torig.apply( this, arguments );\n\t\t};\n\t})( $.fn.focus ),\n\n\tscrollParent: function() {\n\t\tvar scrollParent;\n\t\tif (($.ui.ie && (/(static|relative)/).test(this.css(\"position\"))) || (/absolute/).test(this.css(\"position\"))) {\n\t\t\tscrollParent = this.parents().filter(function() {\n\t\t\t\treturn (/(relative|absolute|fixed)/).test($.css(this,\"position\")) && (/(auto|scroll)/).test($.css(this,\"overflow\")+$.css(this,\"overflow-y\")+$.css(this,\"overflow-x\"));\n\t\t\t}).eq(0);\n\t\t} else {\n\t\t\tscrollParent = this.parents().filter(function() {\n\t\t\t\treturn (/(auto|scroll)/).test($.css(this,\"overflow\")+$.css(this,\"overflow-y\")+$.css(this,\"overflow-x\"));\n\t\t\t}).eq(0);\n\t\t}\n\n\t\treturn (/fixed/).test(this.css(\"position\")) || !scrollParent.length ? $(document) : scrollParent;\n\t},\n\n\tzIndex: function( zIndex ) {\n\t\tif ( zIndex !== undefined ) {\n\t\t\treturn this.css( \"zIndex\", zIndex );\n\t\t}\n\n\t\tif ( this.length ) {\n\t\t\tvar elem = $( this[ 0 ] ), position, value;\n\t\t\twhile ( elem.length && elem[ 0 ] !== document ) {\n\t\t\t\t// Ignore z-index if position is set to a value where z-index is ignored by the browser\n\t\t\t\t// This makes behavior of this function consistent across browsers\n\t\t\t\t// WebKit always returns auto if the element is positioned\n\t\t\t\tposition = elem.css( \"position\" );\n\t\t\t\tif ( position === \"absolute\" || position === \"relative\" || position === \"fixed\" ) {\n\t\t\t\t\t// IE returns 0 when zIndex is not specified\n\t\t\t\t\t// other browsers return a string\n\t\t\t\t\t// we ignore the case of nested elements with an explicit value of 0\n\t\t\t\t\t// <div style=\"z-index: -10;\"><div style=\"z-index: 0;\"></div></div>\n\t\t\t\t\tvalue = parseInt( elem.css( \"zIndex\" ), 10 );\n\t\t\t\t\tif ( !isNaN( value ) && value !== 0 ) {\n\t\t\t\t\t\treturn value;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\telem = elem.parent();\n\t\t\t}\n\t\t}\n\n\t\treturn 0;\n\t},\n\n\tuniqueId: function() {\n\t\treturn this.each(function() {\n\t\t\tif ( !this.id ) {\n\t\t\t\tthis.id = \"ui-id-\" + (++uuid);\n\t\t\t}\n\t\t});\n\t},\n\n\tremoveUniqueId: function() {\n\t\treturn this.each(function() {\n\t\t\tif ( runiqueId.test( this.id ) ) {\n\t\t\t\t$( this ).removeAttr( \"id\" );\n\t\t\t}\n\t\t});\n\t}\n});\n\n// selectors\nfunction focusable( element, isTabIndexNotNaN ) {\n\tvar map, mapName, img,\n\t\tnodeName = element.nodeName.toLowerCase();\n\tif ( \"area\" === nodeName ) {\n\t\tmap = element.parentNode;\n\t\tmapName = map.name;\n\t\tif ( !element.href || !mapName || map.nodeName.toLowerCase() !== \"map\" ) {\n\t\t\treturn false;\n\t\t}\n\t\timg = $( \"img[usemap=#\" + mapName + \"]\" )[0];\n\t\treturn !!img && visible( img );\n\t}\n\treturn ( /input|select|textarea|button|object/.test( nodeName ) ?\n\t\t!element.disabled :\n\t\t\"a\" === nodeName ?\n\t\t\telement.href || isTabIndexNotNaN :\n\t\t\tisTabIndexNotNaN) &&\n\t\t// the element and all of its ancestors must be visible\n\t\tvisible( element );\n}\n\nfunction visible( element ) {\n\treturn $.expr.filters.visible( element ) &&\n\t\t!$( element ).parents().addBack().filter(function() {\n\t\t\treturn $.css( this, \"visibility\" ) === \"hidden\";\n\t\t}).length;\n}\n\n$.extend( $.expr[ \":\" ], {\n\tdata: $.expr.createPseudo ?\n\t\t$.expr.createPseudo(function( dataName ) {\n\t\t\treturn function( elem ) {\n\t\t\t\treturn !!$.data( elem, dataName );\n\t\t\t};\n\t\t}) :\n\t\t// support: jQuery <1.8\n\t\tfunction( elem, i, match ) {\n\t\t\treturn !!$.data( elem, match[ 3 ] );\n\t\t},\n\n\tfocusable: function( element ) {\n\t\treturn focusable( element, !isNaN( $.attr( element, \"tabindex\" ) ) );\n\t},\n\n\ttabbable: function( element ) {\n\t\tvar tabIndex = $.attr( element, \"tabindex\" ),\n\t\t\tisTabIndexNaN = isNaN( tabIndex );\n\t\treturn ( isTabIndexNaN || tabIndex >= 0 ) && focusable( element, !isTabIndexNaN );\n\t}\n});\n\n// support: jQuery <1.8\nif ( !$( \"<a>\" ).outerWidth( 1 ).jquery ) {\n\t$.each( [ \"Width\", \"Height\" ], function( i, name ) {\n\t\tvar side = name === \"Width\" ? [ \"Left\", \"Right\" ] : [ \"Top\", \"Bottom\" ],\n\t\t\ttype = name.toLowerCase(),\n\t\t\torig = {\n\t\t\t\tinnerWidth: $.fn.innerWidth,\n\t\t\t\tinnerHeight: $.fn.innerHeight,\n\t\t\t\touterWidth: $.fn.outerWidth,\n\t\t\t\touterHeight: $.fn.outerHeight\n\t\t\t};\n\n\t\tfunction reduce( elem, size, border, margin ) {\n\t\t\t$.each( side, function() {\n\t\t\t\tsize -= parseFloat( $.css( elem, \"padding\" + this ) ) || 0;\n\t\t\t\tif ( border ) {\n\t\t\t\t\tsize -= parseFloat( $.css( elem, \"border\" + this + \"Width\" ) ) || 0;\n\t\t\t\t}\n\t\t\t\tif ( margin ) {\n\t\t\t\t\tsize -= parseFloat( $.css( elem, \"margin\" + this ) ) || 0;\n\t\t\t\t}\n\t\t\t});\n\t\t\treturn size;\n\t\t}\n\n\t\t$.fn[ \"inner\" + name ] = function( size ) {\n\t\t\tif ( size === undefined ) {\n\t\t\t\treturn orig[ \"inner\" + name ].call( this );\n\t\t\t}\n\n\t\t\treturn this.each(function() {\n\t\t\t\t$( this ).css( type, reduce( this, size ) + \"px\" );\n\t\t\t});\n\t\t};\n\n\t\t$.fn[ \"outer\" + name] = function( size, margin ) {\n\t\t\tif ( typeof size !== \"number\" ) {\n\t\t\t\treturn orig[ \"outer\" + name ].call( this, size );\n\t\t\t}\n\n\t\t\treturn this.each(function() {\n\t\t\t\t$( this).css( type, reduce( this, size, true, margin ) + \"px\" );\n\t\t\t});\n\t\t};\n\t});\n}\n\n// support: jQuery <1.8\nif ( !$.fn.addBack ) {\n\t$.fn.addBack = function( selector ) {\n\t\treturn this.add( selector == null ?\n\t\t\tthis.prevObject : this.prevObject.filter( selector )\n\t\t);\n\t};\n}\n\n// support: jQuery 1.6.1, 1.6.2 (http://bugs.jquery.com/ticket/9413)\nif ( $( \"<a>\" ).data( \"a-b\", \"a\" ).removeData( \"a-b\" ).data( \"a-b\" ) ) {\n\t$.fn.removeData = (function( removeData ) {\n\t\treturn function( key ) {\n\t\t\tif ( arguments.length ) {\n\t\t\t\treturn removeData.call( this, $.camelCase( key ) );\n\t\t\t} else {\n\t\t\t\treturn removeData.call( this );\n\t\t\t}\n\t\t};\n\t})( $.fn.removeData );\n}\n\n\n\n\n\n// deprecated\n$.ui.ie = !!/msie [\\w.]+/.exec( navigator.userAgent.toLowerCase() );\n\n$.support.selectstart = \"onselectstart\" in document.createElement( \"div\" );\n$.fn.extend({\n\tdisableSelection: function() {\n\t\treturn this.bind( ( $.support.selectstart ? \"selectstart\" : \"mousedown\" ) +\n\t\t\t\".ui-disableSelection\", function( event ) {\n\t\t\t\tevent.preventDefault();\n\t\t\t});\n\t},\n\n\tenableSelection: function() {\n\t\treturn this.unbind( \".ui-disableSelection\" );\n\t}\n});\n\n$.extend( $.ui, {\n\t// $.ui.plugin is deprecated. Use $.widget() extensions instead.\n\tplugin: {\n\t\tadd: function( module, option, set ) {\n\t\t\tvar i,\n\t\t\t\tproto = $.ui[ module ].prototype;\n\t\t\tfor ( i in set ) {\n\t\t\t\tproto.plugins[ i ] = proto.plugins[ i ] || [];\n\t\t\t\tproto.plugins[ i ].push( [ option, set[ i ] ] );\n\t\t\t}\n\t\t},\n\t\tcall: function( instance, name, args ) {\n\t\t\tvar i,\n\t\t\t\tset = instance.plugins[ name ];\n\t\t\tif ( !set || !instance.element[ 0 ].parentNode || instance.element[ 0 ].parentNode.nodeType === 11 ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tfor ( i = 0; i < set.length; i++ ) {\n\t\t\t\tif ( instance.options[ set[ i ][ 0 ] ] ) {\n\t\t\t\t\tset[ i ][ 1 ].apply( instance.element, args );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t},\n\n\t// only used by resizable\n\thasScroll: function( el, a ) {\n\n\t\t//If overflow is hidden, the element might have extra content, but the user wants to hide it\n\t\tif ( $( el ).css( \"overflow\" ) === \"hidden\") {\n\t\t\treturn false;\n\t\t}\n\n\t\tvar scroll = ( a && a === \"left\" ) ? \"scrollLeft\" : \"scrollTop\",\n\t\t\thas = false;\n\n\t\tif ( el[ scroll ] > 0 ) {\n\t\t\treturn true;\n\t\t}\n\n\t\t// TODO: determine which cases actually cause this to happen\n\t\t// if the element doesn't have the scroll set, see if it's possible to\n\t\t// set the scroll\n\t\tel[ scroll ] = 1;\n\t\thas = ( el[ scroll ] > 0 );\n\t\tel[ scroll ] = 0;\n\t\treturn has;\n\t}\n});\n\n})( jQuery );\n"
  },
  {
    "path": "r2/r2/public/static/js/lib/ui.datepicker.js",
    "content": "/*!\n * jQuery UI Datepicker 1.10.4\n * http://jqueryui.com\n *\n * Copyright 2014 jQuery Foundation and other contributors\n * Released under the MIT license.\n * http://jquery.org/license\n *\n * http://api.jqueryui.com/datepicker/\n *\n * Depends:\n *\tjquery.ui.core.js\n */\n(function( $, undefined ) {\n\n$.extend($.ui, { datepicker: { version: \"1.10.4\" } });\n\nvar PROP_NAME = \"datepicker\",\n\tinstActive;\n\n/* Date picker manager.\n   Use the singleton instance of this class, $.datepicker, to interact with the date picker.\n   Settings for (groups of) date pickers are maintained in an instance object,\n   allowing multiple different settings on the same page. */\n\nfunction Datepicker() {\n\tthis._curInst = null; // The current instance in use\n\tthis._keyEvent = false; // If the last event was a key event\n\tthis._disabledInputs = []; // List of date picker inputs that have been disabled\n\tthis._datepickerShowing = false; // True if the popup picker is showing , false if not\n\tthis._inDialog = false; // True if showing within a \"dialog\", false if not\n\tthis._mainDivId = \"ui-datepicker-div\"; // The ID of the main datepicker division\n\tthis._inlineClass = \"ui-datepicker-inline\"; // The name of the inline marker class\n\tthis._appendClass = \"ui-datepicker-append\"; // The name of the append marker class\n\tthis._triggerClass = \"ui-datepicker-trigger\"; // The name of the trigger marker class\n\tthis._dialogClass = \"ui-datepicker-dialog\"; // The name of the dialog marker class\n\tthis._disableClass = \"ui-datepicker-disabled\"; // The name of the disabled covering marker class\n\tthis._unselectableClass = \"ui-datepicker-unselectable\"; // The name of the unselectable cell marker class\n\tthis._currentClass = \"ui-datepicker-current-day\"; // The name of the current day marker class\n\tthis._dayOverClass = \"ui-datepicker-days-cell-over\"; // The name of the day hover marker class\n\tthis.regional = []; // Available regional settings, indexed by language code\n\tthis.regional[\"\"] = { // Default regional settings\n\t\tcloseText: \"Done\", // Display text for close link\n\t\tprevText: \"Prev\", // Display text for previous month link\n\t\tnextText: \"Next\", // Display text for next month link\n\t\tcurrentText: \"Today\", // Display text for current month link\n\t\tmonthNames: [\"January\",\"February\",\"March\",\"April\",\"May\",\"June\",\n\t\t\t\"July\",\"August\",\"September\",\"October\",\"November\",\"December\"], // Names of months for drop-down and formatting\n\t\tmonthNamesShort: [\"Jan\", \"Feb\", \"Mar\", \"Apr\", \"May\", \"Jun\", \"Jul\", \"Aug\", \"Sep\", \"Oct\", \"Nov\", \"Dec\"], // For formatting\n\t\tdayNames: [\"Sunday\", \"Monday\", \"Tuesday\", \"Wednesday\", \"Thursday\", \"Friday\", \"Saturday\"], // For formatting\n\t\tdayNamesShort: [\"Sun\", \"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\"], // For formatting\n\t\tdayNamesMin: [\"Su\",\"Mo\",\"Tu\",\"We\",\"Th\",\"Fr\",\"Sa\"], // Column headings for days starting at Sunday\n\t\tweekHeader: \"Wk\", // Column header for week of the year\n\t\tdateFormat: \"mm/dd/yy\", // See format options on parseDate\n\t\tfirstDay: 0, // The first day of the week, Sun = 0, Mon = 1, ...\n\t\tisRTL: false, // True if right-to-left language, false if left-to-right\n\t\tshowMonthAfterYear: false, // True if the year select precedes month, false for month then year\n\t\tyearSuffix: \"\" // Additional text to append to the year in the month headers\n\t};\n\tthis._defaults = { // Global defaults for all the date picker instances\n\t\tshowOn: \"focus\", // \"focus\" for popup on focus,\n\t\t\t// \"button\" for trigger button, or \"both\" for either\n\t\tshowAnim: \"fadeIn\", // Name of jQuery animation for popup\n\t\tshowOptions: {}, // Options for enhanced animations\n\t\tdefaultDate: null, // Used when field is blank: actual date,\n\t\t\t// +/-number for offset from today, null for today\n\t\tappendText: \"\", // Display text following the input box, e.g. showing the format\n\t\tbuttonText: \"...\", // Text for trigger button\n\t\tbuttonImage: \"\", // URL for trigger button image\n\t\tbuttonImageOnly: false, // True if the image appears alone, false if it appears on a button\n\t\thideIfNoPrevNext: false, // True to hide next/previous month links\n\t\t\t// if not applicable, false to just disable them\n\t\tnavigationAsDateFormat: false, // True if date formatting applied to prev/today/next links\n\t\tgotoCurrent: false, // True if today link goes back to current selection instead\n\t\tchangeMonth: false, // True if month can be selected directly, false if only prev/next\n\t\tchangeYear: false, // True if year can be selected directly, false if only prev/next\n\t\tyearRange: \"c-10:c+10\", // Range of years to display in drop-down,\n\t\t\t// either relative to today's year (-nn:+nn), relative to currently displayed year\n\t\t\t// (c-nn:c+nn), absolute (nnnn:nnnn), or a combination of the above (nnnn:-n)\n\t\tshowOtherMonths: false, // True to show dates in other months, false to leave blank\n\t\tselectOtherMonths: false, // True to allow selection of dates in other months, false for unselectable\n\t\tshowWeek: false, // True to show week of the year, false to not show it\n\t\tcalculateWeek: this.iso8601Week, // How to calculate the week of the year,\n\t\t\t// takes a Date and returns the number of the week for it\n\t\tshortYearCutoff: \"+10\", // Short year values < this are in the current century,\n\t\t\t// > this are in the previous century,\n\t\t\t// string value starting with \"+\" for current year + value\n\t\tminDate: null, // The earliest selectable date, or null for no limit\n\t\tmaxDate: null, // The latest selectable date, or null for no limit\n\t\tduration: \"fast\", // Duration of display/closure\n\t\tbeforeShowDay: null, // Function that takes a date and returns an array with\n\t\t\t// [0] = true if selectable, false if not, [1] = custom CSS class name(s) or \"\",\n\t\t\t// [2] = cell title (optional), e.g. $.datepicker.noWeekends\n\t\tbeforeShow: null, // Function that takes an input field and\n\t\t\t// returns a set of custom settings for the date picker\n\t\tonSelect: null, // Define a callback function when a date is selected\n\t\tonChangeMonthYear: null, // Define a callback function when the month or year is changed\n\t\tonClose: null, // Define a callback function when the datepicker is closed\n\t\tnumberOfMonths: 1, // Number of months to show at a time\n\t\tshowCurrentAtPos: 0, // The position in multipe months at which to show the current month (starting at 0)\n\t\tstepMonths: 1, // Number of months to step back/forward\n\t\tstepBigMonths: 12, // Number of months to step back/forward for the big links\n\t\taltField: \"\", // Selector for an alternate field to store selected dates into\n\t\taltFormat: \"\", // The date format to use for the alternate field\n\t\tconstrainInput: true, // The input is constrained by the current date format\n\t\tshowButtonPanel: false, // True to show button panel, false to not show it\n\t\tautoSize: false, // True to size the input for the date format, false to leave as is\n\t\tdisabled: false // The initial disabled state\n\t};\n\t$.extend(this._defaults, this.regional[\"\"]);\n\tthis.dpDiv = bindHover($(\"<div id='\" + this._mainDivId + \"' class='ui-datepicker ui-widget ui-widget-content ui-helper-clearfix ui-corner-all'></div>\"));\n}\n\n$.extend(Datepicker.prototype, {\n\t/* Class name added to elements to indicate already configured with a date picker. */\n\tmarkerClassName: \"hasDatepicker\",\n\n\t//Keep track of the maximum number of rows displayed (see #7043)\n\tmaxRows: 4,\n\n\t// TODO rename to \"widget\" when switching to widget factory\n\t_widgetDatepicker: function() {\n\t\treturn this.dpDiv;\n\t},\n\n\t/* Override the default settings for all instances of the date picker.\n\t * @param  settings  object - the new settings to use as defaults (anonymous object)\n\t * @return the manager object\n\t */\n\tsetDefaults: function(settings) {\n\t\textendRemove(this._defaults, settings || {});\n\t\treturn this;\n\t},\n\n\t/* Attach the date picker to a jQuery selection.\n\t * @param  target\telement - the target input field or division or span\n\t * @param  settings  object - the new settings to use for this date picker instance (anonymous)\n\t */\n\t_attachDatepicker: function(target, settings) {\n\t\tvar nodeName, inline, inst;\n\t\tnodeName = target.nodeName.toLowerCase();\n\t\tinline = (nodeName === \"div\" || nodeName === \"span\");\n\t\tif (!target.id) {\n\t\t\tthis.uuid += 1;\n\t\t\ttarget.id = \"dp\" + this.uuid;\n\t\t}\n\t\tinst = this._newInst($(target), inline);\n\t\tinst.settings = $.extend({}, settings || {});\n\t\tif (nodeName === \"input\") {\n\t\t\tthis._connectDatepicker(target, inst);\n\t\t} else if (inline) {\n\t\t\tthis._inlineDatepicker(target, inst);\n\t\t}\n\t},\n\n\t/* Create a new instance object. */\n\t_newInst: function(target, inline) {\n\t\tvar id = target[0].id.replace(/([^A-Za-z0-9_\\-])/g, \"\\\\\\\\$1\"); // escape jQuery meta chars\n\t\treturn {id: id, input: target, // associated target\n\t\t\tselectedDay: 0, selectedMonth: 0, selectedYear: 0, // current selection\n\t\t\tdrawMonth: 0, drawYear: 0, // month being drawn\n\t\t\tinline: inline, // is datepicker inline or not\n\t\t\tdpDiv: (!inline ? this.dpDiv : // presentation div\n\t\t\tbindHover($(\"<div class='\" + this._inlineClass + \" ui-datepicker ui-widget ui-widget-content ui-helper-clearfix ui-corner-all'></div>\")))};\n\t},\n\n\t/* Attach the date picker to an input field. */\n\t_connectDatepicker: function(target, inst) {\n\t\tvar input = $(target);\n\t\tinst.append = $([]);\n\t\tinst.trigger = $([]);\n\t\tif (input.hasClass(this.markerClassName)) {\n\t\t\treturn;\n\t\t}\n\t\tthis._attachments(input, inst);\n\t\tinput.addClass(this.markerClassName).keydown(this._doKeyDown).\n\t\t\tkeypress(this._doKeyPress).keyup(this._doKeyUp);\n\t\tthis._autoSize(inst);\n\t\t$.data(target, PROP_NAME, inst);\n\t\t//If disabled option is true, disable the datepicker once it has been attached to the input (see ticket #5665)\n\t\tif( inst.settings.disabled ) {\n\t\t\tthis._disableDatepicker( target );\n\t\t}\n\t},\n\n\t/* Make attachments based on settings. */\n\t_attachments: function(input, inst) {\n\t\tvar showOn, buttonText, buttonImage,\n\t\t\tappendText = this._get(inst, \"appendText\"),\n\t\t\tisRTL = this._get(inst, \"isRTL\");\n\n\t\tif (inst.append) {\n\t\t\tinst.append.remove();\n\t\t}\n\t\tif (appendText) {\n\t\t\tinst.append = $(\"<span class='\" + this._appendClass + \"'>\" + appendText + \"</span>\");\n\t\t\tinput[isRTL ? \"before\" : \"after\"](inst.append);\n\t\t}\n\n\t\tinput.unbind(\"focus\", this._showDatepicker);\n\n\t\tif (inst.trigger) {\n\t\t\tinst.trigger.remove();\n\t\t}\n\n\t\tshowOn = this._get(inst, \"showOn\");\n\t\tif (showOn === \"focus\" || showOn === \"both\") { // pop-up date picker when in the marked field\n\t\t\tinput.focus(this._showDatepicker);\n\t\t}\n\t\tif (showOn === \"button\" || showOn === \"both\") { // pop-up date picker when button clicked\n\t\t\tbuttonText = this._get(inst, \"buttonText\");\n\t\t\tbuttonImage = this._get(inst, \"buttonImage\");\n\t\t\tinst.trigger = $(this._get(inst, \"buttonImageOnly\") ?\n\t\t\t\t$(\"<img/>\").addClass(this._triggerClass).\n\t\t\t\t\tattr({ src: buttonImage, alt: buttonText, title: buttonText }) :\n\t\t\t\t$(\"<button type='button'></button>\").addClass(this._triggerClass).\n\t\t\t\t\thtml(!buttonImage ? buttonText : $(\"<img/>\").attr(\n\t\t\t\t\t{ src:buttonImage, alt:buttonText, title:buttonText })));\n\t\t\tinput[isRTL ? \"before\" : \"after\"](inst.trigger);\n\t\t\tinst.trigger.click(function() {\n\t\t\t\tif ($.datepicker._datepickerShowing && $.datepicker._lastInput === input[0]) {\n\t\t\t\t\t$.datepicker._hideDatepicker();\n\t\t\t\t} else if ($.datepicker._datepickerShowing && $.datepicker._lastInput !== input[0]) {\n\t\t\t\t\t$.datepicker._hideDatepicker();\n\t\t\t\t\t$.datepicker._showDatepicker(input[0]);\n\t\t\t\t} else {\n\t\t\t\t\t$.datepicker._showDatepicker(input[0]);\n\t\t\t\t}\n\t\t\t\treturn false;\n\t\t\t});\n\t\t}\n\t},\n\n\t/* Apply the maximum length for the date format. */\n\t_autoSize: function(inst) {\n\t\tif (this._get(inst, \"autoSize\") && !inst.inline) {\n\t\t\tvar findMax, max, maxI, i,\n\t\t\t\tdate = new Date(2009, 12 - 1, 20), // Ensure double digits\n\t\t\t\tdateFormat = this._get(inst, \"dateFormat\");\n\n\t\t\tif (dateFormat.match(/[DM]/)) {\n\t\t\t\tfindMax = function(names) {\n\t\t\t\t\tmax = 0;\n\t\t\t\t\tmaxI = 0;\n\t\t\t\t\tfor (i = 0; i < names.length; i++) {\n\t\t\t\t\t\tif (names[i].length > max) {\n\t\t\t\t\t\t\tmax = names[i].length;\n\t\t\t\t\t\t\tmaxI = i;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\treturn maxI;\n\t\t\t\t};\n\t\t\t\tdate.setMonth(findMax(this._get(inst, (dateFormat.match(/MM/) ?\n\t\t\t\t\t\"monthNames\" : \"monthNamesShort\"))));\n\t\t\t\tdate.setDate(findMax(this._get(inst, (dateFormat.match(/DD/) ?\n\t\t\t\t\t\"dayNames\" : \"dayNamesShort\"))) + 20 - date.getDay());\n\t\t\t}\n\t\t\tinst.input.attr(\"size\", this._formatDate(inst, date).length);\n\t\t}\n\t},\n\n\t/* Attach an inline date picker to a div. */\n\t_inlineDatepicker: function(target, inst) {\n\t\tvar divSpan = $(target);\n\t\tif (divSpan.hasClass(this.markerClassName)) {\n\t\t\treturn;\n\t\t}\n\t\tdivSpan.addClass(this.markerClassName).append(inst.dpDiv);\n\t\t$.data(target, PROP_NAME, inst);\n\t\tthis._setDate(inst, this._getDefaultDate(inst), true);\n\t\tthis._updateDatepicker(inst);\n\t\tthis._updateAlternate(inst);\n\t\t//If disabled option is true, disable the datepicker before showing it (see ticket #5665)\n\t\tif( inst.settings.disabled ) {\n\t\t\tthis._disableDatepicker( target );\n\t\t}\n\t\t// Set display:block in place of inst.dpDiv.show() which won't work on disconnected elements\n\t\t// http://bugs.jqueryui.com/ticket/7552 - A Datepicker created on a detached div has zero height\n\t\tinst.dpDiv.css( \"display\", \"block\" );\n\t},\n\n\t/* Pop-up the date picker in a \"dialog\" box.\n\t * @param  input element - ignored\n\t * @param  date\tstring or Date - the initial date to display\n\t * @param  onSelect  function - the function to call when a date is selected\n\t * @param  settings  object - update the dialog date picker instance's settings (anonymous object)\n\t * @param  pos int[2] - coordinates for the dialog's position within the screen or\n\t *\t\t\t\t\tevent - with x/y coordinates or\n\t *\t\t\t\t\tleave empty for default (screen centre)\n\t * @return the manager object\n\t */\n\t_dialogDatepicker: function(input, date, onSelect, settings, pos) {\n\t\tvar id, browserWidth, browserHeight, scrollX, scrollY,\n\t\t\tinst = this._dialogInst; // internal instance\n\n\t\tif (!inst) {\n\t\t\tthis.uuid += 1;\n\t\t\tid = \"dp\" + this.uuid;\n\t\t\tthis._dialogInput = $(\"<input type='text' id='\" + id +\n\t\t\t\t\"' style='position: absolute; top: -100px; width: 0px;'/>\");\n\t\t\tthis._dialogInput.keydown(this._doKeyDown);\n\t\t\t$(\"body\").append(this._dialogInput);\n\t\t\tinst = this._dialogInst = this._newInst(this._dialogInput, false);\n\t\t\tinst.settings = {};\n\t\t\t$.data(this._dialogInput[0], PROP_NAME, inst);\n\t\t}\n\t\textendRemove(inst.settings, settings || {});\n\t\tdate = (date && date.constructor === Date ? this._formatDate(inst, date) : date);\n\t\tthis._dialogInput.val(date);\n\n\t\tthis._pos = (pos ? (pos.length ? pos : [pos.pageX, pos.pageY]) : null);\n\t\tif (!this._pos) {\n\t\t\tbrowserWidth = document.documentElement.clientWidth;\n\t\t\tbrowserHeight = document.documentElement.clientHeight;\n\t\t\tscrollX = document.documentElement.scrollLeft || document.body.scrollLeft;\n\t\t\tscrollY = document.documentElement.scrollTop || document.body.scrollTop;\n\t\t\tthis._pos = // should use actual width/height below\n\t\t\t\t[(browserWidth / 2) - 100 + scrollX, (browserHeight / 2) - 150 + scrollY];\n\t\t}\n\n\t\t// move input on screen for focus, but hidden behind dialog\n\t\tthis._dialogInput.css(\"left\", (this._pos[0] + 20) + \"px\").css(\"top\", this._pos[1] + \"px\");\n\t\tinst.settings.onSelect = onSelect;\n\t\tthis._inDialog = true;\n\t\tthis.dpDiv.addClass(this._dialogClass);\n\t\tthis._showDatepicker(this._dialogInput[0]);\n\t\tif ($.blockUI) {\n\t\t\t$.blockUI(this.dpDiv);\n\t\t}\n\t\t$.data(this._dialogInput[0], PROP_NAME, inst);\n\t\treturn this;\n\t},\n\n\t/* Detach a datepicker from its control.\n\t * @param  target\telement - the target input field or division or span\n\t */\n\t_destroyDatepicker: function(target) {\n\t\tvar nodeName,\n\t\t\t$target = $(target),\n\t\t\tinst = $.data(target, PROP_NAME);\n\n\t\tif (!$target.hasClass(this.markerClassName)) {\n\t\t\treturn;\n\t\t}\n\n\t\tnodeName = target.nodeName.toLowerCase();\n\t\t$.removeData(target, PROP_NAME);\n\t\tif (nodeName === \"input\") {\n\t\t\tinst.append.remove();\n\t\t\tinst.trigger.remove();\n\t\t\t$target.removeClass(this.markerClassName).\n\t\t\t\tunbind(\"focus\", this._showDatepicker).\n\t\t\t\tunbind(\"keydown\", this._doKeyDown).\n\t\t\t\tunbind(\"keypress\", this._doKeyPress).\n\t\t\t\tunbind(\"keyup\", this._doKeyUp);\n\t\t} else if (nodeName === \"div\" || nodeName === \"span\") {\n\t\t\t$target.removeClass(this.markerClassName).empty();\n\t\t}\n\t},\n\n\t/* Enable the date picker to a jQuery selection.\n\t * @param  target\telement - the target input field or division or span\n\t */\n\t_enableDatepicker: function(target) {\n\t\tvar nodeName, inline,\n\t\t\t$target = $(target),\n\t\t\tinst = $.data(target, PROP_NAME);\n\n\t\tif (!$target.hasClass(this.markerClassName)) {\n\t\t\treturn;\n\t\t}\n\n\t\tnodeName = target.nodeName.toLowerCase();\n\t\tif (nodeName === \"input\") {\n\t\t\ttarget.disabled = false;\n\t\t\tinst.trigger.filter(\"button\").\n\t\t\t\teach(function() { this.disabled = false; }).end().\n\t\t\t\tfilter(\"img\").css({opacity: \"1.0\", cursor: \"\"});\n\t\t} else if (nodeName === \"div\" || nodeName === \"span\") {\n\t\t\tinline = $target.children(\".\" + this._inlineClass);\n\t\t\tinline.children().removeClass(\"ui-state-disabled\");\n\t\t\tinline.find(\"select.ui-datepicker-month, select.ui-datepicker-year\").\n\t\t\t\tprop(\"disabled\", false);\n\t\t}\n\t\tthis._disabledInputs = $.map(this._disabledInputs,\n\t\t\tfunction(value) { return (value === target ? null : value); }); // delete entry\n\t},\n\n\t/* Disable the date picker to a jQuery selection.\n\t * @param  target\telement - the target input field or division or span\n\t */\n\t_disableDatepicker: function(target) {\n\t\tvar nodeName, inline,\n\t\t\t$target = $(target),\n\t\t\tinst = $.data(target, PROP_NAME);\n\n\t\tif (!$target.hasClass(this.markerClassName)) {\n\t\t\treturn;\n\t\t}\n\n\t\tnodeName = target.nodeName.toLowerCase();\n\t\tif (nodeName === \"input\") {\n\t\t\ttarget.disabled = true;\n\t\t\tinst.trigger.filter(\"button\").\n\t\t\t\teach(function() { this.disabled = true; }).end().\n\t\t\t\tfilter(\"img\").css({opacity: \"0.5\", cursor: \"default\"});\n\t\t} else if (nodeName === \"div\" || nodeName === \"span\") {\n\t\t\tinline = $target.children(\".\" + this._inlineClass);\n\t\t\tinline.children().addClass(\"ui-state-disabled\");\n\t\t\tinline.find(\"select.ui-datepicker-month, select.ui-datepicker-year\").\n\t\t\t\tprop(\"disabled\", true);\n\t\t}\n\t\tthis._disabledInputs = $.map(this._disabledInputs,\n\t\t\tfunction(value) { return (value === target ? null : value); }); // delete entry\n\t\tthis._disabledInputs[this._disabledInputs.length] = target;\n\t},\n\n\t/* Is the first field in a jQuery collection disabled as a datepicker?\n\t * @param  target\telement - the target input field or division or span\n\t * @return boolean - true if disabled, false if enabled\n\t */\n\t_isDisabledDatepicker: function(target) {\n\t\tif (!target) {\n\t\t\treturn false;\n\t\t}\n\t\tfor (var i = 0; i < this._disabledInputs.length; i++) {\n\t\t\tif (this._disabledInputs[i] === target) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t},\n\n\t/* Retrieve the instance data for the target control.\n\t * @param  target  element - the target input field or division or span\n\t * @return  object - the associated instance data\n\t * @throws  error if a jQuery problem getting data\n\t */\n\t_getInst: function(target) {\n\t\ttry {\n\t\t\treturn $.data(target, PROP_NAME);\n\t\t}\n\t\tcatch (err) {\n\t\t\tthrow \"Missing instance data for this datepicker\";\n\t\t}\n\t},\n\n\t/* Update or retrieve the settings for a date picker attached to an input field or division.\n\t * @param  target  element - the target input field or division or span\n\t * @param  name\tobject - the new settings to update or\n\t *\t\t\t\tstring - the name of the setting to change or retrieve,\n\t *\t\t\t\twhen retrieving also \"all\" for all instance settings or\n\t *\t\t\t\t\"defaults\" for all global defaults\n\t * @param  value   any - the new value for the setting\n\t *\t\t\t\t(omit if above is an object or to retrieve a value)\n\t */\n\t_optionDatepicker: function(target, name, value) {\n\t\tvar settings, date, minDate, maxDate,\n\t\t\tinst = this._getInst(target);\n\n\t\tif (arguments.length === 2 && typeof name === \"string\") {\n\t\t\treturn (name === \"defaults\" ? $.extend({}, $.datepicker._defaults) :\n\t\t\t\t(inst ? (name === \"all\" ? $.extend({}, inst.settings) :\n\t\t\t\tthis._get(inst, name)) : null));\n\t\t}\n\n\t\tsettings = name || {};\n\t\tif (typeof name === \"string\") {\n\t\t\tsettings = {};\n\t\t\tsettings[name] = value;\n\t\t}\n\n\t\tif (inst) {\n\t\t\tif (this._curInst === inst) {\n\t\t\t\tthis._hideDatepicker();\n\t\t\t}\n\n\t\t\tdate = this._getDateDatepicker(target, true);\n\t\t\tminDate = this._getMinMaxDate(inst, \"min\");\n\t\t\tmaxDate = this._getMinMaxDate(inst, \"max\");\n\t\t\textendRemove(inst.settings, settings);\n\t\t\t// reformat the old minDate/maxDate values if dateFormat changes and a new minDate/maxDate isn't provided\n\t\t\tif (minDate !== null && settings.dateFormat !== undefined && settings.minDate === undefined) {\n\t\t\t\tinst.settings.minDate = this._formatDate(inst, minDate);\n\t\t\t}\n\t\t\tif (maxDate !== null && settings.dateFormat !== undefined && settings.maxDate === undefined) {\n\t\t\t\tinst.settings.maxDate = this._formatDate(inst, maxDate);\n\t\t\t}\n\t\t\tif ( \"disabled\" in settings ) {\n\t\t\t\tif ( settings.disabled ) {\n\t\t\t\t\tthis._disableDatepicker(target);\n\t\t\t\t} else {\n\t\t\t\t\tthis._enableDatepicker(target);\n\t\t\t\t}\n\t\t\t}\n\t\t\tthis._attachments($(target), inst);\n\t\t\tthis._autoSize(inst);\n\t\t\tthis._setDate(inst, date);\n\t\t\tthis._updateAlternate(inst);\n\t\t\tthis._updateDatepicker(inst);\n\t\t}\n\t},\n\n\t// change method deprecated\n\t_changeDatepicker: function(target, name, value) {\n\t\tthis._optionDatepicker(target, name, value);\n\t},\n\n\t/* Redraw the date picker attached to an input field or division.\n\t * @param  target  element - the target input field or division or span\n\t */\n\t_refreshDatepicker: function(target) {\n\t\tvar inst = this._getInst(target);\n\t\tif (inst) {\n\t\t\tthis._updateDatepicker(inst);\n\t\t}\n\t},\n\n\t/* Set the dates for a jQuery selection.\n\t * @param  target element - the target input field or division or span\n\t * @param  date\tDate - the new date\n\t */\n\t_setDateDatepicker: function(target, date) {\n\t\tvar inst = this._getInst(target);\n\t\tif (inst) {\n\t\t\tthis._setDate(inst, date);\n\t\t\tthis._updateDatepicker(inst);\n\t\t\tthis._updateAlternate(inst);\n\t\t}\n\t},\n\n\t/* Get the date(s) for the first entry in a jQuery selection.\n\t * @param  target element - the target input field or division or span\n\t * @param  noDefault boolean - true if no default date is to be used\n\t * @return Date - the current date\n\t */\n\t_getDateDatepicker: function(target, noDefault) {\n\t\tvar inst = this._getInst(target);\n\t\tif (inst && !inst.inline) {\n\t\t\tthis._setDateFromField(inst, noDefault);\n\t\t}\n\t\treturn (inst ? this._getDate(inst) : null);\n\t},\n\n\t/* Handle keystrokes. */\n\t_doKeyDown: function(event) {\n\t\tvar onSelect, dateStr, sel,\n\t\t\tinst = $.datepicker._getInst(event.target),\n\t\t\thandled = true,\n\t\t\tisRTL = inst.dpDiv.is(\".ui-datepicker-rtl\");\n\n\t\tinst._keyEvent = true;\n\t\tif ($.datepicker._datepickerShowing) {\n\t\t\tswitch (event.keyCode) {\n\t\t\t\tcase 9: $.datepicker._hideDatepicker();\n\t\t\t\t\t\thandled = false;\n\t\t\t\t\t\tbreak; // hide on tab out\n\t\t\t\tcase 13: sel = $(\"td.\" + $.datepicker._dayOverClass + \":not(.\" +\n\t\t\t\t\t\t\t\t\t$.datepicker._currentClass + \")\", inst.dpDiv);\n\t\t\t\t\t\tif (sel[0]) {\n\t\t\t\t\t\t\t$.datepicker._selectDay(event.target, inst.selectedMonth, inst.selectedYear, sel[0]);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tonSelect = $.datepicker._get(inst, \"onSelect\");\n\t\t\t\t\t\tif (onSelect) {\n\t\t\t\t\t\t\tdateStr = $.datepicker._formatDate(inst);\n\n\t\t\t\t\t\t\t// trigger custom callback\n\t\t\t\t\t\t\tonSelect.apply((inst.input ? inst.input[0] : null), [dateStr, inst]);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t$.datepicker._hideDatepicker();\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\treturn false; // don't submit the form\n\t\t\t\tcase 27: $.datepicker._hideDatepicker();\n\t\t\t\t\t\tbreak; // hide on escape\n\t\t\t\tcase 33: $.datepicker._adjustDate(event.target, (event.ctrlKey ?\n\t\t\t\t\t\t\t-$.datepicker._get(inst, \"stepBigMonths\") :\n\t\t\t\t\t\t\t-$.datepicker._get(inst, \"stepMonths\")), \"M\");\n\t\t\t\t\t\tbreak; // previous month/year on page up/+ ctrl\n\t\t\t\tcase 34: $.datepicker._adjustDate(event.target, (event.ctrlKey ?\n\t\t\t\t\t\t\t+$.datepicker._get(inst, \"stepBigMonths\") :\n\t\t\t\t\t\t\t+$.datepicker._get(inst, \"stepMonths\")), \"M\");\n\t\t\t\t\t\tbreak; // next month/year on page down/+ ctrl\n\t\t\t\tcase 35: if (event.ctrlKey || event.metaKey) {\n\t\t\t\t\t\t\t$.datepicker._clearDate(event.target);\n\t\t\t\t\t\t}\n\t\t\t\t\t\thandled = event.ctrlKey || event.metaKey;\n\t\t\t\t\t\tbreak; // clear on ctrl or command +end\n\t\t\t\tcase 36: if (event.ctrlKey || event.metaKey) {\n\t\t\t\t\t\t\t$.datepicker._gotoToday(event.target);\n\t\t\t\t\t\t}\n\t\t\t\t\t\thandled = event.ctrlKey || event.metaKey;\n\t\t\t\t\t\tbreak; // current on ctrl or command +home\n\t\t\t\tcase 37: if (event.ctrlKey || event.metaKey) {\n\t\t\t\t\t\t\t$.datepicker._adjustDate(event.target, (isRTL ? +1 : -1), \"D\");\n\t\t\t\t\t\t}\n\t\t\t\t\t\thandled = event.ctrlKey || event.metaKey;\n\t\t\t\t\t\t// -1 day on ctrl or command +left\n\t\t\t\t\t\tif (event.originalEvent.altKey) {\n\t\t\t\t\t\t\t$.datepicker._adjustDate(event.target, (event.ctrlKey ?\n\t\t\t\t\t\t\t\t-$.datepicker._get(inst, \"stepBigMonths\") :\n\t\t\t\t\t\t\t\t-$.datepicker._get(inst, \"stepMonths\")), \"M\");\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// next month/year on alt +left on Mac\n\t\t\t\t\t\tbreak;\n\t\t\t\tcase 38: if (event.ctrlKey || event.metaKey) {\n\t\t\t\t\t\t\t$.datepicker._adjustDate(event.target, -7, \"D\");\n\t\t\t\t\t\t}\n\t\t\t\t\t\thandled = event.ctrlKey || event.metaKey;\n\t\t\t\t\t\tbreak; // -1 week on ctrl or command +up\n\t\t\t\tcase 39: if (event.ctrlKey || event.metaKey) {\n\t\t\t\t\t\t\t$.datepicker._adjustDate(event.target, (isRTL ? -1 : +1), \"D\");\n\t\t\t\t\t\t}\n\t\t\t\t\t\thandled = event.ctrlKey || event.metaKey;\n\t\t\t\t\t\t// +1 day on ctrl or command +right\n\t\t\t\t\t\tif (event.originalEvent.altKey) {\n\t\t\t\t\t\t\t$.datepicker._adjustDate(event.target, (event.ctrlKey ?\n\t\t\t\t\t\t\t\t+$.datepicker._get(inst, \"stepBigMonths\") :\n\t\t\t\t\t\t\t\t+$.datepicker._get(inst, \"stepMonths\")), \"M\");\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// next month/year on alt +right\n\t\t\t\t\t\tbreak;\n\t\t\t\tcase 40: if (event.ctrlKey || event.metaKey) {\n\t\t\t\t\t\t\t$.datepicker._adjustDate(event.target, +7, \"D\");\n\t\t\t\t\t\t}\n\t\t\t\t\t\thandled = event.ctrlKey || event.metaKey;\n\t\t\t\t\t\tbreak; // +1 week on ctrl or command +down\n\t\t\t\tdefault: handled = false;\n\t\t\t}\n\t\t} else if (event.keyCode === 36 && event.ctrlKey) { // display the date picker on ctrl+home\n\t\t\t$.datepicker._showDatepicker(this);\n\t\t} else {\n\t\t\thandled = false;\n\t\t}\n\n\t\tif (handled) {\n\t\t\tevent.preventDefault();\n\t\t\tevent.stopPropagation();\n\t\t}\n\t},\n\n\t/* Filter entered characters - based on date format. */\n\t_doKeyPress: function(event) {\n\t\tvar chars, chr,\n\t\t\tinst = $.datepicker._getInst(event.target);\n\n\t\tif ($.datepicker._get(inst, \"constrainInput\")) {\n\t\t\tchars = $.datepicker._possibleChars($.datepicker._get(inst, \"dateFormat\"));\n\t\t\tchr = String.fromCharCode(event.charCode == null ? event.keyCode : event.charCode);\n\t\t\treturn event.ctrlKey || event.metaKey || (chr < \" \" || !chars || chars.indexOf(chr) > -1);\n\t\t}\n\t},\n\n\t/* Synchronise manual entry and field/alternate field. */\n\t_doKeyUp: function(event) {\n\t\tvar date,\n\t\t\tinst = $.datepicker._getInst(event.target);\n\n\t\tif (inst.input.val() !== inst.lastVal) {\n\t\t\ttry {\n\t\t\t\tdate = $.datepicker.parseDate($.datepicker._get(inst, \"dateFormat\"),\n\t\t\t\t\t(inst.input ? inst.input.val() : null),\n\t\t\t\t\t$.datepicker._getFormatConfig(inst));\n\n\t\t\t\tif (date) { // only if valid\n\t\t\t\t\t$.datepicker._setDateFromField(inst);\n\t\t\t\t\t$.datepicker._updateAlternate(inst);\n\t\t\t\t\t$.datepicker._updateDatepicker(inst);\n\t\t\t\t}\n\t\t\t}\n\t\t\tcatch (err) {\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t},\n\n\t/* Pop-up the date picker for a given input field.\n\t * If false returned from beforeShow event handler do not show.\n\t * @param  input  element - the input field attached to the date picker or\n\t *\t\t\t\t\tevent - if triggered by focus\n\t */\n\t_showDatepicker: function(input) {\n\t\tinput = input.target || input;\n\t\tif (input.nodeName.toLowerCase() !== \"input\") { // find from button/image trigger\n\t\t\tinput = $(\"input\", input.parentNode)[0];\n\t\t}\n\n\t\tif ($.datepicker._isDisabledDatepicker(input) || $.datepicker._lastInput === input) { // already here\n\t\t\treturn;\n\t\t}\n\n\t\tvar inst, beforeShow, beforeShowSettings, isFixed,\n\t\t\toffset, showAnim, duration;\n\n\t\tinst = $.datepicker._getInst(input);\n\t\tif ($.datepicker._curInst && $.datepicker._curInst !== inst) {\n\t\t\t$.datepicker._curInst.dpDiv.stop(true, true);\n\t\t\tif ( inst && $.datepicker._datepickerShowing ) {\n\t\t\t\t$.datepicker._hideDatepicker( $.datepicker._curInst.input[0] );\n\t\t\t}\n\t\t}\n\n\t\tbeforeShow = $.datepicker._get(inst, \"beforeShow\");\n\t\tbeforeShowSettings = beforeShow ? beforeShow.apply(input, [input, inst]) : {};\n\t\tif(beforeShowSettings === false){\n\t\t\treturn;\n\t\t}\n\t\textendRemove(inst.settings, beforeShowSettings);\n\n\t\tinst.lastVal = null;\n\t\t$.datepicker._lastInput = input;\n\t\t$.datepicker._setDateFromField(inst);\n\n\t\tif ($.datepicker._inDialog) { // hide cursor\n\t\t\tinput.value = \"\";\n\t\t}\n\t\tif (!$.datepicker._pos) { // position below input\n\t\t\t$.datepicker._pos = $.datepicker._findPos(input);\n\t\t\t$.datepicker._pos[1] += input.offsetHeight; // add the height\n\t\t}\n\n\t\tisFixed = false;\n\t\t$(input).parents().each(function() {\n\t\t\tisFixed |= $(this).css(\"position\") === \"fixed\";\n\t\t\treturn !isFixed;\n\t\t});\n\n\t\toffset = {left: $.datepicker._pos[0], top: $.datepicker._pos[1]};\n\t\t$.datepicker._pos = null;\n\t\t//to avoid flashes on Firefox\n\t\tinst.dpDiv.empty();\n\t\t// determine sizing offscreen\n\t\tinst.dpDiv.css({position: \"absolute\", display: \"block\", top: \"-1000px\"});\n\t\t$.datepicker._updateDatepicker(inst);\n\t\t// fix width for dynamic number of date pickers\n\t\t// and adjust position before showing\n\t\toffset = $.datepicker._checkOffset(inst, offset, isFixed);\n\t\tinst.dpDiv.css({position: ($.datepicker._inDialog && $.blockUI ?\n\t\t\t\"static\" : (isFixed ? \"fixed\" : \"absolute\")), display: \"none\",\n\t\t\tleft: offset.left + \"px\", top: offset.top + \"px\"});\n\n\t\tif (!inst.inline) {\n\t\t\tshowAnim = $.datepicker._get(inst, \"showAnim\");\n\t\t\tduration = $.datepicker._get(inst, \"duration\");\n\t\t\tinst.dpDiv.zIndex($(input).zIndex()+1);\n\t\t\t$.datepicker._datepickerShowing = true;\n\n\t\t\tif ( $.effects && $.effects.effect[ showAnim ] ) {\n\t\t\t\tinst.dpDiv.show(showAnim, $.datepicker._get(inst, \"showOptions\"), duration);\n\t\t\t} else {\n\t\t\t\tinst.dpDiv[showAnim || \"show\"](showAnim ? duration : null);\n\t\t\t}\n\n\t\t\tif ( $.datepicker._shouldFocusInput( inst ) ) {\n\t\t\t\tinst.input.focus();\n\t\t\t}\n\n\t\t\t$.datepicker._curInst = inst;\n\t\t}\n\t},\n\n\t/* Generate the date picker content. */\n\t_updateDatepicker: function(inst) {\n\t\tthis.maxRows = 4; //Reset the max number of rows being displayed (see #7043)\n\t\tinstActive = inst; // for delegate hover events\n\t\tinst.dpDiv.empty().append(this._generateHTML(inst));\n\t\tthis._attachHandlers(inst);\n\t\tinst.dpDiv.find(\".\" + this._dayOverClass + \" a\").mouseover();\n\n\t\tvar origyearshtml,\n\t\t\tnumMonths = this._getNumberOfMonths(inst),\n\t\t\tcols = numMonths[1],\n\t\t\twidth = 17;\n\n\t\tinst.dpDiv.removeClass(\"ui-datepicker-multi-2 ui-datepicker-multi-3 ui-datepicker-multi-4\").width(\"\");\n\t\tif (cols > 1) {\n\t\t\tinst.dpDiv.addClass(\"ui-datepicker-multi-\" + cols).css(\"width\", (width * cols) + \"em\");\n\t\t}\n\t\tinst.dpDiv[(numMonths[0] !== 1 || numMonths[1] !== 1 ? \"add\" : \"remove\") +\n\t\t\t\"Class\"](\"ui-datepicker-multi\");\n\t\tinst.dpDiv[(this._get(inst, \"isRTL\") ? \"add\" : \"remove\") +\n\t\t\t\"Class\"](\"ui-datepicker-rtl\");\n\n\t\tif (inst === $.datepicker._curInst && $.datepicker._datepickerShowing && $.datepicker._shouldFocusInput( inst ) ) {\n\t\t\tinst.input.focus();\n\t\t}\n\n\t\t// deffered render of the years select (to avoid flashes on Firefox)\n\t\tif( inst.yearshtml ){\n\t\t\torigyearshtml = inst.yearshtml;\n\t\t\tsetTimeout(function(){\n\t\t\t\t//assure that inst.yearshtml didn't change.\n\t\t\t\tif( origyearshtml === inst.yearshtml && inst.yearshtml ){\n\t\t\t\t\tinst.dpDiv.find(\"select.ui-datepicker-year:first\").replaceWith(inst.yearshtml);\n\t\t\t\t}\n\t\t\t\torigyearshtml = inst.yearshtml = null;\n\t\t\t}, 0);\n\t\t}\n\t},\n\n\t// #6694 - don't focus the input if it's already focused\n\t// this breaks the change event in IE\n\t// Support: IE and jQuery <1.9\n\t_shouldFocusInput: function( inst ) {\n\t\treturn inst.input && inst.input.is( \":visible\" ) && !inst.input.is( \":disabled\" ) && !inst.input.is( \":focus\" );\n\t},\n\n\t/* Check positioning to remain on screen. */\n\t_checkOffset: function(inst, offset, isFixed) {\n\t\tvar dpWidth = inst.dpDiv.outerWidth(),\n\t\t\tdpHeight = inst.dpDiv.outerHeight(),\n\t\t\tinputWidth = inst.input ? inst.input.outerWidth() : 0,\n\t\t\tinputHeight = inst.input ? inst.input.outerHeight() : 0,\n\t\t\tviewWidth = document.documentElement.clientWidth + (isFixed ? 0 : $(document).scrollLeft()),\n\t\t\tviewHeight = document.documentElement.clientHeight + (isFixed ? 0 : $(document).scrollTop());\n\n\t\toffset.left -= (this._get(inst, \"isRTL\") ? (dpWidth - inputWidth) : 0);\n\t\toffset.left -= (isFixed && offset.left === inst.input.offset().left) ? $(document).scrollLeft() : 0;\n\t\toffset.top -= (isFixed && offset.top === (inst.input.offset().top + inputHeight)) ? $(document).scrollTop() : 0;\n\n\t\t// now check if datepicker is showing outside window viewport - move to a better place if so.\n\t\toffset.left -= Math.min(offset.left, (offset.left + dpWidth > viewWidth && viewWidth > dpWidth) ?\n\t\t\tMath.abs(offset.left + dpWidth - viewWidth) : 0);\n\t\toffset.top -= Math.min(offset.top, (offset.top + dpHeight > viewHeight && viewHeight > dpHeight) ?\n\t\t\tMath.abs(dpHeight + inputHeight) : 0);\n\n\t\treturn offset;\n\t},\n\n\t/* Find an object's position on the screen. */\n\t_findPos: function(obj) {\n\t\tvar position,\n\t\t\tinst = this._getInst(obj),\n\t\t\tisRTL = this._get(inst, \"isRTL\");\n\n\t\twhile (obj && (obj.type === \"hidden\" || obj.nodeType !== 1 || $.expr.filters.hidden(obj))) {\n\t\t\tobj = obj[isRTL ? \"previousSibling\" : \"nextSibling\"];\n\t\t}\n\n\t\tposition = $(obj).offset();\n\t\treturn [position.left, position.top];\n\t},\n\n\t/* Hide the date picker from view.\n\t * @param  input  element - the input field attached to the date picker\n\t */\n\t_hideDatepicker: function(input) {\n\t\tvar showAnim, duration, postProcess, onClose,\n\t\t\tinst = this._curInst;\n\n\t\tif (!inst || (input && inst !== $.data(input, PROP_NAME))) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (this._datepickerShowing) {\n\t\t\tshowAnim = this._get(inst, \"showAnim\");\n\t\t\tduration = this._get(inst, \"duration\");\n\t\t\tpostProcess = function() {\n\t\t\t\t$.datepicker._tidyDialog(inst);\n\t\t\t};\n\n\t\t\t// DEPRECATED: after BC for 1.8.x $.effects[ showAnim ] is not needed\n\t\t\tif ( $.effects && ( $.effects.effect[ showAnim ] || $.effects[ showAnim ] ) ) {\n\t\t\t\tinst.dpDiv.hide(showAnim, $.datepicker._get(inst, \"showOptions\"), duration, postProcess);\n\t\t\t} else {\n\t\t\t\tinst.dpDiv[(showAnim === \"slideDown\" ? \"slideUp\" :\n\t\t\t\t\t(showAnim === \"fadeIn\" ? \"fadeOut\" : \"hide\"))]((showAnim ? duration : null), postProcess);\n\t\t\t}\n\n\t\t\tif (!showAnim) {\n\t\t\t\tpostProcess();\n\t\t\t}\n\t\t\tthis._datepickerShowing = false;\n\n\t\t\tonClose = this._get(inst, \"onClose\");\n\t\t\tif (onClose) {\n\t\t\t\tonClose.apply((inst.input ? inst.input[0] : null), [(inst.input ? inst.input.val() : \"\"), inst]);\n\t\t\t}\n\n\t\t\tthis._lastInput = null;\n\t\t\tif (this._inDialog) {\n\t\t\t\tthis._dialogInput.css({ position: \"absolute\", left: \"0\", top: \"-100px\" });\n\t\t\t\tif ($.blockUI) {\n\t\t\t\t\t$.unblockUI();\n\t\t\t\t\t$(\"body\").append(this.dpDiv);\n\t\t\t\t}\n\t\t\t}\n\t\t\tthis._inDialog = false;\n\t\t}\n\t},\n\n\t/* Tidy up after a dialog display. */\n\t_tidyDialog: function(inst) {\n\t\tinst.dpDiv.removeClass(this._dialogClass).unbind(\".ui-datepicker-calendar\");\n\t},\n\n\t/* Close date picker if clicked elsewhere. */\n\t_checkExternalClick: function(event) {\n\t\tif (!$.datepicker._curInst) {\n\t\t\treturn;\n\t\t}\n\n\t\tvar $target = $(event.target),\n\t\t\tinst = $.datepicker._getInst($target[0]);\n\n\t\tif ( ( ( $target[0].id !== $.datepicker._mainDivId &&\n\t\t\t\t$target.parents(\"#\" + $.datepicker._mainDivId).length === 0 &&\n\t\t\t\t!$target.hasClass($.datepicker.markerClassName) &&\n\t\t\t\t!$target.closest(\".\" + $.datepicker._triggerClass).length &&\n\t\t\t\t$.datepicker._datepickerShowing && !($.datepicker._inDialog && $.blockUI) ) ) ||\n\t\t\t( $target.hasClass($.datepicker.markerClassName) && $.datepicker._curInst !== inst ) ) {\n\t\t\t\t$.datepicker._hideDatepicker();\n\t\t}\n\t},\n\n\t/* Adjust one of the date sub-fields. */\n\t_adjustDate: function(id, offset, period) {\n\t\tvar target = $(id),\n\t\t\tinst = this._getInst(target[0]);\n\n\t\tif (this._isDisabledDatepicker(target[0])) {\n\t\t\treturn;\n\t\t}\n\t\tthis._adjustInstDate(inst, offset +\n\t\t\t(period === \"M\" ? this._get(inst, \"showCurrentAtPos\") : 0), // undo positioning\n\t\t\tperiod);\n\t\tthis._updateDatepicker(inst);\n\t},\n\n\t/* Action for current link. */\n\t_gotoToday: function(id) {\n\t\tvar date,\n\t\t\ttarget = $(id),\n\t\t\tinst = this._getInst(target[0]);\n\n\t\tif (this._get(inst, \"gotoCurrent\") && inst.currentDay) {\n\t\t\tinst.selectedDay = inst.currentDay;\n\t\t\tinst.drawMonth = inst.selectedMonth = inst.currentMonth;\n\t\t\tinst.drawYear = inst.selectedYear = inst.currentYear;\n\t\t} else {\n\t\t\tdate = new Date();\n\t\t\tinst.selectedDay = date.getDate();\n\t\t\tinst.drawMonth = inst.selectedMonth = date.getMonth();\n\t\t\tinst.drawYear = inst.selectedYear = date.getFullYear();\n\t\t}\n\t\tthis._notifyChange(inst);\n\t\tthis._adjustDate(target);\n\t},\n\n\t/* Action for selecting a new month/year. */\n\t_selectMonthYear: function(id, select, period) {\n\t\tvar target = $(id),\n\t\t\tinst = this._getInst(target[0]);\n\n\t\tinst[\"selected\" + (period === \"M\" ? \"Month\" : \"Year\")] =\n\t\tinst[\"draw\" + (period === \"M\" ? \"Month\" : \"Year\")] =\n\t\t\tparseInt(select.options[select.selectedIndex].value,10);\n\n\t\tthis._notifyChange(inst);\n\t\tthis._adjustDate(target);\n\t},\n\n\t/* Action for selecting a day. */\n\t_selectDay: function(id, month, year, td) {\n\t\tvar inst,\n\t\t\ttarget = $(id);\n\n\t\tif ($(td).hasClass(this._unselectableClass) || this._isDisabledDatepicker(target[0])) {\n\t\t\treturn;\n\t\t}\n\n\t\tinst = this._getInst(target[0]);\n\t\tinst.selectedDay = inst.currentDay = $(\"a\", td).html();\n\t\tinst.selectedMonth = inst.currentMonth = month;\n\t\tinst.selectedYear = inst.currentYear = year;\n\t\tthis._selectDate(id, this._formatDate(inst,\n\t\t\tinst.currentDay, inst.currentMonth, inst.currentYear));\n\t},\n\n\t/* Erase the input field and hide the date picker. */\n\t_clearDate: function(id) {\n\t\tvar target = $(id);\n\t\tthis._selectDate(target, \"\");\n\t},\n\n\t/* Update the input field with the selected date. */\n\t_selectDate: function(id, dateStr) {\n\t\tvar onSelect,\n\t\t\ttarget = $(id),\n\t\t\tinst = this._getInst(target[0]);\n\n\t\tdateStr = (dateStr != null ? dateStr : this._formatDate(inst));\n\t\tif (inst.input) {\n\t\t\tinst.input.val(dateStr);\n\t\t}\n\t\tthis._updateAlternate(inst);\n\n\t\tonSelect = this._get(inst, \"onSelect\");\n\t\tif (onSelect) {\n\t\t\tonSelect.apply((inst.input ? inst.input[0] : null), [dateStr, inst]);  // trigger custom callback\n\t\t} else if (inst.input) {\n\t\t\tinst.input.trigger(\"change\"); // fire the change event\n\t\t}\n\n\t\tif (inst.inline){\n\t\t\tthis._updateDatepicker(inst);\n\t\t} else {\n\t\t\tthis._hideDatepicker();\n\t\t\tthis._lastInput = inst.input[0];\n\t\t\tif (typeof(inst.input[0]) !== \"object\") {\n\t\t\t\tinst.input.focus(); // restore focus\n\t\t\t}\n\t\t\tthis._lastInput = null;\n\t\t}\n\t},\n\n\t/* Update any alternate field to synchronise with the main field. */\n\t_updateAlternate: function(inst) {\n\t\tvar altFormat, date, dateStr,\n\t\t\taltField = this._get(inst, \"altField\");\n\n\t\tif (altField) { // update alternate field too\n\t\t\taltFormat = this._get(inst, \"altFormat\") || this._get(inst, \"dateFormat\");\n\t\t\tdate = this._getDate(inst);\n\t\t\tdateStr = this.formatDate(altFormat, date, this._getFormatConfig(inst));\n\t\t\t$(altField).each(function() { $(this).val(dateStr); });\n\t\t}\n\t},\n\n\t/* Set as beforeShowDay function to prevent selection of weekends.\n\t * @param  date  Date - the date to customise\n\t * @return [boolean, string] - is this date selectable?, what is its CSS class?\n\t */\n\tnoWeekends: function(date) {\n\t\tvar day = date.getDay();\n\t\treturn [(day > 0 && day < 6), \"\"];\n\t},\n\n\t/* Set as calculateWeek to determine the week of the year based on the ISO 8601 definition.\n\t * @param  date  Date - the date to get the week for\n\t * @return  number - the number of the week within the year that contains this date\n\t */\n\tiso8601Week: function(date) {\n\t\tvar time,\n\t\t\tcheckDate = new Date(date.getTime());\n\n\t\t// Find Thursday of this week starting on Monday\n\t\tcheckDate.setDate(checkDate.getDate() + 4 - (checkDate.getDay() || 7));\n\n\t\ttime = checkDate.getTime();\n\t\tcheckDate.setMonth(0); // Compare with Jan 1\n\t\tcheckDate.setDate(1);\n\t\treturn Math.floor(Math.round((time - checkDate) / 86400000) / 7) + 1;\n\t},\n\n\t/* Parse a string value into a date object.\n\t * See formatDate below for the possible formats.\n\t *\n\t * @param  format string - the expected format of the date\n\t * @param  value string - the date in the above format\n\t * @param  settings Object - attributes include:\n\t *\t\t\t\t\tshortYearCutoff  number - the cutoff year for determining the century (optional)\n\t *\t\t\t\t\tdayNamesShort\tstring[7] - abbreviated names of the days from Sunday (optional)\n\t *\t\t\t\t\tdayNames\t\tstring[7] - names of the days from Sunday (optional)\n\t *\t\t\t\t\tmonthNamesShort string[12] - abbreviated names of the months (optional)\n\t *\t\t\t\t\tmonthNames\t\tstring[12] - names of the months (optional)\n\t * @return  Date - the extracted date value or null if value is blank\n\t */\n\tparseDate: function (format, value, settings) {\n\t\tif (format == null || value == null) {\n\t\t\tthrow \"Invalid arguments\";\n\t\t}\n\n\t\tvalue = (typeof value === \"object\" ? value.toString() : value + \"\");\n\t\tif (value === \"\") {\n\t\t\treturn null;\n\t\t}\n\n\t\tvar iFormat, dim, extra,\n\t\t\tiValue = 0,\n\t\t\tshortYearCutoffTemp = (settings ? settings.shortYearCutoff : null) || this._defaults.shortYearCutoff,\n\t\t\tshortYearCutoff = (typeof shortYearCutoffTemp !== \"string\" ? shortYearCutoffTemp :\n\t\t\t\tnew Date().getFullYear() % 100 + parseInt(shortYearCutoffTemp, 10)),\n\t\t\tdayNamesShort = (settings ? settings.dayNamesShort : null) || this._defaults.dayNamesShort,\n\t\t\tdayNames = (settings ? settings.dayNames : null) || this._defaults.dayNames,\n\t\t\tmonthNamesShort = (settings ? settings.monthNamesShort : null) || this._defaults.monthNamesShort,\n\t\t\tmonthNames = (settings ? settings.monthNames : null) || this._defaults.monthNames,\n\t\t\tyear = -1,\n\t\t\tmonth = -1,\n\t\t\tday = -1,\n\t\t\tdoy = -1,\n\t\t\tliteral = false,\n\t\t\tdate,\n\t\t\t// Check whether a format character is doubled\n\t\t\tlookAhead = function(match) {\n\t\t\t\tvar matches = (iFormat + 1 < format.length && format.charAt(iFormat + 1) === match);\n\t\t\t\tif (matches) {\n\t\t\t\t\tiFormat++;\n\t\t\t\t}\n\t\t\t\treturn matches;\n\t\t\t},\n\t\t\t// Extract a number from the string value\n\t\t\tgetNumber = function(match) {\n\t\t\t\tvar isDoubled = lookAhead(match),\n\t\t\t\t\tsize = (match === \"@\" ? 14 : (match === \"!\" ? 20 :\n\t\t\t\t\t(match === \"y\" && isDoubled ? 4 : (match === \"o\" ? 3 : 2)))),\n\t\t\t\t\tdigits = new RegExp(\"^\\\\d{1,\" + size + \"}\"),\n\t\t\t\t\tnum = value.substring(iValue).match(digits);\n\t\t\t\tif (!num) {\n\t\t\t\t\tthrow \"Missing number at position \" + iValue;\n\t\t\t\t}\n\t\t\t\tiValue += num[0].length;\n\t\t\t\treturn parseInt(num[0], 10);\n\t\t\t},\n\t\t\t// Extract a name from the string value and convert to an index\n\t\t\tgetName = function(match, shortNames, longNames) {\n\t\t\t\tvar index = -1,\n\t\t\t\t\tnames = $.map(lookAhead(match) ? longNames : shortNames, function (v, k) {\n\t\t\t\t\t\treturn [ [k, v] ];\n\t\t\t\t\t}).sort(function (a, b) {\n\t\t\t\t\t\treturn -(a[1].length - b[1].length);\n\t\t\t\t\t});\n\n\t\t\t\t$.each(names, function (i, pair) {\n\t\t\t\t\tvar name = pair[1];\n\t\t\t\t\tif (value.substr(iValue, name.length).toLowerCase() === name.toLowerCase()) {\n\t\t\t\t\t\tindex = pair[0];\n\t\t\t\t\t\tiValue += name.length;\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t\tif (index !== -1) {\n\t\t\t\t\treturn index + 1;\n\t\t\t\t} else {\n\t\t\t\t\tthrow \"Unknown name at position \" + iValue;\n\t\t\t\t}\n\t\t\t},\n\t\t\t// Confirm that a literal character matches the string value\n\t\t\tcheckLiteral = function() {\n\t\t\t\tif (value.charAt(iValue) !== format.charAt(iFormat)) {\n\t\t\t\t\tthrow \"Unexpected literal at position \" + iValue;\n\t\t\t\t}\n\t\t\t\tiValue++;\n\t\t\t};\n\n\t\tfor (iFormat = 0; iFormat < format.length; iFormat++) {\n\t\t\tif (literal) {\n\t\t\t\tif (format.charAt(iFormat) === \"'\" && !lookAhead(\"'\")) {\n\t\t\t\t\tliteral = false;\n\t\t\t\t} else {\n\t\t\t\t\tcheckLiteral();\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tswitch (format.charAt(iFormat)) {\n\t\t\t\t\tcase \"d\":\n\t\t\t\t\t\tday = getNumber(\"d\");\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"D\":\n\t\t\t\t\t\tgetName(\"D\", dayNamesShort, dayNames);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"o\":\n\t\t\t\t\t\tdoy = getNumber(\"o\");\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"m\":\n\t\t\t\t\t\tmonth = getNumber(\"m\");\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"M\":\n\t\t\t\t\t\tmonth = getName(\"M\", monthNamesShort, monthNames);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"y\":\n\t\t\t\t\t\tyear = getNumber(\"y\");\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"@\":\n\t\t\t\t\t\tdate = new Date(getNumber(\"@\"));\n\t\t\t\t\t\tyear = date.getFullYear();\n\t\t\t\t\t\tmonth = date.getMonth() + 1;\n\t\t\t\t\t\tday = date.getDate();\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"!\":\n\t\t\t\t\t\tdate = new Date((getNumber(\"!\") - this._ticksTo1970) / 10000);\n\t\t\t\t\t\tyear = date.getFullYear();\n\t\t\t\t\t\tmonth = date.getMonth() + 1;\n\t\t\t\t\t\tday = date.getDate();\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"'\":\n\t\t\t\t\t\tif (lookAhead(\"'\")){\n\t\t\t\t\t\t\tcheckLiteral();\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tliteral = true;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tcheckLiteral();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (iValue < value.length){\n\t\t\textra = value.substr(iValue);\n\t\t\tif (!/^\\s+/.test(extra)) {\n\t\t\t\tthrow \"Extra/unparsed characters found in date: \" + extra;\n\t\t\t}\n\t\t}\n\n\t\tif (year === -1) {\n\t\t\tyear = new Date().getFullYear();\n\t\t} else if (year < 100) {\n\t\t\tyear += new Date().getFullYear() - new Date().getFullYear() % 100 +\n\t\t\t\t(year <= shortYearCutoff ? 0 : -100);\n\t\t}\n\n\t\tif (doy > -1) {\n\t\t\tmonth = 1;\n\t\t\tday = doy;\n\t\t\tdo {\n\t\t\t\tdim = this._getDaysInMonth(year, month - 1);\n\t\t\t\tif (day <= dim) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tmonth++;\n\t\t\t\tday -= dim;\n\t\t\t} while (true);\n\t\t}\n\n\t\tdate = this._daylightSavingAdjust(new Date(year, month - 1, day));\n\t\tif (date.getFullYear() !== year || date.getMonth() + 1 !== month || date.getDate() !== day) {\n\t\t\tthrow \"Invalid date\"; // E.g. 31/02/00\n\t\t}\n\t\treturn date;\n\t},\n\n\t/* Standard date formats. */\n\tATOM: \"yy-mm-dd\", // RFC 3339 (ISO 8601)\n\tCOOKIE: \"D, dd M yy\",\n\tISO_8601: \"yy-mm-dd\",\n\tRFC_822: \"D, d M y\",\n\tRFC_850: \"DD, dd-M-y\",\n\tRFC_1036: \"D, d M y\",\n\tRFC_1123: \"D, d M yy\",\n\tRFC_2822: \"D, d M yy\",\n\tRSS: \"D, d M y\", // RFC 822\n\tTICKS: \"!\",\n\tTIMESTAMP: \"@\",\n\tW3C: \"yy-mm-dd\", // ISO 8601\n\n\t_ticksTo1970: (((1970 - 1) * 365 + Math.floor(1970 / 4) - Math.floor(1970 / 100) +\n\t\tMath.floor(1970 / 400)) * 24 * 60 * 60 * 10000000),\n\n\t/* Format a date object into a string value.\n\t * The format can be combinations of the following:\n\t * d  - day of month (no leading zero)\n\t * dd - day of month (two digit)\n\t * o  - day of year (no leading zeros)\n\t * oo - day of year (three digit)\n\t * D  - day name short\n\t * DD - day name long\n\t * m  - month of year (no leading zero)\n\t * mm - month of year (two digit)\n\t * M  - month name short\n\t * MM - month name long\n\t * y  - year (two digit)\n\t * yy - year (four digit)\n\t * @ - Unix timestamp (ms since 01/01/1970)\n\t * ! - Windows ticks (100ns since 01/01/0001)\n\t * \"...\" - literal text\n\t * '' - single quote\n\t *\n\t * @param  format string - the desired format of the date\n\t * @param  date Date - the date value to format\n\t * @param  settings Object - attributes include:\n\t *\t\t\t\t\tdayNamesShort\tstring[7] - abbreviated names of the days from Sunday (optional)\n\t *\t\t\t\t\tdayNames\t\tstring[7] - names of the days from Sunday (optional)\n\t *\t\t\t\t\tmonthNamesShort string[12] - abbreviated names of the months (optional)\n\t *\t\t\t\t\tmonthNames\t\tstring[12] - names of the months (optional)\n\t * @return  string - the date in the above format\n\t */\n\tformatDate: function (format, date, settings) {\n\t\tif (!date) {\n\t\t\treturn \"\";\n\t\t}\n\n\t\tvar iFormat,\n\t\t\tdayNamesShort = (settings ? settings.dayNamesShort : null) || this._defaults.dayNamesShort,\n\t\t\tdayNames = (settings ? settings.dayNames : null) || this._defaults.dayNames,\n\t\t\tmonthNamesShort = (settings ? settings.monthNamesShort : null) || this._defaults.monthNamesShort,\n\t\t\tmonthNames = (settings ? settings.monthNames : null) || this._defaults.monthNames,\n\t\t\t// Check whether a format character is doubled\n\t\t\tlookAhead = function(match) {\n\t\t\t\tvar matches = (iFormat + 1 < format.length && format.charAt(iFormat + 1) === match);\n\t\t\t\tif (matches) {\n\t\t\t\t\tiFormat++;\n\t\t\t\t}\n\t\t\t\treturn matches;\n\t\t\t},\n\t\t\t// Format a number, with leading zero if necessary\n\t\t\tformatNumber = function(match, value, len) {\n\t\t\t\tvar num = \"\" + value;\n\t\t\t\tif (lookAhead(match)) {\n\t\t\t\t\twhile (num.length < len) {\n\t\t\t\t\t\tnum = \"0\" + num;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn num;\n\t\t\t},\n\t\t\t// Format a name, short or long as requested\n\t\t\tformatName = function(match, value, shortNames, longNames) {\n\t\t\t\treturn (lookAhead(match) ? longNames[value] : shortNames[value]);\n\t\t\t},\n\t\t\toutput = \"\",\n\t\t\tliteral = false;\n\n\t\tif (date) {\n\t\t\tfor (iFormat = 0; iFormat < format.length; iFormat++) {\n\t\t\t\tif (literal) {\n\t\t\t\t\tif (format.charAt(iFormat) === \"'\" && !lookAhead(\"'\")) {\n\t\t\t\t\t\tliteral = false;\n\t\t\t\t\t} else {\n\t\t\t\t\t\toutput += format.charAt(iFormat);\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tswitch (format.charAt(iFormat)) {\n\t\t\t\t\t\tcase \"d\":\n\t\t\t\t\t\t\toutput += formatNumber(\"d\", date.getDate(), 2);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase \"D\":\n\t\t\t\t\t\t\toutput += formatName(\"D\", date.getDay(), dayNamesShort, dayNames);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase \"o\":\n\t\t\t\t\t\t\toutput += formatNumber(\"o\",\n\t\t\t\t\t\t\t\tMath.round((new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime() - new Date(date.getFullYear(), 0, 0).getTime()) / 86400000), 3);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase \"m\":\n\t\t\t\t\t\t\toutput += formatNumber(\"m\", date.getMonth() + 1, 2);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase \"M\":\n\t\t\t\t\t\t\toutput += formatName(\"M\", date.getMonth(), monthNamesShort, monthNames);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase \"y\":\n\t\t\t\t\t\t\toutput += (lookAhead(\"y\") ? date.getFullYear() :\n\t\t\t\t\t\t\t\t(date.getYear() % 100 < 10 ? \"0\" : \"\") + date.getYear() % 100);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase \"@\":\n\t\t\t\t\t\t\toutput += date.getTime();\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase \"!\":\n\t\t\t\t\t\t\toutput += date.getTime() * 10000 + this._ticksTo1970;\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase \"'\":\n\t\t\t\t\t\t\tif (lookAhead(\"'\")) {\n\t\t\t\t\t\t\t\toutput += \"'\";\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tliteral = true;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\toutput += format.charAt(iFormat);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn output;\n\t},\n\n\t/* Extract all possible characters from the date format. */\n\t_possibleChars: function (format) {\n\t\tvar iFormat,\n\t\t\tchars = \"\",\n\t\t\tliteral = false,\n\t\t\t// Check whether a format character is doubled\n\t\t\tlookAhead = function(match) {\n\t\t\t\tvar matches = (iFormat + 1 < format.length && format.charAt(iFormat + 1) === match);\n\t\t\t\tif (matches) {\n\t\t\t\t\tiFormat++;\n\t\t\t\t}\n\t\t\t\treturn matches;\n\t\t\t};\n\n\t\tfor (iFormat = 0; iFormat < format.length; iFormat++) {\n\t\t\tif (literal) {\n\t\t\t\tif (format.charAt(iFormat) === \"'\" && !lookAhead(\"'\")) {\n\t\t\t\t\tliteral = false;\n\t\t\t\t} else {\n\t\t\t\t\tchars += format.charAt(iFormat);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tswitch (format.charAt(iFormat)) {\n\t\t\t\t\tcase \"d\": case \"m\": case \"y\": case \"@\":\n\t\t\t\t\t\tchars += \"0123456789\";\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"D\": case \"M\":\n\t\t\t\t\t\treturn null; // Accept anything\n\t\t\t\t\tcase \"'\":\n\t\t\t\t\t\tif (lookAhead(\"'\")) {\n\t\t\t\t\t\t\tchars += \"'\";\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tliteral = true;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tchars += format.charAt(iFormat);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn chars;\n\t},\n\n\t/* Get a setting value, defaulting if necessary. */\n\t_get: function(inst, name) {\n\t\treturn inst.settings[name] !== undefined ?\n\t\t\tinst.settings[name] : this._defaults[name];\n\t},\n\n\t/* Parse existing date and initialise date picker. */\n\t_setDateFromField: function(inst, noDefault) {\n\t\tif (inst.input.val() === inst.lastVal) {\n\t\t\treturn;\n\t\t}\n\n\t\tvar dateFormat = this._get(inst, \"dateFormat\"),\n\t\t\tdates = inst.lastVal = inst.input ? inst.input.val() : null,\n\t\t\tdefaultDate = this._getDefaultDate(inst),\n\t\t\tdate = defaultDate,\n\t\t\tsettings = this._getFormatConfig(inst);\n\n\t\ttry {\n\t\t\tdate = this.parseDate(dateFormat, dates, settings) || defaultDate;\n\t\t} catch (event) {\n\t\t\tdates = (noDefault ? \"\" : dates);\n\t\t}\n\t\tinst.selectedDay = date.getDate();\n\t\tinst.drawMonth = inst.selectedMonth = date.getMonth();\n\t\tinst.drawYear = inst.selectedYear = date.getFullYear();\n\t\tinst.currentDay = (dates ? date.getDate() : 0);\n\t\tinst.currentMonth = (dates ? date.getMonth() : 0);\n\t\tinst.currentYear = (dates ? date.getFullYear() : 0);\n\t\tthis._adjustInstDate(inst);\n\t},\n\n\t/* Retrieve the default date shown on opening. */\n\t_getDefaultDate: function(inst) {\n\t\treturn this._restrictMinMax(inst,\n\t\t\tthis._determineDate(inst, this._get(inst, \"defaultDate\"), new Date()));\n\t},\n\n\t/* A date may be specified as an exact value or a relative one. */\n\t_determineDate: function(inst, date, defaultDate) {\n\t\tvar offsetNumeric = function(offset) {\n\t\t\t\tvar date = new Date();\n\t\t\t\tdate.setDate(date.getDate() + offset);\n\t\t\t\treturn date;\n\t\t\t},\n\t\t\toffsetString = function(offset) {\n\t\t\t\ttry {\n\t\t\t\t\treturn $.datepicker.parseDate($.datepicker._get(inst, \"dateFormat\"),\n\t\t\t\t\t\toffset, $.datepicker._getFormatConfig(inst));\n\t\t\t\t}\n\t\t\t\tcatch (e) {\n\t\t\t\t\t// Ignore\n\t\t\t\t}\n\n\t\t\t\tvar date = (offset.toLowerCase().match(/^c/) ?\n\t\t\t\t\t$.datepicker._getDate(inst) : null) || new Date(),\n\t\t\t\t\tyear = date.getFullYear(),\n\t\t\t\t\tmonth = date.getMonth(),\n\t\t\t\t\tday = date.getDate(),\n\t\t\t\t\tpattern = /([+\\-]?[0-9]+)\\s*(d|D|w|W|m|M|y|Y)?/g,\n\t\t\t\t\tmatches = pattern.exec(offset);\n\n\t\t\t\twhile (matches) {\n\t\t\t\t\tswitch (matches[2] || \"d\") {\n\t\t\t\t\t\tcase \"d\" : case \"D\" :\n\t\t\t\t\t\t\tday += parseInt(matches[1],10); break;\n\t\t\t\t\t\tcase \"w\" : case \"W\" :\n\t\t\t\t\t\t\tday += parseInt(matches[1],10) * 7; break;\n\t\t\t\t\t\tcase \"m\" : case \"M\" :\n\t\t\t\t\t\t\tmonth += parseInt(matches[1],10);\n\t\t\t\t\t\t\tday = Math.min(day, $.datepicker._getDaysInMonth(year, month));\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase \"y\": case \"Y\" :\n\t\t\t\t\t\t\tyear += parseInt(matches[1],10);\n\t\t\t\t\t\t\tday = Math.min(day, $.datepicker._getDaysInMonth(year, month));\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tmatches = pattern.exec(offset);\n\t\t\t\t}\n\t\t\t\treturn new Date(year, month, day);\n\t\t\t},\n\t\t\tnewDate = (date == null || date === \"\" ? defaultDate : (typeof date === \"string\" ? offsetString(date) :\n\t\t\t\t(typeof date === \"number\" ? (isNaN(date) ? defaultDate : offsetNumeric(date)) : new Date(date.getTime()))));\n\n\t\tnewDate = (newDate && newDate.toString() === \"Invalid Date\" ? defaultDate : newDate);\n\t\tif (newDate) {\n\t\t\tnewDate.setHours(0);\n\t\t\tnewDate.setMinutes(0);\n\t\t\tnewDate.setSeconds(0);\n\t\t\tnewDate.setMilliseconds(0);\n\t\t}\n\t\treturn this._daylightSavingAdjust(newDate);\n\t},\n\n\t/* Handle switch to/from daylight saving.\n\t * Hours may be non-zero on daylight saving cut-over:\n\t * > 12 when midnight changeover, but then cannot generate\n\t * midnight datetime, so jump to 1AM, otherwise reset.\n\t * @param  date  (Date) the date to check\n\t * @return  (Date) the corrected date\n\t */\n\t_daylightSavingAdjust: function(date) {\n\t\tif (!date) {\n\t\t\treturn null;\n\t\t}\n\t\tdate.setHours(date.getHours() > 12 ? date.getHours() + 2 : 0);\n\t\treturn date;\n\t},\n\n\t/* Set the date(s) directly. */\n\t_setDate: function(inst, date, noChange) {\n\t\tvar clear = !date,\n\t\t\torigMonth = inst.selectedMonth,\n\t\t\torigYear = inst.selectedYear,\n\t\t\tnewDate = this._restrictMinMax(inst, this._determineDate(inst, date, new Date()));\n\n\t\tinst.selectedDay = inst.currentDay = newDate.getDate();\n\t\tinst.drawMonth = inst.selectedMonth = inst.currentMonth = newDate.getMonth();\n\t\tinst.drawYear = inst.selectedYear = inst.currentYear = newDate.getFullYear();\n\t\tif ((origMonth !== inst.selectedMonth || origYear !== inst.selectedYear) && !noChange) {\n\t\t\tthis._notifyChange(inst);\n\t\t}\n\t\tthis._adjustInstDate(inst);\n\t\tif (inst.input) {\n\t\t\tinst.input.val(clear ? \"\" : this._formatDate(inst));\n\t\t}\n\t},\n\n\t/* Retrieve the date(s) directly. */\n\t_getDate: function(inst) {\n\t\tvar startDate = (!inst.currentYear || (inst.input && inst.input.val() === \"\") ? null :\n\t\t\tthis._daylightSavingAdjust(new Date(\n\t\t\tinst.currentYear, inst.currentMonth, inst.currentDay)));\n\t\t\treturn startDate;\n\t},\n\n\t/* Attach the onxxx handlers.  These are declared statically so\n\t * they work with static code transformers like Caja.\n\t */\n\t_attachHandlers: function(inst) {\n\t\tvar stepMonths = this._get(inst, \"stepMonths\"),\n\t\t\tid = \"#\" + inst.id.replace( /\\\\\\\\/g, \"\\\\\" );\n\t\tinst.dpDiv.find(\"[data-handler]\").map(function () {\n\t\t\tvar handler = {\n\t\t\t\tprev: function () {\n\t\t\t\t\t$.datepicker._adjustDate(id, -stepMonths, \"M\");\n\t\t\t\t},\n\t\t\t\tnext: function () {\n\t\t\t\t\t$.datepicker._adjustDate(id, +stepMonths, \"M\");\n\t\t\t\t},\n\t\t\t\thide: function () {\n\t\t\t\t\t$.datepicker._hideDatepicker();\n\t\t\t\t},\n\t\t\t\ttoday: function () {\n\t\t\t\t\t$.datepicker._gotoToday(id);\n\t\t\t\t},\n\t\t\t\tselectDay: function () {\n\t\t\t\t\t$.datepicker._selectDay(id, +this.getAttribute(\"data-month\"), +this.getAttribute(\"data-year\"), this);\n\t\t\t\t\treturn false;\n\t\t\t\t},\n\t\t\t\tselectMonth: function () {\n\t\t\t\t\t$.datepicker._selectMonthYear(id, this, \"M\");\n\t\t\t\t\treturn false;\n\t\t\t\t},\n\t\t\t\tselectYear: function () {\n\t\t\t\t\t$.datepicker._selectMonthYear(id, this, \"Y\");\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t};\n\t\t\t$(this).bind(this.getAttribute(\"data-event\"), handler[this.getAttribute(\"data-handler\")]);\n\t\t});\n\t},\n\n\t/* Generate the HTML for the current state of the date picker. */\n\t_generateHTML: function(inst) {\n\t\tvar maxDraw, prevText, prev, nextText, next, currentText, gotoDate,\n\t\t\tcontrols, buttonPanel, firstDay, showWeek, dayNames, dayNamesMin,\n\t\t\tmonthNames, monthNamesShort, beforeShowDay, showOtherMonths,\n\t\t\tselectOtherMonths, defaultDate, html, dow, row, group, col, selectedDate,\n\t\t\tcornerClass, calender, thead, day, daysInMonth, leadDays, curRows, numRows,\n\t\t\tprintDate, dRow, tbody, daySettings, otherMonth, unselectable,\n\t\t\ttempDate = new Date(),\n\t\t\ttoday = this._daylightSavingAdjust(\n\t\t\t\tnew Date(tempDate.getFullYear(), tempDate.getMonth(), tempDate.getDate())), // clear time\n\t\t\tisRTL = this._get(inst, \"isRTL\"),\n\t\t\tshowButtonPanel = this._get(inst, \"showButtonPanel\"),\n\t\t\thideIfNoPrevNext = this._get(inst, \"hideIfNoPrevNext\"),\n\t\t\tnavigationAsDateFormat = this._get(inst, \"navigationAsDateFormat\"),\n\t\t\tnumMonths = this._getNumberOfMonths(inst),\n\t\t\tshowCurrentAtPos = this._get(inst, \"showCurrentAtPos\"),\n\t\t\tstepMonths = this._get(inst, \"stepMonths\"),\n\t\t\tisMultiMonth = (numMonths[0] !== 1 || numMonths[1] !== 1),\n\t\t\tcurrentDate = this._daylightSavingAdjust((!inst.currentDay ? new Date(9999, 9, 9) :\n\t\t\t\tnew Date(inst.currentYear, inst.currentMonth, inst.currentDay))),\n\t\t\tminDate = this._getMinMaxDate(inst, \"min\"),\n\t\t\tmaxDate = this._getMinMaxDate(inst, \"max\"),\n\t\t\tdrawMonth = inst.drawMonth - showCurrentAtPos,\n\t\t\tdrawYear = inst.drawYear;\n\n\t\tif (drawMonth < 0) {\n\t\t\tdrawMonth += 12;\n\t\t\tdrawYear--;\n\t\t}\n\t\tif (maxDate) {\n\t\t\tmaxDraw = this._daylightSavingAdjust(new Date(maxDate.getFullYear(),\n\t\t\t\tmaxDate.getMonth() - (numMonths[0] * numMonths[1]) + 1, maxDate.getDate()));\n\t\t\tmaxDraw = (minDate && maxDraw < minDate ? minDate : maxDraw);\n\t\t\twhile (this._daylightSavingAdjust(new Date(drawYear, drawMonth, 1)) > maxDraw) {\n\t\t\t\tdrawMonth--;\n\t\t\t\tif (drawMonth < 0) {\n\t\t\t\t\tdrawMonth = 11;\n\t\t\t\t\tdrawYear--;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tinst.drawMonth = drawMonth;\n\t\tinst.drawYear = drawYear;\n\n\t\tprevText = this._get(inst, \"prevText\");\n\t\tprevText = (!navigationAsDateFormat ? prevText : this.formatDate(prevText,\n\t\t\tthis._daylightSavingAdjust(new Date(drawYear, drawMonth - stepMonths, 1)),\n\t\t\tthis._getFormatConfig(inst)));\n\n\t\tprev = (this._canAdjustMonth(inst, -1, drawYear, drawMonth) ?\n\t\t\t\"<a class='ui-datepicker-prev ui-corner-all' data-handler='prev' data-event='click'\" +\n\t\t\t\" title='\" + prevText + \"'><span class='ui-icon ui-icon-circle-triangle-\" + ( isRTL ? \"e\" : \"w\") + \"'>\" + prevText + \"</span></a>\" :\n\t\t\t(hideIfNoPrevNext ? \"\" : \"<a class='ui-datepicker-prev ui-corner-all ui-state-disabled' title='\"+ prevText +\"'><span class='ui-icon ui-icon-circle-triangle-\" + ( isRTL ? \"e\" : \"w\") + \"'>\" + prevText + \"</span></a>\"));\n\n\t\tnextText = this._get(inst, \"nextText\");\n\t\tnextText = (!navigationAsDateFormat ? nextText : this.formatDate(nextText,\n\t\t\tthis._daylightSavingAdjust(new Date(drawYear, drawMonth + stepMonths, 1)),\n\t\t\tthis._getFormatConfig(inst)));\n\n\t\tnext = (this._canAdjustMonth(inst, +1, drawYear, drawMonth) ?\n\t\t\t\"<a class='ui-datepicker-next ui-corner-all' data-handler='next' data-event='click'\" +\n\t\t\t\" title='\" + nextText + \"'><span class='ui-icon ui-icon-circle-triangle-\" + ( isRTL ? \"w\" : \"e\") + \"'>\" + nextText + \"</span></a>\" :\n\t\t\t(hideIfNoPrevNext ? \"\" : \"<a class='ui-datepicker-next ui-corner-all ui-state-disabled' title='\"+ nextText + \"'><span class='ui-icon ui-icon-circle-triangle-\" + ( isRTL ? \"w\" : \"e\") + \"'>\" + nextText + \"</span></a>\"));\n\n\t\tcurrentText = this._get(inst, \"currentText\");\n\t\tgotoDate = (this._get(inst, \"gotoCurrent\") && inst.currentDay ? currentDate : today);\n\t\tcurrentText = (!navigationAsDateFormat ? currentText :\n\t\t\tthis.formatDate(currentText, gotoDate, this._getFormatConfig(inst)));\n\n\t\tcontrols = (!inst.inline ? \"<button type='button' class='ui-datepicker-close ui-state-default ui-priority-primary ui-corner-all' data-handler='hide' data-event='click'>\" +\n\t\t\tthis._get(inst, \"closeText\") + \"</button>\" : \"\");\n\n\t\tbuttonPanel = (showButtonPanel) ? \"<div class='ui-datepicker-buttonpane ui-widget-content'>\" + (isRTL ? controls : \"\") +\n\t\t\t(this._isInRange(inst, gotoDate) ? \"<button type='button' class='ui-datepicker-current ui-state-default ui-priority-secondary ui-corner-all' data-handler='today' data-event='click'\" +\n\t\t\t\">\" + currentText + \"</button>\" : \"\") + (isRTL ? \"\" : controls) + \"</div>\" : \"\";\n\n\t\tfirstDay = parseInt(this._get(inst, \"firstDay\"),10);\n\t\tfirstDay = (isNaN(firstDay) ? 0 : firstDay);\n\n\t\tshowWeek = this._get(inst, \"showWeek\");\n\t\tdayNames = this._get(inst, \"dayNames\");\n\t\tdayNamesMin = this._get(inst, \"dayNamesMin\");\n\t\tmonthNames = this._get(inst, \"monthNames\");\n\t\tmonthNamesShort = this._get(inst, \"monthNamesShort\");\n\t\tbeforeShowDay = this._get(inst, \"beforeShowDay\");\n\t\tshowOtherMonths = this._get(inst, \"showOtherMonths\");\n\t\tselectOtherMonths = this._get(inst, \"selectOtherMonths\");\n\t\tdefaultDate = this._getDefaultDate(inst);\n\t\thtml = \"\";\n\t\tdow;\n\t\tfor (row = 0; row < numMonths[0]; row++) {\n\t\t\tgroup = \"\";\n\t\t\tthis.maxRows = 4;\n\t\t\tfor (col = 0; col < numMonths[1]; col++) {\n\t\t\t\tselectedDate = this._daylightSavingAdjust(new Date(drawYear, drawMonth, inst.selectedDay));\n\t\t\t\tcornerClass = \" ui-corner-all\";\n\t\t\t\tcalender = \"\";\n\t\t\t\tif (isMultiMonth) {\n\t\t\t\t\tcalender += \"<div class='ui-datepicker-group\";\n\t\t\t\t\tif (numMonths[1] > 1) {\n\t\t\t\t\t\tswitch (col) {\n\t\t\t\t\t\t\tcase 0: calender += \" ui-datepicker-group-first\";\n\t\t\t\t\t\t\t\tcornerClass = \" ui-corner-\" + (isRTL ? \"right\" : \"left\"); break;\n\t\t\t\t\t\t\tcase numMonths[1]-1: calender += \" ui-datepicker-group-last\";\n\t\t\t\t\t\t\t\tcornerClass = \" ui-corner-\" + (isRTL ? \"left\" : \"right\"); break;\n\t\t\t\t\t\t\tdefault: calender += \" ui-datepicker-group-middle\"; cornerClass = \"\"; break;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tcalender += \"'>\";\n\t\t\t\t}\n\t\t\t\tcalender += \"<div class='ui-datepicker-header ui-widget-header ui-helper-clearfix\" + cornerClass + \"'>\" +\n\t\t\t\t\t(/all|left/.test(cornerClass) && row === 0 ? (isRTL ? next : prev) : \"\") +\n\t\t\t\t\t(/all|right/.test(cornerClass) && row === 0 ? (isRTL ? prev : next) : \"\") +\n\t\t\t\t\tthis._generateMonthYearHeader(inst, drawMonth, drawYear, minDate, maxDate,\n\t\t\t\t\trow > 0 || col > 0, monthNames, monthNamesShort) + // draw month headers\n\t\t\t\t\t\"</div><table class='ui-datepicker-calendar'><thead>\" +\n\t\t\t\t\t\"<tr>\";\n\t\t\t\tthead = (showWeek ? \"<th class='ui-datepicker-week-col'>\" + this._get(inst, \"weekHeader\") + \"</th>\" : \"\");\n\t\t\t\tfor (dow = 0; dow < 7; dow++) { // days of the week\n\t\t\t\t\tday = (dow + firstDay) % 7;\n\t\t\t\t\tthead += \"<th\" + ((dow + firstDay + 6) % 7 >= 5 ? \" class='ui-datepicker-week-end'\" : \"\") + \">\" +\n\t\t\t\t\t\t\"<span title='\" + dayNames[day] + \"'>\" + dayNamesMin[day] + \"</span></th>\";\n\t\t\t\t}\n\t\t\t\tcalender += thead + \"</tr></thead><tbody>\";\n\t\t\t\tdaysInMonth = this._getDaysInMonth(drawYear, drawMonth);\n\t\t\t\tif (drawYear === inst.selectedYear && drawMonth === inst.selectedMonth) {\n\t\t\t\t\tinst.selectedDay = Math.min(inst.selectedDay, daysInMonth);\n\t\t\t\t}\n\t\t\t\tleadDays = (this._getFirstDayOfMonth(drawYear, drawMonth) - firstDay + 7) % 7;\n\t\t\t\tcurRows = Math.ceil((leadDays + daysInMonth) / 7); // calculate the number of rows to generate\n\t\t\t\tnumRows = (isMultiMonth ? this.maxRows > curRows ? this.maxRows : curRows : curRows); //If multiple months, use the higher number of rows (see #7043)\n\t\t\t\tthis.maxRows = numRows;\n\t\t\t\tprintDate = this._daylightSavingAdjust(new Date(drawYear, drawMonth, 1 - leadDays));\n\t\t\t\tfor (dRow = 0; dRow < numRows; dRow++) { // create date picker rows\n\t\t\t\t\tcalender += \"<tr>\";\n\t\t\t\t\ttbody = (!showWeek ? \"\" : \"<td class='ui-datepicker-week-col'>\" +\n\t\t\t\t\t\tthis._get(inst, \"calculateWeek\")(printDate) + \"</td>\");\n\t\t\t\t\tfor (dow = 0; dow < 7; dow++) { // create date picker days\n\t\t\t\t\t\tdaySettings = (beforeShowDay ?\n\t\t\t\t\t\t\tbeforeShowDay.apply((inst.input ? inst.input[0] : null), [printDate]) : [true, \"\"]);\n\t\t\t\t\t\totherMonth = (printDate.getMonth() !== drawMonth);\n\t\t\t\t\t\tunselectable = (otherMonth && !selectOtherMonths) || !daySettings[0] ||\n\t\t\t\t\t\t\t(minDate && printDate < minDate) || (maxDate && printDate > maxDate);\n\t\t\t\t\t\ttbody += \"<td class='\" +\n\t\t\t\t\t\t\t((dow + firstDay + 6) % 7 >= 5 ? \" ui-datepicker-week-end\" : \"\") + // highlight weekends\n\t\t\t\t\t\t\t(otherMonth ? \" ui-datepicker-other-month\" : \"\") + // highlight days from other months\n\t\t\t\t\t\t\t((printDate.getTime() === selectedDate.getTime() && drawMonth === inst.selectedMonth && inst._keyEvent) || // user pressed key\n\t\t\t\t\t\t\t(defaultDate.getTime() === printDate.getTime() && defaultDate.getTime() === selectedDate.getTime()) ?\n\t\t\t\t\t\t\t// or defaultDate is current printedDate and defaultDate is selectedDate\n\t\t\t\t\t\t\t\" \" + this._dayOverClass : \"\") + // highlight selected day\n\t\t\t\t\t\t\t(unselectable ? \" \" + this._unselectableClass + \" ui-state-disabled\": \"\") +  // highlight unselectable days\n\t\t\t\t\t\t\t(otherMonth && !showOtherMonths ? \"\" : \" \" + daySettings[1] + // highlight custom dates\n\t\t\t\t\t\t\t(printDate.getTime() === currentDate.getTime() ? \" \" + this._currentClass : \"\") + // highlight selected day\n\t\t\t\t\t\t\t(printDate.getTime() === today.getTime() ? \" ui-datepicker-today\" : \"\")) + \"'\" + // highlight today (if different)\n\t\t\t\t\t\t\t((!otherMonth || showOtherMonths) && daySettings[2] ? \" title='\" + daySettings[2].replace(/'/g, \"&#39;\") + \"'\" : \"\") + // cell title\n\t\t\t\t\t\t\t(unselectable ? \"\" : \" data-handler='selectDay' data-event='click' data-month='\" + printDate.getMonth() + \"' data-year='\" + printDate.getFullYear() + \"'\") + \">\" + // actions\n\t\t\t\t\t\t\t(otherMonth && !showOtherMonths ? \"&#xa0;\" : // display for other months\n\t\t\t\t\t\t\t(unselectable ? \"<span class='ui-state-default'>\" + printDate.getDate() + \"</span>\" : \"<a class='ui-state-default\" +\n\t\t\t\t\t\t\t(printDate.getTime() === today.getTime() ? \" ui-state-highlight\" : \"\") +\n\t\t\t\t\t\t\t(printDate.getTime() === currentDate.getTime() ? \" ui-state-active\" : \"\") + // highlight selected day\n\t\t\t\t\t\t\t(otherMonth ? \" ui-priority-secondary\" : \"\") + // distinguish dates from other months\n\t\t\t\t\t\t\t\"' href='#'>\" + printDate.getDate() + \"</a>\")) + \"</td>\"; // display selectable date\n\t\t\t\t\t\tprintDate.setDate(printDate.getDate() + 1);\n\t\t\t\t\t\tprintDate = this._daylightSavingAdjust(printDate);\n\t\t\t\t\t}\n\t\t\t\t\tcalender += tbody + \"</tr>\";\n\t\t\t\t}\n\t\t\t\tdrawMonth++;\n\t\t\t\tif (drawMonth > 11) {\n\t\t\t\t\tdrawMonth = 0;\n\t\t\t\t\tdrawYear++;\n\t\t\t\t}\n\t\t\t\tcalender += \"</tbody></table>\" + (isMultiMonth ? \"</div>\" +\n\t\t\t\t\t\t\t((numMonths[0] > 0 && col === numMonths[1]-1) ? \"<div class='ui-datepicker-row-break'></div>\" : \"\") : \"\");\n\t\t\t\tgroup += calender;\n\t\t\t}\n\t\t\thtml += group;\n\t\t}\n\t\thtml += buttonPanel;\n\t\tinst._keyEvent = false;\n\t\treturn html;\n\t},\n\n\t/* Generate the month and year header. */\n\t_generateMonthYearHeader: function(inst, drawMonth, drawYear, minDate, maxDate,\n\t\t\tsecondary, monthNames, monthNamesShort) {\n\n\t\tvar inMinYear, inMaxYear, month, years, thisYear, determineYear, year, endYear,\n\t\t\tchangeMonth = this._get(inst, \"changeMonth\"),\n\t\t\tchangeYear = this._get(inst, \"changeYear\"),\n\t\t\tshowMonthAfterYear = this._get(inst, \"showMonthAfterYear\"),\n\t\t\thtml = \"<div class='ui-datepicker-title'>\",\n\t\t\tmonthHtml = \"\";\n\n\t\t// month selection\n\t\tif (secondary || !changeMonth) {\n\t\t\tmonthHtml += \"<span class='ui-datepicker-month'>\" + monthNames[drawMonth] + \"</span>\";\n\t\t} else {\n\t\t\tinMinYear = (minDate && minDate.getFullYear() === drawYear);\n\t\t\tinMaxYear = (maxDate && maxDate.getFullYear() === drawYear);\n\t\t\tmonthHtml += \"<select class='ui-datepicker-month' data-handler='selectMonth' data-event='change'>\";\n\t\t\tfor ( month = 0; month < 12; month++) {\n\t\t\t\tif ((!inMinYear || month >= minDate.getMonth()) && (!inMaxYear || month <= maxDate.getMonth())) {\n\t\t\t\t\tmonthHtml += \"<option value='\" + month + \"'\" +\n\t\t\t\t\t\t(month === drawMonth ? \" selected='selected'\" : \"\") +\n\t\t\t\t\t\t\">\" + monthNamesShort[month] + \"</option>\";\n\t\t\t\t}\n\t\t\t}\n\t\t\tmonthHtml += \"</select>\";\n\t\t}\n\n\t\tif (!showMonthAfterYear) {\n\t\t\thtml += monthHtml + (secondary || !(changeMonth && changeYear) ? \"&#xa0;\" : \"\");\n\t\t}\n\n\t\t// year selection\n\t\tif ( !inst.yearshtml ) {\n\t\t\tinst.yearshtml = \"\";\n\t\t\tif (secondary || !changeYear) {\n\t\t\t\thtml += \"<span class='ui-datepicker-year'>\" + drawYear + \"</span>\";\n\t\t\t} else {\n\t\t\t\t// determine range of years to display\n\t\t\t\tyears = this._get(inst, \"yearRange\").split(\":\");\n\t\t\t\tthisYear = new Date().getFullYear();\n\t\t\t\tdetermineYear = function(value) {\n\t\t\t\t\tvar year = (value.match(/c[+\\-].*/) ? drawYear + parseInt(value.substring(1), 10) :\n\t\t\t\t\t\t(value.match(/[+\\-].*/) ? thisYear + parseInt(value, 10) :\n\t\t\t\t\t\tparseInt(value, 10)));\n\t\t\t\t\treturn (isNaN(year) ? thisYear : year);\n\t\t\t\t};\n\t\t\t\tyear = determineYear(years[0]);\n\t\t\t\tendYear = Math.max(year, determineYear(years[1] || \"\"));\n\t\t\t\tyear = (minDate ? Math.max(year, minDate.getFullYear()) : year);\n\t\t\t\tendYear = (maxDate ? Math.min(endYear, maxDate.getFullYear()) : endYear);\n\t\t\t\tinst.yearshtml += \"<select class='ui-datepicker-year' data-handler='selectYear' data-event='change'>\";\n\t\t\t\tfor (; year <= endYear; year++) {\n\t\t\t\t\tinst.yearshtml += \"<option value='\" + year + \"'\" +\n\t\t\t\t\t\t(year === drawYear ? \" selected='selected'\" : \"\") +\n\t\t\t\t\t\t\">\" + year + \"</option>\";\n\t\t\t\t}\n\t\t\t\tinst.yearshtml += \"</select>\";\n\n\t\t\t\thtml += inst.yearshtml;\n\t\t\t\tinst.yearshtml = null;\n\t\t\t}\n\t\t}\n\n\t\thtml += this._get(inst, \"yearSuffix\");\n\t\tif (showMonthAfterYear) {\n\t\t\thtml += (secondary || !(changeMonth && changeYear) ? \"&#xa0;\" : \"\") + monthHtml;\n\t\t}\n\t\thtml += \"</div>\"; // Close datepicker_header\n\t\treturn html;\n\t},\n\n\t/* Adjust one of the date sub-fields. */\n\t_adjustInstDate: function(inst, offset, period) {\n\t\tvar year = inst.drawYear + (period === \"Y\" ? offset : 0),\n\t\t\tmonth = inst.drawMonth + (period === \"M\" ? offset : 0),\n\t\t\tday = Math.min(inst.selectedDay, this._getDaysInMonth(year, month)) + (period === \"D\" ? offset : 0),\n\t\t\tdate = this._restrictMinMax(inst, this._daylightSavingAdjust(new Date(year, month, day)));\n\n\t\tinst.selectedDay = date.getDate();\n\t\tinst.drawMonth = inst.selectedMonth = date.getMonth();\n\t\tinst.drawYear = inst.selectedYear = date.getFullYear();\n\t\tif (period === \"M\" || period === \"Y\") {\n\t\t\tthis._notifyChange(inst);\n\t\t}\n\t},\n\n\t/* Ensure a date is within any min/max bounds. */\n\t_restrictMinMax: function(inst, date) {\n\t\tvar minDate = this._getMinMaxDate(inst, \"min\"),\n\t\t\tmaxDate = this._getMinMaxDate(inst, \"max\"),\n\t\t\tnewDate = (minDate && date < minDate ? minDate : date);\n\t\treturn (maxDate && newDate > maxDate ? maxDate : newDate);\n\t},\n\n\t/* Notify change of month/year. */\n\t_notifyChange: function(inst) {\n\t\tvar onChange = this._get(inst, \"onChangeMonthYear\");\n\t\tif (onChange) {\n\t\t\tonChange.apply((inst.input ? inst.input[0] : null),\n\t\t\t\t[inst.selectedYear, inst.selectedMonth + 1, inst]);\n\t\t}\n\t},\n\n\t/* Determine the number of months to show. */\n\t_getNumberOfMonths: function(inst) {\n\t\tvar numMonths = this._get(inst, \"numberOfMonths\");\n\t\treturn (numMonths == null ? [1, 1] : (typeof numMonths === \"number\" ? [1, numMonths] : numMonths));\n\t},\n\n\t/* Determine the current maximum date - ensure no time components are set. */\n\t_getMinMaxDate: function(inst, minMax) {\n\t\treturn this._determineDate(inst, this._get(inst, minMax + \"Date\"), null);\n\t},\n\n\t/* Find the number of days in a given month. */\n\t_getDaysInMonth: function(year, month) {\n\t\treturn 32 - this._daylightSavingAdjust(new Date(year, month, 32)).getDate();\n\t},\n\n\t/* Find the day of the week of the first of a month. */\n\t_getFirstDayOfMonth: function(year, month) {\n\t\treturn new Date(year, month, 1).getDay();\n\t},\n\n\t/* Determines if we should allow a \"next/prev\" month display change. */\n\t_canAdjustMonth: function(inst, offset, curYear, curMonth) {\n\t\tvar numMonths = this._getNumberOfMonths(inst),\n\t\t\tdate = this._daylightSavingAdjust(new Date(curYear,\n\t\t\tcurMonth + (offset < 0 ? offset : numMonths[0] * numMonths[1]), 1));\n\n\t\tif (offset < 0) {\n\t\t\tdate.setDate(this._getDaysInMonth(date.getFullYear(), date.getMonth()));\n\t\t}\n\t\treturn this._isInRange(inst, date);\n\t},\n\n\t/* Is the given date in the accepted range? */\n\t_isInRange: function(inst, date) {\n\t\tvar yearSplit, currentYear,\n\t\t\tminDate = this._getMinMaxDate(inst, \"min\"),\n\t\t\tmaxDate = this._getMinMaxDate(inst, \"max\"),\n\t\t\tminYear = null,\n\t\t\tmaxYear = null,\n\t\t\tyears = this._get(inst, \"yearRange\");\n\t\t\tif (years){\n\t\t\t\tyearSplit = years.split(\":\");\n\t\t\t\tcurrentYear = new Date().getFullYear();\n\t\t\t\tminYear = parseInt(yearSplit[0], 10);\n\t\t\t\tmaxYear = parseInt(yearSplit[1], 10);\n\t\t\t\tif ( yearSplit[0].match(/[+\\-].*/) ) {\n\t\t\t\t\tminYear += currentYear;\n\t\t\t\t}\n\t\t\t\tif ( yearSplit[1].match(/[+\\-].*/) ) {\n\t\t\t\t\tmaxYear += currentYear;\n\t\t\t\t}\n\t\t\t}\n\n\t\treturn ((!minDate || date.getTime() >= minDate.getTime()) &&\n\t\t\t(!maxDate || date.getTime() <= maxDate.getTime()) &&\n\t\t\t(!minYear || date.getFullYear() >= minYear) &&\n\t\t\t(!maxYear || date.getFullYear() <= maxYear));\n\t},\n\n\t/* Provide the configuration settings for formatting/parsing. */\n\t_getFormatConfig: function(inst) {\n\t\tvar shortYearCutoff = this._get(inst, \"shortYearCutoff\");\n\t\tshortYearCutoff = (typeof shortYearCutoff !== \"string\" ? shortYearCutoff :\n\t\t\tnew Date().getFullYear() % 100 + parseInt(shortYearCutoff, 10));\n\t\treturn {shortYearCutoff: shortYearCutoff,\n\t\t\tdayNamesShort: this._get(inst, \"dayNamesShort\"), dayNames: this._get(inst, \"dayNames\"),\n\t\t\tmonthNamesShort: this._get(inst, \"monthNamesShort\"), monthNames: this._get(inst, \"monthNames\")};\n\t},\n\n\t/* Format the given date for display. */\n\t_formatDate: function(inst, day, month, year) {\n\t\tif (!day) {\n\t\t\tinst.currentDay = inst.selectedDay;\n\t\t\tinst.currentMonth = inst.selectedMonth;\n\t\t\tinst.currentYear = inst.selectedYear;\n\t\t}\n\t\tvar date = (day ? (typeof day === \"object\" ? day :\n\t\t\tthis._daylightSavingAdjust(new Date(year, month, day))) :\n\t\t\tthis._daylightSavingAdjust(new Date(inst.currentYear, inst.currentMonth, inst.currentDay)));\n\t\treturn this.formatDate(this._get(inst, \"dateFormat\"), date, this._getFormatConfig(inst));\n\t}\n});\n\n/*\n * Bind hover events for datepicker elements.\n * Done via delegate so the binding only occurs once in the lifetime of the parent div.\n * Global instActive, set by _updateDatepicker allows the handlers to find their way back to the active picker.\n */\nfunction bindHover(dpDiv) {\n\tvar selector = \"button, .ui-datepicker-prev, .ui-datepicker-next, .ui-datepicker-calendar td a\";\n\treturn dpDiv.delegate(selector, \"mouseout\", function() {\n\t\t\t$(this).removeClass(\"ui-state-hover\");\n\t\t\tif (this.className.indexOf(\"ui-datepicker-prev\") !== -1) {\n\t\t\t\t$(this).removeClass(\"ui-datepicker-prev-hover\");\n\t\t\t}\n\t\t\tif (this.className.indexOf(\"ui-datepicker-next\") !== -1) {\n\t\t\t\t$(this).removeClass(\"ui-datepicker-next-hover\");\n\t\t\t}\n\t\t})\n\t\t.delegate(selector, \"mouseover\", function(){\n\t\t\tif (!$.datepicker._isDisabledDatepicker( instActive.inline ? dpDiv.parent()[0] : instActive.input[0])) {\n\t\t\t\t$(this).parents(\".ui-datepicker-calendar\").find(\"a\").removeClass(\"ui-state-hover\");\n\t\t\t\t$(this).addClass(\"ui-state-hover\");\n\t\t\t\tif (this.className.indexOf(\"ui-datepicker-prev\") !== -1) {\n\t\t\t\t\t$(this).addClass(\"ui-datepicker-prev-hover\");\n\t\t\t\t}\n\t\t\t\tif (this.className.indexOf(\"ui-datepicker-next\") !== -1) {\n\t\t\t\t\t$(this).addClass(\"ui-datepicker-next-hover\");\n\t\t\t\t}\n\t\t\t}\n\t\t});\n}\n\n/* jQuery extend now ignores nulls! */\nfunction extendRemove(target, props) {\n\t$.extend(target, props);\n\tfor (var name in props) {\n\t\tif (props[name] == null) {\n\t\t\ttarget[name] = props[name];\n\t\t}\n\t}\n\treturn target;\n}\n\n/* Invoke the datepicker functionality.\n   @param  options  string - a command, optionally followed by additional parameters or\n\t\t\t\t\tObject - settings for attaching new datepicker functionality\n   @return  jQuery object */\n$.fn.datepicker = function(options){\n\n\t/* Verify an empty collection wasn't passed - Fixes #6976 */\n\tif ( !this.length ) {\n\t\treturn this;\n\t}\n\n\t/* Initialise the date picker. */\n\tif (!$.datepicker.initialized) {\n\t\t$(document).mousedown($.datepicker._checkExternalClick);\n\t\t$.datepicker.initialized = true;\n\t}\n\n\t/* Append datepicker main container to body if not exist. */\n\tif ($(\"#\"+$.datepicker._mainDivId).length === 0) {\n\t\t$(\"body\").append($.datepicker.dpDiv);\n\t}\n\n\tvar otherArgs = Array.prototype.slice.call(arguments, 1);\n\tif (typeof options === \"string\" && (options === \"isDisabled\" || options === \"getDate\" || options === \"widget\")) {\n\t\treturn $.datepicker[\"_\" + options + \"Datepicker\"].\n\t\t\tapply($.datepicker, [this[0]].concat(otherArgs));\n\t}\n\tif (options === \"option\" && arguments.length === 2 && typeof arguments[1] === \"string\") {\n\t\treturn $.datepicker[\"_\" + options + \"Datepicker\"].\n\t\t\tapply($.datepicker, [this[0]].concat(otherArgs));\n\t}\n\treturn this.each(function() {\n\t\ttypeof options === \"string\" ?\n\t\t\t$.datepicker[\"_\" + options + \"Datepicker\"].\n\t\t\t\tapply($.datepicker, [this].concat(otherArgs)) :\n\t\t\t$.datepicker._attachDatepicker(this, options);\n\t});\n};\n\n$.datepicker = new Datepicker(); // singleton instance\n$.datepicker.initialized = false;\n$.datepicker.uuid = new Date().getTime();\n$.datepicker.version = \"1.10.4\";\n\n})(jQuery);\n"
  },
  {
    "path": "r2/r2/public/static/js/lib/underscore-1.4.4-1.js",
    "content": "// This file has been modified to include\n// https://github.com/jashkenas/underscore/commit/14c3f9a11fe4711876148c31f9eafba947611477\n// If updated, please be sure to use at least 1.5.2 or backport this patch.\n\n//     Underscore.js 1.4.4\n//     http://underscorejs.org\n//     (c) 2009-2013 Jeremy Ashkenas, DocumentCloud Inc.\n//     Underscore may be freely distributed under the MIT license.\n\n(function() {\n\n  // Baseline setup\n  // --------------\n\n  // Establish the root object, `window` in the browser, or `global` on the server.\n  var root = this;\n\n  // Save the previous value of the `_` variable.\n  var previousUnderscore = root._;\n\n  // Establish the object that gets returned to break out of a loop iteration.\n  var breaker = {};\n\n  // Save bytes in the minified (but not gzipped) version:\n  var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype;\n\n  // Create quick reference variables for speed access to core prototypes.\n  var push             = ArrayProto.push,\n      slice            = ArrayProto.slice,\n      concat           = ArrayProto.concat,\n      toString         = ObjProto.toString,\n      hasOwnProperty   = ObjProto.hasOwnProperty;\n\n  // All **ECMAScript 5** native function implementations that we hope to use\n  // are declared here.\n  var\n    nativeForEach      = ArrayProto.forEach,\n    nativeMap          = ArrayProto.map,\n    nativeReduce       = ArrayProto.reduce,\n    nativeReduceRight  = ArrayProto.reduceRight,\n    nativeFilter       = ArrayProto.filter,\n    nativeEvery        = ArrayProto.every,\n    nativeSome         = ArrayProto.some,\n    nativeIndexOf      = ArrayProto.indexOf,\n    nativeLastIndexOf  = ArrayProto.lastIndexOf,\n    nativeIsArray      = Array.isArray,\n    nativeKeys         = Object.keys,\n    nativeBind         = FuncProto.bind;\n\n  // Create a safe reference to the Underscore object for use below.\n  var _ = function(obj) {\n    if (obj instanceof _) return obj;\n    if (!(this instanceof _)) return new _(obj);\n    this._wrapped = obj;\n  };\n\n  // Export the Underscore object for **Node.js**, with\n  // backwards-compatibility for the old `require()` API. If we're in\n  // the browser, add `_` as a global object via a string identifier,\n  // for Closure Compiler \"advanced\" mode.\n  if (typeof exports !== 'undefined') {\n    if (typeof module !== 'undefined' && module.exports) {\n      exports = module.exports = _;\n    }\n    exports._ = _;\n  } else {\n    root._ = _;\n  }\n\n  // Current version.\n  _.VERSION = '1.4.4';\n\n  // Collection Functions\n  // --------------------\n\n  // The cornerstone, an `each` implementation, aka `forEach`.\n  // Handles objects with the built-in `forEach`, arrays, and raw objects.\n  // Delegates to **ECMAScript 5**'s native `forEach` if available.\n  var each = _.each = _.forEach = function(obj, iterator, context) {\n    if (obj == null) return;\n    if (nativeForEach && obj.forEach === nativeForEach) {\n      obj.forEach(iterator, context);\n    } else if (obj.length === +obj.length) {\n      for (var i = 0, l = obj.length; i < l; i++) {\n        if (iterator.call(context, obj[i], i, obj) === breaker) return;\n      }\n    } else {\n      for (var key in obj) {\n        if (_.has(obj, key)) {\n          if (iterator.call(context, obj[key], key, obj) === breaker) return;\n        }\n      }\n    }\n  };\n\n  // Return the results of applying the iterator to each element.\n  // Delegates to **ECMAScript 5**'s native `map` if available.\n  _.map = _.collect = function(obj, iterator, context) {\n    var results = [];\n    if (obj == null) return results;\n    if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context);\n    each(obj, function(value, index, list) {\n      results[results.length] = iterator.call(context, value, index, list);\n    });\n    return results;\n  };\n\n  var reduceError = 'Reduce of empty array with no initial value';\n\n  // **Reduce** builds up a single result from a list of values, aka `inject`,\n  // or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available.\n  _.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) {\n    var initial = arguments.length > 2;\n    if (obj == null) obj = [];\n    if (nativeReduce && obj.reduce === nativeReduce) {\n      if (context) iterator = _.bind(iterator, context);\n      return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator);\n    }\n    each(obj, function(value, index, list) {\n      if (!initial) {\n        memo = value;\n        initial = true;\n      } else {\n        memo = iterator.call(context, memo, value, index, list);\n      }\n    });\n    if (!initial) throw new TypeError(reduceError);\n    return memo;\n  };\n\n  // The right-associative version of reduce, also known as `foldr`.\n  // Delegates to **ECMAScript 5**'s native `reduceRight` if available.\n  _.reduceRight = _.foldr = function(obj, iterator, memo, context) {\n    var initial = arguments.length > 2;\n    if (obj == null) obj = [];\n    if (nativeReduceRight && obj.reduceRight === nativeReduceRight) {\n      if (context) iterator = _.bind(iterator, context);\n      return initial ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator);\n    }\n    var length = obj.length;\n    if (length !== +length) {\n      var keys = _.keys(obj);\n      length = keys.length;\n    }\n    each(obj, function(value, index, list) {\n      index = keys ? keys[--length] : --length;\n      if (!initial) {\n        memo = obj[index];\n        initial = true;\n      } else {\n        memo = iterator.call(context, memo, obj[index], index, list);\n      }\n    });\n    if (!initial) throw new TypeError(reduceError);\n    return memo;\n  };\n\n  // Return the first value which passes a truth test. Aliased as `detect`.\n  _.find = _.detect = function(obj, iterator, context) {\n    var result;\n    any(obj, function(value, index, list) {\n      if (iterator.call(context, value, index, list)) {\n        result = value;\n        return true;\n      }\n    });\n    return result;\n  };\n\n  // Return all the elements that pass a truth test.\n  // Delegates to **ECMAScript 5**'s native `filter` if available.\n  // Aliased as `select`.\n  _.filter = _.select = function(obj, iterator, context) {\n    var results = [];\n    if (obj == null) return results;\n    if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context);\n    each(obj, function(value, index, list) {\n      if (iterator.call(context, value, index, list)) results[results.length] = value;\n    });\n    return results;\n  };\n\n  // Return all the elements for which a truth test fails.\n  _.reject = function(obj, iterator, context) {\n    return _.filter(obj, function(value, index, list) {\n      return !iterator.call(context, value, index, list);\n    }, context);\n  };\n\n  // Determine whether all of the elements match a truth test.\n  // Delegates to **ECMAScript 5**'s native `every` if available.\n  // Aliased as `all`.\n  _.every = _.all = function(obj, iterator, context) {\n    iterator || (iterator = _.identity);\n    var result = true;\n    if (obj == null) return result;\n    if (nativeEvery && obj.every === nativeEvery) return obj.every(iterator, context);\n    each(obj, function(value, index, list) {\n      if (!(result = result && iterator.call(context, value, index, list))) return breaker;\n    });\n    return !!result;\n  };\n\n  // Determine if at least one element in the object matches a truth test.\n  // Delegates to **ECMAScript 5**'s native `some` if available.\n  // Aliased as `any`.\n  var any = _.some = _.any = function(obj, iterator, context) {\n    iterator || (iterator = _.identity);\n    var result = false;\n    if (obj == null) return result;\n    if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context);\n    each(obj, function(value, index, list) {\n      if (result || (result = iterator.call(context, value, index, list))) return breaker;\n    });\n    return !!result;\n  };\n\n  // Determine if the array or object contains a given value (using `===`).\n  // Aliased as `include`.\n  _.contains = _.include = function(obj, target) {\n    if (obj == null) return false;\n    if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1;\n    return any(obj, function(value) {\n      return value === target;\n    });\n  };\n\n  // Invoke a method (with arguments) on every item in a collection.\n  _.invoke = function(obj, method) {\n    var args = slice.call(arguments, 2);\n    var isFunc = _.isFunction(method);\n    return _.map(obj, function(value) {\n      return (isFunc ? method : value[method]).apply(value, args);\n    });\n  };\n\n  // Convenience version of a common use case of `map`: fetching a property.\n  _.pluck = function(obj, key) {\n    return _.map(obj, function(value){ return value[key]; });\n  };\n\n  // Convenience version of a common use case of `filter`: selecting only objects\n  // containing specific `key:value` pairs.\n  _.where = function(obj, attrs, first) {\n    if (_.isEmpty(attrs)) return first ? null : [];\n    return _[first ? 'find' : 'filter'](obj, function(value) {\n      for (var key in attrs) {\n        if (attrs[key] !== value[key]) return false;\n      }\n      return true;\n    });\n  };\n\n  // Convenience version of a common use case of `find`: getting the first object\n  // containing specific `key:value` pairs.\n  _.findWhere = function(obj, attrs) {\n    return _.where(obj, attrs, true);\n  };\n\n  // Return the maximum element or (element-based computation).\n  // Can't optimize arrays of integers longer than 65,535 elements.\n  // See: https://bugs.webkit.org/show_bug.cgi?id=80797\n  _.max = function(obj, iterator, context) {\n    if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) {\n      return Math.max.apply(Math, obj);\n    }\n    if (!iterator && _.isEmpty(obj)) return -Infinity;\n    var result = {computed : -Infinity, value: -Infinity};\n    each(obj, function(value, index, list) {\n      var computed = iterator ? iterator.call(context, value, index, list) : value;\n      computed >= result.computed && (result = {value : value, computed : computed});\n    });\n    return result.value;\n  };\n\n  // Return the minimum element (or element-based computation).\n  _.min = function(obj, iterator, context) {\n    if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) {\n      return Math.min.apply(Math, obj);\n    }\n    if (!iterator && _.isEmpty(obj)) return Infinity;\n    var result = {computed : Infinity, value: Infinity};\n    each(obj, function(value, index, list) {\n      var computed = iterator ? iterator.call(context, value, index, list) : value;\n      computed < result.computed && (result = {value : value, computed : computed});\n    });\n    return result.value;\n  };\n\n  // Shuffle an array.\n  _.shuffle = function(obj) {\n    var rand;\n    var index = 0;\n    var shuffled = [];\n    each(obj, function(value) {\n      rand = _.random(index++);\n      shuffled[index - 1] = shuffled[rand];\n      shuffled[rand] = value;\n    });\n    return shuffled;\n  };\n\n  // An internal function to generate lookup iterators.\n  var lookupIterator = function(value) {\n    return _.isFunction(value) ? value : function(obj){ return obj[value]; };\n  };\n\n  // Sort the object's values by a criterion produced by an iterator.\n  _.sortBy = function(obj, value, context) {\n    var iterator = lookupIterator(value);\n    return _.pluck(_.map(obj, function(value, index, list) {\n      return {\n        value : value,\n        index : index,\n        criteria : iterator.call(context, value, index, list)\n      };\n    }).sort(function(left, right) {\n      var a = left.criteria;\n      var b = right.criteria;\n      if (a !== b) {\n        if (a > b || a === void 0) return 1;\n        if (a < b || b === void 0) return -1;\n      }\n      return left.index < right.index ? -1 : 1;\n    }), 'value');\n  };\n\n  // An internal function used for aggregate \"group by\" operations.\n  var group = function(obj, value, context, behavior) {\n    var result = {};\n    var iterator = lookupIterator(value || _.identity);\n    each(obj, function(value, index) {\n      var key = iterator.call(context, value, index, obj);\n      behavior(result, key, value);\n    });\n    return result;\n  };\n\n  // Groups the object's values by a criterion. Pass either a string attribute\n  // to group by, or a function that returns the criterion.\n  _.groupBy = function(obj, value, context) {\n    return group(obj, value, context, function(result, key, value) {\n      (_.has(result, key) ? result[key] : (result[key] = [])).push(value);\n    });\n  };\n\n  // Counts instances of an object that group by a certain criterion. Pass\n  // either a string attribute to count by, or a function that returns the\n  // criterion.\n  _.countBy = function(obj, value, context) {\n    return group(obj, value, context, function(result, key) {\n      if (!_.has(result, key)) result[key] = 0;\n      result[key]++;\n    });\n  };\n\n  // Use a comparator function to figure out the smallest index at which\n  // an object should be inserted so as to maintain order. Uses binary search.\n  _.sortedIndex = function(array, obj, iterator, context) {\n    iterator = iterator == null ? _.identity : lookupIterator(iterator);\n    var value = iterator.call(context, obj);\n    var low = 0, high = array.length;\n    while (low < high) {\n      var mid = (low + high) >>> 1;\n      iterator.call(context, array[mid]) < value ? low = mid + 1 : high = mid;\n    }\n    return low;\n  };\n\n  // Safely convert anything iterable into a real, live array.\n  _.toArray = function(obj) {\n    if (!obj) return [];\n    if (_.isArray(obj)) return slice.call(obj);\n    if (obj.length === +obj.length) return _.map(obj, _.identity);\n    return _.values(obj);\n  };\n\n  // Return the number of elements in an object.\n  _.size = function(obj) {\n    if (obj == null) return 0;\n    return (obj.length === +obj.length) ? obj.length : _.keys(obj).length;\n  };\n\n  // Array Functions\n  // ---------------\n\n  // Get the first element of an array. Passing **n** will return the first N\n  // values in the array. Aliased as `head` and `take`. The **guard** check\n  // allows it to work with `_.map`.\n  _.first = _.head = _.take = function(array, n, guard) {\n    if (array == null) return void 0;\n    return (n != null) && !guard ? slice.call(array, 0, n) : array[0];\n  };\n\n  // Returns everything but the last entry of the array. Especially useful on\n  // the arguments object. Passing **n** will return all the values in\n  // the array, excluding the last N. The **guard** check allows it to work with\n  // `_.map`.\n  _.initial = function(array, n, guard) {\n    return slice.call(array, 0, array.length - ((n == null) || guard ? 1 : n));\n  };\n\n  // Get the last element of an array. Passing **n** will return the last N\n  // values in the array. The **guard** check allows it to work with `_.map`.\n  _.last = function(array, n, guard) {\n    if (array == null) return void 0;\n    if ((n != null) && !guard) {\n      return slice.call(array, Math.max(array.length - n, 0));\n    } else {\n      return array[array.length - 1];\n    }\n  };\n\n  // Returns everything but the first entry of the array. Aliased as `tail` and `drop`.\n  // Especially useful on the arguments object. Passing an **n** will return\n  // the rest N values in the array. The **guard**\n  // check allows it to work with `_.map`.\n  _.rest = _.tail = _.drop = function(array, n, guard) {\n    return slice.call(array, (n == null) || guard ? 1 : n);\n  };\n\n  // Trim out all falsy values from an array.\n  _.compact = function(array) {\n    return _.filter(array, _.identity);\n  };\n\n  // Internal implementation of a recursive `flatten` function.\n  var flatten = function(input, shallow, output) {\n    each(input, function(value) {\n      if (_.isArray(value)) {\n        shallow ? push.apply(output, value) : flatten(value, shallow, output);\n      } else {\n        output.push(value);\n      }\n    });\n    return output;\n  };\n\n  // Return a completely flattened version of an array.\n  _.flatten = function(array, shallow) {\n    return flatten(array, shallow, []);\n  };\n\n  // Return a version of the array that does not contain the specified value(s).\n  _.without = function(array) {\n    return _.difference(array, slice.call(arguments, 1));\n  };\n\n  // Produce a duplicate-free version of the array. If the array has already\n  // been sorted, you have the option of using a faster algorithm.\n  // Aliased as `unique`.\n  _.uniq = _.unique = function(array, isSorted, iterator, context) {\n    if (_.isFunction(isSorted)) {\n      context = iterator;\n      iterator = isSorted;\n      isSorted = false;\n    }\n    var initial = iterator ? _.map(array, iterator, context) : array;\n    var results = [];\n    var seen = [];\n    each(initial, function(value, index) {\n      if (isSorted ? (!index || seen[seen.length - 1] !== value) : !_.contains(seen, value)) {\n        seen.push(value);\n        results.push(array[index]);\n      }\n    });\n    return results;\n  };\n\n  // Produce an array that contains the union: each distinct element from all of\n  // the passed-in arrays.\n  _.union = function() {\n    return _.uniq(concat.apply(ArrayProto, arguments));\n  };\n\n  // Produce an array that contains every item shared between all the\n  // passed-in arrays.\n  _.intersection = function(array) {\n    var rest = slice.call(arguments, 1);\n    return _.filter(_.uniq(array), function(item) {\n      return _.every(rest, function(other) {\n        return _.indexOf(other, item) >= 0;\n      });\n    });\n  };\n\n  // Take the difference between one array and a number of other arrays.\n  // Only the elements present in just the first array will remain.\n  _.difference = function(array) {\n    var rest = concat.apply(ArrayProto, slice.call(arguments, 1));\n    return _.filter(array, function(value){ return !_.contains(rest, value); });\n  };\n\n  // Zip together multiple lists into a single array -- elements that share\n  // an index go together.\n  _.zip = function() {\n    var args = slice.call(arguments);\n    var length = _.max(_.pluck(args, 'length'));\n    var results = new Array(length);\n    for (var i = 0; i < length; i++) {\n      results[i] = _.pluck(args, \"\" + i);\n    }\n    return results;\n  };\n\n  // Converts lists into objects. Pass either a single array of `[key, value]`\n  // pairs, or two parallel arrays of the same length -- one of keys, and one of\n  // the corresponding values.\n  _.object = function(list, values) {\n    if (list == null) return {};\n    var result = {};\n    for (var i = 0, l = list.length; i < l; i++) {\n      if (values) {\n        result[list[i]] = values[i];\n      } else {\n        result[list[i][0]] = list[i][1];\n      }\n    }\n    return result;\n  };\n\n  // If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**),\n  // we need this function. Return the position of the first occurrence of an\n  // item in an array, or -1 if the item is not included in the array.\n  // Delegates to **ECMAScript 5**'s native `indexOf` if available.\n  // If the array is large and already in sort order, pass `true`\n  // for **isSorted** to use binary search.\n  _.indexOf = function(array, item, isSorted) {\n    if (array == null) return -1;\n    var i = 0, l = array.length;\n    if (isSorted) {\n      if (typeof isSorted == 'number') {\n        i = (isSorted < 0 ? Math.max(0, l + isSorted) : isSorted);\n      } else {\n        i = _.sortedIndex(array, item);\n        return array[i] === item ? i : -1;\n      }\n    }\n    if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item, isSorted);\n    for (; i < l; i++) if (array[i] === item) return i;\n    return -1;\n  };\n\n  // Delegates to **ECMAScript 5**'s native `lastIndexOf` if available.\n  _.lastIndexOf = function(array, item, from) {\n    if (array == null) return -1;\n    var hasIndex = from != null;\n    if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) {\n      return hasIndex ? array.lastIndexOf(item, from) : array.lastIndexOf(item);\n    }\n    var i = (hasIndex ? from : array.length);\n    while (i--) if (array[i] === item) return i;\n    return -1;\n  };\n\n  // Generate an integer Array containing an arithmetic progression. A port of\n  // the native Python `range()` function. See\n  // [the Python documentation](http://docs.python.org/library/functions.html#range).\n  _.range = function(start, stop, step) {\n    if (arguments.length <= 1) {\n      stop = start || 0;\n      start = 0;\n    }\n    step = arguments[2] || 1;\n\n    var len = Math.max(Math.ceil((stop - start) / step), 0);\n    var idx = 0;\n    var range = new Array(len);\n\n    while(idx < len) {\n      range[idx++] = start;\n      start += step;\n    }\n\n    return range;\n  };\n\n  // Function (ahem) Functions\n  // ------------------\n\n  // Create a function bound to a given object (assigning `this`, and arguments,\n  // optionally). Delegates to **ECMAScript 5**'s native `Function.bind` if\n  // available.\n  _.bind = function(func, context) {\n    if (func.bind === nativeBind && nativeBind) return nativeBind.apply(func, slice.call(arguments, 1));\n    var args = slice.call(arguments, 2);\n    return function() {\n      return func.apply(context, args.concat(slice.call(arguments)));\n    };\n  };\n\n  // Partially apply a function by creating a version that has had some of its\n  // arguments pre-filled, without changing its dynamic `this` context.\n  _.partial = function(func) {\n    var args = slice.call(arguments, 1);\n    return function() {\n      return func.apply(this, args.concat(slice.call(arguments)));\n    };\n  };\n\n  // Bind all of an object's methods to that object. Useful for ensuring that\n  // all callbacks defined on an object belong to it.\n  _.bindAll = function(obj) {\n    var funcs = slice.call(arguments, 1);\n    if (funcs.length === 0) funcs = _.functions(obj);\n    each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); });\n    return obj;\n  };\n\n  // Memoize an expensive function by storing its results.\n  _.memoize = function(func, hasher) {\n    var memo = {};\n    hasher || (hasher = _.identity);\n    return function() {\n      var key = hasher.apply(this, arguments);\n      return _.has(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments));\n    };\n  };\n\n  // Delays a function for the given number of milliseconds, and then calls\n  // it with the arguments supplied.\n  _.delay = function(func, wait) {\n    var args = slice.call(arguments, 2);\n    return setTimeout(function(){ return func.apply(null, args); }, wait);\n  };\n\n  // Defers a function, scheduling it to run after the current call stack has\n  // cleared.\n  _.defer = function(func) {\n    return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1)));\n  };\n\n  // Returns a function, that, when invoked, will only be triggered at most once\n  // during a given window of time.\n  _.throttle = function(func, wait) {\n    var context, args, timeout, result;\n    var previous = 0;\n    var later = function() {\n      previous = new Date;\n      timeout = null;\n      result = func.apply(context, args);\n    };\n    return function() {\n      var now = new Date;\n      var remaining = wait - (now - previous);\n      context = this;\n      args = arguments;\n      if (remaining <= 0) {\n        clearTimeout(timeout);\n        timeout = null;\n        previous = now;\n        result = func.apply(context, args);\n      } else if (!timeout) {\n        timeout = setTimeout(later, remaining);\n      }\n      return result;\n    };\n  };\n\n  // Returns a function, that, as long as it continues to be invoked, will not\n  // be triggered. The function will be called after it stops being called for\n  // N milliseconds. If `immediate` is passed, trigger the function on the\n  // leading edge, instead of the trailing.\n  _.debounce = function(func, wait, immediate) {\n    var timeout, result;\n    return function() {\n      var context = this, args = arguments;\n      var later = function() {\n        timeout = null;\n        if (!immediate) result = func.apply(context, args);\n      };\n      var callNow = immediate && !timeout;\n      clearTimeout(timeout);\n      timeout = setTimeout(later, wait);\n      if (callNow) result = func.apply(context, args);\n      return result;\n    };\n  };\n\n  // Returns a function that will be executed at most one time, no matter how\n  // often you call it. Useful for lazy initialization.\n  _.once = function(func) {\n    var ran = false, memo;\n    return function() {\n      if (ran) return memo;\n      ran = true;\n      memo = func.apply(this, arguments);\n      func = null;\n      return memo;\n    };\n  };\n\n  // Returns the first function passed as an argument to the second,\n  // allowing you to adjust arguments, run code before and after, and\n  // conditionally execute the original function.\n  _.wrap = function(func, wrapper) {\n    return function() {\n      var args = [func];\n      push.apply(args, arguments);\n      return wrapper.apply(this, args);\n    };\n  };\n\n  // Returns a function that is the composition of a list of functions, each\n  // consuming the return value of the function that follows.\n  _.compose = function() {\n    var funcs = arguments;\n    return function() {\n      var args = arguments;\n      for (var i = funcs.length - 1; i >= 0; i--) {\n        args = [funcs[i].apply(this, args)];\n      }\n      return args[0];\n    };\n  };\n\n  // Returns a function that will only be executed after being called N times.\n  _.after = function(times, func) {\n    if (times <= 0) return func();\n    return function() {\n      if (--times < 1) {\n        return func.apply(this, arguments);\n      }\n    };\n  };\n\n  // Object Functions\n  // ----------------\n\n  // Retrieve the names of an object's properties.\n  // Delegates to **ECMAScript 5**'s native `Object.keys`\n  _.keys = nativeKeys || function(obj) {\n    if (obj !== Object(obj)) throw new TypeError('Invalid object');\n    var keys = [];\n    for (var key in obj) if (_.has(obj, key)) keys[keys.length] = key;\n    return keys;\n  };\n\n  // Retrieve the values of an object's properties.\n  _.values = function(obj) {\n    var values = [];\n    for (var key in obj) if (_.has(obj, key)) values.push(obj[key]);\n    return values;\n  };\n\n  // Convert an object into a list of `[key, value]` pairs.\n  _.pairs = function(obj) {\n    var pairs = [];\n    for (var key in obj) if (_.has(obj, key)) pairs.push([key, obj[key]]);\n    return pairs;\n  };\n\n  // Invert the keys and values of an object. The values must be serializable.\n  _.invert = function(obj) {\n    var result = {};\n    for (var key in obj) if (_.has(obj, key)) result[obj[key]] = key;\n    return result;\n  };\n\n  // Return a sorted list of the function names available on the object.\n  // Aliased as `methods`\n  _.functions = _.methods = function(obj) {\n    var names = [];\n    for (var key in obj) {\n      if (_.isFunction(obj[key])) names.push(key);\n    }\n    return names.sort();\n  };\n\n  // Extend a given object with all the properties in passed-in object(s).\n  _.extend = function(obj) {\n    each(slice.call(arguments, 1), function(source) {\n      if (source) {\n        for (var prop in source) {\n          obj[prop] = source[prop];\n        }\n      }\n    });\n    return obj;\n  };\n\n  // Return a copy of the object only containing the whitelisted properties.\n  _.pick = function(obj) {\n    var copy = {};\n    var keys = concat.apply(ArrayProto, slice.call(arguments, 1));\n    each(keys, function(key) {\n      if (key in obj) copy[key] = obj[key];\n    });\n    return copy;\n  };\n\n   // Return a copy of the object without the blacklisted properties.\n  _.omit = function(obj) {\n    var copy = {};\n    var keys = concat.apply(ArrayProto, slice.call(arguments, 1));\n    for (var key in obj) {\n      if (!_.contains(keys, key)) copy[key] = obj[key];\n    }\n    return copy;\n  };\n\n  // Fill in a given object with default properties.\n  _.defaults = function(obj) {\n    each(slice.call(arguments, 1), function(source) {\n      if (source) {\n        for (var prop in source) {\n          if (obj[prop] == null) obj[prop] = source[prop];\n        }\n      }\n    });\n    return obj;\n  };\n\n  // Create a (shallow-cloned) duplicate of an object.\n  _.clone = function(obj) {\n    if (!_.isObject(obj)) return obj;\n    return _.isArray(obj) ? obj.slice() : _.extend({}, obj);\n  };\n\n  // Invokes interceptor with the obj, and then returns obj.\n  // The primary purpose of this method is to \"tap into\" a method chain, in\n  // order to perform operations on intermediate results within the chain.\n  _.tap = function(obj, interceptor) {\n    interceptor(obj);\n    return obj;\n  };\n\n  // Internal recursive comparison function for `isEqual`.\n  var eq = function(a, b, aStack, bStack) {\n    // Identical objects are equal. `0 === -0`, but they aren't identical.\n    // See the Harmony `egal` proposal: http://wiki.ecmascript.org/doku.php?id=harmony:egal.\n    if (a === b) return a !== 0 || 1 / a == 1 / b;\n    // A strict comparison is necessary because `null == undefined`.\n    if (a == null || b == null) return a === b;\n    // Unwrap any wrapped objects.\n    if (a instanceof _) a = a._wrapped;\n    if (b instanceof _) b = b._wrapped;\n    // Compare `[[Class]]` names.\n    var className = toString.call(a);\n    if (className != toString.call(b)) return false;\n    switch (className) {\n      // Strings, numbers, dates, and booleans are compared by value.\n      case '[object String]':\n        // Primitives and their corresponding object wrappers are equivalent; thus, `\"5\"` is\n        // equivalent to `new String(\"5\")`.\n        return a == String(b);\n      case '[object Number]':\n        // `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for\n        // other numeric values.\n        return a != +a ? b != +b : (a == 0 ? 1 / a == 1 / b : a == +b);\n      case '[object Date]':\n      case '[object Boolean]':\n        // Coerce dates and booleans to numeric primitive values. Dates are compared by their\n        // millisecond representations. Note that invalid dates with millisecond representations\n        // of `NaN` are not equivalent.\n        return +a == +b;\n      // RegExps are compared by their source patterns and flags.\n      case '[object RegExp]':\n        return a.source == b.source &&\n               a.global == b.global &&\n               a.multiline == b.multiline &&\n               a.ignoreCase == b.ignoreCase;\n    }\n    if (typeof a != 'object' || typeof b != 'object') return false;\n    // Assume equality for cyclic structures. The algorithm for detecting cyclic\n    // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`.\n    var length = aStack.length;\n    while (length--) {\n      // Linear search. Performance is inversely proportional to the number of\n      // unique nested structures.\n      if (aStack[length] == a) return bStack[length] == b;\n    }\n    // Add the first object to the stack of traversed objects.\n    aStack.push(a);\n    bStack.push(b);\n    var size = 0, result = true;\n    // Recursively compare objects and arrays.\n    if (className == '[object Array]') {\n      // Compare array lengths to determine if a deep comparison is necessary.\n      size = a.length;\n      result = size == b.length;\n      if (result) {\n        // Deep compare the contents, ignoring non-numeric properties.\n        while (size--) {\n          if (!(result = eq(a[size], b[size], aStack, bStack))) break;\n        }\n      }\n    } else {\n      // Objects with different constructors are not equivalent, but `Object`s\n      // from different frames are.\n      var aCtor = a.constructor, bCtor = b.constructor;\n      if (aCtor !== bCtor && !(_.isFunction(aCtor) && (aCtor instanceof aCtor) &&\n                               _.isFunction(bCtor) && (bCtor instanceof bCtor))) {\n        return false;\n      }\n      // Deep compare objects.\n      for (var key in a) {\n        if (_.has(a, key)) {\n          // Count the expected number of properties.\n          size++;\n          // Deep compare each member.\n          if (!(result = _.has(b, key) && eq(a[key], b[key], aStack, bStack))) break;\n        }\n      }\n      // Ensure that both objects contain the same number of properties.\n      if (result) {\n        for (key in b) {\n          if (_.has(b, key) && !(size--)) break;\n        }\n        result = !size;\n      }\n    }\n    // Remove the first object from the stack of traversed objects.\n    aStack.pop();\n    bStack.pop();\n    return result;\n  };\n\n  // Perform a deep comparison to check if two objects are equal.\n  _.isEqual = function(a, b) {\n    return eq(a, b, [], []);\n  };\n\n  // Is a given array, string, or object empty?\n  // An \"empty\" object has no enumerable own-properties.\n  _.isEmpty = function(obj) {\n    if (obj == null) return true;\n    if (_.isArray(obj) || _.isString(obj)) return obj.length === 0;\n    for (var key in obj) if (_.has(obj, key)) return false;\n    return true;\n  };\n\n  // Is a given value a DOM element?\n  _.isElement = function(obj) {\n    return !!(obj && obj.nodeType === 1);\n  };\n\n  // Is a given value an array?\n  // Delegates to ECMA5's native Array.isArray\n  _.isArray = nativeIsArray || function(obj) {\n    return toString.call(obj) == '[object Array]';\n  };\n\n  // Is a given variable an object?\n  _.isObject = function(obj) {\n    return obj === Object(obj);\n  };\n\n  // Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp.\n  each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp'], function(name) {\n    _['is' + name] = function(obj) {\n      return toString.call(obj) == '[object ' + name + ']';\n    };\n  });\n\n  // Define a fallback version of the method in browsers (ahem, IE), where\n  // there isn't any inspectable \"Arguments\" type.\n  if (!_.isArguments(arguments)) {\n    _.isArguments = function(obj) {\n      return !!(obj && _.has(obj, 'callee'));\n    };\n  }\n\n  // Optimize `isFunction` if appropriate.\n  if (typeof (/./) !== 'function') {\n    _.isFunction = function(obj) {\n      return typeof obj === 'function';\n    };\n  }\n\n  // Is a given object a finite number?\n  _.isFinite = function(obj) {\n    return isFinite(obj) && !isNaN(parseFloat(obj));\n  };\n\n  // Is the given value `NaN`? (NaN is the only number which does not equal itself).\n  _.isNaN = function(obj) {\n    return _.isNumber(obj) && obj != +obj;\n  };\n\n  // Is a given value a boolean?\n  _.isBoolean = function(obj) {\n    return obj === true || obj === false || toString.call(obj) == '[object Boolean]';\n  };\n\n  // Is a given value equal to null?\n  _.isNull = function(obj) {\n    return obj === null;\n  };\n\n  // Is a given variable undefined?\n  _.isUndefined = function(obj) {\n    return obj === void 0;\n  };\n\n  // Shortcut function for checking if an object has a given property directly\n  // on itself (in other words, not on a prototype).\n  _.has = function(obj, key) {\n    return hasOwnProperty.call(obj, key);\n  };\n\n  // Utility Functions\n  // -----------------\n\n  // Run Underscore.js in *noConflict* mode, returning the `_` variable to its\n  // previous owner. Returns a reference to the Underscore object.\n  _.noConflict = function() {\n    root._ = previousUnderscore;\n    return this;\n  };\n\n  // Keep the identity function around for default iterators.\n  _.identity = function(value) {\n    return value;\n  };\n\n  // Run a function **n** times.\n  _.times = function(n, iterator, context) {\n    var accum = Array(n);\n    for (var i = 0; i < n; i++) accum[i] = iterator.call(context, i);\n    return accum;\n  };\n\n  // Return a random integer between min and max (inclusive).\n  _.random = function(min, max) {\n    if (max == null) {\n      max = min;\n      min = 0;\n    }\n    return min + Math.floor(Math.random() * (max - min + 1));\n  };\n\n  // List of HTML entities for escaping.\n  var entityMap = {\n    escape: {\n      '&': '&amp;',\n      '<': '&lt;',\n      '>': '&gt;',\n      '\"': '&quot;',\n      \"'\": '&#x27;',\n      /* This isn't needed and is causing problems.\n         See https://github.com/jashkenas/underscore/commit/14c3f9a11fe4711876148c31f9eafba947611477\n\n      '/': '&#x2F;'\n\n      */\n    }\n  };\n  entityMap.unescape = _.invert(entityMap.escape);\n\n  // Regexes containing the keys and values listed immediately above.\n  var entityRegexes = {\n    escape:   new RegExp('[' + _.keys(entityMap.escape).join('') + ']', 'g'),\n    unescape: new RegExp('(' + _.keys(entityMap.unescape).join('|') + ')', 'g')\n  };\n\n  // Functions for escaping and unescaping strings to/from HTML interpolation.\n  _.each(['escape', 'unescape'], function(method) {\n    _[method] = function(string) {\n      if (string == null) return '';\n      return ('' + string).replace(entityRegexes[method], function(match) {\n        return entityMap[method][match];\n      });\n    };\n  });\n\n  // If the value of the named property is a function then invoke it;\n  // otherwise, return it.\n  _.result = function(object, property) {\n    if (object == null) return null;\n    var value = object[property];\n    return _.isFunction(value) ? value.call(object) : value;\n  };\n\n  // Add your own custom functions to the Underscore object.\n  _.mixin = function(obj) {\n    each(_.functions(obj), function(name){\n      var func = _[name] = obj[name];\n      _.prototype[name] = function() {\n        var args = [this._wrapped];\n        push.apply(args, arguments);\n        return result.call(this, func.apply(_, args));\n      };\n    });\n  };\n\n  // Generate a unique integer id (unique within the entire client session).\n  // Useful for temporary DOM ids.\n  var idCounter = 0;\n  _.uniqueId = function(prefix) {\n    var id = ++idCounter + '';\n    return prefix ? prefix + id : id;\n  };\n\n  // By default, Underscore uses ERB-style template delimiters, change the\n  // following template settings to use alternative delimiters.\n  _.templateSettings = {\n    evaluate    : /<%([\\s\\S]+?)%>/g,\n    interpolate : /<%=([\\s\\S]+?)%>/g,\n    escape      : /<%-([\\s\\S]+?)%>/g\n  };\n\n  // When customizing `templateSettings`, if you don't want to define an\n  // interpolation, evaluation or escaping regex, we need one that is\n  // guaranteed not to match.\n  var noMatch = /(.)^/;\n\n  // Certain characters need to be escaped so that they can be put into a\n  // string literal.\n  var escapes = {\n    \"'\":      \"'\",\n    '\\\\':     '\\\\',\n    '\\r':     'r',\n    '\\n':     'n',\n    '\\t':     't',\n    '\\u2028': 'u2028',\n    '\\u2029': 'u2029'\n  };\n\n  var escaper = /\\\\|'|\\r|\\n|\\t|\\u2028|\\u2029/g;\n\n  // JavaScript micro-templating, similar to John Resig's implementation.\n  // Underscore templating handles arbitrary delimiters, preserves whitespace,\n  // and correctly escapes quotes within interpolated code.\n  _.template = function(text, data, settings) {\n    var render;\n    settings = _.defaults({}, settings, _.templateSettings);\n\n    // Combine delimiters into one regular expression via alternation.\n    var matcher = new RegExp([\n      (settings.escape || noMatch).source,\n      (settings.interpolate || noMatch).source,\n      (settings.evaluate || noMatch).source\n    ].join('|') + '|$', 'g');\n\n    // Compile the template source, escaping string literals appropriately.\n    var index = 0;\n    var source = \"__p+='\";\n    text.replace(matcher, function(match, escape, interpolate, evaluate, offset) {\n      source += text.slice(index, offset)\n        .replace(escaper, function(match) { return '\\\\' + escapes[match]; });\n\n      if (escape) {\n        source += \"'+\\n((__t=(\" + escape + \"))==null?'':_.escape(__t))+\\n'\";\n      }\n      if (interpolate) {\n        source += \"'+\\n((__t=(\" + interpolate + \"))==null?'':__t)+\\n'\";\n      }\n      if (evaluate) {\n        source += \"';\\n\" + evaluate + \"\\n__p+='\";\n      }\n      index = offset + match.length;\n      return match;\n    });\n    source += \"';\\n\";\n\n    // If a variable is not specified, place data values in local scope.\n    if (!settings.variable) source = 'with(obj||{}){\\n' + source + '}\\n';\n\n    source = \"var __t,__p='',__j=Array.prototype.join,\" +\n      \"print=function(){__p+=__j.call(arguments,'');};\\n\" +\n      source + \"return __p;\\n\";\n\n    try {\n      render = new Function(settings.variable || 'obj', '_', source);\n    } catch (e) {\n      e.source = source;\n      throw e;\n    }\n\n    if (data) return render(data, _);\n    var template = function(data) {\n      return render.call(this, data, _);\n    };\n\n    // Provide the compiled function source as a convenience for precompilation.\n    template.source = 'function(' + (settings.variable || 'obj') + '){\\n' + source + '}';\n\n    return template;\n  };\n\n  // Add a \"chain\" function, which will delegate to the wrapper.\n  _.chain = function(obj) {\n    return _(obj).chain();\n  };\n\n  // OOP\n  // ---------------\n  // If Underscore is called as a function, it returns a wrapped object that\n  // can be used OO-style. This wrapper holds altered versions of all the\n  // underscore functions. Wrapped objects may be chained.\n\n  // Helper function to continue chaining intermediate results.\n  var result = function(obj) {\n    return this._chain ? _(obj).chain() : obj;\n  };\n\n  // Add all of the Underscore functions to the wrapper object.\n  _.mixin(_);\n\n  // Add all mutator Array functions to the wrapper.\n  each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) {\n    var method = ArrayProto[name];\n    _.prototype[name] = function() {\n      var obj = this._wrapped;\n      method.apply(obj, arguments);\n      if ((name == 'shift' || name == 'splice') && obj.length === 0) delete obj[0];\n      return result.call(this, obj);\n    };\n  });\n\n  // Add all accessor Array functions to the wrapper.\n  each(['concat', 'join', 'slice'], function(name) {\n    var method = ArrayProto[name];\n    _.prototype[name] = function() {\n      return result.call(this, method.apply(this._wrapped, arguments));\n    };\n  });\n\n  _.extend(_.prototype, {\n\n    // Start chaining a wrapped Underscore object.\n    chain: function() {\n      this._chain = true;\n      return this;\n    },\n\n    // Extracts the result from a wrapped and chained object.\n    value: function() {\n      return this._wrapped;\n    }\n\n  });\n\n}).call(this);\n"
  },
  {
    "path": "r2/r2/public/static/js/link-click-tracking.js",
    "content": "/* The ready method */\n$(function() {\n  var clientTime = Date.now();\n  var serverTime = r.config.server_time * 1000;\n  var disabledDueToDrift = false;\n  var beaconsEnabled = r.config.feature_outbound_beacons && 'sendBeacon' in window.navigator;\n\n  // If the browser supports beacons, we set the outbound URL\n  // here on mousedown or other navigate action. Then, before navigate\n  // we will send a beacon derived from this element. On mouseup we clear\n  // this field.\n  var beaconURL = null;\n\n  // if our server time is more than 5 minutes greater than client time, that\n  // means our client clock has future drift. Disable due to hmac signing\n  // expiration not being reliable.\n  if (serverTime > clientTime + (5 * 60 * 1000)) {\n    disabledDueToDrift = true;\n  }\n\n  // if our server time is more than 60 minutes in the past, that means our\n  // client clock has drift or this whole page has been cached for more than\n  // an hour. Disable due to hmac signing expiration not being reliable.\n  if (serverTime < clientTime - (60 * 60 * 1000)) {\n    disabledDueToDrift = true;\n  }\n\n  function setOutboundURL(elem) {\n    // log trackable clicks, either through redirect or beacon\n    var $elem = $(elem);\n    var now = Date.now();\n\n    var url;\n    if ($elem.attr('data-inbound-url')) {\n      url = $elem.attr('data-inbound-url');\n    } else if (!disabledDueToDrift && $elem.attr('data-outbound-expiration') > now) {\n      // Use outbound tracking link if it has not expired.\n      url = $elem.attr('data-outbound-url');\n    }\n\n    if (url) {\n      if (beaconsEnabled) {\n        beaconURL = url;\n      } else {\n        elem.href = url;\n      }\n    }\n\n    return true;\n  }\n\n  function resetOriginalURL(elem) {\n    /* after clicking outbound link, reset url for clean mouseover view */\n    if (beaconsEnabled) {\n      beaconURL = null;\n    } else {\n      elem.href = $(elem).attr('data-href-url');\n    }\n    return true;\n  }\n\n  var $delegate = $(\"a[data-outbound-url], a[data-inbound-url], .sitetable, organic-listing\");\n  var linkSelector = 'a[data-outbound-url], a[data-inbound-url]';\n  /* mouse click */\n  $delegate.on('mousedown', linkSelector, function(e) {\n    // if right click (context menu), don't show redirect url\n    if (e.which === 3) {\n      return true;\n    }\n    return setOutboundURL(this);\n  });\n\n  $delegate.on('mouseleave', linkSelector, function() {\n    return resetOriginalURL(this);\n  });\n\n  /* keyboard nav */\n  $delegate.on('keydown', linkSelector, function(e) {\n    if (e.which === 13) {\n      setOutboundURL(this);\n    }\n    return true;\n  });\n\n  $delegate.on('keyup', linkSelector, function(e) {\n    // If ctrl (17) + click was used, reset the url\n    // when ctrl has been released, so that a user\n    // can copy the correct link without leaving\n    if (e.which === 13 || e.which === 17) {\n      resetOriginalURL(this);\n    }\n    return true;\n  });\n\n  /* touch device click */\n  $delegate.on('touchstart', linkSelector,  function(e) {\n    return setOutboundURL(this);\n  });\n\n  $(window).on('unload', function() {\n    if (beaconsEnabled && beaconURL !== null) {\n      // For the clicktracker, a GET to the tracking URL (with query parameters) will\n      // perform a redirect to the destination. A POST to the same url (with query\n      // parameters as payload) will return a 204 No Content (to support beacons).\n      // So we take our redirect URL and split it into a POST of the query parameters\n      var tempA = $('<a>', {href: beaconURL})[0];\n      var beaconPath = tempA.protocol + '//' + tempA.hostname + tempA.pathname;\n      var beaconPayload = tempA.search.replace(/^\\?/, '');\n\n      navigator.sendBeacon(beaconPath, beaconPayload);\n    }\n  });\n});\n"
  },
  {
    "path": "r2/r2/public/static/js/locked.js",
    "content": "/*\n  If the current post is locked, show a modal on restricted actions.\n\n  requires r.config (base.js)\n  requires r.access (access.js)\n  requires r.ui.Popup (popup.js)\n*/\n!function(r) {\n  // initialized early so click handlers can be bound on declaration\n  r.locked = {};\n\n  _.extend(r.locked, {\n    init: function() {\n      $('body').on('click', '.locked-error', this._handleClick);\n\n      this._popup = r.ui.createGatePopup({\n        templateId: 'locked-popup',\n        className: 'locked-error-modal',\n      });\n    },\n\n    _handleClick: function onClick(e) {\n      if (r.access.isLinkRestricted(e.target)) {\n        return;\n      }\n\n      this._popup.show();\n      return false;\n    }.bind(r.locked),\n  });\n}(r);\n"
  },
  {
    "path": "r2/r2/public/static/js/logging.js",
    "content": "r.logging = {}\n\nr.logging.pageAgeLimit = 5*60  // seconds\nr.logging.sendThrottle = 8  // seconds\n\nr.logging.exceptionMessageTemplate = _.template('Client Error: \"<%= errorType %>\" thrown at ' +\n                                        'L<%= line %>:<%= character %> ' +\n                                        'in <%= file %> Message: \"<%= message %>\"')\n\nr.logging.defaultExceptionValues = {\n  message: 'UNKNOWN MESSAGE',\n  file: 'UNKNOWN FILE',\n  line: '?',\n  character: '?',\n  errorType: 'UNKNOWN ERROR TYPE',\n}\n\nr.logging.sendException = function(exception) {\n  if (!exception) {\n    throw 'No exception object was passed in.'\n  }\n\n  _.defaults(exception, r.logging.defaultExceptionValues)\n  var errorMessage = r.logging.exceptionMessageTemplate(exception)\n\n  r.logging.sendError(errorMessage, { tag: 'unknown' })\n}\n\nr.logging.init = function() {\n    _.each(['debug', 'log', 'warn', 'error'], function(name) {\n        // suppress debug messages unless config.debug is set\n        r[name] = (name != 'debug' || r.config.debug)\n                && window.console && console[name]\n                ? _.bind(console[name], console)\n                : function() {}\n    })\n    r.sendError = r.logging.sendError\n}\n\nr.logging.serverLogger = {\n    logCount: 0,\n    _queuedLogs: [],\n\n    queueLog: function(logData) {\n        // Just in case we get an error before config is initialized. Can happen while parsing files.\n        if (!r.config) {\n          return\n        }\n\n        if (!r.warn) {\n          r.warn = function(){}\n        }\n\n        if (this.logCount >= 3) {\n            r.warn('Not sending debug log; already sent', this.logCount)\n            return\n        }\n\n        // don't send messages for pages older than 5 minutes to prevent CDN \n        // cached pages from slamming us if we need to turn off logs\n        var pageAge = (new Date / 1000) - r.config.server_time\n        if (Math.abs(pageAge) > r.logging.pageAgeLimit) {\n            r.warn('Not sending debug log; page too old:', pageAge)\n            return\n        }\n\n        if (!r.config.send_logs) {\n            r.warn('Server-side debug logging disabled')\n            return\n        }\n\n        logData.url = window.location.toString()\n        this._queuedLogs.push(logData)\n        this.logCount++\n\n        // defer so that errors get batched until JS yields\n        _.defer(_.bind(function() {\n            this._sendLogs()\n        }, this))\n    },\n\n    _sendLogs: _.throttle(function() {\n        var queueCount = this._queuedLogs.length\n        r.ajax({\n            type: 'POST',\n            url: '/web/log/error.json',\n            data: {logs: JSON.stringify(this._queuedLogs)},\n            headers: {\n                'X-Loggit': true\n            },\n            success: function() {\n                r.log('Sent', queueCount, 'debug logs to server')\n            },\n            error: function(xhr, err, status) {\n                r.warn('Error sending debug logs to server:', err, ';', status)\n            }\n        })\n        this._queuedLogs = []\n    }, r.logging.sendThrottle * 1000)\n}\n\nr.logging.sendError = function() {\n    var args = _.toArray(arguments)\n    var lastArg = _.last(args)\n    var options = {}\n\n    if (_.isObject(lastArg)) {\n      options = lastArg\n      args.pop()\n    }\n\n    var log = _.defaults({\n      msg: args.join(' ')\n    }, options)\n\n    if (r.error) {\n      r.error.apply(r, arguments)\n    }\n\n    r.logging.serverLogger.queueLog(log)\n}\n\nr.hooks.get('setup').register(function() {\n    r.logging.init();\n});\n"
  },
  {
    "path": "r2/r2/public/static/js/login.js",
    "content": "r.login = {\n    post: function(form, action) {\n        var username = $('input[name=\"user\"]', form.$el).val(),\n            endpoint = r.config.https_endpoint || ('http://'+r.config.ajax_domain),\n            apiTarget = endpoint+'/api/'+action+'/'+username\n\n        if (r.config.currentOrigin == endpoint || $.support.cors) {\n            var params = form.serialize()\n            params.push({name:'api_type', value:'json'})\n            return $.ajax({\n                url: apiTarget,\n                type: 'POST',\n                dataType: 'json',\n                data: params,\n                xhrFields: {\n                    withCredentials: true\n                }\n            })\n        } else {\n            var iframe = $('<iframe>'),\n                postForm = form.$el.clone(true),\n                frameName = ('resp'+Math.random()).replace('.', '')\n\n            iframe\n                .css('display', 'none')\n                .attr('name', frameName)\n                .appendTo('body')\n\n            iframe[0].contentWindow.name = frameName\n\n            postForm\n                .unbind()\n                .css('display', 'none')\n                .attr('action', apiTarget)\n                .attr('target', frameName)\n                .appendTo('body')\n            \n            $('<input>')\n                .attr({\n                    type: 'hidden',\n                    name: 'api_type',\n                    value: 'json'\n                })\n                .appendTo(postForm)\n\n            $('<input>')\n                .attr({\n                    type: 'hidden',\n                    name: 'hoist',\n                    value: r.login.hoist.type\n                })\n                .appendTo(postForm)\n\n            var deferred = r.login.hoist.watch(action)\n            if (!r.config.debug) {\n                deferred.done(function() {\n                    iframe.remove()\n                    postForm.remove()\n                })\n            }\n\n            postForm.submit()\n            return deferred\n        }\n    }\n}\n\nr.login.hoist = {\n    type: 'cookie',\n    watch: function(name) {\n        var cookieName = 'hoist_'+name,\n            deferred = new $.Deferred\n\n        var interval = setInterval(function() {\n            data = $.cookie(cookieName)\n            if (data) {\n                try {\n                    data = JSON.parse(data)\n                } catch(e) {\n                    data = null\n                }\n                $.cookie(cookieName, null, {domain:r.config.cur_domain, path:'/'})\n                clearInterval(interval)\n                deferred.resolve(data)\n            }\n        }, 100)\n\n        return deferred\n    }\n}\n\nr.login.ui = {\n    init: function() {\n        if (!r.config.logged) {\n            $('.content .login-form, .content #login-form, .side .login-form').each(function(i, el) {\n                new r.ui.LoginForm(el)\n            })\n\n            $('.content .register-form, .content #register-form').each(function(i, el) {\n                new r.ui.RegisterForm(el)\n            })\n\n            this.popup = new r.ui.LoginPopup();\n\n            $(document).delegate('.login-required', 'click', $.proxy(this, 'loginRequiredAction'))\n        }\n    },\n\n    _getActionDetails: function(el) {\n      var $el = $(el);\n\n      if ($el.hasClass('up')) {\n        return {\n            eventName: 'upvote',\n            description: r._('You need to be logged in to upvote things.')\n        };\n      } else if ($el.hasClass('down')) {\n        return {\n            eventName: 'downvote',\n            description: r._('You need to be logged in to downvote things.')\n        };\n      } else if ($el.hasClass('arrow')) {\n        return {\n            eventName: 'arrow',\n            description: r._('You need to be logged in to vote on things.')\n        };\n      } else if ($el.hasClass('give-gold')) {\n        return {\n            eventName: 'give-gold',\n            description: r._('You need to be logged in to give gold.')\n        };\n      } else if ($el.parents(\"#header\").length && $el.attr('href').indexOf('login') !== -1) {\n        return {\n            eventName: 'login-or-register'\n        };\n      } else if ($el.parents('.subscribe-button').length) {\n        return {\n            eventName: 'subscribe-button',\n            description: r._('You need to be logged in to subscribe to subreddits.')\n        };\n      } else if ($el.parents('.submit-link').length) {\n        return {\n            eventName: 'submit-link',\n            description: r._('You need to be logged in to submit things.')\n        };\n      } else if ($el.parents('.submit-text').length) {\n        return {\n            eventName: 'submit-text',\n            description: r._('You need to be logged in to submit things.')\n        };\n      } else {\n        return {\n            eventName: $el.attr('class'),\n            description: r._('You need to be logged in to do that.')\n        };\n      }\n    },\n\n  _logEvent: function(e) {\n      var target = $(e.target);\n      var thing = target.thing();\n\n      var targetType = target.data('type') || thing.data('type');\n      var targetFullname = target.data('fullname') || thing.data('fullname');\n      var actionName = target.data('event-action');\n      var actionDetail = target.data('event-detail');\n\n      // set default action for modal\n      if (!actionName) {\n        actionName = 'modal';\n        actionDetail = null;\n      }\n\n      // set target using page context\n      if (!targetFullname && targetType == 'subreddit') {\n        targetFullname = r.config.cur_site;\n      } else if (!targetFullname && targetType == 'link') {\n        targetFullname = r.config.cur_link;\n      }\n\n      r.analytics.loginRequiredEvent(actionName, actionDetail, targetType, targetFullname);\n    },\n\n    loginRequiredAction: function(e) {\n        if (r.config.logged) {\n            return true\n        } else {\n            var el = $(e.target);\n            var href = el.attr('href');\n            var actionDetails = this._getActionDetails(el);\n            var dest;\n\n            if (href && href != '#' && !/\\/login\\/?$/.test(href)) {\n                // User clicked on a link that requires login to continue\n                dest = href\n            } else {\n                // User clicked on a thing button that requires login\n                var thing = el.thing()\n                if (thing.length) {\n                    dest = thing.find('.comments').attr('href')\n                }\n            }\n\n            this.popup.showLogin(actionDetails.description, dest && $.proxy(function(result) {\n                window.location = dest\n            }, this))\n\n            this._logEvent(e);\n            r.analytics.fireGAEvent('login-required-popup', 'opened', actionDetails.eventName);\n\n            return false\n        }\n    }\n}\n\nr.ui.LoginForm = function() {\n    r.ui.Form.apply(this, arguments)\n}\nr.ui.LoginForm.prototype = $.extend(new r.ui.Form(), {\n    showErrors: function(errors) {\n        r.ui.Form.prototype.showErrors.call(this, errors)\n        if (errors.length) {\n            this.$el.find('.recover-password')\n                .addClass('attention')\n        }\n    },\n\n    showStatus: function() {\n        this.$el.find('.error').css('opacity', 1)\n        r.ui.Form.prototype.showStatus.apply(this, arguments)\n    },\n    \n    resetErrors: function() {\n        if (this.$el.hasClass('login-form-side')) {\n            // Dim the error in place so the form doesn't change size.\n            var errorEl = this.$el.find('.error')\n            if (errorEl.is(':visible')) {\n                errorEl.fadeTo(100, .35)\n            }\n        } else {\n            r.ui.Form.prototype.resetErrors.apply(this, arguments)\n        }\n    },\n\n    _submit: function() {\n        r.analytics.fireGAEvent('login-form', 'submit');\n        return r.login.post(this, 'login')\n    },\n\n    _handleResult: function(result) {\n        if (!result.json.errors.length) {\n            // Success. Load the destination page with the new session cookie.\n            if (this.successCallback) {\n                this.successCallback(result)\n            } else {\n                this.$el.addClass('working')\n                var base = r.config.extension ? '/.'+r.config.extension : '/',\n                    defaultDest = /\\/login\\/?$/.test($.url().attr('path')) ? base : window.location,\n                    destParam = this.$el.find('input[name=\"dest\"]').val()\n                var redir = destParam || defaultDest\n                if (window.location === redir) {\n                    window.location.reload();\n                } else {\n                    window.location = redir;\n                }\n            }\n        } else {\n            r.ui.Form.prototype._handleResult.call(this, result)\n        }\n    },\n\n    _handleNetError: function(xhr) {\n        r.ui.Form.prototype._handleNetError.apply(this, arguments)\n        if (xhr.status == 0 && r.config.currentOrigin != r.config.https_endpoint) {\n            $('<p>').append(\n                $('<a>')\n                    .text(r._('try using our secure login form.'))\n                    .attr('href', r.config.https_endpoint + '/login')\n            ).appendTo(this.$el.find('.status'))\n        }\n    },\n\n    focus: function() {\n        this.$el.find('input[name=\"user\"]').focus()\n    }\n})\n\n\nr.ui.RegisterForm = function() {\n    r.ui.Form.apply(this, arguments)\n\n    this.$user = this.$el.find('[name=\"user\"]');\n\n    if (!this.$user.is('[data-validate-url]')) {\n        this.checkUsernameDebounced = _.debounce($.proxy(this, 'checkUsername'), 500);\n        this.$user.on('keyup', $.proxy(this, 'usernameChanged'));\n    }\n\n    this.$el.find('[name=\"passwd2\"]').on('keyup', $.proxy(this, 'checkPasswordMatch'));\n    this.$el.find('[name=\"passwd\"][data-validate-url]')\n        .strengthMeter({\n            related: [\n                '#user_reg',\n                '#email_reg',\n            ],\n            delay: 0,\n            trigger: 'loaded.validator',\n        })\n        .on('score.strengthMeter', function(e, score) {\n            var $el = $(this);\n\n            if ($el.stateify('getCurrentState') === 'error') {\n                return;\n            }\n\n            var message;\n\n            if (score > 90) {\n                message = r._('Password is strong');\n            } else if (score > 70) {\n                message = r._('Password is good');\n            } else if (score > 30) {\n                message = r._('Password is fair');\n            } else {\n                message = r._('Password is weak');\n            }\n\n            $el.stateify('showMessage', message);\n        });\n\n    this.$submit = this.$el.find('.submit button');\n}\nr.ui.RegisterForm.prototype = $.extend(new r.ui.Form(), {\n    maxName: 0,\n    usernameChanged: function() {\n        var name = this.$user.val()\n        if (name == this._priorName) {\n            return\n        } else {\n            this._priorName = name\n        }\n\n        this.$el.find('.error.field-user').hide()\n        this.$el.removeClass('name-checking name-available name-taken')\n\n        this.maxName = Math.max(this.maxName, name.length)\n        if (name && this.maxName >= 3) {\n            this.$el.addClass('name-checking')\n            this.checkUsernameDebounced()\n        }\n\n        this.$submit.attr('disabled', false)\n    },\n\n    checkPasswordMatch: _.debounce(function() {\n        var $confirm = this.$el.find('[name=\"passwd2\"]');\n        var $password = this.$el.find('[name=\"passwd\"]');\n        var confirm = $confirm.val();\n        var password = $password.val();\n\n        if (!confirm || $password.stateify('getCurrentState') !== 'success') {\n            $confirm.stateify('clear');\n            return;\n        }\n\n        if (confirm === password) {\n            $confirm.stateify('set', 'success');\n        } else {\n            $confirm.stateify('set', 'error', r._('passwords do not match'));\n        }\n\n    }, $.fn.validator.Constructor.DEFAULTS.delay),\n\n    checkUsername: function() {\n        var name = this.$user.val()\n\n        if (name) {\n            $.ajax({\n                url: '/api/username_available.json',\n                data: {user: name},\n                success: $.proxy(this, 'displayUsernameStatus'),\n                complete: $.proxy(function() { this.$el.removeClass('name-checking') }, this)\n            })\n        } else {\n            this.$el.removeClass('name-available name-taken')\n        }\n    },\n\n    displayUsernameStatus: function(result) {\n        if (result.json && result.json.errors) {\n            this.showErrors(result.json.errors)\n            this.$submit.attr('disabled', true)\n        } else {\n            this.$el.addClass(result ? 'name-available' : 'name-taken')\n            this.$submit.attr('disabled', result == false)\n        }\n    },\n\n    _submit: function() {\n        r.analytics.fireGAEvent('register-form', 'submit');\n        return r.login.post(this, 'register')\n    },\n\n    _handleResult: r.ui.LoginForm.prototype._handleResult,\n    focus: r.ui.LoginForm.prototype.focus\n})\n\nr.ui.LoginPopup = function() {\n    var content = $('#login-popup').prop('innerHTML');\n\n    r.ui.Popup.call(this, {\n        size: 'large',\n        content: content,\n        className: 'login-modal',\n    });\n\n    this.login = new r.ui.LoginForm(this.$.find('#login-form'));\n    this.register = new r.ui.RegisterForm(this.$.find('#register-form'));\n};\n\nr.ui.LoginPopup.prototype = _.extend({}, r.ui.Popup.prototype, {\n\n    show: function(notice, callback) {\n        this.login.successCallback = callback;\n        this.register.successCallback = callback;\n\n        this.$.find('#cover-msg').text(notice).toggle(!!notice);\n\n        r.ui.Popup.prototype.show.call(this);\n\n        return false;\n    },\n\n    showLogin: function() {\n        var login = this.login;\n\n        this.show.apply(this, arguments);\n        this.once('opened.r.popup', function() {\n            login.focus();\n        });\n    },\n\n    showRegister: function() {\n        var register = this.register;\n\n        this.show.apply(this, arguments);\n        this.once('opened.r.popup', function() {\n            register.focus();\n        });\n    },\n\n});\n"
  },
  {
    "path": "r2/r2/public/static/js/messagecompose.js",
    "content": "/**\n * @fileoverview The custom behavior for the standalone private message\n * composition form.\n */\n\n!function(r, undefined) {\n  'use strict';\n\n  /** How long to run the state transition animations. @const */\n  var ANIM_MS = 0;\n\n\n  /** How long to wait for the subreddit rules to load. @const */\n  var RULES_TIMEOUT_MS = 10000;\n\n\n  /** This looks like a subreddit name. @const */\n  var SUBREDDIT = /^(?:#|\\/?r\\/)(.*)/;\n\n\n  var mc = r.messagecompose = {\n\n    /**\n     * The interesting bits of the compose message form.\n     * @const {Object<string, jQuery|undefined>}\n     */\n    dom: {\n      /** The compose message form.  */\n      $form: undefined,\n\n      $to: undefined,\n\n      $subject: undefined,\n\n      /** The subject-rule selector. */\n      $rule: undefined,\n\n      /** The \"Other\" rule option for a custom subject. */\n      $other: undefined,\n\n      /** The blank default and invalid rule option. */\n      $blank: undefined,\n    },\n\n\n    /**\n     * The current or previous XMLHttpRequest loading rules.\n     * @private {XMLHttpRequest|undefined}\n     */\n    rulesReq: undefined,\n\n\n    /**\n     * The URL of the currently running rules request, if any.\n     * @private {string|undefined}\n     */\n    rulesReqInProgressUrl: undefined,\n\n    /**\n     * The subject the user expects to see for the \"Other\" rule.\n     * @private {string|undefined}\n     */\n    customSubject: undefined,\n\n    /**\n     * Module initialization.\n     * @private\n     */\n    init: function() {\n      mc.dom.$form = $('#compose-message');\n      mc.dom.$to = mc.dom.$form.find('[name=\"to\"]');\n      mc.dom.$subject = mc.dom.$form.find('[name=\"subject\"]');\n      mc.dom.$rule = mc.dom.$form.find('select.rule_subject');\n      mc.dom.$other = mc.dom.$rule.find('option.other');\n      mc.dom.$blank = mc.dom.$rule.find('option.blank');\n\n      if (mc.dom.$form.length) {\n        // Clean up the form after sucessful submission.\n        // This is delayed until after the ajax implementation of the form\n        // submission (post_form in reddit.js) so that it does't interfere with the\n        // form submission, but can still clean up after. I haven't found any other\n        // hooks in to the form submission that let us run code after the form is\n        // reset.\n        mc.dom.$form.on('submit', function() {\n          var handler = function() {\n            $(document).off('ajaxSuccess.messagecompose', handler);\n            // If there are no errors marked in the form, reset the subject too.\n            if (mc.dom.$form.find('.error:visible').length == 0) {\n              mc.customSubject = undefined;\n              mc.onToUpdate();\n              mc.dom.$subject.val('');\n            }\n          };\n          $(document).on('ajaxSuccess.messagecompose', handler);\n        });\n\n        if (mc.dom.$subject.val()) {\n          mc.customSubject = mc.dom.$subject.val();\n        }\n\n        if (mc.dom.$to.length) {\n          mc.dom.$to.change(mc.onToUpdate);\n          // The page is not pre-rendered with the rules for the selected\n          // subreddit, so we have to load them up front.\n          mc.onToUpdate();\n        }\n\n        mc.dom.$rule.change(mc.onSelectedRuleChange);\n        mc.dom.$subject.change(mc.onSubjectChange);\n      }\n    },\n\n    /**\n     * Loads the rules for the given subreddit.\n     * Returns a promise that resolves with the rules JSON object or rejects if the\n     * subreddit doesn't exist or otherwise fails to load.\n     *\n     * @private\n     * @param {string} sr The name of the subreddit.\n     * @return {Promise<{\n     *     sr_name: string,\n     *     rules: Array<{short_name:string}>|undefined,\n     *     site_rules: Array<string>|undefined,\n     *  }>}\n     */\n    loadSubredditRules: function(sr) {\n      var url = '/r/' + sr + '/about/rules.json';\n\n      // If we're already loading this subreddit, continue, otherwise, abort\n      // the old one and start over.\n      if (mc.rulesReqInProgressUrl === url) {\n        return mc.rulesReq;\n      }\n\n      if (mc.rulesReq) {\n        mc.rulesReq.abort();\n      }\n\n      (mc.rulesReq = r.ajax({\n        url: url,\n        type: 'GET',\n        dataType: 'json',\n        timeout: RULES_TIMEOUT_MS\n      })).always(function() {\n        mc.rulesReqInProgressUrl = undefined;\n        mc.rulesReq = undefined;\n      }).then(function(rulesJson, textStatus, jqXHR) {\n        // The API gets redirected to search if the subreddit does not exist.\n        // Detect that by looking for a lack of the rules or a result kind of Listing.\n        if (!rulesJson['rules'] || rulesJson['kind'] === 'Listing') {\n          return $.Deferred().reject(jqXHR, rulesJson, 'No subreddit');\n        } else if (rulesJson['error']) {\n          return $.Deferred().reject(jqXHR, rulesJson, rulesJson['error']);\n        }\n\n        // Annotate the rules with the subreddit, so we can reason about them later.\n        rulesJson['sr_name'] = sr;\n        return rulesJson;\n      });\n\n      mc.rulesReqInProgressUrl = url;\n      return mc.rulesReq;\n    },\n\n\n    /**\n     * Renders the rules of the subreddit into the subject field, while maintaining\n     * any user entered value.\n     *\n     * @private\n     * @param {!Object} rulesJson\n     */\n    renderSubredditSubject: function(rulesJson) {\n      // Old underscore.js. _.property isn't defined.\n      var property = function(p) { return function (v) {return v[p]; }; };\n\n      var rules = _(rulesJson['rules'])\n        .sortBy(property('priority'))\n        .map(property('short_name'))\n        .concat(rulesJson['site_rules']);\n\n      // Clear out any old rules.\n      mc.dom.$rule.find('option.rule').remove();\n\n      // Add the new rules.\n      rules.forEach(function(r) {\n        var rule = document.createElement('option');\n        $(rule).val(r).text(r);\n        rule.className = 'rule';\n        mc.dom.$other.before(rule);\n      });\n\n      // If the user already entered a subject, keep it as the 'Other', unless they\n      // entered a rule, then select that rule.\n      if (rules.length) {\n        mc.dom.$rule.show(ANIM_MS);\n        if (_(rules).contains(mc.dom.$subject.val())) {\n          // Move the custom subject to the selected rule.\n          mc.dom.$rule.val(mc.dom.$subject.val());\n        } else if (/\\S/.test(mc.dom.$subject.val())\n            || _(mc.dom.$subject).contains(document.activeElement)\n            || rulesJson['sr_name'] !== 'reddit.com') {\n          // Select Other if the user has entered anything in the custom\n          // subject OR if the keyboard focus is already on the subject OR if\n          // the subreddit is not reddit.com.\n          //\n          // This call happens after a network round trip to load the rules, so\n          // there's plenty of time for the user to have started typing.\n          mc.dom.$other.prop('selected', true);\n        } else {\n          // To reddit.com. Select Blank by default to encourage consistent subjects.\n          mc.dom.$blank.show();\n          mc.dom.$blank.prop('selected', true);\n        }\n      }\n      mc.dom.$rule.change();\n    },\n\n\n    /**\n     * Changes the state of the form to be correct for a non-subreddit recipient.\n     * @private\n     */\n    renderGeneralSubject: function() {\n      // If the focus is on the rule selector, switch it to the subject.\n      if (_(mc.dom.$rule).contains(document.activeElement)) {\n        mc.dom.$subject.focus();\n      }\n\n      if (mc.customSubject && /\\S/.test(mc.customSubject)) {\n        mc.dom.$subject.val(mc.customSubject);\n      } else {\n        // Move the rule to the custom value, if the user has selected one.\n        mc.dom.$rule.val(mc.dom.$subject.val());\n      }\n\n      mc.dom.$rule.hide(ANIM_MS);\n      mc.dom.$subject.show(ANIM_MS);\n    },\n\n\n    /** Handles an update to the recipient of the message. */\n    onToUpdate: function() {\n      var toValue = mc.dom.$to.val();\n      // Is this probably a subreddit?\n      var m = SUBREDDIT.exec(toValue);\n      if (m) {\n        mc.loadSubredditRules(m[1])\n          .then(mc.renderSubredditSubject, mc.renderGeneralSubject);\n      } else {\n        mc.renderGeneralSubject();\n      }\n    },\n\n\n    /**\n     * Handles a change to which rule based subject is selected.\n     * @private\n     * @param {!Event} e\n     */\n    onSelectedRuleChange: function(e) {\n      if (e.target.options[e.target.selectedIndex] === mc.dom.$other[0]) {\n        // Handle selecting \"Other\" by showing the custom subject field.\n        if (mc.customSubject && /\\S/.test(mc.customSubject)) {\n          mc.dom.$subject.val(mc.customSubject);\n        }\n        mc.dom.$subject.show(ANIM_MS);\n        mc.dom.$subject.focus();\n      } else {\n        // The user selected a normal rule. Merge the custom subject back into\n        // the rule.\n        mc.dom.$subject.hide(ANIM_MS);\n        if (_(mc.dom.$subject).contains(document.activeElement)) {\n          mc.dom.$rule.focus();\n        }\n\n        // Use the rule as the subject of the message.\n        mc.dom.$subject.val(mc.dom.$rule.val());\n      }\n\n      // Destroy the blank default option once a different one is chosen.\n      if (e.target.options[e.target.selectedIndex] === mc.dom.$blank[0]) {\n        mc.dom.$blank.hide();\n      }\n    },\n\n\n    /**\n     * Handles the user changing the subject.\n     * @private\n     * @param {!Event} e\n     */\n    onSubjectChange: function(e) {\n      mc.customSubject = mc.dom.$subject.val();\n    },\n  };\n\n\n  // No exports, this is a leaf module for now.\n\n  r.hooks.get('reddit-init').register(r.messagecompose.init);\n\n}(r);\n"
  },
  {
    "path": "r2/r2/public/static/js/messages.js",
    "content": "r.messages = {}\n\n/**\n * After triggering a mark-as-read request, poll our account to see if our messages have been cleared.\n * If our messages have not been cleared after a number of iterations, it's likely we had a\n * message race or other issue, redirect back to stop polling and potentially display the new message.\n */\nr.messages.pollUnread = _.debounce(function(count) {\n  count = count + 1 || 1;\n\n  // Took too long, redirect in case of issue.\n  if (count > 20) {\n    document.location = \"/message/unread\";\n    return;\n  }\n\n  r.ajax({\n    type: 'GET',\n    url: '/api/me.json',\n    success: function(response) {\n      if (!response['data']['has_mail']) {\n        document.location = \"/message/unread\";\n      } else {\n        r.messages.pollUnread(count);\n      }\n    },\n  });\n}, 2000);\n\nr.messages.init = function() {\n  $('a.mark-all-read').on('click', function(e) {\n    var $this = $(this);\n\n    e.preventDefault();\n    e.stopPropagation();\n\n    if ($this.hasClass('disabled')) {\n      return;\n    }\n\n    $this.addClass('disabled');\n    $this.parent().addClass('working');\n\n    r.ajax({\n      type: 'POST',\n      url: '/api/read_all_messages',\n      data: {},\n      success: function(response) {\n        r.messages.pollUnread();\n      },\n      error: function(response) {\n        $this.parent().removeClass('working');\n      },\n    });\n  });\n}\n"
  },
  {
    "path": "r2/r2/public/static/js/migrate-global-reddit.js",
    "content": "/*\nAdds temporary logging of gets/sets through the legacy global `reddit` object.\n */\n!function(r, undefined) {\n  r.hooks.get('setup').register(function() {\n    try {\n      // create a new object to detect if anywhere is still using the\n      // the legacy config global object\n      window.reddit = {};\n\n      function migrateWarn(message) {\n        r.sendError(message, { tag: 'reddit-config-migrate-error' })\n      }\n\n      var keys = Object.keys(r.config);\n\n      keys.forEach(function(key) {\n        Object.defineProperty(window.reddit, key, {\n          configurable: false,\n          enumerable: true,\n          get: function() {\n            var message = \"config property %(key)s accessed through global reddit object.\";\n            migrateWarn(message.format({ key: key }));\n            return r.config[key];\n          },\n          set: function(value) {\n            var message = \"config property %(key)s set through global reddit object.\";\n            migrateWarn(message.format({ key: key }));\n            return r.config[key] = value;\n          },\n        });\n      });\n    } catch (err) {\n      // for the odd browser that doesn't support getters/setters, just let\n      // it function as-is.\n      window.reddit = r.config;\n    }\n  });\n}(r);\n"
  },
  {
    "path": "r2/r2/public/static/js/models/subreddit-rule.js",
    "content": "/*\n  requires Backbone\n  requires r.errors\n  requires r.models.validators.js\n */\n!function(models, Backbone, undefined) {\n  r.models = r.models || {};\n\n\n  var SHORT_NAME_MAX_LENGTH = 50;\n  var DESCRIPTION_MAX_LENGTH = 500;\n\n\n  function ValidRulesLength(attrName, maxLength) {\n    return function validate(collection) {\n      if (collection.length > maxLength) {\n        return r.errors.createAPIError(attrName, 'SR_RULE_TOO_MANY');\n      }\n    }\n  }\n\n\n  function ValidRule(attrName) {\n    var vLength = r.models.validators.StringLength(attrName, 1, SHORT_NAME_MAX_LENGTH);\n\n    return function validate(model) {\n      var collection = model.collection;\n      var isNew = model.isNew();\n\n      if (collection && isNew) {\n        var vRulesLength = ValidRulesLength(attrName, collection.maxLength - 1);\n        var collectionError = vRulesLength(collection);\n\n        if (collectionError) {\n          return collectionError;\n        }\n      }\n\n      var lengthError = vLength(model, attrName);\n\n      if (lengthError) {\n        return lengthError;\n      }\n\n      if (collection) {\n        var query = {};\n        query[model.idAttribute] = model.get(model.idAttribute);\n        var matches = collection.where(query);\n        var isDuplicate = matches && matches.length > (isNew ? 0 : 1);\n\n        if (isDuplicate) {\n          return r.errors.createAPIError(attrName, 'SR_RULE_EXISTS');\n        }\n      }\n    };\n  };\n\n\n  var SubredditRule = Backbone.Model.extend({\n    idAttribute: 'short_name',\n\n    SHORT_NAME_MAX_LENGTH: SHORT_NAME_MAX_LENGTH,\n    DESCRIPTION_MAX_LENGTH: DESCRIPTION_MAX_LENGTH,\n\n    validators: [\n      ValidRule('short_name'),\n      r.models.validators.StringLength('description', 0, DESCRIPTION_MAX_LENGTH),\n    ],\n\n    api: {\n      create: function(model) {\n        var data = model.toJSON();\n        delete data.description_html;\n        return {\n          url: 'add_subreddit_rule',\n          data: data,\n        };\n      },\n\n      update: function(model) {\n        var data = model.toJSON();\n        data.old_short_name = model._old_short_name;\n        delete data.description_html;\n        return {\n          url: 'update_subreddit_rule',\n          data: data,\n        };\n      },\n\n      delete: function(model) {\n        var data = { short_name: model._old_short_name };\n        return {\n          url: 'remove_subreddit_rule',\n          data: data,\n        };\n      },\n    },\n\n    defaults: function() {\n      return {\n        short_name: '',\n        description: '',\n        description_html: '',\n        priority: 0,\n        kind: 'all',\n      };\n    },\n\n    toApiJSON: function() {\n      // same as toJSON, but strips it down to only what we'd get from the API\n      var data = this.toJSON();\n      delete data.description_html;\n      if (data.kind === 'all') {\n        delete data.kind;\n      }\n      return data;\n    },\n\n    initialize: function() {\n      var short_name = this.get('short_name');\n      this._old_short_name = short_name;\n\n      if (this.isNew()) {\n        this.once('sync:create', function(model) {\n          model.updateOldShortName();\n        });\n      }\n\n      this.on('sync:update', function(model) {\n        model.updateOldShortName();\n      });\n    },\n\n    updateOldShortName: function() {\n      this._old_short_name = this.get('short_name');\n    },\n\n    isNew: function() {\n      return !this._old_short_name;\n    },\n\n    revert: function() {\n      return this.set(this.previousAttributes(), { silent: true });\n    },\n\n    sync: function(method, model) {\n      if (!this.api[method]) {\n        throw new Error('Invalid action');\n      }\n      \n      var req = this.api[method](model);\n      req.data.api_type = 'json';\n      this.trigger('request', this);\n\n      $.request(req.url, req.data, function(res) {\n        var errors = r.errors.getAPIErrorsFromResponse(res);\n        \n        if (errors) {\n          this.trigger('error', this, errors);\n          return;\n        }\n\n        if (res && res.json && res.json.data && res.json.data) {\n          var description_html = _.unescape(res.json.data.description_html || '');\n          this.set({ description_html: description_html });\n        }\n\n        this.trigger('sync:' + method, this);\n        this.trigger('sync', this, method);\n      }.bind(this), undefined, undefined, undefined, function(res) {\n        var errors = r.errors.getAPIErrorsFromResponse(res);\n        this.trigger('error', this, errors);\n      }.bind(this));\n    },\n\n    validate: function(attrs) {\n      return r.models.validators.validate(this, this.validators);\n    },\n  });\n\n\n  var RULES_COLLECTION_MAX_LENGTH = 10;\n\n  var SubredditRuleCollection = Backbone.Collection.extend({\n    model: SubredditRule,\n    maxLength: RULES_COLLECTION_MAX_LENGTH,\n    subredditName: null,\n    subredditFullname: null,\n\n    initialize: function(models, options) {\n      this._disabled = this.length >= this.maxLength;\n      \n      if (options && options.subredditName) {\n        this.subredditName = options.subredditName;\n      }\n      if (options && options.subredditFullname) {\n        this.subredditFullname = options.subredditFullname;\n      }\n\n      this.on('add', function() {\n        if (!this._disabled && this.length >= this.maxLength) {\n          this._disabled = true;\n          this.trigger('disabled');\n        }\n      }.bind(this));\n\n      this.on('remove', function() {\n        if (this._disabled && this.length < this.maxLength) {\n          this._disabled = false;\n          this.trigger('enabled');\n        }\n      }.bind(this));\n    },\n\n    toApiJSON: function() {\n      return {\n        sr_name: this.subredditName,\n        rules: this.models.map(function(model) {\n          return model.toApiJSON();\n        }),\n      };\n    },\n  });\n\n\n  r.models.SubredditRule = SubredditRule;\n  r.models.SubredditRuleCollection = SubredditRuleCollection;\n}(r, Backbone);\n"
  },
  {
    "path": "r2/r2/public/static/js/models/validators.js",
    "content": "/*\nProvides Backbone models a method of defining attribute validators.\n\nrequires r.errors\n */\n!function(r, undefined) {\n  r.models = r.models || {};\n\n  r.models.validators = {\n    validate: function(model, validators) {\n      for (var i = 0; i < validators.length; i++) {\n        validator = validators[i];\n        error = validator(model);\n\n        if (error) {\n          return error;\n        }\n      }\n    },\n  };\n\n  r.models.validators.StringLength = function (attrName, minLength, maxLength) {\n    minLength = Math.max(0, parseInt(minLength, 10));\n    maxLength = Math.max(0, parseInt(maxLength, 10));\n\n    return function validate(model) {\n      var value = model.get(attrName);\n\n      if (typeof value !== 'string') {\n        return r.errors.createAPIError(attrName, 'NO_TEXT');\n      } else if (value.length < minLength) {\n        return r.errors.createAPIError(attrName, 'TOO_SHORT', {\n          min_length: minLength,\n        });\n      } else if (maxLength && value.length > maxLength) {\n        return r.errors.createAPIError(attrName, 'TOO_LONG', {\n          max_length: maxLength,\n        });\n      }\n    };\n  };\n}(r);\n"
  },
  {
    "path": "r2/r2/public/static/js/multi.js",
    "content": "r.multi = {\n    init: function() {\n        this.multis = new r.multi.GlobalMultiCache()\n        this.mine = new r.multi.MyMultiCollection()\n\n        // this collection gets fetched frequently by hover bubbles.\n        this.mine.fetch = _.throttle(this.mine.fetch, 60 * 1000)\n\n        var detailsEl = $('.multi-details')\n        if (detailsEl.length) {\n            var multi = this.multis.touch(detailsEl.data('path'))\n            multi.fetch()\n\n            var detailsView = new r.multi.MultiDetails({\n                model: multi,\n                el: detailsEl\n            }).render()\n            var subredditList = new r.multi.SubredditList({\n                model: multi,\n                el: detailsEl\n            })\n\n            if (location.hash == '#created') {\n                detailsView.focusAdd()\n            }\n\n            // if page has a recs box, wire it up to refresh with the multi.\n            var recsEl = $('#multi-recs')\n            if (recsEl.length) {\n                detailsView.initRecommendations(recsEl)\n            }\n        }\n\n        var subscribeBubbleGroup = {}\n        $('.subscribe-button').each(function(idx, el) {\n            new r.multi.SubscribeButton({\n                el: el,\n                bubbleGroup: subscribeBubbleGroup\n            })\n        })\n\n        $('.listing-chooser').each(function(idx, el) {\n            new r.multi.ListingChooser({el: el})\n        })\n    }\n}\n\nr.multi.MultiRedditList = Backbone.Collection.extend({\n    model: Backbone.Model.extend({\n        initialize: function() {\n            this.id = this.get('name').toLowerCase()\n        }\n    }),\n\n    comparator: function(model) {\n        return model.id\n    },\n\n    getByName: function(name) {\n        return this.get(name.toLowerCase())\n    }\n})\n\nr.multi.MultiReddit = Backbone.Model.extend({\n    idAttribute: 'path',\n    url: function() {\n        return r.utils.joinURLs('/api/multi', this.id)\n    },\n\n    defaults: {\n        visibility: 'private'\n    },\n\n    initialize: function(attributes, options) {\n        this.uncreated = options && !!options.isNew\n        this.subreddits = new r.multi.MultiRedditList(this.get('subreddits'), {\n            url: this.url() + '/r/',\n            parse: true\n        })\n        this.on('change:subreddits', function(model, value) {\n            this.subreddits.set(value, {parse: true})\n        }, this)\n        this.subreddits.on('request', function(model, xhr, options) {\n            this.trigger('request', model, xhr, options)\n        }, this)\n    },\n\n    parse: function(response) {\n        return response.data\n    },\n\n    toJSON: function() {\n        data = Backbone.Model.prototype.toJSON.apply(this)\n        data.subreddits = this.subreddits.toJSON()\n        return data\n    },\n\n    isNew: function() {\n        return this.uncreated\n    },\n\n    name: function() {\n        return this.get('path').split('/').pop()\n    },\n\n    sync: function(method, model, options) {\n        var res = Backbone.sync.apply(this, arguments)\n        if (method == 'create') {\n            res.done(_.bind(function() {\n                // upon successful creation, unset new flag\n                this.uncreated = false\n            }, this))\n        }\n        return res\n    },\n\n    addSubreddit: function(names, options) {\n        names = r.utils.tup(names)\n        if (names.length == 1) {\n            this.subreddits.create({name: names[0]}, options)\n        } else {\n            // batch add by syncing the entire multi\n            var subreddits = this.subreddits,\n                tmp = subreddits.clone()\n            tmp.add(_.map(names, function(srName) {\n                return {name: srName}\n            }))\n\n            // temporarily swap out the subreddits collection so we can\n            // serialize and send the new data without updating the UI\n            // this is similar to how the \"wait\" option is handled in\n            // Backbone.Model.set\n            this.subreddits = tmp\n            this.save(null, options)\n            this.subreddits = subreddits\n        }\n    },\n\n    removeSubreddit: function(name, options) {\n        this.subreddits.getByName(name).destroy(options)\n    },\n\n    _copyOp: function(op, newCollection, newName) {\n        var deferred = new $.Deferred\n        Backbone.ajax({\n            type: 'POST',\n            url: '/api/multi/' + op,\n            data: {\n                from: this.get('path'),\n                to: newCollection.pathByName(newName)\n            },\n            success: _.bind(function(resp) {\n                if (op == 'rename') {\n                    this.trigger('destroy', this, this.collection)\n                }\n                var multi = r.multi.multis.reify(resp)\n                newCollection.add(multi)\n                deferred.resolve(multi)\n            }, this),\n            error: _.bind(deferred.reject, deferred)\n        })\n        return deferred\n    },\n\n    copyTo: function(newCollection, name) {\n        return this._copyOp('copy', newCollection, name)\n    },\n\n    renameTo: function(newCollection, name) {\n        return this._copyOp('rename', newCollection, name)\n    },\n\n    getSubredditNames: function() {\n        return this.subreddits.pluck('name')\n    }\n})\n\nr.multi.MyMultiCollection = Backbone.Collection.extend({\n    url: '/api/multi/mine',\n    model: r.multi.MultiReddit,\n    comparator: function(model) {\n        return model.get('path').toLowerCase()\n    },\n\n    parse: function(data) {\n        return _.map(data, function(multiData) {\n            return r.multi.multis.reify(multiData)\n        })\n    },\n\n    pathByName: function(name) {\n        return '/user/' + r.config.logged + '/m/' + name\n    }\n})\n\nr.multi.GlobalMultiCache = Backbone.Collection.extend({\n    model: r.multi.MultiReddit,\n\n    touch: function(path) {\n        var multi = this.get(path)\n        if (!multi) {\n            multi = new this.model({\n                path: path\n            })\n            this.add(multi)\n        }\n        return multi\n    },\n\n    reify: function(response) {\n        var data = this.model.prototype.parse(response),\n            multi = this.touch(data.path)\n\n        multi.set(data)\n        return multi\n    }\n})\n\nr.multi.MultiSubredditItem = Backbone.View.extend({\n    tagName: 'li',\n\n    template: _.template('<a href=\"/r/<%- sr_name %>\">/r/<%- sr_name %></a><button class=\"remove-sr\">x</button>'),\n\n    events: {\n        'click .remove-sr': 'removeSubreddit'\n    },\n\n    render: function() {\n        this.$el.append(this.template({\n            sr_name: this.model.get('name')\n        }))\n\n        if (r.config.logged) {\n            this.bubble = new r.multi.MultiSubscribeBubble({\n                parent: this.$el,\n                group: this.options.bubbleGroup,\n                srName: this.model.get('name')\n            })\n        }\n\n        return this\n    },\n\n    remove: function() {\n        if (this.bubble) {\n            this.bubble.remove()\n        }\n        Backbone.View.prototype.remove.apply(this)\n    },\n\n    removeSubreddit: function(ev) {\n        this.options.multi.removeSubreddit(this.model.get('name'))\n    }\n})\n\nr.multi.SubredditList = Backbone.View.extend({\n    events: {\n        'submit .add-sr': 'addSubreddit'\n    },\n\n    initialize: function() {\n        this.listenTo(this.model.subreddits, 'add', this.addOne)\n        this.listenTo(this.model.subreddits, 'remove', this.removeOne)\n        this.listenTo(this.model.subreddits, 'sort', this.resort)\n        new r.ui.ConfirmButton({el: this.$('button.delete')})\n\n        this.listenTo(this.model.subreddits, 'add remove', function() {\n            r.ui.showWorkingDeferred(this.$el, r.ui.refreshListing())\n        })\n\n        this.model.on('request', function(model, xhr) {\n            r.ui.showWorkingDeferred(this.$el, xhr)\n        }, this)\n\n        this.itemView = this.options.itemView || r.multi.MultiSubredditItem\n        this.itemViews = {}\n        this.bubbleGroup = {}\n        this.$('.subreddits').empty()\n        this.model.subreddits.each(this.addOne, this)\n    },\n    \n    addOne: function(sr) {\n        var view = new this.itemView({\n            model: sr,\n            multi: this.model,\n            bubbleGroup: this.bubbleGroup\n        })\n        this.itemViews[sr.id] = view\n        this.$('.subreddits').append(view.render().$el)\n    },\n\n    resort: function() {\n        this.model.subreddits.each(function(sr) {\n            this.itemViews[sr.id].$el.appendTo(this.$('.subreddits'))\n        }, this)\n    },\n\n    removeOne: function(sr) {\n        this.itemViews[sr.id].remove()\n        delete this.itemViews[sr.id]\n    },\n\n    addSubreddit: function(ev) {\n        ev.preventDefault()\n\n        var nameEl = this.$('.add-sr .sr-name'),\n            srNames = nameEl.val()\n        srNames = srNames.split(/[+,\\-\\s]+/)\n        // Strip any /r/ or r/ prefixes.\n        srNames = srNames.map(function(sr) { return sr.replace(/^\\/?r\\//, '') })\n        srNames = _.compact(srNames)\n        if (!srNames.length) {\n            return\n        }\n\n        nameEl.val('')\n        this.$('.add-error').css('visibility', 'hidden')\n        this.model.addSubreddit(srNames, {\n            wait: true,\n            success: _.bind(function() {\n                this.$('.add-error').hide()\n            }, this),\n            error: _.bind(function(model, xhr) {\n                var resp = JSON.parse(xhr.responseText)\n                this.$('.add-error')\n                    .text(resp.explanation)\n                    .css('visibility', 'visible')\n                    .show()\n            }, this)\n        })\n    }\n})\n\nr.multi.MultiDetails = Backbone.View.extend({\n    events: {\n        'change [name=\"visibility\"]': 'setVisibility',\n        'change [name=\"key_color\"]': 'setKeyColor',\n        'change [name=\"icon_name\"]': 'setIconName',\n        'click .show-copy': 'showCopyMulti',\n        'click .show-rename': 'showRenameMulti',\n        'click .edit-description': 'editDescription',\n        'submit .description': 'saveDescription',\n        'confirm .delete': 'deleteMulti'\n    },\n\n    initialize: function() {\n        this.listenTo(this.model, 'change', this.render)\n        this.listenTo(this.model.subreddits, 'add remove reset', this.render)\n\n        this.addBubble = new r.multi.MultiAddNoticeBubble({\n            parent: this.$('.add-sr .sr-name'),\n            trackHover: false\n        })\n    },\n\n    // create child model and view to manage recommendations\n    initRecommendations: function(recsEl) {\n        var recs = new r.recommend.RecommendationList()\n        this.recsView = new r.recommend.RecommendationsView({\n            collection: recs,\n            el: recsEl\n        })\n \n        // fetch initial data\n        if (!this.model.subreddits.isEmpty()) {\n            recs.fetchForSrs(this.model.getSubredditNames())\n        }\n \n        // update recs when multi changes\n        this.listenTo(this.model.subreddits, 'add remove reset',\n            function() {\n                var srNames = this.model.getSubredditNames()\n                recs.fetchForSrs(srNames)\n            })\n        // update multi when a rec is selected\n        this.recsView.bind('recs:select',\n            function(data) {\n                this.model.addSubreddit(data['srName'])\n            }, this)\n    },\n\n    render: function() {\n        var canEdit = this.model.get('can_edit')\n        if (canEdit) {\n            if (this.model.subreddits.isEmpty()) {\n                this.addBubble.show()\n            } else {\n                this.addBubble.hide()\n            }\n        }\n\n        this.$el.toggleClass('readonly', !canEdit)\n        this.$el.toggleClass('public', this.model.get('visibility') == 'public')\n\n        if (this.model.has('description_html')) {\n            this.$('.description .usertext-body').html(\n                this.model.get('description_html')\n            )\n        }\n\n        this.$('.count').text(this.model.subreddits.length)\n\n        return this\n    },\n\n    setVisibility: function() {\n        this.model.save({\n            visibility: this.$('[name=\"visibility\"]:checked').val()\n        })\n    },\n\n    setKeyColor: function() {\n        this.model.save({\n            key_color: this.$('[name=\"key_color\"]').val()\n        })\n    },\n\n    setIconName: function() {\n        this.model.save({\n            icon_name: this.$('[name=\"icon_name\"]').val()\n        })\n    },\n\n    showCopyMulti: function() {\n        this.$('form.rename-multi').hide()\n\n        var $copyForm = this.$('form.copy-multi')\n\n        $copyForm\n            .show()\n            .find('.multi-name')\n                .val(this.model.name())\n                .select()\n                .focus()\n\n        if (!this.copyForm) {\n            this.copyForm = new r.multi.MultiCopyForm({\n                el: $copyForm,\n                navOnCreate: true,\n                sourceMulti: this.model\n            })\n        }\n    },\n\n    showRenameMulti: function() {\n        this.$('form.copy-multi').hide()\n\n        var $renameForm = this.$('form.rename-multi')\n\n        $renameForm\n            .show()\n            .find('.multi-name')\n                .val(this.model.name())\n                .select()\n                .focus()\n\n        if (!this.renameForm) {\n            this.renameForm = new r.multi.MultiRenameForm({\n                el: $renameForm,\n                navOnCreate: true,\n                sourceMulti: this.model\n            })\n        }\n    },\n\n    deleteMulti: function() {\n        this.model.destroy({\n            success: function() {\n                window.location = '/'\n            }\n        })\n    },\n\n    editDescription: function() {\n        show_edit_usertext(this.$el)\n    },\n\n    saveDescription: function(ev) {\n        ev.preventDefault()\n        this.model.save({\n            'description_md': this.$('.description textarea').val()\n        }, {\n            success: _.bind(function() {\n                hide_edit_usertext(this.$el)\n            }, this)\n        })\n    },\n\n    focusAdd: function() {\n        this.$('.add-sr .sr-name').focus()\n    }\n})\n\nr.multi.MultiAddNoticeBubble = r.ui.Bubble.extend({\n    className: 'multi-add-notice hover-bubble anchor-right',\n    template: _.template('<h3><%- awesomeness_goes_here %></h3><p><%- add_multi_sr %></p>'),\n\n    render: function() {\n        this.$el.html(this.template({\n            awesomeness_goes_here: r._('awesomeness goes here'),\n            add_multi_sr: r._('add a subreddit to your multi.')\n        }))\n    }\n})\n\nr.multi.SubscribeButton = Backbone.View.extend({\n    events: {\n        'mouseenter': 'createBubble'\n    },\n\n    createBubble: function() {\n        if (this.bubble) {\n            return\n        }\n\n        this.bubble = new r.multi.MultiSubscribeBubble({\n            parent: this.$el,\n            group: this.options.bubbleGroup,\n            srName: String(this.$el.data('sr_name'))\n        })\n\n        var bubbleClass = this.$el.data('bubble_class')\n        if (bubbleClass) {\n            this.bubble.$el.removeClass('anchor-right')\n            this.bubble.$el.addClass(bubbleClass)\n        }\n\n        this.bubble.queueShow()\n    }\n})\n\nr.multi.MultiSubscribeBubble = r.ui.Bubble.extend({\n    className: 'multi-selector hover-bubble anchor-right',\n    template: _.template('<div class=\"title\"><strong><%- title %></strong><a class=\"sr\" href=\"/r/<%- sr_name %>\">/r/<%- sr_name %></a></div><div class=\"throbber\"></div>'),\n    itemTemplate: _.template('<label><input class=\"add-to-multi\" type=\"checkbox\" data-path=\"<%- path %>\" <%- checked %>><%- name %><a href=\"<%- path %>\" target=\"_blank\" title=\"<%- open_multi %>\">&rsaquo;</a></label>'),\n    itemCreateTemplate: _.template('<label><form class=\"create-multi\"><input type=\"text\" class=\"multi-name\" placeholder=\"<%- create_msg %>\"><div class=\"error create-multi-error\"></div></form></label>'),\n\n    events: {\n        'click .add-to-multi': 'toggleSubscribed'\n    },\n\n    initialize: function() {\n        this.listenTo(this, 'show', this.load)\n        this.listenTo(r.multi.mine, 'reset add', _.debounce(this.render, 100))\n        r.ui.Bubble.prototype.initialize.apply(this)\n    },\n\n    load: function() {\n        r.ui.showWorkingDeferred(this.$el, r.multi.mine.fetch())\n    },\n\n    render: function() {\n        this.$el.html(this.template({\n            title: r._('categorize'),\n            sr_name: this.options.srName\n        }))\n\n        var content = $('<div class=\"multi-list\">')\n        r.multi.mine.chain()\n            .sortBy(function(multi) {\n                // sort multireddits containing this subreddit to the top.\n                return multi.subreddits.getByName(this.options.srName)\n            }, this)\n            .each(function(multi) {\n                content.append(this.itemTemplate({\n                    name: multi.get('name'),\n                    path: multi.get('path'),\n                    checked: multi.subreddits.getByName(this.options.srName)\n                             ? 'checked' : '',\n                    open_multi: r._('open this multi')\n                }))\n            }, this)\n        content.append(this.itemCreateTemplate({\n            create_msg: r._('create a new multi')\n        }))\n        this.$el.append(content)\n\n        this.createForm = new r.multi.MultiCreateForm({\n            el: this.$('form.create-multi')\n        })\n    },\n\n    toggleSubscribed: function(ev) {\n        var checkbox = $(ev.target),\n            multi = r.multi.mine.get(checkbox.data('path'))\n        if (checkbox.is(':checked')) {\n            multi.addSubreddit(this.options.srName)\n        } else {\n            multi.removeSubreddit(this.options.srName)\n        }\n    }\n})\n\nr.multi.MultiCreateForm = Backbone.View.extend({\n    events: {\n        'submit': 'createMulti'\n    },\n\n    createMulti: function(ev) {\n        ev.preventDefault()\n\n        var name = this.$('input.multi-name').val()\n        name = $.trim(name)\n        if (!name) {\n            return\n        }\n\n        var deferred = this._createMulti(name)\n\n        deferred\n            .done(_.bind(function(multi) {\n                this.trigger('create', multi)\n                if (this.options.navOnCreate) {\n                    window.location = multi.get('path') + '#created'\n                }\n            }, this))\n            .fail(_.bind(function(xhr) {\n                var resp = JSON.parse(xhr.responseText)\n                this.showError(resp.explanation)\n            }, this))\n\n        r.ui.showWorkingDeferred(this.$el, deferred)\n    },\n\n    _createMulti: function(name) {\n        var newMulti = new r.multi.MultiReddit({\n                path: r.multi.mine.pathByName(name)\n            }, {isNew: true})\n\n        var deferred = new $.Deferred\n        r.multi.mine.create(newMulti, {\n            wait: true,\n            success: _.bind(deferred.resolve, deferred),\n            error: function(multi, xhr) {\n                deferred.reject(xhr)\n            }\n        })\n\n        return deferred\n    },\n\n    showError: function(error) {\n        this.$('.error').text(_.unescape(error)).show()\n    },\n\n    focus: function() {\n        this.$('.multi-name').focus()\n    }\n})\n\nr.multi.MultiCopyForm = r.multi.MultiCreateForm.extend({\n    _createMulti: function(name) {\n        return this.options.sourceMulti.copyTo(r.multi.mine, name)\n    }\n})\n\nr.multi.MultiRenameForm = r.multi.MultiCopyForm.extend({\n    _createMulti: function(name) {\n        return this.options.sourceMulti.renameTo(r.multi.mine, name)\n    }\n})\n\nr.multi.ListingChooser = Backbone.View.extend({\n    events: {\n        'click .create button': 'createClick',\n        'click .grippy': 'toggleCollapsed'\n    },\n\n    initialize: function() {\n        this.$el.addClass('initialized')\n\n        // transition collapsed state to server pref\n        if (store.safeGet('ui.collapse.listingchooser') == true) {\n            this.toggleCollapsed(true)\n        }\n        store.safeSet('ui.collapse.listingchooser')\n\n        // HACK: fudge page heights for long lists of multis / short pages\n        var thisHeight = this.$('.contents').height(),\n            bodyHeight = $('body').height()\n        if (thisHeight > bodyHeight) {\n            $('body').css('padding-bottom', thisHeight - bodyHeight + 100)\n        }\n    },\n\n    createClick: function(ev) {\n        if (!this.$('.create').is('.expanded')) {\n            ev.preventDefault()\n            this.$('.create').addClass('expanded')\n            this.createForm = new r.multi.MultiCreateForm({\n                el: this.$('.create form'),\n                navOnCreate: true\n            })\n            this.createForm.focus()\n        }\n    },\n\n    toggleCollapsed: function(value) {\n        $('body').toggleClass('listing-chooser-collapsed', value)\n        Backbone.ajax({\n            type: 'POST',\n            url: '/api/set_left_bar_collapsed.json',\n            data: {\n                'collapsed': $('body').is('.listing-chooser-collapsed')\n            }\n        })\n    }\n})\n"
  },
  {
    "path": "r2/r2/public/static/js/newsletter.js",
    "content": "r.newsletter = {\n  post: function(form) {\n    var email = $('input[name=\"email\"]', form.$el).val();\n    var apiTarget = form.$el.attr('action');\n\n    var params = form.serialize();\n    params.push({name:'api_type', value:'json'});\n\n    return r.ajax({\n      url: apiTarget,\n      type: 'POST',\n      dataType: 'json',\n      data: params,\n      xhrFields: {\n        withCredentials: true\n      }\n    });\n  }\n};\n\nr.newsletter.ui = {\n  _setupNewsletterBar: function() {\n    var newsletterBarSeen = !!store.safeGet('newsletterbar.seen');\n    if (newsletterBarSeen || r.ui.isSmallScreen()) {\n      return;\n    }\n\n    $('.newsletterbar').show();\n\n    $('.newsletter-close').on('click', function() {\n      $('.newsletterbar').hide();\n    });\n\n    store.safeSet('newsletterbar.seen', true);\n  },\n\n  _setupNewsletter: function() {\n    if (!$('body').hasClass('newsletter')) {\n      return;\n    }\n\n    $('.faq-toggle').click(function(e) {\n      e.preventDefault();\n      $(this).toggleClass('active');\n      $('.faq').slideToggle();\n      r.analytics.fireGAEvent('newsletter-form', 'faq-toggle');\n    });\n  },\n\n  init: function() {\n    $('.newsletter-signup').each(function(i, el) {\n      new r.newsletter.ui.NewsletterForm(el)\n    });\n\n    this._setupNewsletterBar();\n\n    this._setupNewsletter();\n  },\n};\n\nr.newsletter.ui.NewsletterForm = function() {\n  r.ui.Form.apply(this, arguments)\n};\n\nr.newsletter.ui.NewsletterForm.prototype = $.extend(new r.ui.Form(), {\n  showStatus: function() {\n    this.$el.find('.error').css('opacity', 1)\n    r.ui.Form.prototype.showStatus.apply(this, arguments)\n  },\n  \n  _submit: function() {\n    r.analytics.fireGAEvent('newsletter-form', 'submit');\n    return r.newsletter.post(this);\n  },\n\n  _showSuccess: function() {\n      var parentEl = this.$el.parents('.newsletter-container');\n      parentEl.find('.result-message').text(r._('check your inbox to confirm your subscription'));\n      parentEl.addClass('success');\n      parentEl.find('header').fadeTo(250, 1);\n  },\n\n  _handleResult: function(result) {\n    if (result.json.errors.length) {\n      return r.ui.Form.prototype._handleResult.call(this, result);\n    }\n\n    var parentEl = this.$el.parents('.newsletter-container');\n    parentEl.find('header, form').fadeTo(250, 0, function() {\n      this._showSuccess();\n    }.bind(this));\n  }\n})\n"
  },
  {
    "path": "r2/r2/public/static/js/policies.js",
    "content": "r.ui.TOCScroller = Backbone.View.extend({\n    scrollSpeed: .4,\n\n    events: {\n        'click a': 'scrollToLinkHash'\n    },\n\n    initialize: function(options) {\n        this.$doc = $(options.doc)\n        this.$toc = $(options.toc)\n        this.$progress = $('<div class=\"location\">').appendTo(this.$toc)\n        this.updateProgress = _.throttle(this._updateProgress, 10)\n    },\n\n    start: function() {\n        $(window).bind('scroll', $.proxy(this, 'updateProgress'))\n        $(window).bind('hashchange', _.bind(function(ev) {\n            this.updateProgress()\n        }, this))\n        this.updateProgress()\n    },\n\n    scrollToLinkHash: function(ev) {\n        var newHash = $(ev.target).attr('href').match(/#.*/)[0]\n        this.smoothScrollToHash(newHash)\n        this.updateProgress()\n        ev.preventDefault()\n    },\n\n    smoothScrollToHash: function(newHash) {\n        var $heading = this._headingForHash(newHash),\n            lastScrollTop = $(document).scrollTop(),\n            scrollTarget = Math.min(this._scrollMax(), $heading.offset().top)\n\n        // we need to animate both `html` and `body` to deal with Webkit/non-Webkit inconsistency\n        $('html, body')\n            .stop(true)\n            .animate({scrollTop: scrollTarget}, {\n                duration: this.scrollSpeed * Math.abs(scrollTarget - lastScrollTop),\n                step: function(now, fx) {\n                    var curScrollTop = $(this).scrollTop()\n                    if (lastScrollTop > 0 && curScrollTop == 0) {\n                        // step called on the element of (html/body) that can't scroll\n                        return\n                    }\n\n                    if (Math.abs(curScrollTop - lastScrollTop) > 5) {\n                        // if the user has scrolled manually, interrupt the animation\n                        $('html, body').stop()\n                    }\n                    lastScrollTop = fx.now\n                },\n                complete: function() {\n                    // this will be called twice due to the two-element\n                    // selector above (workaround for firefox/chrome viewport\n                    // overflow inconsistency), which doesn't matter because it\n                    // is idempotent.\n                    location.replace(location.toString().replace(/#.*$/, '') + newHash)\n                }\n            })\n    },\n\n    _scrollMax: function() {\n        return $(document).height() - $(window).height()\n    },\n\n    _headingForHash: function(hash) {\n        return this.$doc.find(document.getElementById(hash.substr(1)))\n    },\n\n    _tocFor: function($heading) {\n        // heading ids can have periods in them, so we'll use getElementsByClassName :(\n        return $(this.$toc[0].getElementsByClassName($heading.attr('id'))).children('a')\n    },\n\n    _updateProgress: function() {\n        var scrollTop = $(document).scrollTop(),\n            scrollMax = this._scrollMax()\n\n        var $hashHeading = this._headingForHash(location.hash).filter('h2')\n        if ($hashHeading.length) {\n            var distance = Math.min(scrollMax, $hashHeading.offset().top) - scrollTop\n            if (Math.abs(distance) <= 10) {\n                this.$progress.css('top', this._tocFor($hashHeading).position().top)\n                return\n            }\n        }\n\n        var $nextHeading = $(_.find(this.$doc.children('h2'), function(el) {\n                return $(el).offset().top >= scrollTop\n            }, this)),\n            $prevHeading = $nextHeading.prevAll('h2').first()\n\n        if (!$nextHeading.length || scrollTop == scrollMax) {\n            $prevHeading = $nextHeading = $('h2:last')\n        }\n\n        if (!$prevHeading.length) {\n            this.$progress.css('top', 0)\n        } else {\n            var nextTop = $nextHeading.offset().top,\n                prevTop = $prevHeading.offset().top,\n                sectionPercent = (scrollTop - prevTop) / (nextTop - prevTop),\n                nextToc = this._tocFor($nextHeading),\n                prevToc = this._tocFor($prevHeading),\n                nextTocTop = nextToc.position().top,\n                prevTocTop = prevToc.position().top\n\n            sectionPercent = r.utils.clamp(sectionPercent, 0, 1)\n            var beadTop = prevTocTop + (nextTocTop - prevTocTop) * sectionPercent\n            this.$progress.css('top', beadTop)\n        }\n    }\n})\n\n$(function() {\n    new r.ui.scrollFixed($('.policy-page .doc-info'))\n    new r.ui.TOCScroller({\n        el: $('.policy-page'),\n        doc: $('.policy-page .md'),\n        toc: $('.policy-page .doc-info .toc')\n    }).start()\n})\n"
  },
  {
    "path": "r2/r2/public/static/js/popup.js",
    "content": ";(function(ui, $, _, window, undefined) {\n  var SIZE_CLASS_LOOKUP = {\n    large: 'modal-dialog-lg',\n    medium: '',\n    small: 'modal-dialog-sm',\n  };\n\n  var DEFAULTS = {\n    template: template,\n    animate: true,\n    close: true,\n    modal: true,\n    shortcuts: true,\n    footer: false,\n    content: '',\n    className: '',\n    title: false,\n    size: 'medium',\n  };\n\n  var template = _.template(\n    '<div class=\"modal <% if (animate) { %> fade <% } %> <%- className %>\">' +\n      '<div class=\"modal-dialog <% if (size) { %><%- SIZE_CLASS_LOOKUP[size] %><% } %>\">' +\n        '<div class=\"modal-content\">' +\n          '<% if (title || close) { %>' +\n            '<div class=\"modal-header\">' +\n              '<% if (close) { %>' +\n                '<a href=\"javascript: void 0;\" class=\"c-close c-hide-text\" data-dismiss=\"modal\">' +\n                  _.escape(r._('close this window')) +\n                '</a>' +\n              '<% } %>' +\n              '<% if (title) { %>' +\n                '<%= title %>' +\n              '<% } %>' +\n            '</div>' +\n          '<% } %>' +\n          '<div class=\"modal-body\">' +\n            '<%= content %>' +\n          '</div>' +\n          '<% if (footer) { %>' +\n            '<div class=\"modal-footer\">' +\n              '<%= footer %>' +\n            '</div>' +\n          '<% } %>' +\n        '</div>' +\n      '</div>' +\n    '</div>'\n  );\n\n  var Popup = ui.Popup = function(options) {\n    options = _.extend({}, DEFAULTS, options, {SIZE_CLASS_LOOKUP: SIZE_CLASS_LOOKUP});\n\n    var html = template(options);\n    var listener = this.listener = $({});\n    var $el = this.$ = $(html);\n\n    $el.modal({\n      show: false,\n      backdrop: !!options.modal,\n      keyboard: options.shortcuts,\n    });\n\n    [\n      ['show.bs.modal', 'show.r.popup'],\n      ['shown.bs.modal', 'opened.r.popup'],\n      ['hide.bs.modal', 'hide.r.popup'],\n      ['hidden.bs.modal', 'closed.r.popup'],\n    ].forEach(function(tuple) {\n      $el.on(tuple[0], function() {\n        listener.trigger(tuple[1]);\n      });\n    });\n\n    // ensures element is 'focusable', otherwise closing with esc key only\n    // works when an input inside the modal is focused.\n    $el.attr('tabindex', $el.attr('tabindex') || 0);\n  };\n\n  [\n    'show',\n    'hide',\n    'toggle'\n  ].forEach(function(fn) {\n    Popup.prototype[fn] = function() {\n      this.$.modal(fn);\n    };\n  });\n\n  [\n    ['on', 'on'],\n    ['once', 'one'],\n    ['off', 'off'],\n  ].forEach(function(tuple) {\n    Popup.prototype[tuple[0]] = function() {\n      this.listener[tuple[1]].apply(this.listener, arguments);\n    };\n  });\n\n})(((this.r = this.r || {}) && (r.ui = r.ui || {})), this.jQuery, this._, this);\n"
  },
  {
    "path": "r2/r2/public/static/js/post-sharing.js",
    "content": "!function(r) {\n  'use strict';\n\n  /*\n    Renders a single sharing option and it's tooltip\n   */\n  var shareOptionTemplate = _.template(\n    '<div class=\"post-sharing-option post-sharing-option-<%- name %>\" ' +\n                 'data-post-sharing-option=\"<%- name %>\">' +\n      '<div class=\"c-tooltip\" role=\"tooltip\">' +\n        '<div class=\"tooltip-arrow bottom\"></div>' +\n        '<div class=\"tooltip-inner\"><%- tooltip %></div>' +\n      '</div>' +\n    '</div>'\n  );\n\n  /*\n    Render's the widget used by $.fn.stateify to display feedback to the user,\n    e.g. error messages.\n   */\n  var feedbackTemplate = _.template(\n    '<div class=\"c-form-control-feedback-wrapper\" ref=\"<%- ref %>\">' +\n      '<span class=\"c-form-control-feedback c-form-control-feedback-throbber\"></span>' +\n      '<span class=\"c-form-control-feedback c-form-control-feedback-error\"></span>' +\n      '<span class=\"c-form-control-feedback c-form-control-feedback-success\"></span>' +\n    '</div>'\n  );\n\n  var postSharingTemplate = _.template(\n    '<a href=\"javascript: void 0;\" class=\"c-close c-hide-text\">' +\n      _.escape(r._('close this window')) +\n    '</a>' +\n    '<div class=\"post-sharing-main post-sharing-form\" ref=\"$mainForm\">' +\n      '<% if (options) { %>' +\n        '<div class=\"c-form-group\">' +\n          '<div class=\"post-sharing-label\">' +\n            _.escape(r._('Share with:')) +\n          '</div>' +\n          '<div class=\"post-sharing-options\">' +\n            '<% options.forEach(function(option) { %>' +\n              '<%= option %>' +\n            '<% }) %>' +\n          '</div>' +\n        '</div>' +\n      '<% } %>' +\n      '<% if (link) { %>' +\n        '<div class=\"c-form-group\">' +\n          '<div class=\"post-sharing-label\">' +\n            _.escape(r._('Link:')) +\n          '</div>' +\n          '<input class=\"post-sharing-link-input c-form-control\" ' +\n                 'ref=\"$postSharingLinkInput\" ' +\n                 'name=\"link\" ' +\n                 'type=\"text\" ' +\n                 'readonly ' +\n                 'value=\"<%- link %>\">' +\n        '</div>' +\n      '<% } %> ' +\n    '</div>' +\n    '<div class=\"post-sharing-email-form post-sharing-form\" ref=\"$emailForm\">' +\n      '<p class=\"post-sharing-label\">' +\n        '<span ref=\"$emailFormEmailLabel\">' + _.escape(r._('Share via email as %(username)s').format({ username: r.config.logged })) + '</span>' +\n        '<span ref=\"$emailFormPMLabel\">' + _.escape(r._('Share via private message on reddit as %(username)s').format({ username: r.config.logged })) + '</span>' +\n      '</p>' +\n      '<div class=\"c-form-group\">' +\n        '<input class=\"post-sharing-recipient-input c-form-control\" ' +\n               'ref=\"$shareTo\" ' +\n               'name=\"recipient\" type=\"text\" ' +\n               'placeholder=\"name@example.com, name@example.com\" ' +\n               'data-placeholder-email=\"name@example.com, name@example.com\" ' +\n               'data-placeholder-reddit-pm=\"username\">' +\n        feedbackTemplate({ ref: '$shareToFeedback' }) +\n      '</div>' +\n      '<div class=\"c-form-group\">' +\n        '<textarea class=\"post-sharing-message-input c-form-control\" ' +\n                  'ref=\"$shareMessage\" ' +\n                  'name=\"message\" ' +\n                  'placeholder=\"' + _.escape(r._('add optional message')) + '\"></textarea>' +\n        feedbackTemplate({ ref: '$messageFeedback' }) +\n      '</div>' +\n      '<div class=\"c-form-group\">' +\n        '<div class=\"post-sharing-buttons\">' +\n          '<button class=\"post-sharing-submit c-btn c-btn-primary c-pull-right\">' +\n            _.escape(r._('send')) +\n          '</button>' +\n          '<button class=\"post-sharing-cancel c-btn c-btn-secondary c-pull-right\">' +\n            _.escape(r._('cancel')) +\n          '</button>' +\n        '</div>' +\n        feedbackTemplate({ ref: '$requestStateFeedback' }) +\n        '<div class=\"post-sharing-shareplane\" ref=\"$shareplane\">' +\n          _.escape(r._('sent!')) +\n        '</div>' +\n      '</div>' +\n    '</div>'\n  );\n\n  var PostSharingState = Backbone.Model.extend({\n    defaults: function() {\n      return {\n        errors: [],\n        options: null,\n        requestState: 'UNSENT',\n        selectedOption: null,\n        link: null,\n      };\n    },\n  });\n\n\n  r.ui.PostSharing = Backbone.View.extend({\n    animationSpeed: 200,\n\n    _template: postSharingTemplate,\n\n    _model: PostSharingState,\n\n    events: {\n      'click .post-sharing-link-input': 'selectLinkInputText',\n      'copy .post-sharing-link-input': 'fireCopyEvent',\n      'click .post-sharing-option': 'setPostSharingOption',\n      'click .post-sharing-cancel': 'clearPostSharingOption',\n      'click .post-sharing-submit': 'shareToEmail',\n      'click .c-close': 'close',\n    },\n\n    initialize: function() {\n      var $thing = this.options.$thing;\n\n      var fullname = $thing.data('fullname');\n      var id = fullname.split('_')[1];\n      var title = $thing.find('.entry a.title').text();\n      var link = $thing.find('.entry a.comments').attr('href');\n\n      this.thingData = {\n        fullname: fullname,\n        title: title,\n        link: link,\n      };\n\n      var props = this.options.props || {};\n      props.link = this.getShareLink('link');\n      this.state = new this._model(props);\n      this.listenTo(this.state, 'change', this.render);\n\n      this.initialRender();\n    },\n\n    initialRender: function() {\n      var renderVars = this.state.toJSON();\n\n      if (renderVars.options) {\n        renderVars.options = renderVars.options.map(shareOptionTemplate);\n      }\n\n      this.$el.html(this._template(renderVars));\n\n      this.refs = {};\n      var $refs = this.$el.find('[ref]');\n      $refs.toArray().forEach(function(ref) {\n        var $ref = $(ref);\n        var refName = $ref.attr('ref');\n        this.refs[refName] = $ref;\n      }, this);\n\n      this.refs.$mainForm.css('display', 'block');\n      this.render();\n    },\n\n    show: function() {\n      this.$el.slideDown(this.animationSpeed, 'swing', function() {\n        this.trigger('show', this);\n        this.selectLinkInputText();\n      }.bind(this));\n    },\n\n    hide: function() {\n      this.$el.slideUp(this.animationSpeed, 'swing', function() {\n        this.trigger('hide');\n      }.bind(this));\n    },\n\n    unmount: function() {\n      this.remove();\n      this.trigger('unmount', this);\n    },\n\n    close: function() {\n      this.once('hide', this.unmount.bind(this));\n      this.hide();\n      this.trigger('close');\n    },\n\n    setPostSharingOption: function(e) {\n      var $el = $(e.target).closest('.post-sharing-option');\n      var option = $el.data('post-sharing-option');\n\n      switch (option) {\n        case 'email':\n          // fall through\n        case 'reddit-pm':\n          return this.state.set({\n            selectedOption: option,\n          });\n\n        case 'facebook':\n          return this.shareToFacebook();\n\n        case 'twitter':\n          return this.shareToTwitter();\n\n        case 'tumblr':\n          return this.shareToTumblr();\n\n        default:\n          this.close();\n      }\n    },\n\n    getShareLink: function(refSource) {\n      var refParams = {\n        ref: 'share',\n        ref_source: refSource,\n      };\n\n      return r.utils.replaceUrlParams(this.thingData.link, refParams);\n    },\n\n    shareToFacebook: function() {\n      var redditUrl = this.getShareLink('facebook');\n      var redirectUrl = r.config.currentOrigin + '/share/close';\n      var shareParams = {\n        app_id: r.config.facebook_app_id,\n        display: 'popup',\n        link: redditUrl,\n        description: this.thingData.title,\n        redirect_uri: redirectUrl,\n      }\n      var shareUrl = r.utils.replaceUrlParams('https://www.facebook.com/dialog/feed', shareParams);\n\n      this.openWebIntent(shareUrl, 'facebook');\n    },\n\n    shareToTwitter: function() {\n      var redditUrl = this.getShareLink('twitter');\n      var twitterHandle = 'reddit'\n      var title = this.thingData.title;\n\n      // current twitter short url length is 23, +1 for space after, and +1 for\n      // padding if the shortlink length grows.  The proper way to handle this\n      // is to set up a twitter app and fetch the current shortlink length from\n      // twitter on a daily cron\n      var twitterShortLinkLength = 25;\n\n      var tweetText = [title, 'via', '@' + twitterHandle].join(' ');\n      var tweetTextLength = tweetText.length + twitterShortLinkLength;\n      var maxTweetLength = 140;\n      var minTitleLength = 10\n\n      // -1 at the end is to account for the ellipsis that we append\n      var truncatedTitleLength = title.length - (tweetTextLength - maxTweetLength) - 1;\n\n      if (tweetText.length > maxTweetLength) {\n        title = title.slice(0, Math.max(minTitleLength, truncatedTitleLength));\n        title = title.trim() + '…';\n      }\n\n      var shareParams = {\n        url: redditUrl,\n        text: title,\n        via: twitterHandle,\n      };\n      var shareUrl = r.utils.replaceUrlParams('https://twitter.com/intent/tweet', shareParams);\n\n      this.openWebIntent(shareUrl, 'twitter');\n    },\n\n    shareToTumblr: function() {\n      var redditUrl = this.getShareLink('tumblr');\n      var title = this.thingData.title;\n      var shareParams = {\n        canonicalUrl: redditUrl,\n        posttype: 'link',\n        title: title,\n      };\n      var shareUrl = r.utils.replaceUrlParams('https://www.tumblr.com/widgets/share/tool', shareParams);\n\n      this.openWebIntent(shareUrl, 'tumblr');\n    },\n\n    openWebIntent: function(shareUrl, windowTitle) {\n      var windowWidth = window.innerWidth;\n      var windowHeight = window.innerHeight;\n      var popupWidth = 550;\n      var popupHeight = 420;\n\n      var windowFeatures = {\n        height: popupHeight,\n        width: popupWidth,\n        left: windowWidth / 2 - popupWidth / 2,\n        top: windowHeight / 2 - popupHeight / 2,\n      };\n\n      var windowFeaturesString = Object.keys(windowFeatures).map(function(key) {\n        return key + '=' + windowFeatures[key];\n      }).join(',');\n\n      window.open(shareUrl, windowTitle, windowFeaturesString);\n      this.trigger('web-intent', windowTitle);\n    },\n\n    shareToEmail: function() {\n      this.state.set({\n        requestState: 'LOADING',\n      });\n\n      $.request('share', {\n          share_from: '',\n          replyto: '',\n          share_to: this.refs.$shareTo.val(),\n          message: this.refs.$shareMessage.val(),\n          parent: this.thingData.fullname,\n          api_type: 'json',\n        }, function(res) {\n          if (this.state.get('requestState') !== 'LOADING') {\n            return;\n          }\n\n          var jsonResponse = res.json;\n\n          if (!jsonResponse) {\n            throw 'share api response error';\n          } else if (!jsonResponse.errors.length) {\n            this.state.set({\n              errors: [],\n              requestState: 'DONE',\n            });\n\n            setTimeout(function() {\n              this.close();\n            }.bind(this), 1500);\n          } else {\n            this.state.set({\n              errors: jsonResponse.errors,\n              requestState: 'DONE',\n            });\n          }\n        }.bind(this));\n    },\n\n    clearPostSharingOption: function(e) {\n      this.state.set({\n        requestState: 'UNSENT',\n        selectedOption: null,\n      });\n    },\n\n    selectLinkInputText: function() {\n      this.refs.$postSharingLinkInput.focus().select();\n    },\n\n    fireCopyEvent: function() {\n      this.trigger('link', 'copy');\n    },\n\n    getFeedbackRef: function(key) {\n      switch (key) {\n        case \"BAD_EMAIL\":\n        case \"BAD_EMAILS\":\n        case \"NO_EMAILS\":\n          return this.refs.$shareToFeedback;\n\n        case \"TOO_LONG\":\n          return this.refs.$messageFeedback;\n\n        default:\n          return this.refs.$requestStateFeedback;\n      }\n    },\n\n    onOpenEmailForm: function() {\n        this.refs.$shareTo.focus();\n    },\n\n    render: function() {\n      var requestState = this.state.get('requestState');\n      var errors = this.state.get('errors');\n      var changed = this.state.changed;\n      var selectedOption = this.state.get('selectedOption');\n\n      if ('selectedOption' in changed) {\n          this.refs.$emailFormEmailLabel.toggle(selectedOption === 'email');\n          this.refs.$emailFormPMLabel.toggle(selectedOption === 'reddit-pm');\n\n          this.refs.$shareTo.attr('placeholder', this.refs.$shareTo.data('placeholder-' + selectedOption));\n\n        if (selectedOption === 'email' || selectedOption === 'reddit-pm') {\n          this.refs.$mainForm.slideUp('fast');\n          this.refs.$emailForm.slideDown('fast', this.onOpenEmailForm.bind(this));\n        } else {\n          this.refs.$mainForm.slideDown('fast');\n          this.refs.$emailForm.slideUp('fast');\n        }\n      }\n\n\n      if (selectedOption === 'email' || selectedOption === 'reddit-pm') {\n        this.refs.$shareToFeedback.stateify('clear');\n        this.refs.$messageFeedback.stateify('clear');\n        this.refs.$requestStateFeedback.stateify('clear');\n\n        if (errors.length) {\n          errors.forEach(function(error) {\n            var key = error[0];\n            var message = error[1];\n            var $ref = this.getFeedbackRef(key)\n\n            $ref.stateify('set', 'error', message);\n          }, this);\n        }\n\n        if (requestState === 'LOADING') {\n          this.refs.$requestStateFeedback.stateify('set', 'loading');\n        } else if (requestState === 'DONE' && !errors.length) {\n          this.refs.$requestStateFeedback.stateify('set', 'success');\n          this.refs.$emailForm.addClass('shared');\n        }\n      }\n    },\n  });\n\n\n  $(function() {\n    $('body').on('click', '.post-sharing-button', function(e) {\n      e.preventDefault();\n\n      var $shareButton = $(e.target);\n      var $parentThing = $shareButton.closest('.thing');\n      var thingId = $parentThing.thing_id();\n\n      if (r.ui.activeShareMenu && r.ui.activeShareMenu.$el.is(':visible')) {\n        var isSameLink = r.ui.activeShareMenu.options.$thing[0] === $parentThing[0];\n        r.ui.activeShareMenu.close();\n\n        if (isSameLink) {\n          return;\n        }\n      }\n\n      var shareOptions = [\n        {\n          name: 'facebook',\n          tooltip: r._('Share to %(name)s').format({name: 'Facebook'}),\n        },\n        {\n          name: 'twitter',\n          tooltip: r._('Share to %(name)s').format({name: 'Twitter'}),\n        },\n        {\n          name: 'tumblr',\n          tooltip: r._('Share to %(name)s').format({name: 'Tumblr'}),\n        },\n      ];\n\n      if (r.config.logged && !r.config.user_in_timeout\n          && r.config.email_verified) {\n        shareOptions.push({\n          name: 'email',\n          tooltip: r._('Email to a Friend'),\n        });\n      }\n\n      if (r.config.logged && !r.config.user_in_timeout) {\n        shareOptions.push({\n          name: 'reddit-pm',\n          tooltip: r._('Private Message a Friend on Reddit'),\n        });\n      }\n\n      var postSharing = new r.ui.PostSharing({\n        className: 'post-sharing',\n        $thing: $parentThing,\n        props: {\n          options: shareOptions,\n        }\n      });\n\n      $parentThing.find('.entry .buttons').after(postSharing.el);\n\n      r.ui.activeShareMenu = postSharing;\n\n      postSharing.on('show', function() {\n        r.analytics.fireGAEvent('post-sharing', 'open', thingId);\n      });\n\n      postSharing.on('unmount', function() {\n        if (r.ui.activeShareMenu === postSharing) {\n          r.ui.activeShareMenu = null;\n        }\n      });\n\n      postSharing.on('close', function() {\n        var eventName = postSharing.state.get('selectedOption') ? 'close' : 'cancel';\n        r.analytics.fireGAEvent('post-sharing', eventName, thingId);\n      });\n\n      postSharing.on('web-intent', function(refSource) {\n        r.analytics.fireGAEvent('post-sharing', 'share-to-' + refSource, thingId);\n      });\n\n      postSharing.on('link', function(action) {\n        r.analytics.fireGAEvent('post-sharing', 'link-' + action, thingId);\n      });\n\n      postSharing.show();\n    });\n  });\n}(r);\n"
  },
  {
    "path": "r2/r2/public/static/js/preload.js",
    "content": "r.preload = {\n    timestamp: new Date(),\n    maxAge: 5 * 60 * 1000,\n    data: {},\n\n    isExpired: function() {\n        return new Date() - this.timestamp > this.maxAge\n    },\n\n    set: function(data) {\n        _.extend(this.data, data)\n    },\n\n    read: function(url) {\n        var data = this.data[url]\n\n        // short circuit \"client side\" fragment urls (which don't expire)\n        if (url[0] == '#') {\n            return data\n        }\n\n        if (this.isExpired()) {\n            return\n        }\n\n        return data\n    }\n}\n\n"
  },
  {
    "path": "r2/r2/public/static/js/qrcode.js",
    "content": "(function($) {\n    $.fn.make_totp_qrcode = function (secret) {\n        var form = $('#pref-otp'),\n            newform = $('#pref-otp-qr'),\n            placeholder = $('<div>')\n\n        var username = encodeURIComponent(\"/u/\" + r.config.logged);\n        var params = $.param({\n          \"secret\": secret,\n          \"issuer\": r.config.cur_domain,\n        });\n        var uri = 'otpauth://totp/' + username + '?' + params;\n\n        newform.find('#otp-secret-info').append(\n            placeholder,\n            $('<p class=\"secret\">').text(secret)\n        )\n\n        placeholder.qrcode({\n            width: 256,\n            height: 256,\n            text: uri\n        })\n\n        newform.show()\n        form.hide()\n    }\n})(jQuery)\n"
  },
  {
    "path": "r2/r2/public/static/js/recommender.js",
    "content": "r.recommend = {\n    init: function() {\n        $('.explore-item').each(function(idx, el) {\n            new r.recommend.ExploreItem({el: el})\n        })\n    }\n}\n\nr.recommend.Recommendation = Backbone.Model.extend()\n\n/**\n * Example usage:\n * // generate recs for people who like book subreddits\n * var recs = r.recommend.RecommendationList()\n * recs.fetchForSrs(['books', 'writing'])  // triggers reset event\n * // get a new set of recs\n * recs.fetchNewRecs()  // triggers reset event\n * // the user also likes /r/excerpts so generate recs for it too\n * recs.fetchForSrs(['books', 'writing', 'excerpts'])\n * // keep fetching until none are left\n * while (recs.models.length > 0) {\n *   recs.fetchNewRecs()\n * }\n * // allow previously seen recs to appear again (but results might not be the\n * // same as above because srNames has changed)\n * recs.clearHistory()\n * recs.fetchRecs()\n */\nr.recommend.RecommendationList = Backbone.Collection.extend({\n\n    // { srName: 'books' }\n    model: r.recommend.Recommendation,\n\n    // names of subreddits for which recommendations are generated\n    // (if user likes srNames, he will also like...)\n    srNames: [],\n\n    // names of subreddits that should be excluded from future recs because\n    // the user has already seen and dismissed them\n    dismissed: [],\n\n    // loads recs for srNames and stores srNames so they can be used in future\n    // fetches. fires reset event\n    fetchForSrs: function(srNames) {\n        if (!srNames.length) {  // skip unnecessary request\n            this.srNames = []\n            this.reset([])\n            return\n        }\n        this.srNames = srNames\n        this.fetchRecs()\n    },\n    \n    // adds current recs to the dismissed list so they won't be shown again\n    // and refetches. fires reset event\n    fetchNewRecs: function() {\n        var currentRecs = this.pluck('sr_name')\n        this.dismissed = _.union(this.dismissed, currentRecs)\n        this.fetchRecs()\n    },\n\n    // requests data from the server based on values of member vars\n    fetchRecs: function() {\n        var url = '/api/recommend/sr/' + this.srNames.join(',')\n        this.fetch({ url: url,\n                     data: {'omit': this.dismissed.join(',')},\n                     reset: true,\n                     error: _.bind(function() {\n                         this.reset([])\n                     }, this)})\n    },\n\n    // allows previously dismissed recs to be shown again\n    clearHistory: function() {\n        this.dismissed = []\n    }\n})\n\nr.recommend.RecommendationsView = Backbone.View.extend({\n    collection: r.recommend.RecommendationList,\n\n    tagName: 'div',\n\n    itemTemplate: _.template('<li class=\"rec-item\"><a href=\"/r/<%- sr_name %>\" title=\"<%- sr_name %>\" target=\"_blank\">/r/<%- sr_name %></a><button class=\"add add-rec\" data-srname=\"<%- sr_name %>\"></button></li>'),\n\n    initialize: function() {\n        this.listenTo(this.collection, 'add remove reset', this.render)\n    },\n\n    events: {\n        'click .add-rec': 'onAddClick',\n        'click .more': 'showMore',\n        'click .reset': 'resetRecommendations'\n    },\n\n    render: function() {\n        this.$('.recommendations').empty()\n        // if there are results, show them\n        if (this.collection.models.length > 0) {\n            this.$('.recs').show()\n            this.$('.endoflist').hide()\n            var el = this.$el\n            var view = this\n            this.collection.each(function(rec) {\n                this.$('.recommendations').append(view.itemTemplate({sr_name: rec.get('sr_name')}))\n            }, this)\n            this.$el.css({opacity: 1.0})\n        // if recs are empty but the dismissed list is not, all available recs\n        // have been seen and we give user an option to start over\n        } else if (this.collection.dismissed.length > 0) {\n            this.$('.recs').hide()\n            this.$('.endoflist').show()\n        // if there were no results at all, hide the panel\n        } else {\n            this.$el.css({opacity: 0})\n        }\n        return this\n    },\n\n    resetRecommendations: function() {\n        this.collection.clearHistory()\n        this.collection.fetchRecs()\n    },\n\n    // get sr name of selected rec and fire it in a custom event\n    onAddClick: function(ev) {\n        var srName = $(ev.target).data('srname')\n        this.trigger('recs:select', {'srName': srName})\n    },\n\n    showMore: function(ev) {\n        this.collection.fetchNewRecs()\n    }\n})\n\nr.recommend.ExploreItem = Backbone.View.extend({\n    events: {\n        'click .explore-feedback-dismiss': 'dismissSubreddit',\n        'click a': 'recordClick'\n    },  \n\n    dismissSubreddit: function(ev) {\n        var listing = $(ev.target).closest('.explore-item')\n        var sr_name = listing.data('sr_name')\n        var src = listing.data('src')\n        r.ajax({\n            type: 'POST',\n            url: '/api/recommend/feedback',\n            data: { type: 'dis',\n                    srnames: sr_name,\n                    src: src,\n                    page: 'explore' }\n        })\n        this.$('.explore-feedback-dismiss').css({'font-weight':'bold'})\n        $(this.el).fadeOut('fast')\n    },\n\n    recordClick: function(ev) {\n        var listing = $(ev.target).closest('.explore-item')\n        var sr_name = listing.data('sr_name')\n        var src = listing.data('src')\n        r.ajax({\n            type: 'POST',\n            url: '/api/recommend/feedback',\n            data: { type: 'clk',\n                    srnames: sr_name,\n                    src: src,\n                    page: 'explore' }\n        })\n    }\n})\n"
  },
  {
    "path": "r2/r2/public/static/js/reddit-hook.js",
    "content": "/*\n  Init modules defined in reddit.js\n\n  requires r.hooks (hooks.js)\n */\n!function(r) {\n  r.hooks.get('reddit').register(function() {\n    try {\n        r.setupBackbone();\n        r.login.ui.init();\n        r.TimeText.init();\n        r.ui.init();\n        r.interestbar.init();\n        r.visited.init();\n        r.apps.init();\n        r.wiki.init();\n        r.gold.init();\n        r.multi.init();\n        r.recommend.init();\n        r.saved.init();\n        r.messages.init();\n        r.filter.init();\n        r.newsletter.ui.init();\n        r.cachePoisoning.init();\n        r.locked.init();\n    } catch (err) {\n        r.sendError('Error during reddit.js init', err.toString());\n    }\n  });\n\n  $(function() {\n    r.hooks.get('reddit').call();\n  });\n}(r);\n"
  },
  {
    "path": "r2/r2/public/static/js/reddit-init-hook.js",
    "content": "/*\n  Init modules defined in reddit-init.js\n\n  requires r.hooks (hooks.js)\n */\n!function(r) {\n  r.hooks.get('reddit-init').register(function() {\n    try {\n        r.events.init();\n        r.analytics.init();\n        r.access.init();\n    } catch (err) {\n        r.sendError('Error during reddit-init.js init', err.toString());\n    }\n  })\n\n  $(function() {\n    r.hooks.get('reddit-init').call();\n  });\n}(r);\n"
  },
  {
    "path": "r2/r2/public/static/js/reddit.js",
    "content": "function open_menu(menu) {\n    $(menu).siblings(\".drop-choices\").not(\".inuse\")\n        .css(\"top\", menu.offsetHeight + 'px')\n                .each(function(){\n                        $(this).css(\"left\", $(menu).position().left + \"px\")\n                            .css(\"top\", ($(menu).height()+\n                                         $(menu).position().top) + \"px\");\n                    })\n        .addClass(\"active inuse\");\n};\n\nfunction close_menu(item) {\n    $(item).closest('.drop-choices')\n        .removeClass('active inuse');\n}\n\nfunction close_menus(event) {\n    $(\".drop-choices.inuse\").not(\".active\")\n        .removeClass(\"inuse\");\n    $(\".drop-choices.active\").removeClass(\"active\").trigger(\"close_menu\")\n\n    // Clear any flairselectors that may have been opened.\n    $(\".flairselector\").empty();\n\n    /* hide the search expando if the user clicks elsewhere on the page */ \n    if ($(event.target).closest(\"#search\").length == 0) {\n        $(\"#moresearchinfo\").slideUp();\n\n        if ($(\"#searchexpando\").length == 1) {\n            $(\"#searchexpando\").slideUp(function() {\n                $(\"#search_showmore\").parent().show();\n            });\n        } else {\n            $(\"#search_showmore\").parent().show();\n        }\n    }\n};\n\nfunction select_tab_menu(tab_link, tab_name) {\n    var target = \"tabbedpane-\" + tab_name;\n    var menu = $(tab_link).parent().parent().parent();\n    menu.find(\".tabmenu li\").removeClass(\"selected\");\n    $(tab_link).parent().addClass(\"selected\");\n    menu.find(\".tabbedpane\").each(function() {\n        this.style.display = (this.id == target) ? \"block\" : \"none\";\n      });\n}\n\nfunction post_user(form, where) {\n  var user = $(form).find('input[name=\"user\"]').val();\n\n  if (user == null) {\n    return post_form (form, where);\n  } else {\n    return post_form (form, where + '/' + user);\n  }\n}\n\nfunction post_form(form, where, statusfunc, nametransformfunc, block) {\n    try {\n        if (form.disabled) {\n            return false;\n        }\n        if(statusfunc == null)\n            statusfunc = function(x) { \n                return r.config.status_msg.submitting; \n            };\n        /* set the submitted state */\n        $(form).find(\".error\").not(\".status\").hide();\n        $(form).find(\".status\").html(statusfunc(form)).show();\n        return simple_post_form(form, where, {}, block);\n    } catch(e) {\n        return false;\n    }\n};\n\nfunction get_form_fields(form, fields, filter_func) {\n    fields = fields || {};\n    if (!filter_func)\n        filter_func = function(x) { return true; };\n    /* consolidate the form's inputs for submission */\n    $(form).find(\"select, input, textarea\").not(\".gray, :disabled\").each(function() {\n            var $el = $(this),\n                type = $el.attr(\"type\");\n            if (!filter_func(this)) {\n                return;\n            }\n            if ($el.data('send-checked')) {\n                fields[$el.attr(\"name\")] = $el.is(':checked');\n            } else if ((type != \"radio\" && type != \"checkbox\") || $el.is(\":checked\")) {\n                fields[$el.attr(\"name\")] = $el.val();\n            }\n        });\n    if (fields.id == null) {\n        fields.id = $(form).attr(\"id\") ? (\"#\" + $(form).attr(\"id\")) : \"\";\n    }\n    return fields;\n};\n\nfunction form_error(form) {\n    return function(req) {\n        var msg\n        if (req == 'ratelimit') {\n            msg = r._('please wait a few seconds and try again.')\n        } else {\n            msg = r._('an error occurred (status: %(status)s)').format({status: req.status})\n        }\n        $(form).find('.status').text(msg)\n    }\n}\n\nfunction simple_post_form(form, where, fields, block, callback) {\n    $.request(where, get_form_fields(form, fields), callback, block, \n              \"json\", false, form_error(form));\n    return false;\n};\n\nfunction post_pseudo_form(form, where, block) {\n    var filter_func = function(x) {\n        var parent = $(x).parents(\"form:first\");\n        return (parent.length == 0 || parent.get(0) == $(form).get(0))\n    };\n    $(form).find(\".error\").not(\".status\").hide();\n    $(form).find(\".status\").html(r.config.status_msg.submitting).show();\n    $.request(where, get_form_fields(form, {}, filter_func), null, block,\n              \"json\", false, form_error(form));\n    return false;\n}\n\nfunction post_multipart_form(form, where) {\n    $(form).find(\".error\").not(\".status\").hide();\n    $(form).find(\".status\").html(r.config.status_msg.submitting).show();\n    return true;\n}\n\nfunction showlang() {\n    var content = $('#lang-popup').prop('innerHTML');\n    var popup = new r.ui.Popup({\n        className: 'lang-modal',\n        content: content,\n    });\n\n    popup.show();\n\n    return false;\n};\n\n/* table handling */\n\nfunction deleteRow(elem) {\n    $(elem).delete_table_row();\n};\n\n\n\n/* general things */\n\nfunction change_state(elem, op, callback, keep, post_callback) {\n    var form = $(elem).parents(\"form\").first();\n    /* look to see if the form has an id specified */\n    var id = form.find('input[name=\"id\"]');\n    if (id.length) \n        id = id.val();\n    else /* fallback on the parent thing */\n        id = $(elem).thing_id();\n\n    simple_post_form(form, op, {id: id}, undefined, post_callback);\n    /* call the callback first before we mangle anything */\n    if (callback) {\n        callback(form.length ? form : elem, op);\n    }\n    if(!$.defined(keep)) {\n        form.html(form.find('[name=\"executed\"]').val());\n    }\n    return false;\n};\n\nfunction unread_thing(elem) {\n    var t = $(elem);\n    if (!t.hasClass(\"thing\")) {\n        t = t.thing();\n    }\n\n    $(t).addClass(\"new unread\");\n}\n\nfunction read_thing(elem) {\n    var t = $(elem);\n    if (!t.hasClass(\"thing\")) {\n        t = t.thing();\n    }\n    if($(t).hasClass(\"new\")) {\n        $(t).removeClass(\"new\");\n    } else {\n        $(t).removeClass(\"unread\");\n    }\n\n    $.request(\"read_message\", {\"id\": $(t).thing_id()});\n}\n\nfunction click_thing(elem) {\n    var t = $(elem);\n    if (!t.hasClass(\"thing\")) {\n        t = t.thing();\n    }\n    if (t.hasClass(\"message\") && t.hasClass(\"recipient\")) {\n        if (t.hasClass(\"unread\")) {\n            t.removeClass(\"unread\");\n        } else if ( t.hasClass(\"new\")) {\n            read_thing(elem);\n        }\n    }\n}\n\nfunction hide_thing(elem) {\n    if ($('body').hasClass('comments-page')) {\n        return;\n    }\n\n    var $thing = $(elem).thing();\n\n    if ($thing.is('.comment') && $thing.has('.child:not(:empty)').length) {\n        var deleted = '[' + _.escape(r._('deleted')) + ']';\n        var $entry = $thing.addClass('deleted').find('.entry:first');\n\n        $entry.find('.usertext')\n            .addClass('grayed')\n            .find('.md')\n                .html('<p>' + deleted + '</p>');\n\n        $entry.find('.author')\n            .replaceWith('<em>' + deleted + '</em>')  ;  \n        \n        $entry.find('.userattrs, .score, .buttons')\n            .remove();\n    } else {\n        $thing.fadeOut(function() {\n            $(this).toggleClass('hidden');\n            var thing_id = $(this).thing_id();\n            $(document).trigger('hide_thing_' + thing_id);\n        });\n    }\n}\n\nfunction toggle(elem, callback, cancelback) {\n    if (r.access.isLinkRestricted(elem)) {\n        return false;\n    }\n\n    r.analytics.breadcrumbs.storeLastClick(elem)\n\n    var self = $(elem).parent().addBack().filter(\".option\");\n    var sibling = self.removeClass(\"active\")\n        .siblings().addClass(\"active\").get(0); \n\n    /*\n    var self = $(elem).siblings().addBack();\n    var sibling = self.filter(\":hidden\").debug();\n    self = self.filter(\":visible\").removeClass(\"active\");\n    sibling = sibling.addClass(\"active\").get(0);\n    */\n\n    if(cancelback && !sibling.onclick) {\n        sibling.onclick = function() {\n            return toggle(sibling, cancelback, callback);\n        }\n    }\n    if(callback) callback(elem);\n    return false;\n};\n\nfunction cancelToggleForm(elem, form_class, button_class, on_hide) {\n    /* if there is a toggle button that triggered this, toggle it if\n     * it is not already active.*/\n    if(button_class && $(elem).filter(\"button\").length) {\n        var sel = $(elem).thing().find(button_class)\n            .children(\":visible\").filter(\":first\");\n        toggle(sel);\n    }\n    $(elem).thing().find(form_class)\n        .each(function() {\n                if(on_hide) on_hide($(this));\n                $(this).hide().remove();\n            });\n    return false;\n};\n\n\n/* links */\n\nfunction linkstatus(form) {\n    return r.config.status_msg.submitting;\n};\n\n\nfunction subscribe(reddit_name) {\n    return function() { \n        if (r.config.logged) {\n            if (r.config.cur_site == reddit_name) {\n                $('body').addClass('subscriber');\n            }\n            $.things(reddit_name).find(\".entry\").addClass(\"likes\");\n            $.request(\"subscribe\", {sr: reddit_name, action: \"sub\"});\n            r.analytics.fireUITrackingPixel(\"sub\", reddit_name, {\"has_subd\": r.config.has_subscribed})\n        }\n    };\n};\n\nfunction unsubscribe(reddit_name) {\n    return function() { \n        if (r.config.logged) {\n            if (r.config.cur_site == reddit_name) {\n                $('body').removeClass('subscriber');\n            }\n            $.things(reddit_name).find(\".entry\").removeClass(\"likes\");\n            $.request(\"subscribe\", {sr: reddit_name, action: \"unsub\"});\n            r.analytics.fireUITrackingPixel(\"unsub\", reddit_name)\n        }\n    };\n};\n\nfunction quarantine_optout(subreddit_name) {\n    return function() {\n        if (r.config.logged) {\n            $.request(\"quarantine_optout\", {sr: subreddit_name});\n            $.redirect(\"/\");\n        }\n    };\n};\n\nfunction friend(user_name, container_name, type) {\n    return function() {\n        if (r.config.logged) {\n            encoded = encodeURIComponent(document.referrer);\n            $.request(\"friend?note=\" + encoded,\n                      {name: user_name, container: container_name, type: type});\n        }\n    }\n};\n\nfunction unfriend(user_name, container_name, type) {\n    return function() {\n        $.request(\"unfriend\",\n                  {name: user_name, container: container_name, type: type});\n    }\n};\n\nfunction reject_promo(elem) {\n    $(elem).thing().find(\".rejection-form\").show().find(\"textare\").focus();\n}\n\nfunction cancel_reject_promo(elem) {  \n    $(elem).thing().find(\".rejection-form\").hide();\n}\n\nfunction complete_reject_promo(elem) {\n    var $el = $(elem);\n\n    $el.thing().removeClass(\"accepted\").addClass(\"rejected\")\n        .find(\".reject_promo\").remove();\n\n    if ($el.data('hide-after-seen')) {\n        hide_thing(elem);\n    }\n}\n\n/* Comment generation */\nfunction helpon(elem) {\n    $(elem).parents(\".usertext-edit:first\").children(\".markhelp:first\").show();\n};\nfunction helpoff(elem) {\n    $(elem).parents(\".usertext-edit:first\").children(\".markhelp:first\").hide();\n};\n\nfunction show_all_messages(elem) {\n    var $rootMessage = $(elem).parents(\".message\");\n    var $childMessages = $rootMessage.find(\".message\");\n    var $messages = $rootMessage.add($childMessages);\n    var ids = [];\n\n    _.each($messages, function(message) {\n      var $message = $(message);\n      var $expander = $message.find(\".expand:first\");\n      var isCollapsed = $message.hasClass(\"collapsed\");\n\n      if (isCollapsed) {\n        $message.toggleClass(\"collapsed noncollapsed\");\n        $expander.text(\"[-]\");\n        ids.push($message.thing_id());\n      }\n    });\n\n    if (ids.length) {\n        $.request(\"uncollapse_message\", {\"id\": ids.join(',')});\n    }\n    return false;\n}\n\nfunction hide_all_messages(elem) {\n    var $rootMessage = $(elem).parents(\".message\");\n    var $childMessages = $rootMessage.find(\".message\");\n    var $messages = $rootMessage.add($childMessages);\n    var ids = [];\n\n    _.each($messages, function(message) {\n      var $message = $(message);\n      var $expander = $message.find(\".expand:first\");\n      var isCollapsed = $message.hasClass(\"collapsed\");\n\n      if (!isCollapsed) {\n        $message.toggleClass(\"collapsed noncollapsed\");\n        $expander.text(\"[+]\");\n        ids.push($message.thing_id());\n      }\n    });\n\n    if (ids.length) {\n        $.request(\"collapse_message\", {\"id\": ids.join(',')});\n    }\n    return false;\n}\n\nfunction togglecomment(elem) {\n  var comment = $(elem).thing()\n  var expander = comment.find(\".expand:first\")\n  var isCollapsed = comment.hasClass(\"collapsed\")\n  comment.toggleClass(\"collapsed noncollapsed\")\n\n  if (!isCollapsed) {\n    expander.text(\"[+]\")\n  } else {\n    expander.text(\"[–]\")\n  }\n}\n\nfunction toggleSrQuarantine(elem) {\n  var $toolbox = $(\".quarantine-tool\");\n  var $expander = $toolbox.find(\".expand:first\");\n  var isCollapsed = $toolbox.hasClass(\"collapsed\");\n  $toolbox.toggleClass(\"collapsed noncollapsed\");\n\n  if (!isCollapsed) {\n    $expander.text('[+]');\n  } else {\n    $expander.text('[–]');\n  }\n}\n\nfunction togglemessage(elem) {\n  var message = $(elem).thing()\n  var expander = message.find(\".expand:first\")\n  var isCollapsed = message.hasClass(\"collapsed\")\n  message.toggleClass(\"collapsed noncollapsed\")\n\n  if (!isCollapsed) {\n    expander.text(\"[+]\")\n    $.request(\"collapse_message\", { \"id\": $(message).thing_id() })\n  } else {\n    expander.text(\"[–]\")\n    $.request(\"uncollapse_message\", { \"id\": $(message).thing_id() })\n  }\n}\n\nfunction morechildren(form, link_id, sort, children, depth) {\n    $(form).html(r.config.status_msg.loading)\n        .css(\"color\", \"red\");\n    var id = $(form).parents(\".thing.morechildren:first\").thing_id();\n    var child_params = {\n        link_id: link_id,\n        sort: sort,\n        children: children,\n        depth: depth,\n        id: id,\n    };\n    $.request('morechildren', child_params, undefined, undefined,\n              undefined, true);\n    return false;\n}\n\nfunction moremessages(elem) {\n    $(elem).html(r.config.status_msg.loading).css(\"color\", \"red\");\n    $.request(\"moremessages\", {parent_id: $(elem).thing_id()});\n    return false;\n}\n\n/* stylesheet and CSS stuff */\n\nfunction add_thing_to_cookie(thing, cookie_name) {\n    var id = $(thing).thing_id();\n\n    if(id && id.length) {\n        return add_thing_id_to_cookie(id, cookie_name);\n    }\n}\n\nfunction add_thing_id_to_cookie(id, cookie_name) {\n    var cookie = $.cookie_read(cookie_name);\n    if(!cookie.data) {\n        cookie.data = \"\";\n    }\n\n    /* avoid adding consecutive duplicates */\n    if(cookie.data.substring(0, id.length) == id) {\n        return;\n    }\n\n    cookie.data = id + ',' + cookie.data;\n\n    var fullnames = cookie.data.split(',');\n    if(fullnames.length > 5) {\n        fullnames = $.uniq(fullnames, 5);\n        cookie.data = fullnames.join(',');\n    }\n\n    $.cookie_write(cookie);\n};\n\nfunction clicked_items() {\n    var cookie = $.cookie_read('recentclicks2');\n    if(cookie && cookie.data) {\n        var fullnames = cookie.data.split(\",\");\n        /* don't return empty ones */\n        for(var i=fullnames.length-1; i >= 0; i--) {\n            if(!fullnames[i] || !fullnames[i].length) {\n                fullnames.splice(i,1);\n            }\n        }\n        return fullnames;\n    } else {\n        return [];\n    }\n}\n\nfunction clear_clicked_items() {\n    var cookie = $.cookie_read('recentclicks2');\n    cookie.data = '';\n    $.cookie_write(cookie);\n    $('.gadget').remove();\n}\n\nfunction updateEventHandlers(thing) {\n    /* this function serves as a default callback every time a new\n     * Thing is inserted into the DOM.  It serves to rewrite a Thing's\n     * event handlers depending on context (as in the case of an\n     * organic listing) and to set the click behavior on links. */\n    thing = $(thing);\n    var listing = thing.parent();\n\n    /* click on a title.. */\n    $(thing).filter(\".link\")\n        .find(\"a.title, a.comments\").mousedown(function() {\n            /* set the click cookie. */\n            add_thing_to_cookie(this, \"recentclicks2\");\n        });\n\n    if (listing.filter(\".organic-listing\").length) {\n        thing.find(\".hide-button a, .del-button a.yes, .report-button a.yes\")\n            .each(function() { $(this).get(0).onclick = null });\n        thing.find(\".hide-button a\")\n           .click(function() {\n                   var a = $(this).get(0);\n                   change_state(a, 'hide', \n                                function() { r.spotlight.next() });\n                });\n        thing.find(\".del-button a.yes\")\n            .click(function() {\n                    var a = $(this).get(0);\n                    change_state(a, 'del',\n                                 function() { r.spotlight.next() });\n                });\n        thing.find(\".report-button a.yes\")\n            .click(function() {\n                    var a = $(this).get(0);\n                    change_state(a, 'report', \n                                 function() { r.spotlight.next() });\n                    }); \n    }\n};\n\nfunction last_click() {\n    var fullname = r.analytics.breadcrumbs.lastClickFullname()\n    if (fullname && $('body').hasClass('listing-page')) {\n        $('.last-clicked').removeClass('last-clicked')\n        $('.id-' + fullname).last().addClass('last-clicked')\n    }\n}\n\nfunction login(elem) {\n    return post_user(this, \"login\");\n};\n\nfunction register(elem) {\n    return post_user(this, \"register\");\n};\n\n/***submit stuff***/\nfunction fetch_title() {\n    var url_field = $(\"#url-field\");\n    var error = url_field.find(\".NO_URL\");\n    var status = url_field.find(\".title-status\");\n    var url = $(\"#url\").val();\n    if (url) {\n        if ($('form#newlink textarea[name=\"title\"]').val() &&\n            !confirm(\"This will replace your existing title, proceed?\")) {\n                return\n        }\n        status.show().text(r.config.status_msg.loading);\n        error.hide();\n        $.request(\"fetch_title\", {url: url});\n    }\n    else {\n        status.hide();\n        error.show().text(\"a url is required\");\n    }\n}\n\n\n\nfunction highlight_reddit(item) {\n    $(\"#sr-drop-down\").children('.sr-selected').removeClass('sr-selected');\n    if (item) {\n        $(item).addClass('sr-selected');\n    }\n}\n\nfunction update_dropdown(sr_names) {\n    var drop_down = $(\"#sr-drop-down\");\n    if (!sr_names.length) {\n        drop_down.hide();\n        return;\n    }\n\n    var first_row = drop_down.children(\":first\");\n    first_row.removeClass('sr-selected');\n    drop_down.children().remove();\n\n    $.each(sr_names, function(i) {\n            if (i > 10) return;\n            var name = sr_names[i];\n            var new_row = first_row.clone();\n            new_row.text(name);\n            drop_down.append(new_row);\n        });\n\n\n    var height = $(\"#sr-autocomplete\").outerHeight();\n    drop_down.css('top', height);\n    drop_down.show();\n}\n\n/*** tabbed pane stuff ***/\nfunction select_form_tab(elem, to_show, to_hide) {\n    //change the menu    \n    var link_parent = $(elem).parent();\n    link_parent\n        .addClass('selected')\n        .siblings().removeClass('selected');\n    \n    //swap content and enable/disable form elements\n    var content = link_parent.parent('ul').next('.formtabs-content');\n    content.find(to_show)\n        .show()\n        .find(\":input\").removeAttr(\"disabled\").end();\n    content.find(to_hide)\n        .hide()\n        .find(\":input\").attr(\"disabled\", true);\n}\n\n/******* editting comments *********/\nfunction show_edit_usertext(form) {\n    var edit = form.find(\".usertext-edit\");\n    var body = form.find(\".usertext-body\");\n    var textarea = edit.find('div > textarea');\n\n    //max of the height of the content or the min values from the css.\n    var body_width = Math.max(body.children(\".md\").width(), 500);\n    var body_height = Math.max(body.children(\".md\").height(), 100);\n\n    //we need to show the textbox first so it has dimensions\n    body.hide();\n    edit.show();\n\n    //restore original (?) css width/height. I can't explain why, but\n    //this is important.\n    textarea.css('width', '');\n    textarea.css('height', '');\n\n    //if there would be scroll bars, expand the textarea to the size\n    //of the rendered body text\n    if (textarea.get(0).scrollHeight > textarea.height()) {\n        var new_width = Math.max(body_width - 5, textarea.width());\n        textarea.width(new_width);\n        edit.width(new_width);\n\n        var new_height = Math.max(body_height, textarea.height());\n        textarea.height(new_height);\n    }\n\n    form\n        .find(\".cancel, .save\").show().end()\n        .find(\".help-toggle\").show().end();\n\n    textarea.focus();\n}\n\nfunction hide_edit_usertext(form) {\n    form\n        .find(\".usertext-edit\").hide().end()\n        .find(\".usertext-body\").show().end()\n        .find(\".cancel, .save\").hide().end()\n        .find(\".help-toggle\").hide().end()\n        .find(\".markhelp\").hide().end()\n}\n\nfunction comment_reply_for_elem(elem) {\n    elem = $(elem);\n    var thing = elem.thing();\n    var thing_id = elem.thing_id();\n    //try to find a previous form\n    var form = thing.find(\".child .usertext:first\");\n    if (!form.length || form.parent().thing_id() != thing.thing_id()) {\n        form = $(\".usertext.cloneable:first\").clone(true);\n        elem.new_thing_child(form);\n        form.prop(\"thing_id\").value = thing_id;\n        form.attr(\"id\", \"commentreply_\" + thing_id);\n        form.find(\".error\").hide();\n    }\n    return form;\n}\n\nfunction edit_usertext(elem) {\n    var t = $(elem).thing();\n    t.find(\".edit-usertext:first\").parent(\"li\").addBack().hide();\n    show_edit_usertext(t.find(\".usertext:first\"));\n}\n\nfunction cancel_usertext(elem) {\n    $(window).off('beforeunload');\n    var t = $(elem);\n    t.thing().find(\".edit-usertext:first\").parent(\"li\").addBack().show(); \n    hide_edit_usertext(t.closest(\".usertext\"));\n}\n\nfunction reply(elem) {\n    if (r.access.isLinkRestricted(elem)) {\n        return;\n    }\n\n    var form = comment_reply_for_elem(elem);\n\n    // quote any selected text and put it in the textarea if it's empty\n    // not compatible with IE < 9\n    var textarea = form.find(\"textarea\")\n    if (window.getSelection && textarea.val().length == 0) {\n        // check if the selection is all inside one markdown element\n        var sel = window.getSelection()\n        var focusParentDiv = $(sel.focusNode).parents(\".md\").first()\n        var anchorParentDiv = $(sel.anchorNode).parents(\".md\").first()\n        if (focusParentDiv.length && focusParentDiv.is(anchorParentDiv)) {\n            var selectedText = sel.toString()\n            if (selectedText.length > 0) {\n                selectedText = selectedText.replace(/^/gm, \"> \")\n                textarea.val(selectedText+\"\\n\\n\")\n                textarea.scrollTop(textarea.scrollHeight)\n            }\n        }\n    }\n\n    //show the right buttons\n    show_edit_usertext(form);\n    //re-show the whole form if required\n    form.show();\n    //update the cancel button to call the toggle button's click\n    form.find(\".cancel\").get(0).onclick = function() {\n      $(window).off('beforeunload');\n      form.hide();\n    };\n    $(e.target).thing().find(\".showreplies:visible\").click();\n    return false;\n}\n\nfunction toggle_distinguish_span(elem) {\n  var form = $(elem).parents(\"form\")[0];\n  $(form).children().toggle();\n}\n\nfunction set_distinguish(elem, value) {\n  if (value === \"yes_sticky\") {\n    $(elem).parents('form').first().find('input[name=\"sticky\"]').val('true');\n    value = 'yes';\n  }\n\n  change_state(elem, \"distinguish/\" + value);\n  $(elem).children().toggle();\n}\n\nfunction toggle_clear_suggested_sort(elem) {\n  var form = $(elem).parents(\"form\")[0];\n  $(form).children().toggle();\n}\n\nfunction set_suggested_sort(elem, value) {\n  $(elem).parents('form').first().find('input[name=\"sort\"]').val(value);\n  change_state(elem, \"set_suggested_sort\");\n  $(elem).children().toggle();\n}\n\n\nfunction populate_click_gadget() {\n    /* if we can find the click-gadget, populate it */\n    if($('.click-gadget').length) {\n        var clicked = clicked_items();\n\n        if(clicked && clicked.length) {\n            clicked = $.uniq(clicked, 5);\n            clicked.sort();\n\n            $.request('gadget/click/' + clicked.join(','), undefined, undefined,\n                      undefined, \"json\", true);\n        }\n    }\n}\n\nfunction fetch_parent(elem, parent_permalink, parent_id) {\n    var thing = $(elem).thing();\n    var parent = '';\n\n    $(elem).css(\"color\", \"red\").html(r.config.status_msg.loading);\n\n    $.getJSON(parent_permalink, function(response) {\n      $.each(response, function() {\n        if (this && this.data.children) {\n          $.each(this.data.children, function() {\n            if (this.data.name == parent_id) {\n              parent = this.data.body_html;\n            }\n          });\n        }\n      });\n\n      if (parent) {\n        /* make a parent div for the contents of the fetch */\n        thing.find(\".md\").first()\n          .before('<div class=\"parent rounded\">' + $.unsafe(parent) + '</div>');\n      }\n\n      /* remove the button */\n      $(elem).parent(\"li\").addBack().remove();\n    });\n    return false;\n}\n\nfunction big_mod_action(elem, dir) {\n   if ( ! elem.hasClass(\"pressed\")) {\n      elem.addClass(\"pressed\");\n\n      var thing_id = elem.thing_id();\n\n      d = {\n         id: thing_id\n      };\n\n      elem.siblings(\".status-msg\").hide();\n      if (dir == -1) {\n        d.spam = false;\n        $.request(\"remove\", d, null, true);\n        elem.siblings(\".removed\").show();\n      } else if (dir == -2) {\n        $.request(\"remove\", d, null, true);\n        elem.siblings(\".spammed\").show();\n      } else if (dir == 1) {\n        $.request(\"approve\", d, null, true);\n        elem.siblings(\".approved\").show();\n      }\n   }\n   elem.siblings(\".pretty-button\").removeClass(\"pressed\");\n   return false;\n}\n\nfunction big_mod_toggle(el, press_action, unpress_action) {\n    el.toggleClass('pressed')\n    $.request(el.is('.pressed') ? press_action : unpress_action, {\n        id: el.thing_id()\n    }, null, true)\n    return false\n}\n\n/* The ready method */\n$(function() {\n        $(\"body\").click(close_menus);\n\n        /* set function to be called on thing creation/replacement,\n         * and call it on all things currently rendered in the\n         * page. */\n        $(\"body\").set_thing_init(updateEventHandlers);\n\n        /* Fall back to the old \".gray\" system if placeholder isn't supported\n         * by this browser */\n        if (!('placeholder' in document.createElement('input'))) {\n            $(\"textarea[placeholder], input[placeholder]\")\n                .addClass(\"gray\")\n                .each(function() {\n                    var element = $(this);\n                    var placeholder_text = element.attr('placeholder');\n                    if (element.val() == \"\") {\n                        element.val(placeholder_text);\n                    }\n                });\n        }\n\n        /* Set up gray inputs and textareas to clear on focus */\n        $(\"textarea.gray, input.gray\")\n            .focus( function() {\n                    $(this).attr(\"rows\", 7)\n                        .filter(\".gray\").removeClass(\"gray\").val(\"\")\n                        });\n        /* set cookies to be from this user if there is one */\n        if (r.config.logged) {\n            $.cookie_name_prefix(r.config.logged);\n        }\n        else {\n            //populate_click_gadget();\n        }\n        /* set up the cookie domain */\n        $.default_cookie_domain(r.config.cur_domain.split(':')[0]);\n\n        // When forcing HTTPS, all cookies need the secure flag\n        $.default_cookie_security(r.config.https_forced)\n        \n        /* visually mark the last-clicked entry */\n        last_click();\n        $(window).on('pageshow', function() {\n            last_click()\n        })\n\n        /* search form help expando */\n        /* TODO: use focusin and focusout in jQuery 1.4 */\n        $('#search input[name=\"q\"]').focus(function () {\n            $(\"#searchexpando\").slideDown();\n        });\n\n        // Store the user's choice for restrict_sr\n        $('#search input[name=\"restrict_sr\"]')\n          .change(function() {\n            store.safeSet('search.restrict_sr.checked', this.checked)\n          });\n        $('#searchexpando input[name=\"restrict_sr\"]')\n          .prop(\"checked\", !!store.safeGet('search.restrict_sr.checked'));\n\n        $(\"#search_showmore\").click(function(event) {\n            $(\"#search_showmore\").parent().hide();\n            $(\"#moresearchinfo\").slideDown();\n            event.preventDefault();\n        });\n\n        $(\"#moresearchinfo\")\n            .prepend('<a href=\"#\" id=\"search_hidemore\">[-]</a>')\n\n        $(\"#search_hidemore\").click(function(event) {\n            $(\"#search_showmore\").parent().show();\n            $(\"#moresearchinfo\").slideUp();\n            event.preventDefault();\n        });\n\n        var query = $('#search input[name=\"q\"]').val();\n        $('.search-result-listing')\n          .find('.search-title, .search-link, .search-subreddit-link, .search-result-body')\n          .highlight(query);\n        \n        // add new search page links to the 'recently viewed' links...\n        $(\".search-result-link\").find(\"a.search-title, a.thumbnail\").mousedown(function() {\n            var fullname = $(this).closest('[data-fullname]').data('fullname');\n            if (fullname) {\n                add_thing_id_to_cookie(fullname, \"recentclicks2\");\n            }\n        });\n\n        /* Select shortlink text on click */\n        $(\"#shortlink-text\").click(function() {\n            $(this).select();\n        });\n\n        $(\".sr_style_toggle\").change(function() {\n          $('#sr_style_throbber')\n            .html('<img src=\"' + r.utils.staticURL('throbber.gif') + '\" />')\n            .css(\"display\", \"inline-block\");\n          return post_form($(this), \"set_sr_style_enabled\");\n        });\n\n        $(\".reddit-themes .theme\").click(function() {\n          $(\"div.theme.selected\").removeClass(\"selected\");\n          $(\"input[name='enable_default_themes']\").prop(\"checked\", true);\n          // if other is selected\n          if ($(this).hasClass(\"select-custom-theme\")) {\n            $(\"#other_theme_selector\").prop(\"checked\", true);\n          } else {\n            $(\"input[name='theme_selector'][value='\" + $(this).attr(\"id\") + \"']\")\n              .prop(\"checked\", true);\n          }\n          $(this).addClass(\"selected\");\n        });\n\n        /* ajax ynbutton */\n        function toggleThis() { return toggle(this); }\n        $(\"body\")\n            .delegate(\".ajax-yn-button\", \"submit\",\n                      function() {\n                          var op = $(this).find('input[name=\"_op\"]').val();\n                          post_form(this, op);\n                          return false;\n                      })\n            .delegate(\".ajax-yn-button .togglebutton\", \"click\", toggleThis)\n            .delegate(\".ajax-yn-button .no\", \"click\", toggleThis)\n            .delegate(\".ajax-yn-button .yes\", \"click\",\n                      function() { $(this).closest(\"form\").submit(); })\n            ;\n    });\n"
  },
  {
    "path": "r2/r2/public/static/js/report.js",
    "content": "$(function() {\n  var sessionStorageKey = 'subreddit-rules';\n\n  var templates;\n  var cachedRules;\n\n  function _getTemplate(id) {\n    var elem = document.getElementById(id);\n    return _.template(elem.innerHTML);\n  }\n\n  function init() {\n    var subredditRulesTemplate = _getTemplate('subreddit-rules-report-template');\n    var subredditDefaultTemplate = _getTemplate('subreddit-default-report-template');\n    var redditTemplate = _getTemplate('reddit-report-template');\n    var reasonTemplate = _getTemplate('report-reason-template');\n\n    templates = {\n      subredditRules: function(data) {\n        var rulesStr = data.rules.map(reasonTemplate).join('\\n');\n        var formStr = subredditRulesTemplate(data);\n        var formEl = $.parseHTML(formStr);\n        var rulesEls = $.parseHTML(rulesStr);\n        $(formEl).find('.report-reason-list').prepend(rulesEls);\n        return formEl;\n      },\n\n      subredditDefault: function(data) {\n        var formStr = subredditDefaultTemplate(data);\n        var formEl = $.parseHTML(formStr);\n        return formEl;\n      },\n\n      reddit: function(data) {\n        var formStr = redditTemplate(data);\n        var formEl = $.parseHTML(formStr);\n        return formEl;\n      },\n    };\n\n    try {\n      cachedRules = window.sessionStorage.getItem(sessionStorageKey);\n      cachedRules = JSON.parse(cachedRules);\n    } finally {\n      cachedRules = cachedRules || {};\n    }\n\n    // temporary, after release this will get cleaned up and r.reports will\n    // be moved into this file\n    r.rulesSessionStorageKey = sessionStorageKey;\n    r.hooks.get('new-report-form').call();\n  }\n\n  function renderFromTemplate(data, thingType) {\n    var hasSubreddit = !!data.sr_name;\n    var hasRules = data.rules && data.rules.length > 0;\n    var template;\n    var templateData;\n\n    if (!hasSubreddit) {\n      template = templates.reddit;\n    } else if (hasRules) {\n      template = templates.subredditRules;\n    } else {\n      template = templates.subredditDefault;\n    }\n\n    if (hasRules) {\n      templateData = _.clone(data);\n      templateData.rules = data.rules.filter(function(rule) {\n        return !rule.kind || rule.kind === thingType;\n      })\n    } else {\n      templateData = data;\n    }\n\n    return template(templateData);\n  }\n\n  function showForm($reportForm, form) {\n    $reportForm.empty();\n    $reportForm.append(form);\n    $(form).css('display', 'block');\n  }\n\n  function toggleReportForm() {\n    var $reportForm = $(this).closest('.reportform');\n    $reportForm.toggleClass('active');\n    return false\n  }\n\n  function toggleOther() {\n    var $reportForm = $(this).closest('.reportform');\n    var $submit = $reportForm.find('[type=\"submit\"]');\n    var $reason = $reportForm.find('[name=reason]:checked');\n    var $other = $reportForm.find('[name=\"other_reason\"]');\n    var isOther = $reason.val() === 'other';\n\n    $submit.removeAttr('disabled');\n\n    if (isOther) {\n      $other.removeAttr('disabled').focus();\n    } else {\n      $other.attr('disabled', 'disabled');\n    }\n    return false\n  }\n\n  function getReportAttrs($el) {\n    return {thing: $el.thing_id()}\n  }\n\n  function openReportForm(e) {\n    if (r.access.isLinkRestricted(e.target)) {\n      return;\n    }\n\n    var $thing = $(this).closest('.thing');\n    var srFullname = $thing.data('subreddit-fullname');\n    var thingType = $thing.data('type');\n    var $flatList = $(this).closest('.flat-list');\n    var $reportForm = $flatList.siblings('.reportform').eq(0);\n    $reportForm.toggleClass('active');\n\n    if (!$reportForm.hasClass('active')) {\n      return;\n    }\n\n    // Automatically focus the radio input when this changes.\n    // known bug: doesn't work if user selects the existing value.\n    $reportForm.on('change', 'select[name=site_reason]', function() {\n      $reportForm.find('.site-reason-radio').focus().prop('checked', true);\n    });\n\n    $reportForm.on('click', 'select[name=site_reason]', function() {\n      $reportForm.find('.site-reason-radio').prop('checked', true);\n      toggleOther.call(this);\n    });\n\n    $reportForm.html('<img class=\"flairthrobber\" />')\n    var $imgChild = $reportForm.children(\"img\");\n    $imgChild.attr('src', r.utils.staticURL('throbber.gif'));\n\n    var attrs = getReportAttrs($(this))\n    var useHtmlAPI = !(templates && r.config.feature_new_report_dialog);\n\n    if (useHtmlAPI) {\n      // deprecated; get rendered html from server\n      $.request(\"report_form\", attrs, function(res) {\n        var form = $.parseHTML(res);\n        showForm($reportForm, form);\n      }, true, \"html\", true);\n    } else if (!srFullname) {\n      // if no subreddit, render the reddit form (never needs to hit API)\n      var formData = { fullname: attrs.thing };\n      var form = renderFromTemplate(formData, thingType);\n      showForm($reportForm, form);\n    } else if (srFullname in cachedRules) {\n      // render from cached if available (only needs to hit API once per subreddit)\n      var formData = cachedRules[srFullname];\n      formData.fullname = attrs.thing;\n      var form = renderFromTemplate(formData, thingType);\n      showForm($reportForm, form);\n    } else {\n      // fetch from the API and cache for later\n      attrs.api_type = 'json';\n      $.request(\"report_form\", attrs, function(res) {\n        var data = res.json.data;\n        cachedRules[srFullname] = data;\n        data.fullname = attrs.thing;\n\n        try {\n          var rulesJSON = JSON.stringify(cachedRules);\n          window.sessionStorage.setItem(sessionStorageKey, rulesJSON);\n        } finally {\n          var form = renderFromTemplate(data, thingType);\n          showForm($reportForm, form);\n        }\n      }, true, 'json', true);  \n    }\n\n    return false;\n  }\n\n  r.hooks.get('setup').register(function() {\n    if (r.config.feature_new_report_dialog) {\n      try {\n        init();\n      } catch (err) {\n        // only meant to catch transient errors. falls back to the HTML api\n      }\n    }\n\n    // temp fix for broken subreddit-report-form\n    $('div.content').on('submit', '.subreddit-report-form', function(e) {\n      var $actionForm = $(e.target);\n      return post_pseudo_form($actionForm, 'report');\n    });\n\n    $(\"div.content\").on(\"click\", \".tagline .reportbtn, .thing .reportbtn\", openReportForm);\n    $(\"div.content\").on(\"click\", \".btn.report-cancel\", toggleReportForm);\n    $(\"div.content\").on(\"change\", \"input[name='reason']\", toggleOther);\n  });\n});\n"
  },
  {
    "path": "r2/r2/public/static/js/safe-store.js",
    "content": "!function(r, store, undefined) {\n  store.safeGet = function(key, errorValue) {\n      if (store.disabled) {\n          return errorValue\n      }\n\n      // errorValue defaults to undefined, equivalent to the key being unset.\n      try {\n          return store.get(key)\n      } catch (err) {\n          r.sendError('Unable to read storage key \"%(key)s\" (%(err)s)'.format({\n              key: key,\n              err: err\n          }))\n          // TODO: reset value to errorValue?\n          return errorValue\n      }\n  }\n\n  store.safeSet = function(key, val) {\n      if (store.disabled) {\n          return false\n      }\n\n      // swallow exceptions upon storage set for non-trivial operations. returns\n      // a boolean value indicating success.\n      try {\n          store.set(key, val)\n          return true\n      } catch (err) {\n          r.warn('Unable to set storage key \"%(key)s\" (%(err)s)'.format({\n              key: key,\n              err: err\n          }))\n          return false\n      }\n  }\n}(r, store);\n"
  },
  {
    "path": "r2/r2/public/static/js/saved.js",
    "content": "r.saved = {}\n\nr.saved.SaveCategories = Backbone.Collection.extend({\n    model: Backbone.Model.extend({idAttribute: 'category'}),\n\n    url: '/api/saved_categories.json',\n\n    fetchOnce: function() {\n        if (!this._fetched) {\n            this._fetched = this.fetch()\n        }\n        return this._fetched\n    },\n\n    comparator: function(item) {\n        return item.get('category')\n    },\n\n    parse: function(response) {\n        return response.categories\n    }\n})\n\nr.saved.SaveDialog = r.ui.Bubble.extend({\n    tagName: 'form',\n    className: 'hover-bubble anchor-left save-selector',\n    confirmTemplate: _.template('<label for=\"savedcategory\"><%- label %></label><span class=\"throbber\"></span><select><option value=\"\"><%- placeholder %></option></select><input maxlength=\"20\" class=\"savedcategory\"  name=\"savedcategory\" placeholder=\"<%- textplaceholder %>\"><input type=\"submit\" value=\"<%- save %>\"><div class=\"error\"></div>'),\n\n    events: {\n        \"click\": \"clicked\",\n        \"submit\": \"save\",\n        \"mouseout\": \"mouseout\",\n        \"mouseover\": \"cancelTimeout\",\n        \"change select\": \"change\"\n    },\n\n    mouseout: function() {\n        if (!this.$el.find('select, .savedcategory').is(\":focus\")) {\n            this.queueHide()\n        }\n    },\n\n    clicked: function(e) {\n        e.stopPropagation()\n    },\n\n    initialize: function(options) {\n        this.options = options\n        this.options.trackHover = false\n        r.ui.Bubble.prototype.initialize.apply(this)\n        r.saved.categories.fetchOnce().then(_.bind(this.show, this))\n        $('body').on('click.savedialog', _.bind(this.hideNow, this))\n    },\n\n    hideNow: function() {\n        r.ui.Bubble.prototype.hideNow.apply(this)\n        $('body').off('click.savedialog')\n        this.remove()\n    },\n\n    error: function() {\n        this.$el.find('select, .savedcategory').attr('disabled', false)\n        this.$el.removeClass('working')\n        this.$el.find('.error').text(r._('Invalid category name'))\n    },\n\n    change: function(e) {\n        var input = this.$el.find('.savedcategory')\n        var selected = this.$el.find('option:selected').val()\n        input.val(selected).focus()\n    },\n\n    success: function() {\n        var $category = this.$parent.parents('.thing').find('.save-category')\n        if ($category.length && this.category) {\n            $category.text('category: ' + this.category)\n            $category.attr('href', '/user/' + r.config.logged + '/saved/' + this.category)\n            $category.show()\n        } else {\n            $category.hide()\n        }\n        r.saved.SaveButton.setSaved(this.$parent)\n        if (this.category) {\n            r.saved.categories.add({category: this.category})\n            r.saved.categories.sort()\n        }\n        this.hide()\n    },\n\n    save: function(e) {\n        e.preventDefault()\n        this.category = this.$el.find('.savedcategory').val()\n        this.$el.find('select, .savedcategory').attr('disabled', true)\n        if (!this.category) {\n            return this.success()\n        }\n        this.$el.addClass('working')\n        r.ajax({\n            type: 'POST',\n            url: '/api/save',\n            data: {'id': this.$parent.thing_id(), 'category': this.category},\n            success: this.success,\n            error: this.error,\n            context: this\n        })\n    },\n\n    addCategory: function(category) {\n        var value = category.get('category')\n        this.$el.find('select').append($('<option>').val(value).text(value))\n    },\n\n    show: function() {\n        r.ui.Bubble.prototype.show.apply(this)\n        this.$el.find('.savedcategory').focus()\n    },\n\n    render: function() {\n        this.$el.html(this.confirmTemplate({\n            label: r._('save category'),\n            placeholder: r._('no category'),\n            save: r._('save'),\n            textplaceholder: r._('new category')\n        }))\n        r.saved.categories.each(this.addCategory, this)\n        this.$el.find('select').first().prop('selected', true)\n    }\n})\n\nr.saved.SaveButton = {\n    request: function($el, type, callback) {\n        r.ajax({\n            type: 'POST',\n            url:  '/api/' + type,\n            data: {'id': $el.thing_id()},\n            success: _.bind(callback, this, $el)\n        })\n    },\n\n    toggleSaved: function($el) {\n        this.isSaved($el) ? this.unsave($el) : this.save($el)\n    },\n\n    unsave: function($el) {\n        this.request($el, 'unsave', this.setUnsaved)\n    },\n\n    save: function($el) {\n        this.request($el, 'save', this.setSaved)\n        if (r.config.gold) {\n            new r.saved.SaveDialog({parent: $el, group: r.saved.SaveButton})\n        }\n    },\n\n    isSaved: function($el) {\n        return $el.thing().hasClass('saved')\n    },\n\n    setUnsaved: function($el) {\n        var $category = $el.parents('.thing').find('.save-category').hide()\n        $el.text(r._('save'))\n        $el.thing().removeClass('saved')\n    },\n\n    setSaved: function($el) {\n        $el.text(r._('unsave'))\n        $el.thing().addClass('saved')\n    }\n}\n\nr.saved.categories = new r.saved.SaveCategories()\n\nr.saved.init = function() {\n    $('body').on('click', '.save-button a, a.save-button', function(e) {\n        e.stopPropagation()\n        e.preventDefault()\n        r.saved.SaveButton.toggleSaved($(this))\n    })\n}\n"
  },
  {
    "path": "r2/r2/public/static/js/scrollupdater.js",
    "content": "!function(r, $){\n    r.ScrollUpdater = Backbone.View.extend({\n        selector: null,\n        startUpdate: function () {},\n        update: function ($el) {},\n        endUpdate: function ($els) {},\n\n        start: function() {\n            this._resetScrollState()\n            this._listen()\n            return this\n        },\n\n        restart: function() {\n            this._resetScrollState()\n            return this\n        },\n\n        _resetScrollState: function() {\n            this._elements = this.$el.find(this.selector)\n            _.sortBy(this._elements, function(el) {\n                return $(el).offset().top\n            })\n\n            this._curIndex = 0\n            this._lastScroll = null\n            this._toUpdate = []\n            this._totalTime = 0\n\n            // Trigger once now to detect any elements currently in view.\n            _.defer($.proxy(this, '_updateThings'))\n        },\n\n        _listen: function() {\n            var throttledUpdate = _.throttle($.proxy(this, '_updateThings'), 20)\n            $(window).on('scroll', throttledUpdate)\n        },\n\n        _updateThings: function(ev) {\n            if (!this._elements.length) {\n                return\n            }\n\n            var startTime = new Date()\n\n            // update the current page of elements and half a page in the\n            // direction of motion\n            var $win = $(window),\n                winHeight = $win.height(),\n                scrollTop = $win.scrollTop(),\n                ceiling = scrollTop,\n                floor = scrollTop + winHeight\n\n            if (scrollTop < this._lastScroll) {\n                ceiling = Math.max(ceiling - Math.floor(winHeight / 2), 0)\n            } else {\n                floor += Math.ceil(winHeight / 2)\n            }\n\n            // scan to ceiling to set the cursor\n            var idx = this._curIndex,\n                $cur = $(this._elements[idx])\n\n            if ($cur.offset().top < ceiling) {\n                // forward\n                while (idx < this._elements.length-1 && $cur.offset().top < ceiling) {\n                    $cur = $(this._elements[idx])\n                    idx++\n                }\n            } else {\n                // backward\n                while (idx > 0 && $cur.offset().top > ceiling) {\n                    $cur = $(this._elements[idx])\n                    idx--\n                }\n            }\n\n            // update forward to floor\n            var count = 0\n            do {\n                $cur = $(this._elements[idx])\n                this._toUpdate.push($cur)\n                idx++\n                count++\n            } while (idx <= this._elements.length-1 && $cur.offset().top <= floor)\n\n            this._curIndex = idx - 1\n            this._lastScroll = scrollTop\n\n            var endTime = new Date()\n            this._totalTime += endTime - startTime\n\n            this._doUpdates()\n        },\n\n        cutoff: 1000 / 60,\n        _doUpdates: function() {\n            this.startUpdate()\n\n            var startTime = new Date(),\n                endTime = startTime,\n                count = 0,\n                els = []\n\n            while (endTime - startTime < this.cutoff) {\n                if (!this._toUpdate.length) {\n                    break\n                }\n                var $el = this._toUpdate.shift()\n                els.push($el)\n                this.update($el)\n                count++\n                endTime = new Date()\n            }\n\n            this._totalTime += endTime - startTime\n\n            if (this._toUpdate.length) {\n                _.defer($.proxy(this, '_doUpdates'))\n            }\n\n            this.endUpdate($(els))\n        }\n    })\n}(r, jQuery)\n"
  },
  {
    "path": "r2/r2/public/static/js/setup.js",
    "content": "r.setup = function(config) {\n    r.config = config\n    r.config.currentOrigin = location.protocol+'//'+location.host\n\n    r.hooks.get('setup').call();\n};\n"
  },
  {
    "path": "r2/r2/public/static/js/sponsored.js",
    "content": "!function(r) {\n\nvar UseDefaultClassName = (function() {\n  var camelCaseRegex = /([a-z])([A-Z])/g;\n  function hyphenate(match, $1, $2) {\n    return $1 + '-' + $2;\n  }\n\n  return {\n    /**\n     * derive a className automatically from the displayName property\n     * e.g. MyDisplayName => my-display-name\n     * if a className state or prop is passed in, add that\n     * if values are passed into the function, add those in as well\n     * @param {string} arguments optionally pass in any number of\n     *                           classNames to to add to the list\n     * @return {string} css class name\n     */\n    getClassName: function(/* classNames */) {\n      var classNames = [];\n\n      if (this.constructor.displayName) {\n        classNames.push(\n          this.constructor.displayName.replace(camelCaseRegex, hyphenate)\n                                      .toLowerCase()\n          );\n      }\n\n      if (this.state && this.state.className) {\n        classNames.push(this.state.className);\n      }\n      else if (this.props.className) {\n        classNames.push(this.props.className);\n      }\n\n      if (arguments.length) {\n        classNames.push.apply(classNames, arguments);\n      }\n\n      return classNames.join(' ');\n    }\n  };\n})();\n\n\nvar CampaignFormattedProps = {\n  componentWillMount: function() {\n    this.formattedProps = this.getFormattedProps(_.clone(this.props), this.props);\n  },\n\n  componentWillUpdate: function(nextProps) {\n    this.formattedProps = this.getFormattedProps(_.clone(nextProps), nextProps);\n  },\n\n  getFormattedProps: function(formattedProps, props) {\n    if (props.impressions) {\n      formattedProps.impressions = r.utils.prettyNumber(props.impressions);\n    }\n    if (props.totalBudgetDollars === null) {\n      formattedProps.totalBudgetDollars = 'N/A';\n    } else if (_.isNaN(props.budget)) {\n      formattedProps.totalBudgetDollars = 0;\n    } else if (props.totalBudgetDollars) {\n      formattedProps.totalBudgetDollars = props.totalBudgetDollars.toFixed(2);\n    }\n    return formattedProps;\n  },\n};\n\n\nvar CampaignButton = React.createClass({\n  displayName: 'CampaignButton',\n\n  mixins: [UseDefaultClassName],\n\n  getDefaultProps: function() {\n    return {\n      isNew: true,\n    };\n  },\n\n  render: function() {\n    if (this.props.isNew) {\n      return React.DOM.div({ className: 'button-group' },\n        React.DOM.button(\n          { ref: 'keepOpen', className: 'campaign-button', onClick: this.handleClick },\n          r._('create')\n        ),\n        React.DOM.button(\n          { className: this.getClassName(), onClick: this.handleClick },\n          r._('+ close')\n        )\n      );\n    }\n    return React.DOM.button(\n      { className: this.getClassName(), onClick: this.handleClick },\n      this.props.isNew ? r._('create') : r._('save')\n    );\n  },\n\n  handleClick: function(e) {\n    var close = true;\n    if (this.refs.keepOpen) {\n      close = !(e.target === this.refs.keepOpen.getDOMNode());\n    }\n    if (typeof this.props.onClick === 'function') {\n      this.props.onClick(close);\n    }\n  },\n});\n\n\nvar InfoText = React.createClass({\n  displayName: 'InfoText',\n\n  mixins: [UseDefaultClassName, CampaignFormattedProps],\n\n  render: function() {\n    var text = Array.isArray(this.props.children)\n             ? this.props.children.join('\\n')\n             : this.props.children;\n    return React.DOM.span({ className: this.getClassName() },\n      text.format(this.formattedProps)\n    );\n  },\n\n});\n\nvar CampaignOptionTable = React.createClass({\n  displayName: 'CampaignOptionTable',\n\n  mixins: [UseDefaultClassName],\n\n  render: function() {\n    return React.DOM.table({ className: this.getClassName() },\n      React.DOM.tbody(null, this.props.children)\n    );\n  }\n})\n\nvar CampaignOption = React.createClass({\n  displayName: 'CampaignOption',\n\n  mixins: [UseDefaultClassName, CampaignFormattedProps],\n\n  getDefaultProps: function() {\n    return {\n      primary: false,\n      start: '',\n      end: '',\n      bid: '',\n      impressions: '',\n      isNew: true,\n      costBasis: '',\n      totalBudgetDollars: '',\n      costBasis: '',\n      bidDollars: '',\n    };\n  },\n\n  render: function() {\n    var customText;\n    if (r.sponsored.isAuction) {\n      customText = '$' + parseFloat(this.props.bidDollars).toFixed(2) + ' ' + this.props.costBasis;\n    } else {\n      customText = this.formattedProps.impressions + ' impressions';\n    }\n    return React.DOM.tr({ className: this.getClassName() },\n      React.DOM.td({ className: 'date start-date' }, this.props.start),\n      React.DOM.td({ className: 'date end-date' }, this.props.end),\n      React.DOM.td({ className: 'total-budget' }, '$', this.formattedProps.totalBudgetDollars,\n        ' total'),\n      React.DOM.td({}, customText),\n      React.DOM.td({ className: 'buttons' },\n        CampaignButton({\n          className: this.props.primary ? 'primary-button' : '',\n          isNew: this.props.isNew,\n          onClick: this.handleClick,\n        })\n      )\n    );\n  },\n\n  handleClick: function(close) {\n    var $startdate = $('#startdate');\n    var $enddate = $('#enddate');\n    var $totalBudgetDollars = $('#total_budget_dollars');\n    var $bidDollars = $('#bid_dollars');\n    var userStartdate = $startdate.val();\n    var userEnddate = $enddate.val();\n    var userTotalBudgetDollars = $totalBudgetDollars.val();\n    var userBidDollars = $bidDollars.val() || 0.;\n    $('#startdate').val(this.props.start);\n    $('#enddate').val(this.props.end);\n    $('#total_budget_dollars').val(this.props.totalBudgetDollars);\n    $('#bid_dollars').val(this.props.bidDollars);\n    setTimeout(function(){\n      send_campaign(close);\n      // hack, needed because post_pseudo_form hides any element in the form\n      // with an `error` class, which might be one of our InfoText components\n      // but we want react to manage that\n      $('.campaign-creator .info-text').removeAttr('style');\n      // reset the form with the user's original values\n      $startdate.val(userStartdate);\n      $enddate.val(userEnddate);\n      $totalBudgetDollars.val(userTotalBudgetDollars);\n      $bidDollars.val(userBidDollars);\n    }, 0);\n  },\n});\n\n\nvar CampaignSet = React.createClass({\n  displayName: 'CampaignSet',\n\n  mixins: [UseDefaultClassName],\n\n  render: function() {\n    return React.DOM.div({ className: this.getClassName() },\n      this.props.children\n    );\n  },\n});\n\nvar CampaignCreator = React.createClass({\n  displayName: 'CampaignCreator',\n\n  mixins: [UseDefaultClassName],\n\n  getDefaultProps: function() {\n    return {\n      totalBudgetDollars: 0,\n      targetName: '',\n      cpm: 0,\n      minBidDollars: 0,\n      maxBidDollars: 0,\n      maxBudgetDollars: 0,\n      minBudgetDollars: 0,\n      dates: [],\n      inventory: [],\n      requested: 0,\n      override: false,\n      isNew: true,\n    };\n  },\n\n  getInitialState: function() {\n    var totalAvailable = this.getAvailable(this.props);\n    var available = totalAvailable;\n    if (this.props.maxBudgetDollars) {\n      available = Math.min(available, this.getImpressions(this.props.maxBudgetDollars));\n    }\n    return {\n      totalAvailable: totalAvailable,\n      available: available,\n      maxTime: 0,\n    };\n  },\n\n  componentWillMount: function() {\n    this.setState({\n      maxTime: dateFromInput('#date-start-max').getTime(),\n    });\n  },\n\n  componentWillReceiveProps: function(nextProps) {\n    var totalAvailable = this.getAvailable(nextProps);\n    var available = totalAvailable;\n    if (this.props.maxBudgetDollars) {\n      available = Math.min(available, this.getImpressions(this.props.maxBudgetDollars));\n    }\n    this.setState({\n      totalAvailable: totalAvailable,\n      available: available,\n    });\n  },\n\n  getAvailable: function(props) {\n    if (props.override) {\n      return _.reduce(props.inventory, sum, 0);\n    }\n    else {\n      return _.min(props.inventory) * props.dates.length;\n    }\n  },\n\n  render: function() {\n    return React.DOM.div({\n        className: this.getClassName(),\n      },\n      this.getCampaignSets()\n    );\n  },\n\n  getCampaignSets: function() {\n    if (r.sponsored.isAuction) {\n      var auction = this.getAuctionOption(),\n          cssClass = null,\n          message = r._('Please confirm the details of your campaign');\n      if (auction.totalBudgetDollars < this.props.minBudgetDollars) {\n        cssClass = {className: 'error', minBudgetDollars: auction.minBudgetDollars};\n        message = r._('your budget must be at least $%(minBudgetDollars)s');\n      } else if (auction.totalBudgetDollars > this.props.maxBudgetDollars &&\n                 this.props.maxBudgetDollars > 0) {\n        cssClass = {className: 'error', maxBudgetDollars: auction.maxBudgetDollars};\n        message = r._('your budget must not exceed $%(maxBudgetDollars)s');\n      } else {\n        if (r.sponsored.userIsSponsor) {\n          auction.primary = true;\n        } else if (auction.maxBidDollars < auction.minBidDollars) {\n          formattedMinBidDollars = parseFloat(auction.minBidDollars).toFixed(2);\n          cssClass = {className: 'error', minBid: formattedMinBidDollars};\n          message = r._('Your campaign must be capable of claiming at least \\\n                         1,000 impressions per day. Please adjust your bid, \\\n                         budget, or schedule in order to enable this.');\n        } else if (auction.bidDollars < auction.minBidDollars) {\n          formattedMinBidDollars = parseFloat(auction.minBidDollars).toFixed(2);\n          cssClass = {className: 'error', minBid: formattedMinBidDollars};\n          message = r._('your bid must be at least $%(minBid)s');\n        } else if (auction.bidDollars > auction.maxBidDollars) {\n          formattedMaxBidDollars = parseFloat(auction.maxBidDollars).toFixed(2);\n          cssClass = {className: 'error', maxBid: formattedMaxBidDollars};\n          message = r._('your bid must not exceed $%(maxBid)s');\n        } else {\n          auction.primary = true;\n        }\n      }\n      return [CampaignSet(null,\n          InfoText(cssClass, message),\n          CampaignOptionTable(null, CampaignOption(auction))\n        ),\n      ];\n    } else {\n      var requested = this.getRequestedOption();\n      requested.primary = true;\n      var maximized = this.getMaximizedOption();\n\n      if (this.props.override) {\n        if (requested.impressions <= this.state.available) {\n          return [CampaignSet(null,\n              InfoText(null, r._('the campaign you requested is available!')),\n              CampaignOptionTable(null, CampaignOption(requested))\n            ),\n            InfoText(maximized,\n                r._('the maximum budget available is $%(totalBudgetDollars)s (%(impressions)s impressions)')\n            )\n          ];\n        }\n        else {\n          return CampaignSet(null,\n            InfoText({\n                className: 'error',\n                available: this.state.available,\n                target: this.props.targetName\n              },\n              r._('we expect to only have %(available)s impressions on %(target)s. ' +\n                   'we may not fully deliver.')\n            ),\n            CampaignOptionTable(null, CampaignOption(requested))\n          );\n        }\n      }\n      else if (requested.totalBudgetDollars >= this.props.minBudgetDollars &&\n               requested.impressions <= this.state.available) {\n        var result = CampaignSet(null,\n          InfoText(null, r._('the campaign you requested is available!')),\n          CampaignOptionTable(null, CampaignOption(requested))\n        );\n        if (maximized.totalBudgetDollars > requested.totalBudgetDollars &&\n            requested.totalBudgetDollars * 1.2 >= maximized.totalBudgetDollars &&\n            this.state.available === this.state.totalAvailable) {\n          var difference = maximized.totalBudgetDollars - requested.totalBudgetDollars;\n          result = [result, CampaignSet(null,\n            InfoText({ difference: difference.toFixed(2) },\n              r._('want to maximize your campaign? for only $%(difference)s more ' +\n                   'you can buy all available inventory for your selected dates!')\n            ),\n            CampaignOptionTable(null, CampaignOption(maximized))\n          )];\n        }\n        else {\n          result = [result, InfoText(maximized,\n            r._('the maximum budget available is $%(totalBudgetDollars)s (%(impressions)s impressions)')\n          )];\n        }\n        return result;\n      }\n      else if (requested.totalBudgetDollars < this.props.minBudgetDollars) {\n        var minimal = this.getMinimizedOption();\n        if (minimal.impressions <= this.state.available) {\n          if (r.sponsored.userIsSponsor) {\n            return CampaignSet(null,\n              InfoText(null, r._('the campaign you requested is available!')),\n              CampaignOptionTable(null, CampaignOption(requested))\n            );\n          } else {\n            return CampaignSet(null,\n              InfoText({ className: 'error' },\n                r._('the campaign you requested is too small! this campaign is available:')\n              ),\n              CampaignOptionTable(null, CampaignOption(minimal))\n            );\n          }\n        }\n        else {\n          return InfoText({ className: 'error' },\n            r._('the campaign you requested is too small!')\n          );\n        }\n      }\n      else if (requested.impressions > this.state.available &&\n               this.state.totalAvailable > this.state.available &&\n               maximized.totalBudgetDollars > this.props.minBudgetDollars) {\n        return CampaignSet(null,\n          InfoText(null,\n            r._('the campaign you requested is too big! the largest campaign ' +\n                 'available is:')\n          ),\n          CampaignOptionTable(null, CampaignOption(maximized))\n        );\n      }\n      else if (requested.impressions > this.state.available) {\n\n        var options = [];\n        if (maximized.totalBudgetDollars >= this.props.minBudgetDollars) {\n          options.push(CampaignOption(maximized));\n        }\n        var reduced = this.getReducedWindowOption();\n        if (reduced && reduced.totalBudgetDollars >= this.props.minBudgetDollars) {\n          if (reduced.impressions > requested.impressions) {\n            reduced.impressions = requested.impressions;\n            reduced.totalBudgetDollars = requested.totalBudgetDollars;\n          }\n          options.push(CampaignOption(reduced));\n        }\n        if (options.length) {\n          return CampaignSet(null,\n            InfoText({\n                className: 'error',\n                target: this.props.targetName,\n              },\n              r._('we have insufficient available inventory targeting %(target)s to fulfill ' +\n                   'your requested dates. the following campaigns are available:')\n            ),\n            CampaignOptionTable(null, options)\n          );\n        }\n        else {\n          r.analytics.fireFunnelEvent('ads', 'inventory-error');\n\n          return InfoText({\n              className: 'error',\n              target: this.props.targetName\n            },\n            r._('inventory for %(target)s is sold out for your requested dates. ' +\n                 'please try a different target or different dates.')\n          );\n        }\n      }\n    }\n\n    return null;\n  },\n\n  formatDate: function(date) {\n    return $.datepicker.formatDate('mm/dd/yy', date);\n  },\n\n  getBudget: function(impressions, requestedBudget) {\n    if (this.getImpressions(requestedBudget) === impressions) {\n      return requestedBudget;\n    } else {\n      return Math.floor((impressions / 1000) * this.props.cpm) / 100;\n    }\n  },\n\n  getImpressions: function(bid) {\n    return Math.floor(bid / this.props.cpm * 1000 * 100);\n  },\n\n  getOptionDates: function(startDate, duration) {\n    var endDate = new Date();\n    endDate.setTime(startDate.getTime());\n    endDate.setDate(startDate.getDate() + duration);\n    return {\n      start: this.formatDate(startDate),\n      end: this.formatDate(endDate),\n    }\n  },\n\n  getFixedCPMOptionData: function(startDate, duration, impressions, requestedBudget) {\n    var dates = this.getOptionDates(startDate, duration);\n    return {\n      start: dates.start,\n      end: dates.end,\n      totalBudgetDollars: this.getBudget(impressions, requestedBudget),\n      impressions: Math.floor(impressions),\n      isNew: this.props.isNew,\n    };\n  },\n\n  getAuctionOption: function() {\n    var dates = this.getOptionDates(this.props.dates[0], this.props.dates.length);\n    return {\n      start: dates.start,\n      end: dates.end,\n      totalBudgetDollars: this.props.totalBudgetDollars,\n      costBasis: this.props.costBasis,\n      bidDollars: this.props.bidDollars,\n      isNew: this.props.isNew,\n      minBidDollars: this.props.minBidDollars,\n      maxBidDollars: this.props.maxBidDollars,\n      minBudgetDollars: this.props.minBudgetDollars,\n      maxBudgetDollars: this.props.maxBudgetDollars\n    };\n  },\n\n  getRequestedOption: function() {\n    return this.getFixedCPMOptionData(\n      this.props.dates[0],\n      this.props.dates.length,\n      this.props.requested,\n      this.props.totalBudgetDollars\n    );\n  },\n\n  getMaximizedOption: function() {\n    return this.getFixedCPMOptionData(\n      this.props.dates[0],\n      this.props.dates.length,\n      this.state.available,\n      this.props.totalBudgetDollars\n    );\n  },\n\n  getMinimizedOption: function() {\n    return this.getFixedCPMOptionData(\n      this.props.dates[0],\n      this.props.dates.length,\n      this.getImpressions(this.props.minBudgetDollars),\n      this.props.minBudgetDollars\n    );\n  },\n\n  getReducedWindowOption: function() {\n    var days = (1000 * 60 * 60 * 24);\n    var maxOffset = (this.state.maxTime - this.props.dates[0].getTime()) / days | 0;\n    var res =  r.sponsored.getMaximumRequest(\n      this.props.inventory,\n      this.getImpressions(this.props.minBudgetDollars),\n      this.props.requested,\n      maxOffset\n    );\n    if (res && res.days.length < this.props.dates.length) {\n      return this.getFixedCPMOptionData(\n        this.props.dates[res.offset],\n        res.days.length,\n        res.maxRequest,\n        this.props.totalBudgetDollars\n      );\n    }\n    else {\n      return null;\n    }\n  },\n});\n\n\nvar exports = r.sponsored = {\n    set_form_render_fnc: function(render) {\n        this.render = render;\n    },\n\n    render: function() {},\n\n    init: function() {\n        $(\"#sr-autocomplete\").on(\"sr-changed blur\", function() {\n            r.sponsored.render()\n        })\n        this.targetValid = true;\n        this.bidValid = true;\n        this.inventory = {}\n        this.campaignListColumns = $('.existing-campaigns thead th').length\n        $(\"input[name='media_url_type']\").on(\"change\", this.mediaInputChange)\n\n        this.initUploads();\n    },\n\n    initUploads: function() {\n      $('.c-image-upload')\n        .imageUpload()\n        .on('failed.imageUpload', function(e, data) {\n          alert(data.message);\n        });\n    },\n\n    setup: function(inventory_by_sr, priceDict, isEmpty, userIsSponsor, forceAuction) {\n        if (forceAuction) {\n            this.isAuction = true;\n        }\n        this.inventory = inventory_by_sr\n        this.priceDict = priceDict\n\n        var $platformField = $('.platform-field');\n        this.$platformInputs = $platformField.find('input[name=platform]');\n        this.$mobileOSInputs = $platformField.find('.mobile-os-group input');\n        this.$iOSDeviceInputs = $platformField.find('.ios-device input');\n        this.$iOSMinSelect = $platformField.find('#ios_min');\n        this.$iOSMaxSelect = $platformField.find('#ios_max');\n        this.$androidDeviceInputs = $platformField.find('.android-device input');\n        this.$androidMinSelect = $platformField.find('#android_min');\n        this.$androidMaxSelect = $platformField.find('#android_max');\n        this.$deviceAndVersionInputs = $platformField.find('input[name=\"os_versions\"]');\n\n        var render = this.render.bind(this);\n\n        $('.platform-field input, .platform-field select').on('change', render);\n\n        if (isEmpty) {\n            this.render();\n            init_startdate()\n            init_enddate()\n            $(\"#campaign\").find(\"button[name=create]\").show().end()\n                .find(\"button[name=save]\").hide().end()\n        }\n        this.userIsSponsor = userIsSponsor\n    },\n\n    setupAuctionFields: function($form, targeting, timing) {\n        if (this.isAuction) {\n            $('.auction-field').show();\n            $('.fixed-cpm-field').hide();\n            $('.priority-field').hide();\n            $('#is_auction_true').prop('checked', true);\n\n            this.setup_auction($form, targeting, timing);\n            this.check_bid_dollars($form);\n        } else {\n            $('.auction-field').hide();\n            $('.fixed-cpm-field').show();\n            $('.priority-field').show();\n            $('#is_auction_false').prop('checked', true);\n        }\n    },\n\n    setupLiveEditing: function(isLive) {\n        var $budgetChangeWarning = $('.budget-unchangeable-warning');\n        var $targetChangeWarning = $('.target-change-warning');\n        if (isLive && !this.userIsSponsor) {\n            $budgetChangeWarning.show();\n            $targetChangeWarning.show();\n            $('#total_budget_dollars').prop('disabled', true);\n            $('#startdate').prop('disabled', true);\n        } else {\n            $budgetChangeWarning.hide();\n            $targetChangeWarning.hide();\n            $('#total_budget_dollars').removeAttr('disabled');\n            $('#startdate').removeAttr('disabled');\n        }\n    },\n\n    setup_collection_selector: function() {\n        var $collectionSelector = $('.collection-selector');\n        var $collectionList = $('.form-group-list');\n        var $collections = $collectionList.find('.form-group .label-group');\n        var collectionCount = $collections.length;\n        var collectionHeight = $collections.eq(0).outerHeight();\n        var $subredditList = $('.collection-subreddit-list ul');\n        var $collectionLabel = $('.collection-subreddit-list .collection-label');\n        var $frontpageLabel = $('.collection-subreddit-list .frontpage-label');\n\n        var subredditNameTemplate = _.template('<% _.each(sr_names, function(name) { %>'\n            + ' <li><%= name %></li> <% }); %>');\n        var render_subreddit_list = _.bind(function(collection) {\n            if (collection === 'none' ||\n                    typeof this.collectionsByName[collection] === 'undefined') {\n                return '';\n            }\n            else {\n                return subredditNameTemplate(this.collectionsByName[collection]);\n            }\n        }, this);\n\n        var collapse = _.bind(function() {\n            this.collapse_collection_selector();\n            this.render();\n        }, this);\n\n        this.collapse_collection_selector = function collapse_widget() {\n            $('body').off('click', collapse);\n            var $selected = get_selected();\n            var index = $collections.index($selected);\n            $collectionSelector.addClass('collapsed').removeClass('expanded');\n            $collectionList.innerHeight(collectionHeight)\n                .css('top', -collectionHeight * index);\n            var val = $collectionList.find('input[type=radio]:checked').val();\n            var subredditListItems = render_subreddit_list(val);\n            $subredditList.html(subredditListItems);\n            if (val === 'none') {\n                $collectionLabel.hide();\n                $frontpageLabel.show();\n            }\n            else {\n                $collectionLabel.show();\n                $frontpageLabel.hide();\n            }\n        }\n\n        function expand() {\n            $('body').on('click', collapse);\n            $collectionSelector.addClass('expanded').removeClass('collapsed');\n            $collectionList\n                .innerHeight(collectionCount * collectionHeight)\n                .css('top', 0);\n        }\n\n        function get_selected() {\n            return $collectionList.find('input[type=radio]:checked')\n                .siblings('.label-group')\n        }\n\n        $collectionSelector\n            .removeClass('uninitialized')\n            .on('click', '.label-group', function(e) {\n                if ($collectionSelector.is('.collapsed')) {\n                    expand();\n                }\n                else {\n                    var $selected = get_selected();\n                    if ($selected[0] !== this) {\n                        $selected.siblings('input').prop('checked', false);\n                        $(this).siblings('input').prop('checked', 'checked');\n                    }\n                    collapse();\n                }\n                return false;\n            });\n\n        collapse();\n    },\n\n    toggleFrequency: function() {\n        var prevChecked = this.frequency_capped;\n        var currentlyChecked = ($('input[name=\"frequency_capped\"]:checked').val() === 'true');\n        if (prevChecked != currentlyChecked) {\n            $('.frequency-cap-field').toggle('slow');\n            this.frequency_capped = currentlyChecked;\n            this.render();\n        }\n    },\n\n    toggleAuctionFields: function() {\n        var prevChecked = this.isAuction;\n        var currentlyChecked = ($('input[name=\"is_auction\"]:checked').val() === 'true');\n        if (prevChecked != currentlyChecked) {\n            $('.auction-field').toggle();\n            $('.fixed-cpm-field').toggle();\n            $('.priority-field').toggle();\n            this.isAuction = currentlyChecked;\n            this.render();\n        }\n    },\n\n    setup_frequency_cap: function(frequency_capped) {\n        this.frequency_capped = !!frequency_capped;\n    },\n\n    setup_mobile_targeting: function(mobileOS, iOSDevices, iOSVersions,\n                                     androidDevices, androidVersions) {\n      this.mobileOS = mobileOS;\n      this.iOSDevices = iOSDevices;\n      this.iOSVersions = iOSVersions;\n      this.androidDevices = androidDevices;\n      this.androidVersions = androidVersions;\n    },\n\n    setup_geotargeting: function(regions, metros) {\n        this.regions = regions\n        this.metros = metros\n    },\n\n    setup_collections: function(collections, defaultValue) {\n        defaultValue = defaultValue || 'none';\n\n        this.collections = [{\n            name: 'none',\n            sr_names: null,\n            description: 'influencers on reddit’s highest trafficking page',\n        }].concat(collections || []);\n\n        this.collectionsByName = _.reduce(collections, function(obj, item) {\n            if (item.sr_names) {\n                item.sr_names = item.sr_names.slice(0, 20);\n            }\n            obj[item.name] = item;\n            return obj;\n        }, {});\n\n        var template = _.template('<label class=\"form-group\">'\n          + '<input type=\"radio\" name=\"collection\" value=\"<%= name %>\"'\n          + '    <% print(name === \\'' + defaultValue + '\\' ? \"checked=\\'checked\\'\" : \"\") %>/>'\n          + '  <div class=\"label-group\">'\n          + '    <span class=\"label\"><% print(name === \\'none\\' ? \\'Reddit front page\\' : name) %></span>'           + '    <small class=\"description\"><%= description %></small>'\n          + '  </div>'\n          + '</label>');\n\n        var rendered = _.map(this.collections, template).join('');\n        $(_.bind(function() {\n            $('.collection-selector .form-group-list').html(rendered);\n            this.setup_collection_selector();\n            this.render_campaign_dashboard_header();\n        }, this))\n    },\n\n    get_dates: function(startdate, enddate) {\n        var start = $.datepicker.parseDate('mm/dd/yy', startdate),\n            end = $.datepicker.parseDate('mm/dd/yy', enddate),\n            ndays = Math.round((end - start) / (1000 * 60 * 60 * 24)),\n            dates = []\n\n        for (var i=0; i < ndays; i++) {\n            var d = new Date(start.getTime())\n            d.setDate(start.getDate() + i)\n            dates.push(d)\n        }\n        return dates\n    },\n\n    get_inventory_key: function(srname, collection, geotarget, platform) {\n        var inventoryKey = collection ? '#' + collection : srname\n        inventoryKey += \"/\" + platform\n        if (geotarget.country != \"\") {\n            inventoryKey += \"/\" + geotarget.country\n        }\n        if (geotarget.metro != \"\") {\n            inventoryKey += \"/\" + geotarget.metro\n        }\n        return inventoryKey\n    },\n\n    needs_to_fetch_inventory: function(targeting, timing) {\n        var dates = timing.dates,\n            inventoryKey = targeting.inventoryKey;\n        return _.some(dates, function(date) {\n            var datestr = $.datepicker.formatDate('mm/dd/yy', date);\n            if (_.has(this.inventory, inventoryKey) && _.has(this.inventory[inventoryKey], datestr)) {\n                return false;\n            }\n            else {\n                r.debug('need to fetch ' + datestr + ' for ' + inventoryKey);\n                return true;\n            }\n        }, this);\n    },\n\n    fetch_inventory: function(targeting, timing) {\n        var srname = targeting.sr,\n            collection = targeting.collection,\n            geotarget = targeting.geotarget,\n            platform = targeting.platform,\n            inventoryKey = targeting.inventoryKey,\n            dates = timing.dates;\n\n        dates.sort(function(d1,d2){return d1 - d2})\n        var end = new Date(dates[dates.length-1].getTime())\n        end.setDate(end.getDate() + 5)\n        return $.ajax({\n            type: 'GET',\n            url: '/api/check_inventory.json',\n            data: {\n                sr: srname,\n                collection: collection,\n                country: geotarget.country,\n                region: geotarget.region,\n                metro: geotarget.metro,\n                startdate: $.datepicker.formatDate('mm/dd/yy', dates[0]),\n                enddate: $.datepicker.formatDate('mm/dd/yy', end),\n                platform: platform\n            },\n        });\n    },\n\n    get_check_inventory: function(targeting, timing) {\n        var inventoryKey = targeting.inventoryKey;\n        if (this.needs_to_fetch_inventory(targeting, timing)) {\n            return this.fetch_inventory(targeting, timing).then(\n                function(data) {\n                    if (!r.sponsored.inventory[inventoryKey]) {\n                        r.sponsored.inventory[inventoryKey] = {}\n                    }\n\n                    for (var datestr in data.inventory) {\n                        if (!r.sponsored.inventory[inventoryKey][datestr]) {\n                            r.sponsored.inventory[inventoryKey][datestr] = data.inventory[datestr]\n                        }\n                    }\n                });\n        } else {\n            return true\n        }\n    },\n\n    get_booked_inventory: function($form, srname, geotarget, isOverride) {\n        var campaign_name = $form.find('input[name=\"campaign_name\"]').val()\n        if (!campaign_name) {\n            return {}\n        }\n\n        var $campaign_row = $('.existing-campaigns .' + campaign_name)\n        if (!$campaign_row.length) {\n            return {}\n        }\n\n        if (!$campaign_row.data('paid')) {\n            return {}\n        }\n\n        var existing_srname = $campaign_row.data(\"targeting\")\n        if (srname != existing_srname) {\n            return {}\n        }\n\n        var existing_country = $campaign_row.data(\"country\")\n        if (geotarget.country != existing_country) {\n            return {}\n        }\n\n        var existing_metro = $campaign_row.data(\"metro\")\n        if (geotarget.metro != existing_metro) {\n            return {}\n        }\n\n        var existingOverride = $campaign_row.data(\"override\")\n        if (isOverride != existingOverride) {\n            return {}\n        }\n\n        var startdate = $campaign_row.data(\"startdate\"),\n            enddate = $campaign_row.data(\"enddate\"),\n            dates = this.get_dates(startdate, enddate),\n            bid = $campaign_row.data(\"bid\"),\n            cpm = $campaign_row.data(\"cpm\"),\n            ndays = this.duration_from_dates(startdate, enddate),\n            impressions = this.calc_impressions(bid, cpm),\n            daily = Math.floor(impressions / ndays),\n            booked = {}\n\n        _.each(dates, function(date) {\n            var datestr = $.datepicker.formatDate('mm/dd/yy', date)\n            booked[datestr] = daily\n        })\n        return booked\n\n    },\n\n    getAvailableImpsByDay: function(dates, booked, inventoryKey) {\n        return _.map(dates, function(date) {\n            var datestr = $.datepicker.formatDate('mm/dd/yy', date);\n            var daily_booked = booked[datestr] || 0;\n            return r.sponsored.inventory[inventoryKey][datestr] + daily_booked;\n        });\n    },\n\n    setup_auction: function($form, targeting, timing) {\n        var dates = timing.dates,\n            totalBudgetDollars = parseFloat($(\"#total_budget_dollars\").val()),\n            costBasisValue = $form.find('#cost_basis').val(),\n            bidDollars = $form.find('#bid_dollars').val() || 0.,\n            minBidDollars = r.sponsored.get_min_bid_dollars(),\n            maxBidDollars = r.sponsored.get_lowest_max_bid_dollars($form),\n            minBudgetDollars = r.sponsored.get_min_budget_dollars(),\n            maxBudgetDollars = r.sponsored.get_max_budget_dollars();\n\n        React.renderComponent(\n          CampaignCreator({\n            totalBudgetDollars: totalBudgetDollars,\n            dates: dates,\n            isNew: $form.find('#is_new').val() === 'true',\n            minBidDollars: minBidDollars,\n            maxBidDollars: maxBidDollars,\n            maxBudgetDollars: parseFloat(maxBudgetDollars),\n            minBudgetDollars: parseFloat(minBudgetDollars),\n            targetName: targeting.displayName,\n            costBasis: costBasisValue.toUpperCase(),\n            bidDollars: parseFloat(bidDollars),\n          }),\n          document.getElementById('campaign-creator')\n        );\n    },\n\n    setup_house: function($form, targeting, timing, isOverride) {\n      $.when(r.sponsored.get_check_inventory(targeting, timing)).then(\n        function() {\n          var booked = this.get_booked_inventory($form, targeting.sr,\n                                                 targeting.geotarget, isOverride);\n          var availableByDate = this.getAvailableImpsByDay(timing.dates, booked,\n                                                           targeting.inventoryKey);\n          var totalImpsAvailable = _.reduce(availableByDate, sum, 0);\n\n          React.renderComponent(\n            React.DOM.div(null,\n              CampaignSet(null,\n                InfoText(null, r._('house campaigns, man.')),\n                CampaignOptionTable(null,\n                  CampaignOption({\n                    bid: null,\n                    end: timing.enddate,\n                    impressions: 'unsold ',\n                    isNew: $form.find('#is_new').val() === 'true',\n                    primary: true,\n                    start: timing.startdate,\n                  })\n                )\n              ),\n              InfoText({impressions: totalImpsAvailable},\n                  r._('maximum possible impressions: %(impressions)s')\n              )\n            ),\n            document.getElementById('campaign-creator')\n          );\n        }.bind(this)\n      );\n\n    },\n\n    check_inventory: function($form, targeting, timing, budget, isOverride) {\n        var totalBudgetDollars = budget.totalBudgetDollars,\n            cpm = budget.cpm,\n            requested = budget.impressions,\n            daily_request = Math.floor(requested / timing.duration),\n            inventoryKey = targeting.inventoryKey,\n            booked = this.get_booked_inventory($form, targeting.sr,\n                    targeting.geotarget, isOverride),\n            minBudgetDollars = r.sponsored.get_min_budget_dollars(),\n            maxBudgetDollars = r.sponsored.get_max_budget_dollars();\n\n        $.when(r.sponsored.get_check_inventory(targeting, timing)).then(\n            function() {\n                var dates = timing.dates;\n                var availableByDay = this.getAvailableImpsByDay(dates, booked, inventoryKey)\n                React.renderComponent(\n                  CampaignCreator({\n                    totalBudgetDollars: totalBudgetDollars,\n                    cpm: cpm,\n                    dates: timing.dates,\n                    inventory: availableByDay,\n                    isNew: $form.find('#is_new').val() === 'true',\n                    maxBudgetDollars: parseFloat(maxBudgetDollars),\n                    minBudgetDollars: parseFloat(minBudgetDollars),\n                    override: isOverride,\n                    requested: requested,\n                    targetName: targeting.displayName,\n                  }),\n\n                  document.getElementById('campaign-creator')\n                );\n            }.bind(this),\n            function () {\n                React.renderComponent(\n                  CampaignSet(null,\n                    InfoText(null,\n                      r._('sorry, there was an error retrieving available impressions. ' +\n                           'please try again later.')\n                    )\n                  ),\n                  document.getElementById('campaign-creator')\n                );\n            }\n        )\n    },\n\n    duration_from_dates: function(start, end) {\n        return Math.round((Date.parse(end) - Date.parse(start)) / (86400*1000))\n    },\n\n    get_total_budget: function($form) {\n        return parseFloat($form.find('*[name=\"total_budget_dollars\"]').val()) || 0\n    },\n\n    get_cpm: function($form) {\n        var isMetroGeotarget = $('#metro').val() !== null && !$('#metro').is(':disabled');\n        var metro = $('#metro').val();\n        var country = $('#country').val();\n        var isGeotarget = country !== '' && !$('#country').is(':disabled');\n        var isSubreddit = $form.find('input[name=\"targeting\"][value=\"one\"]').is(':checked');\n        var collectionVal = $form.find('input[name=\"collection\"]:checked').val();\n        var isFrontpage = !isSubreddit && collectionVal === 'none';\n        var isCollection = !isSubreddit && !isFrontpage;\n        var sr = isSubreddit ? $form.find('*[name=\"sr\"]').val() : '';\n        var collection = isCollection ? collectionVal : null;\n        var prices = [];\n\n        if (isMetroGeotarget) {\n            var metroKey = metro + country;\n            prices.push(this.priceDict.METRO[metro] || this.priceDict.METRO_DEFAULT);\n        } else if (isGeotarget) {\n            prices.push(this.priceDict.COUNTRY[country] || this.priceDict.COUNTRY_DEFAULT);\n        }\n\n        if (isFrontpage) {\n            prices.push(this.priceDict.COLLECTION_DEFAULT);\n        } else if (isCollection) {\n            prices.push(this.priceDict.COLLECTION[collectionVal] || this.priceDict.COLLECTION_DEFAULT);\n        } else {\n            prices.push(this.priceDict.SUBREDDIT[sr] || this.priceDict.SUBREDDIT_DEFAULT);\n        }\n\n        return _.max(prices);\n    },\n\n    getPlatformTargeting: function() {\n      var platform = this.$platformInputs.filter(':checked').val();\n      var isMobile = platform === 'mobile' || platform === 'all';\n\n      function mapTargets(target) {\n        targets = target.filter(':checked').map(function() {\n          return $(this).attr('value');\n        }).toArray().join(',');\n        return targets.length === 1 ? targets[0] : targets\n      }\n\n      function getSelect(target) {\n        return target.find(':selected').val();\n      }\n\n      var targets;\n      if (isMobile) {\n        targets = {\n          os: mapTargets(this.$mobileOSInputs),\n          deviceAndVersion: mapTargets(this.$deviceAndVersionInputs),\n          iOSDevices: mapTargets(this.$iOSDeviceInputs),\n          iOSVersionRange: (getSelect(this.$iOSMinSelect) + ','\n            + getSelect(this.$iOSMaxSelect)),\n          iOSMinVersion: getSelect(this.$iOSMinSelect),\n          iOSMaxVersion: getSelect(this.$iOSMaxSelect),\n          androidDevices: mapTargets(this.$androidDeviceInputs),\n          androidVersionRange: (getSelect(this.$androidMinSelect) + ','\n            + getSelect(this.$androidMaxSelect)),\n          androidMinVersion: getSelect(this.$androidMinSelect),\n          androidMaxVersion: getSelect(this.$androidMaxSelect),\n        };\n      } else {\n        targets = {\n          os: null,\n          deviceAndVersion: null,\n          iOSDevices: null,\n          iOSVersionRange: null,\n          iOSMinVersion: null,\n          iOSMaxVersion: null,\n          androidDevices: null,\n          androidVersionRange: null,\n          androidMinVersion: null,\n          androidMaxVersion: null,\n        };\n      }\n\n      return $.extend({\n        platform: platform,\n        isMobile: isMobile,\n      }, targets);\n    },\n\n    get_targeting: function($form) {\n        var isSubreddit = $form.find('input[name=\"targeting\"][value=\"one\"]').is(':checked'),\n            collectionVal = $form.find('input[name=\"collection\"]:checked').val(),\n            isFrontpage = !isSubreddit && collectionVal === 'none',\n            isCollection = !isSubreddit && !isFrontpage,\n            type = isFrontpage ? 'frontpage' : isCollection ? 'collection' : 'subreddit',\n            sr = isSubreddit ? $form.find('*[name=\"sr\"]').val() : '',\n            collection = isCollection ? collectionVal : null,\n            canGeotarget = isFrontpage || this.userIsSponsor || this.isAuction,\n            country = canGeotarget && $('#country').val() || '',\n            region = canGeotarget && $('#region').val() || '',\n            metro = canGeotarget && $('#metro').val() || '',\n            geotarget = {'country': country, 'region': region, 'metro': metro},\n            inventoryKey = this.get_inventory_key(sr, collection, geotarget, platform),\n            isValid = isFrontpage || (isSubreddit && sr) || (isCollection && collection);\n\n        var displayName;\n        switch(type) {\n            case 'frontpage':\n                displayName = 'the frontpage'\n                break;\n            case 'subreddit':\n                displayName = '/r/' + sr\n                break;\n            default:\n                displayName = collection\n        }\n\n        if (canGeotarget) {\n            var geoStrings = []\n            if (country) {\n                if (region) {\n                    if (metro) {\n                        var metroName = $('#metro option[value=\"'+metro+'\"]').text()\n                        // metroName is in the form 'metro, state abbreviation';\n                        // since we want 'metro, full state', split the metro\n                        // from the state, then add the full state separately\n                        geoStrings.push(metroName.split(',')[0])\n                    }\n                    var regionName = $('#region option[value=\"'+region+'\"]').text()\n                    geoStrings.push(regionName)\n                }\n                var countryName = $('#country option[value=\"'+country+'\"]').text()\n                geoStrings.push(countryName)\n            }\n\n            if (geoStrings.length > 0) {\n                displayName += ' in '\n                displayName += geoStrings.join(', ')\n            }\n        }\n\n        var targets = {\n            'type': type,\n            'displayName': displayName,\n            'isValid': isValid,\n            'sr': sr,\n            'collection': collection,\n            'canGeotarget': canGeotarget,\n            'geotarget': geotarget,\n        };\n\n        if (this.$platformInputs) {\n            var platformTargets = this.getPlatformTargeting();\n\n            var os = platformTargets.os;\n            var platform = platformTargets.platform;\n            var iOSDevices = platformTargets.iOSDevices;\n            var iOSVersionRange = platformTargets.iOSVersionRange;\n            var androidDevices = platformTargets.androidDevices;\n            var androidVersionRange = platformTargets.androidVersionRange;\n\n            platformTargetsList = ['platform',\n                                   'iOSDevices',\n                                   'iOSVersionRange',\n                                   'androidDevices',\n                                   'androidVersionRange',];\n\n            platformTargetsList.forEach(function(platformStr) {\n              targets[platformStr] = eval(platformStr)\n            });\n\n            targets['inventoryKey'] = this.get_inventory_key(sr, collection, geotarget, platform);\n        } else {\n            targets['inventoryKey'] = this.get_inventory_key(sr, collection, geotarget);\n        }\n\n        return targets;\n    },\n\n    get_timing: function($form) {\n        var startdate = $form.find('*[name=\"startdate\"]').val(),\n            enddate = $form.find('*[name=\"enddate\"]').val(),\n            duration = this.duration_from_dates(startdate, enddate),\n            dates = r.sponsored.get_dates(startdate, enddate);\n\n        return {\n            'startdate': startdate,\n            'enddate': enddate,\n            'duration': duration,\n            'dates': dates,\n        }\n    },\n\n    get_budget: function($form) {\n        var totalBudgetDollars = this.get_total_budget($form),\n            cpm = this.get_cpm($form),\n            impressions = this.calc_impressions(totalBudgetDollars, cpm);\n\n        return {\n            'totalBudgetDollars': totalBudgetDollars,\n            'cpm': cpm,\n            'impressions': impressions,\n        };\n    },\n\n    get_priority: function($form) {\n        var priority = $form.find('*[name=\"priority\"]:checked'),\n            isOverride = priority.data(\"override\"),\n            isHouse = priority.data(\"house\");\n\n        return {\n            isOverride: isOverride,\n            isHouse: isHouse,\n        };\n    },\n\n\n    get_reporting: function($form) {\n        var link_text = $form.find('[name=link_text]').val(),\n            owner = $form.find('[name=owner]').val();\n\n        return {\n            link_text: link_text,\n            owner: owner,\n        };\n    },\n\n    get_campaigns: function($list, $form) {\n        var campaignRows = $list.find('.existing-campaigns tbody tr').toArray();\n        var collections = this.collectionsByName;\n        var fixedCPMCampaigns = 0;\n        var fixedCPMSubreddits = {};\n        var totalFixedCPMBudgetDollars = 0;\n        var auctionCampaigns = 0;\n        var auctionSubreddits = {};\n        var totalAuctionBudgetDollars = 0;\n        var totalImpressions = 0;\n\n        function mapSubreddit(name, subreddits) {\n            subreddits[name] = 1;\n        }\n\n        function getSubredditsByCollection(name) {\n            return collections[name] && collections[name].sr_names || null;\n        }\n\n        function mapCollection(name, subreddits) {\n            var subredditNames = getSubredditsByCollection(name);\n            if (subredditNames) {\n                _.each(subredditNames, function(subredditName) {\n                    mapSubreddit(subredditName, subreddits);\n                });\n            }\n        }\n\n        _.each(campaignRows, function(row) {\n            var data = $(row).data();\n            var isCollection = (data.targetingCollection === 'True');\n            var mappingFunction = isCollection ? mapCollection : mapSubreddit;\n            var budget = parseFloat(data.total_budget_dollars, 10);\n\n            if (data.is_auction === 'True') {\n                auctionCampaigns++;\n                mappingFunction(data.targeting, auctionSubreddits);\n                totalAuctionBudgetDollars += budget;\n            } else {\n                fixedCPMCampaigns++;\n                mappingFunction(data.targeting, fixedCPMSubreddits);\n                totalFixedCPMBudgetDollars += budget;\n                var bid = data.bid_dollars;\n                var impressions = Math.floor(budget / bid * 1000);\n                totalImpressions += impressions;\n            }\n        });\n\n        return {\n            count: campaignRows.length,\n            fixedCPMCampaigns: fixedCPMCampaigns,\n            auctionCampaigns: auctionCampaigns,\n            fixedCPMSubreddits: fixedCPMSubreddits,\n            auctionSubreddits: _.keys(auctionSubreddits),\n            fixedCPMSubreddits: _.keys(fixedCPMSubreddits),\n            prettyTotalAuctionBudgetDollars: '$' + totalAuctionBudgetDollars.toFixed(2),\n            prettyTotalFixedCPMBudgetDollars: '$' + totalFixedCPMBudgetDollars.toFixed(2),\n            totalImpressions: r.utils.prettyNumber(totalImpressions),\n        };\n    },\n\n    auction_dashboard_help_template: _.template('<p>there '\n        + '<% auctionCampaigns > 1 ? print(\"are\") : print(\"is\") %> '\n        + '<%= auctionCampaigns %> auction campaign'\n        + '<% auctionCampaigns > 1 && print(\"s\") %> with a total budget of '\n        + '<%= prettyTotalAuctionBudgetDollars %> in '\n        + '<%= auctionSubreddits.length %> subreddit'\n        + '<% auctionSubreddits.length > 1 && print(\"s\") %></p>'),\n\n    fixed_cpm_dashboard_help_template: _.template('<p>there '\n        + '<% fixedCPMCampaigns > 1 ? print(\"are\") : print(\"is\") %> '\n        + '<%= fixedCPMCampaigns %> fixed CPM campaign'\n        + '<% fixedCPMCampaigns > 1 && print(\"s\") %> with a total budget of '\n        + '<%= prettyTotalFixedCPMBudgetDollars %> in '\n        + '<%= fixedCPMSubreddits.length %> subreddit'\n        + '<% fixedCPMSubreddits.length > 1 && print(\"s\") %>, amounting to a '\n        + 'total of <%= totalImpressions %> impressions</p>'),\n\n    render_campaign_dashboard_header: function() {\n        var $form = $(\"#campaign\");\n        var campaigns = this.get_campaigns($('.campaign-list'), $form);\n        var $campaignDashboardHeader = $('.campaign-dashboard header');\n        if (campaigns.count) {\n            var templateText = '';\n            if (campaigns.auctionCampaigns > 0) {\n                templateText += this.auction_dashboard_help_template(campaigns);\n            }\n            if (campaigns.fixedCPMCampaigns > 0) {\n                templateText += this.fixed_cpm_dashboard_help_template(campaigns);\n            }\n            $campaignDashboardHeader\n                .find('.help').show().html(templateText).end()\n                .find('.error').hide();\n        }\n        else {\n            $campaignDashboardHeader\n                .find('.error').show().end()\n                .find('.help').hide();\n        }\n    },\n\n    on_date_change: function() {\n        this.render()\n    },\n\n    on_bid_change: function() {\n        this.render()\n    },\n\n    on_cost_basis_change: function() {\n        this.render();\n    },\n\n    on_budget_change: function() {\n        this.render()\n    },\n\n    on_impression_change: function() {\n        var $form = $(\"#campaign\"),\n            cpm = this.get_cpm($form),\n            impressions = parseInt($form.find('*[name=\"impressions\"]').val().replace(/,/g, \"\") || 0),\n            totalBudgetDollars = this.calc_budget_dollars_from_impressions(impressions, cpm),\n            $totalBudgetDollars = $form.find('*[name=\"total_budget_dollars\"]')\n        $totalBudgetDollars.val(totalBudgetDollars)\n        $totalBudgetDollars.trigger(\"change\")\n    },\n\n    on_frequency_cap_change: function() {\n        this.render();\n    },\n\n    validateDeviceAndVersion: function(os, generalData, osData) {\n      var deviceError = false;\n      var versionError = false;\n      /* if OS is selected to target, populate hidden inputs */\n      if (generalData.platformTargetingOS.indexOf(os) !== -1) {\n        osData.deviceHiddenInput.val(osData.platformTargetingDevices);\n        osData.versionHiddenInput.val(osData.platformTargetingVersions);\n        osData.group.show();\n\n        if (generalData.deviceAndVersion == 'filter') {\n          /* check that at least one devices is selected */\n          if (!osData.deviceHiddenInput.val()) {\n            deviceError = true;\n          }\n\n          /* check that min version is less-or-equal-to max */\n          var versions = osData.versionHiddenInput.val().split(',');\n          if ((versions[1] !== '') && (versions[0] > versions[1])) {\n            versionError = true;\n          }\n        }\n      } else {\n        osData.deviceHiddenInput.val('');\n        osData.versionHiddenInput.val('');\n        osData.group.hide();\n      }\n      return {'deviceError': deviceError,\n              'versionError': versionError}\n    },\n\n    fill_campaign_editor: function() {\n\n        var $form = $(\"#campaign\");\n        var platformTargeting = this.getPlatformTargeting();\n        var platformOverride = platformTargeting.isMobile && platformTargeting.platform === 'mobile';\n\n        this.currentPlatform = platformTargeting.platform;\n\n        var priority = this.get_priority($form),\n            targeting = this.get_targeting($form),\n            timing = this.get_timing($form),\n            ndays = timing.duration,\n            budget = this.get_budget($form),\n            cpm = budget.cpm,\n            impressions = budget.impressions,\n            validTargeting = targeting.isValid;\n\n        var durationInDays = ndays + \" \" + ((ndays > 1) ? r._(\"days\") : r._(\"day\"))\n        $(\".duration\").text(durationInDays)\n        var totalBudgetDollars = parseFloat($(\"#total_budget_dollars\").val())\n        var dailySpend = totalBudgetDollars / parseInt(durationInDays)\n        $(\".daily-max-spend\").text((isNaN(dailySpend) ? 0.00 : dailySpend).toFixed(2));\n\n        $(\".price-info\").text(r._(\"$%(cpm)s per 1,000 impressions\").format({cpm: (cpm/100).toFixed(2)}))\n        $form.find('*[name=\"impressions\"]').val(r.utils.prettyNumber(impressions))\n        $(\".OVERSOLD\").hide()\n\n        var costBasisValue = $form.find('#cost_basis').val();\n        var $costBasisLabel = $form.find('.cost-basis-label');\n        var $pricingMessageDiv = $form.find('.pricing-message');\n\n        var pricingMessage = (costBasisValue === 'cpc') ? 'click' : '1,000 impressions';\n\n        $costBasisLabel.text(costBasisValue);\n        $pricingMessageDiv.text('Set how much you\\'re willing to pay per ' + pricingMessage);\n\n        var $mobileOSGroup = $('.mobile-os-group');\n        var $mobileOSHiddenInput = $('#mobile_os');\n\n        var $OSDeviceGroup = $('.os-device-group');\n        var $iOSDeviceHiddenInput = $('#ios_device');\n        var $iOSVersionHiddenInput = $('#ios_version_range');\n        var $androidDeviceHiddenInput = $('#android_device');\n        var $androidVersionHiddenInput = $('#android_version_range');\n\n        if (platformTargeting.isMobile) {\n          var $mobileOSError = $mobileOSGroup.find('.error');\n          var $OSDeviceError = $OSDeviceGroup.find('.error.device-error');\n          var $OSVersionError = $OSDeviceGroup.find('.error.version-error');\n\n          $mobileOSGroup.show();\n          $mobileOSHiddenInput.val(platformTargeting.os || '');\n\n          $OSDeviceGroup.show();\n\n          if (!platformTargeting.os) {\n            $mobileOSError.show();\n          } else {\n            $mobileOSError.hide();\n          }\n\n          var $deviceVersionGroup = $('.device-version-group');\n          var $deviceAndVersion = (platformTargeting.deviceAndVersion || 'all')\n\n          if ($deviceAndVersion === 'all') {\n            $deviceVersionGroup.hide();\n            $OSDeviceError.hide();\n            $OSVersionError.hide();\n            $iOSDeviceHiddenInput.val('');\n            $iOSVersionHiddenInput.val('');\n            $androidDeviceHiddenInput.val('');\n            $androidVersionHiddenInput.val('');\n          } else {\n            $deviceVersionGroup.show();\n\n            $iOSGroup = $('.ios-group');\n            $androidGroup = $('.android-group');\n\n            var generalData = {\n              platformTargetingOS: platformTargeting.os,\n              deviceAndVersion: $deviceAndVersion,\n            }\n\n            var iOSData = {\n              deviceHiddenInput: $iOSDeviceHiddenInput,\n              versionHiddenInput: $iOSVersionHiddenInput,\n              platformTargetingDevices: platformTargeting.iOSDevices,\n              platformTargetingVersions: platformTargeting.iOSVersionRange,\n              group: $iOSGroup,\n            }\n\n            var androidData = {\n              deviceHiddenInput: $androidDeviceHiddenInput,\n              versionHiddenInput: $androidVersionHiddenInput,\n              platformTargetingDevices: platformTargeting.androidDevices,\n              platformTargetingVersions: platformTargeting.androidVersionRange,\n              group: $androidGroup,\n            }\n\n            var iOSErrors = this.validateDeviceAndVersion('iOS', generalData, iOSData);\n            var androidErrors = this.validateDeviceAndVersion('Android', generalData, androidData);\n            var iOSDeviceError = iOSErrors['deviceError']\n            var iOSVersionError = iOSErrors['versionError'];\n            var androidDeviceError = androidErrors['deviceError'];\n            var androidVersionError = androidErrors['versionError'];\n\n            if (iOSDeviceError || androidDeviceError) {\n              $OSDeviceError.show();\n            } else {\n              $OSDeviceError.hide();\n            }\n\n            if (iOSVersionError || androidVersionError) {\n              $OSVersionError.show();\n            } else {\n              $OSVersionError.hide();\n            }\n          }\n        } else {\n          $mobileOSHiddenInput.val('');\n          $iOSDeviceHiddenInput.val('');\n          $iOSVersionHiddenInput.val('');\n          $androidDeviceHiddenInput.val('');\n          $androidVersionHiddenInput.val('');\n          $mobileOSGroup.hide();\n          $OSDeviceGroup.hide();\n        }\n\n        if (targeting.isValid) {\n            this.targetValid = true;\n            this.enable_form($form);\n        } else {\n            this.targetValid = false;\n            this.disable_form($form);\n        }\n\n        if (priority.isHouse) {\n            this.hide_budget()\n        } else {\n            this.show_budget()\n            this.check_budget($form)\n        }\n\n        this.setupAuctionFields($form, targeting, timing);\n        if (priority.isHouse && validTargeting) {\n            this.setup_house($form, targeting, timing, priority.isOverride);\n        } else if (!this.isAuction && validTargeting) {\n            this.check_inventory($form, targeting, timing, budget, priority.isOverride)\n        }\n\n        if (targeting.canGeotarget) {\n            this.enable_geotargeting();\n        } else {\n            this.disable_geotargeting();\n        }\n\n        var $frequencyCapped = $form.find('[name=frequency_capped]');\n        if (this.frequency_capped === null) {\n            this.frequency_capped = !!$frequencyCapped.val();\n        }\n        // In some cases, the frequency cap is automatically set, but no\n        // frequencyCapped field is rendered; if so, skip this check\n        if (this.frequency_capped && $frequencyCapped.length > 0) {\n            var $frequencyCapField = $form.find('#frequency_cap'),\n                frequencyCapValue = $frequencyCapField.val(),\n                frequencyCapMin = $frequencyCapField.data('frequency_cap_min'),\n                $frequencyCapError = $('.frequency-cap-field').find('.error');\n\n            if (frequencyCapValue < frequencyCapMin || _.isNaN(parseInt(frequencyCapValue, 10))) {\n                $frequencyCapError.show();\n                this.disable_form($form);\n            } else {\n                $frequencyCapError.hide();\n                this.enable_form($form);\n            }\n        }\n\n        // If campaign is new, don't set up live editing fields\n        if ($form.find('#is_new').val() === 'true') {\n            this.setupLiveEditing(false);\n        }\n    },\n\n    disable_geotargeting: function() {\n        $('.geotargeting-selects').find('select').prop('disabled', true).end().hide();\n        $('.geotargeting-disabled').show();\n    },\n\n    enable_geotargeting: function() {\n        $('.geotargeting-selects').find('select').prop('disabled', false).end().show();\n        $('.geotargeting-disabled').hide();\n    },\n\n    disable_form: function($form) {\n        $form.find('button[class*=\"campaign-button\"]')\n            .prop(\"disabled\", true)\n            .addClass(\"disabled\");\n    },\n\n    enable_form: function($form) {\n        if (this.bidValid && this.targetValid) {\n            $form.find('button[class*=\"campaign-button\"]')\n                .prop(\"disabled\", false)\n                .removeClass(\"disabled\");\n        }\n    },\n\n    hide_budget: function() {\n        $('.budget-field').css('display', 'none');\n    },\n\n    show_budget: function() {\n        $('.budget-field').css('display', 'block');\n    },\n\n    subreddit_targeting: function() {\n        $('.subreddit-targeting').find('*[name=\"sr\"]').prop(\"disabled\", false).end().slideDown();\n        $('.collection-targeting').find('*[name=\"collection\"]').prop(\"disabled\", true).end().slideUp();\n        this.render()\n    },\n\n    collection_targeting: function() {\n        $('.subreddit-targeting').find('*[name=\"sr\"]').prop(\"disabled\", true).end().slideUp();\n        $('.collection-targeting').find('*[name=\"collection\"]').prop(\"disabled\", false).end().slideDown();\n        this.render()\n    },\n\n    priority_changed: function() {\n        this.render()\n    },\n\n    update_regions: function() {\n        var $country = $('#country'),\n            $region = $('#region'),\n            $metro = $('#metro')\n\n        $region.find('option').remove().end().hide()\n        $metro.find('option').remove().end().hide()\n        $region.prop('disabled', true)\n        $metro.prop('disabled', true)\n\n        if (_.has(this.regions, $country.val())) {\n            _.each(this.regions[$country.val()], function(item) {\n                var code = item[0],\n                    name = item[1],\n                    selected = item[2]\n\n                $('<option/>', {value: code, selected: selected}).text(name).appendTo($region)\n            })\n            $region.prop('disabled', false)\n            $region.show()\n        }\n    },\n\n    update_metros: function() {\n        var $region = $('#region'),\n            $metro = $('#metro')\n\n        $metro.find('option').remove().end().hide()\n        if (_.has(this.metros, $region.val())) {\n            _.each(this.metros[$region.val()], function(item) {\n                var code = item[0],\n                    name = item[1],\n                    selected = item[2]\n\n                $('<option/>', {value: code, selected: selected}).text(name).appendTo($metro)\n            })\n            $metro.prop('disabled', false)\n            $metro.show()\n        }\n    },\n\n    country_changed: function() {\n        this.update_regions()\n        this.render()\n    },\n\n    region_changed: function() {\n        this.update_metros()\n        this.render()\n    },\n\n    metro_changed: function() {\n        this.render()\n    },\n\n    get_min_bid_dollars: function() {\n        return $('#bid_dollars').data('min_bid_dollars');\n    },\n\n    get_max_bid_dollars: function() {\n        return $('#bid_dollars').data('max_bid_dollars');\n    },\n\n    get_lowest_max_bid_dollars: function($form) {\n      var totalBudgetDollars = $form.find('#total_budget_dollars').val(),\n          duration = this.get_timing($form).duration;\n\n        // maxBidDollars should be the lowest either\n        // of maxBidDollars or dailyMaxBid\n        var maxBidDollars = r.sponsored.get_max_bid_dollars(),\n            dailyMaxBid = totalBudgetDollars / duration;\n\n        return Math.min(maxBidDollars, dailyMaxBid);\n    },\n\n    get_min_budget_dollars: function() {\n        return $('#total_budget_dollars').data('min_budget_dollars');\n    },\n\n    get_max_budget_dollars: function() {\n        return $('#total_budget_dollars').data('max_budget_dollars');\n    },\n\n    check_budget: function($form) {\n        var budget = this.get_budget($form),\n            minBudgetDollars = this.get_min_budget_dollars(),\n            maxBudgetDollars = this.get_max_budget_dollars(),\n            campaignName = $form.find('*[name=campaign_name]').val()\n\n        $('.budget-change-warning').hide()\n        if (campaignName != '') {\n            var $campaignRow = $('.' + campaignName),\n                campaignIsPaid = $campaignRow.data('paid'),\n                campaignTotalBudgetDollars = $campaignRow.data('total_budget_dollars')\n            if (campaignIsPaid && budget.totalBudgetDollars != campaignTotalBudgetDollars) {\n                $('.budget-change-warning').show()\n            }\n        }\n\n        $(\".minimum-spend\").removeClass(\"error\");\n\n        if (!this.userIsSponsor) {\n            if (budget.totalBudgetDollars < minBudgetDollars) {\n                this.bidValid = false;\n                $(\".minimum-spend\").addClass(\"error\");\n            } else if (budget.totalBudgetDollars > maxBudgetDollars) {\n                this.bidValid = false;\n            } else {\n                this.bidValid = true;\n                $(\".minimum-spend\").removeClass(\"error\");\n            }\n        } else {\n            this.bidValid = true;\n            $(\".minimum-spend\").removeClass(\"error\");\n        }\n\n        if (this.bidValid) {\n            this.enable_form($form);\n        } else {\n            this.disable_form($form);\n        }\n    },\n\n    check_bid_dollars: function($form) {\n      var maxBidDollars = r.sponsored.get_lowest_max_bid_dollars($form);\n      var minBidDollars = r.sponsored.get_min_bid_dollars();\n      var bidDollars = $form.find('#bid_dollars').val() || 0.;\n\n      $form.find('.daily-max-spend').text(maxBidDollars.toFixed(2));\n\n      // Form validation\n      if ((maxBidDollars < minBidDollars) ||\n              (bidDollars < minBidDollars) ||\n              (bidDollars > maxBidDollars)) {\n          this.disable_form($form);\n      }\n    },\n\n    calc_impressions: function(bid, cpm_pennies) {\n        return Math.floor(bid / cpm_pennies * 1000 * 100);\n    },\n\n    calc_budget_dollars_from_impressions: function(impressions, cpm_pennies) {\n        return (Math.floor(impressions * cpm_pennies / 1000) / 100).toFixed(2)\n    },\n\n    render_timing_duration: function($form, ndays) {\n        var totalBudgetDollars = ndays + ' ' + ((ndays > 1) ? r._('days') : r._('day'));\n        $form.find('.timing-field .duration').text(totalBudgetDollars);\n    },\n\n    fill_inventory_form: function() {\n        var $form = $('.inventory-dashboard'),\n            targeting = this.get_targeting($form),\n            timing = this.get_timing($form);\n\n        this.render_timing_duration($form, timing.duration);\n    },\n\n    submit_inventory_form: function() {\n        var $form = $('.inventory-dashboard'),\n            targeting = this.get_targeting($form),\n            timing = this.get_timing($form);\n\n        var data = {\n            startdate: timing.startdate,\n            enddate: timing.enddate,\n        };\n\n        if (targeting.type === 'collection') {\n            data.collection_name = targeting.collection;\n        }\n        else if (targeting.type === 'subreddit') {\n            data.sr_name = targeting.sr;\n        }\n\n        this.reload_with_params(data);\n    },\n\n    fill_reporting_form: function() {\n        var $form = $('.reporting-dashboard'),\n            timing = this.get_timing($form);\n\n        this.render_timing_duration($form, timing.duration);\n    },\n\n    submit_reporting_form: function() {\n        var $form = $('.reporting-dashboard'),\n            timing = this.get_timing($form),\n            reporting = this.get_reporting($form),\n            grouping = $form.find(\"[name='grouping']\").val();\n\n        var data = {\n            startdate: timing.startdate,\n            enddate: timing.enddate,\n            link_text: reporting.link_text,\n            owner: reporting.owner,\n            grouping: grouping,\n        };\n\n        this.reload_with_params(data);\n    },\n\n    reload_with_params: function(data) {\n        var queryString = '?' + $.param(data);\n        var location = window.location;\n        window.location = location.origin + location.pathname + queryString;\n    },\n\n    mediaInputChange: function() {\n        var $scraperInputWrapper = $('#scraper_input');\n        var $rgInputWrapper = $('#rg_input');\n        var isScraper = $(this).val() === 'scrape';\n\n        $scraperInputWrapper.toggle(isScraper);\n        $scraperInputWrapper.find('input').prop('disabled', !isScraper);\n        $rgInputWrapper.toggle(!isScraper);\n        $rgInputWrapper.find('input').prop('disabled', isScraper);\n    },\n};\n\n}(r);\n\nvar dateFromInput = function(selector, offset) {\n   if(selector) {\n     var input = $(selector);\n     if(input.length) {\n        var d = new Date();\n        offset = $.with_default(offset, 0);\n        d.setTime(Date.parse(input.val()) + offset);\n        return d;\n     }\n   }\n};\n\nfunction attach_calendar(where, min_date_src, max_date_src, callback, min_date_offset) {\n     $(where).siblings(\".datepicker\").mousedown(function() {\n            $(this).addClass(\"clicked active\");\n         }).click(function() {\n            $(this).removeClass(\"clicked\")\n               .not(\".selected\").siblings(\"input\").focus().end()\n               .removeClass(\"selected\");\n         }).end()\n         .focus(function() {\n          var target = $(this);\n          var dp = $(this).siblings(\".datepicker\");\n          if (dp.children().length == 0) {\n             dp.each(function() {\n               $(this).datepicker(\n                  {\n                      defaultDate: dateFromInput(target),\n                          minDate: dateFromInput(min_date_src, min_date_offset),\n                          maxDate: dateFromInput(max_date_src),\n                          prevText: \"&laquo;\", nextText: \"&raquo;\",\n                          altField: \"#\" + target.attr(\"id\"),\n                          onSelect: function() {\n                              $(dp).addClass(\"selected\").removeClass(\"clicked\");\n                              $(target).blur();\n                              if(callback) callback(this);\n                          }\n                })\n              })\n              .addClass(\"drop-choices\");\n          };\n          dp.addClass(\"inuse active\");\n     }).blur(function() {\n        $(this).siblings(\".datepicker\").not(\".clicked\").removeClass(\"inuse\");\n     }).click(function() {\n        $(this).siblings(\".datepicker.inuse\").addClass(\"active\");\n     });\n}\n\nfunction sum(a, b) {\n    // for things like _.reduce(list, sum);\n    return a + b;\n}\n\nfunction check_enddate(startdate, enddate) {\n  var startdate = $(startdate)\n  var enddate = $(enddate);\n  if(dateFromInput(startdate) >= dateFromInput(enddate)) {\n    var newd = new Date();\n    newd.setTime(startdate.datepicker('getDate').getTime() + 86400*1000);\n    enddate.val((newd.getMonth()+1) + \"/\" +\n      newd.getDate() + \"/\" + newd.getFullYear());\n  }\n  $(\"#datepicker-\" + enddate.attr(\"id\")).datepicker(\"destroy\");\n}\n\n(function($) {\n    $.update_campaign = function(campaign_name, campaign_html) {\n        cancel_edit(function() {\n            var $existing = $('.existing-campaigns .' + campaign_name),\n                tableWasEmpty = $('.existing-campaigns table tr.campaign-row').length == 0\n\n            if ($existing.length) {\n                $existing.replaceWith(campaign_html)\n                $existing.fadeIn()\n            } else {\n                $(campaign_html).hide()\n                .appendTo('.existing-campaigns tbody')\n                .css('display', 'table-row')\n                .fadeIn()\n            }\n\n            if (tableWasEmpty) {\n                $('.existing-campaigns p.error').hide()\n                $('.existing-campaigns table').fadeIn()\n                $('#campaign .buttons button[name=cancel]').removeClass('hidden')\n                $(\"button.new-campaign\").prop(\"disabled\", false);\n            }\n\n            r.sponsored.render_campaign_dashboard_header();\n        })\n    }\n}(jQuery));\n\nfunction detach_campaign_form() {\n    /* remove datepicker from fields */\n    $(\"#campaign\").find(\".datepicker\").each(function() {\n            $(this).datepicker(\"destroy\").siblings().unbind();\n        });\n\n    /* detach and return */\n    var campaign = $(\"#campaign\").detach();\n    return campaign;\n}\n\nfunction cancel_edit(callback) {\n    var $campaign = $('#campaign');\n    var isEditingExistingCampaign = !!$campaign.parents('tr:first').length;\n\n    if (isEditingExistingCampaign) {\n        var tr = $campaign.parents(\"tr:first\").prev();\n        /* copy the campaign element */\n        /* delete the original */\n        $campaign.slideUp(function() {\n                $(this).parent('tr').prev().fadeIn();\n                var td = $(this).parent();\n                var campaign = detach_campaign_form();\n                td.delete_table_row(function() {\n                        tr.fadeIn(function() {\n                                $('.new-campaign-container').append(campaign);\n                                campaign.hide();\n                                if (callback) { callback(); }\n                            });\n                    });\n            });\n    } else {\n        var keep_open = $campaign.hasClass('keep-open');\n\n        if ($campaign.is(':visible') && !keep_open) {\n            $campaign.slideUp(callback);\n        } else if (callback) {\n            callback();\n        }\n\n        if (keep_open) {\n            $campaign.removeClass('keep-open');\n            $campaign.find('.status')\n                .text(r._('Created new campaign!'))\n                .show()\n                .delay(1000)\n                .fadeOut();\n\n            r.sponsored.render();\n        }\n    }\n}\n\nfunction send_campaign(close) {\n    if (!close) {\n        $('#campaign').addClass('keep-open');\n    }\n\n    post_pseudo_form('.campaign', 'edit_campaign');\n}\n\nfunction del_campaign($campaign_row) {\n    var link_id36 = $(\"#campaign\").find('*[name=\"link_id36\"]').val(),\n        campaign_id36 = $campaign_row.data('campaign_id36')\n    $.request(\"delete_campaign\", {\"campaign_id36\": campaign_id36,\n                                  \"link_id36\": link_id36},\n              null, true, \"json\", false);\n    $campaign_row.children(\":first\").delete_table_row(function() {\n        r.sponsored.render_campaign_dashboard_header();\n        return check_number_of_campaigns();\n    });\n}\n\nfunction toggle_pause_campaign($campaign_row, shouldPause) {\n    var link_id36 = $('#campaign').find('*[name=\"link_id36\"]').val(),\n        campaign_id36 = $campaign_row.data('campaign_id36')\n    $.request('toggle_pause_campaign', {'campaign_id36': campaign_id36,\n                                        'link_id36': link_id36,\n                                        'should_pause': shouldPause},\n              null, true, 'json', false);\n    r.sponsored.render();\n}\n\nfunction edit_campaign($campaign_row) {\n    cancel_edit(function() {\n        cancel_edit_promotion();\n        var campaign = detach_campaign_form(),\n            campaignTable = $(\".existing-campaigns table\").get(0),\n            editRowIndex = $campaign_row.get(0).rowIndex + 1\n            $editRow = $(campaignTable.insertRow(editRowIndex)),\n            $editCell = $(\"<td>\").attr(\"colspan\", r.sponsored.campaignListColumns).append(campaign)\n\n        $editRow.attr(\"id\", \"edit-campaign-tr\")\n        $editRow.append($editCell)\n        $campaign_row.fadeOut(function() {\n            /* fill inputs from data in campaign row */\n            _.each(['startdate', 'enddate', 'bid', 'campaign_id36', 'campaign_name',\n                    'frequency_cap', 'total_budget_dollars',\n                    'bid_dollars'],\n                function(input) {\n                    var val = $campaign_row.data(input),\n                        $input = campaign.find('*[name=\"' + input + '\"]')\n                    $input.val(val)\n            })\n\n            if ($campaign_row.data('is_auction') === 'True') {\n              r.sponsored.isAuction = true;\n            } else {\n              r.sponsored.isAuction = false;\n            }\n\n            var platform = $campaign_row.data('platform');\n            campaign.find('*[name=\"platform\"][value=\"' + platform + '\"]').prop(\"checked\", \"checked\");\n\n            /* set mobile targeting */\n            r.sponsored.setup_mobile_targeting(\n              $campaign_row.data('mobile_os'),\n              $campaign_row.data('ios_devices'),\n              $campaign_row.data('ios_versions'),\n              $campaign_row.data('android_devices'),\n              $campaign_row.data('android_versions')\n            );\n\n            /* pre-select mobile OS checkboxes if current platform is not mobile */\n            campaign.find('.mobile-os-group input').prop(\"checked\", !r.sponsored.mobileOS);\n\n            /* logic if filtering by device and OS */\n            if (r.sponsored.iOSDevices || r.sponsored.androidDevices) {\n              /* pre-select the device and OS version radio button */\n              campaign.find('#filter_os_devices').prop('checked', 'checked');\n\n              /* first, clear all checked devices (they're checked by default),\n                 but only if the campaign has devices for the OS */\n              if (r.sponsored.iOSDevices) {\n                campaign.find('.ios-device input[type=\"checkbox\"]').prop('checked', false);\n              }\n              if (r.sponsored.androidDevices) {\n                campaign.find('.android-device input[type=\"checkbox\"]').prop('checked', false);\n              }\n\n              /* then, pre-select all appropriate devices */\n              var allDevices = [].concat(r.sponsored.iOSDevices, r.sponsored.androidDevices);\n              allDevices.forEach(function(device) {\n                if (device) {\n                  campaign.find('#'+device.toLowerCase()).prop('checked', true);\n                }\n              });\n\n              /* pre-select iOS versions */\n              if (r.sponsored.iOSVersions) {\n                campaign.find('#ios_min').val(r.sponsored.iOSVersions[0]);\n                campaign.find('#ios_max').val(r.sponsored.iOSVersions[1]);\n              }\n\n              /* pre-select Android versions */\n              if (r.sponsored.androidVersions) {\n                campaign.find('#android_min').val(r.sponsored.androidVersions[0]);\n                campaign.find('#android_max').val(r.sponsored.androidVersions[1]);\n              }\n            } else {\n              campaign.find('#all_os_devices').prop('checked', true);\n            }\n\n            var mobile_os_names = $campaign_row.data('mobile_os');\n            if (mobile_os_names) {\n              mobile_os_names.forEach(function(name) {\n                campaign.find('#mobile_os_' + name).prop(\"checked\", \"checked\");\n              });\n            }\n\n            r.sponsored.setup_frequency_cap($campaign_row.data('frequency_cap'));\n            /* show frequency inputs */\n            if ($campaign_row.data('frequency_cap')) {\n              $('.frequency-cap-field').show();\n              $('#frequency_capped_true').prop('checked', 'checked');\n            }\n\n            /* set priority */\n            var priorities = campaign.find('*[name=\"priority\"]'),\n                campPriority = $campaign_row.data(\"priority\")\n\n            priorities.filter('*[value=\"' + campPriority + '\"]')\n                .prop(\"checked\", \"checked\")\n\n            /* check if targeting is turned on */\n            var targeting = $campaign_row.data(\"targeting\"),\n                radios = campaign.find('*[name=\"targeting\"]'),\n                isCollection = ($campaign_row.data(\"targeting-collection\") === \"True\"),\n                collectionTargeting = isCollection ? targeting : 'none';\n            if (targeting && !isCollection) {\n                radios.filter('*[value=\"one\"]')\n                    .prop(\"checked\", \"checked\");\n                campaign.find('*[name=\"sr\"]').val(targeting).prop(\"disabled\", false).end()\n                    .find(\".subreddit-targeting\").show();\n                $(\".collection-targeting\").hide();\n            } else {\n                radios.filter('*[value=\"collection\"]')\n                    .prop(\"checked\", \"checked\");\n                $('.collection-targeting input[value=\"' + collectionTargeting + '\"]')\n                    .prop(\"checked\", \"checked\");\n                campaign.find('*[name=\"sr\"]').val(\"\").prop(\"disabled\", true).end()\n                    .find(\".subreddit-targeting\").hide();\n                $('.collection-targeting').show();\n            }\n\n            r.sponsored.collapse_collection_selector();\n\n            /* set geotargeting */\n            var country = $campaign_row.data(\"country\"),\n                region = $campaign_row.data(\"region\"),\n                metro = $campaign_row.data(\"metro\")\n            campaign.find(\"#country\").val(country)\n            r.sponsored.update_regions()\n            if (region != \"\") {\n                campaign.find(\"#region\").val(region)\n                r.sponsored.update_metros()\n\n                if (metro != \"\") {\n                    campaign.find(\"#metro\").val(metro)\n                }\n            }\n\n            /* set cost basis */\n            $('#cost_basis').val($campaign_row.data('cost_basis'));\n\n            /* attach the dates to the date widgets */\n            init_startdate();\n            init_enddate();\n\n            /* setup fields for live campaign editing */\n            r.sponsored.setupLiveEditing($campaign_row.data('is_live') === 'True');\n\n            campaign.find('#is_new').val('false')\n\n            campaign.find('button[name=\"save\"]').show().end()\n                .find('.create').hide().end();\n            campaign.slideDown();\n            r.sponsored.render();\n        })\n    })\n}\n\nfunction check_number_of_campaigns(){\n    if ($(\".campaign-row\").length >= $(\".existing-campaigns\").data(\"max-campaigns\")){\n      $(\".error.TOO_MANY_CAMPAIGNS\").fadeIn();\n      $(\"button.new-campaign\").prop(\"disabled\", true);\n      return true;\n    } else {\n      $(\".error.TOO_MANY_CAMPAIGNS\").fadeOut();\n      $(\"button.new-campaign\").prop(\"disabled\", false);\n      return false;\n    }\n}\n\nfunction create_campaign() {\n    if (check_number_of_campaigns()){\n        return;\n    }\n\n    r.analytics.fireFunnelEvent('ads', 'new-campaign');\n\n    cancel_edit(function() {\n            cancel_edit_promotion();\n            var defaultBudgetDollars = $(\"#total_budget_dollars\").data(\"default_budget_dollars\");\n\n            init_startdate();\n            init_enddate();\n\n            $('#campaign')\n                .find(\".collection-targeting\").show().end()\n                .find('input[name=\"collection\"]').prop(\"disabled\", false).end()\n                .find('input[name=\"collection\"]').eq(0).prop(\"checked\", \"checked\").end().end()\n                .find('input[name=\"collection\"]').slice(1).prop(\"checked\", false).end().end()\n                .find('.collection-selector .form-group-list').css('top', 0).end()\n            r.sponsored.collapse_collection_selector();\n\n            $(\"#campaign\")\n                .find('button[name=\"save\"]').hide().end()\n                .find('.create').show().end()\n                .find('input[name=\"campaign_id36\"]').val('').end()\n                .find('input[name=\"campaign_name\"]').val('').end()\n                .find('input[name=\"sr\"]').val('').prop(\"disabled\", true).end()\n                .find('input[name=\"targeting\"][value=\"collection\"]').prop(\"checked\", \"checked\").end()\n                .find('input[name=\"priority\"][data-default=\"true\"]').prop(\"checked\", \"checked\").end()\n                .find('input[name=\"total_budget_dollars\"]').val(defaultBudgetDollars).end()\n                .find(\".subreddit-targeting\").hide().end()\n                .find('select[name=\"country\"]').val('').end()\n                .find('select[name=\"region\"]').hide().end()\n                .find('select[name=\"metro\"]').hide().end()\n                .find('input[name=\"frequency_cap\"]').val('').end()\n                .find('input[name=\"startdate\"]').prop('disabled', false).end()\n                .find('#frequency_capped_false').prop('checked', 'checked').end()\n                .find('.frequency-cap-field').hide().end()\n                .find('input[name=\"is_new\"]').val('true').end()\n                .slideDown();\n            r.sponsored.render();\n        });\n}\n\nfunction free_campaign($campaign_row) {\n    var link_id36 = $(\"#campaign\").find('*[name=\"link_id36\"]').val(),\n        campaign_id36 = $campaign_row.data('campaign_id36')\n    $.request(\"freebie\", {\"campaign_id36\": campaign_id36, \"link_id36\": link_id36},\n              null, true, \"json\", false);\n    $campaign_row.find(\".free\").fadeOut();\n    return false;\n}\n\nfunction terminate_campaign($campaign_row) {\n    var link_id36 = $(\"#campaign\").find('*[name=\"link_id36\"]').val(),\n        campaign_id36 = $campaign_row.data('campaign_id36')\n    $.request(\"terminate_campaign\", {\"campaign_id36\": campaign_id36,\n                                     \"link_id36\": link_id36},\n              null, true, \"json\", false);\n}\n\nfunction edit_promotion() {\n    $(\"button.new-campaign\").prop(\"disabled\", false);\n    cancel_edit(function() {\n        $('.promotelink-editor')\n            .find('.collapsed-display').slideUp().end()\n            .find('.uncollapsed-display').slideDown().end()\n    })\n    return false;\n}\n\nfunction cancel_edit_promotion() {\n    $('.promotelink-editor')\n        .find('.collapsed-display').slideDown().end()\n        .find('.uncollapsed-display').slideUp().end()\n\n    return false;\n}\n\nfunction cancel_edit_campaign() {\n    $(\"button.new-campaign\").prop(\"disabled\", false);\n    return cancel_edit()\n}\n\n!function(exports) {\n    /*\n     * @param {number[]} days An array of inventory for the campaign's timing\n     * @param {number} minValidRequest The minimum request a campaign is allowed\n     *                                 to have, should be in the same units as `days`\n     * @param {number} requested The campaign's requested inventory, in the same\n     *                           units as `days` and `minValidRequest`.\n     * @param {number} maxOffset maximum valid start index\n     * @returns {{days: number[], maxRequest: number, offset:number}|null}\n     *                            The sub-array, maximum request for it, and\n     *                            its offset from the original `days` array.\n     */\n    exports.getMaximumRequest = _.memoize(\n      function getMaximumRequest(days, minValidRequest, requested, maxOffset) {\n        return check(days, 0);\n\n        /**\n         * check if a set of days is valid, then compare to results of this\n         * function called on subsets of that date range\n         * @param  {Number[]} days inventory values\n         * @param  {Number} offset offset from the original days array we are\n         *                         working on\n         * @return {Object|null}  object describing the best range found,\n         *                        or null if no valid range was found\n         */\n        function check(days, offset) {\n          var bestOption = null;\n          if (days.length > 0 && offset <= maxOffset) {\n            // check the validity of the days array.\n            var minValue = min(days);\n            var maxRequest = minValue * days.length;\n            if (maxRequest >= minValidRequest) {\n              bestOption = {days: days, maxRequest: maxRequest, offset: offset};\n            }\n          }\n          if (bestOption === null || bestOption.maxRequest < requested) {\n            // if bestOptions does not hit our target, check sub-arrays.  start\n            // by splitting on values that invalidate the date range (anything\n            // with inventory below the minimum daily amount).\n            // subtract 0.1 because the comparison used to filter is > (not >=)\n            var minDaily = days.length / minValidRequest - 0.1;\n            return split(days, offset, bestOption, minDaily, check, true)\n          }\n          else {\n            return bestOption;\n          }\n        }\n      },\n      function hashFunction(days, minValidRequest, requested) {\n        return [days.join(','), minValidRequest, requested].join('|');\n      }\n    );\n\n    /**\n     * compare two date range options, returning the better\n     * options are compared on their maximum request first, then their duration\n     * @param  {Object|null} a\n     * @param  {Object|null} b\n     * @return {Object|null}\n     */\n    function compare(a, b) {\n      if (!b) {\n        return a;\n      }\n      else if (!a) {\n        return b;\n      }\n      if (b.maxRequest > a.maxRequest ||\n          (b.maxRequest === a.maxRequest && b.days.length > a.days.length)) {\n        return b;\n      }\n      else {\n        return a;\n      }\n    }\n\n    function min(arr) {\n      return Math.min.apply(Math, arr);\n    }\n\n    /**\n     * split an array of inventory into sub-arrays, checking each\n     * @param  {number[]} days - inventory data for a range of contiguous dates\n     * @param  {number} offset - index offset from original array\n     * @param  {Object|null} bestOption - current best option\n     * @param  {number} minValue - value used to split the days array on; values\n     *                             below this are excluded\n     * @param  {function} check - function to call on sub-arrays\n     * @param  {boolean} recurse - whether or not to call this function again if\n     *                             unable to split array (more on this below)\n     * @return {Object|null} - best option found\n     */\n    function split(days, offset, bestOption, minValue, check, recurse) {\n      var sub = [];\n      var subOffset = 0;\n      for (var i = 0, l = days.length; i < l; i++) {\n        if (days[i] > minValue) {\n          if (sub.length === 0) {\n            subOffset = offset + i;\n          }\n          sub.push(days[i])\n        }\n        else {\n          // whenever we hit the end of a contiguous set of days above the\n          // minValue threshold, compare that sub-array to our current bestOption\n          if (sub.length) {\n            bestOption = compare(bestOption, check(sub, subOffset))\n            sub = [];\n          }\n        }\n      }\n      if (sub.length === days.length) {\n        // if the array was not split at all:\n        if (recurse) {\n          // if we were previously splitting on the minimum valid value, try\n          // splitting on the smallest value in the array.  The `recurse` value\n          // prevents this from looping infinitely\n          return compare(bestOption, split(days, offset, null, min(days), check, false));\n        }\n        else {\n          // otherwise, just return the current best\n          return bestOption;\n        }\n      }\n      else if (sub.length) {\n        // need to compare the last sub array, as it won't checked in the for loop\n        return compare(bestOption, check(sub, subOffset));\n      }\n      else {\n        // if _no_ values were found above the minValue threshold\n        return bestOption;\n      }\n    }\n}(r.sponsored);\n"
  },
  {
    "path": "r2/r2/public/static/js/spotlight.js",
    "content": "!function(r, _, $) {\n  r.spotlight = {\n    _bindEvents: function() {\n      // unbind everything and then selectively rebind\n      this.$listing.off('.spotlight');\n      this.$listing.find('.arrow.prev').off('.spotlight');\n      this.$listing.find('.arrow.next').off('.spotlight');\n      $(document).off('.spotlight');\n      $(window).off('.spotlight');\n\n      this.$listing.on('click.spotlight', function(e) {\n        var $target = $(e.target);\n        if ($target.is('.thumbnail, .title')) {\n          this.adWasClicked = true;\n        }\n      }.bind(this));\n\n      if (this.$listing.length) {\n        this.$listing.find('.arrow.prev').on('click.spotlight', this.prev);\n        this.$listing.find('.arrow.next').on('click.spotlight', this.next);\n      }\n\n      if (this.showPromo) {\n        // IE 9 and below do not have this prop or work with\n        // visibilitychange.\n        if ('hidden' in document) {\n          $(document).on('visibilitychange.spotlight', this._requestOrSaveTimestamp.bind(this));\n        } else {\n          $(window).on('focus.spotlight blur.spotlight', this._requestOrSaveTimestamp.bind(this));\n        }\n      }\n    },\n\n    setup: function(organicLinks, interestProb, showPromo, srnames) {\n      this.organics = [];\n      this.lineup = [];\n      this.adWasClicked = false;\n      this.interestProb = interestProb;\n      this.showPromo = showPromo;\n      this.srnames = srnames;\n      this.loid = $.cookie('loid');\n      this.lastTabChangeTimestamp = Date.now();\n      this.MIN_PROMO_TIME = 3000;\n      this.next = this._advance.bind(this, 1);\n      this.prev = this._advance.bind(this, -1);\n      this.$listing = $('.organic-listing');\n      this.adBlockIsEnabled = $('#siteTable_organic').is(\":hidden\");\n\n      if (this.adBlockIsEnabled) {\n        this.showPromo = false;\n      }\n\n      this._bindEvents();\n\n      organicLinks.forEach(function(name) {\n        this.organics.push(name);\n        this.lineup.push({ fullname: name, });\n      }, this);\n\n      if (interestProb) {\n        this.lineup.push('.interestbar');\n      }\n\n      var selectedThing;\n      var lastClickFullname = r.analytics.breadcrumbs.lastClickFullname();\n      var $lastClickThing = $(lastClickFullname ? '.id-' + lastClickFullname : null);\n\n      if ($lastClickThing.length && this.$listing.has($lastClickThing).length) {\n        r.debug('restoring spotlight selection to last click');\n        selectedThing = { fullname: lastClickFullname, };\n      } else {\n        var shouldForcePromo = this._isDocumentVisible() && this.showPromo;\n        selectedThing = this.chooseRandom(shouldForcePromo);\n      }\n\n      this.lineup = _.chain(this.lineup)\n        .reject(function(el) {\n          return _.isEqual(selectedThing, el);\n        })\n        .shuffle()\n        .unshift(selectedThing)\n        .value();\n\n      this.lineup.pos = 0;\n      this._advance(0);\n    },\n\n    _requestOrSaveTimestamp: function() {\n      if ( this._isDocumentVisible() ) {\n        this.requestNewPromo();\n      } else {\n        this.lastTabChangeTimestamp = Date.now();\n      }\n    },\n\n    _isDocumentVisible: function () {\n      if ('hidden' in document) {\n        return !document.hidden;\n      } else {\n        return document.hasFocus();\n      }\n    },\n\n    requestNewPromo: function() {\n      var $promotedLink = this.$listing.find('.promotedlink');\n      // if there isn't an ad visible currently don't fetch a new one\n      if (!$promotedLink.is(':visible')) {\n        return;\n      }\n      // we don't want to fetch a new ad when the user has clicked so they \n      // can have a chance to vote or comment on the last ad.\n      if (this.adWasClicked) {\n        return;\n      }\n\n      \n      var $clearLeft = $promotedLink.next('.clearleft');\n\n      if (this.adBlockIsEnabled ||\n          Date.now() - this.lastTabChangeTimestamp < this.MIN_PROMO_TIME) {\n        return;\n      }\n\n      if ($promotedLink.length && $promotedLink.offset().top < window.scrollY) {\n        return;\n      }\n\n      var newPromo = this.requestPromo({\n        refresh: true,\n      });\n\n      newPromo.then(function($promo) {\n        if (!$promo || !$promo.length) {\n          return;\n        }\n\n        var $link = $promo.eq(0);\n        var fullname = $link.data('fullname');\n\n        if ($promotedLink.length) {\n          this.organics[this.lineup.pos] = fullname;\n          this.lineup[this.lineup.pos] = newPromo;\n        } else {\n          this.organics[this.lineup.pos + 1] = fullname;\n          this.lineup[this.lineup.pos + 1] = newPromo;\n        }\n\n        if (!$link.hasClass('adsense-wrap')) {\n          if ($promotedLink.length) {\n            $promotedLink.add($clearLeft).remove(); \n            $promo.show();            \n          } else {\n            this.next()\n          }\n        }\n        // force a redraw to prevent showing duplicate ads\n        this.$listing.hide().show();\n      }.bind(this));\n    },\n\n    requestPromo: function(options) {\n      options = options || {};\n\n      return $.ajax({\n        type: 'POST',\n        url: '/api/request_promo',\n        timeout: 1000,\n        data: {\n          srnames: this.srnames,\n          r: r.config.post_site,\n          loid: this.loid,\n          is_refresh: options.refresh,\n        },\n      }).pipe(function(promo) {\n        var prevPromo = this.$listing.find('.promotedlink')\n        if (promo) {\n          if (this.showPromo) {\n            $('#siteTable_organic').show('slow');\n          }\n\n          var $item = $(promo);\n          // adsense will throw error if inserted while hidden\n          if (!$item.hasClass('adsense-wrap')) {\n            $item.hide().appendTo(this.$listing);\n          } else {\n            var $promotedLink = this.$listing.find('.promotedlink');\n            $promotedLink.remove()\n            $item.appendTo(this.$listing);\n          }\n          return $item;\n        } else {\n          if (!prevPromo.length && !this.organics.length) {\n            // spotlight box must be hidden when no ad is returned\n            // and there is no other content.\n            $('#siteTable_organic').hide();\n          }\n          return false;\n        }\n      }.bind(this));\n    },\n\n    chooseRandom: function(forcePromo) {\n      if (forcePromo) {\n        return this.requestPromo();\n      } else if (Math.random() < this.interestProb) {\n        return '.interestbar';\n      } else {\n        var name = this.organics[_.random(this.organics.length)];\n        return (name) ? { fullname: name, } : null;\n      }\n    },\n\n    _materialize: function(item) {\n      if (!item || item instanceof $ || item.promise) {\n        return item;\n      }\n\n      var itemSel;\n\n      if (typeof item === 'string') {\n        itemSel = item;\n      } else if (item.campaign) {\n        itemSel = '[data-cid=\"' + item.campaign + '\"]';\n      } else {\n        itemSel = '[data-fullname=\"' + item.fullname + '\"]';\n      }\n\n      var $item = this.$listing.find(itemSel);\n\n      if ($item.length) {\n        return $item;\n      } else {\n        r.error('unable to locate spotlight item', itemSel, item);\n      }\n    },\n\n    _advancePos: function(dir) {\n      return (this.lineup.pos + dir + this.lineup.length) % this.lineup.length;\n    },\n\n    _materializePos: function(pos) {\n      return this.lineup[pos] = this._materialize(this.lineup[pos]);\n    },\n\n    _advance: function(dir) {\n      var $nextprev = this.$listing.find('.nextprev');\n      var $visible = this.$listing.find('.thing:visible');\n      var nextPos = this._advancePos(dir);\n      var $next = this._materializePos(nextPos);\n\n      var showWorking = setTimeout(function() {\n        $nextprev.toggleClass('working', $next.state && $next.state() == 'pending');\n      }, 200);\n\n      this.lineup.pos = nextPos;\n      var $nextLoad = $.when($next);\n\n      $nextLoad.always(function($next) {\n        clearTimeout(showWorking);\n\n        if (this.lineup.pos != nextPos) {\n          // we've been passed!\n          return;\n        }\n\n        if ($nextLoad.state() == 'rejected' || !$next) {\n          if (this.lineup.length > 1) {\n            this._advance(dir || 1);\n            return;\n          } else {\n            this.$listing.hide();\n            return;\n          }\n        }\n\n        $nextprev.removeClass('working');\n        this.$listing.removeClass('loading');\n\n        // match the listing background to that of the displayed thing\n        if ($next) {\n          var nextColor = $next.css('background-color');\n          if (nextColor) {\n            this.$listing.css('background-color', nextColor);\n          }\n        }\n\n        $visible.hide();\n        $next.show();\n        this.help($next);\n\n        // prefetch forward and backward if advanced beyond default state\n        if (this.lineup.pos != 0) {\n          this._materializePos(this._advancePos(1));\n          this._materializePos(this._advancePos(-1));\n        }\n      }.bind(this));\n    },\n\n    help: function($thing) {\n      var $help = $('#spotlight-help');\n\n      if (!$help.length) {\n        return;\n      }\n\n      // this function can be called before the help bubble has initialized\n      $(function() {\n        var help = $help.data('HelpBubble');\n\n        // `r.ui.refreshListing` replaces the help and it needs\n        // to be reinitialized.\n        if (!help) {\n          help = new r.ui.Bubble({el: $help.get(0)});\n        }\n\n        help.hide(function() {\n          $help.find('.help-section').hide();\n          if ($thing.hasClass('promoted')) {\n            $help.find('.help-promoted').show();\n          } else if ($thing.hasClass('interestbar')) {\n            $help.find('.help-interestbar').show();\n          } else if ($thing.hasClass('adsense-wrap')) {\n            $help.find('.help-adserver').show()\n          } else {\n            $help.find('.help-organic').show();\n          }\n        });\n      });\n    },\n  };\n}(r, _, jQuery);\n"
  },
  {
    "path": "r2/r2/public/static/js/sr-autocomplete.js",
    "content": "/*\nthis file is a quick fix to help detangle frontend dependencies\n */\n\nr.srAutocomplete = {};\n\n/**** sr completing ****/\nfunction sr_cache() {\n    if (!$.defined(r.config.sr_cache)) {\n        r.srAutocomplete.sr_cache = new Array();\n    } else {\n        r.srAutocomplete.sr_cache = r.config.sr_cache;\n    }\n    return r.srAutocomplete.sr_cache;\n}\n\nfunction sr_search(query) {\n    query = query.toLowerCase();\n    var cache = sr_cache();\n    if (!cache[query]) {\n        $.request('search_reddit_names.json', {query: query, include_over_18: r.config.over_18},\n                  function (r) {\n                      cache[query] = r['names'];\n                      update_dropdown(r['names']);\n                  });\n    }\n    else {\n        update_dropdown(cache[query]);\n    }\n}\n\nfunction sr_name_up(e) {\n    var new_sr_name = $(\"#sr-autocomplete\").val();\n    var old_sr_name = window.old_sr_name || '';\n    window.old_sr_name = new_sr_name;\n\n    if (new_sr_name == '') {\n        hide_sr_name_list();\n    }\n    else if (e.keyCode == 38 || e.keyCode == 40 || e.keyCode == 9) {\n    }\n    else if (e.keyCode == 27 && r.srAutocomplete.orig_sr) {\n        $(\"#sr-autocomplete\").val(r.srAutocomplete.orig_sr);\n        hide_sr_name_list();\n    }\n    else if (new_sr_name != old_sr_name) {\n        r.srAutocomplete.orig_sr = new_sr_name;\n        sr_search($(\"#sr-autocomplete\").val());\n    }\n}\n\nfunction sr_name_down(e) {\n    var input = $(\"#sr-autocomplete\");\n    \n    if (e.keyCode == 38 || e.keyCode == 40) {\n        var dir = e.keyCode == 38 && 'up' || 'down';\n\n        var cur_row = $(\"#sr-drop-down .sr-selected:first\");\n        var first_row = $(\"#sr-drop-down .sr-name-row:first\");\n        var last_row = $(\"#sr-drop-down .sr-name-row:last\");\n\n        var new_row = null;\n        if (dir == 'down') {\n            if (!cur_row.length) new_row = first_row;\n            else if (cur_row.get(0) == last_row.get(0)) new_row = null;\n            else new_row = cur_row.next(':first');\n        }\n        else {\n            if (!cur_row.length) new_row = last_row;\n            else if (cur_row.get(0) == first_row.get(0)) new_row = null;\n            else new_row = cur_row.prev(':first');\n        }\n        highlight_reddit(new_row);\n        if (new_row) {\n            input.val($.trim(new_row.text()));\n        }\n        else {\n            input.val(r.srAutocomplete.orig_sr);\n        }\n        return false;\n    }\n    else if (e.keyCode == 13) {\n        $(\"#sr-autocomplete\").trigger(\"sr-changed\");\n        hide_sr_name_list();\n        input.parents(\"form\").submit();\n        return false;\n    }   \n}\n\nfunction hide_sr_name_list(e) {\n    $(\"#sr-drop-down\").hide();\n}\n\nfunction sr_dropdown_mdown(row) {\n    r.srAutocomplete.sr_mouse_row = row; //global\n    return false;\n}\n\nfunction sr_dropdown_mup(row) {\n    if (r.srAutocomplete.sr_mouse_row == row) {\n        var name = $(row).text();\n        $(\"#sr-autocomplete\").val(name);\n        $(\"#sr-drop-down\").hide();\n        $(\"#sr-autocomplete\").trigger(\"sr-changed\");\n    }\n}\n\nfunction set_sr_name(link) {\n    var name = $(link).text();\n    $(\"#sr-autocomplete\").trigger('focus').val(name);\n    $(\"#sr-autocomplete\").trigger(\"sr-changed\");\n}\n"
  },
  {
    "path": "r2/r2/public/static/js/stateify.js",
    "content": ";(function($) {\n  'use strict';\n\n  var TOOLTIP_TEMPLATE = '<div class=\"c-tooltip\" role=\"tooltip\"><div class=\"tooltip-arrow\"></div><div class=\"tooltip-inner\"></div></div>';\n  var GROUP_CLASS = 'c-form-group';\n  var CONTROL_CLASS = 'c-form-control';\n  var CONTROL_SELECTOR = '.' + CONTROL_CLASS;\n  var CONTROL_FEEDBACK_CLASS_PREFIX = 'c-form-control-feedback-';\n  var CONTROL_FEEDBACK_SELECTOR_PREFIX = '.' + CONTROL_FEEDBACK_CLASS_PREFIX;\n  var FEEDBACK_CLASS = 'c-has-feedback';\n  var STATE_CLASSES = {\n    loading: 'c-has-throbber',\n    success: 'c-has-success',\n    error:  'c-has-error',\n  };\n  var FEEDBACK_SUFFIX = {\n    loading: 'throbber',\n    success: 'success',\n    error: 'error',\n  };\n\n  function rightPosition($el) {\n    var offset = $el.offset();\n\n    return $el.outerWidth() + (offset ? offset.left : 0);\n  }\n\n  function getClassNames() {\n    var classNames = STATE_CLASSES;\n\n    if (arguments.length) {\n      classNames = _.pick(classNames, _.toArray(arguments));\n    }\n\n    return _.values(classNames).concat(FEEDBACK_CLASS).join(' ');\n  }\n\n  function hasTooltip($el) {\n    return !!$el.data('bs.tooltip');\n  }\n\n  var Stateify = function(element, options) {\n    this.initialize(element, options);\n  };\n\n  _.extend(Stateify.prototype, {\n\n    _currentState: null,\n\n    _getFeedbackSelector: function() {\n      var state = this.getCurrentState();\n\n      return CONTROL_FEEDBACK_SELECTOR_PREFIX + FEEDBACK_SUFFIX[state];\n    },\n\n    initialize: function(element, options) {\n      this.$el = $(element).closest('.' + GROUP_CLASS);\n\n      return this;\n    },\n\n    getCurrentState: function() {\n      return this._currentState;\n    },\n\n    set: function(state /*, args.. */) {\n      if (this._currentState !== state) {\n        this.clear();\n\n        this._currentState = state;\n        this.$el.addClass(getClassNames(state));\n      }\n\n      if (arguments.length > 1) {\n        this.showMessage.apply(this, _.toArray(arguments).slice(1));\n      }\n\n      return this;\n    },\n\n    showMessage: function(message) {\n      if (!message) {\n        return;\n      }\n\n      var feedbackSelector = this._getFeedbackSelector();\n      var $control = this.$el.find(CONTROL_SELECTOR);\n      var $feedback = this.$el.find(feedbackSelector);\n\n      // Message already set.\n      if (message === $feedback.attr('data-original-title')) {\n        return;\n      }\n\n      $feedback.attr('title', message);\n\n      // If a tooltip is already attached just change the title\n      if (hasTooltip($feedback)) {\n        $feedback.tooltip('fixTitle');\n\n        if ($control.is(':focus')) {\n          $feedback.tooltip('show');\n        }\n\n        return;\n      }\n\n      $feedback\n        .tooltip({\n          template: TOOLTIP_TEMPLATE,\n          placement: 'right',\n          trigger: 'manual',\n        });\n\n      if ($control.is(':focus') || $control.parents('form').find('[type=\"submit\"]:focus').length) {\n        $feedback.tooltip('show');\n\n        // check that tooltip position isn't outside the viewport.\n        var $viewport = $('body');\n        var tip = $feedback.data('bs.tooltip');\n        var $tip = tip.$tip;\n\n        if ($viewport.length && rightPosition($viewport) < rightPosition($tip)) {\n          tip.options.placement = 'top-right';\n\n          $feedback.tooltip('show');\n        }\n      }\n\n      $control\n        .on('focus.c.stateify', function() {\n          $control.parents('form')\n            .find(feedbackSelector)\n              .not($feedback)\n                .tooltip('hide');\n\n          var tooltip = $feedback.data('bs.tooltip');\n\n          // Cancel hide after fade out.\n          if (tooltip) {\n            tooltip.tip().off('bsTransitionEnd');\n          }\n\n          $feedback.tooltip('show');\n        })\n        .on('blur.c.stateify', function() {\n          $feedback.tooltip('hide');\n        });\n\n      $feedback\n        .on('mouseenter.c.stateify', function() {\n          if (!$control.is(':focus')) {\n            $feedback.tooltip('show');\n          }\n        })\n        .on('mouseleave.c.stateify', function() {\n          if (!$control.is(':focus')) {\n            $feedback.tooltip('hide');\n          }\n        });\n    },\n\n    clear: function() {\n      var $feedback = this.$el.find(this._getFeedbackSelector());\n      var $control = this.$el.find(CONTROL_SELECTOR);\n\n      $feedback\n          .tooltip('destroy')\n          .removeAttr('data-original-title') // Destroy should do this but doesn't.\n          .off('mouseenter.c.stateify mouseleave.c.stateify');\n\n      $control.off('focus.c.stateify blur.c.stateify');\n\n      this.$el.removeClass(getClassNames());\n\n      this._currentState = null;\n\n      return this;\n    },\n\n  });\n\n  $.fn.stateify = function(option /* ,args... */) {\n    var args = _.toArray(arguments).slice(1);\n\n    if (option && /^get/.test(option)) {\n      var data = this.data('c.stateify');\n\n      return data && data[option].apply(data, args);\n    }\n\n    return this.each(function() {\n      var $el = $(this);\n      var data = $el.data('c.stateify');\n      var options = typeof option === 'object' && option;\n\n      if (!data) {\n        data = new Stateify(this, options);\n        $el.data('c.stateify', data);\n      }\n\n      if (typeof option === 'string') {\n        data[option].apply(data, args);\n      }\n    });\n  };\n\n}(window.jQuery));\n"
  },
  {
    "path": "r2/r2/public/static/js/strength-meter.js",
    "content": ";(function($) {\n  'use strict';\n\n  var StrengthMeter = function(element, options) {\n    this.initialize(element, options);\n  };\n\n  StrengthMeter.DEFAULTS = {\n    template: '<div class=\"strength-meter\">' +\n                '<div class=\"strength-meter-fill\"></div>' +\n              '</div>',\n    delay: 100,\n    minimumDisplay: 5,\n    trigger: 'keyup change blur',\n  };\n\n  var KNOWN_WEAK = [\n    /^password(\\d+)?$/i,\n    /^letmein(\\d+)?$/i,\n    /^welcome(\\d+)?$/i,\n    /^secret(\\d+)?$/i,\n    /^reddit(\\d+)?$/i,\n    /^(reddit)\\1+/i,\n    /^(test)\\1+$/i,\n    /^abcd?e?f?1234?5?6?$/i,\n    /^iloveyou$/i,\n    /^admin$/i,\n    /^trustno1$/i,\n    /^.werty$/i,\n    /^sunshine$/i,\n    /^monkey$/i,\n    /^shadow$/i,\n    /^princess$/i,\n    /^dragon$/i,\n  ];\n\n  var ALPHA_ORDERED = 'abcdefghijklmnopqrstuvwxyz';\n  var QWERTY_ORDERED = 'qwertyuiopasdfghjklzxcvbnm';\n  var QAZWSX_ORDERED = 'qazwsxedcrfvtgbyhnujmikolp';\n  var NUMERIC_ORDERED = '01234567890';\n  var SYMBOLS_ORDERED = '!@#$%^&*()';\n\n  function isWeak(password, related) {\n    return _.any(related, function(string) {\n        return (string && password.indexOf(string) !== -1);\n      }) ||\n      _.any(KNOWN_WEAK, function(regex) {\n        return regex.test(password);\n      });\n  }\n\n  function testConsecutive(characterClass) {\n    return function(string) {\n      var regex = new RegExp(characterClass + '+', 'g');\n      var consecutiveGroups = string.match(regex) || [];\n\n      return _.reduce(consecutiveGroups, function(total, group) {\n        return total += group.length;\n      }, 0);\n    };\n  }\n\n  function testRepeat(string) {\n    var charArray = _.toArray(string);\n    var unique = _.unique(charArray).length;\n    var totalRepeatDistance = 0;\n\n    for (var i = 0, l = charArray.length; i < l; i++) {\n      for (var j = 0; j < l; j++) {\n        if (charArray[i] === charArray[j] && i !== j) {\n          // The distance between repeat characters adjusted for total length\n          totalRepeatDistance += Math.abs(l / (j - i));\n        }\n      }\n    }\n\n    return Math.ceil(unique === 0 ? totalRepeatDistance : totalRepeatDistance / unique);\n  }\n\n  function testOrdered(ordered) {\n    return function(string) {\n      var insensitive = string.toLowerCase();\n      var total = 0;\n\n      for (var i = 0, l = ordered.length - 3; i < l; i++) {\n        var forward = ordered.substring(i, i + 3);\n        var reverse = _.toArray(forward).reverse().join('');\n\n        if (insensitive.indexOf(forward) != -1 || insensitive.indexOf(reverse) != -1) {\n          total++;\n        }\n      }\n\n      return total;\n    };\n  }\n\n  var TESTS = [{\n    test: /./g,\n    weight: 4,\n  }, {\n    test: /[A-Z]/g,\n    weight: function (string, n) {\n      return !n || string.length === n ?\n        0 : ((string.length - n) * 2);\n    },\n  }, {\n    test: /[a-z]/g,\n    weight: function (string, n) {\n      return !n || string.length === n ?\n        0 : ((string.length - n) * 2);\n    },\n  }, {\n    test: /\\d/g,\n    weight: function(string, n) {\n      return !n || string.length === n ?\n        0 : (n * 4);\n    },\n  }, {\n    test: /\\W|_/g,\n    weight: 6,\n  }, {\n    test: /^[a-z]+$/i,\n    weight: -1,\n  }, {\n    test: /^\\d+$/i,\n    weight: -1,\n  }, {\n    test: testOrdered(ALPHA_ORDERED),\n    weight: -3,\n  }, {\n    test: testOrdered(QWERTY_ORDERED),\n    weight: -3,\n  }, {\n    test: testOrdered(QAZWSX_ORDERED),\n    weight: -3,\n  }, {\n    test: testOrdered(NUMERIC_ORDERED),\n    weight: -3,\n  }, {\n    test: testOrdered(SYMBOLS_ORDERED),\n    weight: -3,\n  }, {\n    test: testConsecutive('[a-z]'),\n    weight: -2,\n  }, {\n    test: testConsecutive('[A-Z]'),\n    weight: -2,\n  }, {\n    test: testConsecutive('\\\\d'),\n    weight: -2,\n  }, {\n    test: testRepeat,\n    weight: -1,\n  }];\n\n  function testRunner(string, definition) {\n    var n = 0;\n\n    if (definition.test instanceof Function) {\n      n = definition.test(string);\n    } else {\n      var matches = string.match(definition.test);\n\n      n = matches ? matches.length : 0;\n    }\n\n    if (definition.weight instanceof Function) {\n      return definition.weight(string, n);\n    } else {\n      return definition.weight * n;\n    }\n  }\n\n  function getScore(password, related) {\n    password = password || '';\n\n    if (isWeak(password, related)) {\n      return 0;\n    }\n\n    return _.reduce(TESTS, function(total, definition) {\n      return total += testRunner(password, definition);\n    }, 0);\n  }\n\n  _.extend(StrengthMeter.prototype, {\n\n    _cancelScore: false,\n\n    initialize: function(element, options) {\n      this.options = $.extend({}, StrengthMeter.DEFAULTS, options);\n\n      var $el = this.$el = $(element);\n      var $meter = this.$meter = $(this.options.template);\n      var trigger = this.options.trigger;\n\n      if (trigger !== 'manual') {\n        $el.on(this.options.trigger, _.debounce(this.score.bind(this), this.options.delay));\n      }\n\n      $meter.insertAfter($el);\n\n      var meterWidth = $meter.outerWidth();\n      var meterPadding = (meterWidth + 5) + 'px';\n\n      $el.css({'padding-right': meterPadding});\n      $el.trigger('initialize.strengthMeter');\n\n      return this;\n    },\n\n    score: function() {\n      var value = this.$el.val();\n      var related = _.map(this.options.related, function(related) {\n        return $(related).val() || '';\n      });\n      var score = getScore(value, related);\n      var displayScore = Math.min(100, Math.max(this.options.minimumDisplay, score));\n\n      this.$el.trigger('score.strengthMeter', displayScore);\n\n      if (!this._cancelScore) {\n        this.$meter.find('.strength-meter-fill').css({width: displayScore + '%'});\n      }\n\n      this._cancelScore = false;\n    },\n\n    cancelScore: function() {\n      this._cancelScore = true;\n    },\n\n  });\n\n\n  function Plugin(option /* ,args... */) {\n    var args = Array.prototype.slice.call(arguments, 1);\n\n    return this.each(function() {\n      var $el = $(this);\n      var data = $el.data('c.strengthMeter');\n      var options = typeof option === 'object' && option;\n\n      if (!data) {\n        data = new StrengthMeter(this, options);\n        $el.data('c.strengthMeter', data);\n      }\n\n      if (typeof option === 'string') {\n        data[option].apply(data, args);\n      }\n    });\n  }\n\n  $.fn.strengthMeter = Plugin;\n  $.fn.strengthMeter.Constructor = StrengthMeter;\n\n}(window.jQuery));\n"
  },
  {
    "path": "r2/r2/public/static/js/synced-session-storage.js",
    "content": "!function(r) {\n\n  // this key will be used to sync sessionStorages through localStorage\n  var SYNC_EVENT_KEY = '__synced_session_storage__';\n  var PERSIST_SYNCED_KEYS_KEY = '__synced_session_storage_keys__';\n\n  var isStorageSupported = true;\n  try {\n    sessionStorage.setItem(\n      PERSIST_SYNCED_KEYS_KEY,\n      sessionStorage.getItem(PERSIST_SYNCED_KEYS_KEY) || ''\n    )\n  } catch (err) {\n    isStorageSupported = false;\n  }\n\n  /*\n    SessionStorage is too restrictive; each new tab in sessionStorage is\n    considered a separate session.  LocalStorage never expires; manually\n    expiring data is a pain, and often we just want data to last for the\n    (logical) session. Enter SyncedSessionStorage.\n\n    SyncedSessionStorage is a wrapper around SessionStorage that uses\n    LocalStorage events for syncing data across multiple open tabs.\n   */\n  function SyncedSessionStorage(sync_key) {\n    this._bootstrapped = false;\n    this._synced_storage_keys = {};\n\n    // We want the API to match localStorage/sessionStorage, so the\n    // public methods intentionally don't catch errors, but we *should* catch\n    // potential errors during instantiation so we can at least be sure\n    // r.syncedSessionStorage exists.\n    if (!this.isSupported) {\n      return this;\n    }\n\n    var persisted_synced_keys = sessionStorage.getItem(PERSIST_SYNCED_KEYS_KEY);\n\n    if (persisted_synced_keys) {\n      // existing session in this tab, use the existing sessionStorage data\n      this._bootstrapped = true;    \n      this._synced_storage_keys = JSON.parse(persisted_synced_keys);\n    } else {\n      // send a request to bootstrap from existing sessions if there are any\n      // any existing sessions will send back a 'bootstrap' event containing\n      // the bootstrap data\n      if (isStorageSupported) {\n        this._sync({\n          type: 'init',\n        });\n      }\n    }\n\n    $(window).on('storage', function(e) {\n      // the localStorage.removeItem will come in with a null newValue\n      if (e.originalEvent.key === SYNC_EVENT_KEY && e.originalEvent.newValue) {\n        var event = JSON.parse(e.originalEvent.newValue);\n        this._handleSync(event);\n      }\n    }.bind(this));\n  }\n\n  SyncedSessionStorage.prototype = {\n    constructor: SyncedSessionStorage,\n\n    isSupported: isStorageSupported,\n\n    getItem: function(key) {\n      if (key in this._synced_storage_keys) {\n        return sessionStorage.getItem(key);\n      } else {\n        return null;\n      }\n    },\n\n    setItem: function(key, value) {\n      this._setItem(key, value);\n      this._sync({\n        type: 'set',\n        key: key,\n        value: value.toString(),\n      });\n    },\n\n    removeItem: function(key) {\n      if (key in this._synced_storage_keys) {\n        this._removeItem(key);\n        this._sync({\n          type: 'remove',\n          key: key,\n        });\n      }\n    },\n\n    _sync: function(event) {\n      // set and remove event data from localStorage, triggering storage event\n      localStorage.setItem(SYNC_EVENT_KEY, JSON.stringify(event));\n      localStorage.removeItem(SYNC_EVENT_KEY);\n    },\n\n    _handleSync: function(event) {\n      // handle the storage event triggered from other tabs\n      if (event.type === 'set') {\n        this._setItem(event.key, event.value);\n      } else if (event.type === 'remove') {\n        this._removeItem(event.key);\n      } else if (event.type === 'init') {\n        this._sendBootstrapEvent();\n      } else if (event.type === 'bootstrap') {\n        this._handleBootstrapEvent(event.payload);\n      }\n    },\n\n    _sendBootstrapEvent: function() {\n      // when a new session needs bootstrapping, send entire current state\n      // if we've seen an 'init' event before receiving a 'bootstrap' event, it\n      // almost certainly means this was just the first tab open in the session\n      if (!this._bootstrapped) {\n        this._bootstrapped = true;\n      }\n\n      var payload = {};\n      \n      for (var key in this._synced_storage_keys) {\n        payload[key] = sessionStorage.getItem(key);\n      }\n\n      this._sync({\n        type: 'bootstrap',\n        payload: payload,\n      });\n    },\n\n    _handleBootstrapEvent: function(payload) {\n      if (this._bootstrapped) { return; }\n\n      for (var key in payload) {\n        this._synced_storage_keys[key] = 1;\n        sessionStorage.setItem(key, payload[key]);\n      }\n\n      this._bootstrapped = true;\n    },\n\n    _removeItem: function(key) {\n      sessionStorage.removeItem(key);\n      delete this._synced_storage_keys[key];\n      sessionStorage.setItem(\n        PERSIST_SYNCED_KEYS_KEY,\n        JSON.stringify(this._synced_storage_keys)\n      )\n    },\n\n    _setItem: function(key, value) {\n      sessionStorage.setItem(key, value);\n      this._synced_storage_keys[key] = 1;\n      sessionStorage.setItem(\n        PERSIST_SYNCED_KEYS_KEY,\n        JSON.stringify(this._synced_storage_keys)\n      )\n    },\n  };\n\n  r.syncedSessionStorage = new SyncedSessionStorage();\n}(r);\n"
  },
  {
    "path": "r2/r2/public/static/js/templates.js",
    "content": "r.templating = {}\n\nr.templating.TemplateSet = function() {\n    this.index = {}\n}\nr.templating.TemplateSet.prototype = {\n    _templateSettings: {\n        variable: 'thing'\n    },\n\n    _key: function(name, style) {\n        return name + '.' + style\n    },\n\n    _create: function(templateData) {\n        return _.template(templateData, null, this._templateSettings)\n    },\n\n    set: function(templates) {\n        _.each(templates, function(tplInfo) {\n            // if uncompressedJS, the template was embedded in the HTML\n            // rather than an external resource and was escaped for safety\n            if (r.config.uncompressedJS) {\n                tplInfo = r.utils.unescapeJson(tplInfo)\n            }\n\n            var key = this._key(tplInfo.name, tplInfo.style)\n            this.index[key] = tplInfo.template\n        }, this)\n    },\n\n    _defaultStyle: function(nameAndStyle) {\n        // `nameAndStyle` can be an array of [name, style] or simply a name,\n        // defaulting the style to r.config.renderstyle.\n        if (!_.isArray(nameAndStyle)) {\n            nameAndStyle = [nameAndStyle, r.config.renderstyle]\n        }\n        return nameAndStyle\n    },\n\n    get: function(nameAndStyle) {\n        nameAndStyle = this._defaultStyle(nameAndStyle)\n        var key = this._key(nameAndStyle[0], nameAndStyle[1])\n\n        if (!this.index[key]) {\n            throw '\"' + nameAndStyle[0] + '.' + nameAndStyle[1] + '\"' + ' template not found.'\n        }\n\n        template = this.index[key]\n        if (!_.isFunction(template)) {\n            template = this.index[key] = this._create(template)\n        }\n        return template\n    },\n\n    make: function(nameAndStyle, data, parentEl) {\n        html = this.get(nameAndStyle)(data)\n        if (parentEl) {\n            $(parentEl).append(html)\n        }\n        return html\n    }\n}\n\nr.templates = new r.templating.TemplateSet()\n"
  },
  {
    "path": "r2/r2/public/static/js/timeouts.js",
    "content": "/*\n  If the current user is 'in timeout', show a modal on restricted actions.\n\n  requires r.config (base.js)\n  requires r.access (access.js)\n  requires r.ui.Popup (popup.js)\n */\n!function(r) {\n  // initialized early so click handlers can be bound on declaration\n  r.timeouts = {};\n\n  _.extend(r.timeouts, {\n    init: function() {\n      $('body').on('click', '.access-required', this._handleClick);\n      $('.access-required').removeAttr('onclick');\n\n      // special handling for the comment box...\n      $('body.comments-page').on('focus', '.usertext.cloneable textarea', function(e) {\n        $(this).blur();\n        r.timeouts._handleClick(e);\n      });\n      $('body.comments-page').on('submit', 'form.usertext.cloneable', this._handleClick);\n      $('body.comments-page form.usertext.cloneable').removeAttr('onsubmit');\n\n      var isLinkRestricted = r.access.isLinkRestricted;\n\n      r.access.isLinkRestricted = function(el) {\n        return r.timeouts.isLinkRestricted(el) || isLinkRestricted(el);\n      }\n\n      this._popup = r.ui.createGatePopup({\n        templateId: 'access-popup',\n        className: 'access-denied-modal',\n      });\n    },\n\n    _logEvent: function(e) {\n      var target = $(e.target);\n      var thing = target.thing();\n\n      var targetType = target.data('type') || thing.data('type');\n      var targetFullname = target.data('fullname') || thing.data('fullname');\n      var actionName = target.data('event-action');\n      var actionDetail = target.data('event-detail');\n\n      // set default action for modal\n      if (!actionName) {\n        actionName = 'modal';\n        actionDetail = null;\n      }\n\n      // set target using page context\n      if (!targetFullname && targetType == 'subreddit') {\n        targetFullname = r.config.cur_site;\n      } else if (!targetFullname && targetType == 'link') {\n        targetFullname = r.config.cur_link;\n      }\n\n      r.analytics.timeoutForbiddenEvent(actionName, actionDetail, targetType, targetFullname);\n    },\n\n    _handleClick: function onClick(e) {\n      this._popup.show();\n      this._logEvent(e);\n      return false;\n    }.bind(r.timeouts),\n\n    isLinkRestricted: function(el) {\n      return $(el).hasClass('access-required') && r.config.user_in_timeout;\n    },\n  });\n\n  r.access.initHook(function() {\n    if (!r.config.user_in_timeout) { return; }\n\n    r.timeouts.init();\n  });\n}(r);\n"
  },
  {
    "path": "r2/r2/public/static/js/timeseries.js",
    "content": "r.timeseries = {\n    _tickSizeByInterval: {\n        'hour': [1, 'day'],\n        'day': [7, 'day'],\n        'month': [1, 'month']\n    },\n    _units : ['', 'k', 'M', 'B'],\n    _tooltip: null,\n    _currentHover: null,\n\n    init: function () {\n        $('table.timeseries').each($.proxy(function (i, table) {\n            var series = this.makeTimeSeriesChartsFromTable(table)\n            this.addBarsToTable(table, series)\n        }, this))\n    },\n\n    _formatTick: function (val, axis) {\n        if (val == 0)\n            return '0'\n        else if (val < 10)\n            return val.toFixed(2)\n\n        for (var i = 1; i < this._units.length; i++) {\n            if (val / Math.pow(1000, i - 1) < 1000) {\n                break\n            }\n        }\n\n        val /= Math.pow(1000, i - 1)\n\n        return val.toFixed(axis.tickDecimals) + this._units[i - 1]\n    },\n\n    makeTimeSeriesChartsFromTable: function (table) {\n        var table = $(table),\n            series = this._readFromTable(table),\n            options = this._configureFlot(table),\n            chartRow = $('<div>')\n\n        $.each(series, function (i, chart) {\n            var figure = $('<div class=\"timeseries\">')\n                            .append($('<span class=\"title\">').text(chart.caption))\n                            .append('<div class=\"timeseries-placeholder\">')\n                            .appendTo(chartRow)\n\n            chart.placeholder = figure.find('.timeseries-placeholder')\n        })\n        $('#charts').append(chartRow)\n\n        $.each(series, function(i, chart) {\n            $.plot(chart.placeholder, [chart], options)\n        })\n\n        table.addClass('charted')\n\n        return series\n    },\n\n    addBarsToTable: function (table, series) {\n        var table = $(table),\n            newColHeader = $('<th scope=\"col\">')\n\n        table.find('thead tr').append(newColHeader)\n\n        table.find('tbody tr').each(function (i, row) {\n            var row = $(row),\n                data = row.children('td'),\n                newcol = $('<td>')\n\n            $.each(series, function (i, s) {\n                var datum = data.eq(s.index).data('value'),\n                    bar = $('<div>').addClass('timeseries-tablebar')\n                                    .css('background-color', s.color)\n                                    .width((datum / s.maxValue) * 100)\n\n                if (datum > 0)\n                    newcol.append(bar)\n\n                if (datum !== 0 && datum === s.maxValue) {\n                    row.addClass('max')\n                       .css('border-color', s.color)\n                }\n            })\n\n            row.append(newcol)\n        })\n    },\n\n    _configureFlot: function (table) {\n        var interval = table.data('interval'),\n            tickUnit = this._tickSizeByInterval[interval],\n            unprocessed = $('#timeseries-unprocessed').data('last-processed'),\n            markings = []\n\n        if (unprocessed) {\n            markings.push({\n                color: '#eee',\n                xaxis: {\n                    from: unprocessed\n                }\n            })\n\n            markings.push({\n                color: '#aaa',\n                xaxis: {\n                    from: unprocessed,\n                    to: unprocessed\n                }\n            })\n        }\n\n        return {\n            grid: {\n                'markings': markings\n            },\n\n            xaxis: {\n                mode: 'time',\n                tickSize: tickUnit\n            },\n\n            yaxis: {\n                min: 0,\n                tickFormatter: $.proxy(this, '_formatTick')\n            }\n        }\n    },\n\n    _readFromTable: function (table) {\n        var maxPoints = parseInt(table.data('maxPoints'), 10),\n            headers = table.find('thead tr:last-child th:not(:first-child)'),\n            series = []\n\n        // initialize the series\n        headers.each(function (i, header) {\n            var header = $(header),\n                caption = header.attr('title'),\n                color = header.data('color')\n\n            if (!color) {\n                return\n            }\n\n            series.push({\n                'lines': {\n                    'show': true,\n                    'steps': true,\n                    'fill': true\n                },\n\n                'color': color,\n                'caption': caption,\n                'data': [],\n                'maxValue': 0,\n                'index': i\n            })\n        })\n\n        // read the data from the table\n        var rows = table.find('tbody tr')\n        rows.each(function (i, row) {\n            var row = $(row),\n                timestamp = row.children('th').data('value'),\n                data = row.children('td')\n\n            $.each(series, function (j, s) {\n                var datum = data.eq(s.index).data('value')\n                // if we have a maximum number of data points to chart, choose\n                // the most recent ones from the table\n                if (!maxPoints || i > rows.length - maxPoints)\n                    s.data.push([timestamp, datum])\n                s.maxValue = Math.max(s.maxValue, datum)\n            })\n        })\n\n        return series\n    }\n}\n"
  },
  {
    "path": "r2/r2/public/static/js/timetext.js",
    "content": "!function(r, $){\n    if (!Date.now) {\n        Date.now = function now() {\n            return new Date().getTime()\n        }\n    }\n\n    var clientTimeInit = Date.now();\n\n    var CHUNKS = [\n        [60 * 60 * 24 * 365, r.NP_('a year ago', '%(num)s years ago')],\n        [60 * 60 * 24 * 30, r.NP_('a month ago', '%(num)s months ago')],\n        [60 * 60 * 24, r.NP_('a day ago', '%(num)s days ago')],\n        [60 * 60, r.NP_('an hour ago', '%(num)s hours ago')],\n        [60, r.NP_('a minute ago', '%(num)s minutes ago')],\n    ]\n\n    var defaults = {\n        maxage: 24 * 60 * 60\n    }\n\n    function TimeText(opts) {\n        this.opts = _.defaults(opts || {}, defaults)\n\n        this.elCache = $([])\n\n        this.refresh = _.throttle(this._refresh, 1000)\n\n        setInterval($.proxy(this.refresh, this), 20 * 1000)\n        this.refresh()\n    }\n\n    TimeText.prototype._refresh = function(){\n        var now = TimeText.now()\n\n        this.elCache.each($.proxy(function (i, el) {\n            this.refreshOne(el, now)\n        }, this))\n    }\n\n    TimeText.prototype.updateCache = function(elCache) {\n        this.elCache = elCache\n        this.refresh()\n    }\n\n    TimeText.prototype.refreshOne = function (el, now) {\n        if (!now){\n            now = TimeText.now()\n        }\n\n        var $el = $(el)\n        var timestamp = r.utils.parseTimestamp($el)\n        var text\n        var age\n        \n        age = (now - timestamp) / 1000\n\n        if (this.opts.maxage !== false && age > this.opts.maxage) {\n            $el.removeClass('live-timestamp')\n            return\n        }\n\n        text = this.formatTime($el, age, timestamp, now)\n        $el.text(text)\n    }\n\n    TimeText.prototype.formatTime = function($el, age, timestamp, now) {\n        var text = r._('just now')\n        $.each(CHUNKS, function(ix, chunk) {\n            var count = Math.floor(age / chunk[0])\n            var keys\n\n            if (count > 0) {\n                keys = chunk[1]\n                text = r.P_(keys[0], keys[1], count).format({num: count})\n                return false\n            }\n        })\n        return text\n    }\n\n    TimeText.clientOffset = 0;\n\n    TimeText.now = function() {\n        return Date.now() - TimeText.clientOffset;\n    }\n\n    TimeText.init = function() {\n        var serverTimeInit = r.config.server_time * 1000;\n        var delta = clientTimeInit - serverTimeInit;\n        var msPerHour = 1000 * 60 * 60;\n        var clientHourOffset = Math.round(delta / msPerHour);\n\n        this.clientOffset = clientHourOffset * msPerHour;\n    }\n\n    r.TimeText = TimeText\n}(r, jQuery)\n"
  },
  {
    "path": "r2/r2/public/static/js/timings.js",
    "content": "!function(r, Backbone, _) {\n  'use strict'\n\n  var Timing = Backbone.Model.extend({\n    duration: function() {\n      return this.get('end') - this.get('start')\n    },\n    isValid: function() {\n      // if the end is 0, window.performance didn't actually track this.\n      return this.get('end') !== 0\n    }\n  })\n\n  var Timings = Backbone.Collection.extend({\n    model: Timing,\n    comparator: 'start',\n\n    initialize: function() {\n      this.on('reset', this.calculate, this)\n    },\n\n    calculate: function() {\n      this.startTime = this.min(function(timing) {\n        return timing.get('start')\n      }).get('start')\n\n      this.endTime = this.max(function(timing) {\n        return timing.get('end')\n      }).get('end')\n\n      this.duration = this.endTime - this.startTime\n    }\n  })\n\n  var NavigationTimings = Timings.extend({\n    fetch: function() {\n      if (!window.performance || !window.performance.timing) {\n        return\n      }\n\n      var pt = window.performance.timing\n      var timings = []\n\n      function timing(key, start, end) {\n        if (!pt[start] || !pt[end]) {\n          return\n        }\n\n        timings.push({\n          key: key,\n          start: pt[start] / 1000,\n          end: pt[end] / 1000\n        })\n      }\n\n      timing('redirect', 'redirectStart', 'redirectEnd')\n      timing('start', 'fetchStart', 'domainLookupStart')\n      timing('dns', 'domainLookupStart', 'domainLookupEnd')\n      timing('tcp', 'connectStart', 'connectEnd')\n      timing('https', 'secureConnectionStart', 'connectEnd')\n      timing('request', 'requestStart', 'responseStart')\n      timing('response', 'responseStart', 'responseEnd')\n      timing('domLoading', 'domLoading', 'domInteractive')\n      timing('domInteractive', 'domInteractive', 'domContentLoadedEventStart')\n      timing('domContentLoaded', 'domContentLoadedEventStart', 'domContentLoadedEventEnd')\n\n      this.reset(_.values(timings))\n    }\n  })\n\n  r.NavigationTimings = NavigationTimings\n  r.Timing = Timing\n  r.Timings = Timings\n\n}(r, Backbone, _)\n"
  },
  {
    "path": "r2/r2/public/static/js/toggles.js",
    "content": "!function($) {\n  'use strict';\n\n  var Togglable = function(element, options) {\n    this.initialize(element, options);\n  };\n\n  _.extend(Togglable.prototype, {\n\n    initialize: function(element, options) {\n      var $el = this.$el = $(element);\n\n      $el.on('click', function(e) {\n        var $el = $(e.target);\n        var selector = $el.data('toggle');\n\n        $el.toggleClass('c-toggle-toggled');\n        $(selector).toggleClass('c-toggle-content-toggled');\n      });\n\n      return this;\n    },\n\n  });\n\n\n  function Plugin(option /* ,args... */) {\n    var args = Array.prototype.slice.call(arguments, 1);\n\n    return this.each(function() {\n      var $el = $(this);\n      var data = $el.data('c.toggle');\n      var options = typeof option === 'object' && option;\n\n      if (!data) {\n        data = new Togglable(this, options);\n        $el.data('c.toggle', data);\n      }\n\n      if (typeof option === 'string') {\n        data[option].apply(data, args);\n      }\n    });\n  }\n\n  $.fn.togglable = Plugin;\n  $.fn.togglable.Constructor = Togglable;\n\n}(window.jQuery);\n"
  },
  {
    "path": "r2/r2/public/static/js/traffic.js",
    "content": "r.traffic = {\n    init: function () {\n        // add a simple method of jumping to any subreddit's traffic page\n        if ($('body').hasClass('traffic-sitewide'))\n            this.addSubredditSelector()\n    },\n\n    addSubredditSelector: function () {\n        $('<form>').append(\n            $('<fieldset>').append(\n                $('<legend>').text(r._('view subreddit traffic')),\n                $('<input type=\"text\" id=\"srname\">'),\n                $('<input type=\"submit\">').attr('value', r._('go'))\n            )\n        ).submit(r.traffic._onSubredditSelected)\n        .prependTo('.traffic-tables-side')\n    },\n\n    _onSubredditSelected: function () {\n        var srname = $(this.srname).val()\n\n        window.location = window.location.protocol + '//' +\n                          r.config.cur_domain +\n                          '/r/' + srname +\n                          '/about/traffic'\n\n        return false\n    }\n}\n\n$(function () {\n    r.traffic.init()\n})\n"
  },
  {
    "path": "r2/r2/public/static/js/ui/formbar.html",
    "content": "<form class=\"form-bar c-clearfix\">\n  <div class=\"md-container-small\">\n    <div class=\"md\">\n      <p class=\"form-bar-text\"><%- thing.text %></p>\n    </div>\n    \n    <button type=\"submit\" class=\"c-btn c-btn-primary\"><%- thing.buttonLabel %></button>\n  </div>\n  <input type=\"hidden\" name=\"<%- thing.key %>\" value=\"<%- thing.value %>\">\n</form>\n"
  },
  {
    "path": "r2/r2/public/static/js/ui/formbar.js",
    "content": "!function(r, undefined) {\n  r.ui.FormBar = Backbone.View.extend({\n    templateName: 'ui/formbar',\n\n    defaults: {\n      text: '',\n      buttonLabel: '',\n      key: '',\n      value: '',\n    },\n\n    events: {\n      'submit .form-bar': 'onSubmit',\n    },\n\n    initialize: function() {\n      var templateProps = _.defaults(this.options, this.defaults);\n      this.render(templateProps);\n    },\n\n    render: function(templateProps) {\n      var content = r.templates.make(this.templateName, templateProps);\n      this.$el.html(content);\n    },\n\n    onSubmit: function(e) {\n      e.preventDefault();\n      this.trigger('submit', get_form_fields(e.target));\n    },\n  });\n}(r);\n"
  },
  {
    "path": "r2/r2/public/static/js/ui.js",
    "content": "r.ui.init = function() {\n    // welcome bar\n    if ($.cookie('reddit_first')) {\n        // save welcome seen state and delete obsolete cookie\n        $.cookie('reddit_first', null, {domain: r.config.cur_domain})\n        store.safeSet('ui.shown.welcome', true)\n    } else if (store.safeGet('ui.shown.welcome') != true) {\n        $('.infobar.welcome').show()\n        store.safeSet('ui.shown.welcome', true)\n    }\n\n    var smallScreen = r.ui.isSmallScreen();\n\n    // mweb beta banner\n    var mwebOptInCookieName = \"__cf_mob_redir\";\n    var onFrontPage = $.url().attr('path') == '/';\n    if (smallScreen && onFrontPage && r.config.renderstyle != 'compact' && !r.ui.inMobileWebBlacklist()) {\n        var a = document.createElement('a');\n        a.href = window.location;\n        a.host = 'm.' + r.config.cur_domain;\n        a.search += (a.search ? '&' : '?') + 'ref=mobile_beta_banner&ref_source=desktop'\n        var url = a.href;\n\n        var $bar = $(_.template(\n          '<a href=\"<%- url %>\" class=\"mobile-web-redirect\"><%- button_text %></a>', {\n            url: url,\n            button_text: r._(\"switch to mobile version\"),\n          }));\n\n        $bar.on('click', function() {\n           $.cookie(mwebOptInCookieName, '1', {\n               domain: r.config.cur_domain,\n               path:'/',\n               expires: 90\n            });\n\n           // redirect\n           return true;\n        });\n\n        $('#header').before($bar)\n    }\n\n    $('.help-bubble').each(function(idx, el) {\n        $(el).data('HelpBubble', new r.ui.Bubble({el: el}))\n    })\n\n    $('.submit_text').each(function(idx, el) {\n        $(el).data('SubredditSubmitText', new r.ui.SubredditSubmitText({el: el}))\n    })\n\n    /* Open links in new tabs if they have the preference set or are logged out\n     * and on a \"large\" screen. */\n    if (r.config.new_window && (r.config.logged || !smallScreen)) {\n        $(document.body).on('click', 'a.may-blank, .may-blank-within a', function(e) {\n\n            if (!this.target) {\n                // Trident doesn't support `rel=\"noreferrer\"` and requires a\n                // fallback to make sure `window.opener` is unset\n                var isWebLink = _.contains(['http:', 'https:'], this.protocol);\n                if (this.href && isWebLink && r.utils.onTrident()) {\n                    var w = window.open(this.href, '_blank');\n                    // some popup blockers appear to return null for\n                    // `window.open` even inside click handlers.\n                    if (w !== null) {\n                        // try to nullify `window.opener` so the new tab can't\n                        // navigate us\n                        w.opener = null;\n                        // suppress normal link opening behaviour\n                        e.preventDefault();\n                        return false;\n                    }\n                }\n\n                this.target = '_blank';\n                // Required so the tabs can't navigate us via `window.opener`\n                this.rel = 'noreferrer';\n            }\n\n            return true; // continue bubbling\n        })\n    }\n\n    r.ui.PermissionEditor.init()\n\n    r.ui.initLiveTimestamps()\n\n    r.ui.initNewCommentHighlighting()\n\n    r.ui.initReadNext();\n\n    r.ui.initTimings();\n}\n\nr.ui.inMobileWebBlacklist = function() {\n  return _.any(r.config.mweb_blacklist_expressions, function(regex) {\n    return (new RegExp(regex)).test(window.location.pathname)\n  });\n}\n\nr.ui.isSmallScreen = function() {\n return window.matchMedia\n          // 736px is the width of the iPhone 6+.\n          ? matchMedia('(max-device-width: 736px)').matches\n          : $(window).width() < 736;\n}\n\nr.ui.TimeTextScrollListener = r.ScrollUpdater.extend({\n    initialize: function(options) {\n        this.timeText = options.timeText\n        this.timeText.updateCache($(this.selector))\n    },\n    selector: '.live-timestamp:visible',\n    endUpdate: function($els) {\n        this.timeText.updateCache($els)\n    }\n})\n\nr.ui.initLiveTimestamps = function() {\n    // We only want a global timestamp scroll listener to instantiate on\n    // pages with `thing`s. Since we don't have a router yet, we'll scope\n    // the element to `.sitetable`s, which will contain it. This is kind of a\n    // dirty hack and should be obsoleted by a router + view system.\n    if ($('.sitetable').length) {\n      var listener = new r.ui.TimeTextScrollListener({\n        el: '.sitetable',\n        timeText: new r.TimeText,\n      })\n      listener.start()\n\n      // Every time we add a new `thing`, we'll need to re-grab our element caches.\n      $(document).on('new_things_inserted', function() {\n          listener.restart()\n      })\n    }\n}\n\nr.ui.initNewCommentHighlighting = function() {\n  if (!$('body').hasClass('comments-page')) {\n    return;\n  }\n\n  $visitSelector = $('#comment-visits');\n  if ($visitSelector.length === 0) {\n    return;\n  }\n\n  $(document).on('new_things_inserted', r.ui.highlightNewComments);\n  $visitSelector.on('change', r.ui.highlightNewComments);\n  r.ui.highlightNewComments();\n}\n\nr.ui.highlightNewComments = function() {\n  var $comments = $('.comment');\n  var selectedVisitTimestamp = $('#comment-visits').val();\n  var selectedVisit;\n\n  if (selectedVisitTimestamp) {\n    selectedVisit = Date.parse(selectedVisitTimestamp);\n  }\n\n  $comments.each(function() {\n    var $commentEl = $(this);\n    var $timeEl = $commentEl.find('> .entry .tagline time:first-of-type');\n    var commentTime = r.utils.parseTimestamp($timeEl);\n    var shouldHighlight = !!selectedVisit && commentTime > selectedVisit;\n\n    $commentEl.toggleClass('new-comment', shouldHighlight);\n  });\n}\n\nr.ui.initReadNext = function() {\n    // 2 week expiration\n    var ttl = (1000 * 60 * 60 * 24 * 14);\n    var $readNextContainer = $('.read-next-container');\n    var isDismissed = !!store.safeGet('readnext.dismissed');\n    var expiration = parseInt(store.safeGet('readnext.expiration'), 10);\n    var now = Date.now();\n\n    if (isDismissed) {\n        if (!expiration) {\n            expiration = now + ttl;\n            store.safeSet('readnext.expiration', expiration);\n        } else if (expiration < now) {\n            store.safeSet('readnext.dismissed', false);\n            isDismissed = false;\n        }\n    }\n\n    var currentLinkFullname = r.config.cur_link;\n\n    if (isDismissed || !$readNextContainer.length) {\n        return;\n    }\n\n    this.readNext = new r.ui.ReadNext({\n        el: $readNextContainer,\n        fixToBottom: !r.ui.isSmallScreen(),\n        currentLinkFullname: currentLinkFullname,\n        ttl: ttl,\n    });\n};\n\nr.ui.ReadNext = Backbone.View.extend({\n    events: {\n        'click .read-next-button.next': 'next',\n        'click .read-next-button.prev': 'prev',\n        'click .read-next-dismiss': 'dismiss',\n    },\n\n    initialize: function() {\n        this.$readNext = this.$el.find('.read-next');\n        this.$links = this.$readNext.find('.read-next-link');\n        this.numLinks = this.$links.length;\n\n        this.state = new Backbone.Model({\n            fixed: false,\n            index: -1,\n        });\n\n        this.state.on('change', this.render.bind(this));\n        \n        if (this.options.fixToBottom) {\n            this.updateScroll = this.updateScroll.bind(this);\n            window.addEventListener('scroll', this.updateScroll);\n            this.updateScroll();\n        }\n\n        var currentLinkId = '#read-next-link-' + this.options.currentLinkFullname;\n        var startingIndex = this.$links.index($(currentLinkId)) + 1;\n\n        this.state.set({\n            index: startingIndex,\n        });\n\n        this.resetRefIndicies(startingIndex);\n        this.$readNext.addClass('active');\n    },\n\n    resetRefIndicies: function(startingIndex) {\n        var a = document.createElement('a');\n\n        this.$links.toArray().forEach(function(link, i) {\n            var url = $.url(link.href);\n            var params = url.param();\n            if (!params.ref) {\n                return;\n            }\n            var relativeIndex = this.moduloIndex(i - startingIndex);\n            params.ref = params.ref.split('_')[0] + '_' + relativeIndex;\n            a.href = link.href;\n            a.search = $.param(params);\n            link.href = a.href;\n        }, this);\n    },\n\n    moduloIndex: function(i) {\n        var numLinks = this.numLinks;\n        return (i + numLinks) % numLinks;\n    },\n\n    next: function() {\n        var currentIndex = this.state.get('index');\n        this.state.set({\n            index: this.moduloIndex(currentIndex + 1),\n        });\n        r.analytics.fireGAEvent('readnext', 'nav-next');\n    },\n\n    prev: function() {\n        var currentIndex = this.state.get('index');\n        var numLinks = this.numLinks;\n        this.state.set({\n            index: this.moduloIndex(currentIndex - 1),\n        });\n        r.analytics.fireGAEvent('readnext', 'nav-prev');\n    },\n\n    dismiss: function() {\n        this.$el.fadeOut();\n        window.removeEventListener('scroll', this.updateScroll);\n        r.analytics.fireGAEvent('readnext', 'dismiss');\n        store.safeSet('readnext.dismissed', true);\n        var expiration = Date.now() + this.options.ttl;\n        store.safeSet('readnext.expiration', expiration);\n    },\n\n    updateScroll: function() {\n        var scrollPosition = window.scrollY;\n        var nodePosition = this.$el.position().top;\n\n        // stick to bottom    \n        var scrollOffset = window.innerHeight;\n        var nodeOffset = this.$readNext.height();\n        scrollPosition += scrollOffset;\n        nodePosition += nodeOffset;\n\n        this.state.set({\n            fixed: scrollPosition >= nodePosition,\n        });\n    },\n\n    render: function() {\n        var currentIndex = this.state.get('index');\n        var fixedPosition = this.state.get('fixed');\n\n        this.$links.removeClass('active');\n        this.$links.eq(currentIndex).addClass('active');\n\n        if (fixedPosition) {\n            this.$readNext.addClass('fixed');\n        } else {\n            this.$readNext.removeClass('fixed');\n        } \n    },\n});\n\n\nr.ui.initTimings = function() {\n  // return if we're not configured for sending stats\n  if (!r.config.pageInfo.actionName || !r.config.stats_domain) {\n    return\n  }\n\n  // Sample based on the configuration sample rate\n  if (Math.random() > r.config.stats_sample_rate / 100) {\n    return\n  }\n\n  var browserTimings = new r.NavigationTimings()\n\n  $(function() {\n    _.defer(function() {\n      browserTimings.fetch()\n\n      var timingData = browserTimings.filter(function(t) {\n        return t.get('key') !== 'start'\n      }).reduce(function(o, t) {\n        if (!t.isValid()) { return o }\n\n        var val = t.duration()\n\n        if (val > 0) {\n        // Add 'Timing' because some of these keys clobber globals in pylons\n          var key = t.get('key') + 'Timing'\n          o[key] = val\n        }\n\n        return o\n      }, {})\n\n      timingData.actionName = r.config.pageInfo.actionName\n      timingData.verification = r.config.pageInfo.verification\n\n      $.ajax({\n        type: 'POST',\n        url: r.config.stats_domain,\n        data: JSON.stringify({ rum: timingData  }),\n        contentType: 'application/json; charset=utf-8',\n        dataType: 'json',\n      })\n    })\n  })\n}\n\nr.ui.showWorkingDeferred = function(el, deferred) {\n    if (!deferred) {\n        return\n    }\n\n    var flickerDelay = 200,\n        key = '_workingCount',\n        $el = $(el)\n\n    // keep a count of active calls on this element so we can track multiple\n    // deferreds at the same time.\n    $el.data(key, ($el.data(key) || 0) + 1)\n\n    // prevent flicker\n    var flickerTimeout = setTimeout(function() {\n        $el.addClass('working')\n    }, flickerDelay)\n\n    deferred.always(function() {\n        clearTimeout(flickerTimeout)\n        var count = Math.max(0, $el.data(key) - 1)\n        $el.data(key, count)\n        if (count == 0) {\n            $el.removeClass('working')\n        }\n    })\n\n    return deferred\n}\n\nr.ui.refreshListing = function() {\n    var url = $.url(),\n        params = url.param()\n    params['bare'] = 'y'\n    return $.ajax({\n        type: 'GET',\n        url: url.attr('base') + url.attr('path'),\n        data: params\n    }).done(function(resp) {\n        $('body > .content')\n            .html(resp)\n            .find('.promotedlink.promoted:visible')\n                .trigger('onshow')\n    })\n}\n\nr.ui.Form = function(el) {\n    r.ui.Base.call(this, el)\n    this.$el.submit($.proxy(function(e) {\n        e.preventDefault()\n        this.submit(e)\n    }, this))\n\n    this.$el.find('[data-validate-url]')\n        .validator({ https: !!r.config.https_endpoint })\n        .on('initialize.validator', function(e) {\n            var $el = $(this);\n\n            if ($el.hasClass('c-has-error')) {\n                $el.stateify('showError');\n            }\n        })\n        .on('valid.validator', function(e) {\n            $(this).stateify('set', 'success');\n        })\n        .on('invalid.validator', function(e, resp) {\n            // resp may not always be set if client side validation triggered, like\n            // from input type=email\n            if (resp) {\n              var error = r.utils.parseError(resp.errors[0]);\n\n              $(this).stateify('set', 'error', error.message);\n            }\n        })\n        .on('loading.validator', function(e) {\n            $(this).stateify('set', 'loading');\n        })\n        .on('cleared.validator', function(e) {\n            $(this).stateify('clear');\n        });\n}\nr.ui.Form.prototype = $.extend(new r.ui.Base(), {\n    showStatus: function(msg, isError) {\n        this.$el.find('.status, .c-alert')\n            .show()\n            .toggleClass('error', !!isError)\n            .text(msg)\n    },\n\n    showErrors: function(errors) {\n        var messages = [];\n\n        $.each(errors, $.proxy(function(i, err) {\n            var obj = r.utils.parseError(err);\n            var $el = this.$el.find('.error.' + obj.name + (obj.field ? '.field-' + obj.field : ''));\n            var $v2el = this.$el.filter('.form-v2').find('[name=\"' + obj.field + '\"]');\n\n            if ($el.length) {\n                $el.show().text(obj.message);\n            } else if ($v2el.length) {\n                $v2el.stateify('set', 'error', obj.message);\n            } else {\n                messages.push(obj.message);\n            }\n        }, this))\n\n        if (messages.length) {\n            this.showStatus(messages.join(', '), true);\n        }\n    },\n\n    resetErrors: function() {\n        this.$el.find('.error').hide()\n    },\n\n    checkCaptcha: function(errors) {\n        if (this.$el.has('input[name=\"captcha\"]').length) {\n            var badCaptcha = $.grep(errors, function(el) {\n                return el[0] == 'badCaptcha'\n            })\n            if (badCaptcha) {\n                $.request(\"new_captcha\", {id: this.$el.attr('id')})\n            }\n        }\n    },\n\n    serialize: function() {\n        return this.$el.serializeArray()\n    },\n\n    submit: function() {\n        this.resetErrors()\n        r.ui.showWorkingDeferred(this.$el, this._submit())\n            .done($.proxy(this, 'handleResult'))\n            .fail($.proxy(this, '_handleNetError'))\n    },\n\n    _submit: function() {},\n\n    handleResult: function(result) {\n        this.checkCaptcha(result.json.errors)\n        this._handleResult(result)\n    },\n\n    _handleResult: function(result) {\n        this.showErrors(result.json.errors)\n    },\n\n    _handleNetError: function(xhr) {\n        var message = r._('an error occurred (status: %(status)s)')\n                         .format({status: xhr.status})\n        this.showStatus(message, true)\n    }\n})\n\nr.ui.Bubble = Backbone.View.extend({\n    showDelay: 150,\n    hideDelay: 750,\n    animateDuration: 150,\n\n    initialize: function() {\n        this.$parent = this.options.parent || this.$el.parent()\n        if (this.options.trackHover != false) {\n            this.$el.hover($.proxy(this, 'queueShow'), $.proxy(this, 'queueHide'))\n            this.$parent.hover($.proxy(this, 'queueShow'), $.proxy(this, 'queueHide'))\n            this.$parent.click($.proxy(this, 'queueShow'))\n        }\n    },\n\n    position: function() {\n        var parentPos = this.$parent.offset(),\n            bodyOffset = $('body').offset(),\n            offsetX, offsetY\n        if (this.$el.is('.anchor-top') || this.$el.is('.anchor-top-centered')) {\n            offsetX = this.$parent.outerWidth(true) - this.$el.outerWidth(true)\n            offsetY = this.$parent.outerHeight(true) + 5\n            this.$el.css({\n                left: Math.max(parentPos.left + offsetX, 0),\n                top: parentPos.top + offsetY - bodyOffset.top\n            })\n        } else if (this.$el.is('.anchor-top-left')) {\n            offsetY = this.$parent.outerHeight(true) + 5\n            this.$el.css({\n                left: parentPos.left,\n                top: parentPos.top + offsetY - bodyOffset.top\n            })\n        } else if (this.$el.is('.anchor-right-fixed')) {\n            offsetX = 32\n            offsetY = 0\n\n            parentPos.top -= $(document).scrollTop()\n            parentPos.left -= $(document).scrollLeft()\n\n            this.$el.css({\n                top: r.utils.clamp(parentPos.top - offsetY, 0, $(window).height() - this.$el.outerHeight()),\n                left: r.utils.clamp(parentPos.left - offsetX - this.$el.width(), 0, $(window).width())\n            })\n        } else if (this.$el.is('.anchor-left')) {\n            offsetX = this.$parent.outerWidth(true) + 16\n            offsetY = 0\n            this.$el.css({\n                left: parentPos.left + offsetX,\n                top: parentPos.top + offsetY - bodyOffset.top\n            })\n        }  else { // anchor-right\n            offsetX = 16\n            offsetY = 0\n            parentPos.right = $(window).width() - parentPos.left\n            this.$el.css({\n                right: parentPos.right + offsetX,\n                top: parentPos.top + offsetY - bodyOffset.top\n            })\n        }\n    },\n\n    show: function() {\n        this.cancelTimeout()\n        if (this.$el.is(':visible')) {\n            return\n        }\n\n        this.trigger('show')\n\n        $('body').append(this.$el)\n\n        this.$el.css('visibility', 'hidden').show()\n        this.render()\n        this.position()\n        this.$el.css({\n            'opacity': 1,\n            'visibility': 'visible'\n        })\n\n        var isSwitch = this.options.group && this.options.group.current && this.options.group.current != this\n        if (isSwitch) {\n            this.options.group.current.hideNow()\n        } else {\n            this._animate('show')\n        }\n\n        if (this.options.group) {\n            this.options.group.current = this\n        }\n    },\n\n    hideNow: function() {\n        this.cancelTimeout()\n        if (this.options.group && this.options.group.current == this) {\n            this.options.group.current = null\n        }\n        this.$el.hide()\n    },\n\n    hide: function(callback) {\n        if (!this.$el.is(':visible')) {\n            callback && callback()\n            return\n        }\n\n        this._animate('hide', $.proxy(function() {\n            this.hideNow()\n            callback && callback()\n        }, this))\n    },\n\n    _animate: function(action, callback) {\n        if (!this.animateDuration) {\n            callback && callback()\n            return\n        }\n\n        var animProp, animOffset\n        if (this.$el.is('.anchor-top') || this.$el.is('.anchor-top-centered') || this.$el.is('.anchor-top-left')) {\n            animProp = 'top'\n            animOffset = '-=5'\n        } else if (this.$el.is('.anchor-right-fixed')) {\n            animProp = 'right'\n            animOffset = '-=5'\n        } else if (this.$el.is('.anchor-left')) {\n            animProp = 'left'\n            animOffset = '+=5'\n        } else { // anchor-right\n            animProp = 'right'\n            animOffset = '-=5'\n        }\n        var curOffset = this.$el.css(animProp)\n\n        hideProps = {'opacity': 0}\n        hideProps[animProp] = animOffset\n        showProps = {'opacity': 1}\n        showProps[animProp] = curOffset\n\n        var start, end\n        if (action == 'show') {\n            start = hideProps\n            end = showProps\n        } else if (action == 'hide') {\n            start = showProps\n            end = hideProps\n        }\n\n        this.$el\n            .css(start)\n            .animate(end, this.animateDuration, callback)\n    },\n\n    cancelTimeout: function() {\n        if (this.timeout) {\n            clearTimeout(this.timeout)\n            this.timeout = null\n        }\n    },\n\n    queueShow: function() {\n        this.cancelTimeout()\n        this.timeout = setTimeout($.proxy(this, 'show'), this.showDelay)\n    },\n\n    queueHide: function() {\n        this.cancelTimeout()\n        this.timeout = setTimeout($.proxy(this, 'hide'), this.hideDelay)\n    }\n})\n\nr.ui.PermissionEditor = function(el) {\n    r.ui.Base.call(this, el)\n    var params = {}\n    this.$el.find('input[type=\"hidden\"]').each(function(idx, el) {\n        params[el.name] = el.value\n    })\n    var permission_type = params.type\n    var name = params.name\n    this.form_id = permission_type + \"-permissions-\" + name\n    this.permission_info = r.permissions[permission_type]\n    this.sorted_perm_keys = $.map(this.permission_info,\n                                  function(v, k) { return k })\n    this.sorted_perm_keys.sort()\n    this.original_perms = this._parsePerms(params.permissions)\n    this.embedded = this.$el.find(\"form\").length == 0\n    this.$menu = null\n    if (this.embedded) {\n        this.$permissions_field = this.$el.find('input[name=\"permissions\"]')\n        this.$menu_controller = this.$el.siblings('.permissions-edit')\n    } else {\n        this.$menu_controller = this.$el.closest('tr').find('.permissions-edit')\n    }\n    this.$menu_controller.find('a').click($.proxy(this, 'show'))\n    this.updateSummary()\n}\nr.ui.PermissionEditor.init = function() {\n    function activate(target) {\n        $(target).find('.permissions').each(function(idx, el) {\n            $(el).data('PermissionEditor', new r.ui.PermissionEditor(el))\n        })\n    }\n    activate('body')\n    for (var permission_type in r.permissions) {\n        $('.' + permission_type + '-table')\n            .on('insert-row', 'tr', function(e) { activate(this) })\n    }\n}\nr.ui.PermissionEditor.prototype = $.extend(new r.ui.Base(), {\n    _parsePerms: function(permspec) {\n        var perms = {}\n        permspec.split(\",\").forEach(function(str) {\n            perms[str.substring(1)] = str[0] == \"+\"\n        })\n        return perms.all ? {\"all\": true} : perms\n    },\n\n    _serializePerms: function(perms) {\n        if (perms.all) {\n            return \"+all\"\n        } else {\n            var parts = []\n            for (var perm in perms) {\n                parts.push((perms[perm] ? \"+\" : \"-\") + perm)\n            }\n            return parts.join(\",\")\n        }\n    },\n\n    _getNewPerms: function() {\n        if (!this.$menu) {\n            return null\n        }\n        var perms = {}\n        this.$menu.find('input[type=\"checkbox\"]').each(function(idx, el) {\n            perms[$(el).attr(\"name\")] = $(el).prop(\"checked\")\n        })\n        return perms\n    },\n\n    _makeMenuLabel: function(perm) {\n        var update = $.proxy(this, \"updateSummary\")\n        var info = this.permission_info[perm]\n        var $input = $('<input type=\"checkbox\">')\n            .attr(\"name\", perm)\n            .prop(\"checked\", this.original_perms[perm])\n        var $label = $('<label>')\n            .append($input)\n            .click(function(e) { e.stopPropagation() })\n        if (perm == \"all\") {\n            $input.change(function() {\n                var disabled = $input.is(\":checked\")\n                $label.siblings()\n                    .toggleClass(\"disabled\", disabled)\n                    .find('input[type=\"checkbox\"]').prop(\"disabled\", disabled)\n                update()\n            })\n            $label.append(\n                document.createTextNode(r._('full permissions')))\n        } else if (info) {\n            $input.change(update)\n            $label.append(document.createTextNode(r._(info.title)))\n            $label.attr(\"title\", r._(info.description))\n        }\n        return $label\n    },\n\n    show: function(e) {\n        if (r.access.isLinkRestricted(e.target)) {\n            return;\n        }\n\n        close_menus(e)\n        this.$menu = $('<div class=\"permission-selector drop-choices\">')\n        this.$menu.append(this._makeMenuLabel(\"all\"))\n        for (var i in this.sorted_perm_keys) {\n            this.$menu.append(this._makeMenuLabel(this.sorted_perm_keys[i]))\n        }\n\n        this.$menu\n            .on(\"close_menu\", $.proxy(this, \"hide\"))\n            .find(\"input\").first().change().end()\n        if (!this.embedded) {\n            var $form = this.$el.find(\"form\").clone()\n            $form.attr(\"id\", this.form_id)\n            $form.click(function(e) { e.stopPropagation() })\n            this.$menu.append('<hr>', $form)\n            this.$permissions_field =\n                this.$menu.find('input[name=\"permissions\"]')\n        }\n        this.$menu_controller.parent().append(this.$menu)\n        open_menu(this.$menu_controller[0])\n        return false\n    },\n\n    hide: function() {\n        if (this.$menu) {\n            if (this.embedded) {\n                this.original_perms = this._getNewPerms()\n                this.$permissions_field\n                    .val(this._serializePerms(this.original_perms))\n            }\n            this.$menu.remove()\n            this.$menu = null\n            this.updateSummary()\n        }\n    },\n\n    _renderBit: function(perm) {\n        var info = this.permission_info[perm]\n        var text\n        if (perm == \"all\") {\n            text = r._(\"full permissions\")\n        } else if (info) {\n            text = r._(info.title)\n        } else {\n            text = perm\n        }\n        var $span = $('<span class=\"permission-bit\"/>').text(text)\n        if (info) {\n            $span.attr(\"title\", r._(info.description))\n        }\n        return $span\n    },\n\n    updateSummary: function() {\n        var new_perms = this._getNewPerms()\n        var spans = []\n        if (new_perms && new_perms.all) {\n            spans.push(this._renderBit(\"all\")\n                .toggleClass(\"added\", this.original_perms.all != true))\n        } else {\n            if (this.original_perms.all && !new_perms) {\n                spans.push(this._renderBit(\"all\"))\n            } else if (!this.original_perms.all) {\n                for (var perm in this.original_perms) {\n                    if (this.original_perms[perm]) {\n                        if (this.embedded && !(new_perms && !new_perms[perm])) {\n                            spans.push(this._renderBit(perm))\n                        }\n                        if (!this.embedded) {\n                            spans.push(this._renderBit(perm)\n                                .toggleClass(\"removed\",\n                                             new_perms != null\n                                             && !new_perms[perm]))\n                        }\n                    }\n                }\n            }\n            if (new_perms) {\n                for (var perm in new_perms) {\n                    if (this.permission_info[perm] && new_perms[perm]\n                        && !this.original_perms[perm]) {\n                        spans.push(this._renderBit(perm)\n                            .toggleClass(\"added\", !this.embedded))\n                    }\n                }\n            }\n        }\n        if (!spans.length) {\n            spans.push($('<span class=\"permission-bit\">')\n                .text(r._('no permissions'))\n                .addClass(\"none\"))\n        }\n        var $new_summary = $('<div class=\"permission-summary\">')\n        for (var i = 0; i < spans.length; i++) {\n            if (i > 0) {\n                $new_summary.append(\", \")\n            }\n            $new_summary.append(spans[i])\n        }\n        $new_summary.toggleClass(\"edited\", this.$menu != null)\n        this.$el.find(\".permission-summary\").replaceWith($new_summary)\n\n        if (new_perms && this.$permissions_field) {\n            this.$permissions_field.val(this._serializePerms(new_perms))\n        }\n    },\n\n    onCommit: function(perms) {\n        this.$el.find('input[name=\"permissions\"]').val(perms)\n        this.original_perms = this._parsePerms(perms)\n        this.hide()\n    }\n})\n\nr.ui.scrollFixed = function(el) {\n    this.$el = $(el)\n    this.$standin = null\n    this.onScroll()\n    $(window).bind('scroll resize', _.bind(_.throttle(this.onScroll, 20), this))\n}\nr.ui.scrollFixed.prototype = {\n    onScroll: function() {\n        if (!this.$el.is('.scroll-fixed')) {\n            var margin = this.$el.outerHeight(true) - this.$el.outerHeight(false)\n            this.origTop = this.$el.offset().top - margin\n        }\n\n        var enoughSpace = this.$el.height() < $(window).height()\n        if (enoughSpace && $(window).scrollTop() > this.origTop) {\n            if (!this.$standin) {\n                this.$standin = $('<' + this.$el.prop('nodeName') + '>')\n                    .css({\n                        width: this.$el.width(),\n                        height: this.$el.height()\n                    })\n                    .attr('class', this.$el.attr('class'))\n                    .addClass('scroll-fixed-standin')\n\n                this.$el\n                    .addClass('scroll-fixed')\n                    .css({\n                        position: 'fixed',\n                        top: 0\n                    })\n                this.$el.before(this.$standin)\n            }\n        } else {\n            if (this.$standin) {\n                this.$el\n                    .removeClass('scroll-fixed')\n                    .css({\n                        position: '',\n                        top: ''\n                    })\n                this.$standin.remove()\n                this.$standin = null\n            }\n        }\n    }\n}\n\nr.ui.ConfirmButton = Backbone.View.extend({\n    confirmTemplate: _.template('<span class=\"confirmation\"><span class=\"prompt\"><%- are_you_sure %></span><button class=\"yes\"><%- yes %></button> / <button class=\"no\"><%- no %></button></div>'),\n    events: {\n        'click': 'click'\n    },\n\n    initialize: function() {\n        // wrap the specified element in a <span> and move its classes over to\n        // the wrapper. this is intended for progressive enhancement of a bare\n        // <button> element.\n        this.$target = this.$el\n        this.$target.wrap('<span>')\n        this.setElement(this.$target.parent())\n        this.$el\n            .attr('class', this.$target.attr('class'))\n            .addClass('confirm-button')\n        this.$target.attr('class', null)\n    },\n\n    click: function(ev) {\n        var target = $(ev.target)\n        if (this.$target.is(target)) {\n            this.$target.hide()\n            this.$el.append(this.confirmTemplate({\n                are_you_sure: r._('are you sure?'),\n                yes: r._('yes'),\n                no: r._('no')\n            }))\n        } else if (target.is('.no')) {\n            this.$('.confirmation').remove()\n            this.$target.show()\n        } else if (target.is('.yes')) {\n            this.$target.trigger('confirm')\n        }\n    }\n})\n\nr.ui.SubredditSubmitText = Backbone.View.extend({\n    initialize: function() {\n        this.lookup = _.throttle(this._lookup, 500)\n        this.cache = new r.utils.LRUCache()\n        this.$input = $('#sr-autocomplete')\n        this.$input.on('sr-changed change input', _.bind(this.lookup, this))\n        this.$sr = this.$el.find('.sr').first()\n        this.$content = this.$el.find('.content').first()\n        if (this.$content.text().trim()) {\n            this.$sr.text(r.config.post_site)\n            this.show()\n        }\n    },\n\n    _lookup: function() {\n        this.$content.empty()\n        var sr = this.$input.val()\n        this.$sr.text(sr)\n        this.$el.addClass('working')\n        if (this.req && this.req.abort) {\n            this.req.abort()\n        }\n        this.req = this.cache.ajax(sr, {\n            url: '/r/' + sr + '/api/submit_text/.json',\n            dataType: 'json'\n        }).done(_.bind(this.settext, this, sr))\n          .fail(_.bind(this.error, this))\n    },\n\n    show: function() {\n        this.$el.addClass('enabled')\n    },\n\n    hide: function() {\n        this.$el.removeClass('enabled')\n    },\n\n    error: function() {\n        delete this.req\n        this.hide()\n    },\n\n    settext: function(sr, data) {\n        delete this.req\n        if (!data.submit_text || !data.submit_text.trim()) {\n            this.hide()\n        } else {\n            this.$sr.text(sr)\n            this.$content.html($.unsafe(data.submit_text_html))\n            this.$el.removeClass('working')\n            this.show()\n        }\n    }\n})\n\nr.ui.TextCounter = Backbone.View.extend({\n    events: {\n      'input input': 'onInput',\n      'input textarea': 'onInput',\n    },\n\n    initialize: function(options) {\n      this.error = false;\n      this.maxLength = options.maxLength;\n      this.$counterDisplay = this.$el.find('.text-counter-display');\n      this.$counterParent = this.$counterDisplay.parent();\n      this.$input = this.$el.find('.text-counter-input');\n      this.update(options.initialText || '');\n    },\n\n    onInput: function(e) {\n      this.update(e.target.value);\n    },\n\n    update: function(inputText) {\n      var remaining = this.maxLength - inputText.length;\n\n      this.$counterDisplay.text(remaining);\n\n      if (remaining < 0 && !this.error) {\n        this.$counterParent.addClass('has-error');\n        this.error = true;\n        this.trigger('invalid');\n      } else if (remaining >= 0 && this.error) {\n        this.$counterParent.removeClass('has-error');\n        this.error = false;\n        this.trigger('valid');\n      }\n    },\n  });\n"
  },
  {
    "path": "r2/r2/public/static/js/uibase.js",
    "content": "r.ui = {}\n\nr.ui.Base = function(el) {\n    this.$el = $(el)\n}\n\nr.ui.collapsibleSideBox = function(id) {\n    var $el = $('#'+id)\n    return new r.ui.Collapse($el.find('.title'), $el.find('.content'), id)\n}\n\nr.ui.Collapse = function(el, target, key) {\n    r.ui.Base.call(this, el)\n    this.target = target\n    this.key = 'ui.collapse.' + key\n    this.isCollapsed = store.safeGet(this.key) == true\n    this.$el.click($.proxy(this, 'toggle', null, false))\n    this.toggle(this.isCollapsed, true)\n}\nr.ui.Collapse.prototype = {\n    animDuration: 200,\n\n    toggle: function(collapsed, immediate) {\n        if (collapsed == null) {\n            collapsed = !this.isCollapsed\n        }\n\n        var duration = immediate ? 0 : this.animDuration\n        if (collapsed) {\n            $(this.target).slideUp(duration)\n        } else {\n            $(this.target).slideDown(duration)\n        }\n\n        this.isCollapsed = collapsed\n        store.safeSet(this.key, collapsed)\n        this.update()\n    },\n\n    update: function() {\n        this.$el.find('.collapse-button').text(this.isCollapsed ? '+' : '-')\n    }\n}\n\nr.ui.Summarize = function(el, maxCount) {\n    r.ui.Base.call(this, el)\n    this.maxCount = maxCount\n\n    this._updateItems()\n    if (this.$hiddenItems.length > 0) {\n        this.$toggleButton = $('<button class=\"expand-summary\">')\n            .click($.proxy(this, '_toggle'))\n        this.$el.after(this.$toggleButton)\n        this._summarize()\n    }\n}\nr.ui.Summarize.prototype = {\n    _updateItems: function() {\n        var $important = this.$el.children('.important'),\n            $unimportant = this.$el.children(':not(.important)'),\n            unimportantToShow = this.maxCount\n                                ? Math.max(0, this.maxCount - $important.length)\n                                : 0,\n            $unimportantToShow = $unimportant.slice(0, unimportantToShow - 1)\n\n        this.$summaryItems = $important.add($unimportantToShow)\n        this.$hiddenItems = $unimportant.slice(unimportantToShow)\n    },\n\n    _summarize: function() {\n        this.$el.addClass('summarized')\n        this.$hiddenItems.hide()\n\n        this.$toggleButton.text(r._('… and %(count)s more ⇒').format({\n            count: this.$hiddenItems.length\n        }))\n    },\n\n    _expand: function() {\n        this.$el.removeClass('summarized')\n        this.$hiddenItems.show()\n        this.$toggleButton.text(r._('⇐ less'))\n    },\n\n    _toggle: function(e) {\n        if (this.$el.hasClass('summarized')) {\n            this._expand()\n        } else {\n            this._summarize()\n        }\n        e.preventDefault()\n    }\n}\n"
  },
  {
    "path": "r2/r2/public/static/js/utils.js",
    "content": "r.utils = {\n\n    /**\n     * update the given url's query params\n     * @param  {String} url\n     * @param  {Object} newParams\n     * @return {String}\n     */\n    replaceUrlParams: function(url, newParams) {\n      var a = document.createElement('a');\n      var urlObj = $.url(url);\n      var params = urlObj.param();\n\n      Object.keys(newParams).forEach(function(key) {\n        params[key] = newParams[key]\n      });\n\n      a.href = url;\n      a.search = $.param(params);\n      return a.href;\n    },\n\n    // Returns human readable file sizes\n    // http://stackoverflow.com/a/25613067/704286\n    formatFileSize: function(size) {\n      var suffixes = ['bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'EiB', 'ZiB'];\n      var order = size ? parseInt(Math.log2(size) / 10, 10) : 0;\n\n      return (size / (1 << (order * 10))).toFixed(3).replace(/\\.?0+$/, '') + ' ' + suffixes[order];\n    },\n\n    fullnameToId: function(fullname) {\n        var parts = fullname.split('_');\n        var id36 = parts && parts[1];\n\n        return id36 && parseInt(id36, 36);\n    },\n\n    escapeSelector: function(str) {\n        return str.replace(/([ #;?%&,.+*~\\':\"!^$[\\]()=>|\\/@])/g,'\\\\$1');\n    },\n\n    clamp: function(val, min, max) {\n        return Math.max(min, Math.min(max, val))\n    },\n\n    staticURL: function (item) {\n        return r.config.static_root + '/' + item\n    },\n\n    s3HTTPS: function(url) {\n        if (location.protocol == 'https:') {\n            return url.replace('http://', 'https://s3.amazonaws.com/')\n        } else {\n            return url\n        }\n    },\n\n    parseTimestamp: function($el) {\n      var timestamp = $el.data('timestamp')\n      var isoTimestamp\n\n      if (!timestamp) {\n        isoTimestamp = $el.attr('datetime')\n        timestamp = Date.parse(isoTimestamp)\n        $el.data('timestamp', timestamp)\n      }\n\n      return timestamp\n    },\n\n    joinURLs: function(/* arguments */) {\n        return _.map(arguments, function(url, idx) {\n            if (idx > 0 && url && url[0] != '/') {\n                url = '/' + url\n            }\n            return url\n        }).join('')\n    },\n\n    _scOn: \"<!-- SC_ON -->\",\n    _scOff: \"<!-- SC_OFF -->\",\n    _scBetweenTags1: />\\s+/g,\n    _scBetweenTags2: /\\s+</g,\n    _scSpaces: /\\s+/g,\n    _scDirectives: /(<!-- SC_ON -->|<!-- SC_OFF -->)/,\n    spaceCompress: function (content) {\n        var res = '';\n        var compressionOn = true;\n        var splitContent = content.split(this._scDirectives);\n        for (var i=0; i<splitContent.length; ++i) {\n            var part = splitContent[i];\n            if (part === this._scOn) {\n                compressionOn = true;\n            } else if (part === this._scOff) {\n                compressionOn = false;\n            } else if (compressionOn) {\n                part = part.replace(this._scSpaces, ' ');\n                part = part.replace(this._scBetweenTags1, '>');\n                part = part.replace(this._scBetweenTags2, '<');\n                res += part;\n            } else {\n                res += part;\n            }\n        }\n        return res;\n    },\n\n    tup: function(list) {\n        if (!_.isArray(list)) {\n            list = [list]\n        }\n        return list\n    },\n\n    structuredMap: function(obj, func) {\n        if (_.isArray(obj)) {\n            return _.map(obj, function(value) {\n                return r.utils.structuredMap(value, func)\n            })\n        } else if (_.isObject(obj)) {\n            var mapped = {}\n            _.each(obj, function(value, key) {\n                mapped[func(key, 'key')] = r.utils.structuredMap(value, func)\n            })\n            return mapped\n        } else {\n            return func(obj, 'value')\n        }\n    },\n\n  unescapeJson: function(json) {\n    return r.utils.structuredMap(json, function(val) {\n      if (_.isString(val)) {\n        return _.unescape(val)\n      } else {\n        return val\n      }\n    })\n  },\n\n    querySelectorFromEl: function(targetEl, selector) {\n        return $(targetEl).parents().addBack()\n            .filter(selector || '*')\n            .map(function(idx, el) {\n                var parts = [],\n                    $el = $(el),\n                    elFullname = $el.data('fullname'),\n                    elId = $el.attr('id'),\n                    elClass = $el.attr('class')\n\n                parts.push(el.nodeName.toLowerCase())\n\n                if (elFullname) {\n                    parts.push('[data-fullname=\"' + elFullname + '\"]')\n                } else {\n                    if (elId) {\n                        parts.push('#' + elId)\n                    } else if (elClass) {\n                        parts.push('.' + _.compact(elClass.split(/\\s+/)).join('.'))\n                    }\n                }\n\n                return parts.join('')\n            })\n            .toArray().join(' ')\n    },\n\n    serializeForm: function(form) {\n        var params = {}\n        $.each(form.serializeArray(), function(index, value) {\n            params[value.name] = value.value\n        })\n        return params\n    },\n\n    _pyStrFormatRe: /%\\((\\w+)\\)s/g,\n    pyStrFormat: function(format, params) {\n        return format.replace(this._pyStrFormatRe, function(match, fieldName) {\n            if (!(fieldName in params)) {\n                throw 'missing format parameter'\n            }\n            return params[fieldName]\n        })\n    },\n\n    _mdLinkRe: /\\[(.*?)\\]\\((.*?)\\)/g,\n    formatMarkdownLinks: function(str) {\n        return _.escape(str).replace(this._mdLinkRe, function(match, text, url) {\n            return '<a href=\"' + url + '\">' + text + '</a>'\n        })\n    },\n\n    prettyNumber: function(number) {\n        // Add commas to separate every third digit\n        var numberAsInt = parseInt(number)\n        if (numberAsInt) {\n            return numberAsInt.toString().replace(/\\B(?=(\\d{3})+(?!\\d))/g, \",\")\n        } else {\n            return number\n\t\t}\n\t},\n\n    LRUCache: function(maxItems) {\n        var _maxItems = maxItems > 0 ? maxItems : 16\n        var _cacheIndex = []\n        var _cache = {}\n\n        var _updateIndex = function(key) {\n            _deleteFromIndex(key)\n            _cacheIndex.push(key)\n            if (_cacheIndex.length > _maxItems) {\n                delete _cache[_cacheIndex.shift()]\n            }\n        }\n\n        var _deleteFromIndex = function(key) {\n            var index = _.indexOf(_cacheIndex, key)\n            if (index >= 0) {\n                _cacheIndex.splice(index, 1)\n            }\n        }\n\n        this.remove = function(key) {\n            _deleteFromIndex(key)\n            delete _cache[key]\n        }\n\n        this.set = function(key, data) {\n            if (_.isUndefined(data)) {\n                this.remove(key)\n            } else {\n                _cache[key] = data\n                _updateIndex(key)\n            }\n        }\n\n        this.get = function(key) {\n            var value = _cache[key]\n            if (!_.isUndefined(value)) {\n                _updateIndex(key)\n            }\n            return value\n        }\n\n        this.ajax = function(key, options) {\n            var cached = this.get(key)\n            if (!_.isUndefined(cached)) {\n                return (new $.Deferred()).resolve(cached)\n            } else {\n                return $.ajax(options).done(_.bind(this.set, this, key))\n            }\n        }\n    },\n\n    parseError: function(error) {\n        var name = error[0];\n        var message = error[1];\n        var field = error[2];\n\n        return {\n            name: name,\n            message: message,\n            field: field,\n        }\n    },\n\n    onTrident: function() {\n        return 'ActiveXObject' in window;\n    },\n\n}\n\n// Nothing is true. Everything is permitted.\nString.prototype.format = function (params) {\n    return r.utils.pyStrFormat(this, params)\n}\n"
  },
  {
    "path": "r2/r2/public/static/js/uuid.js",
    "content": "!function(r) {\n  // http://stackoverflow.com/a/8809472/704286\n  r.uuid = function() {\n    var d = new Date().getTime();\n    return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {\n      var r = (d + Math.random() * 16) % 16 | 0;\n\n      d = Math.floor(d / 16);\n\n      return (c == 'x' ? r : (r & 0x3 | 0x8)).toString(16);\n    });\n  };\n}(r);\n"
  },
  {
    "path": "r2/r2/public/static/js/validator.js",
    "content": ";(function($) {\n  'use strict';\n\n  var Validator = function(element, options) {\n    this.initialize(element, options);\n  };\n\n  Validator.DEFAULTS = {\n    delay: 600,\n    loadingTimeout: 250,\n    https: false,\n  };\n\n  _.extend(Validator.prototype, {\n\n    _loadingTimeout: false,\n\n    initialize: function(element, options) {\n      var $el = this.$el = $(element);\n      var events = $el.data('validate-on') || 'keyup change blur';\n\n      this.options = $.extend({}, Validator.DEFAULTS, options);\n\n      $el.on(events, _.debounce($.proxy(this._validate, this), this.options.delay));\n\n      $el.trigger('initialize.validator');\n\n      return this;\n    },\n\n    _validate: function(e) {\n      // Don't validate on enter/tab key\n      if (e.keyCode === 9 || e.keyCode === 13) {\n        return;\n      }\n\n      // Don't validate individual field on blur when submit is pressed.\n      if (e.type === 'blur' &&\n          e.relatedTarget && $(e.relatedTarget).is('[type=submit]')) {\n        return;\n      }\n\n      var _this = this;\n      var $el = this.$el;\n      var min = $el.data('validate-min');\n      var value = $el.val();\n\n      // Don't validate on `keyup` below the specified `min`.\n      if (e.type === 'keyup' && value.length < min) {\n        delete this._keyupValue;\n        $el.trigger('cleared.validator');\n\n        return;\n      }\n\n      // Don't validate again on `change|blur` if that value was already checked by `keyup`.\n      if (/change|blur/.test(e.type) && this._keyupValue === value) {\n        return;\n      }\n\n      // Clear validation state on empty values\n      if (/^\\s*$/.test(value) && !$el.data('validate-noclear')) {\n        $el.trigger('cleared.validator');\n        return;\n      }\n\n      var $form = $el.parents('form');\n      var url = $el.data('validate-url');\n      var validateWith = ($el.data('validate-with') || '').split(/,s*/);\n      var data = {};\n\n      if (this.options.https) {\n        var parser = document.createElement('a');\n\n        parser.href = url;\n        parser.protocol = 'https:';\n        url = parser.href;\n      }\n\n      data[$el.attr('name')] = value;\n\n      // Store the latest value checked on `keyup`.\n      if (e.type === 'keyup') {\n        this._keyupValue = value;\n      }\n\n      _.each(validateWith, function(name) {\n        data[name] = $form.find('[name=\"' + name + '\"]').val();\n      });\n\n      this._loadingTimeout = setTimeout(function() {\n        $el.trigger('loading.validator');\n      }, this.options.loadingTimeout);\n\n      if (this.xhr && this.xhr.readyState !== 4) {\n        this.xhr.abort();\n      }\n\n      this.xhr = $.ajax({\n        type: 'POST',\n        url: url,\n        data: data,\n        dataType: 'json',\n        success: function(resp, text, xhr) {\n          var json = resp && resp.json;\n\n          if (json && json.errors && json.errors.length) {\n            $el.trigger('invalid.validator', json);\n          } else {\n            $el.trigger('valid.validator');\n          }\n        },\n        error: function(xhr) {\n          // Request aborted.\n          // Do nothing.\n          if (xhr.status === 0) {\n            return;\n          }\n\n          // Can't determine validation due to a conflict (likely due to another field)\n          // Example: confirmation passwords require a valid password to already exist.\n          if (xhr.status === 409) {\n            $el.trigger('cleared.validator');\n\n            return;\n          }\n\n          // Something unknown went wrong.\n          // Mark as success and let the full submit worry about it.\n          $el.trigger('valid.validator');\n        },\n        complete: function() {\n          clearTimeout(_this._loadingTimeout);\n\n          $el.trigger('loaded.validator');\n        },\n      });\n    },\n\n  });\n\n\n  function Plugin(option /* ,args... */) {\n    var args = Array.prototype.slice.call(arguments, 1);\n\n    return this.each(function() {\n      var $el = $(this);\n      var data = $el.data('c.validator');\n      var options = typeof option === 'object' && option;\n\n      if (!data) {\n        data = new Validator(this, options);\n        $el.data('c.validator', data);\n      }\n\n      if (typeof option === 'string') {\n        data[option].apply(data, args);\n      }\n    });\n  }\n\n  $.fn.validator = Plugin;\n  $.fn.validator.Constructor = Validator;\n\n}(window.jQuery));\n"
  },
  {
    "path": "r2/r2/public/static/js/visited.js",
    "content": "r.visited = {\n    key: 'visited',\n\n    init: function() {\n        this.sendVisits = _.throttle(this._sendVisits, 100)\n        if (r.config.logged && r.config.store_visits) {\n            $('.content').on('click mousedown keydown', '.link:not(.visited) a.title, .link:not(.visited) a.thumbnail', _.bind(this.onVisit, this))\n\n            // listen for custom \"visit\" event for third-party extensions to trigger in a non-UI specific way\n            $('.content').on('visit', '.link:not(.visited)', _.bind(this.onVisit, this))\n\n            // send any pending visits\n            this.sendVisits()\n        }\n    },\n\n    onVisit: function(ev) {\n        if (ev.type === 'keydown' && ev.which !== 13) {\n            // only handle enter key presses\n            return;\n        }\n        if (ev.type === 'mousedown') {\n            // Hold out for a \"click\" event for left clicks (so we can ignore\n            // dragging,) and throw out right clicks.\n            if(ev.which === 1 || ev.which === 3) {\n                return;\n            }\n        }\n        // IE might generate a `click` event for middle click as well!\n        if (ev.type === 'click' && ev.which !== 1) {\n            return;\n        }\n\n        this.storeVisit($(ev.target).closest('.thing').data('fullname'))\n        this.sendVisits()\n    },\n\n    storeVisit: function(fullname) {\n        var fullnames = store.safeGet(this.key) || []\n        fullnames.push(fullname)\n        store.safeSet(this.key, fullnames)\n    },\n\n    _sendVisits: function() {\n        var fullnames = store.safeGet(this.key) || []\n        if (!fullnames.length) {\n            return\n        }\n\n        fullnames = _.last(_.uniq(fullnames), 100)\n\n        r.ajax({\n            type: 'POST',\n            url: '/api/store_visits',\n            data: {\n                'links': fullnames.join(',')\n            }\n        })\n\n        store.safeSet(this.key, [])\n        $.things.apply($, fullnames).addClass(\"visited\");\n    }\n}\n"
  },
  {
    "path": "r2/r2/public/static/js/voting.js",
    "content": "!function(r) {\n  var UP_CLS = \"up\";\n  var DOWN_CLS = \"down\";\n  var theFakeClick;\n  var MouseEvent = window.MouseEvent;\n  var createEvent = document.createEvent;\n\n  if (createEvent) {\n    // document.createEvent throws if not called from document;\n    createEvent = createEvent.bind(document);\n  }\n\n  try {\n    // Some browsers (e.g. IE11) throw an error here.\n    if (MouseEvent) {\n      theFakeClick = new MouseEvent('click', {bubbles: true});\n    }\n  } catch (e) {\n    // We'll handle this below as if MouseEvent doesn't exist.\n  }\n\n  try {\n    // To be on the safe side, we'll wrap this one in a try/catch as well.\n    // It needs to be in a separate try/catch because if creating theFakeClick\n    // with MouseEvent fails, we still want to fall back to trying with createEvent.\n    if (!theFakeClick && createEvent) {\n      theFakeClick = createEvent('MouseEvent');\n    }\n  } catch (e) {\n    // We'll handle this below as if createEvent doesn't exist.\n  }\n\n  if (!theFakeClick) {\n    // If no method for creating a custom mouse event exists, just use an object.\n    theFakeClick = {};\n  }\n\n  window.MouseEvent = function(type, init) {\n    return theFakeClick;\n  }\n\n  document.createEvent = function(type) {\n    if (type === 'MouseEvent' || type === 'MouseEvents') {\n      return theFakeClick;\n    } else {\n      return createEvent(type);\n    }\n  }\n\n  $(function() {\n    $(document.body).on('click', '.arrow', function vote(e) {\n      var $el = $(this);\n      \n      if (!r.config.logged || r.access.isLinkRestricted(this)) {\n        return;\n      }\n\n      if ($el.hasClass('archived')) {\n        return;\n      }\n\n      var $thing = $el.thing();\n      var id = $thing.thing_id();\n      var dir = $el.hasClass(UP_CLS) ? 1 : $el.hasClass(DOWN_CLS) ? -1 : 0;\n      var isTrusted;\n\n      if (!e || !e.originalEvent) {\n        isTrusted = false;\n      } else if (MouseEvent instanceof Function &&\n                 'isTrusted' in MouseEvent.prototype) {\n        isTrusted = e.originalEvent.isTrusted;\n      } else if (MouseEvent instanceof Function) {\n        isTrusted = (e.originalEvent instanceof MouseEvent &&\n                     e.originalEvent !== theFakeClick);\n      } else {\n        isTrusted = (e.originalEvent !== theFakeClick);\n      }\n\n      var voteData = {\n        id: id,\n        dir: dir,\n        vh: r.config.vote_hash,\n        isTrusted: isTrusted,\n      };\n\n      var rank = $thing.data('rank');\n      if (rank) {\n        voteData.rank = parseInt(rank);\n      }\n\n      $.request(\"vote\", voteData);\n      $thing.updateThing({ voted: dir });\n    });\n  });\n}(r);\n"
  },
  {
    "path": "r2/r2/public/static/js/warn-on-unload.js",
    "content": "$(function() {\n\n  r.warn_on_unload = function() {\n    /*\n     * To add a warning message to a form if the\n     * user tries to leave a page where a form is in a\n     * dirty state, add the following classes to your form:\n     *\n     * warn-on-unload - this class will prompt the user if\n     * they try to leave a page with a dirty form\n     */\n    $(window).on('beforeunload', function (e) {\n      var form = $(\"form.warn-on-unload\");\n\n      if(!$(form).length) {\n        return;\n      }\n\n      var elements = form.find(\"input[type=text],\" +\n                               \"input[type=checkbox],\" +\n                               \"input[type=url],\" +\n                               \"textarea\")\n                         .not(\":hidden\");\n\n      var isDirty = false;\n      elements.each(function() {\n\n        switch(this.type) {\n          case \"checkbox\":\n            isDirty = (this.defaultChecked !== this.checked);\n            break;\n          case \"textarea\":\n          case \"text\":\n          case \"url\":\n            isDirty = (this.defaultValue !== this.value);\n            break;\n          default:\n            return true;\n        }\n\n        if(isDirty) {\n          return false;\n        }\n\n      });\n\n      if(isDirty) {\n        return r._(\"You have unsaved changes!\");\n      }\n    });\n  };\n\n  $(\"form.warn-on-unload\").on(\"keypress\", function(e) {\n    $(window).off('beforeunload');\n    r.warn_on_unload();\n  });\n\n  // Remove beforeunload event handler if a user clears their\n  // comment, exclude the newlink form textareas\n  $(\".usertext.warn-on-unload textarea\")\n    .not(\":hidden\")\n    .not(\"form#newlink .usertext textarea\")\n    .on(\"blur\", function(e) {\n\n    if(this.defaultValue === this.value) {\n      $(window).off('beforeunload');\n    }\n  });\n\n});\n"
  },
  {
    "path": "r2/r2/public/static/js/websocket.js",
    "content": "r.WebSocket = function (url) {\n    this._url = url\n    this._connectionAttempts = 0\n\n    this.on({\n        'message:refresh': this._onRefresh,\n    }, this)\n}\n_.extend(r.WebSocket.prototype, Backbone.Events, {\n    _backoffTime: 2000,\n    _maximumRetries: 9,\n    _retryJitterAmount: 3000,\n\n    start: function () {\n        var websocketsAvailable = 'WebSocket' in window\n        if (websocketsAvailable) {\n            this._connect()\n        }\n    },\n\n    _connect: function () {\n        r.debug('websocket: connecting')\n        this.trigger('connecting')\n\n        this._connectionStart = Date.now()\n        this._socket = new WebSocket(this._url)\n        this._socket.onopen = _.bind(this._onOpen, this)\n        this._socket.onmessage = _.bind(this._onMessage, this)\n        this._socket.onclose = _.bind(this._onClose, this)\n\n        this._connectionAttempts += 1\n    },\n\n    _sendStats: function (payload) {\n      if (!r.config.stats_domain) {\n        return\n      }\n\n      $.ajax({\n        type: 'POST',\n        url: r.config.stats_domain,\n        data: JSON.stringify(payload),\n        contentType: 'application/json; charset=utf-8',\n      })\n    },\n\n    _onOpen: function (ev) {\n        r.debug('websocket: connected')\n        this.trigger('connected')\n        this._connectionAttempts = 0\n\n        this._sendStats({\n          websocketPerformance: {\n            connectionTiming: Date.now() - this._connectionStart,\n          },\n        })\n    },\n\n    _onMessage: function (ev) {\n        var parsed = JSON.parse(ev.data)\n        r.debug('websocket: received \"' + parsed.type + '\" message')\n        this.trigger('message message:' + parsed.type, parsed.payload)\n    },\n\n    _onRefresh: function () {\n        // delay a random amount to reduce thundering herd\n        var delay = Math.random() * 300 * 1000\n        setTimeout(function () { location.reload() }, delay)\n    },\n\n    _onClose: function (ev) {\n        if (this._connectionAttempts < this._maximumRetries) {\n            var baseDelay = this._backoffTime * Math.pow(2, this._connectionAttempts),\n                jitter = (Math.random() * this._retryJitterAmount) - (this._retryJitterAmount / 2),\n                delay = Math.round(baseDelay + jitter)\n            r.debug('websocket: connection lost, reconnecting in ' + delay + 'ms')\n            r.debug(\"(can't connect? Make sure you've allowed https access in your browser.)\")\n            this.trigger('reconnecting', delay)\n            setTimeout(_.bind(this._connect, this), delay)\n        } else {\n            r.debug('websocket: maximum retries exceeded. bailing out')\n            this.trigger('disconnected')\n        }\n\n        this._sendStats({\n          websocketError: {\n            error: 1,\n          },\n        })\n    }\n})\n"
  },
  {
    "path": "r2/r2/public/static/js/wiki.js",
    "content": "r.wiki = {\n    request: function(req) {\n        if (r.config.logged) {\n            req.data.uh = r.config.modhash\n        }\n        req.data.page = r.config.wiki_page\n        $.ajax(req)\n    },\n\n    baseApiUrl: function() {\n        return r.wiki.baseUrl(true)\n    },\n\n    baseUrl: function(api) {\n        var base_url = ''\n        if (api) {\n            base_url += '/api'\n        }\n        base_url += '/wiki'\n        if (!r.config.is_fake) {\n            base_url = '/r/' + r.config.post_site + base_url\n        }\n        return base_url\n    },\n\n    init: function() {\n        $('body.wiki-page').on('click', '.revision_hide', this.toggleHide)\n        $('body.wiki-page').on('click', '.revision_delete', this.toggleDelete)\n        $('body.wiki-page').on('click', '.toggle-source', this.toggleSource)\n    },\n\n    toggleSource: function(event) {\n        event.preventDefault()\n        $('.wiki-page .source').toggle('slow')\n    },\n\n    toggleDelete: function(event) {\n        event.preventDefault()\n        var $this = $(this),\n            url = r.wiki.baseApiUrl() + '/delete',\n            $this_parent = $this.parents('.revision'),\n            deleted = $this_parent.hasClass('deleted')\n        $this_parent.toggleClass('deleted')\n        r.wiki.request({\n            url: url,\n            type: 'POST',\n            dataType: 'json',\n            data: {\n                revision: $this.data('revision'),\n                deleted: !deleted\n            },\n            error: function() {\n                $this_parent.toggleClass('deleted')\n            },\n            success: function(data) {\n                if (!data.status) {\n                    $this_parent.removeClass('deleted')\n                } else {\n                    $this_parent.addClass('deleted')\n                }\n            }\n        })\n    },\n\n    toggleHide: function(event) {\n        event.preventDefault()\n\n        if (r.access.isLinkRestricted(this)) {\n            return;\n        }\n\n        var $this = $(this),\n            url = r.wiki.baseApiUrl() + '/hide',\n            $this_parent = $this.parents('.revision')\n        $this_parent.toggleClass('hidden')\n        r.wiki.request({\n            url: url,\n            type: 'POST',\n            dataType: 'json',\n            data: {\n                revision: $this.data('revision')\n            },\n            error: function() {\n                $this_parent.toggleClass('hidden')\n            },\n            success: function(data) {\n                if (!data.status) {\n                    $this_parent.removeClass('hidden')\n                } else {\n                    $this_parent.addClass('hidden')\n                }\n            }\n        })\n    },\n\n    addUser: function(event) {\n        event.preventDefault()\n        $('#usereditallowerror').hide()\n        var $this = $(event.target),\n            url = r.wiki.baseApiUrl() + '/alloweditor/add'\n        r.wiki.request({\n            url: url,\n            type: 'POST',\n            data: {\n                username: $this.find('[name=\"username\"]').val()\n            },\n            dataType: 'json',\n            error: function() {\n                $('#usereditallowerror').show()\n            },\n            success: function(data) {\n                location.reload()\n            }\n        })\n    },\n\n    submitEdit: function(event) {\n        event.preventDefault()\n        var $this = $(event.target),\n            url = r.wiki.baseApiUrl() + '/edit',\n            conflict = $('#wiki_edit_conflict'),\n            special = $('#wiki_special_error')\n        conflict.hide()\n        special.hide()\n        params = r.utils.serializeForm($this)\n        $('#wiki_save_button').attr(\"disabled\", true)\n        $this.addClass(\"working\")\n        r.wiki.request({\n            url: url,\n            type: 'POST',\n            dataType: 'json',\n            data: params,\n            error: function() {\n                $this.removeClass(\"working\")\n                $('#wiki_save_button').removeAttr(\"disabled\")\n            },\n            success: function() {\n                window.location = r.wiki.baseUrl() + '/' + r.config.wiki_page\n            },\n            statusCode: {\n                409: function(xhr) {\n                    var info = JSON.parse(xhr.responseText)\n                        ,content = $this.children('#wiki_page_content')\n                        ,diff = conflict.children('#yourdiff')\n                    conflict.children('#youredit').val(content.val())\n                    diff.html($.unsafe(info.diffcontent))\n                    $this.children('#previous').val(info.newrevision)\n                    content.val(info.newcontent)\n                    conflict.fadeIn('slow')\n                },\n                415: function(xhr) {\n                    var errors = JSON.parse(xhr.responseText).special_errors\n                        ,specials = special.children('#specials')\n                    specials.empty()\n                    for(i in errors) {\n                        specials.append($('<pre>').text($.unsafe(errors[i])))\n                    }\n                    special.fadeIn('slow')\n                },\n                429: function(xhr) {\n                    var message = JSON.parse(xhr.responseText).message\n                        ,specials = special.children('#specials')\n                    specials.empty()\n                    specials.text(message)\n                    special.fadeIn('slow')\n                }\n            }\n        })\n    },\n\n    goCompare: function() {\n        v1 = $('input:radio[name=v1]:checked').val()\n        v2 = $('input:radio[name=v2]:checked').val()\n        url = r.wiki.baseUrl() + '/' + r.config.wiki_page + '?v=' + v1\n        if (v2 != v1) {\n            url += '&v2=' + v2\n        }\n        window.location = url\n    },\n\n    helpon: function(elem) {\n        $(elem).parents(\"form\").children(\".markhelp:first\").show();\n    },\n\n    helpoff: function(elem) {\n        $(elem).parents(\"form\").children(\".markhelp:first\").hide();\n    }\n\n}\n"
  },
  {
    "path": "r2/r2/public/static/opensearch.xml",
    "content": "<?xml version=\"1.0\"?>\n<OpenSearchDescription xmlns=\"http://a9.com/-/spec/opensearch/1.1/\">\n  <ShortName>reddit search</ShortName>\n  <Description>Search for content on reddit!</Description>\n  <Url type=\"text/html\"\n    method=\"get\"\n    template=\"https://www.reddit.com/search?q={searchTerms}&amp;utm_source=opensearch\"/>\n</OpenSearchDescription>\n"
  },
  {
    "path": "r2/r2/public/static/sureroute.html",
    "content": "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\">\n<html>\n<head><title>sureroute test obect</title></head>\n<body>\n<p><strong>sureroute test object</strong></p>\n<p>SureRoute for Performance chooses the fastest path to the origin to ensure that your site is continuously accessible and that uncacheable content is delivered to end users with optimal performance</p>\n</body>\n</html>\n"
  },
  {
    "path": "r2/r2/templates/__init__.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\n"
  },
  {
    "path": "r2/r2/templates/accountactivitybox.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<div class=\"account-activity-box\">\n    <p><a href=\"/account-activity\">${_(\"account activity\")}</a></p>\n</div>\n"
  },
  {
    "path": "r2/r2/templates/adminawardgive.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%namespace file=\"utils.html\" import=\"error_field\"/>\n\n<form action=\"/post/givetrophy\" method=\"post\" class=\"pretty-form medium-text\"\n      onsubmit=\"return post_form(this, 'givetrophy');\">\n\n  <input type=\"hidden\" name=\"fullname\" value=\"${thing.award._fullname}\" />\n\n  <table class=\"lined-table borderless\">\n    <tr>\n      <td>\n        <img src=\"${thing.award.imgurl % 40}\"/>\n      </td>\n      <td>\n        <h1>${thing.award.title}</h1>\n      </td>\n    </tr>\n    <tr>\n      <td>\n        recipient\n      </td>\n      <td>\n        <input type=\"text\" name=\"recipient\" value=\"${thing.recipient}\" />\n        ${error_field(\"NO_USER\", \"recipient\", \"span\")}\n        ${error_field(\"USER_DOESNT_EXIST\", \"recipient\", \"span\")}\n      </td>\n    </tr>\n    <tr>\n      <td>\n        description / period\n      </td>\n      <td>\n        <input type=\"text\" name=\"description\" value=\"${thing.description}\" />\n      </td>\n    </tr>\n    <tr>\n      <td>\n        url\n      </td>\n      <td>\n        <input type=\"text\" name=\"url\" value=\"${thing.url}\" />\n      </td>\n    </tr>\n  </table>\n\n  <button class=\"btn\" type=\"submit\">give</button>\n\n  <span class=\"status\"></span>\n\n  <p>\n    <a href=\"/admin/awards\">back to awards</a>\n  </p>\n</form>\n\n"
  },
  {
    "path": "r2/r2/templates/adminawards.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%namespace file=\"utils.html\" import=\"error_field\"/>\n\n<%def name=\"awardbuttons(codename)\">\n  <ul class=\"flat-list buttons\">\n    <li><a href=\"#\"\n        onclick=\"$(this).parents('td').find('form').toggle(); return false;\">\n        edit</a></li>\n    <li><a href=\"/admin/awards/${codename}/give\">give</a></li>\n    <li><a href=\"/admin/awards/${codename}/winners\">winners</a></li>\n  </ul>\n</%def>\n\n<%def name=\"awardtype_radio(val, label, award_fn, current)\">\n  <input id=\"awardtype_${award_fn}_${val}\" class=\"nomargin\"\n         type=\"radio\"  value=\"${val}\" name=\"awardtype\"\n         ${\"checked='checked'\" if current == val else ''} />\n  <label for=\"awardtype_${award_fn}_${val}\">${label}</label>\n  <br/>\n</%def>\n\n<%def name=\"awardedit(fullname, title='', awardtype='', codename='', imgurl='',\n                      api_ok=False)\">\n <form action=\"/post/editaward\" method=\"post\" class=\"pretty-form medium-text\"\n       style=\"display:none\"\n       onsubmit=\"return post_form(this, 'editaward');\" id=\"awardedit-${fullname}\">\n  <input type=\"hidden\" name=\"fullname\" value=\"${fullname}\" />\n\n  <table class=\"lined-table borderless\">\n    <tr>\n      <td>codename</td>\n      <td>\n        <input type=\"text\" name=\"codename\" value=\"${codename}\" />\n        ${error_field(\"NO_TEXT\", \"codename\", \"span\")}\n        ${error_field(\"INVALID_OPTION\", \"codename\", \"span\")}\n      </td>\n    </tr>\n    <tr>\n      <td>title</td>\n      <td>\n        <input type=\"text\" name=\"title\" value=\"${title}\" />\n        ${error_field(\"NO_TEXT\", \"title\", \"span\")}\n      </td>\n    </tr>\n    <tr>\n      <td>type</td>\n      <td>\n        ${awardtype_radio(\"regular\", \"regular\", fullname, awardtype)}\n        ${awardtype_radio(\"manual\", \"manual\", fullname, awardtype)}\n        ${awardtype_radio(\"invisible\", \"invisible\", fullname, awardtype)}\n        ${error_field(\"NO_TEXT\", \"awardtype\", \"span\")}\n      </td>\n    </tr>\n    <tr>\n      <td>API ok?</td>\n      <td>\n        <input name=\"api_ok\" id=\"award_${fullname}_api_ok\"\n          type=\"checkbox\"\n          %if api_ok:\n            checked=\"checked\"\n          %endif\n          />\n        <label for=\"award_${fullname}_api_ok\">\n          allow adding/removing this award via API\n        </label>\n      </td>\n    </tr>\n    <tr>\n      <td>img url</td>\n      <td>\n        <input type=\"text\" name=\"imgurl\" value=\"${imgurl}\" />\n        ${error_field(\"NO_TEXT\", \"imgurl\", \"span\")}\n        ${error_field(\"BAD_URL\", \"imgurl\", \"span\")}\n      </td>\n    </tr>\n  </table>\n  <button class=\"btn\" type=\"submit\">save</button>\n  <span class=\"status\"></span>\n </form>\n</%def>\n\n<table class=\"lined-table\">\n <tbody>\n   <tr>\n     <th>fn</th>\n     <th>cn</th>\n     <th>img</th>\n     <th>title</th>\n     <th>type</th>\n     <th>buttons</th>\n   </tr>\n  %for award in thing.awards:\n   <tr>\n     <td>${award._fullname}</td>\n     <td>${award.codename}</td>\n     <td><img src=\"${award.imgurl % 40}\"/></td>\n     <td>${award.title}</td>\n     <td>${award.awardtype}</td>\n     <td class=\"entry\">\n       ${awardbuttons(award.codename)}\n       ${awardedit(award._fullname, award.title, award.awardtype,\n       award.codename, award.imgurl, award.api_ok)}\n     </td>\n   </tr>\n  %endfor\n </tbody>\n</table>\n\n<button onclick=\"$('#awardedit-NEW').show()\">new award</button>\n\n${awardedit(\"NEW\")}\n"
  },
  {
    "path": "r2/r2/templates/adminawardwinners.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%def name=\"winnerline(trophy)\">\n    <tr>\n      <td>\n        <a href=\"/user/${trophy._thing1.name}\">${trophy._thing1.name}</a>\n      </td>\n      <td>\n        ${trophy._name}\n      </td>\n      <td>\n        ${getattr(trophy, \"description\", \"\")}\n      </td>\n      <td>\n        %if hasattr(trophy, \"url\"):\n         <a href=\"${trophy.url}\">${trophy.url}</a>\n        %endif\n      </td>\n    </tr>\n</%def>\n\n<table class=\"lined-table\">\n  <tr>\n    <td>\n      <img src=\"${thing.award.imgurl % 40}\"/>\n    </td>\n    <td>\n      <h1>${thing.award.title}</h1>\n    </td>\n    <th>\n      description\n    </th>\n    <th>\n      url\n    </th>\n  </tr>\n  %for trophy in thing.trophies:\n    ${winnerline(trophy)}\n  %endfor\n</table>\n\n<p>\n  <a href=\"/admin/awards\">back to awards</a>\n</p>\n\n"
  },
  {
    "path": "r2/r2/templates/adminbar.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n   from r2.lib.pages.admin_pages import admin_menu\n%>\n<%namespace file=\"less.html\" import=\"less_stylesheet\"/>\n<%namespace file=\"utils.html\" import=\"classes\"/>\n\n<%def name=\"adminbar_stylesheet()\">\n  %if c.show_admin_bar:\n    ${less_stylesheet('adminbar.less')}\n  %endif\n</%def>\n\n<%def name=\"indicator(name, label, on)\">\n  %if on:\n    <span class=\"indicator ${name}\"><span class=\"icon\"></span>${label}</span>\n  %endif\n</%def>\n\n%if c.show_admin_bar:\n  <div id=\"admin-bar\" ${classes('admin' if c.user_is_admin else None, 'debug' if g.debug else None)}>\n    <div class=\"status-bar\">\n      <span class=\"caption\">${_('status')}</span>\n      ${indicator('admin', _('admin mode enabled'), c.user_is_admin)}\n      %if c.user_is_admin:\n        <span class=\"admin-off\">${_('admin off')}</span>\n      %endif\n      ${indicator('debug', _('debug mode'), g.debug)}\n      ${indicator('secure', _('secure'), c.secure)}\n      ${indicator('dev-statics', _('development statics'), g.uncompressedJS)}\n      ${indicator('prod-statics', _('production statics'), g.debug and not g.uncompressedJS)}\n      ${indicator('disabled', _('ads disabled'), g.disable_ads)}\n      ${indicator('disabled', _('captcha disabled'), g.disable_captcha)}\n      ${indicator('disabled', _('ratelimit disabled'), g.disable_ratelimit)}\n      <span class=\"controls\">\n        %if c.user_is_admin:\n          ${admin_menu()}\n        %endif\n        <span class=\"timings-button\"><span class=\"state\">-</span>${_('timings')}</span>\n        <span class=\"hide-button\">${_('hide')}</span>\n      </span>\n    </div>\n    <div class=\"timings-bar\">\n      <div class=\"expand-button\">+</div>\n      <div class=\"timelines\">\n        <div class=\"timeline timeline-browser\"></div>\n        <div class=\"timeline timeline-server\"></div>\n      </div>\n    </div>\n    <div class=\"show-button\"></div>\n  </div>\n  <% from r2.lib import js %>\n  ${unsafe(js.use('admin'))}\n%endif\n"
  },
  {
    "path": "r2/r2/templates/admincreddits.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%namespace file=\"utils.html\" import=\"error_field\"/>\n\n<h1>${_(\"give creddits to a user\")}</h1>\n\n<form id=\"creddits\" action=\"/api/givecreddits\" method=\"post\" class=\"pretty-form medium-text\"\n      onsubmit=\"return post_form(this, 'givecreddits');\">\n\n  <table class=\"lined-table borderless\">\n    <tr>\n      <td>${_(\"recipient\")}</td>\n      <td>\n        <input type=\"text\" name=\"recipient\" value=\"${thing.recipient}\">\n        ${error_field(\"NO_USER\", \"recipient\", \"span\")}\n        ${error_field(\"USER_DOESNT_EXIST\", \"recipient\", \"span\")}\n      </td>\n    </tr>\n    <tr>\n        <td>${_(\"creddits\")}</td>\n      <td>\n        <input type=\"number\" name=\"num_creddits\">\n        ${_(\"(negative to take away)\")}\n      </td>\n    </tr>\n  </table>\n\n  <button class=\"btn\" type=\"submit\">${_(\"give\")}</button>\n\n  <span class=\"status\"></span>\n</form>\n\n"
  },
  {
    "path": "r2/r2/templates/adminerrorlog.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%namespace file=\"utils.html\" import=\"error_field\"/>\n\n<%def name=\"status_radio(val, datehex, current)\">\n  <input id=\"status-${datehex}-${val}\"\n         class=\"nomargin\" type=\"radio\" value=\"${val}\" name=\"status\"\n         ${\"checked='checked'\" if current == val else ''} />\n  <label class=\"${val}\" for=\"status-${datehex}-${val}\">${val}</label>\n</%def>\n\n<div class=\"error-logs\">\n  %for date, groupings in thing.date_summaries:\n\n  <div class=\"error-log\">\n    <a class=\"date\" href=\"#\"\n       onclick=\"$(this).parent().find('.rest').toggle();return false\">\n      ${date}\n    </a>\n\n    <div class=\"rest\">\n      %for gr in groupings:\n        %if gr[0] > 0:\n          ${exception(date, *gr)}\n        %else:\n          ${text(date, *gr)}\n        %endif\n      %endfor\n    </div>\n  </div>\n  %endfor\n</div>\n\n<%def name=\"exception(date, frequency, hexkey, d)\">\n  <% datehex = \"-\".join([date.replace(\"/\",\"\"), hexkey]) %>\n\n  <div class=\"exception ${thing.statuses[hexkey]} rounded\">\n    <a class=\"frequency hover\" href=\"#\"\n       onclick=\"$(this).parent().find('.occurrences').toggle();return false\">\n      ${frequency} occurrences\n    </a>\n\n    <span class=\"${thing.statuses[hexkey]}\">\n      ${thing.statuses[hexkey]}:\n    </span>\n\n    <a class=\"nickname\" name=\"${datehex}\" href=\"#${datehex}\"\n       onclick=\"$(this).parent().find('.edit-area').toggle();return false\">\n      ${thing.nicknames[hexkey]}\n    </a>\n\n    <br/>\n\n    <div class=\"edit-area\" style=\"display: none\">\n      <form action=\"/post/edit_error\" method=\"post\"\n          onsubmit=\"return post_form(this, 'edit_error');\"\n            id=\"nickname-${hexkey}\">\n\n        <input type=\"hidden\" name=\"hexkey\" value=\"${hexkey}\" />\n\n        <table>\n          <tr>\n            <th>\n            nickname:\n            </th>\n            <td>\n              <input type=\"text\" value=\"${thing.nicknames[hexkey]}\" name=\"nickname\"/>\n            </td>\n          </tr>\n          <tr>\n            <th>\n              status:\n            </th>\n            <td>\n              ${status_radio(\"new\"   , datehex, thing.statuses[hexkey])}\n              ${status_radio(\"severe\", datehex, thing.statuses[hexkey])}\n              ${status_radio(\"interesting\", datehex, thing.statuses[hexkey])}\n              ${status_radio(\"normal\", datehex, thing.statuses[hexkey])}\n              ${status_radio(\"fixed\" , datehex, thing.statuses[hexkey])}\n            </td>\n          </tr>\n          <tr>\n            <td>\n              <button class=\"save-button\" type=\"submit\">\n                save\n              </button>\n            </td>\n            <td>\n              ${error_field(\"NO_TEXT\", \"codename\", \"span\")}\n              <span class=\"status\"></span>\n            </td>\n          </tr>\n        </table>\n      </form>\n    </div>\n\n    <a class=\"hover\" href=\"#\"\n       onclick=\"$(this).parent().find('.stacktrace').toggle();return false\">\n\n      <span class=\"exception-name\">\n        ${d['exception']}\n      </span>\n\n      <span class=\"hexkey\">(${hexkey})</span>\n\n    </a>\n\n    <div class=\"occurrences\" style=\"display: none\">\n    %for o in d['occurrences']:\n      <span class=\"occurrence\">\n        ${o}\n      </span>\n      &#32;\n    %endfor\n    </div>\n\n    <table class=\"stacktrace lined-table wide\" style=\"display: none\">\n      <thead>\n        <tr>\n          <th>file</th>\n          <th>line#</th>\n          <th>func</th>\n          <th>code</th>\n        </tr>\n      </thead>\n      <tbody>\n        %for row in d['traceback']:\n            <tr>\n              %for i, col in enumerate(row):\n                <td class=\"col-${i}\">\n                  %if i == 2:\n                    ${col}()\n                  %else:\n                    ${col}\n                  %endif\n                </td>\n              %endfor\n            </tr>\n        %endfor\n      </tbody>\n    </table>\n  </div>\n</%def>\n\n<%def name=\"textocc(text, occ, hide)\">\n  %if hide:\n  <tr class=\"extra-occs\" style=\"display: none\">\n  %else:\n  <tr>\n  %endif\n    <td class=\"actual-text\">\n      ${text}\n    </td>\n    <td class=\"occ\">\n      ${occ}\n    </td>\n  </tr>\n</%def>\n\n<%def name=\"text(date, sort_order, level, classification, textoccs)\">\n<div class=\"logtext ${level}\">\n  <span class=\"loglevel rounded\">\n    ${level}:\n  </span>\n  <span class=\"classification\">\n    ${classification}\n  </span>\n  <table class=\"lined-table wide\">\n    %for i, (text, occ) in enumerate (textoccs):\n      %if i < 3 or i >= len(textoccs) - 3:\n        ${textocc(text, occ, False)}\n      %elif i == 3:\n        <tr class=\"extra-occs\">\n          <td colspan=\"2\" class=\"dotdotdot\">\n            <a href=\"#\" \n               onclick=\"$(this).closest('table').find('.extra-occs').toggle();return false\">\n              <b>...</b>\n              &#32;\n              (${len(textoccs) - 6} more lines)\n            </a>\n          </td>\n        </tr>\n\n        ${textocc(text, occ, True)}\n      %else:\n        ${textocc(text, occ, True)}\n      %endif\n    %endfor\n  </table>\n</div>\n</%def>\n"
  },
  {
    "path": "r2/r2/templates/admingold.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%namespace file=\"utils.html\" import=\"error_field\"/>\n\n<h1>${_(\"give or take months of gold for a user\")}</h1>\n\n<form id=\"gold\" action=\"/api/givegold\" method=\"post\" class=\"pretty-form medium-text\"\n      onsubmit=\"return post_form(this, 'givegold');\">\n\n  <table class=\"lined-table borderless\">\n    <tr>\n      <td>${_(\"recipient\")}</td>\n      <td>\n        <input type=\"text\" name=\"recipient\" value=\"${thing.recipient}\">\n        ${error_field(\"NO_USER\", \"recipient\", \"span\")}\n        ${error_field(\"USER_DOESNT_EXIST\", \"recipient\", \"span\")}\n      </td>\n    </tr>\n\n    <tr>\n      <td>${_(\"months\")}</td>\n      <td>\n        <input type=\"number\" name=\"num_months\" value=0>\n        ${_(\"(negative to take away)\")}\n      </td>\n    </tr>\n  </table>\n\n  <button class=\"btn\" type=\"submit\">${_(\"give\")}</button>\n\n  <span class=\"status\"></span>\n</form>\n\n"
  },
  {
    "path": "r2/r2/templates/admininterstitial.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n  from r2.lib.template_helpers import static\n%>\n\n<%namespace name=\"utils\" file=\"utils.html\"/>\n<%namespace file=\"utils.html\" import=\"error_field\"/>\n\n<%inherit file=\"interstitial.html\"/>\n\n<%def name=\"interstitial_image_attrs()\">\n  src=\"${static('interstitial-image-admin.png')}\"\n  title=\"${_('Are you one of us?')}\"\n</%def>\n\n<%def name=\"interstitial_title()\">\n  ${_('Enter your password and one-time code')}\n</%def>\n\n<%def name=\"interstitial_message()\">\n</%def>\n\n<%def name=\"interstitial_buttons()\">\n  <form action=\"/post/adminon\" method=\"post\"\n      onsubmit=\"return post_form(this, 'adminon')\" id=\"adminon\">\n    <div class=\"spacer\">\n      <%utils:round_field title=\"${_('password')}\" description=\"${_('(required)')}\" css_class=\"adminpasswordform\">\n      % if thing.dest:\n          <input type=\"hidden\" name=\"dest\" value=\"${thing.dest}\" />\n      % endif\n      <input type=\"password\" name=\"password\" tabindex=\"1\" autofocus />\n      ${error_field(\"WRONG_PASSWORD\", \"password\")}\n      </%utils:round_field>\n\n      % if not g.disable_require_admin_otp or c.user.otp_secret:\n      <%utils:round_field title=\"${_('one-time verification code')}\" description=\"${_('(required)')}\" css_class=\"adminpasswordform\">\n      <input type=\"text\" name=\"otp\" maxlength=\"6\" tabindex=\"1\" required pattern=\"[0-9]{6}\" autocomplete=\"off\"\n      % if c.otp_cached:\n      disabled\n      % endif\n      />\n      ${error_field(\"WRONG_PASSWORD\", \"otp\")}\n      ${error_field(\"NO_OTP_SECRET\", \"otp\")}\n      ${error_field(\"RATELIMIT\", \"otp\")}\n\n      <label>\n          <input type=\"checkbox\" name=\"remember\" tabindex=\"1\"\n          % if c.otp_cached:\n            disabled\n            checked\n          % endif\n          > ${_(\"remember this computer\")}</label>\n      </%utils:round_field>\n      % endif\n\n      <div class=\"buttons\">\n        <button class=\"c-btn c-btn-primary\" type=\"submit\">\n          ${_('turn admin on')}\n        </button>\n      </div>\n\n      <p class=\"status error\"></p>\n\n    </div>\n  </form>\n</%def>\n"
  },
  {
    "path": "r2/r2/templates/adminnotessidebar.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%namespace file=\"utils.html\" import=\"error_field\"/>\n<%namespace file=\"utils.html\" import=\"md\" />\n<%namespace file=\"utils.html\" import=\"timestamp\" />\n\n<div class=\"raisedbox spacer\">\n    ${_(thing.SYSTEMS[thing.system])}: ${thing.subject}\n    <form id=\"adminnotes-form\" method=\"post\" action=\"/api/add_admin_note\"\n        onsubmit=\"return post_form(this, 'add_admin_note')\">\n        <input type=\"hidden\" name=\"system\" value=\"${thing.system}\">\n        <input type=\"hidden\" name=\"subject\" value=\"${thing.subject}\">\n        <input type=\"hidden\" name=\"author\" value=\"${thing.author}\">\n        <textarea name=\"note\" rows=4></textarea>\n        ${error_field(\"TOO_LONG\", \"notes\", \"span\")}\n        <input type=\"submit\" class=\"notes-button\" value=\"Add a new Note\">\n    </form>\n    %if thing.notes:\n        ${_(\"Past notes\")}:\n        <ul id=\"past-notes\">\n            %for note in thing.notes:\n            <li class=\"adminnote\">\n                <div class=\"adminnote-text\">\n                ${md(note[\"note\"])}\n                </div>\n                <div class=\"adminnote-info tagline\">\n                ${_(\"by %(author)s\") % dict(\n                    author=note[\"author\"],\n                )}&nbsp;\n                ${timestamp(note[\"when\"], include_tense=True)}\n                </div>\n            </li>\n            %endfor\n        </ul>\n    %else:\n        ${_(thing.EMPTY_MESSAGE[thing.system])}\n    %endif\n</div>\n"
  },
  {
    "path": "r2/r2/templates/ads.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n   from r2.lib import tracking\n %>\n\n<iframe id=\"${thing.frame_id}\" frameborder=\"0\" scrolling=\"no\" name=\"${thing.frame_id}\"\n        src=\"${thing.ad_url}\">\n</iframe>\n\n<script type=\"text/javascript\">\n    <% \n      tracker_url = tracking.get_impression_pixel_url(\"adblock\")\n     %>\n    $(function() {\n      var ad = $(\"#${thing.frame_id}\");\n      if(!ad.length || ad.height() == 0 || ad.width() == 0 || ad.offset().left == 0) {\n        $(\".footer\").append(\"<img alt='' src='${unsafe(tracker_url)}&random=\" +\n                                    Math.random()*10000000000000000 + \"'/>\");\n      }\n    });\n</script>\n"
  },
  {
    "path": "r2/r2/templates/adverttrafficsummary.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%inherit file=\"reddittraffic.html\"/>\n\n<%!\n    from r2.lib.template_helpers import format_number\n%>\n\n<%def name=\"sidetables()\">\n  <table class=\"traffic-table\">\n  <caption>${_(\"top adverts\")}</caption>\n  <thead>\n  <tr>\n    <th scope=\"col\">${_(\"ad\")}</th>\n    <th scope=\"col\">${_(\"uniques\")}</th>\n    <th scope=\"col\">${_(\"impressions\")}</th>\n  </tr>\n  </thead>\n  <tbody>\n  % for (name, url), data in thing.advert_summary:\n  <tr>\n    <th scope=\"row\"><a href=\"${url}\" title=\"${name}\">${name[:25]}</a></th>\n    % for datum in data:\n    <td>${format_number(datum)}</td>\n    % endfor\n  </tr>\n  % endfor\n  </tbody>\n  </table>\n</%def>\n\n<%def name=\"tables()\">\n  ${thing.totals}\n</%def>\n"
  },
  {
    "path": "r2/r2/templates/allinfobar.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%namespace file=\"utils.html\" import=\"plain_link, classes, _md\"/>\n\n<div ${classes(\"titlebox\", \"rounded\", thing.css_class)}>\n  <h1 class=\"hover redditname special\">\n    ${plain_link(thing.sr.name, thing.sr.path, _sr_path=False, _class=\"hover\")}\n  </h1>\n\n  <div class=\"usertext\">\n    ${_md(thing.description, wrap=True)}\n  </div>\n</div>\n\n%if thing.allminus_url:\n  <div class=\"giftgold allminus-link\">\n    <a href=\"${thing.allminus_url}\">${_(\"Exclude your subscribed subreddits\")}</a>\n  </div>\n%endif\n\n<div class=\"giftgold allminus-link\">\n    <a href=\"/me/f/all\">${_(\"Exclude custom subreddits\")}</a>\n</div>\n\n%if not thing.gilding_listing:\n<div class=\"gilded-link\">\n  <a href=\"/r/all/gilded\">${_(\"See gilded comments and submissions\")}</a>\n</div>\n%endif\n"
  },
  {
    "path": "r2/r2/templates/apihelp.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n    import re\n    from r2.lib.filters import safemarkdown\n    from r2.controllers.api_docs import section_info\n    from r2.models.token import OAuth2Scope\n    from r2.lib.db.thing import thing_types\n%>\n\n<%def name=\"api_method_id(uri, method)\">${method}_${uri.replace('/', '_').strip('_')}</%def>\n<%def name=\"api_uri(uri)\">${unsafe(re.sub(r'{(\\w+)}', r'<em class=\"placeholder\">\\1</em>', uri))}</%def>\n<%def name=\"api_uri_with_sr_maybe(uri, in_sr)\">${api_uri((\"[/r/{subreddit}]\" if in_sr else \"\") + uri)}</%def>\n\n<%def name=\"mode_selector(current_mode)\">\n  <span class=\"mode-selector\">\n    <a class=\"mode ${'mode-current' if current_mode == 'section' else ''}\"\n       href=\"/dev/api\">by section</a>\n    <a class=\"mode ${'mode-current' if current_mode == 'oauth' else ''}\"\n       href=\"/dev/api/oauth\">by oauth scope</a>\n  </span>\n</%def>\n\n<%def name=\"api_method_toc(api)\">\n  <strong>API methods</strong>\n  ${mode_selector('section')}\n  <ul>\n  %for section in sorted(api):\n    <li>\n      <a href=\"#section_${section}\" class=\"section\">${section_info[section]['title']}</a>\n      <ul>\n        %for uri in sorted(api[section]):\n          <%\n             methods = sorted(api[section][uri].keys())\n             has_oauth = any(api[section][uri][method]['oauth_scopes']\n                             for method in methods)\n          %>\n          <li class=\"${'supports-oauth' if has_oauth else ''}\">\n            <a href=\"#${api_method_id(uri, methods[0])}\">${api_uri(uri)}</a>\n          </li>\n        %endfor\n      </ul>\n    </li>\n  %endfor\n  </ul>\n</%def>\n\n<%def name=\"oauth_toc(api, oauth_index)\">\n  <strong>API methods</strong>\n  ${mode_selector('oauth')}\n  <ul>\n  %for scope, keys in sorted(oauth_index.iteritems()):\n    <li>\n      <a href=\"#scope_${scope}\" class=\"section\">${scope}</a>\n      <ul>\n        %for section, uri, method in sorted(keys):\n          <li>\n            <a href=\"#${api_method_id(uri, method)}\">${api_uri(uri)}</a>\n          </li>\n        %endfor\n      </ul>\n    </li>\n  %endfor\n  </ul>\n</%def>\n\n<%def name=\"section_header(section)\">\n  <h2 id=\"section_${section}\">${section_info[section]['title']}</h2>\n  %if 'description' in section_info[section]:\n    <div class=\"description\">\n      ${unsafe(safemarkdown(section_info[section]['description']))}\n    </div>\n  %endif\n</%def>\n\n<%def name=\"scope_header(scope)\">\n  <h2 id=\"scope_${scope}\">\n    ${OAuth2Scope.scope_info[scope]['name']}\n    <span class=\"scope-id\">${scope}</span>\n  </h2>\n  <div class=\"description\">\n    ${unsafe(safemarkdown(OAuth2Scope.scope_info[scope]['description']))}\n  </div>\n</%def>\n\n<%\n  api = thing.api_docs\n%>\n\n<div class=\"sidebar\">\n  <div class=\"head\"></div>\n  <div class=\"toc\">\n    <ul>\n      <li>\n        %if thing.mode == \"oauth\":\n          ${oauth_toc(thing.api_docs, thing.oauth_index)}\n        %else:\n          ${api_method_toc(thing.api_docs)}\n        %endif\n      </li>\n    </ul>\n  </div>\n  <div class=\"feet\"></div>\n</div>\n\n<div class=\"contents\">\n  <div class=\"section introduction\">\n    <p>This is automatically-generated documentation for the reddit API.</p>\n    <p><strong>Please take care to respect our&#32;<a href=\"https://github.com/reddit/reddit/wiki/API\">API access rules</a>.</strong></p>\n  </div>\n\n  <div class=\"section overview\">\n    <h2>overview</h2>\n\n    <h3 id=\"listings\">listings</h3>\n\n    <%text filter=\"safemarkdown\">\nMany endpoints on reddit use the same protocol for controlling pagination and\nfiltering. These endpoints are called Listings and share five common\nparameters: `after` / `before`, `limit`, `count`, and `show`.\n\nListings do not use page numbers because their content changes so frequently.\nInstead, they allow you to view slices of the underlying data. Listing JSON\nresponses contain `after` and `before` fields which are equivalent to the\n\"next\" and \"prev\" buttons on the site and in combination with `count` can be\nused to page through the listing.\n\nThe common parameters are as follows:\n\n* `after` / `before` - only one should be specified. these indicate the\n[fullname](#fullnames) of an item in the listing to use as the anchor point of\nthe slice.\n* `limit` - the maximum number of items to return in this slice of the listing.\n* `count` - the number of items already seen in this listing. on the html site,\nthe builder uses this to determine when to give values for `before` and `after`\nin the response.\n* `show` - optional parameter; if `all` is passed, filters such as \"hide links\nthat I have voted on\" will be disabled.\n\nTo page through a listing, start by fetching the first page without specifying\nvalues for `after` and `count`. The response will contain an `after` value\nwhich you can pass in the next request. It is a good idea, but not required, to\nsend an updated value for `count` which should be the number of items already\nfetched.\n    </%text>\n\n    <h3 id=\"modhashes\">modhashes</h3>\n\n    <%text filter=\"safemarkdown\">\nA modhash is a token that the reddit API requires to help prevent\n[CSRF](http://en.wikipedia.org/wiki/CSRF). Modhashes can be obtained via the\n[/api/me.json](#GET_api_me.json) call or in response data of listing endpoints.\n\nThe preferred way to send a modhash is to include an `X-Modhash` custom HTTP\nheader with your requests.\n\nModhashes are not required when authenticated with OAuth.\n    </%text>\n\n    <h3 id=\"fullnames\">fullnames</h3>\n\n    <%text filter=\"safemarkdown\">\nA fullname is a combination of a thing's type (e.g. `Link`) and its unique ID\nwhich forms a compact encoding of a globally unique ID on reddit.\n\nFullnames start with the type prefix for the object's type, followed by the\nthing's unique ID in [base 36](http://en.wikipedia.org/wiki/Base36).  For\nexample, `t3_15bfi0`.\n    </%text>\n\n    <table class=\"parameters\">\n      <caption>type prefixes</caption>\n      % for typeid in sorted(thing_types):\n      <tr>\n        <th scope=\"row\">t${typeid}_</th>\n        <td>${thing_types[typeid].__name__}</td>\n      </tr>\n      % endfor\n    </table>\n\n    <h3 id=\"response_body_encoding\">response body encoding</h3>\n\n    <%text filter=\"safemarkdown\">\nFor legacy reasons, all JSON response bodies currently have `<`, `>`, and `&`\nreplaced with `&lt;`, `&gt;`, and `&amp;`, respectively. If you wish to opt out\nof this behaviour, add a `raw_json=1` parameter to your request.\n    </%text>\n  </div>\n\n  <div class=\"section methods\">\n    %for section in sorted(api):\n      %if thing.mode == 'oauth':\n        ${scope_header(section)}\n      %else:\n        ${section_header(section)}\n      %endif\n      %for uri in sorted(api[section]):\n        %for method in sorted(api[section][uri]):\n          <%\n            docs = api[section][uri][method]\n            # skip uri variants in the index\n            if docs['uri'] != uri:\n              continue\n\n            in_subreddit = 'in-subreddit' in docs\n\n            extends = docs.get('extends')\n          %>\n          <div class=\"endpoint\" id=\"${api_method_id(uri, method)}\">\n            <div class=\"links\">\n              <a href=\"#${api_method_id(uri, method)}\">#</a>\n            </div>\n            <h3>\n              <span class=\"method\">${method}&nbsp;</span>\n              ${api_uri_with_sr_maybe(uri, in_subreddit)}\n              %if docs['oauth_scopes']:\n                <span class=\"oauth-scope-list\">\n                  %for scope in docs['oauth_scopes']:\n                    <a href=\"https://github.com/reddit/reddit/wiki/OAuth2\">\n                        <span class=\"api-badge oauth-scope\">${scope or \"any\"}</span>\n                    </a>\n                  %endfor\n                </span>\n              %endif\n              %if docs['supports_rss']:\n                <a href=\"https://www.reddit.com/wiki/rss\">\n                    <span class=\"api-badge rss-support\">rss support</span>\n                </a>\n              %endif\n            </h3>\n            %if 'uri_variants' in docs:\n              <ul class=\"uri-variants\">\n                %for variant in docs['uri_variants']:\n                <li id=\"${api_method_id(variant, method)}\">&rarr; ${api_uri_with_sr_maybe(variant, in_subreddit)}</li>\n                %endfor\n              </ul>\n            %endif\n            <div class=\"info\">\n              ${unsafe(safemarkdown(docs.get('doc')))}\n              <%\n                json_model = docs.get('json_model')\n                if json_model:\n                  params = None\n                  base_params = None\n                else:\n                  params = docs.get('parameters')\n                  base_params = extends.get('parameters') if extends else None\n              %>\n              %if params or base_params or json_model:\n                <table class=\"parameters\">\n                %if params:\n                  %for param in sorted(params):\n                    <tr>\n                      <th scope=\"row\">${param}</th>\n                      <td>${unsafe(safemarkdown(params[param], wrap=False))}</td>\n                    </tr>\n                  %endfor\n                %endif\n                %if base_params:\n                  %for param in sorted(base_params):\n                    %if param not in params:\n                      <tr class=\"base-param\">\n                        <th scope=\"row\">${param}</th>\n                        <td>${unsafe(safemarkdown(base_params[param], wrap=False))}</td>\n                      </tr>\n                    %endif\n                  %endfor\n                %endif\n                %if json_model:\n                  <tr class=\"json-model\">\n                    <th>${_(\"This endpoint expects JSON data of this format\")}</th>\n                    <td>${unsafe(safemarkdown(json_model.docs_model(), wrap=False))}</td>\n                  </tr>\n                %endif\n                </table>\n              %endif\n            </div>\n          </div>\n        %endfor\n      %endfor\n    %endfor\n  </div>\n</div>\n"
  },
  {
    "path": "r2/r2/templates/archivedinterstitial.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n  from r2.lib.template_helpers import static, _wsf\n%>\n\n<%inherit file=\"interstitial.html\"/>\n\n<%def name=\"interstitial_image_attrs()\">\n  src=\"${static('interstitial-image-archived.png')}\"\n  alt=\"${_('archived')}\"\n</%def>\n\n<%def name=\"interstitial_title()\">\n  ${_(\"This is an archived post. You won't be able to vote or comment.\")}\n</%def>\n\n<%def name=\"interstitial_message()\">\n  <%\n    months = ungettext('month', 'months', thing.archive_age_months)\n  %>\n  <p>\n    ${_wsf(\"Posts are automatically archived after %(num)s %(months)s.\",\n           num=thing.archive_age_months, months=months)}\n  </p>\n</%def>\n\n<%def name=\"interstitial_buttons()\">\n  <div class=\"buttons\">\n    <a href=\"/\" class=\"c-btn c-btn-primary\">${_(\"Got It\")}</a>\n  </div>\n</%def>\n"
  },
  {
    "path": "r2/r2/templates/automoderatorconfig.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n    from r2.lib.filters import SC_OFF, SC_ON, unsafe\n%>\n\n<pre>\n  <code>${unsafe(SC_OFF)}${thing.automoderator_config}${unsafe(SC_ON)}</code>\n</pre>\n"
  },
  {
    "path": "r2/r2/templates/awardreceived.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n<%namespace file=\"trophycase.html\" import=\"trophy_info\" />\n\n\n<div class=\"centered\">\n%if thing.preexisting:\n  <h1>${_(\"you already have that trophy!\")}</h1>\n%else:\n  <h1>${_(\"trophy claimed!\")}</h1>\n%endif\n\n${trophy_info(thing.trophy, False)}\n\n</div>\n"
  },
  {
    "path": "r2/r2/templates/bannedinterstitial.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n  from r2.lib.template_helpers import _wsf, static\n%>\n\n<%namespace file=\"utils.html\" import=\"_md, buffered_timestamp\"/>\n\n<%inherit file=\"interstitial.html\"/>\n\n<%def name=\"interstitial_image_attrs()\">\n  src=\"${static('interstitial-image-banned.png')}\"\n  alt=\"${_('banned')}\"\n</%def>\n\n<%def name=\"interstitial_title()\">\n  ${_(\"This community has been banned\")}\n</%def>\n\n<%def name=\"interstitial_message()\">\n  %if thing.message:\n    ${parent.interstitial_message()}\n  %else:\n    ${_md(\"This community has been banned for violating the [Reddit rules](/rules).\")}\n  %endif\n\n  %if thing.ban_time:\n    <div class=\"note\">\n      ${_wsf(\"Banned %(time_ago)s.\", time_ago=unsafe(buffered_timestamp(thing.ban_time, include_tense=True)))}\n    </div>\n  %endif\n</%def>\n"
  },
  {
    "path": "r2/r2/templates/banneduserinterstitial.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n  from r2.lib.template_helpers import _wsf, format_html\n%>\n\n<%inherit file=\"bannedinterstitial.html\"/>\n\n<%def name=\"interstitial_title()\">\n  <%\n    suspended_link = format_html('&#32;<a href=\"https://reddit.zendesk.com/hc/en-us/articles/205687686\">%s</a>',\n                                 _('suspended'))\n  %>\n  ${_wsf(\"This account has been %(suspended_link)s\", suspended_link=suspended_link)}\n</%def>\n\n<%def name=\"interstitial_message()\">\n  ## no message\n</%def>\n"
  },
  {
    "path": "r2/r2/templates/base.compact",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n<%!\n   from r2.lib.template_helpers import static\n   from r2.lib import js\n%>\n<%namespace file=\"utils.html\" import=\"js_setup, googleanalytics, classes\"/>\n<html xmlns=\"http://www.w3.org/1999/xhtml\" lang=\"${c.lang}\">\n  <head>\n    <link rel=\"apple-touch-icon\" \n          href=\"/static/compact/reddit-apple-mobile-device.png\"/>\n    <link rel=\"apple-touch-startup-image\" \n          href=\"/static/compact/reddit_startimg.png\" />\n    <link rel=\"canonical\" href=\"${thing.canonical_link}\" />\n    %if hasattr(thing, \"shortlink\"):\n      <link rel=\"shorturl\" href=\"https://${thing.shortlink}\" />\n    %endif\n    <meta name=\"apple-mobile-web-app-status-bar-style\" content=\"black\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no\"/>\n    <title>${self.Title()}</title>\n    <meta name=\"title\" content=\"${self.Title()}\" />\n    <meta name=\"description\" content=\"${thing.short_description or g.short_description}\" />\n    <meta name=\"referrer\" content=\"${c.referrer_policy}\" />\n    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n    ${self.robots()}\n    <link rel=\"stylesheet\" href=\"${static('compact.css')}\" type=\"text/css\" media=\"screen\" />\n\n    <!--[if gte IE 9]> <!-->\n      ${unsafe(js.use('reddit-init'))}\n    <!-- <![endif]-->\n\n    <!--[if lt IE 9]>\n      ${unsafe(js.use('reddit-init-legacy'))}\n    <![endif]-->\n\n    ${js_setup(thing.extra_js_config)}\n    ${googleanalytics('mobile')}\n  </head>\n  <body ${classes(*thing.page_classes())}>\n    ${self.bodyContent()}\n    ${unsafe(js.use('mobile'))}\n  </body>\n</html>\n\n<%def name=\"bodyContent()\">\n</%def>\n\n<%def name=\"Title()\">\n${thing.title}\n</%def>\n\n<%def name=\"robots()\">\n   %if hasattr(thing, 'robots') and thing.robots:\n     <meta name=\"robots\" content=\"${thing.robots}\" />\n   %endif\n</%def>\n\n"
  },
  {
    "path": "r2/r2/templates/base.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<!doctype html>\n<%!\n   from r2.lib.template_helpers import static\n   from r2.models import Link, Comment, Subreddit\n   from r2.lib import tracking\n%>\n<%namespace file=\"utils.html\" import=\"js_setup, googleanalytics, googletagmanager, classes\"/>\n<html xmlns=\"http://www.w3.org/1999/xhtml\" lang=\"${c.lang}\"\n      xml:lang=\"${c.lang}\">\n  <head>\n    <title>${self.Title()}</title>\n    <meta name=\"keywords\" content=\"${self.keywords()}\" />\n    <meta name=\"description\" content=\"${getattr(thing, 'short_description', None) or g.short_description}\" />\n    <meta name=\"referrer\" content=\"${c.referrer_policy}\">\n    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n    <link type=\"application/opensearchdescription+xml\" rel=\"search\" href=\"/static/opensearch.xml\"/>\n    <link rel=\"canonical\" href=\"${thing.canonical_link}\" />\n    ${self.viewport()}\n    ${self.robots()}\n    ${self.pagemeta()}\n    ${self.stylesheet()}\n    ${self.javascript()}\n    ${js_setup(getattr(thing, \"extra_js_config\", None))}\n\n    ##things here may depend on globals, or included js, so we run them last\n    <style type=\"text/css\">\n      ## <style>s are treated as RAWTEXT segments, so we can't `websafe()` their contents.\n      ## According to the HTML spec's RAWTEXT parsing rules, this should work for escaping.\n      ## http://www.w3.org/TR/html5/syntax.html#rawtext-less-than-sign-state\n      ${unsafe(_(\"/* Custom css: use this block to insert special translation-dependent css in the page header */\").replace(\"</\", \"\"))}\n    </style>\n\n    ${self.head()}\n  </head>\n\n  <body ${classes(*thing.page_classes())}>\n    ${googletagmanager()}\n    ${self.bodyContent()}\n    ${self.javascript_bottom()}\n  </body>\n</html>\n\n<%def name=\"bodyContent()\">\n</%def>\n\n<%def name=\"Title()\">\n${c.site.title}\n</%def>\n\n<%def name=\"keywords()\">\nreddit, reddit.com, vote, comment, submit\n</%def>\n\n<%def name=\"viewport()\">\n<meta name=\"viewport\" content=\"width=1024\">\n</%def>\n\n<%def name=\"robots()\">\n   %if hasattr(thing, 'robots') and thing.robots:\n     <meta name=\"robots\" content=\"${thing.robots}\" />\n   %endif\n</%def>\n\n<%def name=\"head()\">\n${googleanalytics('web', thing.is_gold_page() if hasattr(thing, 'is_gold_page') else False)}\n</%def>\n\n<%def name=\"pagemeta()\">\n</%def>\n\n<%def name=\"stylesheet()\">\n</%def>\n\n<%def name=\"javascript()\">\n</%def>\n\n<%def name=\"javascript_bottom()\">\n</%def>\n"
  },
  {
    "path": "r2/r2/templates/base.htmllite",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%namespace file=\"utils.html\" import=\"optionalstyle\"/>\n\n<%!\n   from pylons.i18n import _\n   from r2.lib.template_helpers import get_domain, style_line, format_html, _wsf\n   from r2.models.subreddit import FakeSubreddit, DefaultSR\n%>\n\n<div class=\"rembeddit\"\n     ${optionalstyle(\"font-family:verdana,arial,helvetica,sans-serif;\" + \n                     style_line(bgcolor=\"FFFFFF\", bordercolor=\"336699\"))}>\n  %if not request.GET.get(\"nobranding\"):\n  <div class=\"reddit-header\" \n       ${optionalstyle(\"padding: 5px; padding-bottom:1px;\" + \n                    (\"background-color:#CEE3F8\" if not c.bgcolor else \"\"))}>\n  <h4 class=\"reddit-title\" \n      ${optionalstyle(\"margin:0;\" + \n                      \"padding-bottom:3px\")}\n      >\n    <a href=\"${g.default_scheme}://${get_domain()}/\" ${optionalstyle(\"margin:5px;\")}\n      %if c.link_target:\n         target=\"${c.link_target}\"\n      %endif\n       >\n      <img src=\"${g.default_scheme}://${get_domain(subreddit=False)}/static/spreddit1.gif\"\n           alt=\"\" \n           ${optionalstyle(\"border:none\")} />\n    </a>\n    <%\n       style = unsafe(capture(optionalstyle, \"text-decoration:none;color:#336699\"))\n       name = c.site.name\n       if not isinstance(c.site, FakeSubreddit):\n           name += \".%s\" % g.domain\n       if c.link_target:\n          link = format_html('<a %s href=\"%s://%s/\" target=\"%s\">%s</a></h3>',\n                             style, g.default_scheme, get_domain(), c.link_target, name)\n       else:\n          link = format_html('<a %s href=\"%s://%s/\">%s</a></h3>',\n                             style, g.default_scheme, get_domain(), name)\n     %>\n    ${self.titlebar(link)}\n  </h4>\n  %if g.domain != \"reddit.com\":\n  <p ${optionalstyle(\"margin: 0px 30px 5px 30px; color: gray;\")}\n     class=\"powered-by-reddit\">\n    <small>\n      powered by&#32; \n      <a href=\"${g.default_scheme}://${g.domain}\"\n         ${optionalstyle(\"text-decoration:none;color:#336699\")}\n        %if c.link_target:\n          target=\"${c.link_target}\"\n        %endif\n      >\n        ${DefaultSR.name}\n      </a>\n    </small>\n  </p>\n  %endif\n  </div>\n  %endif\n  <div class=\"rembeddit-content\" ${optionalstyle(\"padding:5px;\")}>\n    ${next.body()}\n  </div>\n</div>\n\n<%def name=\"titlebar(site)\">\n  ${_wsf(\"links from %(site)s\", site=site)}\n</%def>\n"
  },
  {
    "path": "r2/r2/templates/base.iframe",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2014\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n    import json\n\n    from r2.lib.filters import unsafe\n    from r2.lib import js\n    from r2.lib.template_helpers import add_sr, get_domain, static\n%>\n<%namespace file=\"less.html\" import=\"less_js, less_stylesheet\"/>\n<%namespace file=\"utils.html\" import=\"text_with_links, plain_link\"/>\n\n<%\n    link_data = {\"redirect-type\": \"link\", \"redirect-thing\": thing.link._id}\n    sr_data = {\"redirect-type\": \"subreddit\"}\n    reddit_data = {\"redirect-type\": \"logo\"}\n%>\n\n<!doctype html>\n<html>\n    <head>\n        <meta charset=utf-8>\n        <base target=\"_blank\" href=\"${add_sr(\"/\", sr_path=False, force_hostname=True)}\">\n        <title></title>\n        ${less_stylesheet(\"reddit-embed.less\")}\n    </head>\n    <body>\n        <div class=\"reddit-embed\">\n          <div class=\"reddit-embed-content\">\n            ${next.body()}\n          </div>\n          <footer class=\"reddit-embed-footer\" role=\"contentinfo\">\n            <p>\n                ${text_with_links(\n                    _(\"from discussion %(link)s on %(subreddit)s\"),\n                    link=dict(link_text=thing.link.title, path=thing.link.permalink, data=link_data),\n                    subreddit=dict(link_text=(\"/r/%s\" % c.site.name), path=c.site.path, data=sr_data),\n                )}\n            </p>\n            ${plain_link(\"reddit\", \"/\", _sr_path=False, _class=\"reddit-embed-footer-img\", data=reddit_data)}\n          </footer>\n        </div>\n        <script>\n            window.REDDIT_EMBED_CONFIG = ${unsafe(json.dumps(c.embed_config))};\n        </script>\n        ${unsafe(js.use('reddit-embed'))}\n        ${less_js()}\n    </body>\n</html>\n"
  },
  {
    "path": "r2/r2/templates/base.mobile",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">\n<%!\n   from pylons.i18n import _\n%>\n<html xmlns=\"http://www.w3.org/1999/xhtml\" lang=\"${c.lang}\" \n      xml:lang=\"${c.lang}\">\n<head>\n\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n<meta name=\"referrer\" content=\"${c.referrer_policy}\" />\n<link rel=\"canonical\" href=\"${thing.canonical_link}\" />\n\n<title>${self.Title()}</title>\n\n${self.head()}\n${self.stylesheet()}\n\n</head>\n\n<body>\n${next.body()}\n</body>\n</html>\n\n\n<%def name=\"Title()\">\n${c.site.title}\n</%def>\n\n\n<%def name=\"head()\">\n</%def>\n\n"
  },
  {
    "path": "r2/r2/templates/base.xml",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n<%!\n    from pylons import app_globals as g\n    from datetime import datetime\n    from r2.lib.template_helpers import add_sr, header_url\n    from r2.lib.template_helpers import static\n    from r2.lib.template_helpers import html_datetime\n    from r2.lib.utils import UrlParser\n    # atom rfc: https://tools.ietf.org/html/rfc4287\n%>\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<feed xmlns=\"http://www.w3.org/2005/Atom\">\n    ${self.Category()}\n    ${self.Updated()}\n\n    %if c.can_apply_styles and not c.css_killswitch:\n        ${self.Icon()}\n    %endif\n\n    ${self.Id()}\n    ${self.Link()}\n\n    %if c.can_apply_styles and not c.css_killswitch:\n        ${self.Logo()}\n    %endif\n\n    ${self.Subtitle()}\n    ${self.Title()}\n\n    ## will be a list of <entry>s\n    ${next.body()}\n</feed>\n\n## these can all be controlled by the type of listing, but they default to\n## c.site's info\n\n<%def name=\"Category()\">\n    <category term=\"${c.site.name}\" label=\"/r/${c.site.name}\"/>\n</%def>\n\n<%def name=\"Updated()\">\n    <updated>${html_datetime(datetime.now(g.tz))}</updated>\n</%def>\n\n<%def name=\"Title()\">\n    %if getattr(thing, 'title', None):\n        <title>${thing.title}</title>\n    %elif c.site.title:\n        <title>${c.site.title}</title>\n    %elif c.site.name:\n        <title>${c.site.name}</title>\n    %endif\n</%def>\n\n<%def name=\"Link()\">\n    <link rel=\"self\" href=\"${add_sr(request.fullpath,\n                                    sr_path=False,\n                                    force_hostname=True)}\"\n     type=\"application/atom+xml\" />\n    <link rel=\"alternate\" href=\"${add_sr(request.fullpath,\n                                         sr_path=False,\n                                         force_hostname=True,\n                                         force_extension='')}\"\n     type=\"text/html\" />\n</%def>\n\n<%def name=\"Id()\">\n    ## the feed <id>, not the entry one\n    ## https://tools.ietf.org/html/rfc4287#section-4.2.6\n\n    <id>\n        %if hasattr(thing, '_fullname'):\n            ${thing._fullname}\n        %else:\n            ${request.fullpath}\n        %endif\n    </id>\n</%def>\n\n<%def name=\"Subtitle()\">\n    %if c.site.public_description:\n        <subtitle>${c.site.public_description}</subtitle>\n    %endif\n</%def>\n\n<%def name=\"Icon()\">\n    ## a 1x1 aspect image\n    ## https://tools.ietf.org/html/rfc4287#section-4.2.5\n    <icon>${static(\"icon.png\", absolute=True)}/</icon>\n</%def>\n\n<%def name=\"Logo()\">\n    ## a 2x1 aspect image\n    ## https://tools.ietf.org/html/rfc4287#section-4.2.8\n    %if c.site.header:\n        <logo>${header_url(c.site.header, absolute=True)}</logo>\n    %endif\n</%def>\n"
  },
  {
    "path": "r2/r2/templates/buttondemopanel.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n   from pylons import app_globals as g\n\n   from r2.lib.template_helpers import get_domain\n %>\n\n<% domain = get_domain(subreddit=False) %>\n\n<div class=\"instructions\">\n  <h1>${_(\"put %(site)s buttons on your site\") % dict(site=c.site.name)}</h1>\n    <p>${_('the reddit button is the smart way to get your content submitted to\\\n      and discussed on reddit.  pick the button you like from below, and then\\\n      copy/paste the code into your HTML editor.')}</p>\n\n    <h2>${_(\"commonly used buttons\")}</h2>\n    <p>${_('use one of these buttons to quickly add reddit links to your site, or \\\n      see below for more options.')}</p>\n    <ul class=\"buttons\">\n      ${demo(capture(drawbadge, 1))}\n      ${demo(capture(drawbadge, 7))}\n      ${demo(capture(draw_point_button, 0))}\n      ${demo(capture(draw_point_button, 1))}\n      ${demo(capture(draw_interactive, 1))}\n    </ul>\n      \n\n    <h2>${_(\"buttons with points\")}</h2>\n    <ul class=\"buttons\">\n    %for x in xrange(0,6):\n       ${demo(capture(draw_point_button, x))}\n    %endfor\n    </ul>\n    <h2>${_(\"customizing the look of your buttons\")}</h2>\n    <p>${_('the buttons with points have three additional options.')}</p>\n    <ul class=\"buttons\" >\n      <li><strong>styled=off</strong><br />\n        ${_('no styles will be added, so you can style it yourself')}</li>\n      <li><strong>url=[URL]</strong><br />\n        ${_('specify a url to use instead of the current url')}</li>\n      <li><strong>newwindow=1</strong><br />\n        ${_('opens links in a new window')}</li>\n    </ul>\n    <p>${_('Example:')}</p>\n    <code>\n      ${point_option_example()}\n    </code>\n\n\n    <h2>${_('simple interactive button')}</h2>\n    <p>${_('put this code on your page:')}</p>\n    <code>\n      ${capture(draw_interactive,False)}\n    </code>\n      <p>${_(\"and you'll get something like this:\")}</p>\n        <span style=\"margin-left: 10px;\">\n          ${draw_interactive(False)}\n        </span>\n\n    <h2>${_(\"more interactive buttons\")}</h2>\n    <ul class=\"buttons\">\n      %for x in xrange(1,4):\n        ${demo(capture(draw_interactive, x))}\n      %endfor\n    </ul>\n\n    <h2>${_('interactive button advanced settings')}</h2>\n    <div class=\"box buttonsettings\">\n      <ul>\n        <li>\n          <p><strong>${_(\"specify a url\")}</strong><br />\n            ${_(\"useful in places like blogs, where you want to link to the post's permalink\")}</p>\n          <code>${drawoption('url','[URL]')}</code>\n        </li>\n        <li>\n          <p><strong>${_(\"specify a community to target\")}</strong></p>\n          <code>${drawoption('target','[COMMUNITY]')}</code>\n        </li>\n        <li>\n          <p><strong>${_(\"specify a title\")}</strong></p>\n          <code>${drawoption('title','[TITLE]')}</code>\n        </li>\n        <li>\n          <p><strong>${_(\"open links in a new window\")}</strong></p>\n          <code>${drawoption('newwindow','1')}</code>\n        </li>\n        <li>\n          <p><strong>${_(\"specify the color\")}</strong></p>\n          <code>${drawoption('bgcolor','[COLOR]')}</code>\n        </li>\n        <li>\n          <p><strong>${_(\"specify a border color\")}</strong></p>\n          <code>${drawoption('bordercolor','[COLOR]')}</code>\n        </li>\n      </ul>\n      <p style=\"font-weight: bold\">${_('Example:')}</p>\n      <p>${_('to make this button:')}</p>\n      <span style=\"margin-left: 10px;\">${draw_interactive_example()}</span>\n      <p>${_('use this code:')}</p>\n      <code>\n        <%\n           ex = websafe(capture(draw_interactive_example))\n           ex = ex.replace(\"\\n\", \"<br/>\").replace(\" \", \"&nbsp;\")\n         %>\n      ${unsafe(ex)}\n      </code>\n    </div>\n\n    <h2>${_(\"more badges and buttons\")}</h2>\n    <ul class=\"buttons\">\n      %for x in xrange(1,15):\n        ${demo(capture(drawbadge, x))}\n      %endfor\n    </ul>\n\n\n</div>\n\n<script type=\"text/javascript\">\n$(function() {\n $(\".view-code\").click(function(evt) { \n    $(this).parent().addClass(\"show-demo\"); \n });\n $(\".hide-code\").click(function(evt) { \n    $(this).parent().removeClass(\"show-demo\"); \n });\n});\n</script>\n\n<%def name=\"drawbadge(image)\">\n  <a href=\"//${domain}/submit\"\n    onclick=\"window.location = '//${domain}/submit?url=' + encodeURIComponent(window.location); return false\">\n   <img src=\"//${g.static_domain}/spreddit${image}.gif\"\n        alt=\"submit to reddit\" border=\"0\" />\n   </a>\n</%def>\n\n<%def name=\"demo(content)\">\n<li class=\"button-demo\">\n  <a class=\"view-code\" href=\"javascript:void(0)\">${_(\"view code\")}</a>\n  <a class=\"hide-code\" href=\"javascript:void(0)\">${_(\"hide code\")}</a>\n  ${unsafe(content)}\n  <br />\n  <code>\n    ${content}\n  </code>\n</li>\n</%def>\n\n<%def name=\"draw_point_button(image)\">\n  <script type=\"text/javascript\" \n          src=\"//${domain}/buttonlite.js?i=${image}\"></script>\n</%def>\n\n<%def name=\"point_option_example()\" buffered=\"True\">\n   <script type=\"text/javascript\" \n        src=\"//${domain}/buttonlite.js?i=1&styled=off&url=foo.com&newwindow=1\"></script>\n</%def>\n\n<%def name=\"draw_interactive(type)\">\n%if type:\n  <script type=\"text/javascript\" \n          src=\"//${g.static_domain}/button/button${type}.js\"></script>\n%else:\n  <script type=\"text/javascript\" src=\"//${g.static_domain}/button/button1.js\"></script>\n%endif\n</%def>\n\n<%def name=\"drawoption(option, val)\" buffered=\"True\">\n  <script type=\"text/javascript\">reddit_${option}='${val}'</script>\n</%def>\n\n<%def name=\"draw_interactive_example()\"><script type=\"text/javascript\">\n  reddit_url = \"//${domain}/buttons\";\n  reddit_title = \"Buttons!\";\n  reddit_bgcolor = \"FF3\";\n  reddit_bordercolor = \"00F\";\n</script>\n<script type=\"text/javascript\" src=\"//${g.static_domain}/button/button3.js\"></script>\n</%def>\n\n"
  },
  {
    "path": "r2/r2/templates/buttonlite.js",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n   from r2.lib.filters import jssafe, websafe\n   from r2.lib.template_helpers import static, get_domain\n   from r2.lib.utils import query_string\n   from r2.lib.strings import Score\n %>\n\n<%def name=\"submiturl(url, title='')\">${(\"%s://%s/submit\" % (g.default_scheme, get_domain(subreddit=True))) + query_string(dict(url=url, title=title))}</%def>\n\n<% \n    if thing._fullname:\n        path = thing.make_permalink_slow(force_domain=True)\n    else:\n        path = capture(submiturl, thing.url, thing.title)\n%>\n(function() {\n       \n    var styled_submit = '<a style=\"color: #369; text-decoration: none;\" href=\"${path}\" target=\"${thing.target}\">';\n    var unstyled_submit = '<a href=\"${submiturl(thing.url)}\" target=\"${path}\">';\n    var write_string='<span class=\"reddit_button\" style=\"';\n%if thing.styled:    \n    write_string += 'color: grey;';\n%endif\n    write_string += '\">';\n%if thing.image > 0:\n    write_string += unstyled_submit + '<img style=\"height: 2.3ex; vertical-align:top; margin-right: 1ex\" src=\"${static('spreddit' + str(thing.image) + '.gif')}\">' + \"</a>\";\n%endif\n%if thing._fullname:\n    write_string += '${jssafe(websafe(Score.safepoints(thing.score)))}';\n    %if thing.styled:  \n        write_string += ' on ' + styled_submit + 'reddit</a>';\n    %else:\n        write_string += ' on ' + unstyled_submit + 'reddit</a>';\n    %endif\n%else:\n    %if thing.styled:\n    write_string += styled_submit + 'submit';\n    %else:\n    write_string += unstyled_submit + 'submit';\n    %endif\n    %if thing.image > 0:\n    write_string += '</a>';\n    %else:\n    write_string += ' to reddit</a>';\n    %endif\n%endif\n    write_string += '</span>';\n\ndocument.write(write_string);\n})()\n"
  },
  {
    "path": "r2/r2/templates/captcha.compact",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%namespace file=\"captcha.html\" import=\"rounded_captcha\"/>\n\n${rounded_captcha()}\n"
  },
  {
    "path": "r2/r2/templates/captcha.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%! from r2.lib.template_helpers import static %>\n\n<%namespace file=\"utils.html\" import=\"error_field\"/>\n<%namespace name=\"utils\" file=\"utils.html\"/>\n\n##${captchagen(thing.iden, thing.error)}\n\n${rounded_captcha()}\n\n<%def name=\"captcha_basics(iden='')\">\n  <%\n    iden = getattr(thing, \"iden\", iden)\n  %>\n  <input name=\"iden\" value=\"${iden}\" type=\"hidden\"/>\n\n  <img class=\"capimage\"\n       alt=\"visual CAPTCHA\"\n       %if hasattr(thing, \"iden\"):\n       src=\"/captcha/${thing.iden}.png\" \n       %else:\n       src=\"${static('kill.png')}\" \n       %endif\n       />\n</%def>\n\n<%def name=\"rounded_captcha()\">\n<%utils:round_field title=\"${_('are you human?')}\" description=\"${_('(sorry)')}\" css_class=\"captcha\">\n    ${captcha_basics()}\n    <input name=\"captcha\" class=\"captcha\" type=\"text\" />\n    ${error_field(\"BAD_CAPTCHA\", \"captcha\")}\n  </%utils:round_field>\n</%def>\n\n<%def name=\"captchagen(iden, error='', tabulate=False, tabular = True, size=60, label=True, show_error = True, tabindex = None)\">\n%if tabulate:\n<table>\n%endif\n  %if tabular:\n  <tr>\n    <td></td>\n    <td>\n  %endif\n  ${captcha_basics(iden)}\n  %if tabular:\n  </td>\n  </tr>\n  <tr>\n     <td align=\"right\">\n  %else:\n     <span class=\"cap-reply\">\n  %endif\n       %if label:\n         <label for=\"captcha_\">${_(\"human?\")}</label>\n       %endif\n     %if tabular:\n     </td>\n     <td>\n     %endif\n          <input class=\"captcha cap-text\" id=\"captcha_\"\n                 name=\"captcha\" type=\"text\" size=\"${size}\"\n                 placeholder=\"type the letters from the image above\"\n                 %if tabindex:\n                   tabindex=\"${tabindex}\"\n                 %endif\n                 />\n     %if tabular:\n     </td>\n     <td>\n     %else:\n     </span>\n     %endif\n     %if show_error:\n       ${error_field(\"BAD_CAPTCHA\", \"captcha\")}\n     %endif\n  %if tabular:\n    </td>\n  </tr>\n  %endif\n%if tabulate:\n</table>\n%endif\n</%def>\n\n"
  },
  {
    "path": "r2/r2/templates/clickgadget.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%namespace file=\"printablebuttons.html\" import=\"simple_button\" />\n\n<div class=\"gadget\">\n  <div class=\"click-gadget\">\n    ${unsafe(thing.content)}\n  </div>\n\n  <div class=\"right\">\n    ${simple_button(_(\"clear\"), \"clear_clicked_items\")}\n  </div>\n</div>\n"
  },
  {
    "path": "r2/r2/templates/clientinfobar.compact",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%inherit file=\"clientinfobar.html\" />\n"
  },
  {
    "path": "r2/r2/templates/clientinfobar.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n   from r2.lib.filters import safemarkdown\n   from r2.lib.template_helpers import make_url_protocol_relative, static\n%>\n<%namespace file=\"utils.html\" import=\"img_link\"/>\n<%def name=\"app_link(app)\" buffered=\"True\">\n  <!-- SC_OFF -->\n  %if app.about_url:\n    <a href=\"${app.about_url}\">${app.name}</a>\n  %else:\n    <b>${app.name}</b>\n  %endif\n  <!-- SC_ON -->\n</%def>\n<%\n  icon_url = make_url_protocol_relative(thing.client.icon_url) or static('defaultapp.png')\n%>\n<div class=\"infobar ${thing.extra_class}\">\n  ${img_link(thing.client.name, icon_url,\n             thing.client.about_url, _class=\"icon\")}\n  <div>\n    <p>\n      ${unsafe(websafe(thing.message) % dict(app=app_link(thing.client)))}\n    </p>\n  </div>\n</div>\n"
  },
  {
    "path": "r2/r2/templates/comment.compact",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n    from r2.config import feature\n    from r2.lib.pages import WrappedUser\n    from r2.lib.template_helpers import add_sr, _wsf\n%>\n<%namespace file=\"printable.compact\" import=\"delete_report_buttons\"/>\n<%namespace file=\"printable.html\" import=\"arrow, score, thing_css_rowclass\"/>\n<%namespace file=\"utils.html\" import=\"plain_link, nsfw_stamp, quarantine_stamp\" />\n<%namespace file=\"utils.compact\" import=\"icon_button\" />\n<%namespace file=\"printablebuttons.html\" import=\"simple_button\"/>\n\n<div class=\"thing comment id-${thing._fullname} ${thing_css_rowclass(thing)}\">\n  %if not thing.deleted:\n    <p class=\"parent\"><a name=\"${thing._id36}\"></a></p>\n  %endif\n  %if c.profilepage:\n    %if thing.link: \n      %if thing.link.title:\n           %if thing.link.is_self:\n             <a href=\"${add_sr(thing.link.url)}\" class=\"title\">\n           %else:\n             <a href=\"${thing.link.url}\" class=\"title\">\n           %endif\n          ${thing.link.title}\n        </a>\n      %else:\n          ${thing.link.url}\n      %endif\n    %endif\n    %if thing.subreddit.quarantine:\n      <span class=\"quarantine-warning\">\n        ${quarantine_stamp()}\n      </span>\n    %endif\n    %if thing.nsfw:\n      <span class=\"nsfw-warning\">\n        ${nsfw_stamp()}\n      </span>\n    %endif\n  %endif\n  <Div class=\"midcol\">\n    ${arrow(thing, 1, thing.likes)}\n    ${arrow(thing, 0, thing.likes == False)}\n  </div>\n  <% \n     like_cls = \"unvoted\"\n     if getattr(thing, \"likes\", None):\n         like_cls = \"likes\"\n     elif getattr(thing, \"likes\", None) is False:\n         like_cls = \"dislikes\"\n   %>\n  <div class=\"entry ${like_cls}\">\n    <div class=\"tagline\">\n     ${WrappedUser(thing.author, thing.attribs, thing, gray=collapse)}&#32;\n     %if thing.score_hidden:\n       [${_(\"score hidden\")}]\n     %else:\n       ${score(thing, tag='span')}\n     %endif\n     &#32;\n     ## thing.timesince is a cache stub\n     ${_wsf(\"%(timeago)s\", timeago=thing.timesince)}\n     % if thing.gilded_message:\n       <span class=\"gilded-icon\" title=\"${thing.gilded_message}\" data-count=\"${thing.gildings}\">\n         % if thing.gildings > 1:\n           x${thing.gildings}\n         % endif\n       </span>\n     % endif\n    </div>\n    <a href=\"javascript:void(0)\" class=\"options_link\"></a>\n    ${thing.usertext}\n        <div class=\"clear options_expando hidden\">\n            <%\n                is_author = (c.user_is_loggedin and thing.author and c.user.name == thing.author.name)\n            %>\n            %if c.user_is_loggedin:\n                ${icon_button(\"Reply\", \"reply-icon\", onclick=\"return reply(this)\", outer_class=\"reply-button\")}\n            %endif\n            ${icon_button(\"Collapse\", \"collapse-icon\", outer_class=\"collapse-button\")}\n            ${icon_button(\"Permalink\", \"permalink-icon\", thing.permalink + \".compact\")}\n            %if c.profilepage:\n                ${icon_button(\"Context\", \"context-icon\", thing.permalink + \".compact?context=3\")}\n            %elif thing.parent_permalink:\n                ${icon_button(\"Parent\", \"parent-icon\", thing.parent_permalink)}\n            %endif\n            %if thing.is_author:\n                ${icon_button(\"Edit\", \"edit-icon\", onclick=\"return edit_usertext(this)\", outer_class=\"edit-button\")}\n            %endif\n        </div>\n  </div>\n  <div class=\"commentspacer\"></div>\n  %if thing.link.contest_mode and hasattr(thing, \"child\") and not thing.parent_id:\n    <button class=\"showreplies newbutton\"\n        onclick=\"$(this).hide();$(this).parent().find('.noncollapsed').show();return false;\">\n        ${_(\"show replies\")}\n    </button>\n    <div class=\"child noncollapsed\" style=\"display:none\">\n  %else:\n    <div class=\"child\">\n  %endif\n  %if thing.childlisting:\n    ${thing.childlisting}\n  %endif\n  </div>\n</div>\n"
  },
  {
    "path": "r2/r2/templates/comment.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n   from r2.config import feature\n   from r2.lib.filters import unsafe\n   from r2.lib.pages.things import CommentButtons\n   from r2.lib.pages import WrappedUser\n%>\n\n<%namespace file=\"utils.html\" import=\"plain_link, thing_timestamp, edited, nsfw_stamp, quarantine_stamp\" />\n<%inherit file=\"comment_skeleton.html\"/>\n\n#####################\n### specific fill-in functions for comment\n##################\n\n<%def name=\"midcol(display=True, cls = '')\">\n${parent.midcol(cls = cls)}\n</%def>\n\n<%def name=\"subreddit()\" buffered=\"True\">\n  ${plain_link(thing.subreddit.name, thing.subreddit_path, _sr_path=False,\n               _class=\"subreddit hover\")}\n</%def>\n\n<%def name=\"link()\" buffered=\"True\">\n  <%\n\n    if thing.link.is_self:\n      url = thing.link.url\n      inbound_tracking_url = thing.link.tracking_link(url, thing, \"title\", site_name=False)\n    else:\n      url = thing.link.url\n      inbound_tracking_url = None\n  %>\n  <a href=\"${url}\" class=\"title\"\n     %if inbound_tracking_url and inbound_tracking_url != url:\n       data-inbound-url=\"${inbound_tracking_url}\"\n       data-href-url=\"${url}\"\n     %endif\n     %if thing.nofollow:\n       rel=\"nofollow\"\n     %endif\n     >\n    ${thing.link.title}\n  </a>\n</%def>\n\n<%def name=\"ParentDiv()\">\n  ${parent.ParentDiv()}\n  %if not thing.deleted:\n    <a name=\"${thing._id36}\"></a>\n  %endif\n  %if c.profilepage:\n    <%\n      tagline_text = conditional_websafe(thing.taglinetext).replace(\" \", \"&#32;\")\n      tagline_attrs = dict(link=self.link(),\n                   subreddit=self.subreddit(),\n                   author=thing.link_author.render())\n    %>\n    ${unsafe(tagline_text % tagline_attrs)}\n  %endif\n</%def>\n\n<%def name=\"tagline()\">\n  <%\n     if c.user_is_admin:\n       show = True\n     else:\n       show = not thing.deleted\n  %>\n\n  <a href=\"javascript:void(0)\" class=\"expand\" onclick=\"return togglecomment(this)\">\n    ${\"[%s]\" % (\"+\" if thing.collapsed else \"–\")}\n  </a>\n\n  %if show:\n     %if thing.deleted:\n       <em>${_(\"deleted comment from\")}</em>&#32;\n     %endif\n     ${WrappedUser(thing.author, thing.attribs, thing)}\n     &#32;\n  %else:\n     <em>${_(\"[deleted]\")}</em>&#32;\n  %endif\n\n  %if thing.collapsed and show and hasattr(thing, \"collapsed_reason\"):\n    <span class=\"collapsed-reason\">${thing.collapsed_reason}</span>\n  %endif\n\n  %if show and thing.score_hidden:\n    <span title=\"${_('this subreddit hides comment scores for %d minutes') % thing.subreddit.comment_score_hide_mins}\">\n      [${_(\"score hidden\")}]\n    </span>&#32;\n  %elif show and not thing.score_hidden:\n    ${unsafe(self.score(thing))}&#32;\n  %endif\n\n  ${thing_timestamp(thing, thing.timesince, live=True, include_tense=True)}\n  ${edited(thing, thing.lastedited)}\n  ${self.gildings()}\n\n  %if thing.is_sticky:\n  &#32;\n  <span class=\"stickied-tagline\" title=\"${_(\"selected by this subreddit's moderators\")}\">${_(\"stickied comment\")}</span>\n  %endif\n\n  &nbsp;\n  <a href=\"javascript:void(0)\" class=\"numchildren\" onclick=\"return togglecomment(this)\">\n    (${thing.numchildren_text})\n  </a>\n\n  ${self.approval_checkmark()}\n  %if getattr(thing, 'savedcategory', None) is not None:\n    ${plain_link(_('category: %s') % thing.savedcategory,\n                 '/user/%s/saved/%s' % (c.user.name, thing.savedcategory),\n                 _class='save-category' + ('' if thing.savedcategory else ' hidden')\n                )}\n  %endif\n</%def>\n\n<%def name=\"Child()\">\n%if not c.profilepage and thing.link.contest_mode and hasattr(thing, \"child\") and not thing.parent_id:\n  <a href=\"#\" class=\"showreplies\"\n     onclick=\"$(this).hide();$(this).parent().find('.noncollapsed').show();return false;\">\n    [${_(\"show replies\")}]\n  </a>\n  <div class=\"child noncollapsed\" style=\"display:none\">\n%else:\n  <div class=\"child\">\n%endif\n  %if thing.childlisting:\n    ${thing.childlisting}\n  %endif\n  </div>\n</%def>\n\n<%def name=\"fullContext()\">\n  <div class=\"md-container-small full-context-info full-context-info-${thing._fullname}\" style=\"display:none;\">\n    <div class=\"md\">\n      <table>\n        <tbody>\n          <tr>\n            <td>${thing.author_slow.name}</td>\n            <td>\n              <div class=\"arrow full-context-vote-${thing._fullname}\"></div>\n            </td>\n            <td><a href=\"\" class=\"full-context-op-${thing._fullname}\"></a></td>\n            <td class=\"full-context-vote-time-${thing._fullname}\"></td>\n          </tr>\n          <tr>\n            <td>${thing.author_slow.name}</td>\n            <td>replied to</td>\n            <td><a href=\"\" class=\"full-context-op-${thing._fullname}\"></a></td>\n            <td class=\"full-context-comment-time-${thing._fullname}\"></td>\n          </tr>\n          <tr>\n            <td><a href=\"\" class=\"full-context-op-${thing._fullname}\"></a></td>\n            <td>\n              <div class=\"arrow full-op-vote-${thing._fullname}\"></div>\n            </td>\n            <td>${thing.author_slow.name}</td>\n            <td class=\"full-op-vote-time-${thing._fullname}\"></td>\n          </tr>\n        </tbody>\n      </table>\n      <div class=\"parent\">\n        <strong>full context:</strong>\n        <p class=\"full-context-comment-${thing._fullname}\"></p>\n      </div>\n    </div>\n  </div>\n</%def>\n\n<%def name=\"commentBody()\">\n  ${parent.commentBody()}\n  %if getattr(thing, 'show_admin_context', None):\n    ${self.fullContext()}\n  %endif\n</%def>\n\n<%def name=\"arrows()\">\n  ${parent.midcol()}\n</%def>\n\n<%def name=\"buttons()\">\n  %if c.profilepage:\n    %if thing.subreddit.quarantine:\n      <li>\n        <span class=\"quarantine-stamp stamp\">${quarantine_stamp()}</span>\n      </li>\n    %endif\n    %if thing.nsfw:\n      <li>\n        <span class=\"nsfw-stamp stamp\">${nsfw_stamp()}</span>\n      </li>\n    %endif\n  %endif\n  ${CommentButtons(thing)}\n  ${self.admintagline()}\n</%def>\n"
  },
  {
    "path": "r2/r2/templates/comment.htmllite",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n   from pylons.i18n import _, ungettext\n   from r2.lib.filters import safemarkdown\n   from r2.lib.template_helpers import get_domain, add_sr\n%>\n<%inherit file=\"printable.htmllite\" />\n\n<%def name=\"parent()\">\n%if c.profilepage:\n<small>\n<a href=\"${thing.link.url}\"\n   %if thing.nofollow:\n     rel=\"nofollow\"\n   %endif\n   >${thing.link.title}</a></small><br \\>\n%endif\n</%def>\n\n<%def name=\"entry()\">\n\n%if thing.deleted:\n\n<small>\n    <b>${_(\"[deleted]\")}</b> ${thing.timesince} ${_(\"ago\")}\n</small>\n\n%else:\n\n<small>\n  <a href=\"${g.default_scheme}://${get_domain()}/user/${thing.author.name}\">\n    <b>${thing.author.name}</b></a>&#32;\n  <span id=\"score_${thing._fullname}\">\n    %if thing.score_hidden:\n      [${_(\"score hidden\")}]\n    %else:\n      ${thing.score} ${ungettext(\"point\", \"points\", thing.score)}\n    %endif\n  </span>&#32;\n  ${thing.timesince}\n  &#32;<a href=\"${add_sr(thing.permalink)}\">permalink</a>\n</small><br/>\n${unsafe(safemarkdown(thing.body, nofollow=thing.nofollow))}\n\n%endif\n\n</%def>\n"
  },
  {
    "path": "r2/r2/templates/comment.iframe",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2014\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n   import simplejson\n   from pylons.i18n import _, ungettext\n   from r2.lib.filters import safemarkdown\n   from r2.lib.strings import Score\n   from r2.lib.template_helpers import get_domain, add_sr\n%>\n<%inherit file=\"printable.iframe\" />\n<%namespace file=\"utils.html\" import=\"thing_timestamp\" />\n\n<%def name=\"parent()\">\n  %if c.profilepage:\n    <a href=\"${thing.link.url}\"\n       %if thing.nofollow:\n         rel=\"nofollow\"\n       %endif\n       >${thing.link.title}</a>\n  %endif\n</%def>\n\n<%def name=\"comment_css_class(thing, hide_edits)\">\n  ${\"reddit-embed-comment-deleted\" if thing.deleted else \"\"}\n  ${\"reddit-embed-comment-edited\" if hide_edits else \"\"}\n</%def>\n\n<%def name=\"entry()\">\n  <%\n    edited_recently = c.embed_config.get(\"thing\").get(\"edited\")\n    hide_edits = (not thing.edits_visible) and edited_recently\n  %>\n  <article class=\"reddit-embed-comment ${comment_css_class(thing, hide_edits)}\">\n    %if thing.deleted:\n      ${_(\"This comment was deleted.\")}\n    %else:\n      <header class=\"reddit-embed-comment-header\">\n        %if thing.author._deleted:\n          <span class=\"reddit-embed-author reddit-embed-author-deleted\">\n            ${_(\"[account deleted]\")}\n          </span>\n        %else:\n          <span class=\"reddit-embed-author\">\n            ${thing.author.name}\n          </span>\n        %endif\n        %if hide_edits:\n          ${_(\"%(name)s's comment was changed.\") % dict(name=\"\")}\n        %else:\n        <div class=\"reddit-embed-comment-meta\">\n          %if not thing.score_hidden:\n            <a href=\"${add_sr(thing.permalink)}?context=3\"\n               class=\"reddit-embed-comment-meta-item reddit-embed-score\"\n               data-redirect-type=\"score\"\n               data-redirect-thing=\"${thing._id}\">\n              ${websafe(Score.safepoints(thing.score))}\n            </a>\n          %endif\n          %if thing.edits_visible and edited_recently:\n            <a href=\"${add_sr(thing.permalink)}?context=3\"\n               class=\"reddit-embed-comment-meta-item reddit-embed-edited\"\n               data-redirect-type=\"edited\"\n               data-redirect-thing=\"${thing._id}\">\n              edited\n            </a>\n          %endif\n          <a href=\"${add_sr(thing.permalink)}?context=3\"\n             class=\"reddit-embed-comment-meta-item reddit-embed-permalink\"\n             data-redirect-type=\"timestamp\"\n             data-redirect-thing=\"${thing._id}\">\n            ${thing_timestamp(thing, thing.timesince, live=True, include_tense=True)}\n          </a>\n        </div>\n        %endif\n      </header>\n      %if hide_edits:\n        <a href=\"${add_sr(thing.permalink)}?context=3\"\n           data-redirect-type=\"hidden_comment\"\n           data-redirect-thing=\"${thing._id}\">\n          ${_(\"View the current version on reddit.\")}\n        </a>\n      %else:\n        <blockquote class=\"reddit-embed-comment-body\">\n          ${unsafe(safemarkdown(thing.body, nofollow=thing.nofollow))}\n        </blockquote>\n        <a class=\"reddit-embed-comment-more\" href=\"javascript:;\" target=\"_self\"\n           data-track-action=\"read_more\">\n          ${_(\"Read more\")}\n        </a>\n      %endif\n    %endif\n  </article>\n</%def>\n"
  },
  {
    "path": "r2/r2/templates/comment.mobile",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n   from pylons.i18n import _, ungettext\n   from r2.lib.filters import safemarkdown\n   from r2.lib.pages import WrappedUser\n%>\n<%inherit file=\"printable.mobile\" />\n\n<%def name=\"parent()\">\n%if c.profilepage:\n<small>\n<a href=\"${thing.link.url}\"\n   %if thing.nofollow:\n     rel=\"nofollow\"\n   %endif\n   >${thing.link.title}</a></small><br \\>\n%endif\n</%def>\n\n<%def name=\"entry()\">\n<div class=\"comment\">\n\n%if thing.deleted:\n  <p>\n    ${_(\"[deleted]\")} ${thing.timesince} ${_(\"ago\")}\n  </p>\n%else:\n\n<p class=\"byline\">\n  ${WrappedUser(thing.author, thing.attribs, thing, gray=collapse)}\n  &nbsp;|<span class=\"score\">\n  %if thing.score_hidden:\n    [${_(\"score hidden\")}]\n  %else:\n    ${thing.score} ${ungettext(\"point\", \"points\", thing.score)} \n  %endif\n  </span> \n  ${_(\"written\")} ${thing.timesince} ${_(\"ago\")}\n</p>\n${unsafe(safemarkdown(thing.body, nofollow=thing.nofollow))}\n\n%endif\n</div>\n\n</%def>\n"
  },
  {
    "path": "r2/r2/templates/comment.xml",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n    from r2.lib.filters import safemarkdown\n    from r2.lib.template_helpers import html_datetime\n    from r2.lib.template_helpers import add_sr\n%>\n<%namespace name=\"utils\" file=\"utils.xml\"/>\n<%\n    if thing.deleted:\n        author = _('[deleted]')\n        comment_body = _('[deleted]')\n    else:\n        if thing.author._deleted:\n            author = _('[deleted]')\n        else:\n            author = thing.author.name\n        comment_body = thing.body\n\n    permalink = add_sr(thing.permalink, force_hostname=True)\n%>\n\n<entry>\n    %if not thing.deleted:\n        <%utils:atom_author author=\"${thing.author}\"/>\n    %endif\n\n    <category term=\"${thing.subreddit.name}\" label=\"/r/${thing.subreddit.name}\" />\n\n    <%utils:atom_content>\n        ${unsafe(safemarkdown(comment_body))}\n    </%utils:atom_content>\n\n    <id>${thing._fullname}</id>\n    <link href=\"${thing.permalink}\"/>\n    <updated>${html_datetime(thing._date)}</updated>\n    <title>/u/${author} ${_(\"on\")} ${thing.link.title}</title>\n</entry>\n\n${hasattr(thing, \"child\") and thing.child or ''}\n"
  },
  {
    "path": "r2/r2/templates/comment_skeleton.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%inherit file=\"printable.html\"/>\n<%namespace file=\"utils.html\" import=\"plain_link\" />\n\n<%def name=\"midcol(display=True, cls = '')\">\n  ${parent.midcol(display=display, cls = cls)}\n</%def>\n\n<%def name=\"tagline()\">\n</%def>\n\n<%def name=\"buttons()\">\n</%def>\n\n<%def name=\"arrows()\">\n</%def>\n\n<%def name=\"commentBody()\">\n  ${thing.usertext}\n</%def>\n\n<%def name=\"thing_css_class(what)\">\n  ${parent.thing_css_class(what)} ${\"stickied\" if getattr(thing, 'is_sticky', None) else \"\"} ${\"collapsed\" if thing.collapsed else \"noncollapsed\"} ${\"collapsed-for-reason\" if hasattr(thing, \"collapsed_reason\") else \"\"}\n</%def>\n\n<%def name=\"thing_data_attributes(what)\">\n  ${parent.thing_data_attributes(what)}\n\n  %if hasattr(what, 'subreddit'):\n    data-subreddit=\"${what.subreddit.name}\"\n    data-subreddit-fullname=\"${what.subreddit._fullname}\"\n  %endif\n\n  %if not getattr(what, 'deleted', False) and getattr(what, 'author', False):\n    data-author=\"${what.author.name}\"\n    data-author-fullname=\"${what.author._fullname}\"\n  %endif\n\n  %if getattr(what, 'can_ban', False):\n    data-can-ban=\"true\"\n  %endif\n</%def>\n\n<%def name=\"entry()\">\n<%\n   from r2.lib.strings import strings\n%>\n\n<p class=\"tagline\">\n  ${self.tagline()}\n</p>\n\n${self.commentBody()}\n\n<ul class=\"flat-list buttons\">\n  ${self.buttons()}\n</ul>\n<div class=\"reportform report-${thing._fullname}\"></div>\n</%def>\n"
  },
  {
    "path": "r2/r2/templates/commentvisitsbox.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n\nfrom r2.lib.utils import timesince\n\n%>\n\n<div class=\"rounded gold-accent comment-visits-box\">\n  <div class=\"title\">\n    ${_(\"Highlight comments posted since previous visit:\")}&#32;\n    <select id=\"comment-visits\">\n      %for i, visit in enumerate(thing.visits):\n        <option value=\"${visit.isoformat()}\"\n          %if i == 0:\n            selected=\"selected\"\n          %endif\n        >${timesince(visit, precision=60)} ${_(\"ago\")}</option>\n      %endfor\n      <option value=\"\">${_(\"no highlighting\")}</option>\n    </select>\n  </div>\n</div>\n"
  },
  {
    "path": "r2/r2/templates/commentvisitsbox.xml",
    "content": ""
  },
  {
    "path": "r2/r2/templates/confirmawardclaim.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n<%!\n    from r2.lib.filters import safemarkdown\n%>\n\n<%namespace file=\"trophycase.html\" import=\"trophy_info\" />\n\n<form action=\"${thing.token.post_url()}\" method=\"post\"\n    class=\"centered confirm-award-claim\">\n\n${unsafe(safemarkdown(_(\"# Claim this award for /u/%s? #\") % thing.user))}\n\n${trophy_info(thing.trophy, False)}\n\n<input type=\"hidden\" name=\"uh\" value=\"${c.modhash}\">\n<button class=\"btn\" type=\"submit\">${_(\"Yes, please!\")}</button>\n\n</form>\n"
  },
  {
    "path": "r2/r2/templates/contactus.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\nfrom r2.lib.template_helpers import static\n%>\n\n  <h1>how can we help you?</h1>\n  <p class=\"info\">\n    reddit is a community, and as such there are a lot of outlets to get help for what ails you.\n  </p>\n\n  <ol class=\"contact-options\">\n    <li id=\"get-help-moderating\">\n      <h2 class=\"button\">get help moderating</h2>\n      <ul class=\"details\">\n        <li>Are you a new moderator?  Need advice?  You'll find a community ready to assist you at&#32;<a href=\"/r/modhelp\">/r/modhelp</a>.</li>\n      </ul>\n    </li>\n    <li id=\"report-a-bug\">\n      <h2 class=\"button\">report a bug</h2>\n      <ul class=\"details\">\n        <li>Check out&#32;<a href=\"/r/bugs\">/r/bugs</a>&#32;for other people with the same problem, or submit your own bug report.</li>\n        <li>If you have an idea for a new feature, tell us about it in&#32;<a href=\"/r/ideasfortheadmins\">/r/ideasfortheadmins</a>.</li>\n      </ul>\n    </li>\n    <li id=\"reddit-trademark\">\n      <h2 class=\"button\">use the reddit trademark</h2>\n      <ul class=\"details\">\n        <li>You'll need a license to use the reddit trademark.  Read our&#32;<a href=\"https://www.reddit.com/wiki/licensing\">licensing page</a>&#32;to find out how to get permission.</li>\n      </ul>\n    </li>\n    <li id=\"press-enquiry\">\n      <h2 class=\"button\">make a press enquiry</h2>\n      <ul class=\"details\">\n        <li>You can email us at&#32;<a href=\"mailto:press@reddit.com\">press@reddit.com</a>&#32;or call us at&#32;<a href=\"tel:+1-424-234-9948\">+1 424-234-9948</a>.</li>\n      </ul>\n    </li>\n    <li id=\"advertise\">\n      <h2 class=\"button\">advertise on reddit</h2>\n      <ul class=\"details\">\n        <li>Subscribe to&#32;<a href=\"/r/selfserve\">/r/selfserve</a>&#32;to talk with other advertisers about advertising on reddit.</li>\n        <li>Check out&#32;<a href=\"/r/ads\">/r/ads</a>&#32;to see the most popular image ads on reddit.</li>\n        <li>Reach the reddit advertising team at&#32;<a href=\"mailto:advertising@reddit.com\">advertising@reddit.com</a>.</li>\n        <li>Learn more about advertising products and best practices at&#32;<a href=\"/advertising\">reddit.com/advertising</a>.</li>\n      </ul>\n    </li>\n    <li id=\"ask-a-question\">\n      <h2 class=\"button\">ask a general question</h2>\n      <ul class=\"details\">\n        <li>Maybe you want to&#32;<a href=\"/r/askreddit\">/r/askreddit</a>?  Or for help try making a post at&#32;<a href=\"/r/help\">/r/help</a>.</li>\n        <li>Need help with a&#32;<a href=\"https://redditgifts.com/exchanges\">redditgifts exchange</a>? Email&#32;<a href=\"mailto:support@redditgifts.com\">support@redditgifts.com</a>.</li>\n        <li>Got a question about&#32;<a href=\"/gold/about\">reddit gold</a>? Please email&#32;<a href=\"mailto:${g.goldsupport_email}\">${g.goldsupport_email}</a>.</li>\n        <li>Anything we didn't cover? Email us at&#32;<a href=\"mailto:contact@reddit.com\">contact@reddit.com</a>&#32;and include your reddit username if you have one.</li>\n      </ul>\n    </li>\n    <li id=\"message-the-admins\">\n      <h2 class=\"button\">message the admins</h2>\n      <ul class=\"details\">\n        <li>Need to contact the admins? You can message them&#32;<a href=\"/message/compose?to=%2Fr%2Freddit.com\">here</a>.</li>\n        <li>Need to file a&#32;<a href=\"/help/useragreement#section_dmca\">DMCA</a>&#32;takedown request? Please email&#32;<a href=\"mailto:dmca@reddit.com\">dmca@reddit.com</a>&#32;with a link to the content on reddit and all pertinent information.</li>\n      </ul>\n    </li>\n  </ol>\n\n  <img class=\"space-snoo\" title=\"${_(\"\\\"In 5-billion years the Sun will expand and engulf our orbit as the charred ember that was once Earth vaporizes. Have a nice day.\\\"\")}\" alt=\"\" src=\"${static('space-snoo.png')}\">\n\n<script type=\"text/javascript\">\n\n$('.contact-options').on('click', 'h2', function() {\n  var toggled_details = $(this).siblings(\".details\");\n  if (toggled_details.is(\":visible\")){\n    toggled_details.slideUp();\n  } else {\n    $(\".details\").slideUp();\n    toggled_details.slideDown();\n  }\n});\n</script>\n"
  },
  {
    "path": "r2/r2/templates/createsubreddit.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n   from r2.config import feature\n   from r2.lib.filters import keep_space\n   from r2.lib.menus import menu\n   from r2.lib.pages import UserText\n   from r2.lib.strings import strings\n%>\n<%namespace file=\"utils.html\" import=\"_md, error_field, language_tool, plain_link\"/>\n<%namespace file=\"utils.html\" import=\"image_upload\"/>\n<%namespace file=\"printablebuttons.html\" import=\"toggle_button, simple_button\" />\n\n<%namespace name=\"utils\" file=\"utils.html\"/>\n \n%if not thing.site and not c.user.can_create_subreddit:\n<div class=\"infobar\">${_md(\"\"\"\n# your account is too new to create a subreddit\n    \ntry participating in other communities on reddit for a little while first before creating your own. please post to /r/help if you need more information about this restriction, or if you require a specific exemption you can [contact the admins](/contact) to request one.\n\"\"\")}</div>\n%else:\n\n<div class=\"create-reddit fancy-settings thing\"\n     %if thing.site:\n       id=\"${thing.site._fullname}\"\n     %endif\n     >\n\n<div class=\"pretty-form\" id=\"sr-form\">\n\n%if not thing.site:\n    <%utils:line_field title=\"${_('name')}\"\n                        description='${_(\"no spaces, e.g., \\\"books\\\" or \\\"bookclub\\\". avoid using solely trademarked names, e.g. use \\\"FansOfAcme\\\" instead of \\\"Acme\\\". once chosen, this name cannot be changed.\")}'>\n      <div class=\"usertext-edit\">\n        <input type=\"text\" name=\"name\" id=\"name\" class=\"text\"\n              value=\"${thing.name}\"/>\n        %if not thing.site:\n          ${error_field(\"SUBREDDIT_EXISTS\", \"name\")}\n          ${error_field(\"BAD_SR_NAME\", \"name\")}\n        %endif\n      </div>\n    </%utils:line_field>\n%else:\n  <input type=\"hidden\" name=\"sr\" id=\"name\" value=\"${thing.site._fullname}\"/>\n%endif\n\n  <%utils:line_field title=\"${_('title')}\"\n       description='${_(\"e.g., books: made from trees or pixels. recommendations, news, or thoughts\")}'>\n    <div class=\"usertext-edit\">\n      %if thing.site:\n        <input id=\"title\" type=\"text\" name=\"title\" class=\"text\"\n               value=\"${thing.site.title}\"/>\n      %else:\n        <input id=\"title\" type=\"text\" name=\"title\" class=\"text\" />\n      %endif\n      ${error_field(\"NO_TEXT\", \"title\")}\n      ${error_field(\"TOO_LONG\", \"title\")}\n    </div>\n  </%utils:line_field>\n\n  <%utils:line_field title=\"${_('description')}\" css_class=\"usertext\"\n                     description=\"${_('Appears in search results and social media links. 500 characters max.')}\">\n    <div class=\"usertext-edit md-container\">\n      <div class=\"md\">\n        <textarea rows=\"1\" cols=\"1\" name=\"public_description\" class=\"usertext\">\n          %if thing.site and thing.site.public_description:\n            ${keep_space(thing.site.public_description or \"\")}\n          %endif\n        </textarea>\n        <div class=\"bottom-area\">\n          ${error_field(\"TOO_LONG\", \"public_description\")}\n        </div>\n      </div>\n    </div>\n  </%utils:line_field>\n\n  <%utils:line_field title=\"${_('sidebar')}\" css_class=\"usertext\"\n                     description=\"${_('shown in the sidebar of your subreddit. 5120 characters max.')}\">\n    %if thing.site and thing.site.description:\n     ${UserText(None, text=thing.site.description or \"\", editable=True, creating=True, name=\"description\", have_form=False)}\n    %else:\n     ${UserText(None, text=\"\", creating=True, name=\"description\", have_form=False)}\n    %endif\n  </%utils:line_field>\n\n  <%utils:line_field title=\"${_('submission text')}\" css_class=\"usertext\"\n                     description=\"${_('text to show on submission page. 1024 characters max.')}\">\n    %if thing.site and thing.site.submit_text:\n     ${UserText(None, text=thing.site.submit_text or \"\", editable=True, creating=True, name=\"submit_text\", have_form=False)}\n    %else:\n     ${UserText(None, text=\"\", creating=True, name=\"submit_text\", have_form=False)}\n    %endif\n  </%utils:line_field>\n\n  <%utils:line_field title=\"${_('language')}\">\n    <div class=\"delete-field\">\n      <%\n         default_lang = thing.site and thing.site.lang or c.lang or ''\n         default_lang = default_lang.split('-')[0]\n         default_lang = g.lang if len(default_lang) != 2 else default_lang\n       %>\n      ${language_tool(all_langs = True, default_lang = default_lang)}\n    </div>\n  </%utils:line_field>\n\n  <%utils:line_field title=\"${_('type')}\">\n    <div class=\"delete-field\">\n      <table>\n      ${utils.radio_type('type', \"public\", _(\"public\"),\n                         _(\"anyone can view and submit\"),\n                         (not thing.site or thing.site.type=='public'))}\n      ${utils.radio_type('type', \"restricted\", _(\"restricted\"),\n                         _(\"anyone can view, but only some are approved to submit links\"),\n                         (thing.site and thing.site.type=='restricted'))}\n      ${utils.radio_type('type', \"private\", _(\"private\"),\n                         _(\"only approved members can view and submit\"),\n                         (thing.site and thing.site.type=='private'))}\n\n      <% is_archived = thing.site and thing.site.type == 'archived' %>\n      %if c.user_is_admin or is_archived:\n        ${utils.radio_type('type', \"archived\", _(\"archived\"),\n                           _(\"anyone can view, but submissions are no longer accepted\"),\n                           is_archived)}\n      %endif\n\n      <% is_gold_restricted = thing.site and thing.site.type == 'gold_restricted' %>\n      %if c.user_is_admin or is_gold_restricted:\n        ${utils.radio_type('type', \"gold_restricted\", _(\"gold restricted\"),\n                           _(\"anyone can view, but only reddit gold members can submit or comment\"),\n                           is_gold_restricted)}\n      %endif\n\n      <% is_employees_only = thing.site and thing.site.type == 'employees_only' %>\n      %if c.user.employee:\n        ${utils.radio_type('type', \"employees_only\", _(\"employees only\"),\n                           _(\"only reddit employees can view; the employee list is pulled from live config\"),\n                           is_employees_only)}\n      %endif\n\n      <% \n          is_gold_only = thing.site and thing.site.type == 'gold_only'\n          can_set_gold_only = (is_gold_only or c.user_is_admin or\n                  (not thing.site and (c.user.gold or c.user.gold_charter)))\n          if is_gold_only:\n              hover_title = \"[!]\"\n              hover_text = _('If you switch this subreddit to another type, you will not be able to switch back to \"gold only\" without the assistance of an admin')\n          else:\n              hover_title = \"[?]\"\n              hover_text = unsafe(capture(_md, 'BETA: Subreddits can be created as \"gold only\" during creation by a user that has gold. You can find more info about this feature [here](/gold/about#gold-only-subreddits)'))\n      %>\n      ${utils.radio_type('type', \"gold_only\", _(\"gold only\"),\n              _(\"only reddit gold members can view and submit\"),\n              checked=is_gold_only, disabled=not can_set_gold_only, hover_title=hover_title,\n              hover_text=hover_text)}\n      </table>\n      ${error_field(\"INVALID_OPTION\", \"type\")}\n      ${error_field(\"ADMIN_REQUIRED\", \"type\")}\n      ${error_field(\"GOLD_REQUIRED\", \"type\")}\n      ${error_field(\"CANT_CONVERT_TO_GOLD_ONLY\", \"type\")}\n    </div>\n  </%utils:line_field>\n\n  <%utils:line_field title=\"${_('content options')}\">\n    <div class=\"delete-field\">\n      <table>\n        ${utils.radio_type('link_type', \"any\", _(\"any\"),\n                           _(\"any link type is allowed\"),\n                           (not thing.site or thing.site.link_type=='any'))}\n        ${utils.radio_type('link_type', \"link\", _(\"links only\"),\n                           _(\"only links to external sites are allowed\"),\n                           (thing.site and thing.site.link_type=='link'))}\n        ${utils.radio_type('link_type', \"self\", _(\"text posts only\"),\n                           _(\"only text/self posts are allowed\"),\n                           (thing.site and thing.site.link_type=='self'))}\n      </table>\n      ${error_field(\"INVALID_OPTION\", \"link_type\")}\n    </div>\n    <div class=\"usertext-edit\">\n      <div class=\"delete-field\">\n        <label for=\"submit_link_label\">${_('Custom label for submit link button (blank for default):')}</label>\n        <input id=\"submit_link_label\" type=\"text\" name=\"submit_link_label\" maxlength=\"60\" placeholder=\"${strings.submit_link_label}\"\n            %if thing.site:\n                value=\"${thing.site.submit_link_label}\"\n            %endif\n        >\n      </div>\n      <div class=\"delete-field\">\n        <label for=\"submit_text_label\">${_('Custom label for submit text post button (blank for default):')}</label>\n        <input id=\"submit_text_label\" type=\"text\" name=\"submit_text_label\" maxlength=\"60\" placeholder=\"${strings.submit_text_label}\"\n            %if thing.site:\n                value=\"${thing.site.submit_text_label}\"\n            %endif\n        >\n      </div>\n    </div>\n  </%utils:line_field>\n  <%utils:line_field title=\"${_('wiki')}\">\n    <div class=\"delete-field\">\n      <table>\n        ${utils.radio_type('wikimode', \"disabled\", _(\"disabled\"),\n                           _(\"Wiki is disabled for all users except mods\"),\n                           (not thing.site or thing.site.wikimode == 'disabled'))}\n        ${utils.radio_type('wikimode', \"modonly\", _(\"mod editing\"),\n                           _(\"Only mods, approved wiki contributors, or those on a page's edit list may edit\"),\n                           (thing.site and thing.site.wikimode == 'modonly'))}\n        ${utils.radio_type('wikimode', \"anyone\", _(\"anyone\"),\n                           _(\"Anyone who can submit to the subreddit may edit\"),\n                           (thing.site and thing.site.wikimode == 'anyone'))}\n      </table>\n      ${error_field(\"INVALID_OPTION\", \"wikimode\")}\n    </div>\n    <div class=\"usertext-edit\">\n    <div class=\"delete-field\">\n    <label for=\"wiki_edit_karma\">${_('Subreddit karma required to edit and create wiki pages:')}</label>\n        %if thing.site:\n            <input id=\"wiki_edit_karma\" type=\"text\" name=\"wiki_edit_karma\" \n                   value=\"${thing.site.wiki_edit_karma}\"/>\n        %else:\n            <input id=\"wiki_edit_karma\" type=\"text\" name=\"wiki_edit_karma\" value=\"100\" />\n        %endif\n        ${error_field(\"BAD_NUMBER\", \"wiki_edit_karma\")}\n    </div>\n    <div class=\"delete-field\">\n    <label for=\"wiki_edit_age\">${_('Account age (days) required to edit and create wiki pages:')}</label>\n        %if thing.site:\n            <input id=\"wiki_edit_age\" type=\"text\" name=\"wiki_edit_age\" \n                   value=\"${thing.site.wiki_edit_age}\"/>\n        %else:\n            <input id=\"wiki_edit_age\" type=\"text\" name=\"wiki_edit_age\" value=\"0\" />\n        %endif\n        ${error_field(\"BAD_NUMBER\", \"wiki_edit_age\")}\n    </div>\n    </div>\n  </%utils:line_field>\n  \n  <%utils:line_field title=\"${_('spam filter strength')}\">\n  \t<div class=\"delete-field\">\n        <p class=\"little gray\">${_(\"high is the standard filter, low disables most filtering, all will filter every post initially and they will need to be approved manually to be visible.\")}</p>\n  \t\t<table>\n  \t\t\t<tr>\n  \t\t\t\t<td>${_(\"links\")}:</td>\n  \t\t\t\t<td>\n  \t\t\t\t\t${utils.inline_radio_type('spam_links', 'low', _(\"low\"),\n  \t\t\t\t\t\tchecked=thing.site and thing.site.spam_links == 'low')}\n  \t\t\t\t\t${utils.inline_radio_type('spam_links', 'high', _(\"high\"),\n  \t\t\t\t\t\tchecked=not thing.site or thing.site.spam_links == 'high')}\n  \t\t\t\t\t${utils.inline_radio_type('spam_links', 'all', _(\"all\"),\n  \t\t\t\t\t\tchecked=thing.site and thing.site.spam_links == 'all')}\n\t  \t\t\t</td>\n\t  \t\t</tr>\n\t  \t\t<tr>\n\t  \t\t\t<td>${_(\"self posts\")}:</td>\n\t  \t\t\t<td>\n\t  \t\t\t\t${utils.inline_radio_type('spam_selfposts', 'low', _(\"low\"),\n  \t\t\t\t\t\tchecked=thing.site and thing.site.spam_selfposts == 'low')}\n  \t\t\t\t\t${utils.inline_radio_type('spam_selfposts', 'high', _(\"high\"),\n  \t\t\t\t\t\tchecked=not thing.site or thing.site.spam_selfposts == 'high')}\n  \t\t\t\t\t${utils.inline_radio_type('spam_selfposts', 'all', _(\"all\"),\n  \t\t\t\t\t\tchecked=thing.site and thing.site.spam_selfposts == 'all')}\n\t  \t\t\t</td>\n\t  \t\t</tr>\n\t  \t\t<tr>\n\t  \t\t\t<td>${_(\"comments\")}:</td>\n\t  \t\t\t<td>\n\t  \t\t\t\t${utils.inline_radio_type('spam_comments', 'low', _(\"low\"),\n  \t\t\t\t\t\tchecked=not thing.site or thing.site.spam_comments == 'low')}\n  \t\t\t\t\t${utils.inline_radio_type('spam_comments', 'high', _(\"high\"),\n  \t\t\t\t\t\tchecked=thing.site and thing.site.spam_comments == 'high')}\n  \t\t\t\t\t${utils.inline_radio_type('spam_comments', 'all', _(\"all\"),\n  \t\t\t\t\t\tchecked=thing.site and thing.site.spam_comments == 'all')}\n\t  \t\t\t</td>\n\t  \t\t</tr>\n\t\t</table>\n  \t</div>\n  </%utils:line_field>\n\n  <%utils:line_field title=\"${_('other options')}\">\n    <div class=\"delete-field\">\n      <ul>\n        <li>\n          <input class=\"nomargin\" type=\"checkbox\"\n                 name=\"over_18\" id=\"over_18\"\n                 %if thing.site and thing.site.over_18:\n                   checked=\"checked\"\n                 %endif\n          >\n          <label for=\"over_18\">\n            ${_(\"viewers must be over eighteen years old\")}\n          </label>\n        </li>\n        %if not thing.site or not thing.site.quarantine:\n        <li>\n          <input class=\"nomargin\" type=\"checkbox\"\n                 name=\"allow_top\" id=\"allow_top\"\n                 %if not thing.site or thing.site.allow_top:\n                   checked=\"checked\"\n                 %endif\n          >\n          <label for=\"allow_top\">\n            ${_(\"allow this subreddit to be included /r/all as well as the default and trending lists\")}\n          </label>\n        </li>\n        <li>\n          <input class=\"nomargin\" type=\"checkbox\"\n                 name=\"show_media\" id=\"show_media\"\n                 %if thing.site and thing.site.show_media:\n                   checked=\"checked\"\n                 %endif\n          >\n          <label for=\"show_media\">\n            ${_(\"show thumbnail images of content\")}\n          </label>\n        </li>\n        %if thing.feature_autoexpand_media_previews:\n          <li>\n            <input class=\"nomargin\" type=\"checkbox\"\n                   name=\"show_media_preview\" id=\"show_media_preview\"\n                   %if thing.site and thing.site.show_media_preview:\n                     checked=\"checked\"\n                   %endif\n            >\n            <label for=\"show_media_preview\">\n              ${_(\"expand media previews on comments pages\")}\n            </label>\n          </li>\n        %endif\n        %endif\n        <li>\n          <input class=\"nomargin\" type=\"checkbox\"\n                 name=\"exclude_banned_modqueue\" id=\"exclude_banned_modqueue\"\n                 %if thing.site and thing.site.exclude_banned_modqueue:\n                   checked=\"checked\"\n                 %endif\n          >\n          <label for=\"exclude_banned_modqueue\">\n            ${_(\"exclude posts by site-wide banned users from modqueue/unmoderated\")}\n          </label>\n        </li>\n        <li>\n          <input class=\"nomargin\" type=\"checkbox\"\n                 name=\"public_traffic\" id=\"public_traffic\"\n                 ${thing.site and thing.site.public_traffic and \"checked='checked'\" or \"\"}>\n          <label for=\"public_traffic\">\n            ${_(\"make the traffic stats page available to everyone\")}\n          </label>\n        </li>\n        <li>\n          <input class=\"nomargin\" type=\"checkbox\"\n                 name=\"collapse_deleted_comments\" id=\"collapse_deleted_comments\"\n                 %if thing.site and thing.site.collapse_deleted_comments:\n                   checked=\"checked\"\n                 %endif\n          >\n          <label for=\"collapse_deleted_comments\">\n            ${_(\"collapse deleted and removed comments\")}\n          </label>\n        </li>\n        %if thing.site and thing.site.type == 'gold_only':\n          <li>\n            <input class=\"nomargin\" type=\"checkbox\"\n                   name=\"hide_ads\" id=\"hide_ads\"\n                   %if thing.site.hide_ads:\n                     checked=\"checked\"\n                   %endif\n            >\n            <label class=\"buygold\" for=\"hide_ads\">\n              ${_(\"hide ads (only available for gold only subreddits)\")}\n            </label>\n          </li>\n        %endif\n        ${error_field(\"GOLD_ONLY_SR_REQUIRED\", \"hide_ads\")}\n      </ul>\n      <div class=\"usertext-edit\">\n        <label for=\"suggested_comment_sort\">${_('suggested comment sort')}&#32;<span class=\"gray\">${_('(all comment threads will use this sorting method by default)')}</span></label>\n        <select class=\"nomargin\" name=\"suggested_comment_sort\" id=\"suggested_comment_sort\">\n           <option value=\"\">${_('none (recommended for most subreddits)')}</option>\n           %for sort in thing.comment_sorts:\n             <option ${'selected=\"selected\"' if thing.site and sort == thing.site.suggested_comment_sort else ''} value=\"${sort}\">${getattr(menu, sort, sort)}</option>\n           %endfor\n        </select>\n      </div>\n    </div>\n    <div class=\"usertext-edit\">\n      <div class=\"delete-field\">\n        <label for=\"comment_score_hide_mins\">${_('Minutes to hide comment scores:')}</label>\n            %if thing.site:\n                <input id=\"comment_score_hide_mins\" type=\"text\" name=\"comment_score_hide_mins\" placeholder=\"0\" \n                       value=\"${thing.site.comment_score_hide_mins}\" />\n            %else:\n                <input id=\"comment_score_hide_mins\" type=\"text\" name=\"comment_score_hide_mins\" value=\"0\" placeholder=\"0\" />\n            %endif\n            ${error_field(\"BAD_NUMBER\", \"comment_score_hide_mins\")}\n      </div>\n    </div>\n  </%utils:line_field>\n\n% if thing.site and thing.site.domain != thing.site._defaults['domain']:\n  <%utils:line_field title=\"${_('domain')}\" css_class=\"usertext\">\n    <div class=\"usertext-edit\">\n      %if thing.site:\n        <input id=\"domain\" type=\"text\" name=\"domain\" class=\"text\"\n               value=\"${getattr(thing.site, 'domain', None) or \"\"}\"/>\n      %else:\n        <input id=\"domain\" type=\"text\" name=\"domain\" class=\"text\" />\n      %endif\n      <div class=\"bottom-area\">\n        ${toggle_button(\"help-toggle\", _(\"what's this?\"), _(\"hide help\"),\n            \"helpon\", \"helpoff\")}\n      </div>\n      <div class=\"infobar markhelp md\" style=\"display: none\">\n        ${_(\"Own a domain?  Enter it here and then go to your DNS provider and add a CNAME record aliasing your domain to rhs.reddit.com. You will be able to access your reddit through your domain.\")}\n      </div>\n    </div>\n  </%utils:line_field>\n% endif\n\n%if thing.site:\n    <%utils:line_field title=\"${_('look and feel')}\">\n      <ul class=\"upload\">\n        <li>\n        ${plain_link(_(\"edit the stylesheet\"),\n                      \"/about/stylesheet\",\n                      _sr_path = True)}\n        &#32;\n        <span class=\"gray\">(${_(\"leaves this page\")})</span>\n        </li>\n        %if thing.allow_image_upload:\n          <li>\n            <label for=\"header-title\">header mouseover text:</label>\n            <input type=\"text\" name=\"header-title\" id=\"header-title\"\n                   value=\"${thing.site.header_title}\"\n                   />\n          </li>\n          <li>\n            <%utils:image_upload post_target=\"/api/upload_sr_img\" \n                                 current_image=\"${thing.site.header}\"\n                                 label=\"${_('upload header image')}\"\n                                 ask_type=\"${True}\">\n              <br/>\n              <button id=\"delete-img\" class=\"delete-img\"\n                      %if not thing.site.header:\n                         style=\"display: none;\"\n                      %endif\n                      onclick=\"return post_form(this.form, 'delete_sr_header');\">\n                ${_('restore default header')}\n              </button>\n              <div class=\"clearleft\"></div>\n              <input type=\"hidden\" name=\"uh\" value=\"${c.modhash}\" />\n              <input type=\"hidden\" name=\"r\"  value=\"${c.site.name}\" />\n              <input type=\"hidden\" name=\"header\" value=\"1\" />\n    \n              <script type=\"text/javascript\">\n                function on_image_success(img) {\n                   $(\"#header-img\").log().attr(\"src\", img.attr(\"src\"));\n                }\n              </script>\n            </%utils:image_upload>\n          </li>\n        %endif\n      </ul>\n    </%utils:line_field>\n%endif\n\n%if thing.site and feature.is_enabled('related_subreddits'):\n  <%utils:line_field title=\"${_('related subreddits')}\">\n    <div class=\"delete-field\">\n      <p class=\"little gray\">${_('similar or related subreddits.')}</p>\n      <ul id=\"related-srs\" class=\"subreddits\">\n        <li id=\"sr-template\" style=\"display:none\">\n          <a href=\"\"></a>\n          <button class=\"remove-sr\" onclick=\"remove_related_sr(this.parentNode)\">${_('remove')}</button>\n        </li>\n        %for sr in thing.site.related_subreddits:\n          %if sr:\n            <li data-name=\"${sr.lower()}\">\n              <a href=\"/r/${sr}\">/r/${sr}</a>\n              <button class=\"remove-sr\" onclick=\"remove_related_sr(this.parentNode)\">${_('remove')}</button>\n            </li>\n          %endif\n        %endfor\n      </ul>\n      %if hasattr(thing, 'subreddit_selector'):\n      <form id=\"add-related-sr\" onsubmit=\"return add_related_sr(this)\">\n        ${thing.subreddit_selector}\n      </form>\n      %endif\n      <textarea id=\"related_subreddits\" name=\"related_subreddits\" style=\"display:none;\">\n        ${keep_space('\\n'.join(thing.site.related_subreddits))}\n      </textarea>\n      ${error_field(\"BAD_SR_NAME\", \"related_subreddits\")}\n      ${error_field(\"TOO_MANY_SUBREDDITS\", \"related_subreddits\")}\n      ${error_field(\"SUBREDDIT_NOEXIST\", \"related_subreddits\")}\n      <script type=\"text/javascript\">\n        function add_related_sr(form) {\n          $('.SUBREDDIT_NOEXIST.field-sr').hide();\n\n          var sr_name = form.sr.value.trim();\n          if (sr_name) {\n            $.post('/api/search_reddit_names.json', { query: sr_name, exact: true }, 'json')\n            .done(function(r) {\n              var sr_name = r.names[0];\n              var sr_path = '/r/' + sr_name;\n\n              var $sr_list = $('#related-srs');\n              var $sr_item = $sr_list.find('[data-name=' + sr_name.toLowerCase() + ']');\n              if (!$sr_item.length) {\n                $sr_item = $('#sr-template').clone().removeAttr('id');\n                $sr_item.data('name', sr_name.toLowerCase()).show();\n                $sr_item.find('a').attr('href', sr_path).text(sr_path);\n              }\n              $sr_item.appendTo($sr_list);\n\n              var re = new RegExp('(^|\\n)' + sr_name + '(\\n|$)', 'gim');\n              var $input = $('#related_subreddits');\n              $input.val($input.val().replace(re, '').trim() + '\\n' + sr_name);\n\n              $('#sr-autocomplete').val('');\n            })\n            .fail(function() {\n              $('.SUBREDDIT_NOEXIST.field-sr').text(r._(\"that subreddit doesn't exist\")).show();\n            });\n          }\n\n          return false;\n        }\n\n        function remove_related_sr(item) {\n          var $sr_item = $(item);\n          var sr_name = $sr_item.data('name');\n\n          $sr_item.remove();\n\n          var re = new RegExp('(^|\\n)' + sr_name + '(\\n|$)', 'gim');\n          var $input = $('#related_subreddits');\n          $input.val($input.val().replace(re, '\\n').trim());\n        }\n      </script>\n    </div>\n  </%utils:line_field>\n%endif\n\n%if feature.is_enabled('mobile_settings'):\n  <%utils:line_field title=\"${_('mobile look and feel')}\" css_class=\"mobile\">\n    <ul class=\"upload\">\n      %if thing.site:\n      <li>\n        <p><label>${_('icon image')}</label> <span class=\"gray\">${_('icon must be 256x256 pixels. PNG or JPG only.')}</span></p>\n        <%utils:image_upload post_target=\"/api/upload_sr_img\"\n                             current_image=\"${thing.site.icon_img}\"\n                             form_id='icon-upload'>\n          <button id=\"delete-icon\" class=\"delete-img\"\n                  %if not thing.site.icon_img:\n                    style=\"display: none;\"\n                  %endif\n                  onclick=\"return post_form(this.form, 'delete_sr_icon');\">\n            ${_('remove custom icon image')}\n          </button>\n          <div class=\"clearleft\"></div>\n          <input type=\"hidden\" name=\"uh\" value=\"${c.modhash}\">\n          <input type=\"hidden\" name=\"r\"  value=\"${c.site.name}\">\n          <input type=\"hidden\" name=\"upload_type\" value=\"icon\">\n        </%utils:image_upload>\n      </li>\n      <li>\n        <p><label>${_('header image')}</label> <span class=\"gray\">${_('header should have 10:3 aspect ratio. PNG or JPG only.')}</span></p>\n        <p class=\"little gray\">${_('minimum size: 640x192px')} / ${_('maximum size: 1280x384px')}</span></p>\n        <%utils:image_upload post_target=\"/api/upload_sr_img\"\n                             current_image=\"${thing.site.banner_img}\"\n                             form_id='banner-upload'>\n          <button id=\"delete-banner\" class=\"delete-img\"\n                  %if not thing.site.banner_img:\n                    style=\"display: none;\"\n                  %endif\n                  onclick=\"return post_form(this.form, 'delete_sr_banner');\">\n            ${_('remove custom header image')}\n          </button>\n          <div class=\"clearleft\"></div>\n          <input type=\"hidden\" name=\"uh\" value=\"${c.modhash}\">\n          <input type=\"hidden\" name=\"r\"  value=\"${c.site.name}\">\n          <input type=\"hidden\" name=\"upload_type\" value=\"banner\">\n        </%utils:image_upload>\n      </li>\n      %endif\n      <li>\n        <p><label>${_('color')}</label> <span class=\"gray\">${_('used as a thematic color for your subreddit on mobile')}</span></p>\n        <ul class=\"colors\">\n          %for color, color_name in thing.color_options.iteritems():\n            %if color:\n            <li>\n              <label>\n                <input type=\"radio\" name=\"key_color\" value=\"${color}\"\n                       %if c.site.key_color.lower() == color.lower():\n                         checked=\"checked\"\n                       %endif\n                >\n                <div class=\"swatch\" style=\"background-color: ${color}\"></div>\n                ${_(color_name)}\n              </label>\n            </li>\n            %endif\n          %endfor\n        </ul>\n      </li>\n    </ul>\n  </%utils:line_field>\n%endif\n\n%if not thing.site:\n  ${thing.captcha}\n%endif\n\n  <div class=\"save-button\">\n  <%\n     if thing.site:\n         name = \"edit\"\n         text = _(\"save options\")\n     else:\n         name = \"create\"\n         text = _(\"create\")\n  %>\n  <button name=\"${name}\" class=\"btn\" type=\"button\"\n          onclick=\"return post_pseudo_form('#sr-form', 'site_admin')\">\n    ${text}\n  </button>\n  &#32;\n  <span class=\"status error\" style=\"display:none\"></span>\n  ${error_field(\"RATELIMIT\", \"ratelimit\")}\n  ${error_field(\"CANT_CREATE_SR\", \"\")}\n  </div>\n</div>\n</div>\n\n%endif\n\n%if thing.site:\n<script type=\"text/javascript\">\n(function(){\n  var dirty = {};\n\n  $(window).on('beforeunload', function(e) {\n    if ($('#sr-form .status:visible').length) {\n      return;\n    }\n    for (field in dirty) {\n      if (dirty[field]) {\n        return r._('There are unsaved changes. Are you sure you want to leave this page?');\n      }\n    }\n  });\n\n  $('#sr-form').on('change', function(e) {\n    var input = e.target;\n    if (input.tagName == 'SELECT') {\n      var $default = $('option[selected]', input);\n      var $current = $('option:selected', input);\n      dirty[input.name] = !$default.is($current);\n    } else if (input.type == 'checkbox' || input.type == 'radio') {\n      dirty[input.name] = input.checked ? !input.defaultChecked : input.defaultChecked;\n    } else if (input.type == 'file') {\n      /* ignore */\n    } else {\n      dirty[input.name] = input.value != input.defaultValue;\n    }\n    if (!dirty[input.name]) {\n      delete dirty[input.name];\n    }\n  });\n})();\n</script>\n%endif\n"
  },
  {
    "path": "r2/r2/templates/creditgild.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n   from r2.lib.strings import strings\n%>\n\n<%namespace file=\"utils.html\" import=\"md, _mdf\"/>\n<%namespace file=\"goldpayment.html\" import=\"stripe_form\"/>\n\n<div class=\"gold-wrap\">\n  <h1 class=\"gold-banner\"><a href=\"/gold\">${_('reddit gold')}</a></h1>\n\n  <div class=\"fancy\">\n    <div class=\"fancy-inner\">\n      <div class=\"fancy-content\">\n        <div class=\"gold-form gold-payment\">\n          <div class=\"container\">\n            <h2 class=\"sidelines\"><span>${_('In Summation')}</span></h2>\n\n            <div class=\"transaction-summary\">\n              ${md(thing.summary)}\n              <div class=\"gift-message\">\n                ${md(thing.description, wrap=True)}\n              </div>\n\n              ${_mdf(strings.gold_summary_gilding_page_footer, price=thing.price)}\n\n              ${stripe_form(display=True)}\n            </div>\n          </div>\n          <span role=\"presentation\" class=\"gold-snoo\" title=\"${_('Felicitations on this momentous occasion!')}\"></span>\n        </div>\n      </div>\n    </div>\n  </div>\n</div>\n"
  },
  {
    "path": "r2/r2/templates/crossdomain.xml",
    "content": "<?xml version=\"1.0\"?>\n<!DOCTYPE cross-domain-policy SYSTEM \"http://www.macromedia.com/xml/dtds/cross-domain-policy.dtd\">\n<cross-domain-policy>\n  <site-control permitted-cross-domain-policies=\"master-only\"/>\n</cross-domain-policy>\n"
  },
  {
    "path": "r2/r2/templates/csserror.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<% error = thing.error %>\n<li>\n  %if hasattr(error,'line'):\n    <a href=\"javascript:void(0)\"\n       onclick=\"$('#stylesheet_contents').select_line(${error.line}); return false;\">\n      [line ${error.line}]\n  %endif\n\n      ${thing.message}\n\n  %if hasattr(error,'offending_line'):\n    <div>\n      <tt>\n        <pre>${error.offending_line}</pre>\n      </tt>\n    </div>\n  %endif\n\n  %if hasattr(error,'line'):\n    </a>\n  %endif\n\n</li>\n"
  },
  {
    "path": "r2/r2/templates/debugfooter.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<p class=\"bottommenu debuginfo\"><span class=\"icon\">&pi;</span>&nbsp;<span class=\"content\">Rendered by PID ${g.reddit_pid} on ${g.reddit_host} at ${c.start_time} running ${g.short_version} ${c.location_info}.</span></p>\n"
  },
  {
    "path": "r2/r2/templates/deleteduserinterstitial.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n  from r2.lib.template_helpers import static \n%>\n\n<%inherit file=\"interstitial.html\"/>\n\n<%def name=\"interstitial_image_attrs()\">\n  src=\"${static('interstitial-image-deleted-user.png')}\"\n  alt=\"${_('deleted')}\"\n</%def>\n\n<%def name=\"interstitial_title()\">\n  ${_(\"This user has deleted their account.\")}\n</%def>\n"
  },
  {
    "path": "r2/r2/templates/emailchangeemail.email",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\nThe e-mail address for /u/${thing.user.name} has been changed. This message is\nbeing sent to your old e-mail address only.\n\nIf you did not change your e-mail, please respond to this e-mail\nimmediately.\n"
  },
  {
    "path": "r2/r2/templates/embed.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n${unsafe(thing.content)}\n\n"
  },
  {
    "path": "r2/r2/templates/errorpage.compact",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%\nfrom r2.lib.filters import unsafe, safemarkdown\n%>\n\n<div id=\"classy-error\">\n<h1>${thing.title}</h1>\n${unsafe(safemarkdown(thing.message))}\n</div>\n"
  },
  {
    "path": "r2/r2/templates/errorpage.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%\nfrom r2.lib.template_helpers import static\nfrom r2.lib.filters import unsafe, safemarkdown\n%>\n\n<div id=\"classy-error\" class=\"content\">\n  <img src=\"${static(thing.image_url)}\" alt=\"\" />\n\n  <h1>${thing.title}</h1>\n  <div class=\"errorpage-message\">\n  ${unsafe(safemarkdown(thing.message, wrap=False))}\n  %if thing.explanation:\n    &mdash; ${thing.explanation}\n  %endif\n  </div>\n\n  % if thing.sr_description:\n  <div class=\"errorpage-message sr-description\">\n      <h2>${_(\"a message from the moderators of /r/%s\") % c.site.name}</h2>\n      ${unsafe(safemarkdown(thing.sr_description, wrap=False))}\n  </div>\n  % endif\n\n  % if thing.include_message_mods_link:\n  <div id=\"private-subreddit-message-link\" class=\"errorpage-message\">\n    <a href=\"/message/compose/?to=/r/${c.site.name}\">${_(\"send a message to the moderators\")}</a>\n  </div>\n  % endif\n</div>\n"
  },
  {
    "path": "r2/r2/templates/exploreitem.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n  from r2.lib.pages import SubscribeButton\n  from r2.lib.filters import unsafe, safemarkdown\n  from r2.lib.strings import Score\n%>\n\n<div class=\"explore-item explore-${thing.type}\" data-sr_name=\"${thing.sr.name}\" data-src=\"${thing.src}\">\n  <div class=\"explore-sr\">\n    <span class=\"explore-label\">\n      <span class=\"explore-label-type\">${_(thing.type)}</span> in \n      <a href=\"/r/${thing.sr.name}\" class=\"explore-label-link\" target=\"_blank\">\n        /r/${thing.sr.name}\n      </a>\n    </span>\n    <span class=\"explore-sr-details\">\n      <span>${unsafe(Score.readers(thing.sr._ups))}</span>\n    </span>\n    <span class=\"explore-feedback\">\n      ${SubscribeButton(thing.sr, bubble_class=\"anchor-left explore-subscribe-bubble\")}\n      <span class=\"explore-feedback-dismiss\" title=\"${_('not interested')}\">\n        ${_(\"hide\")}\n      </span>\n    </span>\n  </div>\n  ${thing.link}\n  %if thing.comment:\n  <div class=\"comment\">\n    ${unsafe(safemarkdown(thing.comment.body))}\n    <div class=\"comment-fade\"></div>\n  </div>\n  <a class=\"comment-link\" href=\"${thing.link.make_permalink(thing.sr)}\" target=\"_blank\">\n    ${_(\"more comments\")}\n  </a>\n  %endif\n</div>\n"
  },
  {
    "path": "r2/r2/templates/exploreitemlisting.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n   from r2.lib.template_helpers import format_html\n%>\n<%namespace file=\"utils.html\" import=\"plain_link\" />\n<%\n   _id = (\"_%s\" % thing.parent_name) if hasattr(thing, 'parent_name') else ''\n   cls = \"exploreitemlisting\"\n %>\n\n<div id=\"siteTable${_id}\" class=\"sitetable ${cls}\">\n  <div class=\"explore-header\">\n    <span id=\"explore-settings\">\n      <form method=\"POST\" action=\"/post/explore_settings\">\n        <input type=\"hidden\" name=\"uh\" value=\"${c.modhash}\">\n        <span>\n          <input type=\"checkbox\" name=\"pers\" value=1 ${\"checked\" if thing.settings.personalized else \"\"}>\n            ${_(\"personalized\")}\n          </input>\n          <input type=\"checkbox\" name=\"disc\" value=1 ${\"checked\" if thing.settings.discovery else \"\"}>\n            ${_(\"discovery\")}\n          </input>\n          <input type=\"checkbox\" name=\"ris\" value=1 ${\"checked\" if thing.settings.rising else \"\"}>\n            ${_(\"rising\")}\n          </input>\n          <input type=\"checkbox\" name=\"nsfw\" value=1 ${\"checked\" if thing.settings.nsfw else \"\"}>\n            ${_(\"nsfw\")}\n          </input>\n        </span>\n        <button type=\"submit\">\n          ${_(\"apply\")}\n        </button>\n      </form>\n    </span>\n  </div>\n  %if thing.things:\n      %for a in thing.things:\n        ${a}\n      %endfor\n      <div class=\"nav-buttons\">\n        <span class=\"nextprev\">${_(\"view more:\")}&#32;\n          ${plain_link(format_html(\"%s &rsaquo;\", _(\"reload suggestions\")), request.url, _sr_path=False)}\n        </span>\n      </div>\n  %else:\n    <div class=\"explore-header\">\n      <span class=\"explore-title\">\n        ${_(\"Our robots have no suggestions at the moment.\")}\n      </span>\n    </div>\n    <div class=\"nav-buttons\">\n      <span class=\"nextprev\">\n        ${plain_link(format_html(\"%s &rsaquo;\", _(\"try again\")), \"/explore\", _sr_path=False)}\n      </span>\n    </div>\n  %endif\n</div>\n"
  },
  {
    "path": "r2/r2/templates/filteredinfobar.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2012\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%namespace file=\"utils.html\" import=\"plain_link, classes, md\"/>\n\n<div ${classes(\"titlebox\", \"rounded\", \"filtered-details\", thing.css_class)}\n     data-path=\"${c.site.multi_path}\">\n  <h1 class=\"hover redditname\">\n    ${plain_link(c.site.name, c.site.user_path, _sr_path=False, _class=\"hover\")}\n  </h1>\n\n  <div class=\"usertext\">\n    ${md(_(\"Displaying content from %s, except the following subreddits: \") % c.site.unfiltered_path, wrap=True)}\n  </div>\n\n  <ul class=\"subreddits\">\n  %for sr in c.site.exclude_srs:\n    <li data-name=\"${sr.name}\">\n      <a href=\"/r/${sr.name}\" data-name=\"${sr.name}\">/r/${sr.name}</a>\n      <button class=\"remove-sr\">${_('remove')}</button>\n    </li>\n  %endfor\n  </ul>\n\n  <form class=\"add-sr\">\n    <input type=\"text\" class=\"sr-name\" placeholder=\"${_('filter subreddit')}\"><button class=\"add\">${_('add')}</button>\n    <div class=\"error add-error\"></div>\n  </form>\n\n  ${plain_link(_('View unfiltered %s') % c.site.unfiltered_path, c.site.unfiltered_path, _sr_path=False, _class=\"unfilter\")}\n</div>\n"
  },
  {
    "path": "r2/r2/templates/flairlist.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%namespace name=\"utils\" file=\"utils.html\"/>\n\n<div class=\"flairgrant flairlist\">\n  <%utils:line_field title=\"${_('jump to user (or add):')}\" css_class=\"flair-jump\">\n    <form method=\"get\">\n      <% name = thing.user.name if thing.user else thing.name %>\n      <input type=\"text\" maxlength=\"20\" id=\"flair_jump_name\" name=\"name\"\n             value=\"${name}\">\n      <button type=\"submit\">${_('go')}</button>\n      ${utils.error_field('USER_DOESNT_EXIST', 'name')}\n    </form>\n    %if thing.user or thing.name:\n        ${utils.plain_link(_('back to full list'), '/about/flair',\n                           class_='flairlisthome')}\n    %elif thing.after:\n        ${utils.plain_link(_('back to the beginning'), '/about/flair',\n                           class_='flairlisthome')}\n    %endif\n  </%utils:line_field>\n  <% flair = thing.flair %>\n  %if flair:\n    <div class=\"usertable\">\n      <span class=\"header tagline\">&nbsp;</span>\n      <span class=\"header\">${_('flair text')}</span>\n      <span class=\"header\">${_('css class')}</span>\n      %for row in flair:\n          ${unsafe(row.render())}\n      %endfor\n    </div>\n  %endif\n</div>\n"
  },
  {
    "path": "r2/r2/templates/flairlistrow.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%namespace name=\"utils\" file=\"utils.html\"/>\n\n<%!\nfrom r2.lib.pages import WrappedUser\n%>\n\n<div id=\"flairrow_${thing.user._id36}\" class=\"flairrow\">\n  <span class=\"tagline flaircell\">\n    ${unsafe(WrappedUser(thing.user, force_show_flair=True,\n                         include_flair_selector=True).render())}\n  </span>\n  <form action=\"/post/flair\" id=\"flair-${thing.user._id36}\" method=\"post\"\n        class=\"medium-text flair-entry\">\n    <input type=\"hidden\" name=\"name\" value=\"${thing.user.name}\" />\n    <span class=\"flaircell\">\n      <input type=\"text\" size=\"32\" maxlength=\"64\" name=\"text\"\n             value=\"${thing.flair_text}\" />\n    </span>\n    <span class=\"flaircell\">\n      <input type=\"text\" size=\"32\" maxlength=\"1000\" name=\"css_class\"\n             value=\"${thing.flair_css_class}\" />\n    </span>\n    <button type=\"submit\">${_('save')}</button>\n    <button class=\"flairdeletebtn\">delete</button>\n    <span class=\"status\"></span>\n    ${utils.error_field('BAD_CSS_NAME', 'css_class')}\n    ${utils.error_field('TOO_MUCH_FLAIR_CSS', 'css_class')}\n  </form>\n</div>\n"
  },
  {
    "path": "r2/r2/templates/flairnextlink.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%namespace file=\"utils.html\" import=\"plain_link\"/>\n\n${plain_link(_('prev') if thing.previous else _('next'),\n             '/about/flair?%s=%s'\n             % ('before' if thing.previous else 'after', thing.after),\n             class_='nextprev')}\n%if thing.needs_border:\n    &#32;|&#32;\n%endif\n"
  },
  {
    "path": "r2/r2/templates/flairpane.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%namespace name=\"utils\" file=\"utils.html\"/>\n<div class=\"flair-settings fancy-settings\">\n  <h1>${_(\"flair settings\")} | &#32;<strong>${c.site.name}</strong></h1>\n\n  <div class=\"pretty-form\">\n    <form method=\"post\" action=\"/post/flairconfig\"\n      onsubmit=\"return post_form(this, 'flairconfig');\">\n      <%utils:line_field title=\"${_('flair options')}\">\n        <input type=\"checkbox\"\n            id=\"sr_flair_enabled\"\n            name=\"flair_enabled\"\n            %if thing.flair_enabled:\n              checked=\"checked\"\n            %endif\n            />\n        <label for=\"sr_flair_enabled\">\n            ${_(\"enable user flair in this subreddit\")}\n        </label>\n        <br>\n        <input type=\"checkbox\"\n            id=\"sr_flair_self_assign_enabled\"\n            name=\"flair_self_assign_enabled\"\n            %if thing.flair_self_assign_enabled:\n              checked=\"checked\"\n            %endif\n            />\n        <label for=\"sr_flair_self_assign_enabled\">\n            ${_(\"allow users to assign their own flair\")}\n        </label>\n        <br>\n        <input type=\"checkbox\"\n            id=\"sr_link_flair_self_assign_enabled\"\n            name=\"link_flair_self_assign_enabled\"\n            %if thing.link_flair_self_assign_enabled:\n              checked=\"checked\"\n            %endif\n            />\n        <label for=\"sr_link_flair_self_assign_enabled\">\n            ${_(\"allow submitters to assign their own link flair\")}\n        </label>\n      </%utils:line_field>\n      <%utils:line_field title=\"${_('user flair position')}\">\n        <table class=\"small-field\">\n          ${utils.radio_type('flair_position', \"left\", _(\"left\"),\n                             _(\"position flair to the left of the username\"),\n                             thing.flair_position == 'left')}\n          ${utils.radio_type('flair_position', \"right\", _(\"right\"),\n                             _(\"position flair to the right of the username\"),\n                             thing.flair_position == 'right')}\n        </table>\n      </%utils:line_field>\n      <%utils:line_field title=\"${_('link flair position')}\">\n        <table class=\"small-field\">\n          ${utils.radio_type('link_flair_position', \"\", _(\"none\"),\n                             _(\"don't show link flair\"),\n                             not thing.link_flair_position)}\n          ${utils.radio_type('link_flair_position', \"left\", _(\"left\"),\n                             _(\"position flair to the left of the link\"),\n                             thing.link_flair_position == 'left')}\n          ${utils.radio_type('link_flair_position', \"right\", _(\"right\"),\n                             _(\"position flair to the right of the link\"),\n                             thing.link_flair_position == 'right')}\n        </table>\n      </%utils:line_field>\n      <div class=\"save-button\">\n        <button type=\"submit\">${_(\"save options\")}</button>\n      </div>\n    </form>\n  </div>\n</div>\n${thing.tabs}\n"
  },
  {
    "path": "r2/r2/templates/flairprefs.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n%if thing.sr_flair_enabled:\n  <form class=\"toggle flairtoggle\">\n    <input id=\"flair_enabled\" type=\"checkbox\" name=\"flair_enabled\"\n        %if thing.user_flair_enabled:\n          checked=\"checked\"\n        %endif\n        >\n    <label for=\"flair_enabled\">\n        ${_(\"Show my flair on this subreddit. It looks like:\")}\n    </label>\n  </form>\n  <div class=\"tagline\">${thing.wrapped_user}</div>\n%endif\n\n"
  },
  {
    "path": "r2/r2/templates/flairselector.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<h2>${_(\"select flair\")}</h2>\n%if thing.choices:\n  <div class=\"flairoptionpane\">\n    <ul>\n      %for choice in thing.choices:\n        <%\n          li_class = 'flairsample-%s' % thing.position\n          if choice.flair_text_editable:\n              li_class += ' texteditable'\n          if choice.flair_template_id == thing.matching_template:\n              li_class += ' selected'\n        %>\n        <li class=\"${li_class}\" id=\"${choice.flair_template_id}\">\n          ${choice}\n        </li>\n      %endfor\n    </ul>\n  </div>\n  <form action=\"/post/selectflair\" method=\"post\">\n    <div class=\"flairselection\">\n      <div class=\"flairremove\">\n        (<a href=\"javascript://void(0)\">${_('remove flair')}</a>)\n      </div>\n    </div>\n    <input type=\"hidden\" name=\"flair_template_id\">\n    <div class=\"customizer\">\n      <input type=\"text\" size=\"16\" maxlength=\"64\" name=\"text\">\n    </div>\n    <button type=\"submit\">${_('save')}</button>\n    <span class=\"status\"></span>\n  </form>\n%else:\n  <div class=\"error\">${_(\"flair selection unavailable\")}</div>\n%endif\n"
  },
  {
    "path": "r2/r2/templates/flairselectorlinksample.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%def name=\"flair()\">\n  <span class=\"linkflairlabel\" title=\"${thing.flair_text}\">${thing.flair_text}</span>\n</%def>\n\n<div class=\"linkflair ${' '.join('linkflair-%s' % c for c in thing.flair_css_class.split())}\">\n%if thing.flair_position == 'left':\n  ${flair()}\n%endif\n<%\n  title = thing.title\n  if len(title) > 10:\n      title = title[:7] + '...'\n%>\n  <a class=\"title\" href=\"javascript://void(0)\">${title}</a>\n%if thing.flair_position == 'right':\n  ${flair()}\n%endif\n</div>\n"
  },
  {
    "path": "r2/r2/templates/flairtemplateeditor.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%namespace name=\"utils\" file=\"utils.html\"/>\n\n<div class=\"flairtemplate flairrow\">\n  <form action=\"/post/flairtemplate\"\n        ${'id=%s' % thing.id if thing.id else ''}\n        method=\"post\" class=\"medium-text flair-entry\">\n    %if thing.id:\n      <input type=\"hidden\" name=\"flair_template_id\" value=\"${thing.id}\" />\n    %endif\n    <input type=\"hidden\" name=\"flair_type\" value=\"${thing.flair_type}\" />\n    <span class=\"flaircell flairsample-${thing.position} tagline\">\n      %if thing.text or thing.css_class:\n        ${unsafe(thing.sample.render())}\n      %endif\n    </span>\n    <span class=\"flaircell narrow\">\n      <input type=\"checkbox\" name=\"text_editable\"\n          ${'checked=\"checked\"' if thing.text_editable else ''} />\n    </span>\n    <span class=\"flaircell\">\n      <input type=\"text\" size=\"32\" maxlength=\"64\" name=\"text\"\n             value=\"${thing.text}\" />\n    </span>\n    <span class=\"flaircell\">\n      <input type=\"text\" size=\"32\" maxlength=\"1000\" name=\"css_class\"\n             value=\"${thing.css_class}\" />\n    </span>\n    <button type=\"submit\">save</button>\n    %if thing.id:\n      <button class=\"flairdeletebtn\">delete</button>\n    %endif\n    <span class=\"status\"></span>\n    ${utils.error_field('BAD_CSS_NAME', 'css_class')}\n    ${utils.error_field('TOO_MUCH_FLAIR_CSS', 'css_class')}\n  </form>\n</div>\n"
  },
  {
    "path": "r2/r2/templates/flairtemplatelist.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%namespace file=\"printablebuttons.html\" import=\"ynbutton\"/>\n\n<%!\n    from r2.models import FlairTemplate, USER_FLAIR, LINK_FLAIR\n    from r2.lib.pages.pages import FlairTemplateEditor\n\n    empty_template = FlairTemplate()\n    empty_template._committed = True  # to disable unnecessary warning\n%>\n\n<div class=\"flairlist usertable\">\n  <div class=\"flairrow\">\n    <span class=\"header tagline\">&nbsp;</span>\n    <span class=\"header narrow\">${_('user can edit?')}</span>\n    <span class=\"header\">${_('flair text')}</span>\n    <span class=\"header\">${_('css class')}</span>\n  </div>\n  <div class=\"flairtemplatelist flairlist pretty-form\">\n    %for flair_template in thing.templates:\n      ${flair_template}\n    %endfor\n\n    <%\n    if thing.flair_type == USER_FLAIR:\n        empty_id = 'empty-user-flair-template'\n    elif thing.flair_type == LINK_FLAIR:\n        empty_id = 'empty-link-flair-template'\n    %>\n    <div id=\"${empty_id}\">\n      ${FlairTemplateEditor(empty_template, thing.flair_type)}\n    </div>\n  </div>\n  ${ynbutton(_(\"clear all flair templates\"), _(\"cleared\"),\n             \"clearflairtemplates\",\n             hidden_data=dict(flair_type=thing.flair_type))}\n</div>\n"
  },
  {
    "path": "r2/r2/templates/flairtemplatesample.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%namespace file=\"link.html\" import=\"entry\" />\n\n<%\nfrom r2.models import USER_FLAIR, LINK_FLAIR\n%>\n\n<%def name=\"flair()\">\n  %if thing.flair_template.text or thing.flair_template.css_class:\n    <span class=\"linkflairlabel\">${thing.flair_template.text}</span>\n  %endif\n</%def>\n\n\n%if thing.flair_type == USER_FLAIR:\n  ${thing.wrapped_user}\n%elif thing.flair_type == LINK_FLAIR:\n  <div class=\"thing linkflair ${' '.join('linkflair-' + c for c in thing.flair_template.css_class.split())}\">\n    <p class=\"title\">\n      %if c.site.link_flair_position != 'right':\n        <%call expr=\"flair()\" />\n      %endif\n      <a class=\"title loggedin\" href=\"javascript://void(0)\">Link sample</a>\n      %if c.site.link_flair_position == 'right':\n        <%call expr=\"flair()\" />\n      %endif\n    </p>\n  </div>\n%endif\n"
  },
  {
    "path": "r2/r2/templates/fraudform.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<form id=\"fraud-action-form\" class=\"action-form fraud-action-form rounded\" data-form-action=\"review_fraud\">\n  <input type=\"hidden\" name=\"thing_id\" value=\"thing-fullname\">\n  <small class=\"fraud-reason\"></small>\n  <span class=\"reason-prompt\">\n    ${_('is this fraud?')}\n  </span>\n  <ol>\n    <li>\n      <label>\n        <input type=\"radio\" name=\"fraud\" value=\"True\">${_(\"yes\")}\n      </label>\n    </li>\n    <li>\n      <label>\n        <input type=\"radio\" name=\"fraud\" value=\"False\">${_(\"no\")}\n      </label>\n    </li>\n  </ol>\n  <button type=\"submit\" class=\"btn submit-action-thing\" disabled>\n    ${_(\"submit\")}\n  </button>\n  <button type=\"button\" class=\"btn cancel-action-thing\">\n    ${_(\"cancel\")}\n  </button>\n  <span class=\"status\"></span>\n</form>\n"
  },
  {
    "path": "r2/r2/templates/geotargetnotice.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n   from r2.lib.filters import unsafe, safemarkdown\n%>\n\n<div class=\"geotarget-notice ${thing.targeting_level}\">\n  ${unsafe(safemarkdown(thing.text))}\n</div>\n"
  },
  {
    "path": "r2/r2/templates/gettextheader.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%namespace file=\"translatedstring.html\" import=\"text_input\"/>\n\n<% \n highlight = lambda x: (\"<span class='orangered' style='padding: 0px 1ex 0px 1ex'>\" + x + \"</span>\")\n singular = unsafe(thing._singular(highlight))\n plural = unsafe(thing._plural(highlight))\n %>\n\n%if c.user_is_admin:\n%for indx in xrange(len(thing.headers)):\n<% header = thing.headers[indx] %>\n%if header[0]:\n<tr id=\"tr_${thing.md5}\" style=\"vertical-align:top\">\n  <td style=\"text-align: right; width: 25em; padding-right: 5px\">\n    ${header[0]}\n  </td>\n  <td></td>\n  <td style=\"padding-bottom: 5px\">\n    ${text_input(header[1], True, index = indx, \n                 len=len(header[1]))}\n  </td>\n</tr>\n%endif\n%endfor\n%endif\n"
  },
  {
    "path": "r2/r2/templates/gilding.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%namespace file=\"utils.html\" import=\"_md, _mdf\"/>\n\n<div class=\"gold-wrap\">\n  <h1 class=\"gold-banner\"><a href=\"/gilding\">${_('gilding')}</a></h1>\n\n  <div class=\"fancy\">\n    <div class=\"fancy-inner\">\n        <div class=\"fancy-content\">\n            <div class=\"container\">\n                <dl class=\"gilding-faq\">\n                  <dt class=\"toggle toggled\" id=\"what-is-gilding\">${_('What is gilding?')}</dt>\n                  <dd>\n                    ${_md(\"Giving [reddit gold](/gold/about) is referred to as 'gilding'. You can either give reddit gold directly to a user or gild one of their posts or comments.\")}\n                  </dd>\n\n                  <dt class=\"toggle toggled\" id=\"how-do-i-gild\">${_('How do I gild someone?')}</dt>\n                  <dd>\n                    ${_(\"You can click the 'give gold' link directly below a submission:\")}\n\n                    <div class=\"example\">\n                      <figure class=\"comment-gild\">\n                      </figure>\n                    </div>\n\n                    ${_(\"Or by giving them gold via their userpage:\")}\n\n                    <div class=\"example\">\n                      <figure class=\"userpage-gild\">\n                      </figure>\n                    </div>\n                  </dd>\n\n                  <dt class=\"toggle toggled\" id=\"how-much-does-it-cost\">${_('How much does it cost?')}</dt>\n                  <dd>\n                    ${_mdf(\"A single gilding costs %(price)s. If you plan to do a lot you can get a discount if you purchase some [creddits](/creddits).\", price=g.gold_month_price)}\n                  </dd>\n\n                  <dt class=\"toggle toggled\" id=\"what-are-creddits\">${_('What are creddits?')}</dt>\n                  <dd>${_('Each creddit you have can be converted into one month of reddit gold. Stored as a balance on your account, creddits allow you to give gold without having to enter payment information.')}&#32;${_('Additionally, buying creddits in bulk lowers the cost of each gilding.')}\n                  </dd>\n\n                  <dt class=\"toggle toggled\" id=\"using-creddits\">${_('How do I use creddits?')}</dt>\n                  <dd>${_('Your creddits are listed as a payment method on the checkout page.')}\n\n                  <div class=\"example\">\n                      <figure class=\"using-creddits\">\n                      </figure>\n                    </div>\n                  </dd>\n\n                </dl>\n          </div>\n            <div class=\"buttons\">\n                <a class=\"btn gold-button\" href=\"/creddits\">${_('purchase creddits')}</a>\n            </div>\n        </div>\n    </div>\n  </div>\n</div>\n"
  },
  {
    "path": "r2/r2/templates/gold.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%namespace file=\"utils.html\" import=\"error_field, radio_type, _md\"/>\n<%namespace file=\"utils/gold.html\" import=\"gold_dropdown\"/>\n\n<div class=\"gold-wrap\">\n  <h1 class=\"gold-banner\"><a href=\"/gold\">${_('reddit gold')}</a></h1>\n\n  <div class=\"fancy\">\n    <div class=\"fancy-inner\">\n      <div class=\"fancy-content gold-checkout\">\n        %if c.user_is_loggedin:\n          ${self.gold_loggedin_content()}\n        %else:\n          ${self.gold_loggedout_content()}\n        %endif\n        <section class=\"gold-question\">\n          <h3 class=\"toggle\">${_('What is reddit gold?')}</h3>\n          <div class=\"details hidden\">\n            <div class=\"container\">\n              ${_('reddit gold is our premium membership.  It upgrades your account with access to extra features.')}\n              &#32;\n              <a href=\"/gold/about\">${_('Learn more')}</a>.\n            </div>\n          </div>\n        </section>\n      </div>\n    </div>\n  </div>  \n</div>\n\n<%def name=\"gold_loggedout_content()\">\n  <header>\n    <h2 class=\"loggedout-give-gold sidelines\"><span>${_('Give Gold')}</span></h2>\n    <div class=\"login-note\">${_('Want to buy gold for yourself?')}&#32;<a href=\"/login\" class=\"login-required\">${_(\"You'll need to log in.\")}</a></div>\n  </header>\n\n  <form class=\"loggedout-gold-form\" name=\"loggedout-gold-form\" action=\"/gold/payment\" method=\"get\">\n    <input type=\"hidden\" name=\"goldtype\" value=\"code\">\n    <div id=\"form-options\" class=\"container\">\n      <section>\n        <label>\n          <h3>${_('How many months of gold would you like to give?')}</h3>\n          ${gold_dropdown(\"months\", thing.months, \"months\")}\n        </label>\n\n        <label class=\"loggedout-email\">\n          <input type=\"email\" name=\"email\" class=\"inline\" placeholder=\"enter your email address\" value=\"${thing.email}\">\n          ## weird validation here because we're using GETs: this explicitly checks of email was passed in the URL so that\n          ## the \"no email\" error doesn't show up on first page view.\n          %if 'email' in request.GET and not thing.email:\n            ${error_field(\"NO_EMAIL\", \"email\", \"span\")}\n          %endif\n          ${error_field(\"BAD_EMAIL\", \"email\", \"span\")}\n          <p class=\"hint\">${_(\"(We'll send a code to your email address you can then give to your recipient)\")}</p>\n        </label>\n      </section>\n    </div>\n\n    <section id=\"payment-options\">\n      <div class=\"buttons\">\n        <button type=\"submit\" class=\"btn gold-button\">${_(\"buy reddit gold\")}</button>\n      </div>\n    </section>\n  </form>\n</%def>\n\n<%def name=\"gold_loggedin_content()\">\n  <%\n  is_gift = thing.goldtype in ('code', 'gift')\n\n  active_tab = None\n  if thing.goldtype:\n      if is_gift or thing.goldtype == 'onetime':\n          active_tab = 'onetime'\n      elif thing.goldtype == 'autorenew':\n          active_tab = 'autorenew'\n      else:\n          active_tab = 'creddits'\n  %>\n  <form name=\"gold-form\" class=\"gold-form\" action=\"/gold/payment\" method=\"get\">\n  <input type=\"hidden\" name=\"edit\" id=\"edit\" value=\"true\">\n  <input type=\"hidden\" name=\"goldtype\" id=\"goldtype\" value=\"${thing.goldtype}\">\n\n    <section class=\"tab-chooser\">\n      <h3>${_('What type of reddit gold would you like to purchase?')}</h3>\n      <a href=\"#onetime\" class=\"tab-toggle ${'active' if active_tab == 'onetime' else ''}\">${_('one-time purchase')}</a>\n      <a href=\"#autorenew\" class=\"tab-toggle ${'active' if active_tab == 'autorenew' else ''}\">${_('ongoing subscription')}</a>\n      <a href=\"#creddits\" class=\"tab-toggle ${'active' if active_tab == 'creddits' else ''}\">${_('creddits')}</a>\n    </section>\n\n    <div id=\"form-options\" class=\"container ${'hidden' if not active_tab else ''}\">\n\n      <section id=\"creddits\" class=\"tab ${'active' if active_tab == 'creddits' else ''}\">\n        <h3>${_('How many creddits would you like to buy?')}</h3>\n        ${gold_dropdown(\"num_creddits\", thing.months, somethings=\"creddits\")}\n\n        <section class=\"creddits-explained\">\n          ${_('Stored as a balance on your account, creddits allow you to give gold without having to enter payment information. Each creddit you have can be converted into one month of reddit gold.')}\n          &#32;<a href=\"/gilding#what-are-creddits\">${_('Learn more about using creddits')}</a>.\n        </section>\n      </section>\n\n      <section id=\"autorenew\" class=\"tab ${'active' if active_tab == 'autorenew' else ''}\">\n        <h3>${_('What type of subscription would you like?')}</h3>\n        <ul>\n          <li>${radio_type(\"period\", \"monthly\", _(\"monthly - %s\") % g.gold_month_price, \"\", thing.period == \"monthly\")}</li>\n\n          <li>${radio_type(\"period\", \"yearly\", _(\"yearly - %s (%s/month)\") % (g.gold_year_price, g.gold_year_price / 12),\"\", thing.period != \"monthly\")}</li>\n        </ul>\n      </section>\n\n      <section id=\"onetime\" class=\"tab ${'active' if active_tab == 'onetime' else ''}\">\n        <h3>${_('How many months?')}</h3>\n        <%\n          append_or_somethings = None\n          if c.user_is_loggedin and c.user.gold_creddits > 0:\n              append_or_somethings = \"creddits\"\n        %>\n        ${gold_dropdown(\"months\", thing.months, append_or_somethings=append_or_somethings)}\n\n        <section id=\"give-as-gift\">\n          <ul>\n            <li>\n            <label>\n              <input type=\"radio\" id=\"notgift\" name=\"gift\" value=\"0\" ${\"checked\" if not is_gift else \"\"}>\n              ${_('purchase this reddit gold for myself')}\n            </label>\n            </li>\n            <li>\n            <label>\n              <input type=\"radio\" id=\"gift\" name=\"gift\" value=\"1\" ${\"checked\" if is_gift else \"\"}>\n              ${_('give this reddit gold as a gift')}\n            </label>\n            </li>\n          </ul>\n          <div id=\"gifting-details\" class=\"${'hidden' if not is_gift else ''}\">\n            <ul class=\"indent\">\n              <li>\n                ${radio_type(\"gifttype\", \"code\", _(\"receive gold as a gift code\"), \"\", thing.goldtype == \"code\")}\n              </li>\n              <li>\n                ${radio_type(\"gifttype\", \"gift\", _(\"send gold to a user\"), \"\", thing.goldtype == \"gift\")}\n\n                <div class=\"gift-details ${'hidden' if not thing.goldtype == 'gift' else ''}\" id=\"gifttype-details-gift\">\n\n                  <label>\n                    ${_('who should receive this gold?')}\n                    <input id=\"recipient\" type=\"text\" name=\"recipient\" value=\"${thing.recipient.name if thing.recipient else ''}\" placeholder=\"${_('enter a username')}\" size=\"13\" maxlength=\"20\" class=\"inline\">\n                    ## weird validation here because we're using GETs: this explicitly checks if email was passed in the URL so that\n                    ## the \"no email\" error doesn't show up on first page view.\n                    %if 'recipient' in request.GET:\n                      ${error_field(\"NO_USER\", \"recipient\", \"span\")}\n                      ${error_field(\"USER_DOESNT_EXIST\", \"recipient\", \"span\")}\n                    %endif\n                  </label>\n\n                  <ul class=\"indent\">\n                    <li>\n                      <label>\n                        <input type=\"checkbox\" id=\"signed-false\" name=\"signed\" value=\"false\" ${\"checked\" if not thing.signed else \"\"}>\n                        ${_('make my gift anonymous')}\n                      </label>\n                    </li>\n                    <li>\n                      <label>\n                        <input type=\"checkbox\" id=\"message\" name=\"message\" value=\"message\" ${\"checked\" if thing.giftmessage else \"\"}>\n                        ${_('include a message')}\n                      </label>\n                    </li>\n                    <li>\n                      <textarea rows=\"5\" cols=\"30\" name=\"giftmessage\" id=\"giftmessage\" placeholder=\"${_('enter your message')}\" class=\"giftmessage\" maxlength=\"500\">${thing.giftmessage}</textarea>\n                    </li>\n                  </ul>\n                </div>\n              </li>\n            </ul>\n          </div>\n        </section>\n      </section>\n    </div>\n\n    <section id=\"payment-options\" class=\"${'hidden' if not active_tab else ''}\">\n      <div class=\"buttons\">\n        <button type=\"submit\" class=\"btn gold-button\">${_('continue')}</button>\n      </div>\n    </section>\n  </form>\n\n  <section id=\"redeem-a-code\" class=\"${'hidden' if active_tab else ''}\">\n    <div class=\"sidelines\"><span>${_('or')}</span></div>\n    <form id=\"redeem-form\" action=\"/api/claimgold\" method=\"post\" onsubmit=\"return post_form(this, 'claimgold');\">\n        <input type=\"text\" name=\"code\" value=\"\" placeholder=\"${_('enter a gift code for redemption')}\" maxlength=\"20\">\n        <div class=\"redeem-submit hidden\">\n          <div class=\"buttons\">\n            <button type=\"submit\" class=\"btn gold-button\">${_(\"redeem this code\")}</button>\n          </div>\n          <div class=\"errors\">\n          ${error_field(\"NO_TEXT\", \"code\", \"span\")}\n          ${error_field(\"INVALID_CODE\", \"code\", \"span\")}\n          ${error_field(\"CLAIMED_CODE\", \"code\", \"span\")}\n          </div>\n        </div>\n    </form>\n  </section>\n</%def>\n"
  },
  {
    "path": "r2/r2/templates/goldgiftcodeemail.email",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n${thing.message}\n\n${_(\"Thank you again for your support, and have fun spreading gold!\")}\n"
  },
  {
    "path": "r2/r2/templates/goldonlyinterstitial.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n  from r2.lib.template_helpers import static\n%>\n\n<%namespace file=\"utils.html\" import=\"_md\"/>\n\n<%inherit file=\"interstitial.html\"/>\n\n<%def name=\"interstitial_image_attrs()\">\n  src=\"${static('interstitial-image-gold-only.png')}\"\n  alt=\"${_('gold only')}\"\n</%def>\n\n<%def name=\"interstitial_title()\">\n  ${_(\"You must be a Reddit Gold member to view this super secret community\")}\n</%def>\n\n<%def name=\"interstitial_message()\">\n  ${_md(\"The moderators of this community have set it to [Reddit Gold only](/gold/about#gold-only-subreddits). You must be a [Reddit Gold member](/gold/about) to visit.\")}\n</%def>\n\n<%def name=\"interstitial_buttons()\">\n  <div class=\"buttons\">\n    <a class=\"c-btn c-btn-primary\" href=\"/gold\">\n      ${_(\"get gold\")}\n    </a>\n  </div>\n</%def>\n"
  },
  {
    "path": "r2/r2/templates/goldpayment.compact",
    "content": ""
  },
  {
    "path": "r2/r2/templates/goldpayment.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n<%!\n   from r2.lib.filters import websafe\n   from r2.lib.template_helpers import _ws\n %>\n<%namespace file=\"utils.html\" import=\"md,_md\"/>\n<%\n   clone_class = ''\n   if thing.clone_template:\n     if thing.thing_type == 'comment':\n       clone_class = 'cloneable-comment'\n     else:\n       clone_class = 'cloneable-link'\n%>\n\n<div class=\"gold-wrap ${clone_class}\">\n  <h1 class=\"gold-banner\"><a href=\"${'/gold' if thing.goldtype != 'creddits' else '/creddits'}\">${_('reddit gold')}</a></h1>\n\n  <div class=\"fancy\">\n    <div class=\"fancy-inner\">\n      <div class=\"fancy-content\">\n        <div class=\"gold-form gold-payment\">\n          %if thing.clone_template:\n            <button class=\"close-button\">${_('close')}</button>\n          %endif\n          <div class=\"container\">\n            <h2 class=\"sidelines\"><span>${_('In Summation')}</span></h2>\n\n            <div class=\"transaction-summary\">\n              ${md(thing.summary)}\n\n              <div>\n                ${_md('> By purchasing Reddit Gold, you agree to the [Reddit User Agreement.](/help/useragreement)')}\n              </div>\n\n              %if thing.thing and not thing.clone_template:\n                <div class=\"gift-message\">\n                  ${md(thing.description, wrap=True)}\n                </div>\n              %endif\n\n              %if thing.giftmessage:\n                <p>${_('The following gift note will be attached:')}</p>\n                <div class=\"gift-message\">\n                  ${md(thing.giftmessage, wrap=True)}\n                </div>\n              %endif\n            </div>\n          </div>\n\n          %if thing.clone_template:\n            <ul class=\"indent\">\n              <li>\n                <input type=\"checkbox\" id=\"signed-false\" name=\"signed\" ${'checked' if not c.user.gild_reveal_username else ''}>${_('make my gift anonymous')}\n              </li>\n              <li>\n                <input type=\"checkbox\" id=\"message\" name=\"message\">${_('include a message')}\n              </li>\n              <li>\n                <textarea rows=\"3\" cols=\"50\" id=\"giftmessage\" name=\"giftmessage\" placeholder=\"${_('enter your message')}\" class=\"hidden giftmessage\" maxlength=\"500\">${thing.giftmessage}</textarea>\n              </li>\n            </ul>\n          %endif\n            <div class=\"buttons\">\n              <p>${_(\"Please select a payment method.\")}</p>\n\n              %if thing.can_use_creddits:\n                ${self.creddits_button()}\n              %endif:\n\n              %if thing.paypal_buttonid:\n                ${self.paypal_button()}\n              %endif\n\n              %if thing.coinbase_button_id:\n                ${self.coinbase_button()}\n              %endif\n\n              %if thing.stripe_key:\n                ${self.stripe_button()}\n              %endif\n\n              %if thing.stripe_key and not thing.clone_template:\n                ${self.stripe_form()}\n              %endif\n\n              %if thing.clone_template:\n                <div class=\"note\">\n                  %if not thing.user_creddits:\n                    ${_md(\"Give gold often? Consider [buying creddits to use](/creddits), they're 40% cheaper if purchased in a set of 12.\")}\n                  %endif\n                  ${_md(\"Would you like to [learn more about giving gold](/gilding)?\")}\n                </div>\n              %endif\n          </div>\n          <div class=\"throbber\"></div>\n          <span role=\"presentation\" class=\"gold-snoo\" title=\"${_('Felicitations on this momentous occasion!')}\"></span>\n        </div>\n      </div>\n    </div>\n  </div>\n</div>\n\n<%def name=\"creddits_button()\">\n  <form id=\"giftgold\" action=\"/api/spendcreddits\" method=\"post\"\n        class=\"gold-checkout creddits-gold\"\n        data-vendor=\"creddits\">\n    <input type=\"hidden\" name=\"months\" value=\"${thing.months}\">\n    <input type=\"hidden\" name=\"passthrough\" value=\"${thing.passthrough}\" class=\"passthrough\">\n    <button class=\"btn gold-button\" type=\"button\"><span class=\"snoo-head\"></span>${_(\"creddits\")}</button>\n    <span class=\"status\">\n      <span class=\"remaining\" data-current=\"${thing.months}\" data-total=\"${thing.user_creddits}\" data-template=\"${websafe(_ws('(use %(current)s of your %(total)s creddits)')) % dict(current='<%- current %>', total='<%- total %>')}\">\n      </span>\n    </span>\n  </form>\n</%def>\n\n<%def name=\"paypal_button()\">\n  <form action=\"https://www.paypal.com/cgi-bin/webscr\" method=\"post\"\n        class=\"gold-checkout\"\n        data-vendor=\"paypal\" target=\"${'_blank' if thing.clone_template else '_top'}\">\n    <input type=\"hidden\" name=\"cmd\" value=\"_s-xclick\">\n    <input type=\"hidden\" name=\"custom\" value=\"${thing.passthrough}\" class=\"passthrough\">\n    %if thing.quantity:\n      <input type=\"hidden\" name=\"quantity\" value=\"${thing.quantity}\">\n    %endif\n    <input type=\"hidden\" name=\"hosted_button_id\" value=\"${thing.paypal_buttonid}\">\n    <button type=\"submit\" class=\"btn gold-button\" type=\"button\">${_(\"PayPal\")}</button>\n  </form>\n</%def>\n\n<%def name=\"stripe_button()\">\n  <span class=\"gold-checkout\">\n    <button data-vendor=\"credit card\" class=\"btn stripe-gold gold-button\">${_('Credit Card')}</button>\n    <input type=\"hidden\" name=\"custom\" value=\"${thing.passthrough}\" class=\"passthrough\">\n  </span>\n</%def>\n\n<%def name=\"base_stripe_form()\">\n  <script type=\"text/javascript\" src=\"https://js.stripe.com/v1/\"></script>\n\n  <div id=\"base-stripe-form\"\n        class=\"gold-checkout\"\n        data-vendor=\"stripe\">\n    <div class=\"stripe-note\">\n      <a class=\"icon\" href=\"https://stripe.com/help/security\">powered by stripe</a>\n      <div>${_('Stripe is PCI compliant and your credit card information is sent directly to them.')}</div>\n    </div>\n    <table class=\"credit-card-input\">\n      <tr>\n        <th><label>${_('name')}</label></th>\n        <td><input type=\"text\" autocomplete=\"off\" class=\"card-name\"></td>\n      </tr>\n      <tr>\n        <th><label>${_('card number')}</label></th>\n        <td><input type=\"text\" autocomplete=\"off\" class=\"card-number\"></td>\n      </tr>\n      <tr>\n        <th><label>${_('cvc')}</label></th>\n        <td><input type=\"text\" autocomplete=\"off\" class=\"card-cvc\"></td>\n      </tr>\n      <tr>\n        <th><label>${_('expiration date')}</label></th>\n        <td>\n          <% \n             import datetime\n             months = ['%02d' % m for m in xrange(1, 13)]\n             years = ['%04d' % y for y in xrange(datetime.datetime.now().year,\n                                                 datetime.datetime.now().year + 25)]\n           %>\n          <select class=\"card-expiry-month\" title=${_('month')}>\n            %for m in months:\n              <option value=\"${m}\">${m}</option>\n            %endfor\n          </select>\n          <select class=\"card-expiry-year\" title=${_('year')}>\n            %for y in years:\n              <option value=\"${y}\">${y}</option>\n            %endfor\n          </select>\n        </td>\n      </tr>\n      <tr>\n        <th><label>${_('address line 1')}</label></th>\n        <td><input type=\"text\" class=\"card-address_line1\"></td>\n      </tr>\n      <tr>\n        <th><label>${_('address line 2')}</label></th>\n        <td><input type=\"text\" class=\"card-address_line2\"></td>\n      </tr>\n      <tr>\n        <th><label>${_('city')}</label></th>\n        <td><input type=\"text\" class=\"card-address_city\"></td>\n      </tr>\n      <tr>\n        <th><label>${_('state/province')}</label></th>\n        <td><input type=\"text\" class=\"card-address_state\"></td>\n      </tr>\n      <tr>\n        <th><label>${_('country')}</label></th>\n        <td><input type=\"text\" class=\"card-address_country\"></td>\n      </tr>\n      <tr>\n        <th><label>${_('zip')}</label></th>\n        <td><input type=\"text\" class=\"card-address_zip\" length=\"12\"></td>\n      </tr>\n    </table>\n    <input type=\"hidden\" name=\"stripePublicKey\" value=\"${thing.stripe_key}\">\n    <input type=\"hidden\" name=\"stripeToken\" value=\"\">\n    <button class=\"btn gold-button stripe-submit\">${_('Submit')}</button>\n    <span class=\"status\"></span>\n  </div>\n</%def>\n\n<%def name=\"stripe_form(display=False)\">\n<div id=\"stripe-payment\"\n     class=\"charge\"\n     ${not display and \"style='display:none'\" or ''}>\n  <input type=\"hidden\" name=\"pennies\" value=\"${thing.price.pennies}\">\n  <input type=\"hidden\" name=\"months\" value=\"${thing.months}\">\n  <input type=\"hidden\" name=\"period\" value=\"${thing.period}\">\n  <input type=\"hidden\" name=\"passthrough\" value=\"${thing.passthrough}\">\n  ${base_stripe_form()}\n</div>\n</%def>\n\n<%def name=\"coinbase_button()\">\n  <button class=\"btn coinbase-gold gold-button gold-checkout\"\n          data-vendor=\"coinbase\"\n          %if not thing.clone_template:\n            onclick=\"window.open('https://coinbase.com/checkouts/${thing.coinbase_button_id}?c=${thing.passthrough}')\"\n          %endif\n          >${_('Bitcoin')}</button>\n  <input type=\"hidden\" name=\"cbbaseurl\" value=\"https://coinbase.com/checkouts/${thing.coinbase_button_id}\">\n  <input type=\"hidden\" name=\"custom\" value=\"${thing.passthrough}\" class=\"passthrough\">\n</%def>\n\n% if not thing.clone_template:\n<script src=\"//checkout.google.com/files/digital/ga_post.js\"\n  type=\"text/javascript\"></script>\n% endif\n\n"
  },
  {
    "path": "r2/r2/templates/goldpayment.htmllite",
    "content": ""
  },
  {
    "path": "r2/r2/templates/goldpayment.mobile",
    "content": ""
  },
  {
    "path": "r2/r2/templates/goldpayment.xml",
    "content": ""
  },
  {
    "path": "r2/r2/templates/goldsubscription.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%namespace file=\"goldpayment.html\" import=\"base_stripe_form\"/>\n\n%if not (thing.has_stripe_subscription or thing.has_paypal_subscription):\n<div class=\"error\">\n    ${_(\"your account doesn't have a gold subscription.\")}\n</div>\n%endif\n\n%if thing.has_stripe_subscription:\n<div class=\"gold-subscription\">\n  ${thing.text}\n\n  <div class=\"buttons\">\n    <button class=\"edit-button\" onclick=\"$('#stripe-payment').toggle()\">\n      ${_(\"use different card\")}\n    </button>\n\n    <button class=\"cancel-button\" onclick=\"$('#stripe-cancel').toggle()\">\n        ${_(\"cancel subscription\")}\n    </button>\n\n    <span id=\"stripe-cancel\" style='display:none'>\n      <input type=\"hidden\" name=\"user\" value=\"${thing.user_fullname}\">\n      <span class=\"option error\">\n        ${_(\"are you sure?\")}\n        &#32;<a href=\"javascript:void(0)\" class=\"yes\"\n           onclick=\"post_form('#stripe-cancel', 'cancel_subscription')\">\n          ${_(\"yes\")}\n        </a>&#32;/&#32;\n        <a href=\"javascript:void(0)\" class=\"no\"\n           onclick=\"$('#stripe-cancel').hide()\">${_(\"no\")}</a>\n      </span>\n      <span class=\"status\"></span>\n    </span>\n  </div>\n\n  <div class=\"gold-form\">\n    <div class=\"modify\" id=\"stripe-payment\" style='display:none'>\n      <div class=\"roundfield\">\n        ${base_stripe_form()}\n      </div>\n    </div>\n  </div>\n</div>\n%endif\n\n%if thing.has_paypal_subscription:\n<div class=\"gold-subscription\">\n    ${_(\"you have a paypal gold subscription. go to %(paypal)s to manage it.\") % dict(paypal=thing.paypal_url)}\n</div>\n%endif\n"
  },
  {
    "path": "r2/r2/templates/goldthanks.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%namespace file=\"utils.html\" import=\"md\"/>\n\n<div class=\"gold-wrap\">\n  <h1 class=\"gold-banner\"><a href=\"/gold\">${_('reddit gold')}</a></h1>\n  <div class=\"fancy\">\n    <div class=\"fancy-inner\">\n      <div class=\"fancy-content\">\n        <div class=\"container\">\n          <p class=\"claim-message\">${thing.claim_msg}\n          %if thing.vendor_url:\n            &#32;<a class=\"vendor-url\" href=\"${thing.vendor_url}\">${thing.vendor_url}</a>\n          %endif\n          </p>\n            \n          %if thing.lounge_md:\n            <span class=\"lounge-msg\">\n              ${md(thing.lounge_md, wrap=True)}\n            </span>\n          %endif\n        </div>\n      </div>\n    </div>\n  </div>\n</div>\n"
  },
  {
    "path": "r2/r2/templates/goldvertisement.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n  from babel.numbers import format_percent\n%>\n\n<div class=\"goldvertisement\">\n  <div class=\"inner\">\n    <h2\n    % if hasattr(thing, \"goal_today\"):\n      title=\"Goal = $${'%0.2f' % thing.goal_today}\"\n    % endif\n    >${_(\"daily reddit gold goal\")}</h2>\n\n    <div class=\"progress\">\n      <p>${format_percent(thing.percent_filled / 100.0, locale=c.locale)}</p>\n      <div class=\"bar\">\n        <span style=\"width: ${min(100, thing.percent_filled)}%\"></span>\n      </div>\n    </div>\n\n    <a href=\"/gold?goldtype=${thing.default_type}&amp;source=progressbar\" target=\"_blank\">${_(\"help support reddit\")}</a>\n\n    <div class=\"gold-bubble hover-bubble help-bubble anchor-top-centered\">\n      <p><span class=\"gold-branding\">reddit gold</span>&#32;gives you extra features\n      and helps keep our servers running. We believe the more reddit can be\n      user-supported, the freer we will be to make reddit the best it can\n      be.</p>\n\n      <p class=\"buy-gold\">Buy gold for yourself to gain access to&#32;<a\n        href=\"/gold/about\" target=\"_blank\">extra features</a>&#32;and&#32;<a\n        href=\"/r/goldbenefits\" target=\"_blank\">special benefits</a>. A month of gold pays for\n      &#32;<b>${thing.hours_paid}</b>&#32;of reddit server time!</p>\n\n      <p class=\"give-gold\">Give gold to thank exemplary people and encourage them to post\n      more.</p>\n\n      <p class=\"aside\">This daily goal updates every 10 minutes and is reset at\n      midnight&#32;<a target=\"_blank\"\n        href=\"https://en.wikipedia.org/wiki/Pacific_Time_Zone\">Pacific Time</a>&#32;\n      (${thing.time_left_today} from now).</p>\n\n      <div class=\"history\">\n        <p\n        % if hasattr(thing, \"goal_yesterday\"):\n          title=\"Goal = $${'%0.2f' % thing.goal_yesterday}\"\n        % endif\n        >${_(\"Yesterday's reddit gold goal\")}</p>\n\n        <div class=\"progress\">\n          <p>${format_percent(thing.percent_filled_yesterday / 100.0, locale=c.locale)}</p>\n          <div class=\"bar\">\n            <span style=\"width: ${min(100, thing.percent_filled_yesterday)}%\"></span>\n          </div>\n        </div>\n      </div>\n    </div>\n  </div>\n</div>\n"
  },
  {
    "path": "r2/r2/templates/googletagmanager.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2014\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n    from r2.lib.filters import scriptsafe_dumps, unsafe\n    from r2.lib import js\n%>\n\n<%namespace file=\"utils.html\" import=\"googletagmanager\"/>\n\n<!doctype html>\n<html>\n<head>\n  <meta charset=utf-8>\n  <meta name=\"referrer\" content=\"no-referrer\">\n  <title></title>\n</head>\n<body>\n  ${unsafe(js.use('gtm'))}\n  <noscript>\n    <iframe src=\"//www.googletagmanager.com/ns.html?id=${thing.container_id}\"\n    height=\"0\" width=\"0\" style=\"display:none;visibility:hidden\"></iframe>\n  </noscript>\n  <script>\n    try {\n      var bootstrap = JSON.parse(window.name);\n      if (bootstrap) {\n        window.googleTagManager = [bootstrap];\n      }\n    } catch (e) {}\n    (function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':\n    new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],\n    j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=\n    '//www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);\n    })(window,document,'script','googleTagManager',${scriptsafe_dumps(thing.container_id)});\n  </script>\n</body>\n</html>\n"
  },
  {
    "path": "r2/r2/templates/googletagmanagerjail.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2014\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n    from r2.lib.filters import (\n      scriptsafe_dumps,\n      unsafe,\n    )\n    from r2.lib import js\n%>\n\n<!doctype html>\n<html>\n<head>\n  <meta charset=utf-8>\n  <meta name=\"referrer\" content=\"no-referrer\">\n  <title></title>\n  <script>\n    window.CONTAINER_ID = ${scriptsafe_dumps(thing.container_id)};\n  </script>\n</head>\n<body>\n  ${unsafe(js.use('gtm-jail'))}\n</body>\n</html>\n"
  },
  {
    "path": "r2/r2/templates/headerbar.mobile",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<div class=\"${thing._class}\"><span>${thing.message}</span></div>\n"
  },
  {
    "path": "r2/r2/templates/helplink.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%namespace file=\"utils.html\" import=\"data\" />\n\n<a class=\"helplink ${'access-required' if thing.access_required else ''}\"\n   href=\"${thing.url}\"\n   ${data(**thing.data_attrs)}\n>\n  ${thing.label}\n</a>\n"
  },
  {
    "path": "r2/r2/templates/helppage.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%inherit file=\"reddit.html\"/>\n\n<%def name=\"stylesheet()\">\n  ${parent.stylesheet()}\n  <link rel=\"stylesheet\"\n        type=\"text/css\"\n        href=\"http://code.reddit.com/chrome/reddit/style.css\" />\n</%def>\n"
  },
  {
    "path": "r2/r2/templates/infobar.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n   from r2.lib.filters import safemarkdown\n%>\n\n<div class=\"infobar ${thing.extra_class}\">\n  ${unsafe(safemarkdown(thing.message))}\n</div>\n"
  },
  {
    "path": "r2/r2/templates/interestbar.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<div class=\"sr-interest-bar\">\n  <div class=\"bubble\">\n    <%\n        if thing.has_subscribed:\n            msg = _(\"ready for something new? %s subscribe %s to some new subreddits.\")\n        else:\n            msg = _(\"it looks like you haven't %s subscribed %s to any subreddits yet. want some ideas?\")\n\n        pre, sub, post = msg.split(\"%s\")\n    %>\n    <p class=\"caption\">${pre}&#32;<span class=\"subscribe\">${sub}</span>&#32;${post}</p>\n    <p class=\"error-caption\"></p>\n    <div class=\"query-box\"><input class=\"query\" placeholder=\"${_('what are you interested in?')}\"><div class=\"throbber\"></div></div>\n    <ul class=\"results\">\n      <li>${_('try these:')}</li>\n      <li>\n        <a href=\"/r/random\" class=\"random\" target=\"_blank\">\n          <span class=\"name\">${_(\"serendipity\")}</span>\n        </a>\n      </li>\n    </ul>\n  </div>\n</div>\n"
  },
  {
    "path": "r2/r2/templates/interstitial.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n  from r2.lib.template_helpers import static \n  from r2.lib.filters import unsafe, safemarkdown\n%>\n\n<%namespace file=\"utils.html\" import=\"md\"/>\n\n<div class=\"interstitial\">\n  <img class=\"interstitial-image\"\n       ${self.interstitial_image_attrs()}\n       height=\"150\"\n       width=\"150\">\n  \n  <div class=\"interstitial-message md-container\">\n    <div class=\"md\">\n      <h3>${self.interstitial_title()}</h3>\n\n      %if thing.sr_name and thing.sr_description:\n        <div class=\"interstitial-subreddit-description\">\n          <h5>${_(\"r/%s\") % thing.sr_name}</h5>\n          ${md(thing.sr_description)}\n        </div>\n      %endif\n\n      ${self.interstitial_message()}\n    </div>\n  </div>\n  \n  ${self.interstitial_buttons()}\n</div>\n\n<%def name=\"interstitial_image_attrs()\">\n  src=\"${static(thing.image)}\"\n</%def>\n\n<%def name=\"interstitial_title()\">\n  ${thing.title}\n</%def>\n\n<%def name=\"interstitial_message()\">\n  %if thing.message:\n    ${md(thing.message)}\n  %endif\n</%def>\n\n<%def name=\"interstitial_buttons()\">\n  <div class=\"buttons\">\n    <a href=\"/\" class=\"c-btn c-btn-primary\">${_(\"back to Reddit\")}</a>\n  </div>\n</%def>\n"
  },
  {
    "path": "r2/r2/templates/intimeoutinterstitial.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n  from r2.lib.template_helpers import static, _wsf, format_html\n%>\n\n<%inherit file=\"interstitial.html\"/>\n\n<%def name=\"interstitial_image_attrs()\">\n  src=\"${static('interstitial-image-banned.png')}\"\n  alt=\"${_('suspended')}\"\n</%def>\n\n<%def name=\"interstitial_title()\">\n  <%\n    suspended_link = format_html('&#32;<a href=\"https://reddit.zendesk.com/hc/en-us/articles/205687686\">%s</a>',\n                                 _('suspended'))\n  %>\n  %if thing.timeout_days_remaining:\n    <%\n      days = ungettext('day', 'days', thing.timeout_days_remaining)\n    %>\n    ${_wsf(\"Your account has been %(suspended_link)s from Reddit  \\nfor %(num)s %(days)s\",\n           suspended_link=suspended_link, num=thing.timeout_days_remaining, days=days)}\n  %else:\n    ${_wsf(\"Your account has been permanently %(suspended_link)s from Reddit\",\n           suspended_link=suspended_link)}\n  %endif\n</%def>\n\n<%def name=\"interstitial_message()\">\n  %if not thing.hide_message:\n    ${_(\"You may not access this page.\")}\n  %endif\n</%def>\n"
  },
  {
    "path": "r2/r2/templates/languagetrafficsummary.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n    from r2.lib.template_helpers import format_number\n%>\n\n<h1>${_(\"language traffic statistics\")}</h1>\n\n<table class=\"traffic-table\">\n<caption>${_(\"traffic by language\")}</caption>\n<thead>\n<tr>\n  <th scope=\"col\">${_(\"language\")}</th>\n  <th scope=\"col\">${_(\"uniques\")}</th>\n  <th scope=\"col\">${_(\"pageviews\")}</th>\n</tr>\n</thead>\n<tbody>\n% for (langcode, langname), data in thing.language_summary:\n<tr>\n  <th scope=\"row\"><a href=\"/traffic/languages/${langcode}\">${langname}</a></th>\n  % for datum in data:\n  <td>${format_number(datum)}</td>\n  % endfor\n</tr>\n% endfor\n</tbody>\n</table>\n"
  },
  {
    "path": "r2/r2/templates/less.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n   from r2.lib.template_helpers import static\n   from r2.lib import js\n%>\n\n<%def name=\"less_stylesheet(*names)\">\n  %for name in names:\n    <% name = name[:name.rfind('.less')] %>\n    %if g.uncompressedJS:\n      <link rel=\"stylesheet/less\" type=\"text/css\" href=\"${static(name+'.less')}\" media=\"all\">\n    %else:\n      <link rel=\"stylesheet\" type=\"text/css\" href=\"${static(name+'.css')}\" media=\"all\">\n    %endif\n  %endfor\n</%def>\n\n<%def name=\"less_js()\">\n  %if g.uncompressedJS:\n    <script type=\"text/javascript\">\n      less = {env: 'development'};\n    </script>\n    ${unsafe(js.use('less'))}\n  %endif\n</%def>\n"
  },
  {
    "path": "r2/r2/templates/link.compact",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n   from r2.config import feature\n   from r2.lib.template_helpers import add_sr\n   from r2.lib.template_helpers import make_url_protocol_relative\n   import urllib\n   from r2.lib.filters import _force_unicode, _force_utf8\n %>\n<%namespace file=\"printable.compact\" import=\"delete_report_buttons\"/>\n<%namespace file=\"printable.html\" import=\"arrow, score\"/>\n<%namespace file=\"link.html\" import=\"tagline, thumbnail\"/>\n<%namespace file=\"utils.html\" import=\"plain_link, nsfw_stamp, quarantine_stamp\" />\n<%namespace file=\"utils.compact\" import=\"icon_button, toggle_button\" />\n<%namespace file=\"printablebuttons.html\" import=\"state_button\" />\n\n<%def name=\"flair()\">\n  %if c.user.pref_show_link_flair:\n    <span class=\"linkflair\">${thing.flair_text}</span>\n  %endif\n</%def>\n\n<%\n    div_class = \"thing link id-%s\" % thing._fullname\n    if thing.use_sticky_style:\n        div_class += \" stickied\"\n%>\n<div class=\"${div_class}\">\n  <span class=\"rank\">\n    ${thing.num_text}\n  </span>\n  <% \n     like_cls = \"unvoted\"\n     if getattr(thing, \"likes\", None):\n         like_cls = \"likes\"\n     elif getattr(thing, \"likes\", None) is False:\n         like_cls = \"dislikes\"\n   %>\n  <div class=\"midcol ${like_cls}\">\n    ${arrow(thing, 1, thing.likes)}\n    ${arrow(thing, 0, thing.likes == False)}\n  </div>\n    %if not c.permalink_page:\n      <div class=\"commentcount\">\n        ${plain_link(thing.comment_label, thing.permalink, _class=thing.commentcls)}\n      </div>\n    %endif\n  <div class=\"entry ${like_cls}\">\n  <%\n    if thing.is_self:\n        url = add_sr(thing.href_url)\n    else:\n        url = thing.url\n  %>\n  %if c.site.link_flair_position == 'left' and thing.flair_text:\n    ${flair()}\n  %endif\n    ${thumbnail()}\n    <p class=\"title\">\n    <a href=\"${url}\" class=\"may-blank\">${thing.title}</a>\n    %if c.site.link_flair_position == 'right' and thing.flair_text:\n      ${flair()}\n    %endif\n    <span class=\"domain\">\n    %if thing.is_self:\n        (self)\n    %else:\n        (<a href=\"${thing.domain_path}.compact\">${thing.domain_str}</a>)\n    %endif\n    </span>\n    %if thing.quarantine:\n      <span class=\"quarantine-warning\">\n        ${quarantine_stamp()}\n      </span>\n    %endif\n    %if thing.nsfw:\n      <span class=\"nsfw-warning\">\n        ${nsfw_stamp()}\n      </span>\n    %endif\n    </p>\n    <%\n      video_hide = thing.link_child and thing.link_child.css_style.strip(' ') == 'video'\n      selftext_hide = thing.is_self and not thing.selftext\n    %>\n    %if thing.link_child and not c.permalink_page and not video_hide and not selftext_hide:\n      <a href=\"javascript:void(0)\" class=\"expando-button collapsed\n                  ${thing.link_child.css_style}\"></a>\n    %endif\n    <div class=\"tagline\">\n      ${tagline()}\n    </div>\n    </div>\n    <a href=\"javascript:void(0)\" class=\"options_link\"></a>\n    <%\n       expand = thing.link_child and thing.link_child.expand\n     %>\n  <div class=\"expando\" ${\"style='display: none'\" if not expand else \"\"}>\n    %if thing.link_child:\n      ${unsafe(thing.link_child.content())}\n    %endif\n  </div>\n  <div class=\"clear options_expando hidden\">\n    <% \n       subject = \"[reddit] I wanted to share this link with you\"\n       body = \"\"\"%(user)s shared a link with you from reddit (https://www.reddit.com/):\n\n%(link)s\n\n\"%(title)s\"\n\nthere's also a discussion going on here:\n\n%(permalink)s\n\"\"\" % dict(user = c.user.name if c.user_is_Loggedin else \"A reddit user\",\n       link = _force_unicode(thing.url),\n       title = _force_unicode(thing.title),\n       permalink = add_sr(thing.permalink, sr_path = False, force_hostname = True, retain_extension=False))\n       url = \"https://reddit.com/%s\" % thing._id36\n       title = _force_unicode(thing.title)\n       tweet = \"%s %s\" % (title[0:(139-len(url))], url)\n       %>\n\n        ${icon_button(\"Share\", \"email-icon\", \"mailto:?subject=\" + urllib.quote(_force_utf8(subject)) + \"&body=\" + urllib.quote(_force_utf8(body)))}\n        ${self.save_button()}\n        ${self.hide_button()}\n        ${icon_button(\"Report\",\"report-icon\",onclick=\"return change_state(this, 'report', hide_thing)\")}\n\n      \n\n  </div>\n</div>\n<%def name=\"hide_button()\">\n    %if c.user_is_loggedin:\n            ${toggle_button(\"hide\", thing.hidden)}\n    %endif\n</%def>\n<%def name=\"save_button()\">\n    %if c.user_is_loggedin:\n        ${toggle_button(\"save\", thing.saved)}\n    %endif\n</%def>\n"
  },
  {
    "path": "r2/r2/templates/link.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n   from pylons import app_globals as g\n\n   from r2.config import feature\n   from r2.lib.filters import conditional_websafe\n   from r2.lib.template_helpers import (\n       get_domain,\n       get_linkflair_css_classes,\n       js_timestamp,\n       make_url_protocol_relative,\n   )\n   from r2.lib.pages.things import LinkButtons\n   from r2.lib.pages import WrappedUser\n   from r2.lib.strings import Score, strings\n%>\n \n<%inherit  file=\"printable.html\"/>\n<%namespace file=\"utils.html\" import=\"plain_link, thing_timestamp, edited, nsfw_stamp, quarantine_stamp, thumbnail_img\" />\n<%namespace file=\"printablebuttons.html\" import=\"toggle_button\" />\n \n<%def name=\"numcol()\">\n  <span class=\"rank\">\n    ${thing.num_text}\n  </span>\n</%def>\n\n<%def name=\"make_link(name, css_class, tabindex=0)\">\n  <%\n    media_override = thing.link_child and getattr(thing, 'media_override', False)\n    if thing.is_self:\n      url = thing.href_url\n      inbound_tracking_url = thing.tracking_link(url, thing, name)\n    else:\n      url = thing.href_url\n      inbound_tracking_url = None\n  %>\n  <a class=\"${css_class} may-blank ${ c.user_is_loggedin and 'loggedin' or ''}\n            ${ media_override and 'open-expando' or '' }\"\n     href=\"${url}\"\n     %if tabindex:\n       tabindex=\"${tabindex}\"\n     %endif\n     %if thing.nofollow:\n       rel=\"nofollow\"\n     %endif\n     %if inbound_tracking_url and inbound_tracking_url != url:\n       data-href-url=\"${url}\"\n       data-inbound-url=\"${inbound_tracking_url}\"\n     %endif\n     >\n     ${caller.body()}\n  </a>\n</%def>\n\n<%def name=\"bottom_buttons()\">\n  <ul class=\"flat-list buttons\">\n    %if thing.quarantine:\n      <li>\n        <span class=\"quarantine-stamp stamp\">${quarantine_stamp()}</span>\n      </li>\n    %endif\n    %if thing.nsfw:\n     <li>\n       <span class=\"nsfw-stamp stamp\">${nsfw_stamp()}</span>\n     </li>\n    %endif\n    ${self.buttons()}\n    ${self.admintagline()}\n  </ul>\n  <div class=\"reportform report-${thing._fullname}\"></div>\n </%def>\n\n<%def name=\"flair()\">\n  %if c.user.pref_show_link_flair and thing.flair_text:\n    <span class=\"linkflairlabel\" title=\"${thing.flair_text}\">${thing.flair_text}</span>\n  %endif\n</%def>\n\n<%def name=\"entry()\">\n  <p class=\"title\">\n    %if c.site.link_flair_position == 'left':\n      <%call expr=\"flair()\" />\n    %endif\n    <%call expr=\"make_link('title', 'title', tabindex=1)\">\n      ${thing.title}\n    </%call>\n    %if c.site.link_flair_position == 'right':\n      <%call expr=\"flair()\" />\n    %endif\n    ${self.approval_checkmark()}\n    &#32;\n\n    ${self.domain()}\n\n    %if c.user_is_admin:\n        %for link_note in thing.link_notes:\n           &#32;<span class=\"link-note\">[${link_note}]</span>\n        %endfor\n    %endif\n  </p>\n\n  ##the expando button\n  <% selftext_hide = thing.is_self and not thing.selftext %>\n  %if thing.link_child:\n    %if not thing.link_child.expand and not selftext_hide:\n      <div class=\"expando-button collapsed\n                  ${thing.link_child.css_style}\"></div>\n    %elif thing.link_child.expand and not thing.link_child.position_inline:\n      <div class=\"expando-button expanded\n                  ${thing.link_child.css_style}\"></div>\n    %endif\n  %endif\n\n  <p class=\"tagline\">\n    ${self.tagline()}\n  </p>\n\n  <% \n     child_content = \"\"\n     if thing.link_child and thing.link_child.load:\n       child_content = unsafe(thing.link_child.content())\n     expand = thing.link_child and thing.link_child.expand\n     position_inline = thing.link_child and thing.link_child.position_inline\n  %>\n\n  ## if we're not on a permalink page we'll render the buttons on top\n  %if not position_inline:\n    ${bottom_buttons()}\n  %endif\n\n  <div class=\"expando expando-uninitialized\" ${\"style='display: none'\" if not expand else \"\"}\n    %if not expand and child_content:\n      data-cachedhtml=\"${websafe(child_content)}\"\n    %endif\n  >\n    %if expand:\n      ${child_content}\n    %else:\n      <span class=\"error\">loading...</span>\n    %endif\n  </div>\n\n  ##if we are on a permalink page, we'll render the buttons below\n  %if position_inline:\n    ${bottom_buttons()}\n  %endif\n</%def>\n\n<%def name=\"thing_css_class(what)\" buffered=\"True\">\n  ${parent.thing_css_class(what)} ${\"over18\" if thing.over_18 else \"\"} ${thing.visited and 'visited' or ''}\n  ${get_linkflair_css_classes(thing, on_class=\"linkflair\", off_class=\"\")}\n</%def>\n\n<%def name=\"thing_data_attributes(what)\">\n  ${parent.thing_data_attributes(what)}\n\n  %if not getattr(what, 'deleted', False):\n    data-author=\"${what.author.name}\"\n    data-author-fullname=\"${what.author._fullname}\"\n  %endif\n\n  data-subreddit=\"${what.subreddit.name}\"\n  data-subreddit-fullname=\"${what.subreddit._fullname}\"\n\n  data-timestamp=\"${js_timestamp(what._date)}\"\n\n  data-url=\"${thing.href_url}\"\n  data-domain=\"${what.domain}\"\n  data-rank=\"${what.num_text}\"\n\n  %if getattr(what, 'can_ban', False):\n    data-can-ban=\"true\"\n  %endif\n</%def>\n\n<%def name=\"subreddit()\" buffered=\"True\">\n  ${plain_link('/r/' + thing.subreddit.name, thing.subreddit_path, _sr_path=False,\n               _class=\"subreddit hover may-blank\")}\n</%def>\n\n<%def name=\"midcol(display=True, cls = '')\">\n    <div class=\"midcol ${cls}\"\n       ${not display and \"style='display:none'\" or ''}>\n    ${self.arrow(thing, 1, thing.likes)}\n    %if thing.pref_compress:\n      <div class=\"score-placeholder\"></div>\n    %elif thing.hide_score:\n      <div class=\"score likes\">&bull;</div> \n      <div class=\"score unvoted\">&bull;</div> \n      <div class=\"score dislikes\">&bull;</div> \n    %else:\n      ${self.score(thing, tag='div')}\n    %endif\n    ${self.arrow(thing, 0, thing.likes == False)}\n  </div>\n ${self.thumbnail()}\n</%def>\n\n\n<%def name=\"domain(link=True)\">\n  <span class=\"domain\">\n    %if link:\n      (<a href=\"${thing.domain_path}\">${thing.domain_str}</a>)\n    %else:\n      (${thing.domain_str})\n    %endif\n    %if c.user_is_admin:\n      &#32;\n      <a class=\"adminbox\" href=\"/admin/domain/${thing.domain}\">d</a>\n    %endif\n  </span>\n</%def>\n\n<%def name=\"tagline()\">\n  <%\n    taglinetext = conditional_websafe(thing.taglinetext).replace(\" \", \"&#32;\")\n  %>\n  ${unsafe(taglinetext % dict(reddit=self.subreddit(),\n                              score=capture(self.score, thing, tag='span'),\n                              when=capture(thing_timestamp, thing, thing.timesince, live=True, include_tense=True),\n                              author=WrappedUser(thing.author, thing.attribs, thing).render(),\n                              lastedited=capture(edited, thing, thing.lastedited)\n                              ))}\n  ${self.gildings()}\n  %if thing.use_sticky_style:\n    &#32;-&#32;<span class=\"stickied-tagline\" title=\"selected by this subreddit's moderators\">announcement</span>\n  %endif\n\n  %if getattr(thing, 'savedcategory', None) is not None:\n    ${plain_link(_('category: %s') % thing.savedcategory,\n                 '/user/%s/saved/%s' % (c.user.name, thing.savedcategory),\n                 _class='save-category' + ('' if thing.savedcategory else ' hidden')\n                )}\n  %endif\n</%def>\n\n<%def name=\"child()\">\n</%def>\n\n<%def name=\"buttons(comments=True, delete=True, report=True, additional='')\">\n  ${LinkButtons(thing, comments = comments, delete = delete,\n                report = report,\n               )}\n</%def>\n\n<%def name=\"thumbnail()\">\n  %if thing.thumbnail:\n  <%call expr=\"make_link('thumbnail', 'thumbnail ' + (thing.thumbnail if thing.thumbnail_sprited else ''))\">\n    ${thumbnail_img(thing)}\n  </%call>\n  %endif\n</%def>\n"
  },
  {
    "path": "r2/r2/templates/link.htmllite",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%namespace file=\"utils.html\" import=\"optionalstyle, nsfw_stamp, quarantine_stamp\"/>\n\n<%!\n   from pylons.i18n import _, ungettext\n   from r2.lib.template_helpers import get_domain\n%>\n<%inherit file=\"printable.htmllite\" />\n\n<%def name=\"flair()\">\n  %if c.user.pref_show_link_flair:\n    <span class=\"linkflairlabel\"\n        ${optionalstyle(\"color: #545454; background-color: #f5f5f5; border: 1px solid #dedede; display: inline-block; font-size: x-small; margin-right: 0.5em; padding: 0 2px; max-width: 10em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;\")}>\n      ${thing.flair_text}\n    </span>\n  %endif\n</%def>\n\n<%def name=\"hide_if_appropriate(state)\">\n  %if thing.like_cls != state:\n   ${optionalstyle(\"display: none;\")}\n  %endif\n</%def>\n\n<%def name=\"entry()\">\n<% \n   from r2.lib.strings import Score\n   domain = get_domain(subreddit=False)\n   permalink = \"%s://%s%s\" % (g.default_scheme, domain, thing.permalink)\n   expanded = request.GET.get(\"expanded\")\n   two_col = request.GET.has_key(\"twocolumn\") if l else False\n %>\n  ${self.arrows(thing)}\n  <div class=\"reddit-entry entry ${thing.like_cls}\" \n       %if expanded:\n         ${optionalstyle(\"margin-left: 58px;\")}\n       %else:\n         ${optionalstyle(\"margin-left: 28px; min-height:32px;\")}\n       %endif\n       >\n    %if c.site.link_flair_position == 'left' and thing.flair_text:\n      ${flair()}\n    %endif\n    <a class=\"reddit-link-title may-blank\"\n      ${optionalstyle(\"text-decoration:none;color:#336699;font-size:small;\")}\n       %if thing.is_self:\n         href=\"${permalink}\"\n       %else:\n         href=\"${thing.href_url}\"\n       %endif\n       %if thing.nofollow:\n         rel=\"nofollow\"\n       %endif\n       %if c.link_target:\n         target=\"${c.link_target}\"\n       %endif\n       >\n      ${thing.title}\n    </a>\n    %if c.site.link_flair_position == 'right' and thing.flair_text:\n      ${flair()}\n    %endif\n    %if not expanded:\n      <br />\n    %endif\n    <small \n       %if expanded:\n         ${optionalstyle(\"color:gray;margin-left:5px;\")}\n       %else:\n         ${optionalstyle(\"color:gray;\")}\n       %endif\n       >\n      %if thing.quarantine:\n        <span class=\"quarantine-stamp stamp\">${quarantine_stamp()}</span>\n      %endif\n      %if thing.nsfw:\n        <span class=\"nsfw-stamp stamp\">${nsfw_stamp()}</span>\n      %endif\n      %if not expanded:\n      <%\n       if thing.hide_score:\n           score_dislikes = score_unvoted = score_likes = unsafe('&bull; points')\n       else:\n           score_dislikes, score_unvoted, score_likes = thing.display_score\n       %>\n      <span class=\"score dislikes\" ${hide_if_appropriate('dislikes')}>\n         ${score_dislikes}\n      </span>\n      <span class=\"score unvoted\" ${hide_if_appropriate('unvoted')}>\n         ${score_unvoted}\n      </span>\n      <span class=\"score likes\" ${hide_if_appropriate('likes')}>\n         ${score_likes}\n      </span>\n      &#32;|&#32;\n      %endif\n      <a class=\"reddit-comment-link may-blank\"\n         ${optionalstyle(\"color:gray\")}\n         %if c.link_target:\n           target=\"${c.link_target}\"\n         %endif\n         href=\"${permalink}\">${thing.comment_label}</a>\n    </small>\n  </div>\n  <div class=\"reddit-link-end\" ${optionalstyle(\"clear:left; padding:3px;\")}></div>\n</%def>\n"
  },
  {
    "path": "r2/r2/templates/link.mobile",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%! \n    from pylons.i18n import _, ungettext\n    from r2.lib.filters import conditional_websafe\n    from r2.lib.template_helpers import add_sr\n%>\n\n<%namespace file=\"utils.html\" import=\"plain_link\" />\n<%inherit file=\"printable.mobile\" />\n\n<%def name=\"flair()\">\n  %if c.user.pref_show_link_flair:\n    <span class=\"linkflair\">${thing.flair_text}</span>\n  %endif\n</%def>\n\n<%def name=\"entry()\">\n<% \n   if thing.num_comments:\n       # generates \"XX comments\" as a noun\n       com_label = \"%d %s\" % \\\n             (thing.num_comments,\n              ungettext(\"comment\", \"comments\", thing.num_comments))\n   else:\n       # generates \"comment\" the imperative verb\n       com_label = _(\"comment\") \n %>\n  <div class=\"link\">\n     %if c.site.link_flair_position == 'left' and thing.flair_text:\n       ${flair()}\n     %endif\n     %if thing.is_self:\n       <a class=\"title\" href=\"${add_sr(thing.href_url)}\">${thing.title}</a>\n     %else:\n      <a class=\"title\" href=\"${thing.url}\">${thing.title}</a>\n     %endif\n     %if c.site.link_flair_position == 'right' and thing.flair_text:\n       ${flair()}\n     %endif\n    <p class=\"byline\">&#32;${thing.score}&#32;${ungettext(\"point\", \"points\", thing.score)}\n    %if thing.num_comments or thing.is_self:\n       ## the comments link only shows the link, the selftext, and the\n       ## comments. since the mobile interface offers no way to leave\n       ## comments, so we can save space by not drawing the link if\n       ## are no comments or selftext\n       |&#32;<span class=\"buttons\">${plain_link(com_label, thing.permalink)}</span>\n    %endif\n    &#32;|${tagline()}</p>\n    %if thing.link_child and thing.link_child.expand:\n      <div class=\"expando\">\n        ${unsafe(thing.link_child.content())}\n      </div>\n    %endif\n  </div>\n</%def>\n\n<%def name=\"domain()\" buffered=\"True\">\n  %if thing.is_self:\n    <a class=\"domain\" href=\"${thing.subreddit.path}.mobile\">${thing.subreddit.name}</a>\n  %else:\n    <a class=\"domain\" href=\"/domain/${thing.domain}/.mobile\">${thing.domain}</a>\n  %endif\n</%def>\n\n<%def name=\"subreddit()\" buffered=\"True\">\n  <a href=\"${thing.subreddit.path}.mobile\" class=\"subreddit\">\n    ${thing.subreddit.name}\n  </a>\n</%def>\n\n\n<%def name=\"tagline()\">\n  <% \n    from r2.lib.utils import timeago\n    from r2.models import FakeSubreddit\n\n    if isinstance(c.site, FakeSubreddit) and thing.is_self:\n        taglinetext = _(\"%(when)s ago by %(author)s to %(domain)s\")\n    elif isinstance(c.site, FakeSubreddit):\n        taglinetext = _(\"%(when)s ago by %(author)s to %(reddit)s from %(domain)s\")\n    else:\n        taglinetext = _(\"%(when)s ago by %(author)s from %(domain)s\")\n    taglinetext = conditional_websafe(taglinetext).replace(\" \", \"&#32;\")\n  %>\n  ${unsafe(taglinetext % dict(reddit = self.subreddit(),\n                              domain = self.domain(),\n                              when = thing.timesince,\n                              author= self.author()))}\n</%def>\n"
  },
  {
    "path": "r2/r2/templates/link.xml",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n   from r2.lib.template_helpers import add_sr, get_domain\n   from r2.lib.filters import unsafe, safemarkdown, keep_space\n   from r2.lib.template_helpers import html_datetime\n   from r2.lib.template_helpers import make_url_https\n%>\n<%namespace name=\"utils\" file=\"utils.xml\"/>\n<%\n    domain = get_domain(subreddit=False)\n    permalink = add_sr(thing.permalink, force_hostname=True, retain_extension=False)\n    use_thumbs = thing.has_thumbnail and thing.thumbnail and not request.GET.has_key(\"nothumbs\")\n    use_thumbs = use_thumbs and not thing.thumbnail_sprited\n    thumbnail = thing.thumbnail\n    if c.secure:\n        thumbnail = make_url_https(thumbnail)\n    if thing.deleted:\n        title = _('[deleted]')\n    else:\n        title = thing.title\n%>\n<entry>\n    %if not thing.deleted:\n        <%utils:atom_author author=\"${thing.author}\"/>\n    %endif\n\n    <category term=\"${thing.subreddit.name}\" label=\"/r/${thing.subreddit.name}\"/>\n\n    <%utils:atom_content>\n        %if use_thumbs:\n            <table>\n                <tr><td>\n                    <a href=\"${permalink}\">\n                        <img src=\"${thumbnail}\" alt=\"${title}\" title=\"${title}\" />\n                    </a>\n                </td><td>\n        %endif\n\n        %if getattr(thing, 'selftext', None):\n            %if thing.expunged:\n                ${_('[removed]')}\n            %else:\n                ${unsafe(safemarkdown(thing.selftext))}\n            %endif\n        %endif\n\n        %if not thing.author._deleted:\n            ${keep_space(' ')} submitted by ${keep_space(' ')}\n            <a href=\"${add_sr('/user/'+thing.author.name,\n                              sr_path=False,\n                              retain_extension=False,\n                              force_hostname=True)}\">\n                /u/${thing.author.name}\n            </a>\n        %endif\n\n        %if thing.different_sr:\n            ${keep_space(' ')} to ${keep_space(' ')}\n            <a href=\"${add_sr(thing.subreddit.path,\n                              sr_path=False,\n                              retain_extension=False,\n                              force_hostname=True)}\">\n                /r/${thing.subreddit.name}\n            </a>\n        %endif\n\n        <br/>\n\n        <span><a href=\"${thing.url}\">[${_(\"link\")}]</a></span>\n        ${keep_space(' ')}\n        <span><a href=\"${permalink}\">[${_(\"comments\")}]</a></span>\n\n        %if use_thumbs:\n            </td></tr></table>\n        %endif\n    </%utils:atom_content>\n\n    <id>${thing._fullname}</id>\n    <link href=\"${permalink}\" />\n    <updated>${html_datetime(thing._date)}</updated>\n    <title>${title}</title>\n</entry>\n"
  },
  {
    "path": "r2/r2/templates/linkcommentsep.mobile",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<hr />\n"
  },
  {
    "path": "r2/r2/templates/linkcommentssettings.compact",
    "content": ""
  },
  {
    "path": "r2/r2/templates/linkcommentssettings.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n  from r2.config import feature\n  from r2.lib.filters import jssafe\n%>\n\n<%namespace file=\"printablebuttons.html\" import=\"ynbutton\" />\n\n\n<%def name=\"suggested_clear_type(name, value=None)\">\n  <a href=\"javascript:void(0)\"\n     onclick=\"return set_suggested_sort(this, '${jssafe(value or name)}')\">${_(name)}</a>\n</%def>\n\n<%def name=\"clear_suggested_sort()\">\n  <li class=\"toggle\">\n    <form method=\"post\" action=\"/api/set_suggested_sort\">\n      <input type=\"hidden\" name=\"id\" value=\"${thing.link._fullname}\" />\n      <input type=\"hidden\" name=\"sort\" value=\"\" />\n      <input type=\"hidden\" value=\"${_('suggested sort cleared')}\" name=\"executed\"/>\n      <a href=\"javascript:void(0)\"\n         onclick=\"return toggle_clear_suggested_sort(this)\"\n         data-event-action=\"unsetsuggestedsort\">\n        ${_(\"clear suggested sort\")}</a>\n      <span class=\"option error\">\n         ${_(\"clear suggested sort?\")}\n\n         %if thing.sr.suggested_comment_sort:\n\n           &#32;\n           ## Set to explicitly \"blank\", which will disallow using the subreddit setting and use the user's setting\n           ${suggested_clear_type('clear', 'blank')}\n           &#32;/\n\n           ## Show \"use subreddit setting\" only if a link's suggested sort is explicitly set\n           %if thing.link.suggested_sort is not None:\n           &#32;\n           ## Set back to default, which will be the subreddit's suggested sort\n           ${suggested_clear_type('use subreddit setting', '')}\n           &#32;/\n           %endif\n\n         %else:\n\n           &#32;\n           ## Set back to \"default\" in case they explicitly set a subreddit suggested sort in the future\n           ${suggested_clear_type('clear', '')}\n           &#32;/\n\n         %endif\n\n         &#32;\n         <a href=\"javascript:void(0)\"\n            onclick=\"return toggle_clear_suggested_sort(this)\">${_('cancel')}</a>\n         &#32;\n      </span>\n    </form>\n  </li>\n</%def>\n\n%if thing.can_edit:\n  %if thing.suggested_sort == thing.sort:\n    <% clear_suggested_sort() %>\n  %else:\n    ${ynbutton(_(\"set as suggested sort\"), _(\"suggested sort set\"), \"set_suggested_sort\",\n      hidden_data=dict(id=thing.link._fullname, sort=thing.sort),\n      event_target='link', event_action='setsuggestedsort')}\n  %endif\n%endif\n\n%if thing.is_author:\n  %if thing.sendreplies:\n    ${ynbutton(_(\"disable inbox replies\"), _(\"inbox replies disabled\"), \"sendreplies\",\n      hidden_data=dict(id=thing.link._fullname, state=False),\n      access_required=False, event_action=\"disable_inbox_replies\")}\n  %else:\n    ${ynbutton(_(\"enable inbox replies\"), _(\"inbox replies enabled\"), \"sendreplies\",\n      hidden_data=dict(id=thing.link._fullname, state=True),\n      access_required=False, event_action=\"enable_inbox_replies\")}\n  %endif\n  &nbsp;<span class=\"help-hoverable\" title=\"${_('inbox replies will send you a message when this link receives a new top-level comment')}\">(?)</span>\n%endif\n\n%if thing.can_edit:\n  %if thing.contest_mode:\n    ${ynbutton(_(\"disable contest mode\"), _(\"contest mode disabled\"), \"set_contest_mode\",\n      hidden_data=dict(id=thing.link._fullname, state=False),\n      event_target='link', event_action='unsetcontestmode')}\n  %else:\n    ${ynbutton(_(\"enable contest mode\"), _(\"contest mode enabled\"), \"set_contest_mode\",\n      hidden_data=dict(id=thing.link._fullname, state=True),\n      event_target='link', event_action='setcontestmode')}\n  %endif\n%endif\n\n%if thing.can_sticky:\n  %if thing.stickied:\n    ${ynbutton(_(\"remove announcement\"), _(\"removed\"), \"set_subreddit_sticky\",\n      hidden_data=dict(id=thing.link._fullname, state=False),\n      event_target='link', event_action='unsticky')}\n  %elif thing.stickies_full:\n    ${ynbutton(_(\"make announcement\"), _(\"announced\"), \"set_subreddit_sticky\",\n      question=_(\"make announcement? (bottom announcement will be replaced)\"),\n      hidden_data=dict(id=thing.link._fullname, state=True),\n      event_target='link', event_action='sticky')}\n  %else:\n    ${ynbutton(_(\"make announcement\"), _(\"announced\"), \"set_subreddit_sticky\",\n      question=_(\"make announcement?\"),\n      hidden_data=dict(id=thing.link._fullname, state=True),\n      event_target='link', event_action='sticky')}\n  %endif\n%endif\n\n%if thing.contest_mode:\n  <div class=\"contest-mode infobar mellow\"><strong>${_(\"this thread is in contest mode\")}</strong>&#32;- \n  %if thing.can_edit:\n    ${_('as a mod, you can sort comments however you wish and scores are visible. regular users have randomized sorting and cannot see the scores.')}\n  %else:\n    ${_('contest mode randomizes comment sorting, hides scores, and collapses replies by default.')}\n  %endif\n  </div>\n%endif\n"
  },
  {
    "path": "r2/r2/templates/linkinfobar.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n   from r2.lib.filters import websafe\n   from r2.lib.strings import Score\n   from r2.lib.template_helpers import format_number, format_percent, html_datetime\n %>\n\n\n<%namespace file=\"printablebuttons.html\" import=\"state_button\" />\n<%namespace file=\"printable.html\" import=\"thing_css_class\" />\n\n\n\n<div class=\"linkinfo\">\n  <div class=\"date\">\n    <span>\n      ${_(\"this post was submitted on\")}\n      &#32;\n    </span>\n    <time datetime=\"${html_datetime(thing.a._date)}\">\n      ${thing.a._date.strftime(thing.datefmt)}\n    </time>\n  </div>\n\n  <div class=\"score\">\n    ${unsafe(Score.PERSON_LABEL % dict(num = format_number(thing.a.score),\n                                       persons = websafe(ungettext(\"point\", \"points\",\n                                                                   thing.a.score))))}\n    &#32;(${_(\"%(percent)s upvoted\") % dict(percent=format_percent(thing.a.upvote_ratio))})\n  </div>\n\n%if getattr(thing.a, \"shortlink\", None):\n  <div class=\"shortlink\">\n    shortlink:\n    &#32;\n    <input type=\"text\" value=\"https://${thing.a.shortlink}\" readonly=\"readonly\" id=\"shortlink-text\" />\n  </div>\n%endif\n\n  ${self.info_table()}\n</div>\n\n<%def name=\"info_table()\">\n</%def>\n\n"
  },
  {
    "path": "r2/r2/templates/linkinfopage.htmllite",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n    from r2.lib.template_helpers import _wsf\n%>\n<%inherit file=\"base.htmllite\"/>\n\n${thing._content}\n\n<%def name=\"titlebar(site)\">\n  ${_wsf(\"comments from %(site)s\", site=site)}\n</%def>\n\n"
  },
  {
    "path": "r2/r2/templates/linkinfopage.iframe",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2014\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n    from r2.lib.template_helpers import _ws\n%>\n<%inherit file=\"base.iframe\"/>\n\n${thing._content}\n"
  },
  {
    "path": "r2/r2/templates/linklisting.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n## a kooky hack to make ranks and voting arrows in the spotlight box line up\n## without late rendering or blasting style attributes everywhere\n<style>\n  body > .content .link .rank, .rank-spacer {\n    width: ${thing.rank_width}ex\n  }\n  body > .content .link .midcol, .midcol-spacer {\n    width: ${thing.midcol_width}ex\n  }\n</style>\n<%include file=\"listing.html\"/>\n"
  },
  {
    "path": "r2/r2/templates/listing.compact",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%namespace file=\"utils.html\" import=\"plain_link\" />\n\n<%\n   _id = (\"_%s\" % thing.parent_name) if hasattr(thing, 'parent_name') else ''\n   cls = thing.lookups[0].__class__.__name__.lower()\n %>\n<div id=\"siteTable${_id}\" class=\"sitetable ${cls}\">\n  %for a in thing.things:\n      ${a}\n  %endfor\n</div>\n\n%if thing.nextprev and thing.next:\n<script type=\"text/javascript\">\n$($(window).scroll(function(){\n            var loading = $(\".loading\").length;\n            if (!loading && $(window).scrollTop() > \n              0.8*( $(document).height() - window.innerHeight) ){\n                fetch_more();\n                } \n            }))\n</script>\n%endif\n\n%if not thing.things:\n  <p id=\"noresults\" class=\"error\">${_(\"there doesn't seem to be anything here\")}</p>\n%endif\n"
  },
  {
    "path": "r2/r2/templates/listing.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n   from r2.lib.template_helpers import format_html\n %>\n<%namespace file=\"utils.html\" import=\"plain_link\" />\n\n<%\n   _id = (\"_%s\" % thing.parent_name) if hasattr(thing, 'parent_name') else ''\n   cls = thing.lookups[0].__class__.__name__.lower()\n\n   # this is a hack to restore linklisting for the search page\n   if cls == 'searchlisting':\n       cls = 'linklisting'\n %>\n<div id=\"siteTable${_id}\" class=\"sitetable ${cls}\">\n  %for a in thing.things:\n      ${a}\n  %endfor\n\n  %if thing.nextprev and (thing.prev or thing.next):\n    <div class=\"nav-buttons\">\n      <span class=\"nextprev\">${_(\"view more:\")}&#32;\n      %if thing.prev:\n        ${plain_link(format_html(\"&lsaquo; %s\", _(\"prev\")), thing.prev, rel=\"nofollow prev\")}\n      %endif\n      %if thing.prev and thing.next:\n        <span class=\"separator\"></span>\n      %endif\n      %if thing.next:\n        ${plain_link(format_html(\"%s &rsaquo;\", _(\"next\")), thing.next, rel=\"nofollow next\")}\n      %endif\n      </span>\n      %if thing.next_suggestions:\n        ${thing.next_suggestions}\n      %endif\n    </div>\n  %endif\n  %if not thing.things:\n    <p id=\"noresults\" class=\"error\">${_(\"there doesn't seem to be anything here\")}</p>\n  %endif\n</div>\n"
  },
  {
    "path": "r2/r2/templates/listing.htmllite",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%namespace file=\"utils.html\" import=\"optionalstyle\"/>\n<%namespace file=\"printable.html\" import=\"thing_css_class\"/>\n<div ${optionalstyle(\"margin-left:5px;margin-top:7px;\")}>\n  <% \n     t = thing.things\n     l = len(t)\n     two_col = request.GET.has_key(\"twocolumn\") if l else False\n   %>\n  %for i, a in enumerate(t):\n   <% \n      cls = \"reddit-link \"\n      cls += \"odd \" if i % 2 else \"even \"\n      cls += \"first-half\" if i < (l+1)/2 else \"second-half\"\n    %>\n   %if two_col:\n     %if i == 0:\n       <div class=\"reddit-listing-left\" \n            ${optionalstyle(\"float:left;width:47%\")}>\n     %elif i - 1 < (l+1)/2 and i >= (l+1)/2:\n       </div>\n       <div class=\"reddit-listing-right\" \n            ${optionalstyle(\"float:right; width:49%;\")}>\n     %endif\n   %endif\n\n     <div class=\"${cls} ${thing_css_class(a)}\">\n       ${a}\n     </div>\n   %if two_col and i == l - 1:\n   </div>\n   %endif\n  %endfor:\n  %if two_col:\n    <div ${optionalstyle(\"clear:both\")}></div>\n  %endif\n</div>\n"
  },
  {
    "path": "r2/r2/templates/listing.iframe",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2014\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n    from r2.models import Comment\n%>\n\n\n%if len(thing.things):\n  <ol class=\"reddit-embed-list\">\n    %for i, t in enumerate(thing.things):\n      <li class=\"reddit-embed-list-item\">\n        ${t}\n      </li>\n    %endfor\n  </ol>\n%endif\n"
  },
  {
    "path": "r2/r2/templates/listing.mobile",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<% from r2.lib.template_helpers import replace_render, add_sr %>\n<%namespace file=\"utils.html\" import=\"plain_link\" />\n\n<ul>\n%for a in thing.things:\n  ${a}\n%endfor:\n</ul>\n\n%if thing.nextprev and (thing.prev or thing.next):\n  <p class=\"nextprev\"> ${_(\"view more:\")}&#32;\n  %if thing.prev:\n    ${plain_link(_(\"prev\"), thing.prev)}  \n  %endif\n  %if thing.prev and thing.next:\n    &#32;|&#32;\n  %endif\n  %if thing.next:\n    ${plain_link(_(\"next\"), thing.next)}  \n  %endif\n  </p>\n%endif\n%if not thing.things:\n  <p id=\"noresults\" class=\"error\">${_(\"there doesn't seem to be anything here\")}</p>\n%endif\n"
  },
  {
    "path": "r2/r2/templates/listing.xml",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n%for a in thing.things:\n    ${a}\n%endfor\n"
  },
  {
    "path": "r2/r2/templates/listingchooser.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%namespace file=\"utils.html\" import=\"classes\"/>\n\n<%def name=\"section_items(itemlist)\">\n  %for item in itemlist:\n    <li ${classes(item['extra_class'], 'selected' if item['selected'] else None)}>\n      <a href=\"${item['path']}\">\n        ${item['name']}\n        %if 'description' in item:\n          <br><span class=\"description\">${item['description']}</span>\n        %endif\n      </a>\n    </li>\n  %endfor\n</%def>\n\n<div class=\"listing-chooser\">\n  <div class=\"grippy\"></div>\n  <div class=\"contents\">\n    <ul class=\"global\">\n      ${section_items(thing.sections['global'])}\n    </ul>\n\n    <h3>${_('multireddits')}</h3>\n    %if thing.show_samples:\n      <div class=\"intro\">\n        <p>${_('new! create sets of subreddits to view together.')}</p>\n        <p>${_('for starters, try one of these:')}</p>\n        <ul class=\"multis\">\n          ${section_items(thing.sections['sample'])}\n        </ul>\n        <p>${_('to hide these samples, create a multi of your own:')}</p>\n      </div>\n    %endif\n    <ul class=\"multis\">\n      ${section_items(thing.sections['multi'])}\n      <li class=\"create\">\n        <form>\n          <input type=\"text\" class=\"multi-name\" placeholder=\"${_('name')}\"></input>\n          <div class=\"error\"></div>\n          <button>${_('create')}</button><div class=\"throbber\"></div>\n        </form>\n      </li>\n    </ul>\n\n    <ul class=\"other\">\n      ${section_items(thing.sections['other'])}\n    </ul>\n  </div>\n</div>\n"
  },
  {
    "path": "r2/r2/templates/listingsuggestions.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%namespace file=\"utils.html\" import=\"text_with_links\" />\n\n%if thing.suggestion_type:\n\n<span class=\"next-suggestions\">\n%if thing.suggestion_type == 'explore':\n  ${_('or')}\n  <a href=\"/explore\">${_('explore some new subreddits')}</a>\n%elif thing.suggestion_type == 'multis':\n  ${_('or try a multi:')}\n  %for multi in thing.suggestions:\n      <a href=\"${multi.path}\">/m/${multi.name}</a>\n  %endfor\n%elif thing.suggestion_type == 'random':\n  ${text_with_links(\n    _('or try a %(random_subreddit)s'),\n    random_subreddit=dict(link_text=_(\"random subreddit\"), path=\"/r/random\")\n  )}\n%endif\n</span>\n\n%endif\n"
  },
  {
    "path": "r2/r2/templates/locationbar.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n   from r2.lib.filters import safemarkdown\n%>\n\n<div class=\"locationbar\">\n  ${unsafe(safemarkdown(\n    _('using the default subreddits for your location (%(location)s)') % dict(location=c.location)\n  ))}\n\n  <span class=\"options\">\n    (<a class=\"use-global\" href=\"javascript:void(0)\">${_(\"use global defaults\")}</a>\n    &#32;|&#32;\n    <a class=\"dismiss\" href=\"javascript:void(0)\">${_(\"dismiss this message\")}</a>)\n  </span>\n</div>\n\n<script type=\"text/javascript\">\n  $(\".use-global\").click(\n    function () {\n      $.request(\"use_global_defaults\");\n    }\n  );\n\n  $(\".dismiss\").click(\n    function () {\n      $.request(\"hide_locationbar\");\n    }\n  );\n</script>\n"
  },
  {
    "path": "r2/r2/templates/lockedinterstitial.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n  from r2.lib.template_helpers import static\n%>\n\n<%inherit file=\"interstitial.html\"/>\n\n<%def name=\"interstitial_image_attrs()\">\n  src=\"${static('interstitial-image-locked.png')}\"\n  alt=\"${_('locked')}\"\n</%def>\n\n<%def name=\"interstitial_title()\">\n  ${_(\"This post has been locked by the community moderators\")}\n</%def>\n\n<%def name=\"interstitial_message()\">\n  <p>\n    ${_(\"New comments can no longer be added.\")}\n  </p>\n</%def>\n\n<%def name=\"interstitial_buttons()\">\n  <div class=\"buttons\">\n    <a href=\"/\" class=\"c-btn c-btn-primary\">${_(\"Got It\")}</a>\n  </div>\n</%def>\n"
  },
  {
    "path": "r2/r2/templates/login.compact",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%namespace file=\"login.html\" import=\"login_form\"/>\n\n%if c.user_is_loggedin:\n  <p class=\"error\">${_(\"You are logged in. Go use the site!\")}</p>\n%else:\n  ${login_form(user=thing.user_login, dest=thing.dest, compact=True)}\n%endif\n"
  },
  {
    "path": "r2/r2/templates/login.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n   from r2.config import feature\n   from r2.lib.template_helpers import add_sr\n   from r2.lib.template_helpers import static\n   from r2.lib.strings import strings\n   from r2.lib.utils import UrlParser\n   import random\n %>\n<%namespace file=\"captcha.html\" import=\"captchagen\"/>\n<%namespace file=\"utils.html\" import=\"error_field, img_link, form_group, text_with_links\"/>\n\n## default content \n%if c.user_is_loggedin:\n  <p class=\"error\">${_(\"You are logged in. Go use the site!\")}</p>\n%else:\n  %if thing.is_popup:\n    <h3 id=\"cover-msg\" class=\"modal-title\">\n      ${_('You need to login to do that.')}\n    </h3>\n  %endif\n  <div id=\"login\">\n    ${login_panel(login_form, \n                  user_reg = thing.user_reg, user_login = thing.user_login, \n                  dest=thing.dest,\n                  registration_info=thing.registration_info)}\n  </div>\n%endif\n\n<%def name=\"login_form(register=False, user='', dest='', compact=False, autofocus=True)\">\n  %if not compact:\n    <%\n      op = \"reg\" if register else \"login\"\n      base = g.https_endpoint\n      tabindex = 2 if register else 3\n    %>\n    <form id=\"${'register' if register else 'login'}-form\" method=\"post\" \n          action=\"${add_sr(base + '/post/' + op)}\"\n          class=\"form-v2\">\n      <input type=\"hidden\" name=\"op\" value=\"${op}\">\n      %if dest:\n        <input type=\"hidden\" name=\"dest\" value=\"${dest}\">\n      %endif\n      <%call expr=\"form_group(\n                    'user',\n                    'USERNAME_TOO_SHORT',\n                    'USERNAME_INVALID_CHARACTERS',\n                    'USERNAME_TAKEN',\n                    show_errors=register)\">\n        <label for=\"user_${op}\" class=\"screenreader-only\">${_('username')}:</label>\n        <input value=\"${user}\" name=\"user\" id=\"user_${op}\" class=\"c-form-control\"\n               type=\"text\" maxlength=\"20\" tabindex=\"${tabindex}\"\n               %if register:\n                placeholder=\"${_('choose a username')}\"\n                data-validate-url=\"/api/check_username.json\"\n                data-validate-min=\"3\"\n                autocomplete=\"off\"\n               %else:\n                placeholder=\"${_('username')}\"\n                %if autofocus:\n                  autofocus\n                %endif\n               %endif\n               >\n      </%call>\n      <%call expr=\"form_group('passwd', 'BAD_PASSWORD', 'WRONG_PASSWORD', show_errors=True)\">\n        <label for=\"passwd_${op}\" class=\"screenreader-only\">${_('password')}:</label>\n        <input id=\"passwd_${op}\" class=\"c-form-control\" name=\"passwd\" type=\"password\"\n               tabindex=\"${tabindex}\" placeholder=\"${_('password')}\"\n               ${\"data-validate-url='/api/check_password.json'\" if register else ''}>\n      </%call>\n      %if register:\n        <%call expr=\"form_group('passwd2', 'BAD_PASSWORD_MATCH', show_errors=register)\">\n          <label for=\"passwd2_${op}\" class=\"screenreader-only\">${_('verify password')}:</label>\n          <input name=\"passwd2\" id=\"passwd2_${op}\" class=\"c-form-control\"\n                 type=\"password\" tabindex=\"${tabindex}\"\n                 placeholder=\"${_('verify password')}\">\n        </%call>\n        <%call expr=\"form_group('email', 'BAD_EMAIL', show_errors=register)\">\n          <label for=\"email_${op}\" class=\"screenreader-only\">\n            ${_('email')}: &nbsp;<i>(${_('optional')})\n          </i></label>\n          <input value=\"\" name=\"email\" id=\"email_${op}\" class=\"c-form-control\"\n                 type=\"text\" tabindex=\"${tabindex}\"\n                 placeholder=\"${_('email (optional)')}\"\n                 data-validate-url=\"/api/check_email.json\"\n                 data-validate-on=\"change blur\">\n        </%call>\n      %endif\n      <div class=\"c-checkbox\">\n        <input type=\"checkbox\" name=\"rem\" id=\"rem_${op}\" tabindex=\"${tabindex}\">\n        <label for=\"rem_${op}\">\n          ${_('remember me')}\n        </label>\n        %if not register:\n          <a href=\"/password\" class=\"c-pull-right\">${_('reset password')}</a>\n        %endif\n      </div>\n      %if register:\n      <div class=\"c-checkbox\">\n        <input type=\"checkbox\" name=\"newsletter_subscribe\" id=\"newsletter_subscribe\" tabindex=\"${tabindex}\"\n          data-validate-url=\"/api/check_email.json\"\n          data-validate-on=\"change blur\"\n          data-validate-with=\"email\"\n        >\n        <label for=\"newsletter_subscribe\">\n          ${_('get the best of reddit emailed to you once a week.')}&#32;\n          <a href=\"/newsletter\" target=\"_blank\">${_('learn more')}</a>\n        </label>\n      </div>\n      %endif\n      <div class=\"c-clearfix c-submit-group\">\n        <span class=\"c-form-throbber\"></span>\n        <button type=\"submit\" class=\"c-btn c-btn-primary c-pull-right\" tabindex=\"${tabindex}\">\n          ${register and _(\"sign up\") or _(\"log in\")}\n        </button>\n      </div>\n      <div>\n        <div class=\"c-alert c-alert-danger\"></div>\n        %if register:\n          ${error_field(\"RATELIMIT\", \"ratelimit\")}\n          ${error_field(\"RATELIMIT\", \"vdelay\")}\n        %endif\n      </div>\n    </form>\n  %else:\n    <%\n      op = \"reg\" if register else \"login\"\n      base = g.https_endpoint\n      tabindex = 2 if register else 3\n    %>\n    <form id=\"login_${op}\" method=\"post\" \n          action=\"${add_sr(base + '/post/' + op)}\"\n          class=\"user-form ${'register-form' if register else 'login-form'}\">\n      <input type=\"hidden\" name=\"op\" value=\"${op}\" />\n      %if dest:\n        <input type=\"hidden\" name=\"dest\" value=\"${dest}\" />\n      %endif\n      <div>\n        <ul>\n          <li class=\"name-entry\">\n            <label for=\"user_${op}\">${_('username')}:</label>\n            <input value=\"${user}\" name=\"user\" id=\"user_${op}\" \n                   type=\"text\" maxlength=\"20\" tabindex=\"${tabindex}\" autofocus />\n            %if register:\n              <span class=\"throbber\"></span>\n              <span class=\"notice-taken\">${_('try another')}</span>\n              <span class=\"notice-available\">${_('available!')}</span>\n              ${error_field(\"BAD_USERNAME\", \"user\", kind=\"span\")}\n              ${error_field(\"USERNAME_TAKEN\", \"user\", kind=\"span\")}\n              ${error_field(\"USERNAME_TAKEN_DEL\", \"user\", kind=\"span\")}\n            %endif\n          </li>\n        %if register:\n          <li>\n            <label for=\"email_${op}\">\n              ${_('account recovery email')}: &nbsp;<i>(${_('optional')})\n            </i></label>\n            <input value=\"\" name=\"email\" id=\"email_${op}\" \n                   type=\"email\" maxlength=\"50\" tabindex=\"${tabindex}\"/>\n            <label for=\"email_${op}\" class=\"note\">${_('we only send email at your request')}</label>\n            %if register:\n              ${error_field(\"BAD_EMAILS\", \"email\", kind=\"span\")}\n            %endif\n          </li>\n        %endif\n          <li>\n            <label for=\"passwd_${op}\">${_('password')}:</label>\n            <input id=\"passwd_${op}\" name=\"passwd\" type=\"password\" \n                   tabindex=\"${tabindex}\"/>\n            %if register:\n              ${error_field(\"BAD_PASSWORD\", \"passwd\", kind=\"span\")}\n            %else:\n              ${error_field(\"WRONG_PASSWORD\", \"passwd\", kind=\"span\")}\n            %endif\n          </li>\n        %if register:\n          <li>\n            <label for=\"passwd2_${op}\">${_('verify password')}:</label>\n            <input name=\"passwd2\" id=\"passwd2_${op}\" \n                   type=\"password\" tabindex=\"${tabindex}\"/>\n            ${error_field(\"BAD_PASSWORD_MATCH\", \"passwd2\", kind=\"span\")}\n          </li>\n          <li>\n            %if not g.disable_captcha:\n            <% iden = hasattr(thing, \"captcha\") and thing.captcha.iden or '' %>\n            ${captchagen(iden, tabulate=True, label=False, size=30, tabindex=tabindex)}\n            %endif\n          </li>\n        %endif\n        <li>\n          <input type=\"checkbox\" name=\"rem\" id=\"rem_${op}\" tabindex=\"${tabindex}\" />\n          <label for=\"rem_${op}\" class=\"remember\">\n            ${_('remember me')}\n          </label>\n        </li>\n        %if not register:\n        <li>\n          <a class=\"recover-password\" href=\"/password\">\n            ${_(\"recover password\")}\n          </a>\n        </li>\n        %endif\n      </ul>\n        <p class=\"submit\">\n          <button type=\"submit\" class=\"button\" tabindex=\"${tabindex}\">\n            ${register and _(\"sign up\") or _(\"log in\")}\n          </button>\n          <span class=\"throbber\"></span>\n          <span class=\"status\"></span>\n          %if register:\n            ${error_field(\"RATELIMIT\", \"ratelimit\")}\n            ${error_field(\"RATELIMIT\", \"vdelay\")}\n          %endif\n        </p>\n      </div>\n    </form>\n  %endif\n</%def>\n\n\n<%def name=\"login_panel(lf, user_reg = '', user_login = '', dest='', registration_info=None)\">\n  <% autofocus = (not thing.is_popup) %>\n  <div class=\"split-panel\">\n    <div class=\"split-panel-section split-panel-divider\">\n      <h4 class=\"modal-title\">${_(\"create a new account\")}</h4>\n      ${lf(register=True, user=user_reg, dest=dest, autofocus=autofocus)}\n    </div>\n    <div class=\"split-panel-section\">\n      <h4 class=\"modal-title\">${_(\"log in\")}</h4>\n      ${lf(user = user_login, dest = dest, autofocus=autofocus)}\n    </div>\n  </div>\n  <p class=\"login-disclaimer\">\n    ${text_with_links(\n      _(\"By signing up, you agree to our %(terms)s and that you have read our %(privacy_policy)s and %(content_policy)s.\"),\n        terms=dict(link_text=_(\"Terms\"), path=\"/help/useragreement/\"),\n        privacy_policy=dict(link_text=_(\"Privacy Policy\"), path=\"/help/privacypolicy/\"),\n        content_policy=dict(link_text=_(\"Content Policy\"), path=\"/help/contentpolicy/\"),\n    )}\n  </p>\n</%def>\n"
  },
  {
    "path": "r2/r2/templates/loginformwide.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n   from r2.lib.template_helpers import add_sr\n   from r2.lib.utils import UrlParser\n   import random\n%>\n\n<%namespace file=\"utils.html\" import=\"error_field\"/>\n\n<%\n  op = \"login-main\"\n  base = g.https_endpoint\n%>\n<form method=\"post\"\n      action=\"${add_sr(base + '/post/login')}\"\n      id=\"login_${op}\"\n      class=\"login-form login-form-side\">\n  <input type=\"hidden\" name=\"op\" value=\"${op}\" />\n  <input name=\"user\" placeholder=\"${_('username')}\" type=\"text\" maxlength=\"20\" tabindex=\"1\"/>\n  <input name=\"passwd\" placeholder=\"${_('password')}\" type=\"password\" tabindex=\"1\"/>\n\n  <div class=\"status\"></div>\n\n  <div id=\"remember-me\">\n    <input type=\"checkbox\" name=\"rem\" id=\"rem-${op}\" tabindex=\"1\" />\n    <label for=\"rem-${op}\">${_(\"remember me\")}</label>\n    <a class=\"recover-password\" href=\"/password\">${_(\"reset password\")}</a>\n  </div>\n\n  <div class=\"submit\">\n    <span class=\"throbber\"></span>\n    <button class=\"btn\" type=\"submit\" tabindex=\"1\">${_(\"login\")}</button>\n  </div>\n  \n  <div class=\"clear\"></div>\n</form>\n\n"
  },
  {
    "path": "r2/r2/templates/mail_opt.email",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n%if thing.leave:\n\nYour email address will no longer receive email from us.  If you want to reconsider, visit:\n\n${g.default_scheme}://${g.domain}/mail/optin?x=${thing.msg_hash}\n\nWe promise not to hold it against you.\n\n%else:\n\nYour email address is once again to allowed to receive messages from ${g.domain}.  Welcome back.  \n\n%endif\n"
  },
  {
    "path": "r2/r2/templates/mediaembed.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n  from r2.lib.utils import randstr\n%>\n<iframe src=\"//${thing.media_domain}/mediaembed/${thing.id36}${thing.credentials}\"\n        id=\"media-embed-${thing.id36}-${randstr(3)}\" class=\"media-embed\"\n        width=\"${thing.width}\" height=\"${thing.height}\" border=\"0\"\n        frameBorder=\"0\" scrolling=\"${'auto' if thing.scrolling else 'no'}\"\n        allowfullscreen></iframe>\n"
  },
  {
    "path": "r2/r2/templates/mediaembedbody.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<!doctype html>\n<html>\n  <head>\n    <style type=\"text/css\">\n        body, object, embed, div, span, p {\n            margin:  0;\n            padding: 0;\n        }\n    </style>\n  </head>\n  <body>${unsafe(thing.body)}</body>\n</html>\n"
  },
  {
    "path": "r2/r2/templates/mediapreview.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n  from r2.lib.filters import unsafe\n%>\n\n<div class=\"media-preview\"\n     id=\"media-preview-${thing.id36}\"\n     style=\"max-width: ${thing.width}px\">\n  <div class=\"media-preview-content\">\n    <a href=\"${thing.url}\" class=\"may-blank\">\n      ${unsafe(thing.media_content)}\n    </a>\n  </div>\n</div>\n"
  },
  {
    "path": "r2/r2/templates/menuarea.compact",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<div class=\"menuarea subtoolbar\">\n  %for menu in thing.menus:\n    ${menu}\n  %endfor\n</div>\n"
  },
  {
    "path": "r2/r2/templates/menuarea.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<div class=\"menuarea\">\n  %for menu in thing.menus:\n    <div class=\"spacer\">\n      ${menu}\n    </div>\n  %endfor\n</div>\n"
  },
  {
    "path": "r2/r2/templates/menulink.compact",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%inherit file=\"menulink.html\" />\n"
  },
  {
    "path": "r2/r2/templates/menulink.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<a class=\"gray\" href=\"${thing.url}\" rel=\"nofollow\">${thing.title}</a>\n"
  },
  {
    "path": "r2/r2/templates/message.compact",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n    from r2.lib.pages import WrappedUser\n    from r2.lib.filters import safemarkdown, websafe, conditional_websafe\n    from r2.models import Account\n    from r2.lib.template_helpers import format_html\n%>\n<%namespace file=\"printable.html\" import=\"arrow, score\"/>\n<%namespace file=\"utils.html\" import=\"plain_link\" />\n<%namespace file=\"utils.compact\" import=\"icon_button\" />\n<%namespace file=\"printablebuttons.html\" import=\"simple_button\"/>\n\n<div class=\"thing message id-${thing._fullname} ${'unread' if thing.new else ''}\">\n  <p class=\"subject\">\n    %if getattr(thing, \"is_parent\", False):\n       %if thing.sr_id:\n         <span class=\"correspondent reddit rounded\">\n           <a href=\"${thing.subreddit.path}message/moderator/inbox.compact\">\n             /r/${thing.subreddit.name}\n           </a>\n         </span>\n       %else:\n         <span class=\"correspondent rounded\">\n           <%\n              corr = thing.author if thing.user_is_recipient else thing.to\n            %>\n           ${WrappedUser(corr)}\n         </span>\n       %endif\n    %endif\n    ${thing.subject}\n  %if thing.was_comment:\n   <a href=\"${thing.link_permalink}.compact\" class=\"title\">${thing.link_title}</a>\n  %endif\n </p>\n\n  <div class=\"entry\">\n  <%\n    substitutions = {}\n\n    if thing.sr_id:\n        substitutions['subreddit'] = format_html(u'<b>&nbsp;<a href=\"%(path)s.compact\">%(path)s</a>&nbsp;</b>', path=thing.subreddit.path)\n\n    substitutions['author'] = format_html(u\"<b>%s</b>\", WrappedUser(thing.author, thing.attribs, thing))\n\n    if isinstance(thing.to, Account):\n        substitutions['dest'] = format_html(u\"<b>%s</b>\", WrappedUser(thing.to, [], thing))\n    elif thing.sr_id:\n        substitutions['dest'] = substitutions['subreddit']\n\n    substitutions['when'] = thing.timesince\n\n    taglinetext = conditional_websafe(thing.taglinetext).replace(' ', '&nbsp;')\n    taglinetext = format_html(taglinetext, **substitutions)\n  %>\n\n    <div class=\"tagline\">\n      ${taglinetext}\n    </div>\n    <a href=\"javascript:void(0)\" class=\"options_link\"></a>\n    ${unsafe(safemarkdown(thing.body))}\n    <div class=\"clear options_expando hidden\">\n        <%\n            is_author = (c.user_is_loggedin and thing.author and c.user._fullname == thing.author._fullname)\n        %>\n        %if c.user_is_loggedin:\n            ${icon_button(\"Reply\", \"reply-icon\", outer_class=\"reply-button\", onclick=\"return reply(this)\")}\n        %endif\n        ${icon_button(\"Permalink\", \"permalink-icon\", thing.permalink + \".compact\")}\n        %if thing.was_comment:\n            ${icon_button(\"Context\", \"context-icon\", thing.permalink + \".compact?context=3\")}\n        %endif\n        ${icon_button(\"Unread\", \"unread-icon\", onclick=\"change_state(this, 'unread_message', unread_thing, true)\" )}\n       </div>\n  </div>\n  <div style=\"clear: both;\"></div>\n  <div class=\"child\">\n    %if thing.childlisting:\n      ${thing.childlisting}\n    %endif\n  </div>\n</div>\n\n\n"
  },
  {
    "path": "r2/r2/templates/message.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n   from r2.lib.filters import safemarkdown, websafe, conditional_websafe\n   from r2.lib.pages.things import MessageButtons\n   from r2.lib.pages import WrappedUser\n   from r2.lib.template_helpers import static, format_html\n   from r2.lib.template_helpers import (\n       add_admin_distinguish,\n       add_moderator_distinguish,\n   )\n   from r2.models import Account\n%>\n\n<%inherit file=\"comment_skeleton.html\"/>\n<%namespace file=\"utils.html\" import=\"thing_timestamp\" />\n<%namespace file=\"wrappeduser.html\" import=\"make_distinguish\" />\n\n## disable voting arrows\n<%def name=\"midcol(cls='')\">\n  %if thing.was_comment and not thing._spam:\n    ${parent.midcol(display=True, cls = cls)}\n  %else:\n    <div class=\"midcol\" style='display:none'></div>\n  %endif\n</%def>\n\n<%def name=\"thing_css_class(what)\" buffered=\"True\">\n  ${parent.thing_css_class(what)}\n  ${\"new\" if thing.new else \"\"}\n  ${\"was-comment\" if thing.was_comment else \"\"}\n  ${\"recipient\" if thing.user_is_recipient else \"\"}\n  ${\"message-reply\" if getattr(thing, \"is_child\", False) else \"\"}\n  ${\"message-parent\" if getattr(thing, \"is_parent\", False) else \"\"}\n  ${\"gold\" if getattr(thing, \"distinguished\", \"\") == \"gold\" else \"\"}\n  ${\"gold-auto\" if getattr(thing, \"distinguished\", \"\") == \"gold-auto\" else \"\"}\n  ${\"threaded\" if getattr(thing, \"threaded\", \"\") else \"\"}\n  ${\"most-recent\" if getattr(thing, \"most_recent\", \"\") else \"\"}\n</%def>\n\n<%def name=\"thing_css_rowclass(what)\">\n  ${parent.thing_css_rowclass(what)}\n  <%\n    accent_color = getattr(thing, \"accent_color\", \"\")\n  %>\n  %if getattr(thing, \"is_parent\", False) and accent_color:\n    color-bar\n  %endif\n</%def>\n\n<%def name=\"thing_data_attributes(what)\">\n  ${parent.thing_data_attributes(what)}\n  <%\n    accent_color = getattr(thing, \"accent_color\", \"\")\n  %>\n  %if getattr(thing, \"is_parent\", False) and accent_color:\n    style=\"border-color:${accent_color};\"\n  %endif\n</%def>\n\n<%def name=\"tagline()\">\n  <a href=\"javascript:void(0)\" class=\"expand\" onclick=\"return togglemessage(this)\">\n    ${\"[%s]\" % (\"+\" if thing.collapsed else \"–\")}\n  </a>\n\n  %if c.user_is_admin:\n     %if not thing.was_comment and hasattr(thing, \"del_on_recipient\") and thing.del_on_recipient:\n       <em>${_(\"deleted message\")}</em>&#32;\n     %endif\n  %endif\n\n  <%\n    substitutions = {}\n\n    if thing.sr_id:\n        path = thing.subreddit.path.rstrip('/')\n\n        if getattr(thing, \"subreddit_distinguish\", None) == \"admin\":\n            distinguish_attribs_list = []\n            add_admin_distinguish(distinguish_attribs_list)\n            distinguish = format_html(capture(make_distinguish, distinguish_attribs_list))\n            type = \"admin-distinguish\"\n            substitutions['subreddit'] = format_html(u'<span class=\"subreddit\"><a href=\"%(path)s\" class=\"%(type)s\">%(path)s</a>%(distinguish)s</span>', path=path, distinguish=distinguish, type=type)\n        elif getattr(thing, \"subreddit_distinguish\", None) == \"moderator\":\n            distinguish_attribs_list = []\n            add_moderator_distinguish(distinguish_attribs_list, thing.subreddit)\n            distinguish = format_html(capture(make_distinguish, distinguish_attribs_list))\n            type = \"moderator-distinguish\"\n            substitutions['subreddit'] = format_html(u'<span class=\"subreddit\"><a href=\"%(path)s\" class=\"%(type)s\">%(path)s</a>%(distinguish)s</span>', path=path, distinguish=distinguish, type=type)\n        else:\n            substitutions['subreddit'] = format_html(u'<span class=\"subreddit\"><a href=\"%(path)s\">%(path)s</a></span>', path=path)\n\n    substitutions['author'] = format_html(u'<span class=\"sender\">%s</span>', WrappedUser(thing.author, thing.attribs, thing))\n\n    if isinstance(thing.to, Account):\n        to_attribs = []\n        if thing.sr_id and not thing.was_comment:\n            if thing.to.name in g.admins:\n                add_admin_distinguish(to_attribs)\n            elif thing.to_is_moderator:\n                add_moderator_distinguish(to_attribs, thing.subreddit)\n        substitutions['dest'] = format_html(u'<span class=\"recipient\">%s</span>', WrappedUser(thing.to, to_attribs, thing))\n    elif thing.sr_id:\n        substitutions['dest'] = format_html(u'<span class=\"recipient subreddit\"><a href=\"%(path)s\">%(path)s</a></span>', path=thing.subreddit.path)\n\n    substitutions['when'] = unsafe(capture(thing_timestamp, thing, thing.timesince, live=True, include_tense=True))\n\n    taglinetext = conditional_websafe(thing.taglinetext).replace(' ', '&#32;')\n    taglinetext = format_html(taglinetext, **substitutions)\n  %>\n\n  <span class=\"head\">\n    ${taglinetext}\n  </span>\n\n  %if c.user_is_admin:\n    ${self.admintagline()}\n  %endif\n</%def>\n\n<%def name=\"subject()\">\n  <p class=\"subject\">\n    %if getattr(thing, \"is_parent\", False):\n       %if thing.sr_id:\n         <span class=\"correspondent reddit rounded\">\n           <%\n             if getattr(thing, \"user_is_moderator\", False):\n               sr_path = \"%smessage/moderator/inbox\" % thing.subreddit.path\n             else:\n               sr_path = thing.subreddit.path\n             accent_color = getattr(thing, \"accent_color\", \"\")\n           %>\n           <a href=\"${sr_path}\">\n             %if accent_color:\n               <span class=\"marker-dot\" style=\"background-color:${accent_color};\"></span>\n             %endif\n             /r/${thing.subreddit.name}\n           </a>\n         </span>\n       %else:\n         <span class=\"correspondent rounded\">\n           <%\n              corr = thing.author if thing.user_is_recipient else thing.to\n            %>\n           ${WrappedUser(corr)}\n         </span>\n       %endif\n    %endif\n    <span class=\"subject-text\">${thing.subject}</span>\n  %if thing.was_comment:\n    <a href=\"${thing.link_permalink}\" class=\"title\">${thing.link_title}</a>\n  %elif getattr(thing, \"is_parent\", False):\n    <br/>\n      <a class=\"expand-btn\" href=\"#\" onclick=\"return show_all_messages(this)\">\n        ${_(\"expand all\")}\n      </a>\n      <a class=\"expand-btn\" href=\"#\"  onclick=\"return hide_all_messages(this)\">\n        ${_(\"collapse all\")}\n      </a>\n  %endif\n </p>\n</%def>\n\n<%def name=\"ParentDiv()\">\n  %if getattr(thing, 'distinguished', '') == 'gold':\n    <div class=\"insignia\"><img src=\"${static('gold/gold-insignia-big.png')}\"></div>\n  %endif\n${self.subject()}\n</%def>\n\n<%def name=\"commentBody()\">\n %if thing.was_comment and hasattr(thing, \"parent\"):\n    <p>\n      <a href=\"#\" class=\"parent-link\"\n         onclick=\"return fetch_parent(this, '${thing.parent_permalink}/.json', '${thing.parent}')\">\n        ${_(\"show parent\")}\n      </a>\n    </p>\n %endif\n <div class=\"md-container\">\n   ${unsafe(safemarkdown(thing.body))}\n </div>\n</%def>\n\n<%def name=\"buttons()\">\n  ${MessageButtons(thing)}\n</%def>\n\n<%def name=\"entry()\">\n  ${parent.entry()}\n</%def>\n"
  },
  {
    "path": "r2/r2/templates/message.xml",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n    from r2.lib.template_helpers import html_datetime, simplified_timesince\n    from r2.lib.filters import safemarkdown, websafe, conditional_websafe\n    from r2.lib.template_helpers import add_sr, format_html\n    from r2.models.account import DeletedUser, Account\n%>\n<%namespace name=\"utils\" file=\"utils.xml\"/>\n<%\n    permalink = add_sr(thing.permalink, force_hostname = True)\n\n    # we get thing.taglinetext set by add_props, but it requires some %(foo)s\n    # formatting substitutions\n\n    substitutions = {}\n\n    if thing.sr_id:\n       substitutions['subreddit'] = thing.subreddit.name\n\n    substitutions['author'] = (DeletedUser() if thing.author._deleted else thing.author).name\n\n    if isinstance(thing.to, Account):\n       substitutions['dest'] = (DeletedUser() if thing.to._deleted else thing.to).name\n    elif thing.sr_id:\n       substitutions['dest'] = substitutions['subreddit']\n\n    # most templates use the thing.timesince StringTemplate for this, but the\n    # double- escaping that <%atom:content/> does means that it can't be\n    # properly replaced. Because clients will be caching these, this display\n    # will almost always be incorrect anyway so we may consider replacing these\n    # with more absolute timestamps\n    substitutions['when'] = simplified_timesince(thing._date)\n\n    taglinetext = conditional_websafe(thing.taglinetext)\n    taglinetext = format_html(taglinetext, **substitutions)\n\n    if thing.was_comment:\n        permalink = permalink + '?context=3'\n        link_text = _(\"[context]\")\n    else:\n        link_text = _(\"[full conversation]\")\n\n%>\n<entry>\n    <%utils:atom_author author=\"${thing.author}\"/>\n\n    %if thing.was_comment:\n        <category term=\"${thing.subreddit.name}\" label=\"/r/${thing.subreddit.name}\" />\n    %endif\n\n    <%utils:atom_content>\n        <div>\n            ${unsafe(taglinetext)}: ${thing.subject}\n        </div>\n\n        ${unsafe(safemarkdown(thing.body))}\n\n        <div>\n            <a href=\"${permalink}\">${link_text}</a>\n        </div>\n    </%utils:atom_content>\n\n    <id>${thing._fullname}</id>\n\n    <link href=\"${permalink}\"/>\n\n    <updated>${html_datetime(thing._date)}</updated>\n\n    <%utils:atom_content tag_name=\"summary\">\n        ${unsafe(taglinetext)}: ${thing.subject}\n    </%utils:atom_content>\n\n    <title>${unsafe(taglinetext)}: ${thing.subject}</title>\n</entry>\n"
  },
  {
    "path": "r2/r2/templates/messagecompose.compact",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%namespace file=\"utils.html\" import=\"error_field, submit_form\"/>\n<%namespace name=\"utils\" file=\"utils.html\"/>\n\n<%!\n   import simplejson\n   from r2.lib.pages import UserText\n%>\n\n\n<%utils:submit_form onsubmit=\"return post_form(this, 'compose', null, null, true)\",\n                    method=\"post\", _id = \"compose-message\",\n                    action=\"/message/compose\">\n\n  %if thing.restrict_recipient:\n    <%utils:round_field title=\"${_('to')}\">\n      <input type=\"text\" class=\"access-required\" name=\"to\" value=\"${g.admin_message_acct}\" readonly />\n    </%utils:round_field>\n  %else:\n    <%utils:round_field title=\"${_('to')}\">\n      <input type=\"text\" name=\"to\" value=\"${thing.to or ''}\"\n             onchange=\"admincheck(this)\"/>\n      ${error_field(\"NO_USER\", \"to\")}\n      ${error_field(\"USER_DOESNT_EXIST\", \"to\")}\n      ${error_field(\"SUBREDDIT_NOEXIST\", \"to\")}\n      ${error_field(\"USER_BLOCKED\", \"to\")}\n      ${error_field(\"USER_MUTED\", \"to\")}\n      ${error_field(\"MUTED_FROM_SUBREDDIT\", \"to\")}\n    </%utils:round_field>\n  %endif\n\n  <%utils:round_field title=\"${_('subject')}\">\n    <input type=\"text\" name=\"subject\" value=\"${thing.subject or ''}\"/>\n    ${error_field(\"NO_SUBJECT\", \"subject\", \"span\")}\n  </%utils:round_field>\n\n  <%utils:round_field title=\"${_('message')}\">\n    ${UserText(None, have_form = False, creating = True)}\n  </%utils:round_field>\n\n  ${thing.captcha}\n\n<span class=\"status\"></span>\n\n</%utils:submit_form>\n\n"
  },
  {
    "path": "r2/r2/templates/messagecompose.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%namespace file=\"utils.html\" import=\"error_field, submit_form\"/>\n<%namespace name=\"utils\" file=\"utils.html\"/>\n\n<%!\n   import simplejson\n   from r2.lib import js\n   from r2.lib.pages import UserText\n   from r2.lib.template_helpers import static\n%>\n\n${unsafe(js.use(\"messagecompose\"))}\n\n<h1>${_(\"send a private message\")}</h1>\n\n<%utils:submit_form onsubmit=\"return post_form(this, 'compose', null, null, true)\",\n                    method=\"post\", _id = \"compose-message\",\n                    action=\"/message/compose\">\n\n${to_field()}\n${subject_field()}\n${message_field()}\n%if thing.admin_check:\n  ${clippy_field()}\n%endif\n${captcha_field()}\n<input type=\"hidden\" name=\"source\" value=\"compose\">\n\n<button id=\"send\" name=\"send\" type=\"submit\">${_(\"send\")}</button>\n<span class=\"status\"></span>\n\n</%utils:submit_form>\n\n\n<%def name=\"to_field()\">\n  <div class=\"spacer\">\n    %if thing.restrict_recipient:\n      <%utils:round_field title=\"${_('to')}\">\n        <input type=\"text\" class=\"access-required\" name=\"to\" value=\"${g.admin_message_acct}\"\n               data-event-action=\"changerecipient\" readonly />\n      </%utils:round_field>\n    %else:\n      <%utils:round_field title=\"${_('to')}\",\n          description=\"${_('(username, or /r/name for that subreddit\\'s moderators)')}\">\n        <input type=\"text\" name=\"to\" value=\"${thing.to or ''}\"\n               onchange=\"${'admincheck(this)' if thing.admin_check else ''}\"/>\n        ${error_field(\"NO_USER\", \"to\")}\n        ${error_field(\"USER_DOESNT_EXIST\", \"to\")}\n        ${error_field(\"SUBREDDIT_NOEXIST\", \"to\")}\n        ${error_field(\"USER_BLOCKED\", \"to\")}\n        ${error_field(\"USER_BLOCKED_MESSAGE\", \"to\")}\n        ${error_field(\"USER_MUTED\", \"to\")}\n        ${error_field(\"MUTED_FROM_SUBREDDIT\", \"to\")}\n      </%utils:round_field>\n    %endif\n  </div>\n</%def>\n\n<%def name=\"subject_field()\">\n  <div class=\"spacer\">\n    <%utils:round_field title=\"${_('subject')}\">\n      ## Hidden by default so that the form is reasonable for users with no JS.\n      <select class=\"rule_subject\" style=\"display:none\">\n        <option value=\"\" selected disabled class=\"blank\"></option>\n        <option value=\"\" class=\"other\">Other</option>\n      </select>\n\n      <input type=\"text\" name=\"subject\" value=\"${thing.subject or ''}\"/>\n      ${error_field(\"NO_SUBJECT\", \"subject\", \"span\")}\n      ${error_field(\"TOO_LONG\", \"subject\", \"span\")}\n    </%utils:round_field>\n  </div>\n</%def>\n\n<%def name=\"message_field()\">\n  <div class=\"spacer\">\n    <%utils:round_field title=\"${_('message')}\">\n      ${UserText(None, text=thing.message, have_form = False, creating = True)}\n    </%utils:round_field>\n  </div>\n</%def>\n\n<%def name=\"clippy_field()\">\n  <script type=\"text/javascript\">\n  function admincheck(elem) {\n    var admins = ${unsafe(simplejson.dumps(thing.admins))};\n\n    if ($.inArray(elem.value, admins) >= 0) {\n      $(\".admin-to\").text(elem.value);\n      $(\".clippy\").show();\n    } else {\n      $(\".clippy\").hide();\n    }\n  }\n  </script>\n\n  <div class=\"clippy\" ${\"style='display:none'\" if thing.to not in thing.admins else \"\"}>\n     <img src=\"${static('alien-clippy.png')}\" />\n     <div class=\"clippy-bubble\">\n       <p class=\"clippy-headline\">It looks like you're writing to an admin!</p>\n       <p>\n         Before you click \"send\", you might want to make sure that\n         &#32;\n         <span class=\"admin-to\">${thing.to}</span>\n         &#32;\n         is the right admin for the job.\n       </p>\n\n       <p>In many cases, there is probably a better alternative than messaging an individual admin:</p>\n\n       <ul>\n         <li>If you'd like to message all the admins at once, send your message to the&#32;<a href=\"/message/compose?to=${g.admin_message_acct|u}\">admin message list.</a></li>\n\n         <li>If you think your posts are being caught in the spam filter,\n           please write to\n           &#32;\n           <a href=\"https://www.reddit.com/wiki/faq#wiki_how_can_i_tell_who_moderates_a_given_subreddit.3F\">\n            the moderators of the subreddit\n           </a>\n           &#32;\n           instead.\n         </li>\n\n         <li>If\n           &#32;\n           <span class=\"admin-to\">${thing.to}</span>\n           &#32;<i>is</i>&#32; a moderator of a specific subreddit you're\n           inquiring about, and that's actually the reason you're writing, please\n           message \n           &#32;\n           <a href=\"https://www.reddit.com/wiki/faq#wiki_how_can_i_tell_who_moderates_a_given_subreddit.3F\">\n            all of the moderators of the subreddit\n           </a>\n           &#32; (using the \"message the mods\" feature located at the top of the\n           mod box) with a direct link to the comment page of the submission or other\n           reason you're writing about.\n         </li>\n\n         <li>If you're concerned that something is wrong with your entire account, please send your message to the admin list by \n           &#32;<a href=\"/message/compose?to=${g.admin_message_acct|u}\">messaging all of the admins of ${g.admin_message_acct}</a>&#32;(It'll be dealt with faster).\n         </li>\n       </ul>\n    </div>\n    <br class=\"clear\"/>\n  </div>\n</%def>\n\n<%def name=\"captcha_field()\">\n  <div class=\"spacer\">\n    ${thing.captcha}\n  </div>\n</%def>\n"
  },
  {
    "path": "r2/r2/templates/messagenotificationemail.email",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2014\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n  from r2.lib.template_helpers import get_domain\n%>\n\n${_(\"You've received a new message from %(username)s!\") % dict(username=thing.sender_username)}\n\n${unsafe(thing.comment.body)}\n\n${_(\"View on the web\")}: ${thing.permalink}\n\n${_(\"Unsubscribe\")}: ${thing.unsubscribe_link}\n"
  },
  {
    "path": "r2/r2/templates/messagenotificationemailsunsubscribe.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<div style=\"text-align: center\">\n    <p>${_(\"Thanks! You're unsubscribed from all message notification emails.\")}</p>\n</div>\n"
  },
  {
    "path": "r2/r2/templates/mobilewebredirectbar.compact",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n  import urlparse\n%>\n\n<%\n  def mobile_web_url():\n    url = request.environ['FULLPATH']\n    scheme, netloc, path, query, fragment = urlparse.urlsplit(url)\n    netloc = \"%s.%s\" % ('m', g.domain)\n    path = path.replace('.compact', '')\n    return urlparse.urlunsplit((None, netloc, path, query, fragment))\n%>\n\n<div hidden class=\"mobile-web-redirect-bar\">\n  <div class=\"mobile-web-redirect-header\">${_(\"You've been invited to try out reddit's new mobile website!\")}</div>\n\n  <a href=\"${mobile_web_url()}\"\n     class=\"mobile-web-redirect-optin\">${_(\"try reddit's mobile website\")}</a>\n\n  <a href=\"#\" class=\"mobile-web-redirect-optout\">${_(\"No thanks\")}</a>\n</div>\n"
  },
  {
    "path": "r2/r2/templates/modaction.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%namespace file=\"utils.html\" import=\"timestamp, plain_link\"/>\n\n<%!\n    from pylons import tmpl_context as c\n    from r2.lib.filters import _force_unicode\n    from r2.models import Account, Comment, Link\n\n    TITLE_MAX_WIDTH = 50\n%>\n\n<tr class=\"modactions\" style=\"background-color: ${thing.bgcolor}\" data-fullname=\"${thing.fullname}\">\n  <td class=\"timestamp whitespace:nowrap\">${timestamp(thing.date, live=True, include_tense=True)}</td>\n  %if thing.is_multi:\n    <td class=\"subreddit\">${plain_link('/r/' + thing.subreddit.name, thing.subreddit.path + 'about/log', title=thing.subreddit.name)}</td>\n  %endif\n  <td class=\"whitespace:nowrap\">${thing.mod_button}</td>\n  <td class=\"button\">${thing.action_button}</td>\n  <td class=\"description\">${thing.text}&#32;\n\n  %if hasattr(thing, \"target\") and isinstance(thing.target, Comment):\n    <%\n      title = _force_unicode(thing.parent_link.title)\n      if len(title) > TITLE_MAX_WIDTH:\n          short_title = title[:TITLE_MAX_WIDTH] + '...'\n      else:\n          short_title = title\n      text = _('comment by %(comment_author)s on \"%(link_title)s\"')\n      text %= {\n        'comment_author': '[deleted]' if thing.target_author._deleted else thing.target_author.name,\n        'link_title': short_title,\n      }\n      permalink = thing.target.make_permalink(thing.parent_link, thing.subreddit)\n    %>\n    ${plain_link(text, permalink, title=title, _sr_path=False, _class=\"may-blank\")}&#32;\n  %elif hasattr(thing, \"target\") and isinstance(thing.target, Link):\n    <%\n      title = _force_unicode(thing.target.title)\n      if len(title) > TITLE_MAX_WIDTH:\n          short_title = title[:TITLE_MAX_WIDTH] + '...'\n      else:\n          short_title = title\n      text = _('link \"%(link_title)s\" by %(link_author)s')\n      text %= {\n        'link_title': short_title,\n        'link_author': '[deleted]' if thing.target_author._deleted else thing.target_author.name,\n      }\n      permalink = thing.target.make_permalink(thing.subreddit)\n    %>\n    ${plain_link(text, permalink, title=title, _sr_path=False, _class=\"may-blank\")}&#32;\n  %elif hasattr(thing, \"wrapped_user_target\"):\n    ${thing.wrapped_user_target}&#32;\n  %endif\n\n  %if thing.details_text:\n    <em>(${thing.details_text})</em>\n  %endif\n  </td>\n</tr>\n"
  },
  {
    "path": "r2/r2/templates/modaction.xml",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n    from r2.lib.filters import _force_unicode\n    from r2.lib.template_helpers import add_sr\n    from r2.models import Account, Comment, Link\n    from r2.lib.template_helpers import html_datetime\n%>\n<%namespace name=\"utils\" file=\"utils.xml\"/>\n<%\n    title = '%(subreddit)s: %(mod)s %(action)s'\n    title %= {\n        'subreddit': thing.subreddit.name,\n        'mod': thing.moderator.name,\n        'action': thing.text,\n    }\n\n    if hasattr(thing, \"target\") and isinstance(thing.target, Comment):\n        title += ' \"%s\"' % _force_unicode(thing.parent_link.title)\n    elif hasattr(thing, \"target\") and isinstance(thing.target, Link):\n        title += ' \"%s\"' % _force_unicode(thing.target.title)\n    elif hasattr(thing, \"target\") and isinstance(thing.target, Account):\n        title += ' %s' % thing.target.name\n\n    if thing.details_text:\n        title += ' (%s)' % thing.details_text\n\n    if isinstance(thing, Comment):\n        permalink = thing.target.make_permalink(thing.parent_link, thing.subreddit)\n    elif isinstance(thing, Link):\n        permalink = thing.target.make_permalink(thing.subreddit)\n    else:\n        permalink = None\n%>\n\n<entry>\n    <%utils:atom_author author=\"${thing.moderator}\" />\n    <category term=\"${thing.subreddit.name}\" label=\"/r/${thing.subreddit.name}\" />\n    <id>${thing._fullname}</id>\n    %if permalink:\n        <link href=\"${add_sr(permalink)}\"/>\n    %endif\n    <updated>${html_datetime(thing.date)}</updated>\n    <title>${title}</title>\n</entry>\n"
  },
  {
    "path": "r2/r2/templates/moderatormessagecompose.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%namespace file=\"utils.html\" import=\"error_field\"/>\n<%namespace name=\"utils\" file=\"utils.html\"/>\n<%namespace file=\"messagecompose.html\" import=\"to_field, subject_field, message_field, captcha_field\"/>\n<%!\n   from r2.lib import js\n%>\n\n${unsafe(js.use(\"messagecompose\"))}\n\n<h1>${_(\"send a private message\")}</h1>\n\n<%utils:submit_form onsubmit=\"return post_form(this, 'compose', null, null, true)\",\n                    method=\"post\", _id = \"compose-message\",\n                    action=\"/message/compose\">\n\n${from_field()}\n${to_field()}\n${subject_field()}\n${message_field()}\n${captcha_field()}\n<input type=\"hidden\" name=\"source\" value=\"compose\">\n\n<button id=\"send\" name=\"send\" type=\"submit\">${_(\"send\")}</button>\n<span class=\"status\"></span>\n\n</%utils:submit_form>\n\n<%def name=\"from_field()\">\n  ## If the user is in timeout, they can only message /r/reddit.com - so it\n  ## only makes sense to let them do that from their user account (the\n  ## default), not as a subreddit moderator.\n  %if not thing.restrict_recipient:\n    <div class=\"spacer\">\n      <%utils:round_field title=\"${_('from')}\">\n        ## On certain pages, it really only makes sense to send as a specific\n        ## subreddit.\n        %if thing.only_as_subreddit:\n          <input type=\"text\" name=\"from_sr\" value=\"${thing.mod_srs[0].name}\" readonly>\n        %else:\n          <select name=\"from_sr\">\n            <option value=\"\">${\"/u/%s\" % c.user.name}</option>\n            %for sr in thing.mod_srs:\n              <option value=\"${sr.name}\">${\"/r/%s\" % sr.name}</option>\n            %endfor\n          </select>\n        %endif\n      </%utils:round_field>\n      ${error_field(\"NO_SR_TO_SR_MESSAGE\", \"from\")}\n    </div>\n  %endif\n</%def>\n"
  },
  {
    "path": "r2/r2/templates/moderatorpermissions.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%namespace file=\"utils.html\" import=\"error_field\"/>\n\n<%def name=\"form_content()\">\n  %if not thing.embedded:\n    <input type=\"hidden\" name=\"name\"\n      value=\"${thing.user.name if thing.user else ''}\" />\n  %endif\n  <input type=\"hidden\" name=\"type\" value=\"${thing.permissions_type}\">\n  <input type=\"hidden\" name=\"permissions\"\n    value=\"${thing.permissions.dumps() if thing.permissions else '+all'}\">\n  %if not thing.embedded:\n    ${error_field(\"USER_DOESNT_EXIST\", \"name\")}\n    ${error_field(\"NO_USER\", \"name\")}\n  %endif\n  ${error_field(\"INVALID_PERMISSION_TYPE\", \"type\")}\n  ${error_field(\"INVALID_PERMISSIONS\", \"permissions\")}\n  %if not thing.embedded:\n    <span class=\"status\"></span>\n  %endif\n</%def>\n\n<div class=\"permissions\">\n  %if thing.embedded:\n    ${form_content()}\n  %else:\n    <form action=\"/post/setpermissions\" method=\"post\"\n          class=\"setpermissions pretty-form medium-text\"\n          onsubmit=\"return post_form(this, 'setpermissions')\">\n      ${form_content()}\n      <button type=\"submit\">${_('save')}</button>\n    </form>\n  %endif\n  <div class=\"permission-summary\">\n  </div>\n</div>\n"
  },
  {
    "path": "r2/r2/templates/modlisting.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%namespace file=\"printablebuttons.html\" import=\"ynbutton\" />\n<%namespace file=\"userlisting.html\" import=\"add_form, listing\"/>\n<%namespace file=\"utils.html\" import=\"error_field\"/>\n\n<div class=\"${thing._class} usertable\">\n  %if thing.can_remove_self:\n      ${ynbutton(op='unfriend',\n                 title=_(\"leave\"),\n                 executed=_(\"you are no longer a moderator\"),\n                 question=_(\"stop being a moderator?\"),\n                 format=_('you are a moderator of this subreddit. %(action)s'),\n                 format_arg='action',\n                 _class=thing.type + ' remove-self',\n                 hidden_data=dict(\n                   id=c.user._fullname,\n                   type=thing.type,\n                   container=thing.container_name),\n                 access_required=False)}\n  %endif\n\n  %if thing.has_invite:\n    ${ynbutton(op='accept_moderator_invite',\n               title=_(\"accept\"),\n               executed=_(\"you are now a moderator. welcome to the team!\"),\n               question=_(\"become a moderator of %s?\" % (\"/r/\" + c.site.name)),\n               format=_('you are invited to become a moderator. %(action)s'),\n               format_arg='action',\n               _class=thing.type + ' accept-invite',\n               access_required=False)}\n  %endif\n\n  %if thing.addable and thing.has_add_form:\n    <%call expr=\"add_form(thing.form_title, thing.destination, thing.type, thing.container_name, verb=_('add'))\">\n      ${error_field(\"ALREADY_MODERATOR\", \"name\")}\n      ${error_field(\"BANNED_FROM_SUBREDDIT\", \"name\")}\n      ${error_field(\"MUTED_FROM_SUBREDDIT\", \"name\")}\n    </%call>\n  %endif\n  ${listing()}\n</div>\n"
  },
  {
    "path": "r2/r2/templates/modsrinfobar.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2012\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%namespace file=\"utils.html\" import=\"plain_link, _md\"/>\n\n<div class=\"titlebox rounded\">\n  <h1 class=\"hover redditname\">\n    ${plain_link(c.site.name, c.site.user_path, _sr_path=False, _class=\"hover\")}\n  </h1>\n\n  <div class=\"usertext\">\n    ${_md(\"/r/mod shows only subreddits you moderate.\")}\n  </div>\n\n  ${plain_link(_(\"filter out subreddits\"), \"/me/f/mod\", _class=\"modsr-link\", _sr_path=False)}\n</div>\n"
  },
  {
    "path": "r2/r2/templates/modtoolspage.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n  from r2.lib.filters import unsafe\n%>\n\n<%namespace file=\"less.html\" import=\"less_stylesheet\"/>\n\n<%inherit file=\"reddit.html\"/>\n\n\n<%def name=\"javascript_bottom()\">\n  <% from r2.lib import js %>\n  \n  ${parent.javascript_bottom()}\n  ${unsafe(js.use('modtools'))}\n</%def>\n\n<%def name=\"global_stylesheets()\">\n  ${parent.global_stylesheets()}\n  ${less_stylesheet(\"modtools.less\")}\n</%def>\n"
  },
  {
    "path": "r2/r2/templates/morechildren.compact",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n   from r2.lib.utils import to36\n %>\n\n<% \n   cids = [to36(cm) for cm in thing.children]\n%>\n<div class=\"morechildren thing id-${thing._fullname}\">\n  <a style=\"font-size: smaller; font-weight: bold\" class=\"newbutton\"\n     id=\"more_${thing._fullname}\" href=\"javascript:void()\" \n     onclick=\"return morechildren(this, '${thing.link_name}', '${thing.sort}', '${\",\".join(cids)}', ${thing.depth})\">\n    ${_(\"load more comments\")}\n    <span class=\"gray\">&nbsp;(${thing.count} ${ungettext(\"reply\", \"replies\", thing.count)})</span>\n  </a>\n</div>\n"
  },
  {
    "path": "r2/r2/templates/morechildren.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n   from r2.lib.utils import to36\n %>\n<%inherit file=\"comment_skeleton.html\"/>\n\n\n<%def name=\"commentBody()\">\n<%\n   cids = [to36(cm) for cm in thing.children]\n%>\n<span class=\"morecomments\">\n<a style=\"font-size: smaller; font-weight: bold\" class=\"button\"\n   id=\"more_${thing._fullname}\" href=\"javascript:void(0)\"\n   onclick=\"return morechildren(this, '${thing.link_name}', '${thing.sort}', '${\",\".join(cids)}', ${thing.depth})\">\n${_(\"load more comments\")}\n<span class=\"gray\">&nbsp;(${thing.count} ${ungettext(\"reply\", \"replies\", thing.count)})</span>\n</a>\n</span>\n</%def>\n\n<%def name=\"arrows()\">\n</%def>\n\n<%def name=\"midcol(cls = '')\">\n</%def>\n"
  },
  {
    "path": "r2/r2/templates/moremessages.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%inherit file=\"message.html\"/>\n\n<%def name=\"thing_css_class(what)\" buffered=\"True\">\nmessage ${parent.thing_css_class(what)} message-parent\n</%def>\n\n<%def name=\"commentBody()\">\n</%def>\n\n<%def name=\"tagline(collapse=False)\">\n<span class=\"head\">\n  <a style=\"font-size: smaller; font-weight: bold\" \n     id=\"more_${thing._fullname}\" href=\"javascript:void(0)\"\n     onclick=\"return moremessages(this)\"\n     >\n    ${_(\"[+] load the full conversation.\")}\n  </a>\n</span>\n</%def>\n\n<%def name=\"arrows()\">\n</%def>\n\n<%def name=\"buttons()\">\n</%def>\n\n<%def name=\"midcol(cls = '')\">\n</%def>\n\n"
  },
  {
    "path": "r2/r2/templates/morerecursion.compact",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<span class=\"deepthread button\">\n  <a href=\"${thing.parent_permalink}.compact\">&raquo;${_(\"continue this thread\")}</a>\n</span>\n"
  },
  {
    "path": "r2/r2/templates/morerecursion.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%inherit file=\"comment_skeleton.html\"/>\n<%!\n    from r2.models import Link\n%>\n\n\n<%def name=\"commentBody()\">\n<%\n  url = thing.parent_permalink\n  inbound_tracking_url = Link.tracking_link(thing.parent_permalink, thing, context='continue_thread')\n%>\n<span class=\"deepthread\"><a\n    href=\"${url}\"\n    %if inbound_tracking_url != url:\n      data-href-url=\"${url}\"\n      data-inbound-url=\"${inbound_tracking_url}\"\n    %endif\n    >${_(\"continue this thread\")}</a></span>\n</%def>\n\n<%def name=\"midcol(cls='')\">\n</%def>\n\n"
  },
  {
    "path": "r2/r2/templates/morerecursion.iframe",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2014\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<a href=\"${thing.parent_permalink}\">${_(\"see more\")} →</a>"
  },
  {
    "path": "r2/r2/templates/multiinfobar.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n   from r2.config import feature\n   from r2.lib.strings import strings, Score\n   from r2.lib.pages import WrappedUser, UserText\n   from r2.lib.template_helpers import _ws, _wsf, format_html\n %>\n\n<%namespace file=\"utils.html\" import=\"plain_link, thing_timestamp, text_with_links\"/>\n<%namespace file=\"printablebuttons.html\" import=\"ynbutton, state_button\" />\n<div class=\"titlebox multi-details\" data-path=\"${thing.multi.path}\">\n  <h1 class=\"hover redditname\">\n    <a href=\"${thing.multi.path}\">${_('%s subreddits') % thing.multi.name}</a><div class=\"throbber\"></div>\n  </h1>\n  <h2><a href=\"/user/${thing.multi.owner.name}\">${_('curated by /u/%s') % thing.multi.owner.name}</a></h2>\n  <div class=\"gray-buttons settings\">\n    %if thing.can_edit:\n      <div class=\"visibility-group\">\n        <label><input type=\"radio\" name=\"visibility\" value=\"private\" ${'checked' if thing.multi.visibility == 'private' else ''}>${_('private')}</label>\n        <label><input type=\"radio\" name=\"visibility\" value=\"public\" ${'checked' if thing.multi.visibility == 'public' else ''}>${_('public')}</label>\n        %if feature.is_enabled('multireddit_customizations') or thing.multi.visibility == 'hidden':\n          <label><input type=\"radio\" name=\"visibility\" value=\"hidden\" ${'checked' if thing.multi.visibility == 'hidden' else ''}>${_('hidden')}</label>\n        %endif\n      </div>\n      <div class=\"spacer gray-buttons settings\">\n        %if thing.can_copy:\n          <button class=\"show-copy\">${_('copy')}</button>\n        %endif\n        %if thing.can_rename:\n          <button class=\"show-rename\">${_('rename')}</button>\n        %endif\n        <button class=\"delete\">${_('delete')}</button>\n      </div>\n      %if feature.is_enabled('multireddit_customizations'):\n      <div>\n        <select name=\"key_color\">\n          %if thing.multi.key_color not in thing.color_options:\n            <option selected value=\"${thing.multi.key_color}\">${_('custom')}</option>\n          %endif\n          %for color, name in thing.color_options.iteritems():\n            <option ${'selected' if color == thing.multi.key_color else ''} value=\"${color}\">${_(name)}</option>\n          %endfor\n        </select>\n        <select name=\"icon_name\">\n          <option ${'' if thing.multi.icon_id else 'selected'} value=\"\">${_('default')}</option>\n          %for icon_name in thing.icon_options:\n            <option ${'selected' if icon_name == thing.multi.icon_id else ''} value=\"${icon_name}\">${icon_name}</option>\n          %endfor\n        </select>\n      </div>\n      %endif\n    %else:\n      %if thing.can_copy:\n        <button class=\"show-copy\">${_('create a copy')}</button>\n      %endif\n    %endif\n  </div>\n\n  %if thing.can_copy:\n    <form class=\"copy-multi\">\n      <input type=\"text\" class=\"multi-name\" placeholder=\"${_('copy name')}\"><button class=\"copy\">${_('copy')} &rsaquo;</button>\n      <div class=\"throbber\"></div>\n      <div class=\"error copy-error\"></div>\n    </form>\n  %endif\n\n  %if thing.can_rename:\n    <form class=\"rename-multi\">\n      <p class=\"warning\">${_('warning: renaming a multi will break any links and references to the old name.')}</p>\n      <input type=\"text\" class=\"multi-name\" placeholder=\"${_('new name')}\"><button class=\"rename\">${_('rename')} &rsaquo;</button>\n      <div class=\"throbber\"></div>\n      <div class=\"error rename-error\"></div>\n    </form>\n  %endif\n\n  <div class=\"description\">\n    ${UserText(None, text=thing.description_md, post_form=None, editable=True, include_errors=False)}\n    %if thing.can_edit:\n      <div class=\"gray-buttons settings\">\n        <button class=\"edit-description\">${_('edit description')}</button>\n\n        %if thing.share_url:\n          <a class=\"share-in-sr\" href=\"${thing.share_url}\">${_('share')} &rsaquo;</a>\n        %endif\n      </div>\n    %endif\n  </div>\n\n  <% sr_count = format_html('<span class=\"count\">%s</span>&#32;', len(thing.srs)) %>\n  <h3>${_wsf('%(count)s subreddits in this multi:', count=sr_count)}</h3>\n  <ul class=\"subreddits\">\n  %for sr in thing.srs:\n    <li data-name=\"${sr.name}\">\n      <a href=\"/r/${sr.name}\">/r/${sr.name}</a>\n      <button class=\"remove-sr\">${_('remove')}</button>\n    </li>\n  %endfor\n  </ul>\n\n  %if thing.can_edit:\n    <form class=\"add-sr\">\n      ${thing.subreddit_selector}\n      <div class=\"error add-error\"></div>\n    </form>\n  %endif\n\n  <div class=\"bottom\">\n    %if thing.multi.owner:\n      ${_wsf(\"created by %(user)s\", user=WrappedUser(thing.multi.owner))}\n    %endif\n\n    <span class=\"age\">\n      ${_(\"a multireddit for\")}&#32;${thing_timestamp(thing.multi)}\n    </span>\n  </div>\n\n  <div id=\"multi-recs\" class=\"recommend-box\">\n\n    <div class=\"recs\">\n      <div>\n        <h1>\n          ${_(\"people also added:\")}\n        </h1>\n      </div>\n      <ul class=\"recommendations\"></ul>\n      <span class=\"more\">${_(\"more suggestions\")}</span>\n    </div>\n\n    <div class=\"endoflist\">\n      <h1>${_(\"no more suggestions!\")}</h1>\n      <div class=\"heading\">${_(\"would you like to...\")}</div>\n      <ul>\n        <li>\n          <a href=\"/r/ModeratorDuck/wiki/subreddit_classification\" target=\"_blank\">\n            ${_(\"check out subreddits by category\")}\n          </a>\n        </li>\n        <li>\n          <a href=\"/r/random\" target=\"_blank\">\n            ${_(\"explore a random subreddit\")}\n          </a>\n        </li>\n        <li>\n          <a class=\"reset\">\n            ${_(\"see the suggestions again\")}\n          </a>\n        </li>\n      </ul>\n    </div>\n\n  </div>\n</div>\n"
  },
  {
    "path": "r2/r2/templates/navbutton.compact",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%namespace file=\"utils.html\" import=\"plain_link, post_link\" />\n\n<%def name=\"plain()\">\n  ${plain_link(thing.title, \n               thing.path, _sr_path = thing.sr_path,\n               target = thing.target, \n               _class = thing.css_class, _id = thing._id)}\n</%def>\n\n<%def name=\"js()\">\n  ${plain_link(thing.title, \n               thing.path, _sr_path = False,\n               _class = thing.css_class, _id = thing._id, \n               onclick = thing.onclick)}\n</%def>\n\n<%def name=\"post()\">\n  ${post_link(thing.title, thing.base_path, thing.base_path, thing.action_params,\n              _sr_path=thing.sr_path, target=thing.target,\n              _class=thing.css_class, _id=thing._id)}\n</%def>\n"
  },
  {
    "path": "r2/r2/templates/navbutton.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%namespace file=\"utils.html\" import=\"plain_link, post_link\" />\n\n<%def name=\"plain()\">\n  ${plain_link(thing.selected_title() if thing.selected else thing.title, \n               thing.path, _sr_path = thing.sr_path,\n               target = thing.target, \n               _class = thing.css_class, _id = thing._id,\n               data=thing.data)}\n</%def>\n\n<%def name=\"js()\">\n  ${plain_link(thing.selected_title() if thing.selected else thing.title, \n               thing.path, _sr_path = False,\n               _class = thing.css_class, _id = thing._id, \n               onclick = thing.onclick,\n               data=thing.data)}\n</%def>\n\n<%def name=\"post()\">\n  ${post_link(thing.selected_title() if thing.selected else thing.title,\n              thing.base_path, thing.base_path, thing.action_params,\n              _sr_path=thing.sr_path,\n              target=thing.target, _class=thing.css_class, _id=thing._id,\n              data=thing.data)}\n</%def>\n"
  },
  {
    "path": "r2/r2/templates/navbutton.mobile",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%inherit file=\"navbutton.html\" />\n\n<%def name=\"plain()\">\n  ${parent.plain()}\n</%def>\n\n<%def name=\"js()\">\n</%def>\n\n\n"
  },
  {
    "path": "r2/r2/templates/navmenu.compact",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%def name=\"dropdown()\">\n  ${flatlist()}\n</%def>\n\n\n<%def name=\"flatlist()\">\n  <% css_class = str(thing.css_class) if thing.css_class else \"\" %>\n  %if thing:\n    <ul class=\"${css_class} hover\"\n        ${\"id='%s'\" % thing._id if thing._id else \"\"}>\n\n\n      %for i, option in enumerate(thing):\n        <%\n           ##option.title isn't the title, option.render() is the entire link\n           if option == thing.selected:\n             class_name = \"class='selected'\"\n             option.selected = True                                           \n           else:\n             class_name = \"\"\n        %>\n        <li ${class_name}>\n          ${option}\n        </li>\n      %endfor\n    </ul>\n  %endif\n</%def>\n\n<%def name=\"tabmenu()\">\n  %if thing:\n    <% css_class = str(thing.css_class) if thing.css_class else \"\" %>\n    <ul class=\"tabmenu ${css_class}\"\n        ${\"id='%s'\" % thing._id if thing._id else \"\"}>\n      %for i, option in enumerate(thing):\n        <%\n           option.selected = (option == thing.selected)\n        %>\n        <li ${\"class='selected'\" if option.selected else \"\"}>\n          ${option}\n        </li>\n      %endfor\n    </ul>\n  %endif\n</%def>\n"
  },
  {
    "path": "r2/r2/templates/navmenu.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%namespace file=\"utils.html\" import=\"plain_link, post_link, img_link, separator\"/>\n\n<%def name=\"dropdown()\">\n  ## caching comment:\n  ##  see caching comment for plain_link.  In addition to the args, \n  ##  this function depends on c.site.name and c.render_style\n  <% css_class = str(thing.css_class) if thing.css_class else \"\" %>\n  %if thing:\n    %if thing.title and thing.selected:\n      <span class=\"dropdown-title ${css_class}\">${thing.title}:&#32;</span>\n    %endif\n\n    <div class=\"dropdown ${css_class}\"\n         ${\"id='%s'\" % thing._id if thing._id else \"\"}\n         onclick=\"open_menu(this)\">\n \n      %if thing.selected:\n          <span class=\"selected\">${thing.selected.selected_title()}</span>\n      %elif thing.title:\n          <span class=\"selected title\">${thing.title}</span>\n      %endif\n    </div>\n\n    <div class=\"drop-choices ${css_class}\">\n      %for option in thing:\n        %if option != thing.selected:\n          ${option}\n        %endif\n      %endfor\n    </div>\n\n  %endif\n</%def>\n\n\n<%def name=\"flatlist()\">\n  <% css_class = str(thing.css_class) if thing.css_class else \"\" %>\n  %if thing:\n    <ul class=\"${css_class} hover\"\n        ${\"id='%s'\" % thing._id if thing._id else \"\"}>\n\n      %if thing.title:\n        <li class=\"${css_class} title\">${thing.title}</li>\n      %endif\n\n      %for i, option in enumerate(thing):\n        <%\n           ##option.title isn't the title, option.render() is the entire link\n           if option == thing.selected:\n             class_name = \"class='selected'\"\n             option.selected = True                                           \n           else:\n             class_name = \"\"\n        %>\n        <li ${class_name}>\n          %if i > 0:\n            ${separator(thing.separator)}\n          %endif\n          ${option}\n        </li>\n      %endfor\n    </ul>\n  %endif\n</%def>\n\n\n<%def name=\"tabmenu()\">\n  %if thing:\n    <% css_class = str(thing.css_class) if thing.css_class else \"\" %>\n    <ul class=\"tabmenu ${css_class}\"\n        ${\"id='%s'\" % thing._id if thing._id else \"\"}>\n      %for i, option in enumerate(thing):\n        <%\n           tab_name = getattr(option, 'tab_name', None)\n           li_id = \"id='tab-%s'\" % tab_name if tab_name else \"\"\n           li_class = \"class='selected'\" if option == thing.selected else \"\"\n        %>\n        <li ${li_id} ${li_class}>\n          ${option}\n        </li>\n      %endfor\n    </ul>\n  %endif\n</%def>\n"
  },
  {
    "path": "r2/r2/templates/navmenu.mobile",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%inherit file=\"navmenu.html\"/>\n\n<%def name=\"dropdown()\">\n</%def>\n\n\n<%def name=\"flatlist()\">\n</%def>\n\n<%def name=\"tabmenu()\">\n  ${parent.tabmenu()}\n</%def>\n"
  },
  {
    "path": "r2/r2/templates/newlink.compact",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n   from r2.lib.strings import strings\n   from r2.lib.pages import SubredditSelector, UserText\n   from r2.lib.template_helpers import add_sr\n%>\n\n<%namespace file=\"utils.html\" import=\"error_field, submit_form, plain_link\"/>\n<%namespace name=\"utils\" file=\"utils.html\"/>\n\n<%utils:submit_form onsubmit=\"return post_form(this, 'submit', linkstatus, null, true)\" \n                    action=${add_sr(\"/submit\")},\n                    _class=\"submit content\",\n                    _id=\"newlink\">\n\n%if thing.show_link and thing.show_self:\n${thing.formtabs_menu}\n%endif\n\n<div class=\"formtabs-content\">\n\n<div class=\"spacer\">\n    %if thing.show_link:\n    <div id=\"link-desc\" class=\"infobar\">${strings.submit_link}</div>\n    %endif\n    %if thing.show_self:\n    <div id=\"text-desc\" class=\"infobar\"\n      %if thing.show_link:\n        style=\"display: none;\"\n      %endif\n     >${strings.submit_text}</div>\n    %endif\n</div>\n\n<div class=\"spacer\">\n  <%utils:round_field title=\"${_('title')}\" id=\"title-field\">\n    <textarea name=\"title\" rows=\"2\" cols=\"1\" wrap=\"hard\">${thing.title}</textarea>\n    ${error_field(\"NO_TEXT\", \"title\", \"div\")}\n    ${error_field(\"TOO_LONG\", \"title\", \"div\")}\n  </%utils:round_field>\n</div>\n\n<input type=\"hidden\" name=\"extension\" value=\"compact\" />\n\n%if thing.show_link:\n<div class=\"spacer\">\n  <%utils:round_field title=\"${_('url')}\" id=\"url-field\">\n    <input name=\"kind\" value=\"link\" type=\"hidden\"/>\n    <input id=\"url\" name=\"url\" type=\"url\" value=\"${thing.url}\"/>\n    ${error_field(\"NO_URL\", \"url\", \"div\")}\n    ${error_field(\"BAD_URL\", \"url\", \"div\")}\n    ${error_field(\"ALREADY_SUB\", \"url\", \"div\")}\n    ${error_field(\"NO_LINKS\", \"sr\")}\n\n    <div>\n      <button type=\"button\" tabindex=\"100\" class=\"button\" onclick=\"fetch_title()\">${_(\"suggest title\")}</button>\n      <div class=\"title-status\" style=\"display: none;\"></div>\n\t\t<div style=\"clear: both;\"><!--clear--></div>\n    </div>\n  </%utils:round_field>\n</div>\n%endif\n\n%if thing.show_self:\n<div class=\"spacer\">\n  <%utils:round_field title=\"${_('text')}\", description=\"${_('(optional)')}\" id=\"text-field\"\n   style=\"${'display: none;' if thing.show_link else ''}\">\n\n    <input name=\"kind\" value=\"self\" type=\"hidden\"/>\n\n    ${UserText(None, text = thing.text, have_form = False, creating = True)}\n\n    ${error_field(\"NO_SELFS\", \"sr\")}\n  </%utils:round_field>\n</div>\n%endif\n\n<div class=\"spacer\">\n  <%utils:round_field title=\"${_('subreddit')}\" id=\"reddit-field\">\n    ${SubredditSelector(thing.default_sr, extra_subreddits=thing.extra_subreddits)}\n  </%utils:round_field>\n</div>\n\n${thing.captcha}\n    \n</div>\n<input name=\"resubmit\" value=\"${thing.resubmit}\" type=\"hidden\"/>\n<div class=\"spacer\">\n<button class=\"btn save button\" name=\"submit\" value=\"form\" type=\"submit\">${_(\"submit\")}</button>\n  <span class=\"status\"></span>\n  ${error_field(\"RATELIMIT\", \"ratelimit\")}\n  ${error_field(\"IN_TIMEOUT\", \"sr\")}\n</div>\n<div style=\"clear: both;\"><!--fffuuuuuuuu--></div>\n</%utils:submit_form>\n\n%if thing.show_link and thing.show_self:\n<script type=\"text/javascript\">\n  var form = $(\"#newlink\");\n  if(form.length) {\n    var default_menu = form.find(\".${thing.default_tab}-button:first\");\n    select_form_tab(default_menu, \"${thing.default_show}\", \"${thing.default_hide}\");\n    }\n</script>\n%endif\n"
  },
  {
    "path": "r2/r2/templates/newlink.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n   from r2.lib.strings import strings\n   from r2.lib.pages import SubredditSelector, UserText\n   from r2.lib.template_helpers import add_sr, _wsf, format_html\n   from r2.lib.filters import safemarkdown\n%>\n\n<%namespace file=\"utils.html\" import=\"error_field, submit_form, _a_buffered, text_with_links\"/>\n<%namespace name=\"utils\" file=\"utils.html\"/>\n\n<%\n  if thing.default_sr:\n    sr = format_html(\"&#32;%s\", unsafe(_a_buffered(thing.default_sr.name, href=thing.default_sr.path)))\n  else:\n    sr = _(\"reddit\")\n%>\n\n<h1>${_wsf(\"submit to %(sr)s\", sr=sr)}</h1>\n\n<%utils:submit_form onsubmit=\"return post_form(this, 'submit', linkstatus, null, true)\"\n                    action=${add_sr(\"/submit\")},\n                    _class=\"submit content warn-on-unload\",\n                    _id=\"newlink\">\n\n%if thing.show_link and thing.show_self:\n${thing.formtabs_menu}\n%endif\n\n<div class=\"formtabs-content\">\n\n<div class=\"spacer\">\n    %if thing.show_link:\n        <div id=\"link-desc\" class=\"infobar\">${strings.submit_link}</div>\n    %endif\n    %if thing.show_self:\n        <div id=\"text-desc\" class=\"infobar\">${strings.submit_text}</div>\n    %endif\n</div>\n\n<div class=\"spacer\">\n  <%utils:round_field title=\"${_('title')}\" id=\"title-field\">\n    <textarea name=\"title\" rows=\"2\" required>${thing.title}</textarea>\n    ${error_field(\"NO_TEXT\", \"title\", \"div\")}\n    ${error_field(\"TOO_LONG\", \"title\", \"div\")}\n  </%utils:round_field>\n</div>\n\n%if thing.show_link:\n<div class=\"spacer\">\n  <%utils:round_field title=\"${_('url')}\" id=\"url-field\">\n    <input name=\"kind\" value=\"link\" type=\"hidden\"/>\n    <input id=\"url\" name=\"url\" type=\"url\" value=\"${thing.url}\" required>\n    ${error_field(\"NO_URL\", \"url\", \"div\")}\n    ${error_field(\"BAD_URL\", \"url\", \"div\")}\n    ${error_field(\"DOMAIN_BANNED\", \"url\", \"div\")}\n    ${error_field(\"ALREADY_SUB\", \"url\", \"div\")}\n    ${error_field(\"NO_LINKS\", \"sr\")}\n    ${error_field(\"NO_SELFS\", \"sr\")}\n\n    <div id=\"suggest-title\">\n      <span class=\"title-status\"></span>\n      <button type=\"button\" tabindex=\"100\" onclick=\"fetch_title()\">${_(\"suggest title\")}</button>\n    </div>\n  </%utils:round_field>\n</div>\n%endif\n\n%if thing.show_self:\n<div class=\"spacer\">\n  <%utils:round_field title=\"${_('text')}\", description=\"${_('(optional)')}\" id=\"text-field\">\n    <input name=\"kind\" value=\"self\" type=\"hidden\"/>\n\n    ${UserText(None, text = thing.text, have_form = False, creating = True)}\n\n    ${error_field(\"NO_SELFS\", \"sr\")}\n  </%utils:round_field>\n</div>\n%endif\n\n<div class=\"spacer\">\n  <%utils:round_field title=\"${_('choose a subreddit')}\" id=\"reddit-field\">\n    ${SubredditSelector(thing.default_sr, extra_subreddits=thing.extra_subreddits, required=True)}\n  </%utils:round_field>\n</div>\n\n<div class=\"spacer\">\n    <div class=\"submit_text roundfield\">\n        <h1>${_wsf('submitting to %(sr)s', sr=unsafe('/r/<span class=\"sr\"></span>'))}</h1>\n        <span class=\"content md-container\">\n            %if thing.default_sr and thing.default_sr.submit_text:\n                ${unsafe(safemarkdown(thing.default_sr.submit_text))}\n            %endif\n        </span>\n    </div>\n</div>\n\n<div class=\"spacer\">\n  <%utils:round_field title=\"${_('options')}\">\n    <input class=\"nomargin\" type=\"checkbox\" checked=\"checked\" name=\"sendreplies\" id=\"sendreplies\" data-send-checked=\"true\"/>\n    <label for=\"sendreplies\">\n      ${_(\"send replies to my inbox\")}\n    </label>\n  </%utils:round_field>\n</div>\n\n${thing.captcha}\n    \n</div>\n\n<div class=\"roundfield info-notice\">\n  ${text_with_links(_(\"please be mindful of reddit's %(content_policy)s and practice %(good_reddiquette)s.\"),\n      content_policy=dict(\n        link_text=_(\"content policy\"),\n        path=\"/help/contentpolicy\",\n        target=\"_blank\"),\n      good_reddiquette=dict(\n        link_text=_(\"good reddiquette\"),\n        path=\"/wiki/reddiquette\",\n        target=\"_blank\"),\n  )}\n</div>\n\n<input name=\"resubmit\" value=\"${thing.resubmit}\" type=\"hidden\"/>\n<div class=\"spacer\">\n  <button class=\"btn\" name=\"submit\" value=\"form\" type=\"submit\">${_(\"submit\")}</button>\n  <span class=\"status\"></span>\n  ${error_field(\"RATELIMIT\", \"ratelimit\")}\n  ${error_field(\"INVALID_OPTION\", \"sr\")}\n  ${error_field(\"IN_TIMEOUT\", \"sr\")}\n</div>\n</%utils:submit_form>\n\n%if thing.show_self and thing.show_link:\n<script type=\"text/javascript\">\n  $(function() {\n  var form = $(\"#newlink\");\n  if(form.length) {\n    var default_menu = form.find(\".${thing.default_tab}-button:first\");\n    select_form_tab(default_menu, \"${thing.default_show}\", \"${thing.default_hide}\");\n    }\n  });\n</script>\n%endif\n"
  },
  {
    "path": "r2/r2/templates/newsletter.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n  from r2.lib.template_helpers import make_url_https, static\n  from r2.models.admintools import wiki_template\n%>\n<%namespace file=\"less.html\" import=\"less_stylesheet\"/>\n<%namespace file=\"utils.html\" import=\"form_group, md\" />\n\n<%inherit file=\"reddit.html\"/>\n\n<%def name=\"Title()\">\n${_('reddit newsletter - upvoted weekly')}\n</%def>\n\n<%def name=\"viewport()\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n</%def>\n\n<%def name=\"bodyContent()\">\n\n<section class=\"newsletter-box newsletter-container\">\n  <header>\n    <h1 class=\"subscribe-callout\">${_(\"subscribe to reddit's official newsletter,\")}&#32;<span class=\"upvoted-weekly-logo\"><span class=\"screenreader-only\">${_('upvoted weekly')}</span></span></h1>\n    <div class=\"subscribe-thanks\"><img src=\"${static('subscribe-header-thanks-black.svg')}\" alt=\"_('thanks for subscribing')\" /></div>\n    <h2 class=\"result-message\">\n      ${_(\"Enter your email to get the very best of reddit's content curated, packaged, and delivered to your inbox once a week. It's free and we'll never spam you.\")}\n    </h2>\n    <div class=\"subscribe-thanks\"><a href=\"/\" class=\"c-btn c-btn-primary\">${_('back to reddit')}</a></div>\n  </header>\n  <form class=\"newsletter-signup form-v2 c-form-inline\" method=\"post\" action=\"${make_url_https('/api/newsletter.json')}\">\n    <input type=\"hidden\" name=\"uh\" value=\"${c.modhash}\" />\n    <input type=\"hidden\" name=\"source\" value=\"standalone\">\n    <%call expr=\"form_group('email', 'BAD_EMAIL', show_errors=True, feedback_inside_input=True)\">\n      <label for=\"email\" class=\"screenreader-only\">${_('email')}:</label>\n      <input value=\"\"\n             name=\"email\"\n             class=\"c-form-control\"\n             type=\"email\"\n             placeholder=\"${_('enter your email')}\"\n             data-validate-url=\"/api/check_email.json\"\n             data-validate-on=\"change blur\">\n    </%call>\n    <button type=\"submit\" class=\"c-btn c-btn-primary\">${_('subscribe')}</button>\n  </form>\n\n  <a class=\"faq-toggle\" href=\"#\">${_('More Info')}&#32;</a>\n\n  <div class=\"faq md\">\n  <%\n    md(wiki_template('newsletter_faq'))\n  %>\n  </div>\n</section>\n\n<div class=\"upvoted-gradient\" role=\"presentation\"></div>\n\n</%def>\n"
  },
  {
    "path": "r2/r2/templates/newsletterbar.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n  from r2.lib.template_helpers import make_url_https, static\n%>\n<%namespace file=\"utils.html\" import=\"form_group\" />\n\n<section hidden class=\"infobar newsletterbar newsletter-container\">\n  <header>\n    <h1 class=\"subscribe-callout\">\n      <a href=\"/newsletter\"><img src=\"${static('subscribe-header.svg')}\" alt=\"${_('subscribe to our newsletter')}\"></a>\n    </h1>\n    <div class=\"subscribe-thanks\"><img src=\"${static('subscribe-header-thanks.svg')}\" alt=\"_('thanks for subscribing')\" /></div>\n    <h2 class=\"result-message\">${_('get the best of reddit, delivered once a week')}</h2>\n  </header>\n  <form class=\"newsletter-signup form-v2 c-form-inline\" method=\"post\" action=\"${make_url_https('/api/newsletter.json')}\">\n    <input type=\"hidden\" name=\"uh\" value=\"${c.modhash}\" />\n    <input type=\"hidden\" name=\"source\" value=\"newsletterbar\">\n    <%call expr=\"form_group('email', 'BAD_EMAIL', show_errors=True, feedback_inside_input=True)\">\n      <label for=\"email\" class=\"screenreader-only\">${_('email')}:</label>\n      <input value=\"\"\n             name=\"email\"\n             class=\"c-form-control\"\n             type=\"email\"\n             placeholder=\"${_('enter your email')}\"\n             data-validate-url=\"/api/check_email.json\"\n             data-validate-on=\"change blur\">\n    </%call>\n\n    <button type=\"submit\" class=\"c-btn c-btn-highlight\">${_('subscribe')}</button>\n  </form>\n  <a href=\"#\" class=\"newsletter-close\" title=\"close\">&times;</a>\n</section>\n"
  },
  {
    "path": "r2/r2/templates/oauth2authorization.compact",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%inherit file=\"oauth2authorization.html\"/>\n"
  },
  {
    "path": "r2/r2/templates/oauth2authorization.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n   import datetime\n   from r2.models import OAuth2AccessToken\n   from r2.lib.template_helpers import static, make_url_protocol_relative, _wsf, format_html, add_sr\n%>\n<%namespace file=\"clientinfobar.html\" import=\"app_link\" />\n<%namespace file=\"prefapps.html\" import=\"scope_details\" />\n<%\n   if thing.client.icon_url:\n     icon = make_url_protocol_relative(thing.client.icon_url)\n   else:\n     icon = static(\"defaultapp.png\")\n   app_name = format_html(\n       \"<!-- SC_OFF --> <b>%s</b> <!-- SC_ON -->\", thing.client.name)\n%>\n<div class=\"content oauth2-authorize\">\n  <div class=\"icon\">\n    &nbsp;\n    <img src=\"${icon}\" alt=\"${thing.client.name} icon\" />\n    &nbsp;\n  </div>\n  <h1>\n    <%\n      username_link = format_html(\n        '<a href=\"%(user_url)s\">%(username)s</a>',\n        username=c.user.name,\n        user_url=add_sr(\"/user/%s\" % c.user.name, sr_path=False),\n      )\n    %>\n    ${_wsf(\n      \"Hey %(username)s! %(app)s would like to connect with your reddit account.\",\n      app=unsafe(app_link(thing.client)),\n      username=username_link,\n    )}\n  </h1>\n  <div class=\"access\">\n    <div class=\"access-permissions\">\n        <h2>${_wsf(\"Allow %(app)s to:\", app=app_name)}</h2>\n        ${scope_details(thing.scope, expiration=thing.expiration)}\n    </div>\n    <p class=\"notice\">\n        ${_wsf(\"%(app)s will not be able to access your reddit password.\", app=app_name)}\n    </p>\n    <form method=\"post\" action=\"/api/v1/authorize\" class=\"pretty-form\">\n      <input type=\"hidden\" name=\"client_id\" value=\"${thing.client._id}\" />\n      <input type=\"hidden\" name=\"redirect_uri\" value=\"${thing.redirect_uri}\" />\n      <input type=\"hidden\" name=\"scope\" value=\"${str(thing.scope)}\" />\n      <input type=\"hidden\" name=\"state\" value=\"${thing.state}\" />\n      <input type=\"hidden\" name=\"response_type\" value=\"${thing.response_type}\" />\n      <input type=\"hidden\" name=\"duration\" value=\"${thing.duration}\" />\n      <input type=\"hidden\" name=\"uh\" value=\"${c.modhash}\"/>\n      <div>\n        <input type=\"submit\" class=\"fancybutton newbutton allow\" name=\"authorize\"\n               value=\"${_(\"Allow\")}\" />\n        <input type=\"submit\" class=\"fancybutton newbutton decline red\"\n               value=\"${_(\"Decline\")}\" />\n      </div>\n    </form>\n  </div>\n</div>\n\n"
  },
  {
    "path": "r2/r2/templates/optout.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<div class=\"opt-form\">\n%if thing.leave:\n  %if thing.sent:\n  <p> \n    ${_(\"The address '%(email)s' will no longer receive email from us.\") % dict(email=thing.email)}\n  </p>\n  <p>\n     ${_(\"A confirmation email has been queued up and should be reaching you shortly.  It will be the last you hear from us.\")}\n   </p>\n  %else:\n   <p>\n     ${_(\"Would you like the address %(email)s to no longer receive email from us?\") % dict(email=thing.email)}\n     <form method=\"post\" action=\"/post/optout\">\n       <input type=\"hidden\" name=\"x\" value=\"${thing.msg_hash}\" />\n       <input type=\"submit\" name=\"accept\" value=\"${_('yes')}\" />\n     </form>\n     <form method=\"get\" action=\"/\">\n       <input type=\"submit\" name=\"decline\" value=\"${_('no')}\" />\n     </form>\n   </p>\n  %endif\n%elif thing.sent:\n  <p> \n    ${_(\"'%(email)s' has been removed from our block list.\") % dict(email=thing.email)}\n  </p>\n%else:\n  <p>\n  ${_(\"Allow '%(email)s' to receive email from us?\") % dict(email=thing.email)}\n  <form method=\"post\" action=\"/post/optin\">\n    <input type=\"hidden\" name=\"x\" value=\"${thing.msg_hash}\" />\n    <input type=\"submit\" name=\"accept\" value=\"${_('yes')}\" />\n  </form>\n  <form method=\"get\" action=\"/\">\n    <input type=\"submit\" name=\"decline\" value=\"${_('no')}\" />\n  </form>\n  </p>\n%endif\n</div>\n"
  },
  {
    "path": "r2/r2/templates/over18interstitial.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n  from r2.lib.template_helpers import static \n%>\n\n<%namespace file=\"utils.html\" import=\"submit_form\"/>\n<%namespace name=\"utils\" file=\"utils.html\"/>\n\n<%inherit file=\"interstitial.html\"/>\n\n<%def name=\"interstitial_image_attrs()\">\n  src=\"${static('interstitial-image-over18.png')}\"\n  alt=\"${_('over 18')}\"\n</%def>\n\n<%def name=\"interstitial_title()\">\n  ${_(\"You must be 18+ to view this community\")}\n</%def>\n\n<%def name=\"interstitial_message()\">\n  <p>\n    ${_(\"You must be at least eighteen years old to view this content. Are you over eighteen and willing to see adult content?\")}\n  </p>\n</%def>\n\n<%def name=\"interstitial_buttons()\">\n  <%utils:submit_form _class=\"pretty-form\">\n    <div class=\"buttons\">\n      <button class=\"c-btn c-btn-primary\" type=\"submit\" name=\"over18\" value=\"no\">\n        ${_(\"no thank you\")}\n      </button>\n      <button class=\"c-btn c-btn-primary\" type=\"submit\" name=\"over18\" value=\"yes\">\n        ${_(\"continue\")}\n      </button>\n    </div>\n  </%utils:submit_form>\n</%def>\n"
  },
  {
    "path": "r2/r2/templates/pagenamenav.compact",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%namespace file=\"utils.html\" import=\"plain_link\"/>\n\n<%def name=\"domain()\">\n  <span class=\"hover pagename redditname\">\n    ${plain_link(c.site.name, c.site.user_path, _sr_path=False)}\n  </span>\n</%def>\n\n<%def name=\"subreddit()\">\n  <span class=\"hover pagename redditname\">\n    ${plain_link(c.site.name, c.site.user_path, _sr_path=False)}\n  </span>\n</%def>\n\n<%def name=\"subreddits()\">\n  <span class=\"hover pagename redditname\">\n    ${plain_link(_(\"subreddits\"), \"/subreddits/\", _sr_path=False)}\n  </span>\n</%def>\n\n<%def name=\"nomenu()\">\n${thing.title}\n</%def>\n\n<%def name=\"help()\">\n  <span class=\"hover pagename redditname\">\n    ${plain_link(thing.title, \"/help\", _sr_path=False)}\n  </span>\n</%def>\n\n<%def name=\"newpagelink()\">\n<span class=\"newpagelink\">reddit all?&#32;${plain_link(\"click here to find new links.\", \"/new/\", _sr_path=False)}</span>\n</%def>\n"
  },
  {
    "path": "r2/r2/templates/pagenamenav.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%namespace file=\"utils.html\" import=\"plain_link, _md\"/>\n\n<%def name=\"subreddit()\">\n  <span class=\"hover pagename redditname\">\n    ${plain_link(c.site.name, c.site.user_path, _sr_path=False)}\n    % if hasattr(thing, \"title\"):\n      : ${thing.title}\n    % endif\n  </span>\n</%def>\n\n<%def name=\"domain()\">\n  <div id=\"sr-name-box\">\n    <span class=\"hover pagename redditname\">\n      ${plain_link(getattr(c.site, \"idn\", c.site.name), c.site.user_path, _sr_path=False)}\n      % if hasattr(thing, \"title\"):\n        : ${thing.title}\n      % endif\n    </span>\n    % if hasattr(c.site, \"idn\"):\n    <span class=\"help help-hoverable tooltip\">\n      ${c.site.name}\n      <div id=\"idn-help\" class=\"hover-bubble help-bubble anchor-top-left\">\n        <div class=\"help-section help-idn\">\n          <p>\n            ${_md(\"This is an [internationalized domain name](http://en.wikipedia.org/wiki/Internationalized_domain_name).  We've modified how it is displayed [for security reasons](http://en.wikipedia.org/wiki/IDN_homograph_attack).\")}\n          </p>\n        </div>\n      </div>\n    </span>\n    % endif\n  </div>\n</%def>\n\n<%def name=\"subreddits()\">\n  <span class=\"hover pagename redditname\">\n    ${plain_link(_(\"subreddits\"), \"/subreddits/\", _sr_path=False)}\n  </span>\n</%def>\n\n<%def name=\"nomenu()\">\n<span class=\"pagename selected\">${thing.title}</span>\n</%def>\n\n<%def name=\"help()\">\n  <span class=\"hover pagename redditname\">\n    ${plain_link(thing.title, \"/wiki\", _sr_path=False)}\n  </span>\n</%def>\n\n<%def name=\"newpagelink()\">\n<span class=\"newpagelink\">reddit all?&#32;${plain_link(\"click here to find new links.\", \"/new/\", _sr_path=False)}</span>\n</%def>\n"
  },
  {
    "path": "r2/r2/templates/pagenamenav.mobile",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%def name=\"domain()\">\n  <span class=\"hover pagename redditname\">\n    <a href=\"${c.site.path}\">${c.site.name}</a>\n  </span>\n</%def>\n\n<%def name=\"subreddit()\">\n  <span class=\"hover pagename redditname\">\n    <a href=\"${c.site.path}\">${c.site.name}</a>\n  </span>\n</%def>\n\n<%def name=\"subreddits()\">\n  <span class=\"hover pagename redditname\">\n    <a href=\"/subreddits/\">${_(\"subreddits\")}</a>\n  </span>\n</%def>\n\n<%def name=\"nomenu()\">\n<span class=\"pagename selected\">${thing.title}</span>\n</%def>\n\n<%def name=\"newpagelink()\">\n</%def>\n"
  },
  {
    "path": "r2/r2/templates/panestack.compact",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%inherit file=\"panestack.html\" />\n"
  },
  {
    "path": "r2/r2/templates/panestack.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n%for t in thing.stack:\n  %if thing.div:\n    <div ${thing.div_id and (\"id='%s'\" % str(thing.div_id)) or \"\"}\n         ${thing.css_class and (\"class='%s'\" % str(thing.css_class)) or \"\"}>\n  %endif\n\n  %if thing.title:\n    <div class=\"panestack-title\">\n      <span class=\"title\">${thing.title}</span>\n      %for link_text, link, link_class in thing.title_buttons:\n         <a href=\"${link}\" class=\"title-button ${link_class}\">${link_text}</a>\n      %endfor\n    </div>\n  %endif\n\n  ${t}\n\n  %if thing.div:\n    </div>\n  %endif\n%endfor\n"
  },
  {
    "path": "r2/r2/templates/panestack.htmllite",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n%for t in thing.stack:\n  ${t}\n%endfor\n"
  },
  {
    "path": "r2/r2/templates/panestack.iframe",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2014\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n%for t in thing.stack:\n  ${t}\n%endfor\n"
  },
  {
    "path": "r2/r2/templates/panestack.mobile",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n%for t in thing.stack:\n  ${t}\n%endfor\n"
  },
  {
    "path": "r2/r2/templates/panestack.xml",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n%for t in thing.stack:\n  ${t}\n%endfor\n"
  },
  {
    "path": "r2/r2/templates/password.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%namespace file=\"utils.html\" import=\"error_field\"/>\n<%namespace name=\"utils\" file=\"utils.html\"/>\n\n<form id=\"passform\" action=\"/api/password\" method=\"post\"\n      class=\"content\"\n      onsubmit=\"return post_form(this, 'password');\">\n  <h1>${_(\"reset your password\")}</h1>\n  <p>${_(\"enter your username below and we'll email you a link to reset your password\")}</p>\n  %if request.params.get('expired'):\n    <span class=\"error\">\n      ${_(\"password reset link expired. please try again\")}\n    </span>\n  %endif\n\n<div class=\"spacer\">\n  <%utils:round_field title=\"${_('username')}\">\n    <input type=\"text\" name=\"name\" maxlength=\"20\"/>\n    ${error_field(\"USER_DOESNT_EXIST\", \"name\")}\n    ${error_field(\"NO_EMAIL_FOR_USER\", \"name\")}\n    ${error_field(\"RATELIMIT\", \"ratelimit\")}\n  </%utils:round_field>\n</div>\n\n<button type=\"submit\" class=\"btn\">${_(\"email me\")}</button>\n<span class=\"status\"></span>\n\n</form>\n\n"
  },
  {
    "path": "r2/r2/templates/passwordchangeemail.email",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\nThe password for /u/${thing.user.name} has been changed.\n\nIf you did not change your password, please respond to this e-mail\nimmediately.\n"
  },
  {
    "path": "r2/r2/templates/passwordreset.email",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\na password reset has been requested for the reddit username: ${thing.user.name}\n\nif you did not make this request, you can safely ignore this email. a password reset request can be made by anyone, and it does not indicate that your account is in any danger of being accessed by someone else.\n\nif you do actually want to reset your password, visit this link:\n\n    ${thing.passlink}\n\nthanks for using the site!\n"
  },
  {
    "path": "r2/r2/templates/paymentform.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%namespace file=\"utils.html\" import=\"error_field\"/>\n\n<div class=\"payment-setup\">\n  <h1>${_(\"set up payment for this link\")}</h1>\n  <form method=\"POST\" action=\"/post/pay\" id=\"pay-form\"\n        class=\"pretty-form\"\n        onsubmit=\"return post_form(this, 'update_pay')\">\n    <p>\n      ${_(\"The duration of this link is %(duration)s (from %(start)s to %(end)s).\") % dict(duration=thing.duration, start=thing.start_date, end=thing.end_date)}\n    </p>\n    <p id=\"bid-field\">\n      <input type=\"hidden\" name=\"campaign\" value=\"${thing.campaign_id36}\" />\n      <input type=\"hidden\" name=\"link\" value=\"${thing.link._fullname}\" />\n      ${_(\"Your current budget is %(budget)s\") % dict(budget=thing.budget)}\n      ${error_field(\"OVERSOLD_DETAIL\", \"bid\", \"div\")}\n  </p>\n  %if thing.profiles:\n  <p>\n    ${_(\"Please pick your credit card:\")}\n    <select onchange=\"change_address(this)\" name=\"account\">\n      <option value=\"0\">${_(\"select...\")}</option>\n      %for profile in thing.profiles:\n        <option value=\"${profile.customerPaymentProfileId}\">\n          Card: ${profile.payment.creditCard.cardNumber}\n        </option>\n      %endfor\n      %if len(thing.profiles) >= thing.max_profiles:\n        <option disabled=true>${_(\"profile limit (%d) reached\" % thing.max_profiles)}</option>\n      %else:\n        <option value=\"\">${_(\"create a new one...\")}</option>\n      %endif\n    </select>\n  </p>\n  %else:\n    <p class=\"info\">\n      ${_(\"please create a new payment profile\")}\n    </p>\n  %endif\n  <p class=\"info\">\n   ${_(\"NOTE: your card will not be charged until the campaign has been queued \"\n       \"for promotion.\")}\n  </p>\n    <input type=\"hidden\" name=\"customer_id\" value=\"${thing.customer_id}\" />\n    \n    ${profile_info(None, disabled=bool(thing.profiles))}\n    %for profile in thing.profiles:\n      ${profile_info(profile, disabled=True)}\n    %endfor\n  </form>\n  \n  <script type=\"text/javascript\">\n    function enable_all(elem) {\n       $(elem).parents(\"div.pay-form\").addBack()\n             .find(\"[disabled]\").removeAttr(\"disabled\").end()\n             .find(\"table\").removeClass(\"disabled\").end()\n             .find(\"[name=edit]\").val(\"on\");\n       return false;\n    }\n    function disable_all(elem) {\n       var what = $(elem).parents(\"div.pay-form\").addBack()\n                     .find(\"table\").addClass(\"disabled\")\n                     .find(\":input\").not(\"button\")\n                     .attr(\"disabled\", \"disabled\");\n       return false;\n    }\n    function change_address(what) {\n       var val = $(what).val();\n       var inputs = $(\".pay-form\").not(\"#form-option-\" + val).hide()\n                       .each(function() { disable_all(this); })\n                       .end()\n                       .filter(\"#form-option-\" + val).show();\n       if (val == \"\" || inputs.find(\"[name=edit]\").val() == \"on\") \n            enable_all(inputs);\n    }\n\n    (function($) {\n      $.payment_redirect = function(url, isNewCard, amount) {\n        r.analytics.fireFunnelEvent('ads', isNewCard ? 'checkout-new' : 'checkout-existing');\n        r.analytics.fireFunnelEvent('ads', 'checkout-pay', {value: amount}, function() {\n          window.location.href = url;\n        });\n      };\n    })(window.jQuery);\n  </script>\n</div>\n\n<%def name=\"profile_info(profile, disabled=False)\">\n   <%\n      address = ((_(\"first name\") , \"firstName\", \"\"),\n                 (_(\"last name\")  , \"lastName\", \"\"),\n                 (_(\"company\")    , \"company\", _(\"(optional)\")),\n                 (_(\"address\")    , \"address\", \"\"),\n                 (_(\"city\")       , \"city\", \"\"),\n                 (_(\"state\")      , \"state\", \"\"),\n                 (_(\"zip\")        , \"zip\", \"\"),\n                 (_(\"country\")    , \"country\", \"\"),\n                 (_(\"phone\")      , \"phoneNumber\", _(\"(optional)\")))\n      cc      = ((_(\"card number\")     , \"cardNumber\", _(\"(14-17 digits)\")),\n                 (_(\"expiration date\") , \"expirationDate\", \"(YYYY-MM please)\"),\n                 (_(\"CCV\")             , \"cardCode\", _(\"(3 or 4 digits)\")))\n      bill_to = getattr(profile, \"billTo\",None)\n      credit  = getattr(profile, \"payment\", None)\n      credit  = getattr(credit,  \"creditCard\", None)\n      prof_id = getattr(profile, \"customerPaymentProfileId\", \"\")\n      display  = \"style='display:none'\" if disabled else ''\n      disabled = \"disabled\" if disabled else \"\"\n    %>\n   <div id=\"form-option-${prof_id}\" class=\"pay-form\" ${display}>\n   %if profile:\n     <input type=\"hidden\" name=\"pay_id\" ${disabled}\n            value=\"${prof_id}\" />\n   %endif\n   <input type=\"hidden\" name=\"edit\" ${disabled}\n          value=\"${'off' if profile else 'on'}\" />\n   <table class=\"content preftable ${'disabled' if disabled else ''}\">\n     %for fields, data, error_name in ((address, bill_to, \"BAD_ADDRESS\"), (cc, credit, \"BAD_CARD\")):\n     %for label, field, optional in fields:\n     <tr class=\"payment-${field.lower()}\" \n         id=\"payment-${field.lower()}-${prof_id}\">\n       <th><label for=\"input-${field}-${prof_id}\">\n         ${label}\n       </label></th>\n       <td>\n         %if field == \"address\":\n           <textarea ${disabled} id=\"input-${field}-${prof_id}\" name=\"${field}\">\n             ${getattr(data, field, '')}\n           </textarea>\n         %elif field == \"country\":\n           <%\n            selected_country = getattr(data, field, thing.default_country)\n            selected_insensitive = selected_country.lower()\n           %>\n           %if any(map(lambda country: country.lower() == selected_insensitive, thing.countries)):\n            <select ${disabled} id=\"input-${field}-${prof_id}\" name=\"${field}\">\n              %for country in thing.countries:\n                <option ${\"selected\" if selected_insensitive == country.lower() else \"\"}>${country}</option>\n              %endfor\n            </select>\n           %else:\n            <input type=\"text\" ${disabled} id=\"input-${field}-${prof_id}\" name=\"${field}\" value=\"${selected_country}\">\n           %endif\n         %else:\n           <input ${disabled}\n                  id=\"input-${field}-${prof_id}\" type=\"text\" name=\"${field}\" \n                  value=\"${getattr(data, field, '')}\" />\n         %endif\n         %if optional:\n           <span class=\"optional\">${optional}</span>\n         %endif\n       </td>\n       <td>\n         ${error_field(error_name, field)}\n       </td>\n     </tr>\n     %endfor\n     %endfor\n     <tr>\n       <td></td>\n       <td>\n         <button type=\"submit\">${_(\"authorize payment\")}</button>\n         %if disabled and profile:\n            <button onclick=\"$(this).hide();return enable_all(this);\">\n              ${_(\"edit\")}\n            </button>\n         %endif\n       </td>\n     </tr>       \n     <tr>\n       <td></td>\n       <td>\n         <span class=\"status\"></span>\n       </td>\n     </tr>\n   </table>\n   </div>\n</%def>\n"
  },
  {
    "path": "r2/r2/templates/permalinkmessage.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<div class=\"infobar\">\n    ${_(\"you are viewing a single comment's thread.\")}\n    <p>\n      <a href=\"${thing.comments_url}\">${_(\"view the rest of the comments\")}</a>\n        &nbsp;&#8594;\n    </p>\n</div>\n"
  },
  {
    "path": "r2/r2/templates/policypage.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%namespace file=\"less.html\" import=\"less_stylesheet\"/>\n<%inherit file=\"reddit.html\"/>\n\n<%def name=\"viewport()\">\n<meta name=\"viewport\" content=\"width=700, initial-scale=1\">\n</%def>\n\n<%def name=\"stylesheet()\">\n  ${parent.stylesheet()}\n  ${less_stylesheet('policies.less')}\n</%def>\n\n<%def name=\"javascript_bottom()\">\n  ${parent.javascript_bottom()}\n  <% from r2.lib import js %>\n  ${unsafe(js.use('policies'))}\n</%def>\n"
  },
  {
    "path": "r2/r2/templates/policyview.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<div class=\"doc-info\">\n  ${thing.toc_html}\n\n  <h4>${_('revisions')}</h4>\n  <ul class=\"revisions\">\n    %for rev in thing.revs:\n      <li\n        %if rev['id'] == thing.display_rev:\n          class=\"selected\"\n        %endif\n      >\n        <a href=\"?v=${rev['id']}\">${rev['title']}</a>\n      </li>\n    %endfor\n  </ul>\n</div>\n\n${thing.body_html}\n"
  },
  {
    "path": "r2/r2/templates/popup.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n\n<script id=\"${thing.popup_id}\" type=\"text/template\">\n  ${thing.content}\n</script>\n"
  },
  {
    "path": "r2/r2/templates/prefapps.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n  from r2.lib.filters import jssafe\n  from r2.lib.template_helpers import static, make_url_protocol_relative\n  from r2.lib.utils import timeuntil\n%>\n\n<%namespace name=\"utils\" file=\"utils.html\" />\n<%namespace file=\"utils.html\" import=\"_md\"/>\n<%namespace file=\"utils.html\" import=\"error_field, plain_link\" />\n<%namespace file=\"printablebuttons.html\" import=\"ajax_ynbutton, ynbutton\" />\n\n<%def name=\"icon(app)\">\n  <div class=\"app-icon\">\n    &nbsp;\n    <img src=\"${make_url_protocol_relative(app.icon_url) or static('defaultapp.png')}\">\n    &nbsp;\n  </div>\n</%def>\n\n<%def name=\"developers(app)\">\n  <% devs = app._developers %>\n  %if devs:\n    <div class=\"app-developers\">\n      Developers:&#32;\n      %for i, dev in enumerate(sorted(devs, key=lambda d: d.name)):\n        %if i:\n          %if i == len(devs) - 1:\n            &#32;and&#32;\n          %else:\n            ,&#32;\n          %endif\n        %endif\n        ${plain_link(dev.name, \"/u/\" + dev.name)}\n      %endfor\n    </div>\n  %endif\n</%def>\n\n<%def name=\"app_type_selector(selection='web')\">\n${utils.radio_type('app_type', \"web\", _(\"web app\"),\n                   _(\"A web based application\"),\n                   selection == \"web\")}\n${utils.radio_type('app_type', \"installed\", _(\"installed app\"),\n                   _(\"An app intended for installation, such as on a mobile phone\"),\n                   selection == \"installed\")}\n${utils.radio_type('app_type', \"script\", _(\"script\"),\n                   _(\"Script for personal use. Will only have access to the developers accounts\"),\n                   selection == \"script\")}\n</%def>\n\n<%def name=\"editable_developer(app, dev)\">\n  <li id=\"app-dev-${app._id}-${dev._id}\">\n    ${dev.name}&#32;\n    %if c.user == dev:\n      <span class=\"gray\">${_(\"(that's you!)\")}</span>&#32;\n    %endif\n    ${ajax_ynbutton(_(\"remove\"), \"removedeveloper\",\n                    hidden_data=dict(client_id=app._id, name=dev.name))}\n  </li>\n</%def>\n\n<%def name=\"developed_app(app, collapsed=True)\">\n  <li id=\"developed-app-${app._id}\"\n      class=\"developed-app rounded ${'collapsed' if collapsed else ''}\">\n    ${icon(app)}\n    <a class=\"edit-app-button ${'' if collapsed else 'collapsed'}\"\n       href=\"javascript:void(0)\">\n       ${_(\"edit\")}\n    </a>\n    <div class=\"app-details\">\n      <h2>\n      %if app.about_url:\n        <a href=\"${app.about_url}\">${app.name}</a>\n      %else:\n        ${app.name}\n      %endif\n      </h2>\n      <h3>\n        %if app.app_type == 'web':\n          ${_(\"web app\")}\n        %elif app.app_type == 'installed':\n          ${_(\"installed app\")}\n        %elif app.app_type == 'script':\n          ${_(\"personal use script\")}\n        %endif\n      </h3>\n      <h3>${app._id}</h3>\n    </div>\n    <div class=\"app-description\">${app.description}</div>\n    %if collapsed:\n      ${developers(app)}\n    %endif\n    <div class=\"edit-app ${'collapsed' if collapsed else ''}\">\n      <a class=\"edit-app-icon-button\" href=\"javascript:void(0)\">\n        change icon\n      </a>\n      <%utils:ajax_upload target=\"/api/setappicon\"\n                          form_id=\"app-icon-upload-${app._id}\">\n        <input type=\"hidden\" name=\"client_id\" value=\"${app._id}\" />\n        ${error_field('TOO_LONG', 'file')}\n        ${error_field('BAD_IMAGE', 'file')}\n      </%utils:ajax_upload>\n      <div class=\"edit-app-form\">\n        <form method=\"post\" action=\"/api/updateapp\" class=\"pretty-form\"\n         id=\"update-app-${app._id}\"\n         onsubmit=\"${\"return post_form(this, 'updateapp', function(x) {return '%s'})\" % jssafe(_(\"updating...\"))}\">\n          <input type=\"hidden\" name=\"uh\" value=\"${c.modhash}\" />\n          <input type=\"hidden\" name=\"client_id\" value=\"${app._id}\" />\n          <input type=\"hidden\" name=\"app_type\" value=\"${app.app_type}\" />\n          <table class=\"preftable\">\n            %if app.is_confidential():\n              <tr>\n                <th>${_(\"secret\")}</th>\n                <td class=\"prefright\">${app.secret}</td>\n              </tr>\n            %endif\n            <tr>\n              <th>${_(\"name\")}</th>\n              <td class=\"prefright\">\n                <input class=\"text\" name=\"name\" value=\"${app.name}\">\n                ${error_field(\"NO_TEXT\", \"name\")}\n              </td>\n            </tr>\n            <tr>\n              <th>${_(\"description\")}</th>\n              <td class=\"prefright\">\n                <textarea name=\"description\">${app.description}</textarea>\n              </td>\n            </tr>\n            <tr>\n              <th>${_(\"about url\")}</th>\n              <td class=\"prefright\">\n                <input class=\"text\" name=\"about_url\" value=\"${app.about_url}\">\n                ${error_field(\"BAD_URL\", \"about_url\")}\n              </td>\n            </tr>\n            <tr>\n              <th>${_(\"redirect uri\")}</th>\n              <td class=\"prefright\">\n                <input class=\"text\" name=\"redirect_uri\"\n                 value=\"${app.redirect_uri if app.redirect_uri else ''}\">\n                ${error_field(\"NO_URL\", \"redirect_uri\")}\n                ${error_field(\"BAD_URL\", \"redirect_uri\")}\n                ${error_field(\"INVALID_SCHEME\", \"redirect_uri\")}\n              </td>\n            </tr>\n          </table>\n          <button type=\"submit\">${_('update app')}</button>\n          <span class=\"status\"></span>\n        </form>\n      </div>\n      <div class=\"edit-app-form pretty-form\">\n        <table class=\"preftable\">\n          <tr>\n            <th>${_(\"developers\")}</th>\n            <td class=\"prefright\">\n              <ul>\n                %for dev in sorted(app._developers, key=lambda d: d.name):\n                  ${editable_developer(app, dev)}\n                %endfor\n              </ul>\n              <br>\n              <form method=\"post\" action=\"/api/adddeveloper\"\n               class=\"pretty-form\" id=\"app-developer-${app._id}\"\n               onsubmit=\"${\"return post_form(this, 'adddeveloper', function(x) {return '%s'})\" % jssafe(_(\"adding...\"))}\">\n                <input type=\"hidden\" name=\"uh\" value=\"${c.modhash}\" />\n                <input type=\"hidden\" name=\"client_id\" value=\"${app._id}\" />\n                ${_('add developer')}: <input class=\"text\" name=\"name\">\n                <br>\n                ${error_field('TOO_MANY_DEVELOPERS', '')}\n                ${error_field('OAUTH2_INVALID_CLIENT', 'client_id')}\n                ${error_field('DEVELOPER_ALREADY_ADDED', 'name')}\n                ${error_field('USER_DOESNT_EXIST', 'name')}\n                ${error_field('NO_USER', 'name')}\n                ${error_field('DEVELOPER_FIRST_PARTY_APP', 'name')}\n                ${error_field('DEVELOPER_PRIVILEGED_ACCOUNT', 'name')}\n                <span class=\"status\"></span>\n              </form>\n            </td>\n          </tr>\n        </table>\n      </div>\n      <div class=\"delete-app-button\">\n        ${ynbutton(_(\"delete app\"),\n                   \"deleted\",\n                   \"deleteapp\",\n                   callback=\"r.apps.deleted\",\n                   hidden_data=dict(client_id=app._id))}\n      </div>\n    </div>\n  </li>\n</%def>\n\n<%def name=\"sr_list(srs)\">\n  %for i, name in enumerate(sorted(srs)):\n    %if i:\n      ,&#32;\n    %endif\n    <a href=\"/r/${name}\">/r/${name}</a>\n  %endfor\n</%def>\n\n<%def name=\"scope_details(scope, compact=False, expiration=None)\">\n  <div class=\"app-permissions\">\n    <ul>\n      %if scope.subreddit_only:\n        %if compact:\n\t  ${_(\"Only in:\")}&#32;\n\t  ${sr_list(scope.subreddits)}\n\t  <br>\n\t%else:\n          <li>\n            ${_(\"Only in the subreddits:\")}&#32;\n            ${sr_list(scope.subreddits)}.\n          </li>\n\t%endif\n      %endif\n      %for name, scope_info in scope.details():\n        <li>\n          %if compact:\n            ${scope_info['name']}\n            <span class=\"app-scope\">${scope_info['description']}</span>\n          %else:\n            ${scope_info['description']}\n          %endif\n        </li>\n      %endfor\n      %if not compact:\n        <li>\n          %if expiration:\n            ${_(\"Expires in:\")}&#32;\n            ${timeuntil(expiration)}\n          %else:\n            ${_(\"Maintain this access indefinitely\"\n                \" (or until manually revoked).\")}\n          %endif\n        </li>\n      %endif\n    </ul>\n    %if compact and expiration:\n      <div class=\"app-permissions-details\">\n        ${_(\"Expires in:\")}&#32;\n        ${timeuntil(expiration)}\n        <br>\n      </div>\n    %endif\n  </div>\n</%def>\n\n<%def name=\"authorized_app(app_data)\">\n  <div id=\"authorized-app-${app_data['client']._id}\" class=\"authorized-app rounded\">\n    ${icon(app_data['client'])}\n    <div class=\"app-details\">\n      <h2>\n      %if app_data['client'].about_url:\n        <a href=\"${app_data['client'].about_url}\">${app_data['client'].name}</a>\n      %else:\n        ${app_data['client'].name}\n      %endif\n      </h2>\n      ## `sorted` should put global permissions first (keyed off `None`)\n      %for sr in sorted(app_data['scopes']):\n        ${scope_details(app_data['scopes'][sr], compact=True)}<br>\n      %endfor\n    </div>\n    <div class=\"app-description\">${app_data['client'].description}</div>\n    ${developers(app_data['client'])}\n    ${ynbutton(_(\"revoke access\"),\n               _(\"revoked\"),\n               \"revokeapp\",\n               callback=\"r.apps.revoked\",\n               hidden_data=dict(client_id=app_data['client']._id),\n               _class=\"revoke-app-button\",\n               access_required=False)}\n  </div>\n</%def>\n\n%if thing.my_apps:\n  <h1>${_(\"authorized applications\")}</h1>\n\n  %for app_data in thing.my_apps.values():\n    ${authorized_app(app_data)}\n  %endfor\n%endif\n\n<div id=\"developed-apps\">\n  <h1 style=\"${'' if thing.developed_apps else 'display:none'}\">\n    ${_(\"developed applications\")}\n  </h1>\n  <ul>\n    %for app in thing.developed_apps:\n      ${developed_app(app)}\n    %endfor\n  </ul>\n</div>\n\n<div class=\"edit-app-form\">\n  <button id=\"create-app-button\" class=\"submit-img\">\n    %if thing.developed_apps:\n      ${_('create another app...')}\n    %else:\n      ${_('are you a developer? create an app...')}\n    %endif\n  </button>\n  <form method=\"post\" action=\"/api/updateapp\" class=\"pretty-form\" id=\"create-app\"\n   onsubmit=\"${\"return post_form(this, 'updateapp', function(x) {return '%s'})\" % jssafe(_(\"creating...\"))}\">\n    <h1>${_(\"create application\")}</h1>\n    <p>${_md(\"Please [read the API usage guidelines](/wiki/api) before creating your application. After creating, you will be required to [register](/wiki/api) for production API use.\")}\n    <input type=\"hidden\" name=\"uh\" value=\"${c.modhash}\" />\n    <table class=\"content preftable\">\n      <tr>\n        <th>${_(\"name\")}</th>\n        <td class=\"prefright\">\n          <input class=\"text\" name=\"name\">\n          ${error_field(\"NO_TEXT\", \"name\")}\n        </td>\n      </tr>\n      ${app_type_selector()}\n      <tr>\n        <th>${_(\"description\")}</th>\n        <td class=\"prefright\">\n          <textarea name=\"description\"></textarea>\n        </td>\n      </tr>\n      <tr>\n        <th>${_(\"about url\")}</th>\n        <td class=\"prefright\">\n          <input class=\"text\" name=\"about_url\">\n          ${error_field(\"BAD_URL\", \"about_url\")}\n        </td>\n      </tr>\n      <tr>\n        <th>${_(\"redirect uri\")}</th>\n        <td class=\"prefright\">\n          <input class=\"text\" name=\"redirect_uri\">\n          ${error_field(\"NO_URL\", \"redirect_uri\")}\n          ${error_field(\"BAD_URL\", \"redirect_uri\")}\n          ${error_field(\"INVALID_SCHEME\", \"redirect_uri\")}\n        </td>\n      </tr>\n    </table>\n    <button type=\"submit\">${_('create app')}</button>\n    <span class=\"status\"></span>\n  </form>\n</div>\n"
  },
  {
    "path": "r2/r2/templates/prefdeactivate.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n   from r2.lib.filters import safemarkdown, jssafe\n%>\n<%namespace file=\"utils.html\" import=\"error_field, text_with_links\"/>\n<%namespace name=\"utils\" file=\"utils.html\"/>\n\n<h1>${_(\"deactivate your reddit account\")}</h1>\n\n<form action=\"javascript:;\" method=\"post\"\n  onsubmit=\"${\"return post_form(this, 'deactivate_user', function(x) {return '%s'})\" % jssafe(_(\"deactivating...\"))}\" id=\"pref-deactivate\">\n\n<div class=\"spacer\">\n  <%utils:round_field title=\"${_('sorry to see you go!')}\">\n    <div class=\"rounded white-field\">\n      ${unsafe(safemarkdown(_(\n        \" * if you're having a problem on reddit, please consider [contacting us](/message/compose?to=%2Fr%2Freddit.com) about it before deactivating your account.\\n\"\n        \" * deactivating your account will not delete the content of posts and comments you've made on reddit. to do so, please delete them individually.\"\n      )))}\n    </div>\n  </%utils:round_field>\n</div>\n\n%if thing.has_paypal_subscription:\n  <div class=\"spacer\">\n    <%utils:round_field title=\"${_('please cancel your gold subscription!')}\">\n      <div class=\"rounded white-field\">\n        <div class=\"gold-subscription\">\n          ${text_with_links(\n            _(\"log in to %%(paypal_link)s to cancel your subscription (%(subscr_id)s).\") % dict(subscr_id=thing.paypal_subscr_id),\n            paypal_link=dict(link_text=\"paypal\", path=thing.paypal_url)\n          )}\n        </div>\n      </div>\n    </%utils:round_field>\n  </div>\n%endif \n\n<div class=\"spacer\">\n  <%utils:round_field title=\"${_('why are you deactivating this account?')}\" description=\"(${_('optional')})\">\n    <textarea name=\"deactivate_message\" id=\"deactivate-message\"></textarea>\n    ${error_field(\"TOO_LONG\", \"deactivate_message\")}\n  </%utils:round_field>\n</div>\n\n<div class=\"spacer\">\n  <%utils:round_field title=\"${_('account credentials')}\" description=\"(${_('for security purposes')})\" css_class=\"credentials\">\n    <label for=\"deactivate-user\">${_(\"username\")}</label>\n    ${error_field(\"NOT_USER\", \"user\")}\n    <input name=\"user\" id=\"deactivate-user\" type=\"text\" />\n    <label for=\"deactivate-password\">${_(\"password\")}</label>\n    ${error_field(\"WRONG_PASSWORD\", \"passwd\")}\n    <input name=\"passwd\" id=\"deactivate-password\" type=\"password\" />\n  </%utils:round_field>\n</div>  \n\n<div class=\"spacer\">\n  <%utils:round_field title=\"${_('confirmation')}\">\n    <div class=\"rounded white-field\">\n      <input name=\"confirm\" id=\"confirm-deactivate\" type=\"checkbox\"/>\n      <label for=\"confirm-deactivate\">${_(\"I understand that deactivated accounts are not recoverable.\")}</label>\n    </div>\n    ${error_field(\"CONFIRM\", \"confirm\")}\n  </%utils:round_field>\n</div>\n\n<div class=\"spacer\">\n  <button type=\"submit\" class=\"btn\">${_(\"deactivate account\")}</button>\n  <span class=\"status\"></span>\n  ${error_field(\"RATELIMIT\", \"vdelay\")}\n</div>\n</form>\n"
  },
  {
    "path": "r2/r2/templates/preffeeds.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n   from r2.models import make_feedurl\n   from r2.lib.template_helpers import get_domain\n   from r2.lib.filters import safemarkdown\n%>\n<div class=\"instructions private-feeds\">\n<h1>${_(\"Private RSS feeds\")}</h1>\n\n${unsafe(safemarkdown(_(\"On this page are links to private RSS feeds so that you can get listings of your content (personalized front page, message panel, saved listing, etc.) without having to deal with cookies or other auth.\")))}\n${unsafe(safemarkdown(_(\"Keep in mind that these urls are intended to be private, so **share at your own risk.**\")))}\n${unsafe(safemarkdown(_(\"All feeds are invalidated if you change your password, however.\")))}\n\n<%def name=\"feedbuttons(path)\">\n<%\n     domain = get_domain(subreddit = False)\n     scheme = \"https\" if feature.is_enabled(\"force_https\") else \"http\"\n %>\n  <a class=\"feedlink rss-link\"\n     href=\"${scheme}://${domain}${make_feedurl(c.user, path, 'rss')}\">\n    RSS\n  </a>\n  <a class=\"feedlink json-link\"\n     href=\"${scheme}://${domain}${make_feedurl(c.user, path, 'json')}\">\n    JSON\n  </a>\n</%def>\n\n<table class=\"preftable\">\n  <tr>\n    <th>private listings</th>\n    <td class=\"prefright\">\n      <%self:feedbuttons path=\"/\"></%self:feedbuttons>\n      ${_(\"your front page\")}\n      <br/>\n      <%self:feedbuttons path=\"/saved\"></%self:feedbuttons>\n      ${_(\"your saved links\")}\n    </td>\n  </tr>\n  <tr>\n    <th>private profile pages</th>\n    <td class=\"prefright\">\n      <%self:feedbuttons path=\"/user/${c.user.name}/upvoted\"></%self:feedbuttons>\n      ${_(\"links you've upvoted\")}\n      <br/>\n      <%self:feedbuttons path=\"/user/${c.user.name}/downvoted\"></%self:feedbuttons>\n      ${_(\"links you've downvoted\")}\n      <br/>\n      <%self:feedbuttons path=\"/user/${c.user.name}/hidden\"></%self:feedbuttons>\n      ${_(\"links you've hidden\")}\n    </td>\n  </tr>\n   <tr>\n    <th>your inbox</th>\n    <td class=\"prefright\">\n      <%self:feedbuttons path=\"/message/inbox/\"></%self:feedbuttons>\n      ${_(\"everything\")}\n      <br/>\n      <%self:feedbuttons path=\"/message/unread/\"></%self:feedbuttons>\n      ${_(\"unread messages\")}\n      <br/>\n      <%self:feedbuttons path=\"/message/messages/\"></%self:feedbuttons>\n      ${_(\"messages only\")}\n      <br/>\n      <%self:feedbuttons path=\"/message/comments/\"></%self:feedbuttons>\n      ${_(\"comment replies only\")}\n      <br/>\n      <%self:feedbuttons path=\"/message/selfreply\"></%self:feedbuttons>\n      ${_(\"self-post replies only\")}\n      <br>\n      <%self:feedbuttons path=\"/message/mentions\"></%self:feedbuttons>\n      ${_(\"mentions of your username only\")}\n    </td>\n   </tr>\n   %if c.user.is_moderator_somewhere:\n   <tr>\n    <th>your moderator inbox</th>\n    <td class=\"prefright\">\n      <%self:feedbuttons path=\"/message/moderator/inbox/\"></%self:feedbuttons>\n      ${_(\"everything\")}\n      <br/>\n      <%self:feedbuttons path=\"/message/moderator/unread/\"></%self:feedbuttons>\n      ${_(\"unread messages\")}\n    </td>\n  </tr>\n  <tr>\n    <th>moderator listings</th>\n    <td class=\"prefright\">\n      <%self:feedbuttons path=\"/r/mod/about/modqueue/\"></%self:feedbuttons>\n      ${_(\"modqueue\")}\n      <br/>\n      <%self:feedbuttons path=\"/r/mod/about/reports/\"></%self:feedbuttons>\n      ${_(\"reports\")}\n      <br/>\n      <%self:feedbuttons path=\"/r/mod/about/spam/\"></%self:feedbuttons>\n      ${_(\"spam\")}\n      <br/>\n      <%self:feedbuttons path=\"/r/mod/about/edited/\"></%self:feedbuttons>\n      ${_(\"edited\")}\n      <br/>\n      <%self:feedbuttons path=\"/r/mod/about/log/\"></%self:feedbuttons>\n      ${_(\"moderation log\")}\n      <br/>\n      <%self:feedbuttons path=\"/r/mod/about/unmoderated/\"></%self:feedbuttons>\n      ${_(\"unmoderated posts\")}\n    </td>\n  </tr>\n  %endif\n</table>\n</div>\n"
  },
  {
    "path": "r2/r2/templates/prefoptions.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n   from r2.config import feature\n   from r2.lib.errors import error_list\n   from r2.lib.menus import CommentSortMenu\n   from r2.lib.template_helpers import add_sr, _wsf, format_html, make_url_protocol_relative\n   from r2.lib.utils import UrlParser\n   import random\n%>\n<%namespace file=\"utils.html\" import=\"md, error_field, language_tool, plain_link\"/>\n\n<%def name=\"checkbox(text, name, disabled = False, disabled_text = '', prefix = 'pref_')\">\n    <input name=\"${name}\" id=\"${name}\" type=\"checkbox\"\n               %if getattr(c.user, prefix + name):\n                 checked=\"checked\"\n               %endif\n               %if disabled:\n                 disabled=\"disabled\"\n               %endif\n               />\n    <label class=\"${'disabled' if disabled else ''}\" for=\"${name}\">\n      ${text}\n    </label>\n    %if disabled and disabled_text:\n      &#32;<span class=\"details\">${disabled_text}</span>\n    %endif\n</%def>\n\n<%def name=\"link_options()\">\n  <select name=\"numsites\">\n        %for x in [10, 25, 50, 100]:\n        <option ${x == c.user.pref_numsites and \"selected='selected'\" or \"\"}>\n          ${x}\n        </option>\n        %endfor\n  </select>\n</%def>\n\n<%def name=\"comment_sort_options()\">\n  <% menu = CommentSortMenu() %>\n  <select name=\"default_comment_sort\">\n        %for sort in menu.visible_options():\n        <option ${'selected=\"selected\"' if sort == c.user.pref_default_comment_sort else \"\"}\n                value=\"${sort}\">\n          ${menu.make_title(sort)}&#32;${_('(recommended)') if sort == menu._default else ''}\n        </option>\n        %endfor\n  </select>\n</%def>\n\n<%def name=\"media_radio(val, label)\">\n  <input id=\"media_${val}\" class=\"nomargin\" \n         type=\"radio\"  value=\"${val}\" name=\"media\"\n         ${\"checked='checked'\" if c.user.pref_media == val else ''} /> \n  <label for=\"media_${val}\">${label}</label>\n  <br/>\n</%def>\n\n<%def name=\"media_preview_radio(val, label)\">\n  <input id=\"media_preview_${val}\" class=\"nomargin\" \n         type=\"radio\"  value=\"${val}\" name=\"media_preview\"\n         ${\"checked='checked'\" if c.user.pref_media_preview == val else ''}>\n  <label for=\"media_preview_${val}\">${label}</label>\n  <br/>\n</%def>\n\n<%def name=\"num_input(s, name)\">\n  <input type=\"text\" class=\"number\" size=\"4\" maxlength=\"4\"\n         name=\"${name}\" value=\"${s if s is not None else ''}\">\n</%def>\n\n<%def name=\"text_input(s, name)\">\n  <input type=\"text\" class=\"text\" size=\"20\" maxlength=\"20\"\n         name=\"${name}\" value=\"${s if s is not None else ''}\">\n</%def>\n\n%if c.user_is_loggedin:\n  %if thing.done:\n    <p class=\"green\">${_(\"your preferences have been updated\")}</p>\n  %elif thing.error_style_override:\n    <p class=\"error\">${_(\"we couldn't save the custom stylesheet preference. please see the error below\")}</p>\n  %elif thing.generic_error:\n    <p class=\"error\">${_(\"your preferences couldn't be saved\")}</p>\n  %endif\n%endif\n\n<%\n   if c.user_is_loggedin:\n       action = \"/post/options\" \n   else:\n       action = \"/post/unlogged_options\" \n   action = add_sr(action, sr_path=False)\n %>\n<form action=\"${action}\" method=\"post\" class=\"pretty-form short-text\">\n  <input type=\"hidden\" name=\"uh\" value=\"${c.modhash}\" />\n<table class=\"content preftable\">\n  <tr>\n    <th>${_(\"interface language\")}</th>\n    <td class=\"prefright\">\n      ${language_tool(allow_blank = False, show_regions = True,\n                      default_lang = c.user.pref_lang)}\n      &#32;<span class=\"details hover\">(*) ${_(\"incomplete\")}\n      &#32;<a href=\"https://www.reddit.com/r/i18n/wiki/getting_started\">${_(\"volunteer to translate\")}</a></span>\n    </td>\n  </tr>\n\n  %if c.user_is_loggedin:\n    %if not c.user.has_subscribed and (c.user.pref_use_global_defaults or c.user.pref_hide_locationbar):\n    <tr>\n      <th>${_(\"location options\")}</th>\n      <td class=\"prefright\">\n        ${checkbox(_(\"use the global default subreddits for my front page\"), \"use_global_defaults\")}\n        <br/>\n        ${checkbox(_(\"hide the location information bar\"), \"hide_locationbar\")}\n      </td>\n    </tr>\n    %endif\n\n  <tr>\n    <th>${_(\"clicking options\")}</th>\n    <td class=\"prefright\">\n      ${checkbox(_(\"open links in a new window\"), \"newwindow\")}\n    </td>\n  </tr>\n  %if thing.feature_autoexpand_media_previews:\n    <tr class=\"preferences-media\">\n  %else:\n    <tr>\n  %endif\n    <th>${_(\"media\")}</th>\n    <td class=\"prefright\">\n      %if thing.feature_autoexpand_media_previews:\n        <h6>${_(\"thumbnails\")}</h6>\n      %endif\n      %if not c.user.pref_compress:\n        ${media_radio(\"on\", _(\"show thumbnails next to links\"))}\n        ${media_radio(\"off\", _(\"don't show thumbnails next to links\"))}\n        ${media_radio(\"subreddit\", _(\"show thumbnails based on that subreddit's media preferences\"))}\n      %else:\n        <p class=\"error\">${_(\"to enable thumbnails, disable compressed link display\")}</p>\n        <input type=\"hidden\" name=\"media\" value=\"${c.user.pref_media}\"/>\n      %endif\n      %if thing.feature_autoexpand_media_previews:\n        <br>\n\n        <h6>${_(\"media previews\")}</h6>\n        ${media_preview_radio(\"on\", _(\"auto-expand media previews\"))}\n        ${media_preview_radio(\"off\", _(\"don't auto-expand media previews on comments pages\"))}\n        ${media_preview_radio(\"subreddit\", _(\"expand media previews based on that subreddit's media preferences\"))}\n        <br>\n\n        <h6>${_(\"NSFW content\")}</h6>\n        ${checkbox(_(\"hide images for NSFW/18+ content \"), \"no_profanity\", disabled = not c.user.pref_over_18, disabled_text = \"(requires over 18)\")}\n        &#32;\n        <span class=\"details\">\n          ${_(\"(Don't show thumbnails or media previews for anything labeled NSFW)\")}\n        </span>\n      %else:\n        ${checkbox(_(\"make safe(r) for work \"), \"no_profanity\", disabled = not c.user.pref_over_18, disabled_text = \"(requires over 18)\")}\n        <span class=\"details\">\n          ${_(\"(Don't show thumbnails next to anything labeled NSFW)\")}\n        </span>\n      %endif\n    </td>\n  </tr>\n  <tr>\n    <th>${_(\"link options\")}</th>\n    <td class=\"prefright\">\n      <p>\n        ${checkbox(_(\"show the spotlight box on the front page\"), \"organic\")}\n        &#32;\n        <span class=\"details\">\n          ${_(\"(it shows new and promoted links, and gives you a say in what's spam and what isn't.)\")}\n        </span>\n      </p>\n      <p>\n        ${checkbox(_(\"show trending subreddits on the front page\"), \"show_trending\")}\n        &#32;\n        <span class=\"details\">\n          ${_(\"(a list of popular and notable subreddits to check out)\")}\n        </span>\n      </p>\n\n      <p>${checkbox(_(\"show me links I've recently viewed\"), \"clickgadget\")}</p>\n      <p>${checkbox(_(\"compress the link display\"), \"compress\")}</p>\n      <p>${checkbox(_(\"show additional details in the domain text when available\"), \"domain_details\")}\n        &#32;\n        <span class=\"details\">\n          ${_(\"(such as the source subreddit or the content author's url/name)\")}\n        </span>\n      </p>\n      <p>${checkbox(_(\"don't show me submissions after I've upvoted them\"), \"hide_ups\")}\n         &#32;\n         <span class=\"details\">${_(\"(except my own)\")}</span>\n      </p>\n      <p>${checkbox(_(\"don't show me submissions after I've downvoted them\"), \"hide_downs\")}\n         &#32;\n         <span class=\"details\">${_(\"(except my own)\")}</span>\n      </p>\n      <p>\n        ${_wsf(\"display %(num)s links at once\", num=unsafe(capture(link_options)))}\n      </p>\n      <%\n         input = unsafe(capture(num_input, c.user.pref_min_link_score,\n         'min_link_score'))\n         %>\n      <p>\n      ${_wsf(\"don't show me submissions with a score less than %(num)s\", num=input)}\n      &#32;<span class=\"details\">${_(\"(leave blank to show all submissions)\")}</span>\n      </p>\n    </td>\n  </tr>\n  <tr>\n    <th>${_(\"comment options\")}</th>\n    <td class=\"prefright\">\n    <p>\n      ${_wsf(\"sort comments by %(sort)s\", sort=unsafe(capture(comment_sort_options)))}\n    </p>\n    <p>\n    ${checkbox(_(u\"ignore suggested sorts\"), \"ignore_suggested_sort\")}\n    &#32;<span class=\"details\">${_(\"(suggested sorts may be set by community moderators for specific threads or subreddits, like Q&As)\")}</span>\n    </p>\n    <p>\n    ${checkbox(_(u\"show a dagger (†) on comments voted controversial\"), \"highlight_controversial\")}\n    &#32;<span class=\"details\">${_(\"(a controversial comment is one that's been both upvoted and downvoted significantly)\")}</span>\n    </p>\n    <% \n       input = unsafe(capture(num_input, c.user.pref_min_comment_score,\n                              'min_comment_score'))\n       %>\n    <p>\n      ${_wsf(\"don't show me comments with a score less than %(num)s\", num=input)}\n      &#32;<span class=\"details\">${_(\"(leave blank to show all comments)\")}</span>\n    </p>\n    <p>\n      <% \n       input = unsafe(capture(num_input, c.user.pref_num_comments,\n                              'num_comments'))\n       %>\n    <% s = c.user.pref_num_comments %>\n    ${_wsf(\"display %(num)s comments by default\", num=input)}\n    &#32;\n    <span class=\"details\">\n      (1 - ${g.max_comments});\n      &#32;\n      ${_(\"the smaller the number, the faster your comments pages will load\")}\n    </span>\n    </td>\n  </tr>\n  <tr>\n    <th>${_(\"messaging options\")}</th>\n    <td class=\"prefright\">\n      ${checkbox(_(\"show message conversations in the inbox\"), \\\n         \"threaded_messages\")}\n      &#32;<span class=\"details\">\n        ${_(\"(only applies when you go to the 'messages' panel)\")}\n      </span>\n      <br/>\n      %if c.user.pref_threaded_messages:\n        ${checkbox(_(\"collapse messages after I've read them\"), \\\n           \"collapse_read_messages\")}\n        &#32;<span class=\"details\">\n          ${_(\"(otherwise, you'll have to collapse them yourself)\")}\n        </span>\n        <br/>\n      %endif\n      ${checkbox(_(\"mark messages as read when I open my inbox\"), \\\n         \"mark_messages_read\")}\n      &#32;<span class=\"details\">\n        ${_(\"(otherwise, they will be marked as read when you click them)\")}\n      </span>\n      <br>\n      ${checkbox(_(\"notify me when people say my username\"), \"monitor_mentions\")}\n      %if feature.is_enabled('orangereds_as_emails'):\n        <br>\n        ${checkbox(_(\"send messages as emails\"), \"email_messages\", disabled=(not c.user.email_verified), disabled_text=_(\"requires a verified email\"))}\n      %endif\n      <br>\n      ${checkbox(_(\"enable threaded modmail display\"), \"threaded_modmail\")}\n    </td>\n  </tr>\n  <tr>\n    <th>${_(\"display options\")}</th>\n    <td class=\"prefright\">\n      ${checkbox(_(\"allow subreddits to show me custom themes\"), \"show_stylesheets\")}\n      %if feature.is_enabled('stylesheets_everywhere'):\n        &#32;<span class=\"details reddit-gold\">\n          ${_(\"(you can also choose which subreddit themes to disable on an individual level)\")}\n        </span>\n      %endif\n      <br/>\n      ${checkbox(_(\"show user flair\"), \"show_flair\")}\n      <br/>\n      ${checkbox(_(\"show link flair\"), \"show_link_flair\")}\n      %if c.user.pref_show_promote is not None:\n        <br/>\n        ${checkbox(_(\"show self-serve advertising tab on front page\"), \n          \"show_promote\")}\n      %endif\n      %if feature.is_enabled('legacy_search_pref'):\n        <br>\n        ${checkbox(_(\"show legacy search page\"), \"legacy_search\")}\n      %endif\n    </td>\n  </tr>\n  <tr>\n    <th>${_(\"content options\")}</th>\n    <td class=\"prefright\">\n      ${checkbox(_(\"I am over eighteen years old and willing to view adult content\"), \"over_18\")}\n      &#32;<span class=\"details\">(${_(\"required to view some subreddits\")})</span>\n      <br/>\n        ${checkbox(_(\"label posts that are not safe for work (NSFW)\"), \"label_nsfw\", disabled = c.user.pref_no_profanity, disabled_text = \"(requires not 'safer for work' mode)\")}\n      <br/>\n        ${checkbox(_(\"enable private RSS feeds\"), \"private_feeds\")} \n       &#32;<span class=\"details\">\n        ${_(\"(available from the 'RSS feed' tab in prefs)\")}</span>\n    </td>\n  <tr>\n    <th>${_(\"privacy options\")}</th>\n    <td class=\"prefright\">\n      ${checkbox(_(\"make my votes public\"), \"public_votes\")}\n      &#32;\n      <span class=\"details\">\n        <%\n           link1 = format_html('&#32;<a href=\"/user/%s/upvoted\">/user/%s/upvoted</a>&#32;', c.user.name, c.user.name)\n           link2 = format_html('&#32;<a href=\"/user/%s/downvoted\">/user/%s/downvoted</a>', c.user.name, c.user.name)\n           %>\n        (${_wsf(\"let everyone see %(link1)s and %(link2)s\", link1=link1, link2=link2)})\n      </span>\n      <br/>\n      ${checkbox(_(\"allow my data to be used for research purposes\"), \"research\")}\n      &#32;\n      <span class=\"details\">\n       (\n         <a href=\"https://www.reddit.com/r/redditdev/comments/dtg4j/want_to_help_reddit_build_a_recommender_a_public/\">\n           ${_(\"details\")}\n         </a>\n       )\n      </span>\n      <br />\n      ${checkbox(_(\"don't allow search engines to index my user profile\"), \"hide_from_robots\")}\n      &#32;\n      <span class=\"details\">\n        (\n        <a href=\"https://www.reddit.com/wiki/noindex\">${_(\"details\")}</a>\n        )\n      </span>\n    </td>\n  </tr>\n  % if feature.is_enabled('beta_opt_in'):\n  <tr>\n    <th>${_(\"beta options\")}</th>\n    <td class=\"prefright\">\n      ${checkbox(_(\"I would like to beta test features for reddit\"), \"beta\")}\n      &#32;\n      <span class=\"details\">\n        <%\n          beta_sr_link = format_html('&#32;<a href=\"/r/%s\">/r/%s</a>&#32;', g.beta_sr, g.beta_sr)\n        %>\n        (${_wsf(\"by enabling you'll be subscribed to %(beta_sr_link)s automatically.\", beta_sr_link=beta_sr_link)}\n          &#32;\n          <a href=\"/r/${g.beta_sr}/wiki\">${_(\"details on the /r/{beta_sr} wiki\").format(beta_sr=g.beta_sr)}</a>)\n      </span>\n    </td>\n  </tr>\n  %endif\n  %endif\n\n  %if c.user.gold:\n  <tr class=\"gold-accent\">\n    <th>${_(\"gold options\")}</th>\n    <td class=\"prefright\">\n        ${checkbox(_(\"hide ads\"), \"hide_ads\")}\n        <br>\n        ${checkbox(_(\"remember what links I've visited\"), \"store_visits\")}\n        &#32;<span class=\"details\">(${_(\"we'll remember and mark what links you've already read, even between computers\")})</span>\n        <br>\n        ${checkbox(_(\"highlight new comments\"), \"highlight_new_comments\")}\n        &#32;<span class=\"details\">\n          (${_(\"we'll remember your visits for 48 hours and show you which comments you haven't seen yet\")})\n        </span>\n        <br>\n        ${checkbox(_(\"show gold expiration\"), \"show_gold_expiration\")}\n        &#32;<span class=\"details\">\n          (${_(\"show how much gold you have remaining on your userpage\")})\n        </span>\n        <br>\n        <% creddit_link = unsafe('&#32;<a href=\"/creddits\">creddit</a>&#32;') %>\n        ${checkbox(_wsf(\"use a %(creddit_link)s to automatically renew my gold if it expires\", creddit_link=creddit_link), \"creddit_autorenew\")}\n    </td>\n  </tr>\n  %elif c.user.is_moderator_somewhere:\n  <tr class=\"gold-accent\">\n    <th>${_(\"gold options\")}</th>\n    <td class=\"prefright\">\n      ${checkbox(_(\"highlight new comments\"), \"highlight_new_comments\")}\n      <% gold_link = unsafe('&#32;<a href=\"/gold/about\">' + _(\"reddit gold\") + '</a>') %>\n      &#32;<span class=\"details\">\n        (${_wsf(\"since you don't have %(gold_link)s, this will only apply in subreddits you moderate\", gold_link=gold_link)})\n      </span>\n    </td>\n  </tr>\n  %endif\n\n  %if feature.is_enabled('stylesheets_everywhere'):\n    <tr class=\"gold-accent\">\n      <th>${_(\"reddit themes\")}</th>\n      <td class=\"prefright\">\n        <div class=\"reddit-themes-description\">\n          <span>\n            ${_(\"reddit themes change the appearance of reddit.  They are used anywhere another custom theme is not present.\")}\n          </span>\n          <br />\n          <span class=\"details\">\n            (${_(\"Note: For security reasons this page will not be changed by themes\")})\n          </span>\n        </div>\n\n        ${checkbox(_(\"use reddit theme\"), \"enable_default_themes\")}\n        &#32;<span class=\"details\">(${_(\"hover to preview\")})</span>\n\n        <div class=\"container reddit-themes\">\n          % if thing.themes:\n            % for theme in thing.themes:\n              ${theme_item(\n                thumbnail_url=theme.thumbnail_url,\n                preview_url=theme.preview_url,\n                id=theme.id,\n                tagline=theme.tagline,\n                checked=theme.checked,\n              )}\n            % endfor\n          % endif\n\n          <div class=\"theme select-custom-theme\">\n            <label><input type=\"radio\" name=\"theme_selector\" id=\"other_theme_selector\" value=\"\" ${\"checked\" if thing.use_other_theme else \"\"}>use theme from /r/</label>\n              <input type=\"text\" class=\"text\" size=\"21\" maxlength=\"21\" name=\"other_theme\" placeholder=\"subreddit\" ${\"value=\" + c.user.pref_default_theme_sr if thing.use_other_theme and c.user.pref_default_theme_sr else \"\"}>\n              <span class=\"details\">\n                  (${_(\"warning: use non-featured themes at your own risk\")})\n              </span>\n          </div>\n        </div>\n      </div>\n      %if thing.error_style_override:\n        <p class=\"error\">${_(error_list[thing.error_style_override])}</p>\n      %endif\n      </td>\n    </tr>\n  %endif\n\n  <tr>\n    <td>\n      <input type=\"submit\" class=\"btn\" value=\"${_('save options')}\"/>\n    </td>\n  </tr>\n</table>\n\n</form>\n\n <%def name=\"theme_item(thumbnail_url, preview_url, id, tagline, checked)\">\n  <div class=\"theme ${'selected' if checked else ''}\" id=\"${id}\">\n    <div class=\"theme-container\">\n      <div class=\"theme-thumbnail\">\n        <img src=\"${make_url_protocol_relative(thumbnail_url)}\"/>\n        <div class=\"theme-preview\">\n          <img src=\"${make_url_protocol_relative(preview_url)}\"/>\n        </div>\n      </div>\n      <label><input type=\"radio\" name=\"theme_selector\" value=\"${id}\" ${\"checked\" if checked else \"\"}>\n        ${md(tagline)}</label>\n    </div>\n  </div>\n</%def>\n"
  },
  {
    "path": "r2/r2/templates/prefsecurity.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n  from r2.lib import js\n  from r2.lib.strings import strings\n%>\n\n<%namespace file=\"utils.html\" import=\"error_field, _md\"/>\n<%namespace name=\"utils\" file=\"utils.html\"/>\n\n\n% if c.user_is_loggedin and c.user.name in g.admins:\n<h1>${_(\"two-factor authentication\")}</h1>\n\n    % if c.user.otp_secret:\n    <form action=\"/post/disable_otp\" method=\"post\" onsubmit=\"return post_form(this, 'disable_otp')\" id=\"pref-otp\">\n\n    ${_md(\"two-factor authentication is currently **enabled**. fill out the form below if you would like to disable it.\", wrap=True)}\n\n    <%utils:round_field title=\"${_('password')}\" description=\"${_('(required)')}\">\n      <input type=\"password\" name=\"password\" />\n      ${error_field(\"WRONG_PASSWORD\", \"password\")}\n    </%utils:round_field>\n\n    <%utils:round_field title=\"${_('one-time password')}\" description=\"${_('(required)')}\">\n      <input type=\"number\" name=\"otp\" maxlength=\"6\" />\n      ${error_field(\"WRONG_PASSWORD\", \"otp\")}\n      ${error_field(\"NO_OTP_SECRET\", \"otp\")}\n      ${error_field(\"RATELIMIT\", \"otp\")}\n    </%utils:round_field>\n\n    <input type=\"submit\" value=\"${_(\"disable\")}\">\n    </form>\n    % else:\n    <form action=\"/post/generate_otp_secret\" method=\"post\" onsubmit=\"return post_form(this, 'generate_otp_secret')\" id=\"pref-otp\">\n\n    ${_md(\"enter your current password below to start the activation process for two-factor authentication.\", wrap=True)}\n\n    <%utils:round_field title=\"${_('password')}\" description=\"${_('(required)')}\">\n      <input type=\"password\" name=\"password\" />\n      ${error_field(\"WRONG_PASSWORD\", \"password\")}\n    </%utils:round_field>\n\n    <input type=\"submit\" value=\"${_(\"activate\")}\">\n\n    </form>\n\n    <form action=\"/post/enable_otp\" method=\"post\" onsubmit=\"return post_form(this, 'enable_otp')\" id=\"pref-otp-qr\">\n\n    <div id=\"otp-secret-info\">\n        ${_md(\"below is your two-factor authentication secret. you can scan the QR code with Google Authenticator or enter the key below manually. you WILL NOT have another chance to see this secret.\")}\n    </div>\n\n    <%utils:round_field title=\"${_('one-time password')}\" description=\"${_('(required)')}\">\n      <input type=\"number\" name=\"otp\" maxlength=\"6\" />\n      ${error_field(\"WRONG_PASSWORD\", \"otp\")}\n      ${error_field(\"EXPIRED\", \"otp\")}\n    </%utils:round_field>\n\n    <input type=\"submit\" value=\"${_(\"enable\")}\">\n\n    </form>\n    % endif\n% endif\n\n${unsafe(js.use(\"qrcode\"))}\n"
  },
  {
    "path": "r2/r2/templates/prefupdate.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%namespace file=\"utils.html\" import=\"error_field\"/>\n<%namespace name=\"utils\" file=\"utils.html\"/>\n\n\n%if thing.email:\n<form action=\"/post/update_email\" method=\"post\" \n      onsubmit=\"return post_form(this, 'update_email')\" id=\"pref-update-email\">\n\n  <h1>\n  %if thing.verify:\n    ${_(\"verify your email\")}\n  %else:\n    ${_(\"update your email\")}\n  %endif\n  </h1>\n\n  <div class=\"spacer\">\n    <%utils:round_field title=\"${_('current password')}\" description=\"${_('(required)')}\">\n      <input type=\"password\" name=\"curpass\">\n      ${error_field(\"WRONG_PASSWORD\", \"curpass\")}\n    </%utils:round_field>\n  </div>\n\n  <%\n     if not c.user.email:\n       description = _(\"not set\")\n       v_link = None\n     elif c.user.email_verified:\n       description = _(\"verified\")\n       v_link = None\n     elif c.user.email_verified is False:\n       description = _(\"verification pending\")\n       v_link = _(\"click to resend\")\n     else:\n       description = _(\"unverified\")\n       v_link = _(\"click to verify\")\n\n     if v_link and not thing.verify:\n       description = \"(%s;&#32;<a href='/verify'>%s</a>)\" % (description, v_link)\n       description = unsafe(description)\n     else:\n       description = \"(%s)\" % description\n  %>\n\n  <div class=\"spacer\">\n    <%utils:round_field title=\"${_('email')}\" description=\"${description}\">\n      <input type=\"text\" name=\"email\" value=\"${getattr(c.user, 'email', '')}\">\n      ${error_field(\"BAD_EMAILS\", \"email\")}\n    </%utils:round_field>\n  </div>\n\n  %if thing.verify and not c.user.email_verified:\n    <div class=\"spacer\">\n      <div class=\"roundfield-actions\">\n        <input type=\"hidden\" name=\"verify\" value=\"1\"/>\n        <input type=\"hidden\" name=\"dest\" value=\"${thing.dest}\">\n        <button type=\"submit\" class=\"btn\">${_('send verification email')}</button>\n        %if thing.subscribe:\n           <a id=\"subscribe\"\n              href=\"http://reddit.us2.list-manage1.com/subscribe?u=8dd663d8559f5530305877239&amp;id=42fb5066f2&amp\"\n              class=\"c-pull-right\">\n             subscribe to our advertising newsletter\n           </a>\n         </div>\n         <script>\n           $('#subscribe').on('click', function(e) {\n             e.preventDefault();\n\n             var $subscribe = $(this);\n             var $email = $subscribe.parents('form').find('[name=email]');\n             var href = $subscribe.attr('href');\n\n             window.open(href + '&MERGE0=' + encodeURIComponent($email.val()), '_blank');\n           });\n         </script>\n        %endif\n      </div>\n  %else:\n    <button type=\"submit\" class=\"btn\">${_('save')}</button>\n  %endif\n  <span class=\"status error\"></span>\n</form>\n%endif\n\n%if thing.email and thing.password:\n<br>\n%endif\n\n%if thing.password:\n<form action=\"/post/update_password\" method=\"post\" \n      onsubmit=\"return post_form(this, 'update_password')\" id=\"pref-update-password\">\n\n  <h1>${_(\"update your password\")}</h1>\n\n  <div class=\"spacer\">\n    <%utils:round_field title=\"${_('current password')}\" description=\"${_('(required)')}\">\n      <input type=\"password\" name=\"curpass\">\n      ${error_field(\"WRONG_PASSWORD\", \"curpass\")}\n    </%utils:round_field>\n  </div>\n\n  <div class=\"spacer\">\n    <%utils:round_field title=\"${_('new password')}\">\n      <input type=\"password\" name=\"newpass\">\n      ${error_field(\"BAD_PASSWORD\", \"newpass\")}\n    </%utils:round_field>\n  </div>\n\n  <div class=\"spacer\">\n    <%utils:round_field title=\"${_('verify password')}\">\n      <input type=\"password\" name=\"verpass\">\n      ${error_field(\"BAD_PASSWORD_MATCH\", \"verpass\")}\n    </%utils:round_field>\n  </div>\n  <div class=\"spacer\">\n    <%utils:round_field title=''>\n      <input type=\"checkbox\" name=\"invalidate_oauth\" id=\"invalidate_oauth\">\n      <label for=\"invalidate_oauth\">\n        log me out everywhere&nbsp;\n        <sup class=\"help help-hoverable\">?</sup>\n      </label>\n      <div class=\"hover-bubble help-bubble anchor-top\">\n        <p>\n          Changing your password logs you out of all browsers on your computer(s).\n          Checking this box also logs you out of all&nbsp; \n          <a href=\"/prefs/apps\">apps you have authorized</a>\n          .\n        </p>\n      </div>\n    </%utils:round_field>\n  </div>\n  <button type=\"submit\" class=\"btn\">${_('save')}</button>\n  <span class=\"status error\"></span>\n</form>\n%endif\n"
  },
  {
    "path": "r2/r2/templates/printable.compact",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%def name=\"delete_report_buttons()\">\n  <%\n      is_author = (c.user_is_loggedin and thing.author and\n      c.user.name == thing.author.name)\n   %>\n  %if is_author:\n    <div class=\"thing_suboption_popup_container hidden\">\n      <div class=\"thing_suboption_popup\">\n       <a href=\"#\" \n          onclick=\"return change_state(this, 'del', hide_thing, true)\">\n        yes\n       </a>\n       <a href=\"#\" class=\"thing_option_close\">no</a>\n      </div>\n    </div>\n    <a href=\"#\" class=\"thing_suboption_link\">Delete</a>\n  %endif\n  <div class=\"thing_suboption_popup_container hidden\">\n    <div class=\"thing_suboption_popup\">\n      <a href=\"#\" \n        onclick=\"return change_state(this, 'report', hide_thing, true)\">\n        yes\n      </a>\n      <a href=\"#\" class=\"thing_option_close\">no</a>\n    </div>\n  </div>\n  <a href=\"#\" class=\"thing_suboption_link\">Report</a>\n</%def>\n\nempty\n"
  },
  {
    "path": "r2/r2/templates/printable.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n   from r2.lib.filters import jssafe\n   from r2.lib.template_helpers import add_sr, static\n   from r2.lib.strings import strings\n   from r2.lib.pages.things import BanButtons\n   from r2.lib.utils import long_datetime\n%>\n\n<%namespace file=\"utils.html\" import=\"classes, plain_link\" />\n\n${self.RenderPrintable()}\n\n<%def name=\"admintagline()\">\n  %if thing.show_spam:\n      %if thing.banned_at:\n        <li title=\"${_(\"removed at %(time)s\") % dict(time=long_datetime(thing.banned_at))}\">\n      %else:\n        <li>\n      %endif\n      <b>[\n        %if c.user_is_admin:\n          ${\"auto\" if thing.autobanned else \"\"}${strings.banned} \n          ${(\"by %s\" % thing.banner) if thing.banner else \"\"}\n        %elif thing.moderator_banned and thing.banner:\n          ${strings.banned_by % thing.banner}\n        %else:\n          ${strings.banned}\n        %endif\n      ]</li></b>\n  %endif\n</%def>\n\n<%def name=\"approval_checkmark()\">\n  %if getattr(thing, \"approval_checkmark\", None):\n      <img class=\"approval-checkmark\" title=\"${thing.approval_checkmark}\"\n           src=\"${static('green-check.png')}\"\n           onclick=\"alert('${thing.approval_checkmark}\\n\\n${jssafe(_(\"(no need to click for this info; just hover over the checkmark next time)\"))}')\"\n         >\n  %endif\n</%def>\n\n<%def name=\"gildings()\">\n  % if thing.gilded_message:\n  <a href=\"${thing.subreddit_path}gilded\">\n    <span class=\"gilded-icon\" title=\"${thing.gilded_message}\" data-count=\"${thing.gildings}\">\n      % if thing.gildings > 1:\n        x${thing.gildings}\n      % endif\n    </span>\n  </a>\n  % endif\n</%def>\n\n<%def name=\"thing_css_class(what)\">\n  <%\n    cssclass = \"thing\"\n    if (not getattr(what, 'deleted', False) or\n            getattr(what, 'can_ban', False) or\n           (getattr(what, 'promoted', False) and c.user_is_sponsor)):\n      cssclass += \" id-\" + what._fullname\n  %>\n  ${cssclass}\n</%def>\n\n<%def name=\"thing_css_rowclass(what)\">\n  <%\n    rowstyle = getattr(what, \"rowstyle\", \"\")\n\n    if what.show_spam:\n      rowclass = \"spam\"\n      if what.show_spam == \"author\":\n        rowclass += \" banned-user\"\n    elif what.show_reports:\n      rowclass = \"reported\"\n    else:\n      rowclass = \"\"\n\n    if getattr(what, \"deleted\", False):\n      rowclass += \" deleted\"\n    if hasattr(what, \"saved\") and what.saved:\n      rowclass += \" saved\"\n    if hasattr(what, \"hidden\") and what.hidden:\n      rowclass += \" hidden\"\n    if hasattr(what, \"gildings\") and what.gildings > 0:\n      rowclass += \" gilded\"\n    if hasattr(what, \"user_gilded\") and what.user_gilded:\n      rowclass += \" user-gilded\"\n    if not getattr(what, \"archived\", False) and getattr(what, \"locked\", False):\n      rowclass += \" locked\"\n    if getattr(what, \"use_sticky_style\", False):\n      rowclass += \" stickied\"\n    if getattr(what, \"is_controversial\", False):\n      rowclass += \" controversial\"\n  %>\n  ${rowstyle}&#32;${rowclass}\n</%def>\n\n<%def name=\"thing_data_attributes(what)\">\n    % if not getattr(what, 'deleted', False):\n      <% cls = thing.lookups[0].__class__.__name__.lower() %>\n      data-fullname=\"${what._fullname}\"\n      data-type=\"${unsafe(cls)}\"\n    % endif\n\n    % if hasattr(what, 'campaign'):\n      data-cid=\"${what.campaign}\"\n    % endif\n\n    % if hasattr(what, 'imp_pixel'):\n      data-imp-pixel=\"${what.imp_pixel}\"\n    % endif\n\n    % if hasattr(what, 'adserver_imp_pixel'):\n      data-adserver-imp-pixel=\"${what.adserver_imp_pixel}\"\n    % endif\n\n    % if hasattr(what, 'adserver_click_url'):\n      data-adserver-click-url=\"${what.adserver_click_url}\"\n    % endif\n\n    % if hasattr(what, 'third_party_tracking_url'):\n      data-third-party-tracking-url=\"${what.third_party_tracking_url}\"\n    % endif\n\n    % if hasattr(what, 'third_party_tracking_url_2'):\n      data-third-party-tracking-two-url=\"${what.third_party_tracking_url_2}\"\n    % endif\n</%def>\n\n<%def name=\"RenderPrintable()\">\n<% cls = thing.lookups[0].__class__.__name__.lower() %>\n<%\n   if hasattr(thing, 'render_css_class'):\n      cls = thing.render_css_class\n   elif hasattr(thing, 'render_class'):\n      cls = thing.render_class.__name__.lower()\n   else:\n      cls = thing.lookups[0].__class__.__name__.lower()\n\n   cls += ' ' + getattr(thing, 'extra_css_class', '')\n\n   if getattr(thing, \"is_self\", False):\n      selflink = \"self\"\n   else:\n      selflink = \"\"\n %>\n<div class=\"${self.thing_css_class(thing)} ${self.thing_css_rowclass(thing)} ${unsafe(cls)} ${selflink}\"\n    %if not getattr(thing, 'deleted', False) or getattr(thing, 'can_ban', False):\n      id=\"thing_${thing._fullname}\"\n    %endif\n    ${thing.display} onclick=\"click_thing(this)\"\n    ${unsafe(self.thing_data_attributes(thing))}>\n  <p class=\"parent\">\n    ${self.ParentDiv()}\n  </p>\n  ${self.numcol()}\n  <%\n     like_cls = \"unvoted\"\n     if getattr(thing, \"likes\", None):\n         like_cls = \"likes\"\n     elif getattr(thing, \"likes\", None) is False:\n         like_cls = \"dislikes\"\n   %>\n  ${self.midcol(cls = like_cls)}\n  <div class=\"entry ${like_cls}\">\n    ${self.entry()}\n  </div>\n  ${self.Child()}\n  <div class=\"clearleft\"></div>\n</div>\n<div class=\"clearleft\"></div>\n</%def>\n\n<%def name=\"buttons(ban=True)\">\n  ${BanButtons(thing)}\n</%def>\n\n<%def name=\"ParentDiv()\">\n</%def>\n\n<%def name=\"numcol()\">\n</%def>\n\n<%def name=\"entry()\">\n</%def>\n\n<%def name=\"Child(display=True)\">\n<div class=\"child\" ${(not display and \"style='display:none'\" or \"\")}>\n  %if thing.childlisting:\n    ${thing.childlisting}\n  %endif\n</div>\n</%def>\n\n<%def name=\"arrow(this, dir, mod)\">\n<% \n   arrow_type = \"up\" if dir > 0 else \"down\"\n   arrow_class = arrow_type + (\"mod\" if mod else \"\")\n   login_class = \"login-required\" if not g.read_only_mode else \"\"\n   archived_class = \"archived\" if not getattr(thing, 'votable', True) else \"\"\n   access_class = \"access-required\"\n%>\n  <div ${classes(\"arrow\", arrow_class, login_class, archived_class, access_class)}\n    data-event-action=\"${arrow_type}vote\"\n    %if not g.read_only_mode:\n       role=\"button\"\n       % if dir > 0:\n       aria-label=\"${_(\"upvote\")}\"\n       % else:\n       aria-label=\"${_(\"downvote\")}\"\n       % endif\n       tabindex=\"0\"\n    %endif\n    >\n  </div>\n</%def>\n\n<%def name=\"score(thing, tag='span')\" >\n  %for cls, score in zip([\"dislikes\", \"unvoted\", \"likes\"], thing.display_score):\n    <${tag} class=\"score ${cls}\">\n      ${score}\n    </${tag}>\n  %endfor\n</%def>\n\n\n<%def name=\"midcol(display=True, cls = '')\">\n  <div class=\"midcol ${cls}\" \n       ${not display and \"style='display:none'\" or ''}>\n    ${self.arrow(thing, 1, thing.likes)}\n    ${self.arrow(thing, 0, thing.likes == False)}\n  </div>\n</%def>\n"
  },
  {
    "path": "r2/r2/templates/printable.htmllite",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%namespace file=\"utils.html\" import=\"optionalstyle\"/>\n<%namespace file=\"printable.html\" import=\"arrow\"/>\n\n<%\n    like_cls = \"unvoted\"\n    if getattr(thing, \"likes\", None):\n        like_cls = \"likes\"\n    elif getattr(thing, \"likes\", None) is False:\n        like_cls = \"dislikes\"\n    thing.like_cls = like_cls\n %>\n${self.parent()}\n${self.entry()}\n${self.Child()}\n\n\n<%def name=\"parent()\">\n</%def>\n\n<%def name=\"Child()\">\n  ${getattr(thing, \"child\", '')}\n</%def>\n\n<%def name=\"entry()\">\n</%def>\n\n\n<%def name=\"static_arrows(thing)\">\n  <%\n   from r2.lib.template_helpers import get_domain\n   domain = get_domain(subreddit=False)\n   permalink = \"%s://%s%s\" % (g.default_scheme, domain, thing.permalink)\n   if thing.likes == False:\n      arrow = \"%s://%s/static/widget_arrows_down.gif\"\n   elif thing.likes:\n      arrow = \"%s://%s/static/widget_arrows_up.gif\"\n   else:\n      arrow = \"%s://%s/static/widget_arrows.gif\"\n   arrow = arrow % (g.default_scheme, domain)\n   %>\n  <a href=\"${permalink}\" class=\"reddit-voting-arrows\" target=\"_blank\"\n     ${optionalstyle(\"float:left; display:block;\")}>\n    <img src=\"${arrow}\" alt=\"vote\" \n         ${optionalstyle(\"border:none;margin-top:3px;\")}/>\n  </a>\n</%def>\n\n<%def name=\"iframe_arrows(thing)\">\n  <% \n   from r2.lib.template_helpers import get_domain\n   %>\n  <div class=\"reddit-voting-arrows\" \n     ${optionalstyle(\"float:left; margin: 1px;\")}>\n    <script type=\"text/javascript\">\n      var reddit_bordercolor=\"FFFFFF\";\n    </script>\n    <%\n       from r2.models import FakeSubreddit\n       url = (\"%s://%s/static/button/button4.html?t=4&id=%s&sr=%s\" %\n               (g.default_scheme, get_domain(subreddit=False), thing._fullname,\n               c.site.name if not isinstance(c.site, FakeSubreddit) else \"\"))\n       if c.bgcolor:\n          url += \"&bgcolor=%s&bordercolor=%s\" % (c.bgcolor, c.bgcolor)\n       else:\n          url += \"&bgcolor=FFFFFF&bordercolor=FFFFFF\"\n     %>\n    <iframe src=\"${url}\" height=\"55\" width=\"51\" scrolling=\"no\" frameborder=\"0\"\n            ${optionalstyle(\"margin:0px;\")}>\n    </iframe>\n  </div>\n</%def>\n\n<%def name=\"real_arrows(thing)\">\n  <div class=\"midcol ${thing.like_cls}\" ${optionalstyle(\"width: 15px\")}>\n    ${arrow(thing, 1, thing.likes)}\n    ${arrow(thing, 0, thing.likes == False)}\n </div>\n</%def>\n\n\n<%def name=\"arrows(thing)\">\n  %if getattr(thing, 'embed_voting_style',None) == 'votable':\n    ${self.real_arrows(thing)}\n  %elif request.GET.get(\"expanded\") or getattr(thing, 'embed_voting_style', None) == 'expanded':\n    ${self.iframe_arrows(thing)}\n  %else:\n    ${self.static_arrows(thing)}\n  %endif\n</%def>\n"
  },
  {
    "path": "r2/r2/templates/printable.iframe",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2014\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n${self.parent()}\n${self.entry()}\n${self.Child()}\n\n\n<%def name=\"parent()\">\n</%def>\n\n<%def name=\"entry()\">\n</%def>\n\n<%def name=\"Child()\">\n  ${getattr(thing, \"child\", \"\")}\n</%def>\n"
  },
  {
    "path": "r2/r2/templates/printable.mobile",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n    from r2.lib.pages import WrappedUser\n %>\n\n<li>\n  ${self.parent()}\n  ${self.entry()}\n  ${self.Child()}\n</li>\n\n\n    \n<%def name=\"parent()\">\n</%def>\n\n<%def name=\"Child()\">\n  %if hasattr(thing, \"child\"):\n    <div id=\"child_${thing._fullname}\" class=\"child\">\n     ${thing.child}\n    </div>\n  %endif\n</%def>\n\n<%def name=\"entry()\">\n</%def>\n\n<%def name=\"author(friend = False, gray = False)\" buffered=\"True\">\n  ${WrappedUser(thing.author, thing.attribs, thing, gray = collapse)}\n</%def>\n\n<%def name=\"score(this, likes=None, inline=True, label = True, _id = True)\">\n</%def>\n\n<%def name=\"delete_or_report_buttons(delete=True, report=True)\">\n</%def>\n\n\n<%def name=\"buttons()\">\n</%def>\n"
  },
  {
    "path": "r2/r2/templates/printablebuttons.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%namespace file=\"utils.html\" import=\"plain_link, pretty_button, data, error_field\" />\n\n<%!\n   from r2.config import feature\n   from r2.lib.filters import conditional_websafe, jssafe\n   from r2.lib.strings import strings\n   from r2.models import Link\n   from r2.models.promo import PROMOTE_STATUS\n %>\n\n<%def name=\"banbuttons()\">\n  %if thing.show_delete:\n    <li>\n      ${ynbutton(_(\"delete\"), _(\"deleted\"), \"del\", \"hide_thing\", access_required=False, event_action=\"delete\")}\n    </li>\n  %endif\n  %if thing.can_ban:\n    %if not getattr(thing.thing, \"use_big_modbuttons\", False):\n      %if not thing.show_spam:\n        <li>\n           ${ynbutton(_(\"spam\"), _(\"spammed\"), \"remove\", event_action='spam')}\n        </li>\n        <li>\n           ${ynbutton(_(\"remove\"), _(\"removed\"), \"remove\", hidden_data=dict(spam=False),\n             event_action='remove')}\n        </li>\n      %endif\n\n      %if thing.show_approve:\n        <li>\n           ${self.state_button(\"approve\", _(\"approve\"),\n              \"return change_state(this, 'approve');\", _(\"approved\"),\n              event_action=\"approve\")}\n        </li>\n      %endif\n    %endif\n  %endif\n\n  %if thing.show_report:\n    <li class=\"report-button\">\n      %if feature.is_enabled(\"new_report_dialog\"):\n        <a href=\"javascript:void(0)\" class=\"reportbtn access-required\"\n           data-event-action=\"report\">\n      %else:\n        <a href=\"javascript:void(0)\" class=\"action-thing reportbtn access-required\"\n           data-event-action=\"report\">\n      %endif\n        ${_(\"report\")}\n      </a>\n    </li>\n  %endif\n  %if thing.show_lock:\n    <li>${ynbutton(_(\"lock\"), _(\"locked\"), \"lock\", event_action='lock')}</li>\n  %endif\n  %if thing.show_unlock:\n    <li>${ynbutton(_(\"unlock\"), _(\"unlocked\"), \"unlock\", event_action='unlock')}</li>\n  %endif\n  %if thing.show_marknsfw:\n    <li>${ynbutton(_(\"nsfw\"), _(\"marked\"), \"marknsfw\", event_action='marknsfw')}</li>\n  %endif\n  %if thing.show_unmarknsfw:\n    <li>${ynbutton(_(\"un-nsfw\"), _(\"unmarked\"), \"unmarknsfw\", event_action='unmarknsfw')}</li>\n  %endif\n  %if not getattr(thing, 'promoted', None) and thing.show_flair:\n    <li>\n      <a class=\"flairselectbtn access-required\" href=\"javascript://void(0)\"\n         data-event-action=\"editflair\" data-event-detail=\"set\">${_('flair')}</a>\n      <div class=\"flairselector drop-choices\"></div>\n    </li>\n  %endif\n  %if thing.show_rescrape:\n    <li>${ynbutton(_(\"retry thumb\"), _(\"check back in a few minutes\"), \"rescrape\",\n            event_action=\"retry_thumbnail\")}</li>\n  %endif\n</%def>\n\n<%def name=\"distinguish_setter(name, value=None, event_action='distinguish')\">\n  <a class=\"access-required\"\n     href=\"javascript:void(0)\"\n     data-event-action=\"${event_action}\"\n     onclick=\"return set_distinguish(this, '${jssafe(value or name)}')\">${_(name)}</a>\n</%def>\n\n<%def name=\"distinguish_noop(text)\">\n  <a class=\"access-required\"\n     href=\"javascript:void(0)\"\n     onclick=\"return toggle_distinguish_span(this)\">${text}</a>\n</%def>\n\n<%def name=\"distinguish_label(distinguish_clicked)\">\n  <%\n    text = _(\"distinguish\") if distinguish_clicked else _(\"undistinguish\")\n  %>\n  <a class=\"access-required\"\n    href=\"javascript:void(0)\"\n    onclick=\"return toggle_distinguish_span(this)\"\n    data-event-action=\"distinguish\">${text}</a>\n</%def>\n\n<%def name=\"distinguish_help()\">\n  <a class=\"nonbutton\" href=\"/wiki/moderation#wiki_distinguishing\">\n    ${_(\"help\")}\n  </a>\n</%def>\n\n<%def name=\"distinguish_options()\">\n  ${_(\"distinguish this?\")}\n\n  ## Note: can_ban is logically equivalent to can_distinguish at this time.\n  %if thing.can_ban:\n    &#32;\n    ${distinguish_setter('yes')}\n    &#32;/\n\n    %if thing.show_sticky_comment:\n      &#32;\n      ${distinguish_setter(_('yes & sticky'), 'yes_sticky', event_action=\"sticky_distinguish\")}\n      &#32;/\n    %endif\n  %endif\n  \n  &#32;\n  ${distinguish_setter('no', event_action=\"undistinguish\")}\n  &#32;\n\n  %if c.user.employee:\n    /&#32;\n    ${distinguish_setter('admin', event_action=\"admin_distinguish\")}\n    &#32;\n  %endif\n  \n  %if c.user_special_distinguish:\n    /&#32;\n    ${distinguish_setter(c.user_special_distinguish['name'], 'special', event_action=\"special_distinguish\")}\n    &#32;\n  %endif\n\n  /&#32;\n  ${distinguish_help()}\n  &#32;\n</%def>\n\n<%def name=\"undistinguish_options()\">\n  ${_(\"undistinguish this?\")}\n\n  &#32;\n  ${distinguish_setter('yes', value=\"no\", event_action=\"undistinguish\")}\n  &#32;/\n\n  &#32;\n  ${distinguish_noop(_(\"no\"))}\n  &#32;\n\n  /&#32;\n  ${distinguish_help()}\n  &#32;\n</%def>\n\n\n<%def name=\"distinguish()\">\n  %if thing.show_distinguish:\n  <li class=\"toggle\">\n    <form method=\"post\" action=\"/post/distinguish\">\n      <input type=\"hidden\" value=\"${c.profilepage}\" name=\"profilepage\">\n      %if thing.show_sticky_comment:\n      <input type=\"hidden\" value=\"false\" name=\"sticky\">\n      %endif\n      %if thing.distinguished:\n        <input type=\"hidden\" value=\"${_('undistinguishing...')}\" name=\"executed\"/>\n      %else:\n        <input type=\"hidden\" value=\"${_('distinguishing...')}\" name=\"executed\"/>\n      %endif\n\n      ${distinguish_label(not thing.distinguished)}\n\n      <span class=\"option error\">\n        %if thing.distinguished:\n          ${undistinguish_options()}\n        %else:\n          ${distinguish_options()}\n        %endif\n      </span>\n    </form>\n  </li>\n  %endif\n</%def>\n\n<%def name=\"give_gold()\">\n  % if thing.show_givegold:\n    <li class=\"give-gold-button\">\n    <a href=\"/gold?goldtype=gift&months=1&thing=${thing.thing._fullname}\"\n        title=\"${_(\"give reddit gold in appreciation of this post.\")}\"\n        class=\"give-gold login-required access-required\"\n        data-event-action=\"gild\"\n        >${_(\"give gold\")}</a>\n    </li>\n  % endif\n</%def>\n\n<%def name=\"ignore_reports_toggle(thing)\">\n  <%\n    label = _(\"ignore reports\")\n    if thing.ignore_reports and thing.reported > 0:\n      label += \" ({0})\".format(thing.reported)\n    event_action = \"unignorereports\" if thing.ignore_reports else \"ignorereports\"\n  %>\n  ${pretty_button(label, \"big_mod_toggle\", \"'ignore_reports', 'unignore_reports'\",\n  \"neutral\" + (\" pressed\" if thing.ignore_reports else \"\"),\n  event_action=event_action)}\n</%def>\n\n<%def name=\"big_modbuttons(thing)\">\n  <span class=\"big-mod-buttons\">\n    %if not thing._deleted:\n      <span role=\"radiogroup\">\n        %if not getattr(thing, \"moderator_banned\", None) or getattr(thing, \"autobanned\", False):\n          ${pretty_button(_(\"spam\"), \"big_mod_action\", -2, \"negative\", event_action=\"spam\")}\n          ${pretty_button(_(\"remove\"), \"big_mod_action\", -1, \"neutral\", event_action=\"remove\")}\n        %endif\n\n        %if getattr(thing, \"approval_checkmark\", None):\n          ${pretty_button(_(\"reapprove\"), \"big_mod_action\",  1, \"positive\", event_action=\"approve\")}\n        %else:\n          ${pretty_button(_(\"approve\"), \"big_mod_action\",  1, \"positive\", event_action=\"approve\")}\n        %endif\n      </span>\n    %endif\n\n    %if not thing._spam:\n      ${ignore_reports_toggle(thing)}\n    %endif\n\n    &#32;\n    <span class=\"status-msg spammed\">\n      ${_(\"spammed\")}\n    </span>\n    <span class=\"status-msg removed\">\n      ${_(\"removed\")}\n    </span>\n    <span class=\"status-msg approved\">\n      ${_(\"approved\")}\n    </span>\n  </span>\n</%def>\n\n<%def name=\"show_admin_context()\">\n  % if thing.show_admin_context:\n    <li>\n      <form onsubmit=\"return post_form(this, 'admin/fullcontext');\" method=\"post\">\n        <input type=\"hidden\" name=\"thing\" id=\"thing\" value=\"${thing.thing._fullname}\" />\n        <input type=\"hidden\" name=\"author\" id=\"author\" value=\"${thing.thing.author_slow._fullname}\" />\n        <a href=\"#\" onclick=\"$(this).closest('form').submit(); return false\">full context</a>\n       </form>\n    </li>\n  % endif\n</%def>\n\n<%def name=\"reports_button()\">\n<li \n    %if thing.mod_reports or thing.user_reports:\n      class=\"rounded reported-stamp stamp has-reasons access-required\"\n      title=\"${_('click to show report reasons')}\"\n    %else:\n      class=\"rounded reported-stamp stamp access-required\"\n    %endif\n    data-event-action=\"viewreports\"\n    >\n  ${strings.reports % thing.thing.reported}\n</li>\n</%def>\n\n<%def name=\"report_reasons()\">\n<ul class=\"report-reasons rounded\">\n  %if thing.mod_reports:\n    <li class=\"report-reason-title\">${_(\"moderator reports:\")}</li>\n    %for reason, user in thing.mod_reports:\n      <li class=\"report-reason mod-report\" title=\"${reason}\">${user}:\n      %if reason:\n        ${reason}\n      %else:\n        ${_(\"<no reason>\")}\n      %endif\n      </li>\n    %endfor\n  %endif\n\n  %if thing.user_reports:\n    <li class=\"report-reason-title\">${_(\"user reports:\")}</li>\n    %for reason, count in thing.user_reports:\n      <li class=\"report-reason\" title=\"${reason}\">${count}:\n      %if reason:\n        ${reason}\n      %else:\n        ${_(\"<no reason>\")}\n      %endif\n      </li>\n    %endfor\n  %endif\n</ul>\n</%def>\n\n<%def name=\"linkbuttons()\">\n  %if thing.show_comments:\n    <li class=\"first\">\n    ${self.bylink_button(\n        thing.comment_label,\n        thing.permalink,\n        sr_path=(thing.promoted is None),\n        a_class=unsafe(\"bylink %s\" % thing.commentcls),\n        event_action=\"comments\")}\n    </li>\n  %endif\n  %if thing.editable and not thing.promoted:\n    <li>\n      ${self.simple_button(_(\"edit\"), \"edit_usertext\", css_class=\"edit-usertext\")}\n    </li>\n  %endif\n  % if thing.show_share:\n    <li class=\"share\">\n      <a class=\"post-sharing-button\" href=\"javascript: void 0;\">${_(\"share\")}</a>\n    </li>\n  % endif\n  %if thing.is_loggedin and not thing.deleted:\n    %if thing.saved:\n      <li class=\"link-unsave-button save-button\"><a href=\"#\">${_(\"unsave\")}</a></li>\n    %else:\n      <li class=\"link-save-button save-button\"><a href=\"#\">${_(\"save\")}</a></li>\n    %endif\n    <li>\n    %if thing.hidden:\n      ${self.state_button(\"unhide\", _(\"unhide\"), \\\n        \"change_state(this, 'unhide', hide_thing);\", _(\"unhidden\"),\n        access_required=False, event_action=\"unhide\")}\n    %else:\n      ${self.state_button(\"hide\", _(\"hide\"), \\\n         \"change_state(this, 'hide', hide_thing);\", _(\"hidden\"),\n         access_required=False, event_action=\"hide\")}\n    %endif\n    </li>\n  %endif\n\n  ${self.distinguish()}\n  ${self.give_gold()}\n  ${self.banbuttons()}\n  %if thing.promoted is not None:\n    %if thing.user_is_sponsor or thing.is_author:\n       <li>\n         ${plain_link(_(\"edit\"), thing.promo_url, _sr_path = False)}\n       </li>\n    %endif\n    %if c.user_is_sponsor:\n      %if thing.show_approval:\n        %if thing.promote_status not in (PROMOTE_STATUS.rejected, PROMOTE_STATUS.finished):\n          <li>\n            <form action=\"/post/reject\" class=\"rejection-form\"\n                 style=\"display:none\"\n                 %if getattr(thing, \"hide_after_seen\", False):\n                   data-hide-after-seen=\"true\"\n                 %endif\n                 method=\"post\" onsubmit=\"reject post_form(this, 'unpromote')\">\n              <br/>\n              <input type=\"hidden\" name=\"executed\" value=\"rejected\" />\n              <label>Reason:</label><br/>\n              <textarea name=\"reason\" value=\"\" ></textarea>\n              <br/>\n              <a href=\"javascript: void 0;\"\n                onclick=\"change_state(this, 'unpromote', complete_reject_promo)\">\n               submit\n              </a>/\n            </form>\n            ${toggle_button(\"reject_promo\", \\\n                           _(\"reject\"), _(\"cancel\"), \\\n                           \"reject_promo\", \"cancel_reject_promo\")}\n          </li>\n        %endif\n        %if thing.promote_status in (PROMOTE_STATUS.unpaid, PROMOTE_STATUS.unseen, PROMOTE_STATUS.rejected, PROMOTE_STATUS.edited_live):\n          <li>\n            ${ynbutton(_(\"accept\"), \\\n                       _(\"accepted\"), \"promote\", \\\n                       callback=\"hide_thing\" if getattr(thing, \"hide_after_seen\", False) else \"null\")}\n          </li>\n        %endif\n      %endif\n      %if thing.is_awaiting_fraud_review:\n        <li class=\"fraud-button\">\n          <a href=\"javascript:void(0)\" class=\"action-thing\" data-action-form=\"#fraud-action-form\" title=\"${thing.payment_flagged_reason}\">\n            ${_(\"fraud\")}\n          </a>\n        </li>\n      %endif\n    %endif\n    %if thing.user_is_sponsor or thing.is_author:\n      <li>\n        ${plain_link(_(\"traffic\"), thing.traffic_url, _sr_path = False)}\n      </li>\n    %endif\n  %endif\n\n  %if thing.show_reports and not thing.show_spam:\n    ${reports_button()}\n  %endif\n\n  %if getattr(thing.thing, \"use_big_modbuttons\", False):\n     ${big_modbuttons(thing.thing)}\n  %elif thing.ignore_reports and thing.can_ban:\n    ${ignore_reports_toggle(thing.thing)}\n  %endif\n\n  %if thing.show_reports and not thing.show_spam and (thing.mod_reports or thing.user_reports):\n    ${report_reasons()}\n  %endif\n\n</%def>\n\n<%def name=\"commentbuttons()\">\n  %if not thing.deleted:\n    <li class=\"first\">\n      ${self.bylink_button(_(\"permalink\"), thing.permalink, event_action=\"permalink\")}\n    </li>\n    %if thing.embed_button:\n      <li>\n        ${thing.embed_button.render()}\n      </li>\n    %endif\n\n    %if thing.saved:\n      <li class=\"comment-unsave-button save-button\">\n        <a href=\"javascript:void(0)\">${_(\"unsave\")}</a>\n      </li>\n    %else:\n      <li class=\"comment-save-button save-button\">\n        <a href=\"javascript:void(0)\">${_(\"save\")}</a>\n      </li>\n    %endif\n\n    %if c.profilepage:\n      <li>\n        ${self.bylink_button(_(\"context\"), thing.permalink + \"?context=3\", event_action=\"context\")}\n      </li>\n      <li class=\"first\">\n      ${self.bylink_button(\n          _(\"full comments\") + \" (%d)\" % thing.full_comment_count, \n          thing.full_comment_path,\n          sr_path=True,\n          a_class=\"bylink may-blank\",\n          event_action=\"full_comments\")}\n      </li>\n    %endif\n\n    %if thing.parent_permalink and not thing.profilepage:\n      <li>\n        ${self.bylink_button(_(\"parent\"), thing.parent_permalink, event_action=\"parent\")}\n      </li>\n    %endif\n\n    %if thing.is_author:\n      %if thing.editable:\n        <li>\n          ${self.simple_button(_(\"edit\"), \"edit_usertext\", css_class=\"edit-usertext\")}\n        </li>\n      %endif\n      <li>\n        %if thing.thing.inbox_replies_enabled:\n          ${ynbutton(_(\"disable inbox replies\"), _(\"inbox replies disabled\"), \"sendreplies\",\n            hidden_data=dict(id=thing.thing._fullname, state=False),\n            access_required=False, event_action=\"disable_inbox_replies\")}\n        %else:\n          ${ynbutton(_(\"enable inbox replies\"), _(\"inbox replies enabled\"), \"sendreplies\",\n            hidden_data=dict(id=thing.thing._fullname, state=True),\n            access_required=False, event_action=\"enable_inbox_replies\")}\n        %endif\n      </li>\n    %endif\n\n    ${self.banbuttons()}\n    ${self.distinguish()}\n    ${self.give_gold()}\n    ${self.show_admin_context()}\n\n    %if not getattr(thing, \"suppress_reply_buttons\", False) and (thing.can_reply or thing.locked):\n      <li class=\"reply-button\">\n      %if thing.can_reply:\n        ${self.simple_button(_(\"reply {verb}\"), \"reply\", css_class=\"access-required\", event_action=\"comment\")}\n      %else:\n        ${self.simple_button(_(\"reply {verb}\"), \"void\", css_class=\"locked-error access-required\", event_action=\"comment\")}\n      %endif\n      </li>\n    %endif\n\n    %if thing.show_reports and not thing.show_spam:\n      ${reports_button()}\n    %endif\n\n    %if getattr(thing.thing, \"use_big_modbuttons\", False):\n       ${big_modbuttons(thing.thing)}\n    %elif thing.ignore_reports and thing.can_ban:\n      ${ignore_reports_toggle(thing.thing)}\n    %endif\n\n    %if thing.show_reports and not thing.show_spam and (thing.mod_reports or thing.user_reports):\n      ${report_reasons()}\n    %endif\n\n  %endif\n</%def>\n\n\n<%def name=\"messagebuttons()\"> \n  %if thing.was_comment:\n    <li>\n      ${self.bylink_button(_(\"context\"), thing.permalink + \"?context=3\", event_action=\"context\")}\n    </li>\n    <li>\n      ${self.bylink_button(\n          _(\"full comments\") + \" (%d)\" % thing.full_comment_count,\n          thing.full_comment_path,\n          a_class=\"bylink may-blank full-comments\",\n          event_action=\"full_comments\")}\n    </li>\n  %else:\n    <li class=\"first\">\n      ${self.bylink_button(_(\"permalink\"), thing.permalink, sr_path=False, event_action=\"permalink\")}\n    </li>\n    ${self.distinguish()}\n  %endif\n  %if thing.user_is_recipient:\n    ## only allow message deleting on recipient side now\n    %if not (thing.was_comment or thing.thing.del_on_recipient) and thing.thing.to_id == c.user._id:\n      <li>\n        ${ynbutton(_(\"delete\"), _(\"deleted\"), \"del_msg\", \"hide_thing\", access_required=False, event_action=\"delete_message\")}\n      </li>\n    %endif\n    %if thing.can_block:\n      ${self.banbuttons()}\n        %if thing.thing.author_id != c.user._id and thing.thing.author_id not in c.user.enemies:\n          %if getattr(thing.thing, \"from_sr\", False):\n            %if not (thing.thing.user_is_moderator or c.user_is_admin):\n              <li>\n                %if getattr(thing.thing, \"sr_blocked\", False):\n                  ${ynbutton(_(\"unblock subreddit\"), _(\"unblocked\"), \"unblock_subreddit\",\n                      access_required=False, event_action=\"unblock_subreddit\")}\n                %else:\n                  ${ynbutton(_(\"block subreddit\"), _(\"blocked\"), \"block\", \"hide_thing\",\n                      access_required=False, event_action=\"block_subreddit\")}\n                %endif\n              </li>\n            %endif\n          %else:\n            <li>\n              ${ynbutton(_(\"block user\"), _(\"blocked\"), \"block\", \"hide_thing\",\n                  access_required=False, event_action=\"block_user\")}\n            </li>\n          %endif\n          %if thing.can_mute:\n            <li>\n              %if getattr(thing.thing, \"sr_muted\", False):\n                ${ynbutton(_(\"unmute user\"), _(\"unmuted\"), \"unmute_message_author\",\n                    event_action=\"unmute_user\")}\n              %else:\n                ${ynbutton(_(\"mute user\"), _(\"muted\"), \"mute_message_author\",\n                    event_action=\"mute_user\")}\n              %endif\n            </li>\n          %endif\n        %endif\n      %endif\n    <li class=\"unread\">\n     ${self.state_button(\"unread\", _(\"mark unread\"), \\\n        \"return change_state(this, 'unread_message', unread_thing, true);\", \\\n         _(\"unread\"), event_action=\"mark_unread\")}\n    </li>\n  %endif\n  %if thing.can_reply:\n    <li>\n       <%\n        css_class = \"\" if thing.is_admin_message else \"access-required\"\n       %>\n       ${self.simple_button(_(\"reply {verb}\"), \"reply\", css_class, event_action='reply')}\n    </li>\n  %endif\n</%def>\n\n##------------\n<%def name=\"state_button(name, title, onclick, executed,\n                         clicked = False,\n                         a_class = '',\n                         fmt = None,\n                         fmt_param = '',\n                         hidden_data = {},\n                         access_required = True,\n                         event_action=None)\">\n  <%def name=\"_link()\" buffered=\"True\">\n    <%\n      access_class = 'access-required' if access_required else ''\n    %>\n    <a href=\"javascript:void(0)\"\n       class=\"${a_class or ''} ${access_class}\"\n       %if event_action:\n         data-event-action=\"${event_action}\"\n       %endif\n       onclick=\"${onclick}\">${title}</a>\n  </%def>\n  <%\n     link = _link()\n     if fmt:\n         link = conditional_websafe(fmt) % {fmt_param: link}\n         ## preserve spaces before and after < & > for space compression\n         link = link.replace(\" <\", \"&#32;<\").replace(\"> \", \">&#32;\")\n   %>   \n\n  %if clicked:\n    ${executed}\n  %else:\n    <form action=\"/post/${name}\" method=\"post\" \n          class=\"state-button ${name}-button\">\n        <input type=\"hidden\" name=\"executed\" value=\"${executed}\" />\n        %for key, value in hidden_data.iteritems():\n          <input type=\"hidden\" name=\"${key}\" value=\"${value}\" />\n        %endfor\n        <span>\n          ${unsafe(link)}\n        </span>\n    </form>\n  %endif\n</%def>\n\n\n<%def name=\"ajax_ynbutton(title, op, question=None, _class='', hidden_data={})\">\n  <form method=\"post\" action=\"/api/${op}\"\n        class=\"toggle ajax-yn-button ${op}-button ${_class}\">\n    <input type=\"hidden\" name=\"_op\" value=\"${op}\" />\n    %for k, v in hidden_data.iteritems():\n      <input type=\"hidden\" name=\"${k}\" value=\"${v}\" />\n    %endfor\n    <span class=\"option main active\">\n      <a href=\"javascript:void(0)\" class=\"togglebutton\">${title}</a>\n    </span>\n    <span class=\"option error\">\n      ${_(\"are you sure?\") if question is None else question}\n      &#32;\n      <a href=\"javascript:void(0)\" class=\"yes\">${_(\"yes\")}</a>\n      &#32;/&#32;\n      <a href=\"javascript:void(0)\" class=\"no\">${_(\"no\")}</a>\n    </span>\n  </form>\n</%def>\n\n<%def name=\"ynbutton(title, executed, op, callback = 'null', \n                     question = None,\n                     post_callback = 'null',\n                     format = '%(link)s',\n                     format_arg = 'link',\n                     hidden_data = {},\n                     access_required = True,\n                     event_target = None,\n                     event_action = None,\n                     event_detail = None,\n                     _class = '')\">\n  <%\n     if question is None:\n         question = _(\"are you sure?\")\n     access_class = 'access-required' if access_required else ''\n\n     data_attrs = {}\n     if event_target:\n       data_attrs['type'] = event_target\n     if event_action:\n       data_attrs['event-action'] = event_action\n     if event_detail:\n       data_attrs['event-detail'] = event_detail\n\n     link = ('<a href=\"#\" class=\"togglebutton ' + access_class + '\" onclick=\"return toggle(this)\" '\n             + capture(data, **data_attrs) + '>'\n             + conditional_websafe(title) + '</a>')\n     link = conditional_websafe(format) % {format_arg : link}\n     link = unsafe(link.replace(\" <\", \"&#32;<\").replace(\"> \", \">&#32;\"))\n   %>\n  <form class=\"toggle ${op}-button ${_class}\" action=\"#\" method=\"get\">\n    <input type=\"hidden\" name=\"executed\" value=\"${executed}\"/>\n    %for k, v in hidden_data.iteritems():\n      <input type=\"hidden\" name=\"${k}\" value=\"${v}\"/>\n    %endfor\n    <span class=\"option main active\">\n      ${link}\n    </span>\n    <span class=\"option error\">\n      ${question}\n      &#32;<a href=\"javascript:void(0)\" class=\"yes\"\n         onclick='change_state(this, \"${op}\", ${callback}, undefined, ${post_callback})'>\n        ${_(\"yes\")}\n      </a>&#32;/&#32;\n      <a href=\"javascript:void(0)\" class=\"no\"\n         onclick=\"return toggle(this)\">${_(\"no\")}</a>\n    </span>\n  </form>\n</%def>\n\n<%def name=\"simple_button(title, nameFunc, css_class='', event_action=None)\">\n <a class=\"${css_class}\" href=\"javascript:void(0)\" \n    %if event_action:\n      data-event-action=\"${event_action}\"\n    %endif\n    onclick=\"return ${nameFunc}(this)\">${title}</a>\n</%def>\n\n<%def name=\"toggle_button(class_name, title, alt_title, \n                    callback, cancelback, \n                    css_class = '', alt_css_class = '',\n                    reverse = False,\n                    login_required = False,\n                    style = '', data_attrs=None)\">\n<%\n   if reverse:\n       callback, cancelback = cancelback, callback\n       title, alt_title = alt_title, title\n       css_class, alt_css_class = alt_css_class, css_class\n   extra_class = \"login-required\" if login_required else \"\"\n %>\n<span class=\"${class_name} toggle\" style=\"${style}\" ${data(**data_attrs or dict())}>\n <a class=\"option active ${css_class} ${extra_class}\" href=\"#\" tabindex=\"100\"\n    %if not login_required or c.user_is_loggedin:\n      onclick=\"return toggle(this, ${callback}, ${cancelback})\"\n    %endif\n    >\n   %if title:\n     ${title}\n   %else:\n     &nbsp;\n   %endif\n </a>\n <a class=\"option ${alt_css_class}\" href=\"#\">\n   %if alt_title:\n     ${alt_title}\n   %else:\n     &nbsp;\n   %endif\n </a>\n</span>\n</%def>\n\n<%def name=\"bylink_button(title, link, sr_path=True, a_class='bylink', event_action=None)\">\n  <%\n    data_attrs = {}\n\n    if event_action:\n      data_attrs[\"event-action\"] = event_action\n\n    inbound_tracking_url = Link.tracking_link(link, thing, element_name=event_action)\n    if inbound_tracking_url != link:\n      data_attrs[\"href-url\"] = link\n      data_attrs[\"inbound-url\"] = inbound_tracking_url\n  %>\n  ${plain_link(\n      title,\n      link,\n      _class=a_class,\n      rel=\"nofollow\",\n      _sr_path=sr_path,\n      data=data_attrs,\n  )}\n</%def>\n"
  },
  {
    "path": "r2/r2/templates/privateinterstitial.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n  from r2.lib.template_helpers import static \n%>\n\n<%inherit file=\"interstitial.html\"/>\n\n<%def name=\"interstitial_image_attrs()\">\n  src=\"${static('interstitial-image-private.png')}\"\n  alt=\"${_('private')}\"\n</%def>\n\n<%def name=\"interstitial_title()\">\n  ${_(\"You must be invited to visit this community\")}\n</%def>\n\n<%def name=\"interstitial_message()\">\n  <p>\n    ${_(\"The moderators of this subreddit have set it to private. You must be a moderator or approved submitter to visit.\")}\n  </p>\n</%def>\n\n<%def name=\"interstitial_buttons()\">\n  <div class=\"buttons\">\n    <a class=\"c-btn c-btn-primary login-required\" href=\"/message/compose/?to=/r/${thing.sr_name}\">\n      ${_(\"message the moderators\")}\n    </a>\n  </div>\n</%def>\n"
  },
  {
    "path": "r2/r2/templates/profilebar.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%! \n   from r2.config import feature\n   from r2.lib.filters import unsafe, safemarkdown\n   from r2.lib.template_helpers import static, format_number, display_comment_karma, display_link_karma\n %>\n<%namespace file=\"utils.html\" import=\"submit_form, plain_link, thing_timestamp\"/>\n<%namespace file=\"printablebuttons.html\" import=\"toggle_button\"/>\n\n<div class=\"titlebox\">\n  <h1>${thing.user.name}\n    %if thing.user.employee:\n      <span class=\"user-distinction\">\n        [\n        <span class=\"admin\" title=\"reddit admin\">A</span>\n        ]\n      </span>\n    %endif\n  </h1>\n\n  %if c.user_is_loggedin and not thing.viewing_self:\n    <div>\n    ${toggle_button(\"fancy-toggle-button\", _(\"+ friends\"), _(\"- friends\"),\n         \"friend('%s', '%s', 'friend')\" % (thing.user.name, c.user._fullname),\n         \"unfriend('%s', '%s', 'friend')\" % (thing.user.name, c.user._fullname),\n         css_class = \"add\", alt_css_class = \"remove\",\n         reverse = thing.is_friend, login_required=True)}\n    </div>\n  %endif\n\n  <span class=\"karma\">${format_number(display_link_karma(thing.user.link_karma))}</span>\n  &#32;\n  ${_(\"post karma\")}\n\n  <br/>\n  <span class=\"karma comment-karma\">${format_number(display_comment_karma(thing.user.comment_karma))}</span>\n  &#32;\n  ${_(\"comment karma\")}\n\n  %if thing.show_private_info:\n    <table id=\"per-sr-karma\"${\" class='more-karmas'\" if not c.user_is_admin else \"\"}>\n     <thead>\n        <tr>\n          <th id=\"sr-karma-header\">subreddit</th>\n          <th>post</th>\n          <th>comment</th>\n        </tr>\n     </thead>\n     <tbody>\n     %for i, (sr_name, (link_karma, comment_karma)) in enumerate(thing.all_karmas.iteritems()):\n       %if c.user_is_admin and i >= 5:\n         <tr class=\"more-karmas\">\n       %else:\n         <tr>\n       %endif\n\n        % if sr_name == \"ancient history\":\n          <th class=\"helpful\" title=\"${_('really obscure karma from before it was cool to track per-subreddit')}\"><span>${_(sr_name)}</span></th>\n        % else:\n          <th>${sr_name}</th>\n        % endif\n\n          <td>${display_link_karma(link_karma)}</td>\n          <td>${display_comment_karma(comment_karma)}</td>\n        </tr>\n     %endfor\n     </tbody>\n    </table>\n    % if not c.user_is_admin or len(thing.all_karmas) > 5:\n    <div class=\"karma-breakdown\">\n      <a href=\"javascript:void(0)\"\n         onclick=\"$('.more-karmas').show();$(this).hide();return false\">\n         show karma breakdown by subreddit\n      </a>\n    </div>\n    % endif\n  %endif\n\n  %if thing.user.gold:\n    %if thing.show_private_info or thing.user.pref_show_snoovatar:\n      <div class=\"gold-accent snoovatar-link\">\n        <a href=\"/user/${thing.user.name}/snoo\">\n          %if thing.viewing_self:\n            ${_(\"View/edit my snoovatar\")}\n          %else:\n            ${_(\"%(username)s's snoovatar\") % dict(username=thing.user.name)}\n          %endif\n        </a>\n      </div>\n    %endif\n  %endif\n\n  %if thing.show_users_gold_expiration or thing.show_private_gold_info:\n    <div class=\"rounded gold-accent gold-expiration-info\">\n      %if hasattr(thing, \"gold_remaining\"):\n        <div class=\"gold-remaining\" title=\"${thing.user.gold_expiration.strftime('%Y-%m-%d')}\">\n          <span class=\"karma\">\n            ${thing.gold_remaining}\n          </span>\n          <br>\n          ${_(\"of reddit gold remaining\")}\n          <br>\n          <a href=\"/gold/about\">${_(\"view gold features/benefits\")}</a>\n        </div>\n        %if thing.show_private_info:\n          %if hasattr(thing, \"paypal_subscr_id\"):\n             <div>\n              <a href=\"${thing.paypal_url}\">\n                ${_(\"Recurring Paypal subscription\")}\n              </a>\n              &#32;\n              ${thing.paypal_subscr_id}\n            </div>\n          %endif\n\n          %if hasattr(thing, \"stripe_customer_id\"):\n             <div>\n              <a href=\"/gold/subscription\">\n                ${_(\"manage recurring subscription\")}\n              </a>\n            </div>\n          %endif\n        %endif\n      %endif\n      %if hasattr(thing, \"gold_creddit_message\"):\n        <div class=\"gold-creddits-remaining\">\n          ${plain_link(thing.gold_creddit_message, \"/gold?goldtype=gift\")}\n        </div>\n      %endif\n      %if hasattr(thing, \"num_gildings_message\"):\n        <div>\n          ${thing.num_gildings_message}\n        </div>\n      %endif\n    </div>\n  %endif\n\n%if hasattr(thing, \"goldlink\"):\n  <div class=\"giftgold\">\n    <a href=\"${thing.goldlink}\" class=\"access-required\"\n       data-type=\"account\" data-fullname=\"${thing.user._fullname}\"\n       data-event-action=\"gild\">\n      ${thing.giftmsg}\n    </a>\n  </div>\n%endif\n\n  <div class=\"bottom\">\n    %if not thing.viewing_self:\n      <img src=\"${static('mailgray.png')}\"/>\n      &#32;\n      <a href=\"${\"/message/compose/?to=%s\" % thing.user.name}\" class=\"access-required\"\n         data-type=\"account\" data-fullname=\"${thing.user._fullname}\"\n         data-event-action=\"compose\">\n        ${_(\"send a private message\")}\n      </a>\n    %endif\n\n    <span class=\"age\">\n      ${_(\"redditor for\")}&#32;${thing_timestamp(thing.user)}\n    </span>\n  </div>\n\n  <div class=\"clear\"> </div>\n\n</div>\n"
  },
  {
    "path": "r2/r2/templates/promo_email.email",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n    from datetime import datetime, timedelta\n    from pylons.i18n import ungettext\n    from r2.models import Email\n    from r2.lib import promote\n    from r2.lib.template_helpers import get_domain\n %>\n<%\n    edit_url =  promote.promo_edit_url(thing.link)\n %>\n\n%if thing.kind == Email.Kind.NEW_PROMO:\nThis email is to confirm reddit.com's recognition of your submitted self-serve\nad. Currently you have not created any campaigns within this ad. To do so, visit\nthe following link, select the date range and budget/impression count you want,\nand choose \"frontpage\" or \"targeted\" and click \"save\":\n\n   ${edit_url}\n\nPlease note that we can't approve your ad until you have authorized your credit\ncard payment, and that your ad must be approved before it goes live on your\nselected dates. We require one business day as a grace period for the approval\nprocess before the ad can go live. To pay for your campaign, click the \"pay\"\nbutton within the campaign dashboard on the link above.\n\nYour credit card will not be charged until 12-24 hours prior to your ad going\nlive on reddit.\n\nIf you have any questions in regards to advertising on reddit, just reply to\nthis email. A reddit team member will get back to you as quickly as they're\nable. Sometimes we're ninja fast, but we need up to 48 hours officially to\nrespond to all inquiries.\n\n%elif thing.kind == Email.Kind.BID_PROMO:\nThis email is to confirm that your payment of ${thing.printable_total_budget}\nfor a self-serve ad on reddit.com has been authorized.  The credit card you\nprovided will be charged 12-24 hours prior to the date your self-serve ad is set\nto run.\n\nHaving second thoughts about your budget? Need to make any edits? You'll have\nuntil ${(thing.start_date - timedelta(1)).strftime(\"%Y-%m-%d\")} to change your\nad here:\n\n    ${unsafe(edit_url + ref_tags % dict(ref=\"promo_total_budget\", campaign=\"edit_promo\"))}\n\n%elif thing.kind == Email.Kind.ACCEPT_PROMO or thing.kind == Email.Kind.EDITED_LIVE_PROMO:\n%if thing.kind == Email.Kind.ACCEPT_PROMO:\nThis email is to confirm that your self-serve reddit.com ad has been approved by\nreddit!  The credit card you provided will not be charged until 12-24 hours\nprior to the date you have set your ad to run.\n\nIf you edit your copy, URL, or thumbnail after approval and before launch, it'll\nhave to be reapproved. If it is near midnight and it is about to go live\nimminently, we can't necessarily guarantee we'll get to it in time, so keep that\nin mind when reviewing your ad.\n%endif\n%if thing.kind == Email.Kind.EDITED_LIVE_PROMO:\nThis email is to confirm that your live ad below is scheduled to be re-approved\nby reddit. Until the ad is re-approved, the ad will be paused and will not be\nshown on the site. But don't worry – re-approving live ads is among our highest\npriorities!\n%endif\n\n    ${edit_url}\n\nPlease email us, selfservicepromotion@reddit.com, if you need an ad approved\nright away. Sometimes we're ninja fast and can get to you, but we do officially\nrequest up to 48 hours to respond to all inquiries.\n\nIt won't be long now until your ad is being displayed to hundreds of thousands\nof the Internet's finest surfers!\n\n%elif thing.kind == Email.Kind.REJECT_PROMO:\nThis email is to inform you that the self-serve ad you submitted to reddit.com\nhas been rejected. \n\nPlease review the following link and optional explanation for reasons for\nrejection:\n\nhttp://www.reddit.com/wiki/selfserve#wiki_why_did_my_ad_get_rejected.3F\n\n%if thing.body:\nOptional note about rejection (for special cases):\n    ${thing.body}\n%endif:\n\nIf you have any questions, please reply to this email.\n\nTo update your promotion please go to:\n    ${edit_url}\nand we'll reconsider it for submission.\n\n%elif thing.kind == Email.Kind.QUEUED_PROMO:\nThis email is to inform you that your self-serve ad on reddit.com is about to go\nlive. Please use this email as your receipt.\n\n%if thing.trans_id > 0:\nYour credit card has been successfully charged by reddit. Feel free to reply to\nthis email if you have any questions.\n\n\n================================================================================\nTRANSACTION #${thing.trans_id}\nDATE: ${datetime.now(g.tz).strftime(\"%Y-%m-%d\")}\n................................................................................\n\nAMOUNT CHARGED: ${thing.printable_total_budget}\nSPONSORSHIP PERMALINK: ${unsafe(thing.link.make_permalink_slow(force_domain=True) + ref_tags % dict(ref=\"promo_queued\", campaign=\"view_promo\"))}\n\n================================================================================\n%else:\nYour promotion was a freebie in the amount of ${thing.printable_total_budget}.\n%endif\n\n%elif thing.kind == Email.Kind.LIVE_PROMO:\nThis email is to inform you that your self-serve ad on reddit.com is now live\nand can be found at the following link:\n\n    ${thing.link.make_permalink_slow(force_domain = True)}\n\nThank you for your business!  You can track your promotion's traffic here:\n\n    ${promote.promo_traffic_url(thing.link)}\n\nNote that there is a delay on tracking, so at first you may not see any data,\nand completed traffic will be a few hours behind. All traffic should be\nconsidered preliminary until 24 hours after the ad has ended.\n\nRemember to log in to reddit.com using the username and password you used when\nyou bought this self-serve ad. Please let us know if you have any questions by\nresponding to this email.\n\n%elif thing.kind == Email.Kind.FINISHED_PROMO:\nThis email is to inform you that your self-serve ad on reddit.com has concluded.\nPlease visit the following link to view traffic results for your ad, and note\nthat traffic stats are to be considered preliminary until 24 hours after your ad\nhas concluded.\n\n    ${promote.promo_traffic_url(thing.link)}\n\nRemember to log in to reddit.com using the username and password you used when\nyou bought this self-serve ad.\n\nThanks again for advertising on reddit, we hope you'll come back and do business\nwith us again!  To extend your campaign, visit this link and click \"+ add new\":\n\n    ${edit_url}\n\nWe'd love to know how your experience with reddit's self-serve ad platform was,\nso feel free to reply to this email to let us know if you have any feedback.\nWe've also set up a community just for self-serve advertisers like yourself to\ndiscuss the platform with each other:\n\n    http://www.reddit.com/r/selfserve\n\nWe're hoping to create a place for you to exchange tips and tricks for getting\nthe most out of your sponsored links, as well as to provide support for new\nusers.\n\n%elif thing.kind == Email.Kind.REFUNDED_PROMO:\nWe're sorry, but we weren't able to deliver as many impressions as you paid for.\n\n  ${edit_url}\n\nWe're working to improve the systems to predict our pageview inventory so this\ndoesn't happen again. You have been refunded the unspent portion of your budget.\nIf you have any questions or concerns please reply to this email.\n\n%elif thing.kind == Email.Kind.VOID_PAYMENT:\nThis email is to inform you that your pending payment of ${thing.printable_total_budget} for a self-serve ad on reddit.com has been voided and you will not be charged.\n\n%if thing.reason == 'changed_budget':\nThe payment was voided because you changed the campaign's budget. Before the campaign can go live you'll need to authorize the new payment amount:\n\n${promote.pay_url(thing.link, thing.campaign)}\n%elif thing.reason == 'changed_payment':\nThe payment was voided because you changed to a different form of payment.\n\nYou can edit your ad at the following url:\n\n${edit_url}\n%elif thing.reason == 'deleted_campaign':\nThe payment was voided because you deleted the campaign.\n\nYou can edit your ad at the following url:\n\n${edit_url}\n%endif\n\n%endif\n\nThank you,\n\nThe reddit team\nselfservicepromotion@reddit.com\n\n_____\nhttp://www.reddit.com/help/selfservicepromotion\n"
  },
  {
    "path": "r2/r2/templates/promotedlink.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n   from r2.lib.filters import conditional_websafe\n   from r2.lib.promote import promo_edit_url\n   from r2.lib import promote\n   from r2.lib.pages.things import LinkButtons\n   from r2.lib.pages import WrappedUser\n%>\n\n<%inherit file=\"link.html\"/>\n<%namespace file=\"printablebuttons.html\" import=\"ynbutton\" />\n<%namespace file=\"utils.html\" import=\"plain_link\" />\n<%namespace file=\"promotelinkedit.html\" import=\"campaign_list\" />\n\n<%def name=\"tagline()\">\n<%\n   if (c.user_is_sponsor or thing.is_author) and not promote.is_promoted(thing):\n     taglinetext = _(\"to be promoted by %(author)s\") \n   else:\n     taglinetext = _(\"promoted by %(author)s\") \n   taglinetext = conditional_websafe(taglinetext).replace(\" \", \"&#32;\")\n   author = WrappedUser(thing.author, thing.attribs, thing,\n                        force_show_flair=False).render()\n %>\n${unsafe(taglinetext % dict(author=author))}\n</%def>\n\n<%def name=\"buttons(comments=True, delete=True, report=True, additional='')\">\n  ${LinkButtons(thing, \n                comments = not getattr(thing, \"disable_comments\", False), \n                delete = delete, \n                report = report)}\n</%def>\n\n<%def name=\"domain()\">\n  %if not thing.is_self:\n    ${parent.domain(link=False)}\n  %endif\n</%def>\n\n<%def name=\"entry()\">\n  ${parent.entry()}\n  <p class=\"sponsored-tagline\">\n    %if thing.is_author or c.user_is_sponsor:\n      %if not promote.is_promo(thing):\n        ${_('deleted sponsored link')}\n      %elif promote.is_unpaid(thing):\n        ${_('unpaid sponsored link')}\n      %elif promote.is_unapproved(thing):\n        ${_('waiting approval')}\n      %elif promote.is_rejected(thing):\n        ${_('rejected sponsored link')}\n      %elif promote.is_promoted(thing):\n        ${_('sponsored link')}\n      %elif promote.is_accepted(thing):\n        ${_('accepted sponsored link')}\n      %elif promote.is_edited_live(thing):\n        ${_('edited live sponsored link')}\n      %endif\n    %else:\n      ${_('sponsored link')}\n    %endif\n  </p>\n\n  %if getattr(thing, \"show_campaign_summary\", False):\n  <div class=\"campaign-detail\">\n    ${campaign_list()}\n  </div>\n  %endif\n</%def>\n\n"
  },
  {
    "path": "r2/r2/templates/promotedlinktraffic.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%namespace file=\"reddittraffic.html\" import=\"load_timeseries_js, last_modified_message\"/>\n<%namespace file=\"utils.html\" import=\"plain_link\" />\n\n<%!\n    from r2.lib.strings import strings\n    from r2.lib.filters import safemarkdown\n    from r2.lib.template_helpers import format_number, js_timestamp, format_html\n%>\n\n<%def name=\"make_traffic_table()\">\n<div class=\"traffic-tables\">\n  <h1>detailed traffic for ${thing.detail_name}</h1>\n  <div id=\"charts\"></div>\n  <table id=\"promotion-history\" class=\"traffic-table timeseries\" data-interval=\"hour\" data-max-points=\"${len(thing.history)}\">\n  <thead>\n  <tr>\n    <th scope=\"col\">${_(\"date\")}</th>\n    <th scope=\"col\" title=\"${_(\"impressions\")}\" data-color=\"#ff5700\">${_(\"impressions\")}</th>\n    <th scope=\"col\" title=\"${_(\"clicks\")}\" data-color=\"#9494ff\">${_(\"clicks\")}</th>\n    <th scope=\"col\" title=\"${_(\"click-through (%)\")}\">${_(\"click-through (%)\")}</th>\n  </tr>\n  </thead>\n  <tbody>\n  % for date, datestr, data in thing.history:\n  <tr>\n    <th scope=\"col\" data-value=\"${js_timestamp(date)}\">${datestr}</th>\n    % for datum in data:\n    <td data-value=\"${datum}\">${format_number(datum)}</td>\n    % endfor\n  </tr>\n  % endfor\n  </tbody>\n  <tfoot>\n  <tr>\n    <th scope=\"row\">\n      ${_(\"total\")}\n      % if thing.is_preliminary:\n      *\n      % endif\n    </th>\n    <td>${format_number(thing.total_impressions)}</td>\n    <td>${format_number(thing.total_clicks)}</td>\n    % if thing.total_impressions != 0:\n    <td>${format_number(thing.total_ctr)}%</td>\n    % else:\n    <td>--</td>\n    % endif\n  </tr>\n  </tfoot>\n  </table>\n\n  % if thing.is_preliminary:\n  <p class=\"totals-are-preliminary\">* ${_(\"totals are preliminary until 24 hours after the end of the promotion.\")}</p>\n  % endif\n\n  ${nextprev()}\n</div>\n</%def>\n\n<%def name=\"nextprev()\">\n  %if thing.prev or thing.next:\n    <p class=\"nextprev\"> ${_(\"view more:\")}&#32;\n    %if thing.prev:\n      ${plain_link(format_html(\"&lsaquo; %s\", _(\"prev\")), thing.prev, _sr_path=False, rel=\"nofollow prev\")}\n    %endif\n    %if thing.prev and thing.next:\n      <span class=\"separator\"></span>\n    %endif\n    %if thing.next:\n      ${plain_link(format_html(\"%s &rsaquo;\", _(\"next\")), thing.next,  _sr_path=False, rel=\"nofollow next\")}\n    %endif\n    </p>\n  %endif\n</%def>\n\n\n<%def name=\"make_campaign_table()\">\n    <h1>campaigns</h1>\n    %if thing.has_early_campaign:\n    <div class=\"promo-traffic-help\">\n        ${_(\"Campaigns created before September 12, 2012 don't have traffic data\")}\n    </div>\n    %endif\n\n    <table class=\"traffic-table promocampaign-table\">\n      <thead>\n        <th>${_(\"id\")}</th>\n        <th></th>\n        <th></th>\n        <th>${_(\"start\")}</th>\n        <th>${_(\"end\")}</th>\n        <th>${_(\"target\")}</th>\n        <th>${_(\"location\")}</th>\n        <th>${_(\"budget\")}</th>\n        <th>${_(\"spent\")}</th>\n        <th>${_(\"imps purchased\")}</th>\n        <th>${_(\"imps delivered\")}</th>\n        <th>${_(\"cpm\")}</th>\n        <th>${_(\"clicks\")}</th>\n        <th>${_(\"ctr\")}</th>\n        <th>${_(\"cpc\")}</th>\n      </thead>\n\n      %for camp in thing.campaign_table:\n          <tr class=\"${'promo-traffic-live' if camp['live'] else ''} ${'active' if camp['active'] else ''} ${'total' if camp['total'] else ''}\">\n              <td>${camp['id']}</td>\n              <td>${plain_link(_(\"detail\"), camp['url'])}</td>\n              <td>${plain_link(_(\"csv\"), camp['csv'])}</td>\n              <td>${camp['start']}</td>\n              <td>${camp['end']}</td>\n              <td>${camp['target']}</td>\n              <td title=\"${camp['location']}\">${camp['location']}</td>\n              <td>${camp['budget']}</td>\n              <td>${camp['spent']}</td>\n              <td>${camp['impressions_purchased']}</td>\n              <td>${camp['impressions_delivered']}</td>\n              <td>${camp['cpm']}</td>\n              <td>${camp['clicks']}</td>\n              <td>${camp['ctr']}</td>\n              <td>${camp['cpc']}</td>\n          </tr>\n      %endfor\n    </table>\n</%def>\n\n\n${load_timeseries_js()}\n\n${unsafe(safemarkdown(strings.traffic_promoted_link_explanation))}\n\n%if thing.has_live_campaign:\n    ${last_modified_message()}\n%endif\n\n${unsafe(safemarkdown(strings.traffic_help_email % dict(email=g.selfserve_support_email)))}\n\n${make_campaign_table()}\n\n${make_traffic_table()}\n\n<script type=\"text/javascript\">\n  r.timeseries.init()\n</script>\n\n"
  },
  {
    "path": "r2/r2/templates/promoteinventory.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n   from r2.lib import js\n   import simplejson\n%>\n\n<%namespace name=\"pr\" file=\"promotelinkbase.html\" />\n<%namespace file=\"utils.html\" import=\"plain_link, error_field\" />\n\n${unsafe(js.use('sponsored'))}\n\n<div class=\"sponsored-page\">\n  <div class=\"dashboard inventory-dashboard\">\n    <header>\n      <h2>promoted link inventory for ${thing.display_name}</h2>\n    </header>\n    <div class=\"dashboard-content\">\n      <div class=\"editor\">\n        <div class=\"editor-group\">\n          ${pr.targeting_field(default_checked=thing.targeting_type)}\n          ${pr.scheduling_field()}\n        </div>\n        <footer class=\"buttons\">\n          <button name=\"submit\" onclick=\"r.sponsored.submit_inventory_form()\">submit</button>\n        </footer>\n      </div>\n    </div>\n  </div>\n  <table class=\"inventory-table\">\n      <caption>* inventory purchased as a part of a collection</caption>\n      <thead>\n          <tr>\n          %for text in thing.header:\n              <th>${text}</th>\n          %endfor\n          </tr>\n      </thead>\n\n      <tbody>\n          %for row in thing.rows:\n          <tr class=\"${'total' if row.is_total else ''}\">\n              <td class=\"title\">\n              %if not row.is_total:\n                  <div class=\"author view-link\">\n                      ${plain_link(row.info['author'], row.info['edit_url'])}\n                  </div>\n              %else:\n                  <div class=\"author\">${row.info['title']}</div>\n              %endif\n              </td>\n              %for column in row.columns:\n                <td\n                    %if column == '0':\n                      class=\"no-inventory\"\n                    %endif\n                    >${column}</td>\n              %endfor\n          %endfor\n      </tbody>\n  </table>\n  %if thing.csv_url:\n  <div class=\"promote-report-csv\">\n    ${plain_link(_(\"download as csv\"), thing.csv_url)}\n  </div>\n  %endif\n</div>\n\n<script type=\"text/javascript\">\n    r.sponsored.set_form_render_fnc(r.sponsored.fill_inventory_form);\n    r.sponsored.setup_geotargeting(${unsafe(simplejson.dumps(thing.regions))},\n                                   ${unsafe(simplejson.dumps(thing.metros))});\n    r.sponsored.setup_collections(${unsafe(simplejson.dumps(thing.collections))},\n                                  ${unsafe(simplejson.dumps(thing.collection_input))});\n\n    $(function() {\n      init_startdate();\n      init_enddate();\n    });\n</script>\n"
  },
  {
    "path": "r2/r2/templates/promotelinkbase.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n  from r2.lib.media import thumbnail_url\n  from r2.lib.filters import jssafe, scriptsafe_dumps\n  from r2.lib.pages import UserText\n  import simplejson\n  from babel.numbers import format_currency\n%>\n\n<%namespace file=\"utils.html\" \n            import=\"error_field, checkbox, image_upload\" />\n<%namespace name=\"utils\" file=\"utils.html\"/>\n\n<%def name=\"javascript_setup()\">\n<script type=\"text/javascript\">\n  r.sponsored.init();\n  r.sponsored.set_form_render_fnc(r.sponsored.fill_campaign_editor);\n  r.sponsored.setup(${unsafe(simplejson.dumps(thing.inventory))},\n                    ${unsafe(simplejson.dumps(thing.price_dict))},\n                    ${simplejson.dumps(not thing.campaigns)},\n                    ${simplejson.dumps(c.user_is_sponsor)},\n                    ${simplejson.dumps(thing.force_auction)});\n  r.sponsored.setup_geotargeting(${unsafe(simplejson.dumps(thing.regions))},\n                                 ${unsafe(simplejson.dumps(thing.metros))});\n  r.sponsored.setup_collections(${unsafe(simplejson.dumps(thing.collections))});\n</script>\n</%def>\n\n## Create a datepicker for a form. min/maxDateSrc are the id of the\n## element containing the min/max date - the '#' is added automatically\n## here (as a workaround for babel message extraction not handling it\n## properly if passed in when the function is called\n<%def name=\"datepicker(name, value, minDateSrc = '', maxDateSrc ='', initfuncname = '', min_date_offset=0)\">\n  <div class=\"date-input\">\n    <input name=\"${name}\"\n           value=\"${value}\" id=\"${name}\" class=\"rounded styled-input\" readonly=\"readonly\" size=\"10\" />\n    <div id=\"datepicker-${name}\" class=\"datepicker\"></div>\n    ${error_field(\"BAD_DATE\", name, \"div\")}\n    ${error_field(\"START_DATE_CANNOT_CHANGE\", name, \"div\")}\n    ${error_field(\"DATE_TOO_EARLY\", name, \"div\")}\n    ${error_field(\"DATE_TOO_LATE\", name, \"div\")}\n    <script type=\"text/javascript\">\n      ${initfuncname} = function() {\n          attach_calendar(\"#${name}\", \"#${minDateSrc}\", \"#${maxDateSrc}\",\n                          ${caller.body()}, ${min_date_offset})\n      };\n    </script>\n  </div>\n</%def>\n\n<%def name=\"title_field(link)\">\n  <%utils:line_field title=\"${_('title')}\" id=\"title-field\" css_class=\"rounded title-field\">\n    <textarea name=\"title\" rows=\"2\" cols=\"1\" \n              wrap=\"hard\" class=\"rounded\">${link.title if link else ''}</textarea>\n    ${error_field(\"NO_TEXT\", \"title\", \"div\")}\n    ${error_field(\"TOO_LONG\", \"title\", \"div\")}\n    <div class=\"help infotext rounded\">\n      <p>${_(\"A good title is important to the success of your campaign. reddit users are an intelligent, thoughtful group, and reward those who engage them.\")}</p>\n    </div>\n  </%utils:line_field>\n</%def>\n\n<%def name=\"content_field(link, enable_override=False, tracker_access=False)\">\n  <%\n    is_link = not link or not link.is_self\n    text = link.selftext if link else ''\n    url = link and link.url\n  %>\n  <%utils:line_field title=\"${_('post type')}\" id=\"kind-selector\" css_class=\"rounded post-type-field\">\n    <div class=\"radio-group\">\n      <label class=\"form-group\">\n        <input id=\"url_link\" class=\"nomargin\" \n               type=\"radio\" value=\"link\" name=\"kind\"\n               onclick=\"$('#text-field').hide(); $('#url-field').show()\"\n               ${\"checked='checked'\" if is_link else \"\"}>\n        <div class=\"label-group\">\n          <span class=\"label\">${_(\"link\")}</span>\n          <small class=\"label\">${_(\"clicks through to your URL\")}</small>\n        </div>\n      </label>\n      <label class=\"form-group\">\n        <input id=\"self_link\" class=\"nomargin\" \n               type=\"radio\" value=\"self\" name=\"kind\"\n               onclick=\"$('#url-field').hide(); $('#text-field').show()\"\n               ${\"\" if is_link else \"checked='checked'\"}>\n        <div class=\"label-group\">\n          <span class=\"label\">${_(\"text\")}</span>\n          <small class=\"label\">${_(\"clicks through to text you customize\")}</small>  \n        </div>\n      </label>\n    </div>\n  </%utils:line_field>\n  <%utils:line_field title=\"${_('text')}\" id=\"text-field\" css_class=\"rounded text-field\"\n                     style=\"${('' if not is_link else 'display:none')}\">\n    ${UserText(None, text=text, textarea_class='rounded', have_form=False,\n               creating=True)}\n  </%utils:line_field>\n  <%utils:line_field title=\"${_('url')}\" id=\"url-field\" css_class=\"rounded url-field\"\n                     style=\"${('' if is_link else 'display:none')}\">\n    <input id=\"url\" name=\"url\" type=\"text\" \n           value=\"${(url if is_link else '') if link else \"\"}\"\n           class=\"rounded\">\n    ${error_field(\"NO_URL\", \"url\", \"div\")}\n    ${error_field(\"BAD_URL\", \"url\", \"div\")}\n    ${error_field(\"DOMAIN_BANNED\", \"url\", \"div\")}\n    ${error_field(\"TOO_LONG\", \"url\", \"div\")}\n    <div class=\"help infotext rounded\">\n      <p>${_(\"Provide the URL of your ad. No redirects please!\")}</p>\n    </div>\n\n    %if enable_override:\n      <label style=\"display:block; text-align:right\" for=\"domain\">${_(\"Override display domain:\")}</label>\n      <input id=\"domain\" name=\"domain\" type=\"text\"\n             %if link and link.domain_override:\n               value=\"${link.domain_override}\" class=\"rounded\"\n             %else:\n               class=\"rounded\" placeholder=\"optional\"\n             %endif\n      >\n      <div class=\"infotext rounded\">\n        <p>${_(\"Choose a different domain name to display on the site (the small grey text next to a link)\")}</p>\n      </div>\n\n      <label style=\"display:block; text-align:right\" for=\"third_party_tracking\">${_(\"Impression tracking URL:\")}</label>\n      <input id=\"third_party_tracking\" name=\"third_party_tracking\" type=\"text\"\n             %if link and link.third_party_tracking:\n               value=\"${link.third_party_tracking}\" class=\"rounded\"\n             %else:\n               class=\"rounded\" placeholder=\"optional\"\n             %endif\n      >\n      <div class=\"infotext rounded\">\n        <p>${_(\"Enter a URL to insert into a 3rd-party impression tracking code snippet\")}</p>\n      </div>\n\n      <label style=\"display:block; text-align:right\" for=\"third_party_tracking_2\">${_(\"Secondary Impression tracking URL:\")}</label>\n      <input id=\"third_party_tracking_2\" name=\"third_party_tracking_2\" type=\"text\"\n             %if link and link.third_party_tracking_2:\n               value=\"${link.third_party_tracking_2}\" class=\"rounded\"\n             %else:\n               class=\"rounded\" placeholder=\"optional (not common)\"\n             %endif\n      >\n      <div class=\"infotext rounded\">\n        <p>${_(\"Enter a second 3rd-party impression tracking URL (not common)\")}</p>\n      </div>\n    %endif\n  </%utils:line_field>\n</%def>\n\n<%def name=\"image_field(link=None, images=None)\">\n  <% \n    if link:\n      thumbnail_url = getattr(link, \"thumbnail_url\", None)\n      mobile_url = getattr(link, \"mobile_ad_url\", None)\n      path = \"ads/%s\" % link._id36\n    else:\n      thumbnail_url = images.get(\"thumbnail\", None)\n      mobile_url = images.get(\"mobile\", None)\n      path = \"ads/%s\" % c.user.name\n  %>\n  <%utils:line_field\n      title=\"${_('thumbnail')}\" \n      css_class=\"rounded image-field\">\n    <div class=\"infotext\">\n      ${_('images will be resized if larger than 140x140 pixels (displayed at 70x70)')}\n    </div>\n    ${utils.s3_image_upload(\n      id=\"thumbnail\",\n      width=\"70\",\n      height=\"70\",\n      src=thumbnail_url,\n      data=dict(\n        max=(1024**2) * 3,\n        url=\"/api/ad_s3_params.json\",\n        params=simplejson.dumps(dict(\n          kind=\"thumbnail\",\n          link=(link and link._id36),\n        )),\n      ),\n    )}\n  </%utils:line_field>\n\n  %if thing.mobile_targeting_enabled:\n    <%utils:line_field title=\"${_('mobile ad image')}\"\n                       css_class=\"rounded image-field\">\n      <div class=\"infotext\">\n        ${_('upload image for use on mobile web.  should be exactly 1200x628 pixels (displayed at 600x314)')}\n      </div>\n      ${utils.s3_image_upload(\n        id=\"mobile\",\n        width=\"600\",\n        height=\"314\",\n        src=mobile_url,\n        data=dict(\n          max=(1024**2) * 3,\n          url=\"/api/ad_s3_params.json\",\n          params=simplejson.dumps(dict(\n            kind=\"mobile\",\n            link=(link and link._id36),\n          )),\n        ),\n      )}\n    </%utils:line_field>\n  %endif\n</%def>\n\n<%def name=\"commenting_field(link)\">\n  <%utils:line_field title=\"${_('options')}\" id=\"commenting-field\" css_class=\"rounded commenting-field\">\n    <div class=\"checkbox-group\">\n      <div class=\"form-group\">\n        ${checkbox(\"disable_comments\", _(\"disable comments\"), link.disable_comments)}\n      </div>\n      \n      <div class=\"form-group\">\n        ${checkbox(\"sendreplies\", _(\"send comments on my ad to my inbox\"), link.sendreplies)}\n      </div>\n    </div>\n\n    <div class=\"help infotext\">\n      <p>${_(\"Comments are a great way to get feedback from customers, and the reddit community is known for being vocal in comment threads.\")}</p>\n    </div>\n  </%utils:line_field>\n</%def>\n\n<%def name=\"media_field(link)\">\n  <%utils:line_field title=\"${_('media')}\" id=\"media-field\" css_class=\"rounded media-field\">\n    <div class=\"delete-field\">\n      <div class=\"radio-group\">\n        <%\n          scraper_checked = link.media_url or not link.gifts_embed_url\n        %>\n        <label class=\"form-group checkbox\">\n          <input type=\"radio\" id=\"scrape\" value=\"scrape\" name=\"media_url_type\"\n                 ${\"checked='checked'\" if scraper_checked else ''}>\n          <span class=\"label\">scraper</span>\n        </label>\n        <label class=\"form-group checkbox\">\n          <input type=\"radio\" id=\"redditgifts\" value=\"redditgifts\" name=\"media_url_type\"\n                 ${\"checked='checked'\" if not scraper_checked else ''}>\n          <span class=\"label\">redditgifts</span>\n        </label>\n      </div>\n      <p id=\"scraper_input\" ${\"\" if scraper_checked else \"style='display:none'\"}>\n        <label for=\"media_url\">source URL to scrape</label>\n        <input id=\"media_url\" name=\"media_url\" type=\"text\" class=\"rounded\"\n               value=\"${link.media_url or \"\"}\"\n               ${\"\" if scraper_checked else \"disabled\"}>\n        ${error_field(\"BAD_URL\", \"media_url\", \"div\")}\n        ${error_field(\"SCRAPER_ERROR\", \"media_url\", \"div\")}\n      </p>\n      <p id=\"rg_input\" ${\"style='display:none'\" if scraper_checked else \"\"}>\n        <label for=\"gifts_embed_url\">redditgifts embed URL</label>\n        <input id=\"gifts_embed_url\" name=\"gifts_embed_url\" type=\"text\" class=\"rounded\"\n               value=\"${link.gifts_embed_url or ''}\"\n               ${\"disabled\" if scraper_checked else \"\"}>\n        ${error_field(\"BAD_URL\", \"gifts_embed_url\", \"div\")}\n      </p>\n      ${checkbox(\"media_autoplay\",\n                 _(\"autoplay\"),\n                 link.media_autoplay)}\n      <br>\n      ${checkbox(\"media-override\",\n                 _(\"media override (adds an onclick to the link to generate a drop-down rather than a link out)\"),\n                 getattr(link, \"media_override\", False) or False)}\n      <br>\n    </div>\n  </%utils:line_field>\n</%def>\n\n<%def name=\"managed_field(link)\">\n  <%utils:line_field title=\"\" id=\"managed-field\" css_class=\"rounded managed-field\">\n    <div class=\"checkbox-group\">\n      <div class=\"form-group\">\n        ${checkbox(\"is_managed\", _(\"managed promotion\"), link.managed_promo if link else False)}\n      </div>\n      <div class=\"infotext rounded\">\n        <p>${_(\"Managed promotions don't appear in the selfserve approval queues.\")}</p>\n      </div>\n    </div>\n  </%utils:line_field>\n</%def>\n\n<%def name=\"scheduling_field()\">\n  <%utils:line_field title=\"${_('scheduling')}\" css_class=\"rounded timing-field\">\n    %if thing.min_start:\n      <input type=\"hidden\" id=\"date-min\" value=\"${thing.min_start}\">\n    %endif\n    %if thing.max_start:\n      <input type=\"hidden\" id=\"date-start-max\" value=\"${thing.max_start}\">\n    %endif\n    %if thing.max_end:\n      <input type=\"hidden\" id=\"date-end-max\" value=\"${thing.max_end}\">\n    %endif\n\n    <div class=\"group\">\n      <div class=\"form-group\">\n        <span class=\"label\">${_(\"start\")}</span>\n        <div class=\"input-group\">\n          <%self:datepicker name=\"startdate\", value=\"${thing.default_start}\"\n                            minDateSrc=\"date-min\" maxDateSrc=\"date-start-max\"\n                            initfuncname=\"init_startdate\">\n            function(elem) {\n              check_enddate(elem, $(\"#enddate\"));\n              r.sponsored.on_date_change();\n            }\n          </%self:datepicker>\n        </div>\n      </div>\n      <div class=\"form-group\">\n        <span class=\"label\">${_(\"end\")}</span>\n        <div class=\"input-group\">\n        <%self:datepicker name=\"enddate\", value=\"${thing.default_end}\"\n                            minDateSrc=\"startdate\" maxDateSrc=\"date-end-max\"\n                            initfuncname=\"init_enddate\" min_date_offset=\"86400000\">\n            function(elem) { r.sponsored.on_date_change(); }\n          </%self:datepicker>\n        </div>\n      </div>\n      <div class=\"form-group\">\n        <span class=\"label\">${_(\"duration\")}</span>\n        <div class=\"display-text duration\"></div>\n      </div>\n    </div>\n    <div>\n      ${error_field(\"BAD_DATE_RANGE\", \"enddate\", \"div\")}\n    </div>\n  </%utils:line_field>\n</%def>\n\n<%def name=\"platform_field()\">\n  <input type=\"hidden\" id=\"mobile_os\" name=\"mobile_os\" value=\"\">\n  <%def name=\"platform_field_content(default_checked='desktop')\">\n    <div class=\"radio-group platform-group\">\n      <span class=\"label\">platform</span>\n      <label class=\"form-group\">\n        <input id=\"all_platform\" class=\"nomargin\"\n               type=\"radio\" value=\"all\" name=\"platform\"\n               %if default_checked == 'all':\n                 checked=\"checked\"\n               %endif\n               >\n        <div class=\"label-group\">\n          <span class=\"label\">${_(\"desktop and mobile web\")}</span>\n        </div>\n      </label>\n      <label class=\"form-group\">\n        <input id=\"desktop_platform\" class=\"nomargin\"\n               type=\"radio\" value=\"desktop\" name=\"platform\"\n               %if default_checked == 'desktop':\n                 checked=\"checked\"\n               %endif\n               >\n        <div class=\"label-group\">\n          <span class=\"label\">${_(\"desktop only\")}</span>\n        </div>\n      </label>\n      <label class=\"form-group\">\n        <input id=\"mobile_platform\" class=\"nomargin\"\n               type=\"radio\" value=\"mobile\" name=\"platform\"\n               %if default_checked == 'mobile':\n                 checked=\"checked\"\n               %endif\n               >\n        <div class=\"label-group\">\n          <span class=\"label\">${_(\"mobile web only\")}</span>\n        </div>\n      </label>\n    </div>\n    <div class=\"checkbox-group mobile-os-group\">\n      <span class=\"label\">mobile OS</span>\n      <label class=\"form-group\">\n        <input type=\"checkbox\" checked id=\"mobile_os_iOS\" value=\"iOS\">\n        <span class=\"label\">${_(\"iOS\")}</span>\n      </label>\n      <label class=\"form-group\">\n        <input type=\"checkbox\" checked id=\"mobile_os_Android\" value=\"Android\">\n        <span class=\"label\">${_(\"Android\")}</span>\n      </label>\n      ${error_field(\"BAD_PROMO_MOBILE_OS\", \"mobile_os\", \"div\")}\n      <div class=\"error\">\n        ${_(\"you must select at least one mobile OS\")}\n      </div>\n    </div>\n    <div class=\"os-device-group\">\n      <div class=\"radio-group\">\n        <span class=\"label\">device and OS version</span>\n        <label class=\"form-group\">\n          <input id=\"all_os_devices\" class=\"nomargin\"\n                 type=\"radio\" value=\"all\" name=\"os_versions\"\n                 checked=\"checked\"\n                 >\n          <div class=\"label-group\">\n            <span class=\"label\">${_(\"All\")}</span>\n          </div>\n        </label>\n        <label class=\"form-group\">\n          <input id=\"filter_os_devices\" class=\"nomargin\"\n                 type=\"radio\" value=\"filter\" name=\"os_versions\"\n                 >\n          <div class=\"label-group\">\n            <span class=\"label\">${_(\"Filter by device and OS\")}</span>\n          </div>\n        </label>\n      </div>\n      <div class=\"device-version-group ios-group\">\n        <input type=\"hidden\" id=\"ios_device\" name=\"ios_device\" value=\"\">\n        <div class=\"checkbox-group ios-device\">\n          <label class=\"form-group\">\n            <input type=\"checkbox\" id=\"iphone\" value=\"iPhone\" checked>\n            <span class=\"label\">${_(\"iPhone\")}</span>\n          </label>\n          <label class=\"form-group\">\n            <input type=\"checkbox\" id=\"ipad\" value=\"iPad\" checked>\n            <span class=\"label\">${_(\"iPad\")}</span>\n          </label>\n          <label class=\"form-group\">\n            <input type=\"checkbox\" id=\"ipod\" value=\"iPod\" checked>\n            <span class=\"label\">${_(\"iPod\")}</span>\n          </label>\n        </div>\n        <input type=\"hidden\" id=\"ios_version_range\"\n               name=\"ios_version_range\">\n        <div class=\"select-group version-select ios-min-version\">\n          <label class=\"form-group\">\n            <span class=\"label\">${_(\"Min\")}</span>\n            <select id=\"ios_min\" title=\"${_('iOS min')}\"\n              %for version in thing.ios_versions:\n              <option ${\"selected='selected'\" if selected else \"\"} value=\"${version}\">\n                ${version}\n              </option>\n              %endfor\n            </select>\n          </label>\n        </div\n        <div class=\"select-group version-select ios-max-version\">\n          <label class=\"form-group\">\n            <span class=\"label\">${_(\"Max\")}</span>\n            <select id=\"ios_max\" title=\"${_('iOS max')}\">\n              <option ${\"selected='selected'\" if selected else \"\"} value=\"\">\n                no max\n              </option>\n              %for version in thing.ios_versions:\n              <option ${\"selected='selected'\" if selected else \"\"} value=\"${version}\">\n                ${version}\n              </option>\n              %endfor\n            </select>\n          </label>\n        </div>\n      </div>\n      <div class=\"device-version-group android-group\">\n        <input type=\"hidden\" id=\"android_device\" name=\"android_device\">\n        <div class=\"checkbox-group android-device\">\n          <label class=\"form-group\">\n            <input type=\"checkbox\" id=\"phone\" value=\"phone\" checked>\n            <span class=\"label\">${_(\"Android Phone\")}</span>\n          </label>\n          <label class=\"form-group\">\n            <input type=\"checkbox\" id=\"tablet\" value=\"tablet\" checked>\n            <span class=\"label\">${_(\"Android Tablet\")}</span>\n          </label>\n        </div>\n        <input type=\"hidden\" id=\"android_version_range\"\n               name=\"android_version_range\">\n        <div class=\"select-group version-select android-min-version\">\n          <label class=\"form-group\">\n            <span class=\"label\">${_(\"Min\")}</span>\n            <select id=\"android_min\" title=\"${_('android min')}\"\n              %for version in thing.android_versions:\n              <option ${\"selected='selected'\" if selected else \"\"} value=\"${version}\">\n                ${version}\n              </option>\n              %endfor\n            </select>\n          </label>\n        </div\n        <div class=\"select-group version-select android-max-version\">\n          <label class=\"form-group\">\n            <span class=\"label\">${_(\"Max\")}</span>\n            <select id=\"android_max\" title=\"${_('Android max')}\">\n              <option ${\"selected='selected'\" if selected else \"\"} value=\"\">\n                no max\n              </option>\n              %for version in thing.android_versions:\n              <option ${\"selected='selected'\" if selected else \"\"} value=\"${version}\">\n                ${version}\n              </option>\n              %endfor\n            </select>\n          </label>\n        </div>\n      </div>\n      ${error_field(\"BAD_PROMO_MOBILE_DEVICE\", \"os_versions\", \"div\")}\n      ${error_field(\"INVALID_OS_VERSION\", \"os_version\", \"div\")}\n      <div class=\"error version-error\">\n        ${_(\"you must select valid versions to target\")}\n      </div>\n      <div class=\"error device-error\">\n        ${_(\"you must select at least one device per OS to target\")}\n      </div>\n    </div>\n  </%def>\n\n  %if thing.mobile_targeting_enabled:\n    <%utils:line_field title=\"${_('platform')}\" css_class=\"rounded platform-field\">\n      ${platform_field_content()}\n    </%utils:line_field>\n  %else:\n    <div class=\"platform-field\" style=\"display:none\">\n      ${platform_field_content()}\n    </div>\n  %endif\n</%def>\n\n<%def name=\"frequency_cap_field(default_checked='false')\">\n  <%def name=\"frequency_select_content()\">\n    <div class=\"radio-group group\">\n      <span class=\"label\">frequency capping</span>\n      <label class=\"form-group\">\n        <input id=\"frequency_capped_false\" class=\"nomargin\"\n               type=\"radio\" value=\"false\" name=\"frequency_capped\"\n               onclick=\"r.sponsored.toggleFrequency()\"\n               %if default_checked == 'false':\n                 checked=\"checked\"\n               %endif\n               >\n        <div class=\"label-group\">\n          <span class=\"label\">${_(\"no frequency cap\")}</span>\n        </div>\n      </label>\n      <label class=\"form-group\">\n        <input id=\"frequency_capped_true\" class=\"nomargin\"\n               type=\"radio\" value=\"true\" name=\"frequency_capped\"\n               onclick=\"r.sponsored.toggleFrequency()\"\n               %if default_checked == 'true':\n                 checked=\"checked\"\n               %endif\n               >\n        <div class=\"label-group\">\n          <span class=\"label\">${_(\"frequency capped\")}</span>\n        </div>\n      </label>\n\n    </div>\n  </%def>\n\n  <%def name=\"frequency_details_content()\">\n    <div class=\"group frequency-cap-inputs\">\n      <div>\n        <div class=\"form-group\">\n          <span class=\"label\">${_(\"cap per 24 hours\")}</span>\n          <div class=\"input-group\">\n            <input id=\"frequency_cap\" name=\"frequency_cap\" size=\"7\" type=\"text\"\n                   class=\"rounded style-input\"\n                   style=\"width:auto\"\n                   onkeyup=\"r.sponsored.on_frequency_cap_change()\"\n                   data-frequency_cap_min=\"${thing.frequency_cap_min}\"/>\n          </div>\n        </div>\n      </div>\n    </div>\n    <div class=\"frequency-cap-message example\">\n      ${_('Example: Display this flight no more than %i times per user per 24 hours.' %\n          g.frequency_cap_min)}\n    </div>\n    <div class=\"frequency-cap-message error\">\n      ${_(\"frequency must be at least %i per 24 hours\" % thing.frequency_cap_min)}\n    </div>\n    ${error_field(\"INVALID_FREQUENCY_CAP\", \"frequency_cap\", \"div\")}\n    ${error_field(\"FREQUENCY_CAP_TOO_LOW\", \"frequency_cap\", \"div\")}\n  </%def>\n\n  %if c.user_is_sponsor:\n    <%utils:line_field title=\"${_('frequency')}\" css_class=\"rounded\">\n      ${frequency_select_content()}\n      <div class=\"frequency-cap-field hidden\">\n        ${frequency_details_content()}\n      </div>\n    </%utils:line_field>\n  %endif\n</%def>\n\n<%def name=\"priority_field()\">\n  <%def name=\"priority_field_content()\">\n    <div class=\"radio-group\">\n      %for value, text, description, default, override, house in thing.priorities:\n        %if value != 'auction':\n          <label class=\"form-group checkbox\">\n            <input id=\"${value}\" class=\"nomargin\" \n                   type=\"radio\"  value=\"${value}\" name=\"priority\"\n                   onclick=\"r.sponsored.priority_changed()\"\n                   ${\"checked='checked'\" if default else \"\"}\n                   data-default=\"${simplejson.dumps(default)}\"\n                   data-override=\"${simplejson.dumps(override)}\"\n                   data-house=\"${simplejson.dumps(house)}\">\n            %if description:\n              <span class=\"label\">${\"%s (%s)\" % (text, description)}</span>\n            %else:\n              <span class=\"label\">${text}</span>\n            %endif\n          %endif\n        </label>\n      %endfor\n    </div>\n  </%def>\n\n  %if c.user_is_sponsor:\n    <%utils:line_field title=\"${_('priority')}\" css_class=\"rounded priority-field hidden\">\n      ${priority_field_content()}\n    </%utils:line_field>\n  %endif\n</%def>\n\n<%def name=\"pricing_field()\">\n  <%utils:line_field title=\"${_('pricing')}\" css_class=\"rounded pricing-field auction-field\">\n    <div class=\"pricing-message\"></div>\n    <div class=\"group\"> \n      %if thing.cpc_pricing:\n        <div class=\"form-group pricing-group\">\n          <span class=\"label\">Cost basis</span>\n          <div class=\"cost-basis-select\">\n            <select class=\"cost-basis-select\" id=\"cost_basis\" name=\"cost_basis\"\n                    title=\"${_(\"price basis\")}\"\n                    onchange=\"r.sponsored.on_cost_basis_change()\">\n              <option ${\"selected='selected'\" if selected else \"\"} value=\"cpc\">\n                CPC\n              </option>\n              <option value=\"cpm\">\n                CPM\n              </option>\n            </select>\n            ${error_field(\"INVALID_LOCATION\", \"location\", \"div\")}\n          </div>\n        </div>\n      %else:\n        <input type=\"hidden\" id=\"cost_basis\" name=\"cost_basis\" value=\"cpm\"/>\n      %endif\n      <div class=\"form-group\">\n        <span class=\"label cost-basis-label\" id=\"cost-basis-label\"></span>\n        <div class=\"input-group\">\n          $<input id=\"bid_dollars\" name=\"bid_dollars\" size=\"7\"\n                  type=\"text\" class=\"rounded styled-input\"\n                  style=\"width:auto\"\n                  onchange=\"r.sponsored.on_bid_change()\"\n                  onkeyup=\"r.sponsored.on_bid_change()\"\n                  value=\"${'%.2f' % (g.default_bid_pennies / 100.)}\"\n                  data-default_bid_dollars=\"${g.default_bid_pennies / 100.}\"\n                  data-min_bid_dollars=\"${thing.min_bid_dollars}\"\n                  data-max_bid_dollars=\"${thing.max_bid_dollars}\"/>\n        </div>\n        ${error_field('BAD_BID', 'bid', 'div')}\n      </div>\n      <div class=\"form-group\">\n        \n      </div>\n    </div>\n  </%utils:line_field>\n</%def>\n\n<%def name=\"budget_field()\">\n  <%utils:line_field title=\"${_('budget')}\" css_class=\"rounded budget-field\">\n    <div class=\"group\">\n      <div class=\"form-group\">\n        <span class=\"label\">${_(\"total budget\")}</span>\n        <div class=\"input-group\">\n          $<input id=\"total_budget_dollars\" name=\"total_budget_dollars\" size=\"7\" type=\"text\"\n                  class=\"rounded styled-input\"\n                  style=\"width:auto\"\n                  onchange=\"r.sponsored.on_budget_change()\"\n                  onkeyup=\"r.sponsored.on_budget_change()\"\n                  value=\"${'%.2f' % thing.default_budget_dollars}\"\n                  data-default_budget_dollars=\"${thing.default_budget_dollars}\"\n                  data-min_budget_dollars=\"${thing.min_budget_dollars}\"\n                  data-max_budget_dollars=\"${thing.max_budget_dollars}\"/>\n          <div class=\"minimum-spend\">\n            ${_('%(minimum)s minimum') % dict(minimum=format_currency(thing.min_budget_dollars, 'USD', locale=c.locale))}\n          </div>\n        </div>\n      </div>\n      <div class=\"form-group fixed-cpm-field\">\n        <span class=\"label\">${_(\"impressions\")}</span>\n        <input id=\"impressions\" name=\"impressions\" size=\"10\" type=\"text\"\n               class=\"rounded styled-input\"\n               onchange=\"r.sponsored.on_impression_change()\">\n      </div>\n      <div class=\"form-group fixed-cpm-field\">\n        <span class=\"label\">${_(\"price\")}</span>\n        <div class=\"display-text price-info\"></div>\n      </div>\n    </div>\n\n    <div>\n      <div class=\"budget-message auction-field\">\n        Your daily spend will not exceed <span class=\"display-text daily-max-spend\"></span>.\n      </div>\n    </div>\n\n    <div>\n      ${error_field(\"BAD_BUDGET\", \"total_budget_dollars\", \"div\")}\n      ${error_field(\"BUDGET_LIVE\", \"total_budget_dollars\", \"div\")}\n      <div class=\"budget-change-warning error\">\n        ${_('if you modify the budget of this paid campaign you will need to reauthorize payment by clicking the \"pay\" button')}\n      </div>\n      <div class=\"budget-unchangeable-warning error\">\n        ${_('the budget for campaigns cannot be adjusted once the campaign has gone live')}\n      </div>\n      <div class=\"available-info\"></div>\n      ${error_field(\"OVERSOLD_DETAIL\", \"total_budget_dollars\", \"div\")}\n    </div>\n  </%utils:line_field>\n</%def>\n\n<%def name=\"targeting_field(default_checked='collection')\">\n  <%utils:line_field title=\"${_('targeting')}\" css_class=\"rounded targeting-field\">\n    <div class=\"radio-group group\">\n      <span class=\"label\">target</span>\n      <label class=\"form-group\">\n        <input id=\"collection_targeting\" class=\"nomargin\"\n               type=\"radio\" value=\"collection\" name=\"targeting\"\n               onclick=\"r.sponsored.collection_targeting()\"\n               %if default_checked == 'collection':\n                 checked=\"checked\"\n               %endif\n               >\n        <div class=\"label-group\">\n\n          <span class=\"label\">${_(\"interests\")}</span>\n          <small class=\"label\">${_(\"targets a collection of similar subreddits\")}</small>\n        </div>\n      </label>\n      <label class=\"form-group\">\n        <input id=\"subreddit_targeting\" class=\"nomargin\"\n               type=\"radio\" value=\"one\" name=\"targeting\"\n               onclick=\"r.sponsored.subreddit_targeting()\"\n               %if default_checked == 'one':\n                 checked=\"checked\"\n               %endif\n               >\n        <div class=\"label-group\">\n          <span class=\"label\">${_(\"subreddits\")}</span>\n          <small class=\"label\">${_(\"targets a subreddit and its subscribers\")}</small>\n        </div>\n      </label>\n    </div>\n    <div class=\"target-group group\">\n      <div class=\"collection-targeting\"\n           %if default_checked != 'collection':\n             style=\"display:none\"\n           %endif\n           >\n        <span class=\"label\">${_(\"interest audience group\")}</span>\n        <div class=\"collection-selector uninitialized\">\n          <div class=\"widget-container\">\n            <div class=\"form-group-list\">\n            </div>\n          </div>\n        </div>\n        <div class=\"collection-subreddit-list\">\n          <div class=\"label frontpage-label\">\n            ${_(\"subreddits included on the frontpage are based on users\\' subscriptions\")}\n          </div>\n          <div class=\"label collection-label\" style=\"display:none;\">\n            ${_('includes these subreddits')}\n            &#32;<a href=\"/wiki/advertising/interestaudiencegroups\">${_('and more!')}</a>\n          </div>\n          <ul></ul>\n          ${error_field(\"COLLECTION_NOEXIST\", \"collection\", \"div\")}\n        </div>\n      </div>\n      <div class=\"subreddit-targeting\"\n           %if default_checked != 'one':\n             style=\"display:none\"\n           %endif\n           >\n        <span class=\"label\">subreddit</span>\n        ${error_field(\"OVERSOLD\", \"sr\", \"div\")}\n        ${thing.subreddit_selector}\n      </div>\n      <div class=\"target-change-warning error\">\n        ${_('changing the target for a live campaign requires reapproval.')}\n        ${_('while your campaign is awaiting reapproval, it will not be displayed.')}\n      </div>\n    </div>\n    <div class=\"select-group geotargeting-group\">\n      <span class=\"label\">location</span>\n      <div class=\"geotargeting-disabled\" style=\"display:none\">\n        ${_(\"location targeting is only available when targeting the frontpage\")}\n      </div>\n      <div class=\"geotargeting-selects\">\n        <select class=\"geotarget-select\" id=\"country\" name=\"country\"\n                title=\"${_(\"country\")}\"\n                onchange=\"r.sponsored.country_changed()\">\n          %for code, name, selected in thing.countries:\n          <option ${\"selected='selected'\" if selected else \"\"} value=\"${code}\">\n            ${name}\n          </option>\n          %endfor\n        </select>\n        <select class=\"geotarget-select\" id=\"region\" name=\"region\"\n                title=\"${_(\"region\")}\" style=\"display:none\"\n                onchange=\"r.sponsored.region_changed()\"></select>\n        <select class=\"geotarget-select\" id=\"metro\" name=\"metro\"\n                title=\"${_(\"metro\")}\" style=\"display:none\"\n                onchange=\"r.sponsored.metro_changed()\"></select>\n        ${error_field(\"INVALID_LOCATION\", \"location\", \"div\")}\n      </div>\n    </div>\n  </%utils:line_field>\n</%def>\n\n<%def name=\"subreddit_targeting_field(subreddit_selector)\">\n  <%utils:line_field title=\"${_('subreddit targeting')}\" css_class=\"rounded subreddit-targeting-field\">\n    <div class=\"target-group group\">\n      <div class=\"subreddit-targeting\">\n        <span class=\"label\">subreddit</span>\n        ${error_field(\"OVERSOLD\", \"sr\", \"div\")}\n        ${subreddit_selector}\n      </div>\n    </div>\n  </%utils:line_field>\n</%def>\n\n<%def name=\"reporting_field(link_text='', owner='')\">\n  <%utils:line_field title=\"${_('report')}\" css_class=\"rounded reporting-field\">\n    <label class=\"form-group\">\n      <div class=\"label\">${_('link ids')}</div>\n      <textarea name=\"link_text\">${link_text}</textarea>\n      ${error_field(\"BAD_LINKS\", \"bad_links\", \"div\")}\n    </label>\n    <label class=\"form-group\">\n      <div class=\"label\">${_('owner')}</div>\n      <input type=\"text\" name=\"owner\" value=\"${owner}\" />\n    </label>\n  </%utils:line_field>\n</%def>\n\n<%def name=\"admin_panel()\">\n  %if c.user_is_sponsor:\n    <div class=\"spacer bidding-history\">\n      %if thing.bids:\n        <%utils:line_field title=\"${_('spend history')}\" css_class=\"rounded\">\n          <table class=\"bid-table\">\n            <tr>\n              <th>date</th>\n              <th>user</th>\n              <th>transaction id</th>\n              <th>campaign id</th>\n              <th>pay id</th>\n              <th>spend</th>\n              <th>charge</th>\n              <th>status</th>\n            </tr>\n            %for bid in thing.bids:\n              <tr class=\"bid-${bid.status}\">\n                <td>${bid.date}</td>\n                <td>${bid.bidder}</td>\n                <td>${bid.transaction}</td>\n                <td>${bid.campaign}</td>\n                <td>${bid.pay_id}</td>\n                <td>${bid.amount_str}</td>\n                <td>${bid.charge_str}</td>\n                <td class=\"bid-status\">${bid.status}</td>\n              </tr>\n            %endfor\n          </table>\n        </%utils:line_field>\n      %endif\n\n      <form id=\"promotion-history\" method=\"post\" action=\"/post/promote_note\"\n            onsubmit=\"post_form(this, 'promote_note'); $('#promote_note').val('');return false;\">\n        <%utils:line_field title=\"${_('promotion history')}\" css_class=\"rounded\">\n          <div style=\"font-size:smaller; margin-bottom: 10px;\">\n            For correspondence, the email address of this author is&#32;\n            <a href=\"mailto:${thing.author.email}\">${thing.author.email}</a>.\n          </div>\n\n          <div style=\"font-size:smaller; margin-bottom: 10px;\">\n            To check with&#32;<a href=\"https://account.authorize.net/\">authorize.net</a>,\n            use CustomerID&#32;<b>${thing.author._fullname}</b>&#32; when searching by batch.\n          </div>\n\n          <input type=\"hidden\" name=\"link\" value=\"${thing.link._fullname}\"/>\n          <label for=\"promote_note\">add note:</label>\n          <input id=\"promote_note\" name=\"note\" value=\"\" type=\"text\" size=\"40\" />\n          <button type=\"submit\">save</button>\n          <div class=\"notes\">\n            %for line in thing.promotion_log:\n              <p>${line}</p>\n            %endfor\n          </div>\n        </%utils:line_field>\n      </form>\n    </div>\n  %endif\n</%def>\n\n<%def name=\"is_auction_field()\">\n  <%def name=\"is_auction_field_content()\">\n    <div class=\"radio-group\">\n      <label class=\"form-group\">\n        <input id=\"is_auction_true\" class=\"nomargin\"\n               type=\"radio\" value=\"true\" name=\"is_auction\"\n               onclick=\"r.sponsored.toggleAuctionFields()\"\n               >\n        <div class=\"label-group\">\n          <span class=\"label\">${_('auction')}</span>\n        </div>\n      </label>\n      <label class=\"form-group\">\n        <input id=\"is_auction_false\" class=\"nomargin\"\n               type=\"radio\" value=\"false\" name=\"is_auction\"\n               onclick=\"r.sponsored.toggleAuctionFields()\"\n               >\n        <div class=\"label-group\">\n          <span class=\"label\">${_('fixed CPM')}</span>\n        </div>\n      </label>\n    </div>\n  </%def>\n  %if thing.auction_optional:\n    <%utils:line_field title=\"${_('campaign type')}\" css_class=\"rounded\">\n      ${is_auction_field_content()}\n    </%utils:line_field>\n  %endif\n</%def>\n\n<%def name=\"is_new_field()\">\n  <input type=\"hidden\" id=\"is_new\" name=\"is_new\" value=\"true\">\n</%def>\n"
  },
  {
    "path": "r2/r2/templates/promotelinkedit.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n  from r2.lib import promote\n  from r2.lib import js\n%>\n\n<%namespace name=\"pr\" file=\"promotelinkbase.html\" />\n<%namespace file=\"utils.html\" name=\"utils\" />\n\n\n${unsafe(js.use('sponsored'))}\n\n<div class=\"create-promotion sponsored-page\">\n  <div class=\"dashboard creative-dashboard\">\n    <header>\n      <h2>${_(\"creative dashboard\")}</h2>\n      ${thing.infobar}\n    </header>\n    <div class=\"dashboard-content\">\n      ${self.creative_editor()}\n    </div>\n  </div>\n\n  <div class=\"dashboard campaign-dashboard\">\n    <header>\n      <h2>${_(\"campaign dashboard\")}</h2>\n      <div class=\"help\" style=\"${'display:none' if not thing.campaigns else ''}\">\n        <p></p>\n      </div>\n      <p class=\"error\"\n         style=\"${'display:none' if thing.campaigns else ''}\">\n        ${_(\"You don't have any campaigns for this link yet.  You should add one.\")}\n      </p>\n      <%\n        newcamp_title = _(\"click to create a new campaign.  To edit an existing \" \n                          \"campaign in the table below, click the 'edit' button.\")\n      %>\n      <button class=\"new-campaign primary-button\"\n          ${'disabled=\"disabled\"' if len(thing.campaigns) >= g.MAX_CAMPAIGNS_PER_LINK or not thing.campaigns else ''}\n          title=\"${newcamp_title}\"\n          name=\"new-campaign\"\n          onclick=\"return create_campaign()\">+ new campaign</button>\n    </header>\n    <div class=\"dashboard-content\">\n      <div class=\"new-campaign-container\">\n        ${self.campaign_editor()}\n      </div>\n      ${self.campaign_list()}\n    </div>\n  </div>\n\n  %if c.user_is_sponsor:\n    <div class=\"dashboard admin-dashboard\">\n      <header>\n        <h2>${_(\"admin dashboard\")}</h2>\n      </header>\n      <div class=\"dashboard-content\">\n        ${pr.admin_panel()}\n      </div>\n    </div>\n  %endif\n</div>\n\n${pr.javascript_setup()}\n\n\n<%def name=\"campaign_editor()\">\n  <div class=\"pretty-form campaign-editor editor\" id=\"campaign\"\n       style=\"${'display:none' if thing.campaigns else ''}\">\n    <div class=\"campaign\" method=\"post\" action=\"/api/add_campaign\">\n      <input type=\"hidden\" name=\"link_id36\" value=\"${thing.link._id36}\">\n      <div class=\"editor-group\">\n        ${pr.targeting_field()}\n        ${pr.is_auction_field()}\n        ${pr.platform_field()}\n        ${pr.frequency_cap_field()}\n        ${pr.priority_field()}\n        ${pr.budget_field()}\n        ${pr.scheduling_field()}\n        ${pr.pricing_field()}\n        ${pr.is_new_field()}\n        <%utils:line_field title=\"${_('confirm')}\" css_class=\"rounded confirmation-field\">\n          <div id=\"campaign-creator\">\n          </div>\n          ${utils.error_field(\"COST_BASIS_CANNOT_CHANGE\", \"cost_basis\", \"div\")}\n          ${utils.error_field(\"BAD_BUDGET\", \"total_budget_dollars\", \"div\")}\n          ${utils.error_field(\"BUDGET_LIVE\", \"total_budget_dollars\", \"div\")}\n        </%utils:line_field>\n      </div>\n      <footer class=\"buttons\">\n        <input type=\"hidden\" name=\"campaign_id36\" value=\"\">\n        <input type=\"hidden\" name=\"campaign_name\" value=\"\">\n        <input type=\"hidden\" name=\"id\" value=\"#campaign\">\n        <span class=\"status error\"></span>\n        <button name=\"cancel\"\n                onclick=\"return cancel_edit_campaign()\">\n          ${_(\"cancel\")}\n        </button>\n      </footer>\n    </div>\n  </div>\n</%def>\n\n<%def name=\"campaign_list()\">\n  <%\n    start_title = _(\"Date when your sponsored link will start running.  We start new campaigns at midnight UTC+5\")\n    end_title = _(\"Date when your sponsored link will end (at midnight UTC+5)\")\n    targeting_title = _(\"name of the community that you are targeting. A blank entry here means that the ad is untargeted and will run site-wide \")\n  %>\n  \n      <div class=\"campaign-list\">\n        <div class=\"error TOO_MANY_CAMPAIGNS field-title infotext ${'hidden' if len(thing.campaigns) < g.MAX_CAMPAIGNS_PER_LINK else ''}\"\n             data-MAX=\"${g.MAX_CAMPAIGNS_PER_LINK}\"\n             data-CAMP=\"${len(thing.campaigns)}\">\n          <p>\n            ${_(\"You have too many campaigns for this link.\")}&#32;\n            <a href=\"/promoted/new_promo/\">${_(\"It's time to start fresh!\")}</a>\n          </p>\n        </div>\n        <div class=\"existing-campaigns\"\n             data-max-campaigns=\"${g.MAX_CAMPAIGNS_PER_LINK}\">\n          <table style=\"${'display:none' if not thing.campaigns else ''}\">\n            <thead>\n              <tr class=\"campaign-header-row\">\n                <th class=\"campaign-start-date\" title=\"${start_title}\">${_(\"start\")}</th>\n                <th class=\"campaign-end-date\" title=\"${end_title}\">${_(\"end\")}</th>\n                <th class=\"campaign-priority\" style=\"${'display:none' if not c.user_is_sponsor else ''}\">\n                  ${_(\"priority\")}\n                </th>\n                <th class=\"campaign-total-budget\">${_(\"total budget\")}</th>\n                <th class=\"campaign-spent\">${_(\"spent\")}</th>\n                %if thing.ads_auction_enabled:\n                  <th class=\"campaign-bid\">${_(\"bid\")}</th>\n                %endif\n                <th class=\"campaign-target\" title=\"${targeting_title}\">${_(\"targeting\")}</th>\n                <th class=\"campaign-location\">${_(\"location\")}</th>\n                %if c.user_is_sponsor:\n                  <th class=\"campaign-payment-mismatch\">${_(\"payment mismatch\")}</th>\n                %endif\n                <th class=\"campaign-buttons\" style=\"align:right\"></th>\n              </tr>\n            </thead>\n            <tbody>\n              %for camp in thing.campaigns:\n                ${camp}\n              %endfor\n            </tbody>\n          </table>\n        </div>\n\n        <span class=\"freebie error\"></span>\n      </div>\n    \n</%def>\n\n<%def name=\"creative_editor()\">\n  <div class=\"pretty-form promotelink-editor editor collapsed\" id=\"promo-form\">\n    <input type=\"hidden\" name=\"link_id36\" value=\"${thing.link._id36}\">\n\n    <input name=\"kind\" value=\"${'self' if thing.link.is_self else 'link'}\" type=\"hidden\">\n    \n    <div class=\"collapsed-display\">\n      ${thing.listing}\n\n      <footer class=\"buttons\">\n        <button name=\"edit-promotion\" onclick=\"return edit_promotion()\">edit creative</button>\n      </footer>\n    </div>\n    <div class=\"uncollapsed-display\" style=\"display:none\">\n      <div class=\"editor-group\">\n        ${pr.image_field(thing.link)}\n        <% is_link = not thing.link.is_self %>\n        ${pr.title_field(thing.link)}\n        ${pr.content_field(thing.link, enable_override=c.user_is_sponsor,\n                           tracker_access=c.user_can_track_ads)}\n        ${utils.error_field(\"NO_CHANGE_KIND\", \"kind\", \"div\")}\n        \n        ${pr.commenting_field(thing.link)}\n\n        %if c.user_is_sponsor:\n          ${pr.managed_field(thing.link)}\n          ${pr.media_field(thing.link)}\n        %endif\n      </div>\n      <footer class=\"buttons\">\n        ${utils.error_field(\"RATELIMIT\", \"ratelimit\")}\n        &#32;\n        <span class=\"status error\"></span>\n        ${utils.error_field(\"RATELIMIT\", \"ratelimit\")}\n        <button name=\"cancel\" class=\"btn\" type=\"button\"\n                onclick=\"return cancel_edit_promotion()\">cancel</button>\n        <button name=\"save\" class=\"btn primary-button\" type=\"button\"\n                onclick=\"return post_pseudo_form('#promo-form', 'edit_promo')\">\n          ${_(\"save\")}\n        </button>\n      </footer>\n    </div>\n  </div>\n</%def>"
  },
  {
    "path": "r2/r2/templates/promotelinknew.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n   from r2.lib import js\n%>\n\n<%namespace file=\"promotelinkbase.html\" import=\"title_field, content_field, managed_field, image_field\" />\n<%namespace file=\"utils.html\" import=\"error_field\" />\n<%namespace name=\"utils\" file=\"utils.html\"/>\n\n${unsafe(js.use('sponsored'))}\n\n<div class=\"create-promotion sponsored-page\">\n  <div class=\"dashboard\">\n    <header>\n      <h2>new promotion</h2>\n    </header>\n    <div class=\"dashboard-content\">\n      <div class=\"pretty-form promotelink-editor editor\" id=\"promo-form\">\n        ## need to set the modhash because we're not using a helper method to post the form\n        <input type=\"hidden\" name=\"uh\" value=\"${c.modhash}\">\n        <input type=\"hidden\" name=\"id\" value=\"#promo-form\">\n        <div class=\"editor-group\">\n          ${image_field(images=thing.images)}\n          %if c.user_is_sponsor:\n            ${username_field()}\n            ${managed_field(None)}\n          %endif\n          ${title_field(None)}\n          ${content_field(None, enable_override=c.user_is_sponsor,\n                          tracker_access=c.user_can_track_ads)}\n          <footer class=\"buttons\">\n            <div class=\"rules\">\n              By clicking \"next\" you agree to the&#32;<a href=\"http://www.reddit.com/wiki/selfserve#wiki_online_self_serve_advertising_rules\" target=\"_blank\">Self Serve Advertising Rules.</a>\n            </div>\n            ${error_field(\"RATELIMIT\", \"ratelimit\")}\n            &#32;\n            <span class=\"status error\"></span>\n            ${error_field(\"RATELIMIT\", \"ratelimit\")}\n            <button\n                name=\"create\" class=\"btn primary-button\" type=\"button\"\n                onclick=\"return post_pseudo_form('#promo-form', 'create_promo')\">\n              ${_(\"next\")}\n            </button>\n          </footer>\n        </div>\n      </form>\n      <iframe src=\"about:blank\" width=\"600\" height=\"200\" \n              style=\"display: none;\"\n              name=\"upload-iframe\" id=\"upload-iframe\"></iframe>\n    </div>\n  </div>\n</div>\n\n<%def name=\"username_field()\">\n  <%utils:line_field title=\"${_('create as user')}\" id=\"username-field\" css_class=\"rounded\">\n    <input id=\"username\" name=\"username\" type=\"text\" \n           class=\"rounded\">\n    ${error_field(\"NO_EMAIL_FOR_USER\", \"username\", \"div\")}\n    ${error_field(\"NO_VERIFIED_EMAIL\", \"username\", \"div\")}\n    ${error_field(\"USER_DOESNT_EXIST\", \"username\", \"div\")}\n    <div class=\"infotext rounded\">\n      <p>${_(\"Create a promotion on another user's account.\")}</p>\n    </div>\n  </%utils:line_field>\n</%def>\n\n<script type=\"text/javascript\">\n  $(function() {\n    r.sponsored.initUploads();\n  });\n</script>\n"
  },
  {
    "path": "r2/r2/templates/promotereport.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n\n<%!\n   from r2.lib import js\n   from r2.lib.template_helpers import format_number\n%>\n\n<%namespace name=\"pr\" file=\"promotelinkbase.html\" />\n<%namespace file=\"utils.html\" import=\"plain_link\" />\n<%namespace name=\"utils\" file=\"utils.html\"/>\n\n${unsafe(js.use('sponsored'))}\n\n<div class=\"sponsored-page\">\n  <div class=\"dashboard reporting-dashboard\">\n    <header>\n      <h2>sponsored link report</h2>\n      <p class=\"note\">\n        note: the end date is not included, so selecting 1/2/2013-1/3/2013 will retrieve traffic for the day of 1/2/2013 only.\n      </p>\n    </header>\n    <div class=\"dashboard-content\">\n      <div class=\"editor\">\n        <div class=\"editor-group\">\n          ${pr.scheduling_field()}\n          <%utils:line_field title=\"${_('grouping')}\" css_class=\"rounded timing-field\">\n            <select id=\"grouping\" name=\"grouping\">\n              <option value=\"total\" ${\"\" if thing.group_by_date else \"selected='selected'\"}>total</option>\n              <option value=\"day\" ${\"selected='selected'\" if thing.group_by_date else \"\"}>daily</option>\n            </select>\n          </%utils:line_field>\n          ${pr.reporting_field(link_text=thing.link_text, owner=thing.owner_name)}\n        </div>\n        <footer class=\"buttons\">\n          <button name=\"submit\" onclick=\"r.sponsored.submit_reporting_form()\">submit</button>\n        </footer>\n      </div>\n    </div>\n  </div>\n</div>\n<script type=\"text/javascript\">\n  r.sponsored.set_form_render_fnc(r.sponsored.fill_reporting_form);\n  $(function() {\n    init_startdate();\n    init_enddate();\n    r.sponsored.render();\n  });\n</script>\n\n%if thing.link_report:\n<h1>links</h1>\n<table id=\"link-report\" class=\"promote-report-table\">\n  <thead>\n    <tr>\n      %if thing.group_by_date:\n        <th>date</th>\n      %endif\n      <th>id</th>\n      <th>owner</th>\n      <th>comments</th>\n      <th>upvotes</th>\n      <th>downvotes</th>\n      <th>clicks</th>\n      <th>impressions</th>\n    </tr>\n  </thead>\n  <tbody>\n    %for row in thing.link_report:\n    <tr>\n      %if thing.group_by_date:\n        <td class=\"text\">${row['date']}</d>\n      %endif\n      <td class=\"text\">${row['id36']}</td>\n      <td class=\"text\">${row['owner']}</td>\n      <td>${format_number(row['comments'])}</td>\n      <td>${format_number(row['upvotes'])}</td>\n      <td>${format_number(row['downvotes'])}</td>\n      <td>${format_number(row['clicks'])}</td>\n      <td>${format_number(row['impressions'])}</td>\n    </tr>\n    %endfor\n  </tbody>\n</table>\n%endif\n\n%if thing.campaign_report:\n<h1>campaigns</h1>\n<table id=\"campaign-report\" class=\"promote-report-table\">\n  <thead>\n    <tr>\n      %if thing.group_by_date:\n        <th class=\"blank\"/>\n      %endif\n      <th class=\"blank\"/>\n      <th class=\"blank\"/>\n      <th class=\"blank\"/>\n      <th class=\"blank\"/>\n      <th class=\"blank\"/>\n      <th colspan=\"2\">frontpage</th>\n      <th colspan=\"2\">subreddit</th>\n      <th colspan=\"2\">total</th>\n    </tr>\n    <tr>\n      %if thing.group_by_date:\n        <th>date</th>\n      %endif\n      <th>link id</th>\n      <th>owner</th>\n      <th>campaign id</th>\n      <th>target</th>\n      <th>bid</th>\n      <th>clicks</th>\n      <th>impressions</th>\n      <th>clicks</th>\n      <th>impressions</th>\n      <th>clicks</th>\n      <th>impressions</th>\n    </tr>\n  </thead>\n  <tbody>\n    %for row in thing.campaign_report:\n    <tr>\n      %if thing.group_by_date:\n        <td class=\"text\">${row['date']}</td>\n      %endif\n      <td class=\"text\">${row['link']}</td>\n      <td class=\"text\">${row['owner']}</td>\n      <td class=\"text\">${row['campaign']}</td>\n      <td class=\"text\">${row['target']}</td>\n      <td>${row['bid']}</td>\n      <td>${format_number(row['fp_clicks'])}</td>\n      <td>${format_number(row['fp_impressions'])}</td>\n      <td>${format_number(row['sr_clicks'])}</td>\n      <td>${format_number(row['sr_impressions'])}</td>\n      <td>${format_number(row['total_clicks'])}</td>\n      <td>${format_number(row['total_impressions'])}</td>\n    </tr>\n    %endfor\n    <tr class=\"total\">\n      %if thing.group_by_date:\n        <td></td>\n      %endif\n      <td></td>\n      <td></td>\n      <td></td>\n      <td></td>\n      <td>${thing.campaign_report_totals['bid']}</td>\n      <td>${format_number(thing.campaign_report_totals['fp_clicks'])}</td>\n      <td>${format_number(thing.campaign_report_totals['fp_imps'])}</td>\n      <td>${format_number(thing.campaign_report_totals['sr_clicks'])}</td>\n      <td>${format_number(thing.campaign_report_totals['sr_imps'])}</td>\n      <td>${format_number(thing.campaign_report_totals['total_clicks'])}</td>\n      <td>${format_number(thing.campaign_report_totals['total_imps'])}</td>\n    </tr>\n  </tbody>\n</table>\n%endif\n\n%if thing.csv_url:\n<div class=\"promote-report-csv\">\n  ${plain_link(unsafe(\"download as csv\"), thing.csv_url)}\n</div>\n%endif\n"
  },
  {
    "path": "r2/r2/templates/quarantineinterstitial.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n  from r2.lib.template_helpers import static \n%>\n\n<%namespace file=\"utils.html\" import=\"submit_form, _md, _mdf\"/>\n<%namespace name=\"utils\" file=\"utils.html\"/>\n\n<%inherit file=\"interstitial.html\"/>\n\n<%def name=\"interstitial_image_attrs()\">\n  src=\"${static('interstitial-image-quarantine.png')}\"\n  alt=\"${_('quarantined')}\"\n</%def>\n\n<%def name=\"interstitial_title()\">\n  %if thing.can_opt_in:\n    ${_(\"Are you sure you want to view this community?\")}\n  %elif thing.logged_in:\n    ${_md(\"You must have a [verified email](/prefs/update) to view this community\")}\n  %else:\n    ${_(\"You must log in and have a verified email to view this community\")}\n  %endif\n</%def>\n\n<%def name=\"interstitial_message()\">\n  <p>\n    <%\n      quarantine_link = 'https://reddit.zendesk.com/hc/en-us/articles/205701245'\n    %>\n\n    %if thing.can_opt_in:\n      ${_mdf(\"Communities that are dedicated to shocking or highly offensive content are [quarantined](%(link)s). Content in this community may be upsetting. Are you certain you want to continue?\", link=quarantine_link)}\n    %else:\n      ${_mdf(\"Communities that are dedicated to shocking or highly offensive content are [quarantined](%(link)s).\", link=quarantine_link)}\n    %endif\n  </p>\n</%def>\n\n<%def name=\"interstitial_buttons()\">\n  %if thing.can_opt_in:\n    <%utils:submit_form _class=\"pretty-form\">\n      <input type=\"hidden\" name=\"sr_name\" value=\"${thing.sr_name}\" />\n\n      <div class=\"buttons\">\n        <button class=\"c-btn c-btn-primary\" type=\"submit\" name=\"accept\" value=\"no\">\n          ${_(\"no thank you\")}\n        </button>\n        <button class=\"c-btn c-btn-primary\" type=\"submit\" name=\"accept\" value=\"yes\">\n          ${_(\"continue\")}\n        </button>\n      </div>\n    </%utils:submit_form>\n  %else:\n    ${parent.interstitial_buttons()}\n  %endif\n</%def>\n"
  },
  {
    "path": "r2/r2/templates/ratelimit_base.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<!doctype html>\n<html>\n  <head>\n    <title>Too Many Requests</title>\n    <style>\n      body {\n          font: small verdana, arial, helvetica, sans-serif;\n          width: 600px;\n          margin: 0 auto;\n      }\n\n      h1 {\n          height: 40px;\n          background: transparent url(${logo_url}) no-repeat scroll top right;\n      }\n    </style>\n  </head>\n  <body>\n    <h1>whoa there, pardner!</h1>\n    ${self.body()}\n    <p>as a reminder to developers, we recommend that clients make no\n    more than <a href=\"http://github.com/reddit/reddit/wiki/API\">one\n    request every two seconds</a> to avoid seeing this message.</p>\n  </body>\n</html>\n"
  },
  {
    "path": "r2/r2/templates/ratelimit_throttled.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%inherit file=\"ratelimit_base.html\"/>\n\n<p>reddit's awesome and all, but you may have a bit of a\nproblem. we've seen far too many requests come from your ip address\nrecently.</p>\n\n<p>if you think that we've incorrectly blocked you or you would like to discuss\neasier ways to get the data you want, please contact us at <a\nhref=\"mailto:ratelimit@reddit.com\">ratelimit@reddit.com</a>.</p>\n\n<p>when contacting us, please include your ip address which is: <strong>${request.ip}</strong></p>\n"
  },
  {
    "path": "r2/r2/templates/ratelimit_toofast.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%inherit file=\"ratelimit_base.html\"/>\n\n<p>we're sorry, but you appear to be a bot and we've seen too many requests\nfrom you lately. we enforce a hard speed limit on requests that appear to come\nfrom bots to prevent abuse.</p>\n\n<p>if you are not a bot but are spoofing one via your browser's user agent\nstring: please change your user agent string to avoid seeing this message\nagain.</p>\n\n<p>please wait ${retry_after} second(s) and try again.</p>\n"
  },
  {
    "path": "r2/r2/templates/rawcode.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n    from r2.lib.filters import SC_OFF, SC_ON, unsafe\n%>\n\n<pre>\n  <code>${unsafe(SC_OFF)}${thing.code}${unsafe(SC_ON)}</code>\n</pre>\n"
  },
  {
    "path": "r2/r2/templates/readnext.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%namespace file=\"utils.html\" import=\"plain_link\"/>\n\n<div class=\"read-next-container\">\n  <aside class=\"read-next\">\n    <header class=\"read-next-header\">\n      <div class=\"read-next-header-title\">\n        ${_(\"discussions in\")}&#32;\n        ${plain_link(\n          '/r/%s' % thing.sr.name,\n          '%s?ref=readnext' % thing.sr.path,\n          _sr_path=False,\n          )}\n      </div>\n      <nav class=\"read-next-nav read-next-nav-left\">\n        <span class=\"read-next-button prev\">&lt;</span>\n        <span class=\"read-next-button next\">&gt;</span>\n      </nav>\n      <div class=\"read-next-nav read-next-nav-right\">\n        <span class=\"read-next-dismiss\">X</span>\n      </div>\n    </header>\n    <div class=\"read-next-list\">\n      ${unsafe(thing.links)}\n    </div>\n  </aside>\n</div>\n"
  },
  {
    "path": "r2/r2/templates/readnextlink.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%namespace file=\"utils.html\" import=\"thumbnail_img, nsfw_stamp\" />\n\n<a id=\"read-next-link-${thing._fullname}\" class=\"read-next-link may-blank\" href=\"${thing.permalink}?ref=readnext\">\n  <div class=\"read-next-meta\">\n    %if not thing.hide_score:\n      <span class=\"score\">${thing.score} points</span>&#32;\n    %endif\n    %if thing.num_comments:\n      &middot;&#32;\n      <span class=\"comments\">${thing.comment_label}</span>&#32;\n    %endif\n  </div>\n  %if thing.thumbnail:\n    ${self.thumbnail()}\n  %endif\n  <div class=\"read-next-title\">\n    %if thing.over_18:\n      <span class=\"nsfw-stamp stamp\">${nsfw_stamp()}</span>&#32;\n    %endif\n    ${thing.title}\n  </div>\n</a>\n\n<%def name=\"thumbnail()\">\n  %if thing.thumbnail and not thing.thumbnail_sprited:\n    <div class=\"read-next-thumbnail\">\n      ${thumbnail_img(thing)}\n    </div>\n  %endif\n</%def>\n"
  },
  {
    "path": "r2/r2/templates/readnextlisting.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<div class=\"listing read-next-listing\">\n  <div class=\"listing-contents\">\n    %for a in thing.things:\n      ${a}\n    %endfor\n  </div>\n</div>\n"
  },
  {
    "path": "r2/r2/templates/reddit.compact",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n   from r2.lib import tracking\n %>\n<%inherit file=\"base.compact\" />\n<%namespace file=\"login.html\" import=\"login_form\"/>\n<%namespace file=\"utils.html\" import=\"tags\"/>\n\n<%def name=\"bodyContent()\">\n  <%include file=\"redditheader.compact\"/>\n  %if thing.content:\n    <div class=\"content\">\n      ${thing.content()}\n    </div>\n  %endif\n  %if g.tracker_url and thing.site_tracking and not c.secure:\n    <img alt=\"\" src=\"${tracking.get_pageview_pixel_url()}\"/>\n  %endif\n</%def>\n"
  },
  {
    "path": "r2/r2/templates/reddit.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%! \n   from r2.config import feature\n   from r2.lib.template_helpers import add_sr, static, join_urls, class_dict, get_domain\n   from r2.lib.filters import unsafe, scriptsafe_dumps\n   from r2.lib.pages import (\n      SearchForm,\n      ClickGadget,\n      SideContentBox,\n      Login,\n      ListingChooser,\n      InTimeoutInterstitial,\n   )\n   from r2.lib import tracking\n   from pylons import request\n   from r2.lib.strings import strings\n   from r2.models import make_feedurl\n%>\n<%namespace file=\"less.html\" import=\"less_js, less_stylesheet\"/>\n<%namespace file=\"utils.html\" import=\"tags, classes\"/>\n<%namespace file=\"adminbar.html\" import=\"adminbar_stylesheet\"/>\n<%inherit file=\"base.html\"/>\n\n<%def name=\"Title()\">\n  %if thing.title:\n    ${thing.title}\n  %else:\n    ${parent.Title()}\n  %endif\n</%def>\n\n<%def name=\"global_stylesheets()\">\n  ${less_stylesheet(\"reddit.less\")}\n  %if getattr(thing, \"feature_new_expando_icons\", False):\n    ${less_stylesheet(\"expando.less\")}\n  %endif\n</%def>\n\n<%def name=\"sr_stylesheets()\">\n  %if thing.subreddit_stylesheet_url:\n    <!--[if gte IE 8]> <!-->\n    <link rel=\"stylesheet\"\n          href=\"${thing.subreddit_stylesheet_url}\"\n          title=\"applied_subreddit_stylesheet\"\n          type=\"text/css\">\n    <!-- <![endif]-->\n  %endif\n</%def>\n\n<%def name=\"extra_stylesheets()\">\n  ${adminbar_stylesheet()}\n  %for extra_stylesheet in getattr(thing, 'extra_stylesheets', ()):\n    ${less_stylesheet(extra_stylesheet)}\n  %endfor\n</%def>\n\n<%def name=\"pagemeta()\">\n  %if hasattr(thing, \"shortlink\"):\n    <link rel=\"shorturl\" href=\"https://${thing.shortlink}\"/>\n  %endif\n\n  %if hasattr(thing, \"og_data\"):\n    %for (og_property, og_value) in thing.og_data.iteritems():\n      <meta property=\"og:${og_property}\" content=\"${og_value}\">\n    %endfor\n  %endif\n\n  %if hasattr(thing, \"twitter_card\"):\n    %for (twitter_card_property, twitter_card_value) in thing.twitter_card.iteritems():\n      <meta property=\"twitter:${twitter_card_property}\" content=\"${twitter_card_value}\">\n    %endfor\n  %endif\n\n  <link rel='icon' href=\"${static('icon.png')}\" sizes=\"256x256\" type=\"image/png\" />\n  <link rel='shortcut icon' href=\"${static('favicon.ico')}\" type=\"image/x-icon\" />\n  <link rel='apple-touch-icon-precomposed' href=\"${static('icon-touch.png')}\" />\n  %if thing.extension_handling:\n    <%\n       rss = add_sr(join_urls(request.path,'.rss'))\n       if thing.extension_handling == \"private\":\n          rss = make_feedurl(c.user, rss)\n     %>\n    <link rel=\"alternate\" type=\"application/atom+xml\" title=\"RSS\"\n          href=\"${rss}\" />\n  %endif\n</%def>\n\n<%def name=\"stylesheet()\">\n  ${self.global_stylesheets()}\n  ${self.sr_stylesheets()}\n  ${self.extra_stylesheets()}\n</%def>\n\n<%def name=\"javascript()\">\n  <% from r2.lib import js %>\n  <!--[if gte IE 9]> <!-->\n    ${unsafe(js.use('reddit-init'))}\n  <!-- <![endif]-->\n\n  <!--[if lt IE 9]>\n    ${unsafe(js.use('reddit-init-legacy'))}\n  <![endif]-->\n  ${less_js()}\n</%def>\n\n<%def name=\"javascript_bottom()\">\n  <% from r2.lib import js %>\n  ${unsafe(js.use('reddit'))}\n  %if getattr(thing, \"feature_expando_nsfw_flow\", False):\n    ${unsafe(js.use(\"expando-nsfw-flow\"))}\n  %endif\n  ${unsafe(c.js_preload.use())}\n</%def>\n\n<%def name=\"bodyContent()\">\n  %if thing.header:\n    <%include file=\"adminbar.html\"/>\n    <%include file=\"redditheader.html\"/>\n  %endif\n\n  %if thing.show_sidebar:\n    <div class=\"side\">\n      ${sidebar(content = thing.rightbox())}\n    </div>\n  %endif\n\n  %if thing.show_chooser:\n    ${ListingChooser()}\n  %endif\n\n  %if thing.auction_announcement:\n    <div id=\"auction-announcement-container\">\n      <div id=\"auction-announcement\">\n        <h1>\n          ad auctions are launching monday 2/8\n        </h1>\n        <p>\n          for more details, see our&nbsp;<a href=\"https://www.reddit.com/r/selfserve/comments/434c0x/the_auction_is_coming/\" target=\"_blank\">announcement in /r/selfserve</a>\n        </p>\n      </div>\n    </div>\n  %endif\n\n  <% content = getattr(self, \"content\", None) or thing.content %>\n  %if content:\n    ##<div class=\"fixedwidth\"></div>\n    ##<div class=\"clearleft\"></div>\n    <a name=\"content\"></a>\n    <div ${tags(id=thing.content_id)} ${classes(\"content\", thing.css_class)} role=\"main\">\n      ${content()}\n    </div>\n  %endif\n\n  ${thing.footer}\n\n  %if not c.user_is_loggedin and not g.read_only_mode:\n    %if thing.enable_login_cover:\n      <script>\n        var BETA_HOST = 'beta.reddit.com';\n        if (location.host === BETA_HOST) {\n          r.config.https_endpoint = 'https://' + BETA_HOST;\n        }\n      </script>\n      <script id=\"login-popup\" type=\"text/template\">\n        ${Login(is_popup=True)}\n      </script>\n    %endif\n    <script id=\"lang-popup\" type=\"text/template\">\n      <%include file=\"prefoptions.html\" />\n    </script>\n  %endif\n  % if c.secure:\n      ## Pixel to pick up HSTS policies from the base domain\n      <img id=\"hsts_pixel\" src=\"//${g.domain}/static/pixel.png\">\n  % endif\n  %if feature.is_enabled(\"test_https_certs\"):\n    <% from r2.lib import js %>\n    ${unsafe(js.use(\"https-tester\"))}\n    <%\n    cur_proto = (\"https:\" if c.secure else \"http:\")\n    https_test_config = {\n        \"runName\": g.live_config.get(\"https_cert_testing_run_name\"),\n        \"controlImg\": cur_proto + g.live_config.get(\"https_cert_testing_img_control\"),\n        \"testImg\": g.live_config.get(\"https_cert_testing_img_test\"),\n        \"logPixel\": cur_proto + g.httpstracker_url,\n    }\n    %>\n    <script type=\"text/javascript\">\n    if(Math.random() < ${scriptsafe_dumps(g.live_config.get(\"https_cert_testing_probability\"))}) {\n        runHTTPSCertTest(${scriptsafe_dumps(https_test_config)});\n    }\n    </script>\n  %endif\n  ${thing.debug_footer}\n</%def>\n\n<%def name=\"sidebar(content=None)\">\n  ${content}\n</%def>\n"
  },
  {
    "path": "r2/r2/templates/reddit.htmllite",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%inherit file=\"base.htmllite\"/>\n\n${thing.content and thing.content() or ''}\n"
  },
  {
    "path": "r2/r2/templates/reddit.mobile",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n   from r2.lib import tracking\n %>\n<%inherit file=\"base.mobile\"/>\n\n<%include file=\"redditheader.mobile\"/>\n\n\n%if c.suggest_compact:\n  <script type=\"text/javascript\">\n     window.location = \"${g.default_scheme}://www.${g.domain}/try.compact?dest=/.mobile\";\n  </script>\n%endif\n\n${thing.content and thing.content() or ''}\n%if g.tracker_url and thing.site_tracking:\n  <img alt=\"\" src=\"${tracking.get_pageview_pixel_url()}\"/>\n%endif\n\n<%def name=\"Title()\">\n  %if thing.title:\n    ${thing.title}\n  %else:\n    ${parent.Title()}\n  %endif\n</%def>\n\n<%def name=\"stylesheet()\">\n  <% from r2.lib.template_helpers import static %>\n  <link rel=\"stylesheet\" href=\"${static('mobile.css')}\" type=\"text/css\" />\n  <link rel='shortcut icon' href=\"${static('favicon.ico')}\" type=\"image/x-icon\" />\n\n  <link rel=\"apple-touch-icon\" href=\"/static/compact/reddit-apple-mobile-device.png\">\n  <link rel=\"apple-touch-startup-image\" href=\"/static/compact/reddit_startimg.png\">\n\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no\" />\n\n</%def>\n\n"
  },
  {
    "path": "r2/r2/templates/reddit.xml",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n<%inherit file=\"base.xml\"/>\n${thing.content()}\n"
  },
  {
    "path": "r2/r2/templates/redditfooter.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n   from r2.lib.strings import strings\n   import datetime\n%>\n<%namespace file=\"utils.html\" import=\"text_with_links\"/>\n\n<div class=\"footer-parent\">\n  <div by-zero class=\"footer rounded\">\n      %for toolbar in thing.nav:\n      <div class=\"col\">\n        ${toolbar}\n      </div>\n      %endfor\n  </div>\n  %if g.domain != \"reddit.com\":\n    <!-- http://code.reddit.com/LICENSE see Exhibit B -->\n    <a href=\"https://www.reddit.com/code/\" style=\"text-align:center;display:block\">\n      <img src=\"https://s3.amazonaws.com/sp.reddit.com/powered_by_reddit.png\"\n           alt=\"Powered by reddit.\"\n           style=\"width:140px; height:47px; margin-bottom: 5px\"/>\n    </a>\n  %endif\n  <p class=\"bottommenu\">\n    ${text_with_links(\n            _(\"Use of this site constitutes acceptance of our \"\n              \"%(user_agreement)s and %(privacy_policy)s\"),\n            user_agreement=dict(\n                link_text=_(\"User Agreement {Genitive}\"),\n                path=\"/help/useragreement\",\n            ),\n            privacy_policy=dict(\n                link_text=_(\"Privacy Policy (updated)\"),\n                path=\"/help/privacypolicy\",\n                _class=\"updated\",\n            ),\n    )}.\n    &copy; ${_(\"%(year)d reddit inc. All rights reserved.\") % \\\n    dict(year=datetime.datetime.now().timetuple()[0])}\n  </p>\n  <p class=\"bottommenu\">REDDIT and the ALIEN Logo are registered trademarks of reddit inc.</p>\n</div>\n"
  },
  {
    "path": "r2/r2/templates/redditheader.compact",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n   from r2.lib.template_helpers import display_link_karma, header_url\n   from r2.models.subreddit import DefaultSR, FakeSubreddit\n   \n %>\n<%namespace file=\"utils.html\" import=\"plain_link, img_link, separator, logout\"/>\n\n<%\n    from r2.lib.menus import PageNameNav\n    toolbars = thing.toolbars\n    nav = \"mobile\"\n    if toolbars and isinstance(toolbars[0], PageNameNav):\n        nav = toolbars[0]\n        toolbars = toolbars[1:]\n    nav = thing.short_title or nav\n %>\n<div id=\"preload\">\n    <div class=\"commentcount\"><div class=\"comments\"></div><div class=\"comments preloaded\"></div></div>\n</div>\n<div id=\"topbar\">\n  <div class=\"left\">\n   <%\n        d = DefaultSR()\n        if c.site.header and c.allow_styles and not c.site.quarantine:\n            header_img = header_url(c.site.header)\n            header_size = c.site.header_size\n            header_title = c.site.header_title\n        else:\n            header_img = header_url(d.header)\n            header_size = None\n            header_title = d.header_title\n    %>\n    ${img_link(c.site.name, header_img, '/', _id = \"header-img-a\", img_id = 'header-img', title = header_title, size = header_size)}\n  </div>\n  <h1>${nav}</h1>\n  <div class=\"right\">\n    %if c.user_is_loggedin:\n      <%\n         if c.have_messages:\n           mail_img_class = 'havemail'\n           mail_path = '/message/unread/'\n         else:\n           mail_img_class = 'nohavemail'\n           mail_path = '/message/inbox/'\n      %>\n      ${plain_link('', mail_path, _sr_path=False, _class=mail_img_class, _id='mail')}\n      %if c.user_is_loggedin and c.user.is_moderator_somewhere:\n        <%\n            mail_path = '/message/moderator/'\n            if c.have_mod_messages:\n                mail_img_class = 'havemail'\n            else:\n                mail_img_class = 'nohavemail'\n        %>\n        ${plain_link('', mail_path, _sr_path=False, _class=mail_img_class, _id='modmail')}\n      %endif\n    %endif\n    <a class=\"topbar-options\" href=\"#\" id=\"topmenu_toggle\"></a>\n  </div>\n  <div id=\"top_menu\">\n    %if c.user_is_loggedin:\n    <div class=\"menuitem\">\n      ${plain_link(c.user.name, \"/user/%s/\" % c.user.name, _sr_path=False)}\n      &nbsp;(<b>${display_link_karma(c.user.link_karma)}</b>)\n    </div>\n    <div class=\"menuitem\">\n      ${plain_link(\"subreddits\", \"/subreddits/mine\", _sr_path=False)}\n    </div>\n    <div class=\"menuitem\">\n      ${plain_link(\"submit\", \"/submit\", _sr_path=False)}\n    </div>\n    %if g.auth_provider.is_logout_allowed():\n    <div class=\"menuitem bottm-bar\">\n      ${logout(dest=request.fullpath)}\n    </div>\n    % endif\n   %else:\n    <div class=\"menuitem\">\n      ${plain_link(\"log in\", \"/login\", _sr_path=False)}\n    </div>\n    <div class=\"menuitem bottm-bar\">\n      ${plain_link(\"sign up\", \"/register\", _sr_path=False)}\n    </div>\n    <div class=\"menuitem\">\n      ${plain_link(\"subreddits\", \"/subreddits/\", _sr_path=False)}\n    </div>\n   %endif\n    <div class=\"menuitem\">\n      ${plain_link(\"search\", \"/search\")}\n    </div>\n    %if not isinstance(c.site, FakeSubreddit):\n    <div class=\"menuitem\">\n      ${plain_link(\"sidebar\", \"/about/sidebar\")}\n    </div>\n    %endif\n  </div>\n</div>\n%if toolbars:\n  <div class=\"subtoolbar\">\n    %for toolbar in toolbars:\n      ${toolbar}\n    %endfor\n  </div>\n%endif\n"
  },
  {
    "path": "r2/r2/templates/redditheader.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n   from pylons import request\n   from r2.config import feature\n   from r2.lib.template_helpers import (\n       add_sr,\n       display_link_karma,\n       format_number,\n       header_url,\n   )\n   from r2.models import FakeSubreddit\n   from r2.models.subreddit import DefaultSR\n   from r2.lib.pages import SearchForm\n%>\n<%namespace file=\"utils.html\" import=\"plain_link, img_link, text_with_links, separator, logout\"/>\n\n<div id=\"header\" role=\"banner\">\n  <a tabindex=\"1\" href=\"#content\" id=\"jumpToContent\">${_('jump to content')}</a>\n  ${thing.srtopbar}\n  <div id=\"header-bottom-left\">\n    <%\n        header_title = c.site.header_title\n        d = thing.default_theme_sr\n\n        if c.site.header and c.can_apply_styles and c.allow_styles and not (thing.no_sr_styles or c.site.quarantine):\n            header_img = c.site.header\n            header_size = c.site.header_size\n        else:\n            header_img = d.header\n            header_size = d.header_size\n            header_title = d.header_title\n    %>\n    \n    % if header_img != g.default_header_url:\n        ${img_link(c.site.name, header_url(header_img),\n            '/', _id=\"header-img-a\", img_id='header-img',\n            title=header_title, size=header_size)}\n    % else:\n        <a href=\"/\" id=\"header-img\" class=\"default-header\" title=\"${header_title}\">${g.domain}</a>\n    % endif\n    \n    ##keeps the height of the header from varying when there isnt any content\n    &nbsp;\n\n    %for toolbar in thing.toolbars:\n      ${toolbar}\n    %endfor\n  </div>\n\n  <div id=\"header-bottom-right\">\n    %if not c.user_is_loggedin:\n      %if thing.enable_login_cover and not g.read_only_mode:\n      <span class=\"user\">\n        <%\n          base = g.https_endpoint\n          login_url = add_sr(base + \"/login\", sr_path=False)\n        %>\n        ${text_with_links(_(\"Want to join? %(login_or_register)s in seconds.\"),\n            login_or_register = dict(link_text=_(\"Log in or sign up\"), path=login_url, _class=\"login-required\"))}\n      </span>\n      ${separator(\"|\")}\n      %endif\n    %else:\n      %if feature.is_enabled('beta_opt_in') and c.user.pref_beta:\n        <div class=\"beta-hint help help-hoverable\">\n          <a class=\"beta-link\" href=\"/r/${g.beta_sr}\">beta</a>\n          <div id=\"beta-help\" class=\"hover-bubble help-bubble anchor-top\">\n            <div class=\"help-section\">\n              <p>${_(\"You're in beta mode! Thanks for helping to test reddit.\")}</p>\n              <p>\n              ${text_with_links(_(\"Please give feedback at %(beta_link)s, or %(learn_more_link)s.\"),\n                  beta_link = dict(link_text=\"/r/\" + g.beta_sr, path=\"/r/\" + g.beta_sr),\n                  learn_more_link = dict(link_text=_(\"learn more on the wiki\"), path=\"/r/\" + g.beta_sr + \"/wiki\")\n                )}\n              </p>\n            </div>\n          </div>\n        </div>\n      %endif\n      <span class=\"user\">\n         ${plain_link(c.user.name, \"/user/%s/\" % c.user.name, _sr_path=False)}\n         &nbsp;(<span class=\"userkarma\" title=\"${_(\"post karma\")}\">${format_number(display_link_karma(c.user.link_karma))}</span>)\n      </span>\n\n      ${separator(\"|\")}\n\n\n      %if c.have_messages:\n        ${plain_link(_(\"messages\"), path=\"/message/unread/\", title=_(\"new mail!\"), _class=\"havemail\", _sr_path=False, _id=\"mail\")}\n        %if c.user.inbox_count > 0:\n          ${plain_link(c.user.inbox_count, path=\"/message/unread/\", _class=\"message-count\", _sr_path=False)}\n        %endif\n      %else:\n        ${plain_link(_(\"messages\"), path=\"/message/inbox/\", title=_(\"no new mail\"), _class=\"nohavemail\", _sr_path=False, _id=\"mail\")}\n      %endif\n      ${separator(\"|\")}\n      %if c.user_is_loggedin and c.user.is_moderator_somewhere:\n         <%\n            if c.have_mod_messages:\n              mail_img_class = 'havemail'\n              mail_img_title = \"new mod mail!\"\n              mail_path = \"/message/moderator/\"\n            else:\n              mail_img_class = 'nohavemail'\n              mail_img_title = \"no new mod mail\"\n              mail_path = \"/message/moderator/\"\n            \n            css_class = \"%s access-required\" % mail_img_class\n            data_attrs = {'event-action': 'pageview', 'event-detail': 'modmail'}\n          %>\n         ${plain_link(_(\"mod messages\"), path=mail_path,\n                    title = mail_img_title, _sr_path = False,\n                    _id = \"modmail\", _class=css_class, data=data_attrs)}\n         ${separator(\"|\")}\n      %endif\n    %endif\n    ${thing.corner_buttons()}\n    %if c.user_is_loggedin and g.auth_provider.is_logout_allowed():\n      ${separator(\"|\")}\n      ${logout(dest=request.fullpath)}\n    %endif\n  </div>\n</div>\n"
  },
  {
    "path": "r2/r2/templates/redditheader.mobile",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n   from r2.lib.template_helpers import static\n   from r2.models import FakeSubreddit\n%>\n<%namespace file=\"utils.html\" import=\"plain_link, img_link, separator\"/>\n\n<div id=\"header\">\n    <a href=\"/.mobile\"><img id=\"header-img\" src=\"${static('littlehead.png')}\" alt=\"${c.site.name}\" /></a><a href=\"/subreddits.mobile\" class=\"or\">other subreddits</a>\n  %for toolbar in thing.toolbars:\n    ${toolbar}\n  %endfor\n</div>\n"
  },
  {
    "path": "r2/r2/templates/redditinfobar.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n   from r2.lib.filters import safemarkdown\n%>\n\n<div class=\"reddit-infobar md-container-small ${'with-icon' if thing.show_icon else ''} ${thing.extra_class}\">\n  ${unsafe(safemarkdown(thing.message))}\n</div>\n"
  },
  {
    "path": "r2/r2/templates/reddittraffic.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n    import babel.dates\n\n    from r2.lib import js\n    from r2.lib.strings import strings\n    from r2.lib.template_helpers import static, js_timestamp, format_number\n\n    import babel.dates\n%>\n\n<%namespace file=\"utils.html\" import=\"_md\"/>\n\n<%def name=\"load_timeseries_js()\">\n  <!--[if lte IE 8]>\n  ${unsafe(js.use('timeseries-ie'))}\n  <![endif]-->\n  <!--[if !(lte IE 8)]><!-->\n  ${unsafe(js.use('timeseries'))}\n  <!--<![endif]-->\n</%def>\n\n<div class=\"md-container wiki-page-content\">\n  % if thing.place:\n    ${_md((\"# traffic statistics for %(place)s\") % dict(place=thing.place), wrap=True)}\n  % endif\n\n  ${self.preamble()}\n\n  ${self.last_modified_message()}\n\n  ${self.top_content()}\n</div>\n\n<%def name=\"last_modified_message()\">\n    <p id=\"timeseries-unprocessed\" data-last-processed=\"${js_timestamp(thing.traffic_last_modified)}\"\n    % if thing.traffic_lag.total_seconds() > 10800:\n      class=\"slow\">\n      ${strings.traffic_processing_slow % dict(date=babel.dates.format_datetime(thing.traffic_last_modified, format=\"long\", locale=c.locale))}\n    % else:\n      >\n      ${strings.traffic_processing_normal % dict(date=babel.dates.format_datetime(thing.traffic_last_modified, format=\"long\", locale=c.locale))}\n    % endif\n    </p>\n</%def>\n\n<%def name=\"top_content()\"></%def>\n\n<div id=\"charts\"></div>\n\n<div class=\"traffic-tables-side\">\n${self.sidetables()}\n</div>\n\n<div class=\"traffic-tables\">\n${self.tables()}\n</div>\n\n<script type=\"text/javascript\">\n  r.timeseries.init()\n</script>\n\n<%def name=\"preamble()\" />\n\n<%def name=\"sidetables()\">\n  <%\n    day_names = babel.dates.get_day_names(locale=c.locale)\n  %>\n\n  % if thing.dow_summary:\n  <table class=\"traffic-table\">\n  <caption>${_(\"traffic by day of week\")}</caption>\n  <thead>\n  <tr>\n    <th scope=\"col\">${_(\"day\")}</th>\n    <th scope=\"col\">${_(\"uniques\")}</th>\n    <th scope=\"col\">${_(\"pageviews\")}</th>\n  </tr>\n  </thead>\n  <tbody>\n  % for dow, cols in thing.dow_summary:\n  <tr>\n    <th scope=\"row\">${day_names[dow]}</th>\n    % for col in cols:\n    <td>${format_number(col)}</td>\n    % endfor\n  </tr>\n  % endfor\n  </tbody>\n  <tfoot>\n  <tr>\n    <th scope=\"row\">${_(\"daily mean\")}</th>\n    % for col in thing.dow_means:\n    <td>${format_number(col)}</td>\n    % endfor\n  </tr>\n  </tfoot>\n  </table>\n  % endif\n</%def>\n\n<%def name=\"tables()\">\n  % for table in thing.tables:\n  ${table}\n  % endfor\n</%def>\n"
  },
  {
    "path": "r2/r2/templates/refundpage.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n  import simplejson\n  from babel.numbers import format_currency, format_number\n  from r2.lib.utils import to36\n%>\n\n<%namespace file=\"utils.html\" import=\"plain_link\"/>\n\n<div class=\"refund-promotion\">\n  <h1>${_(\"refund promotion\")}</h1>\n\n  ${thing.listing}\n\n  <h1>${_(\"campaign\")}</h1>\n\n  <div id=\"refund-form\" class=\"pretty-form\">\n    <table class=\"content preftable\">\n      <tr>\n        <th>${_(\"id\")}</th>\n        <td class=\"prefright\">${thing.campaign._id36}</td>\n      </tr>\n      <tr>\n        <th>${_(\"dates\")}</th>\n        <td class=\"prefright\">\n          ${thing.campaign.start_date.strftime(\"%m/%d/%Y\")}\n          -\n          ${thing.campaign.end_date.strftime(\"%m/%d/%Y\")}\n        </td>\n      </tr>\n      <tr>\n        <th>${_(\"target\")}</th>\n        <td class=\"prefright\">${thing.campaign.target.pretty_name}</td>\n      </tr>\n      <tr>\n        <th>${_(\"budget\")}</th>\n        <td class=\"prefright\">${thing.printable_total_budget}</td>\n      </tr>\n      <tr>\n        <th>${_(\"cpm\")}</th>\n        <td class=\"prefright\">${thing.printable_bid}</td>\n      </tr>\n      <tr>\n        <th>${_(\"impressions purchased\")}</th>\n        <td class=\"prefright\">${format_number(thing.campaign.impressions, locale=c.locale)}</td>\n      </tr>\n      <tr>\n        <th>${_(\"impressions received\")}</th>\n        <td class=\"prefright\">\n          ${format_number(thing.billable_impressions, locale=c.locale)}\n          &#32;\n          (${plain_link(_(\"detail\"), thing.traffic_url)})\n        </td>\n      </tr>\n      <tr>\n        <th>${_(\"billable amount\")}</th>\n        <td class=\"prefright\">${format_currency(thing.billable_amount, 'USD', locale=c.locale)}</td>\n      </tr>\n      <tr>\n        <th>${_(\"refund amount\")}</th>\n        <td class=\"prefright\">${format_currency(thing.refund_amount, 'USD', locale=c.locale)}</td>\n      </tr>\n    </table>\n\n    <input type=\"hidden\" name=\"link\" value=\"${to36(thing.link._id)}\"/>\n    <input type=\"hidden\" name=\"campaign\" value=\"${to36(thing.campaign._id)}\"/>\n\n    <button name=\"save\" class=\"btn\" type=\"button\"\n            onclick=\"return post_pseudo_form('#refund-form', 'refund_campaign')\">\n      ${_(\"issue refund\")}\n    </button>\n    <span class=\"status\"></span>\n  </div>\n\n</div>"
  },
  {
    "path": "r2/r2/templates/register.compact",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%namespace file=\"login.html\" import=\"login_form\"/>\n\n%if c.user_is_loggedin:\n  <p class=\"error\">${_(\"You are logged in. Go use the site!\")}</p>\n%else:\n  ${login_form(register = True, user = thing.user_reg, dest = thing.dest, compact=True)}\n  <script type=\"text/javascript\">\n    $.request(\"new_captcha\");\n  </script>\n%endif\n"
  },
  {
    "path": "r2/r2/templates/registrationinfo.html",
    "content": "${thing.content_html}\n"
  },
  {
    "path": "r2/r2/templates/renderablecampaign.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n  import json\n  from r2.lib.strings import strings\n  from babel.numbers import format_currency\n%>\n\n<% jquery_row = \"$('.\" + thing.campaign._fullname + \"')\" %>\n\n<tr class=\"campaign-row ${thing.campaign._fullname} ${'refund' if (not thing.free and thing.needs_refund) else ''}\"\n    data-startdate=\"${thing.campaign.start_date.strftime('%m/%d/%Y')}\"\n    data-enddate=\"${thing.campaign.end_date.strftime('%m/%d/%Y')}\"\n    data-targeting-collection=\"${thing.campaign.target.is_collection}\"\n    data-targeting=\"${thing.targeting_data}\"\n    data-country=\"${thing.country}\"\n    data-region=\"${thing.region}\"\n    data-metro=\"${thing.metro}\"\n    data-campaign_id36=\"${thing.campaign._id36}\"\n    data-campaign_name=\"${thing.campaign._fullname}\"\n    data-frequency_cap=\"${thing.campaign.frequency_cap}\"\n    data-priority=\"${thing.campaign.priority.name}\"\n    data-override=\"${json.dumps(thing.campaign.priority.inventory_override)}\"\n    data-is_auction=\"${thing.is_auction}\"\n    data-platform=\"${thing.platform}\"\n    data-mobile_os=\"${json.dumps(thing.mobile_os)}\"\n    data-ios_devices=\"${json.dumps(thing.ios_devices) if thing.ios_devices else ''}\"\n    data-ios_versions=\"${json.dumps(thing.ios_versions) if thing.ios_versions else ''}\"\n    data-android_devices=\"${json.dumps(thing.android_devices) if thing.android_devices else ''}\"\n    data-android_versions=\"${json.dumps(thing.android_versions) if thing.android_versions else ''}\"\n    data-paid=\"${json.dumps(thing.paid)}\"\n    data-has-served=\"${json.dumps(thing.campaign.has_served or thing.is_live or thing.is_complete)}\"\n    data-total_budget_dollars=\"${'%.2f' % thing.total_budget_dollars}\"\n    data-cost_basis=\"${thing.cost_basis}\"\n    data-bid_dollars=\"${'%.2f' % (thing.bid_pennies / 100.)}\"\n    data-is_live=\"${thing.is_live}\">\n  <td class=\"campaign-start-date\">\n    ${thing.campaign.start_date.strftime(\"%m/%d/%Y\")}\n  </td>\n\n  <td class=\"campaign-end-date\">\n    ${thing.campaign.end_date.strftime(\"%m/%d/%Y\")}\n  </td>\n\n  <td class=\"campaign-priority\"\n      style=\"${'display:none' if not c.user_is_sponsor else ''}\">\n    ${thing.campaign.priority.text}\n  </td>\n\n  <td class=\"campaign-total-budget ${'paid' if thing.paid else ''}\">\n    <span class=\"total-budget\">\n      %if thing.campaign.is_house:\n        ${_(\"N/A\")}\n      %else:\n        ${format_currency(thing.total_budget_dollars, 'USD', locale=c.locale)}\n      %endif\n    </span>\n    %if not thing.campaign.is_house:\n      %if not thing.paid:\n        %if c.user_is_sponsor:\n          <button class=\"free\" onclick=\"free_campaign(${jquery_row})\">\n            ${_(\"free\")}\n          </button>\n        %else:\n          <button class=\"pay\" onclick=\"$.redirect('${thing.pay_url}')\">\n            ${_(\"pay\")}\n          </button>\n        %endif\n      %elif not thing.free and not (thing.is_complete or thing.is_live):\n        <button class=\"pay\" onclick=\"$.redirect('${thing.pay_url}')\">\n          ${_(\"change\")}\n        </button>\n      %endif\n\n      %if thing.free:\n        <span class='info'>${_(\"freebie\")}</span>\n      %endif\n\n      %if not thing.free and c.user_is_sponsor and thing.needs_refund:\n        <button class=\"refund\" onclick=\"$.redirect('${thing.refund_url}')\">\n          ${_(\"refund\")}\n        </button>\n      %endif\n    %endif\n  </td>\n\n  <td class=\"campaign-spent\">\n      ${format_currency(thing.spent, 'USD', locale=c.locale)}\n  </td>\n\n  %if thing.ads_auction_enabled:\n    <td class=\"campaign-bid\">\n      %if getattr(thing.campaign, 'cost_basis') is not 0:\n        ${thing.printable_bid}\n        ${thing.cost_basis.upper()}\n      %else:\n        ${_(\"N/A\")}\n      %endif\n    </td>\n  %endif\n\n  <td class=\"campaign-target\" title=\"${thing.campaign.target.pretty_name}\">\n    ${thing.campaign.target.pretty_name}\n  </td>\n\n  <td class=\"campaign-location\" title=\"${thing.location_str}\">\n    ${thing.location_str}\n  </td>\n\n  %if c.user_is_sponsor:\n    <td>\n      %if thing.campaign.trans_country_match is None:\n        N/A\n      %elif thing.campaign.trans_country_match:\n        no\n      %else:\n        <span title=\"${thing.campaign.trans_billing_country} vs. ${thing.campaign.trans_ip_country}\">\n          ${thing.campaign.trans_billing_country}/${thing.campaign.trans_ip_country}\n        </span>\n      %endif\n    </td>\n  %endif\n\n  <td class=\"campaign-buttons\">\n    %if thing.is_complete:\n      <span class='info'>${_(\"complete\")}</span>\n    %elif thing.is_edited_live:\n      <span class='info'>${_(\"edited live\")}</span>\n    %else:\n      %if thing.editable:\n        <button class=\"edit\" onclick=\"edit_campaign(${jquery_row})\">\n          ${_(\"edit\")}\n        </button>\n      %endif\n\n      %if not thing.is_live:\n        <button class=\"delete\" onclick=\"del_campaign(${jquery_row})\">\n          ${_(\"delete\")}\n        </button>\n      %endif\n\n      %if thing.is_live:\n        <button class=\"view\" onclick=\"$.redirect('${thing.view_live_url}')\">\n          ${_(\"view\")}\n        </button>\n      %endif\n\n      %if thing.pause_ads_enabled and thing.is_live:\n        %if thing.campaign.paused:\n          <button class=\"resume\" onclick=\"toggle_pause_campaign(${jquery_row}, false)\">\n            ${_(\"resume\")}\n          </button>\n        %elif thing.needs_approval:\n          <span class='info'>${_(\"awaiting approval\")}</span>\n        %else:\n          <button class=\"pause\" onclick=\"toggle_pause_campaign(${jquery_row}, true)\">\n            ${_(\"pause\")}\n          </button>\n        %endif\n      %endif\n\n      %if thing.is_live and c.user_is_sponsor:\n        <button class=\"terminate\" onclick=\"terminate_campaign(${jquery_row})\">\n          ${_(\"terminate\")}\n        </button>\n      %endif\n    %endif\n  </td>\n</tr>\n"
  },
  {
    "path": "r2/r2/templates/reportform.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n<%!\n    from r2.config import feature\n    from r2.lib.template_helpers import static\n%>\n<%namespace file=\"utils.html\" import=\"error_field\" />\n  <%\n    additional_cls = \"\"\n    if not feature.is_enabled(\"subreddit_rules\", subreddit=c.site.name):\n        additional_cls = \"report-action-form\"\n  %>\n  <form id=\"report-action-form\" class=\"action-form ${additional_cls} rounded\" data-form-action=\"report\">\n    <input type=\"hidden\" name=\"thing_id\" value=\"${thing.thing_fullname}\">\n    <span class=\"reason-prompt\">\n      ${_(\"why are you reporting this?\")}\n    </span>\n    <ol>\n      %for rule in thing.rules:\n        <li>\n          <label>\n            <input type=\"radio\" name=\"reason\" value=\"${rule}\">${rule}\n          </label>\n        </li>\n      %endfor\n      %if thing.system_rules:\n        <li>\n          <label>\n            <input type=\"radio\" name=\"reason\" value=\"site_reason_selected\">\n            <select name=\"site_reason\">\n              %for rule in thing.system_rules:\n                <option value=\"${rule}\">${_(\"reddit rule: %(rule_name)s\" % dict(rule_name=rule))}</option>\n              %endfor\n            </select>\n          </label>\n        </li>\n      %endif\n      <li>\n        <label>\n          <input type=\"radio\" name=\"reason\" value=\"other\">${_(\"other (max %(num)s characters):\") % dict(num=100)}\n        </label>\n        <input name=\"other_reason\" value=\"\" maxlength=\"100\" type=\"text\" disabled>\n      </li>\n    </ol>\n    <button type=\"submit\" class=\"btn submit-action-thing\" disabled>\n      ${_(\"submit\")}\n    </button>\n    <button type=\"button\" class=\"btn cancel-action-thing report-cancel\">\n      ${_(\"cancel\")}\n    </button>\n    <span class=\"status\"></span>\n    ${error_field(\"TOO_LONG\", \"reason\", \"span\")}\n  </form>\n"
  },
  {
    "path": "r2/r2/templates/reportformtemplates.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n    from r2.lib.filters import keep_space, unsafe, safemarkdown\n%>\n\n<%namespace file=\"subredditreportform.html\" \n            import=\"subreddit_report_form, reddit_report_form, report_form_reason\" />\n\n<script id=\"subreddit-rules-report-template\" type=\"text/template\">\n    ${subreddit_report_form(\n        fullname=unsafe(\"<%- fullname %>\"),\n        system_rules=thing.system_rules,\n        rules=None,\n        sr_name=\"<%- sr_name %>\",\n    )}\n</script>\n<script id=\"subreddit-default-report-template\" type=\"text/template\">\n    ${reddit_report_form(\n        fullname=unsafe(\"<%- fullname %>\"),\n        system_rules=thing.system_rules,\n        sr_name=True,\n    )}\n</script>\n<script id=\"reddit-report-template\" type=\"text/template\">\n    ${reddit_report_form(\n        fullname=unsafe(\"<%- fullname %>\"),\n        system_rules=thing.system_rules,\n    )}\n</script>\n<script id=\"report-reason-template\" type=\"text/template\">\n    ${report_form_reason(\n        rule=unsafe(\"<%- short_name %>\"),\n    )}\n</script>\n"
  },
  {
    "path": "r2/r2/templates/resetpassword.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%namespace file=\"utils.html\" import=\"error_field\"/>\n<%namespace name=\"utils\" file=\"utils.html\"/>\n\n%if thing.done:\n<p class=\"error\">Your password has been reset and you've been logged in.  Go use the site!</p>\n%else:\n${error_field(\"EXPIRED\", 'p')}\n\n<form id=\"chpass\" method=\"post\" action=\"/api/resetpassword\" \n      onsubmit=\"return post_form(this,'resetpassword')\">\n\n  <h1>choose a new password for /u/${thing.username}</h1>\n  <input type=\"hidden\" name=\"key\" value=\"${thing.key}\"/>\n\n  <div class=\"spacer\">\n    <%utils:round_field title=\"${_('new password')}\">\n      <input type=\"password\" name=\"passwd\" />\n      ${error_field(\"BAD_PASSWORD\", \"passwd\")}\n    </%utils:round_field>\n  </div>\n\n  <div class=\"spacer\">\n    <%utils:round_field title=\"${_('verify password')}\">\n      <input type=\"password\" name=\"passwd2\" />\n      ${error_field(\"BAD_PASSWORD_MATCH\", \"passwd2\")}\n    </%utils:round_field>\n  </div>\n\n<button class=\"btn\" type=\"submit\">submit</button>\n<span class=\"status\"></span>\n\n</form>\n%endif\n"
  },
  {
    "path": "r2/r2/templates/robots.txt",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n<%!\nfrom r2.lib.template_helpers import add_sr\n%>\n# 80legs\nUser-agent: 008\nDisallow: /\n\n# 80legs' new crawler\nUser-agent: voltron\nDisallow: /\n\nUser-Agent: bender\nDisallow: /my_shiny_metal_ass\n\nUser-Agent: Gort\nDisallow: /earth\n\nUser-Agent: *\nDisallow: /*.json\nDisallow: /*.json-compact\nDisallow: /*.json-html\nDisallow: /*.xml\nDisallow: /*.rss\nDisallow: /*.i\nDisallow: /*.embed\nDisallow: /*/comments/*?*sort=\nDisallow: /r/*/comments/*/*/c*\nDisallow: /comments/*/*/c*\nDisallow: /r/*/submit\nDisallow: /message/compose*\nDisallow: /api\nDisallow: /post\nDisallow: /submit\nDisallow: /goto\nDisallow: /*after=\nDisallow: /*before=\nDisallow: /domain/*t=\nDisallow: /login\nDisallow: /reddits/search\nDisallow: /search\nDisallow: /r/*/search\nAllow: /\n\nSitemap: ${thing.subreddit_sitemap}\n"
  },
  {
    "path": "r2/r2/templates/rules.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%namespace file=\"utils.html\" import=\"error_field\"/>\n<%namespace name=\"utils\" file=\"utils.html\"/>\n\n<%!\n    from r2.lib.filters import keep_space, unsafe, safemarkdown\n%>\n\n<%def name=\"mod_action_icon(name, title)\">\n  <span class=\"mod-action-icon mod-action-icon-${name}\"\n       title=\"${title}\"\n       alt=\"${title}\"></span>\n</%def>\n\n<div class=\"subreddit-rules-page ${'editable' if thing.can_edit else ''}\">\n    <header class=\"md-container\">\n        <div class=\"md\">\n            <h2>${thing.title}</h2>\n            <p>\n                ${_(\"Rules that visitors must follow to participate. May be used as reasons to report or ban.\")}\n            </p>\n        </div>\n    </header>\n\n    <div class=\"md-container\">\n        <div id=\"subreddit-rule-list\" class=\"md\">\n            %if thing.rules:\n                %for rule in thing.rules:\n                    <% description_html = unsafe(safemarkdown(rule['description'], wrap=False)) %>\n                    <%\n                        kind = rule.get('kind', 'all')\n                        kind_label = thing.kind_labels.get(kind)\n                    %>\n                    <div class=\"subreddit-rule-item\"\n                         data-priority=\"${rule['priority']}\"\n                         data-description=\"${keep_space(rule['description'])}\"\n                         data-kind=\"${kind}\">\n                        ${self.subreddit_rule(\n                            short_name=keep_space(rule['short_name']),\n                            description=description_html,\n                            kind=kind_label,\n                            editable=thing.can_edit,\n                        )}\n                    </div>\n                %endfor\n            %endif\n        </div>\n    </div>\n\n    %if thing.can_edit:\n        <footer class=\"md-container\">\n            <div id=\"subreddit-rule-add-form\" class=\"md\" hidden>\n                <div class=\"subreddit-rule-add-form-buttons\">\n                    <button class=\"subreddit-rule-edit-button\">\n                        ${self.mod_action_icon('add', _('Add a new rule.'))}&#32;\n                        ${_(\"Add a rule\")}\n                    </button>\n                    <div class=\"subreddit-rule-too-many-notice\" hidden>\n                        ${_(\"You have reached the maximum number of rules.\")}\n                    </div>\n                </div>\n            </div>\n        </footer>\n\n        <script id=\"subreddit-rule-template\" type=\"text/template\">\n            ${self.subreddit_rule(\n                short_name=unsafe(\"<%- short_name %>\"),\n                description=unsafe(\"<%= description_html %>\"),\n                kind=unsafe(\"<%- kind %>\"),\n                editable=True,\n            )}\n        </script>\n        <script id=\"subreddit-rule-form-template\" type=\"text/template\">\n            ${self.subreddit_rule_form(\n                short_name=unsafe(\"<%- short_name %>\"),\n                description=unsafe(\"<%- description %>\"),\n            )}\n        </script>\n    %endif\n</div>\n\n<%def name=\"subreddit_rule(short_name='', description='', kind='', editable=False)\">\n    <div class=\"subreddit-rule ${'editable' if editable else ''}\">\n        <div class=\"subreddit-rule-contents\">\n            <div class=\"subreddit-rule-contents-display\">\n                <h4 class=\"subreddit-rule-title\">\n                    ${short_name}\n                </h4>\n                <div class=\"subreddit-rule-kind\">\n                    ${kind}\n                </div>\n                <div class=\"subreddit-rule-description\">\n                    ${description}\n                </div>\n            </div>\n            %if editable:\n                <div class=\"subreddit-rule-buttons\">\n                    <button class=\"subreddit-rule-delete-button\">\n                        ${self.mod_action_icon('delete', _('Delete this rule.'))}\n                    </button>\n                    <button class=\"subreddit-rule-edit-button\">\n                        ${self.mod_action_icon('edit', _('Edit this rule.'))}\n                    </button>\n                </div>\n            %endif\n        </div>\n        %if editable:\n            <div class=\"md-container-small\">\n                <div class=\"subreddit-rule-delete-confirmation\" hidden>\n                    ${_(\"Delete this rule?\")}&#32;\n                    <button class=\"subreddit-rule-delete-button\">${_(\"delete\")}</button>\n                    &#32;<span class=\"separator\">|</span>&#32;\n                    <button class=\"subreddit-rule-cancel-button\">${_(\"cancel\")}</button>\n                </div>\n            </div>\n        %endif\n    </div>\n</%def>\n\n<%def name=\"subreddit_rule_form(short_name='', description='')\">\n    <form method=\"post\" class=\"subreddit-rule-form\">\n        <div class=\"form-inputs\">\n            <div class=\"c-form-group form-group-short_name\">\n                <div class=\"md-container-small\">\n                    <div class=\"md\">\n                        <label for=\"short_name\" class=\"label required\">${_(\"Short name\")}</label>\n                        <div class=\"text-counter\">\n                            <span class=\"text-counter-display\"></span>&#32;\n                            ${_(\"remaining\")}\n                        </div>\n                    </div>\n                </div>\n                <input type=\"text\" class=\"c-form-control text-counter-input\" name=\"short_name\" value=\"${short_name}\">\n                <div class=\"error-fields\">\n                    ${error_field(\"TOO_SHORT\", \"short_name\")}\n                    ${error_field(\"NO_TEXT\", \"short_name\")}\n                    ${error_field(\"TOO_LONG\", \"short_name\")}\n                    ${error_field(\"SR_RULE_EXISTS\", \"short_name\")}\n                    ${error_field(\"SR_RULE_TOO_MANY\", \"short_name\")}\n                </div>\n            </div>\n            <div class=\"c-form-group form-group-kind\">\n                <div class=\"md-container-small\">\n                    <div class=\"md\">\n                        <div class=\"label\">${_(\"Applies to\")}</div>\n                        <label>\n                            <input type=\"radio\" name=\"kind\" value=\"all\" checked>\n                            ${thing.kind_labels.get('all')}\n                        </label>\n                        <label>\n                            <input type=\"radio\" name=\"kind\" value=\"link\">\n                            ${thing.kind_labels.get('link')}\n                        </label>\n                        <label>\n                            <input type=\"radio\" name=\"kind\" value=\"comment\">\n                            ${thing.kind_labels.get('comment')}\n                        </label>\n                        ${error_field(\"INVALID_OPTION\", \"kind\")}\n                    </div>\n                </div>\n            </div>\n            <div class=\"c-form-group form-group-description\">\n                <div class=\"md-container-small\">\n                    <div class=\"md\">\n                        <label class=\"label\" for=\"description\">${_(\"Full description of this rule\")}</label>\n                        <div class=\"text-counter\">\n                            <span class=\"text-counter-display\" rel=\"description\"></span>&#32;\n                            ${_(\"remaining\")}\n                        </div>\n                    </div>\n                </div>\n                <textarea class=\"c-form-control text-counter-input\" name=\"description\" rows=4>${description}</textarea>\n                <div class=\"error-fields\">\n                    ${error_field(\"TOO_LONG\", \"description\")}\n                </div>\n            </div>\n        </div>\n        <div class=\"form-buttons\">\n            <button type=\"reset\" class=\"subreddit-rule-cancel-button\">\n                ${self.mod_action_icon('cancel', _('Cancel this action.'))}\n            </button>\n            <button type=\"submit\" class=\"subreddit-rule-submit-button\">\n                ${self.mod_action_icon('confirm', _('Confirm this action.'))}\n            </button>\n        </div>\n        ${error_field(\"UNKNOWN_ERROR\", \"unknown\")}\n    </form>\n</%def>\n"
  },
  {
    "path": "r2/r2/templates/searchbar.compact",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<div class=\"searchpane raisedbox\">\n  <h4 style=\"color:gray\">${thing.header}</h4>\n\n  <div id=\"previoussearch\">\n    ${thing.search_form}\n  </div>\n</div>\n"
  },
  {
    "path": "r2/r2/templates/searchbar.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n%if thing.prev_search and thing.converted_data:\n  <div class=\"search-summary\">\n    <div>\n    <p class=\"debuginfo\">\n      <span class=\"icon\">&delta;</span>&nbsp;\n      <span class=\"content\">${_('converted query to %(syntax)s syntax: %(converted)s') % thing.converted_data}</span>\n    </p>\n    </div>\n  </div>\n%endif\n\n<div class=\"searchpane raisedbox\">\n  <h4 style=\"color:gray\">${thing.header}</h4>\n\n  <div id=\"previoussearch\">\n    ${thing.search_form}\n  </div>\n</div>\n"
  },
  {
    "path": "r2/r2/templates/searchform.compact",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%inherit file=\"searchform.html\" />\n"
  },
  {
    "path": "r2/r2/templates/searchform.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n   from r2.config import feature\n   from r2.models.subreddit import DefaultSR, AllSR\n   from r2.lib.template_helpers import add_sr, static\n%>\n\n<%def name=\"search_faq()\">\n  <div id=\"moresearchinfo\">\n  <p>use the following search parameters to narrow your results:</p>\n\n  <dl>\n      <dt>subreddit:<i>subreddit</i></dt>\n      <dd>${_('find submissions in \"subreddit\"')}</dd>\n      <dt>author:<i>username</i></dt>\n      <dd>${_('find submissions by \"username\"')}</dd>\n      <dt>site:<i>example.com</i></dt>\n      <dd>${_('find submissions from \"example.com\"')}</dd>\n      <dt>url:<i>text</i></dt>\n      <dd>${_('search for \"text\" in url')}</dd>\n      <dt>selftext:<i>text</i></dt>\n      <dd>${_('search for \"text\" in self post contents')}</dd>\n      <dt>self:yes (or self:no)</dt>\n      <dd>${_('include (or exclude) self posts')}</dd>\n      <dt>nsfw:yes (or nsfw:no)</dt>\n      <dd>${_('include (or exclude) results marked as NSFW')}</dd>\n  </dl>\n\n  <p>e.g.&#32;<code>subreddit:aww site:imgur.com dog</code></p>\n  <p><a href=\"https://www.reddit.com/wiki/search\">${_('see the search faq for details.')}</a></p>\n  </div>\n\n  <p><a href=\"https://www.reddit.com/wiki/search\" id=\"search_showmore\">${_('advanced search: by author, subreddit...')}</a></p>\n</%def>\n\n<form action=\"${add_sr(thing.search_path)}\" id=\"search\" role=\"search\">\n  <input type=\"text\" \n         %if thing.prev_search:\n           value=\"${thing.prev_search}\" style=\"color:black\"\n         %endif\n         name=\"q\" placeholder=\"${_('search')}\" tabindex=\"20\">\n\n  %if feature.is_enabled('legacy_search') or c.user.pref_legacy_search or thing.simple:\n    <input type=\"submit\" value=\"\" tabindex=\"22\">\n  %else:\n    <button class=\"search-submit-button c-btn c-btn-primary\" type='submit' aria-label=\"${_(\"Search\")}\">\n      <span class=\"search-icon\"></span>\n    </button>\n  %endif\n\n  %if thing.subreddit_search:\n    %if thing.over18_url and thing.prev_search:\n      <p><a id=\"search_over18\" href=\"${thing.over18_url}\" rel=\"nofollow\">${_('enable NSFW results')}</a></p>\n    %endif\n  %elif thing.simple:\n  <div id=\"searchexpando\" class=\"infobar\">\n    %if not isinstance(c.site, (DefaultSR, AllSR)):\n      <label><input type=\"checkbox\" name=\"restrict_sr\" tabindex=\"21\">${_('limit my search to %(path)s') % dict(path=c.site.path.rstrip('/'))}</label>\n    % endif\n    ${search_faq()}\n  </div>\n  %else:\n    %if not thing.site or isinstance(thing.site, (DefaultSR, AllSR)):\n      <input type=\"hidden\" name=\"restrict_sr\">\n    %else:\n      <label><input type=\"checkbox\" ${'checked=\"checked\"' if thing.restrict_sr else ''} name=\"restrict_sr\" tabindex=\"21\">\n      ${_('limit my search to %(path)s') % dict(path=thing.site.path.rstrip('/'))}</label>\n    %endif\n    ${search_faq()}\n  %endif\n\n  %for k, v in thing.search_params.iteritems():\n    <input type=\"hidden\" name=\"${k}\" value=\"${v}\">\n  %endfor\n</form>\n"
  },
  {
    "path": "r2/r2/templates/searchlisting.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n   from r2.lib.template_helpers import format_html\n %>\n\n<%namespace file=\"utils.html\" import=\"plain_link\" />\n\n<div class=\"listing search-result-listing\">\n  <div class=\"search-result-group\">\n    %if thing.heading:\n      <header class=\"search-result-group-header\">\n        <span class=\"search-header-label\">${thing.heading}</span>\n        %if thing.nav_menus:\n          <div class=\"search-header-menus\">\n            %for menu in thing.nav_menus:\n              <div class=\"search-menu\">${menu}</div>\n            %endfor\n          </div>\n        %endif\n      </header>\n    %endif\n    <div class=\"contents\">\n      %for a in thing.things:\n        ${a}\n      %endfor\n    </div>\n    <footer>\n      %if thing.nextprev and (thing.prev or thing.next):\n        <div class=\"nav-buttons\">\n          <span class=\"nextprev\">${_(\"view more:\")}&#32;\n          %if thing.prev:\n            ${plain_link(format_html(\"&lsaquo; %s\", _(\"prev\")), thing.prev, rel=\"nofollow prev\")}\n          %endif\n          %if thing.prev and thing.next:\n            <span class=\"separator\"></span>\n          %endif\n          %if thing.next:\n            ${plain_link(format_html(\"%s &rsaquo;\", _(\"next\")), thing.next, rel=\"nofollow next\")}\n          %endif\n          </span>\n          %if thing.next_suggestions:\n            ${thing.next_suggestions}\n          %endif\n        </div>\n      %endif\n      %if not thing.things:\n        <p class=\"info\">${_(\"there doesn't seem to be anything here\")}</p>\n      %endif\n    </footer>\n  </div>\n</div>\n"
  },
  {
    "path": "r2/r2/templates/searchresultbase.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<div class=\"${self.search_result_css_class()}\" data-fullname=\"${thing.fullname}\">\n  ${self.search_result_content()}\n</div>\n\n<%def name=\"search_result_content()\">\n  ${self.search_result_header()}\n  ${self.search_result_meta()}\n  ${self.search_result_body()}\n  ${self.search_result_footer()}\n</%def>\n\n<%def name=\"search_result_css_class()\">\n  search-result\n</%def>\n\n<%def name=\"search_result_header()\">\n  <header class=\"search-result-header\">\n    ${caller.body()}\n  </header>\n</%def>\n\n<%def name=\"search_result_meta()\">\n  <div class=\"search-result-meta\">\n    ${caller.body()}\n  </div>\n</%def>\n\n<%def name=\"search_result_body()\">\n  <div class=\"search-result-body\">\n    ${caller.body()}\n  </div>\n</%def>\n\n<%def name=\"search_result_footer()\">\n  <div class=\"search-result-footer\">\n    ${caller.body()}\n  </div>\n</%def>\n\n<%def name=\"search_result_icon(name)\">\n  <span class=\"search-result-icon search-result-icon-${name}\"></span>\n</%def>\n"
  },
  {
    "path": "r2/r2/templates/searchresultlink.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%inherit  file=\"searchresultbase.html\"/>\n\n<%namespace file=\"utils.html\" import=\"plain_link, thing_timestamp, md, nsfw_stamp, quarantine_stamp, thumbnail_img\" />\n<%namespace file=\"link.html\" import=\"thumbnail\" />\n\n<%!\n  from r2.lib.template_helpers import (\n      format_html,\n      format_number,\n      get_linkflair_css_classes,\n  )\n  from r2.lib.pages import WrappedUser\n%>\n\n<%def name=\"search_result_css_class()\">\n  <%\n    has_thumbnail_class = \"has-thumbnail\" if thing.thumbnail else \"\"\n    linkflair_classes = get_linkflair_css_classes(thing)\n    visited_class = 'visited' if thing.visited else ''\n  %>\n  ${parent.search_result_css_class()} search-result-link ${has_thumbnail_class} ${linkflair_classes} ${visited_class}\n</%def>\n\n<%def name=\"search_result_content()\">\n  %if thing.thumbnail:\n    <a href=\"${thing.permalink}\"\n       class=\"may-blank thumbnail ${thing.thumbnail if thing.thumbnail_sprited else ''}\">\n      ${thumbnail_img(thing)}\n    </a>\n  %endif\n  <div>\n    ${parent.search_result_content()}\n  </div>\n</%def>\n\n<%def name=\"search_result_header()\">\n  <%parent:search_result_header>\n    %if c.site.link_flair_position == 'left':\n      ${self.flair()}\n    %endif\n    ${plain_link(\n        thing.title,\n        thing.permalink,\n        _sr_path=False,\n        _class='search-title may-blank',\n      )}\n    %if c.site.link_flair_position == 'right':\n      ${self.flair()}\n    %endif\n  </%parent:search_result_header>\n</%def>\n\n<%def name=\"flair()\">\n  %if c.user.pref_show_link_flair and thing.flair_text:\n    <span class=\"linkflairlabel\" title=\"${thing.flair_text}\">${thing.flair_text}</span>\n  %endif\n</%def>\n\n<%def name=\"search_result_meta()\">\n  <%parent:search_result_meta>\n    %if thing.quarantine:\n      <span class=\"quarantine-stamp stamp bold-stamp\">${quarantine_stamp()}</span>&#32;\n    %endif\n    %if thing.over_18:\n      <span class=\"stamp nsfw-stamp\">${nsfw_stamp()}</span>&#32;\n    %endif\n    %if not thing.hide_score:\n      ${self.search_result_icon('score')}\n      <span class=\"search-score\">${format_number(thing.score)} ${ungettext('point', 'points', thing.score)}</span>&#32;\n    %endif\n    ${plain_link(\n        '%s %s' % (format_number(thing.num_comments), ungettext('comment', 'comments', thing.num_comments)),\n        thing.permalink,\n        _sr_path=False,\n        _class=\"search-comments may-blank\",\n      )}&#32;\n    <span class=\"search-time\">${_(\"submitted\")}&#32;${thing_timestamp(thing, thing.timesince, include_tense=True)}</span>&#32;\n    <span class=\"search-author\">${_(\"by\")}&#32;${WrappedUser(thing.author, thing.attribs, thing)}</span>&#32;\n    <%\n      subreddit_link_fmt = format_html('<span>%s</span>', _('to %s')).replace(' ', '&#32;')\n    %>\n    ${plain_link(\n        '/r/%s' % thing.subreddit.name,\n        '/r/%s' % thing.subreddit.name,\n        _sr_path=False,\n        fmt=subreddit_link_fmt,\n        _class='search-subreddit-link may-blank',\n      )}\n  </%parent:search_result_meta>\n</%def>\n\n<%def name=\"search_result_body()\">\n  %if thing.selftext:\n    <div class=\"search-expando collapsed\">\n      <%parent:search_result_body>\n        ${md(thing.selftext, wrap=True)}\n      </%parent:search_result_body>\n    </div>\n    <div class=\"search-expando-button collapsed\">\n      <span class=\"search-expando-button-label-collapsed\">${_(\"more\")}</span>\n      <span class=\"search-expando-button-label-expanded\">${_(\"less\")}</span>\n    </div>\n  %endif\n</%def>\n\n<%def name=\"search_result_footer()\">\n  %if not thing.is_self:\n    <%parent:search_result_footer>\n      ${self.search_result_icon('external')}\n      ${plain_link(\n          thing.url,\n          thing.url,\n          _sr_path=False,\n          _class='search-link may-blank',\n        )}\n    </%parent:search_result_footer>\n  %endif\n</%def>\n"
  },
  {
    "path": "r2/r2/templates/searchresultsubreddit.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%inherit  file=\"searchresultbase.html\"/>\n\n<%namespace file=\"utils.html\" import=\"plain_link, nsfw_stamp, quarantine_stamp\" />\n<%namespace file=\"subscribebutton.html\" import=\"subscribe_button\" />\n\n<%!\n  from r2.lib.template_helpers import format_number, search_url\n  from r2.lib.utils import timesince\n%>\n\n<%def name=\"search_result_css_class()\">\n  ${parent.search_result_css_class()} search-result-subreddit\n</%def>\n\n<%def name=\"search_result_header()\">\n  <%parent:search_result_header>\n    ${plain_link(\n        thing.title,\n        thing.search_path,\n        _sr_path=False,\n        _class='search-title may-blank',\n      )}\n  </%parent:search_result_header>\n</%def>\n\n<%def name=\"search_result_meta()\">\n  <%parent:search_result_meta>\n    <% data_attrs = {\"sr_name\": thing.name} %>\n    %if thing.display_type != \"private\":\n      ${subscribe_button(thing, data_attrs, css_class='search-subscribe-button')}\n    %endif\n    ${self.permissions_stamps()}\n    ${plain_link(\n        thing.path.rstrip('/'),\n        thing.search_path,\n        _sr_path=False,\n        _class='search-subreddit-link may-blank',\n      )}&#32;\n    %if not thing.score_hidden:\n      <span class=\"search-subscribers\">${format_number(thing._ups)} ${ungettext('subscriber', 'subscribers', thing._ups)},</span>&#32;\n    %endif\n    <span class=\"search-time\">${_(\"a community for %(time)s\") % dict(time=timesince(thing._date))}</span>&#32;\n  </%parent:search_result_meta>\n</%def>\n\n<%def name=\"search_result_body()\">\n  %if thing.public_description:\n    <%parent:search_result_body>\n      ${thing.public_description}\n    </%parent:search_result_body>\n  %endif\n</%def>\n\n<%def name=\"search_result_footer()\">\n  <%parent:search_result_footer>\n    <% pretty_name = thing.path.rstrip('/') %>\n    ${self.search_result_icon('filter')}\n    ${plain_link(\n        _(\"search within %(subreddit)s\") % dict(subreddit=pretty_name),\n        search_url(thing.prev_search, thing.name, restrict_sr='on', sort=thing.sort, recent=thing.recent),\n        _sr_path=False,\n        _class='search-link',\n        title=_('search in %(subreddit)s' % dict(subreddit=pretty_name)),\n      )}\n  </%parent:search_result_footer>\n</%def>\n\n<%def name=\"permissions_stamps()\">\n  %if thing.quarantine:\n    <span class=\"quarantine-stamp stamp bold-stamp\">${quarantine_stamp()}</span>&#32;\n  %endif\n  %if thing.over_18:\n    <span class=\"stamp nsfw-stamp\">${nsfw_stamp()}</span>&#32;\n  %endif\n  %if thing.display_type == \"private\":\n    <span class=\"stamp private-stamp\">${_(\"private\")}</span>&#32;\n  %elif thing.display_type == \"restricted\": \n    <span class=\"stamp restricted-stamp\">${_(\"restricted\")}</span>&#32;\n  %elif thing.display_type == \"archived\":\n    <span class=\"stamp archived-stamp\">${_(\"archived\")}</span>&#32;\n  %endif\n</%def>\n"
  },
  {
    "path": "r2/r2/templates/selftext.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n    from r2.lib.filters import unsafe, safemarkdown, keep_space\n%>\n<%namespace file=\"utils.html\" import=\"error_field\"/>\n\n<div class=\"selftextcontainer\">\n  <div class=\"selftext rounded\">\n    ${unsafe(safemarkdown(thing.link.selftext))}\n  </div>\n</div>\n\n"
  },
  {
    "path": "r2/r2/templates/serversecondsbar.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%namespace file=\"utils.html\" import=\"inline_radio_type, md\"/>\n\n<div class=\"titlebox\">\n  <div class=\"server-seconds rounded\">\n    %if thing.message:\n      ${md(thing.message)}\n    %endif\n\n    %if thing.gift_message:\n      ${md(thing.gift_message)}\n    %endif\n\n    %if thing.is_user:\n    <div class=\"server-seconds-public bottom\">\n      <div class=\"title\">${_(\"visible to:\")}</div>\n      <form id=\"seconds_visibility_form\">\n        ${inline_radio_type(\"seconds_visibility\", \"private\", _(\"only me\"), checked=not thing.is_public)}\n        ${inline_radio_type(\"seconds_visibility\", \"public\", _(\"everyone\"), checked=thing.is_public)}\n      </form>\n      <div class=\"note\">${_('note: you will only be eligible for a gilding trophy if this is set to \"everyone\"')}</div>\n    </div>\n    %endif\n  </div>\n</div>\n\n<script type=\"text/javascript\">\n  $('[name=\"seconds_visibility\"]').click(\n    function () {\n      var form = $('#seconds_visibility_form');\n      post_form(form, 'server_seconds_visibility');\n    }\n  ) \n</script>\n"
  },
  {
    "path": "r2/r2/templates/share.email",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n  from r2.lib.template_helpers import get_domain\n  from r2.lib.filters import unsafe\n%>\n\n${thing.username} from ${g.default_scheme}://${get_domain(subreddit=False)}/ has shared a link with you.\n\n${unsafe(thing.body)}\n\n___\nIf you would not like to receive emails from reddit.com in the future, visit ${g.default_scheme}://${g.domain}/mail/optout?x=${thing.msg_hash}\n"
  },
  {
    "path": "r2/r2/templates/shareclose.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<script>\n  window.close();\n</script>\n"
  },
  {
    "path": "r2/r2/templates/sidebarmessage.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n   from r2.lib.filters import safemarkdown\n%>\n\n<%namespace file=\"utils.html\" import=\"classes\"/>\n\n<div ${classes('side-message', thing.extra_class)}>\n  ${unsafe(safemarkdown(thing.message))}\n</div>\n"
  },
  {
    "path": "r2/r2/templates/sidebarmodlist.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n   from r2.config import feature\n%>\n\n<%namespace file=\"utils.html\" import=\"nsfw_stamp, quarantine_stamp\" />\n\n<ul id=\"side-mod-list\">\n%for sr in thing.subreddits:\n  <%\n    if sr.spammy():\n      sr_class = 'sr-banned'\n    else:\n      sr_class = ''\n  %>\n\n  <li class=\"${sr_class}\">\n    <a href=\"${sr.path}\" title=\"/r/${sr.name}\">/r/${sr.name}</a>\n    %if sr.quarantine:\n      <span class=\"quarantine-stamp stamp\">${quarantine_stamp()}</span>\n    %elif sr.over_18 and c.user.pref_label_nsfw:\n      <span class=\"nsfw-stamp stamp\">${nsfw_stamp()}</span>\n    %endif\n  </li>\n%endfor\n</ul>\n\n<script>new r.ui.Summarize($('#side-mod-list'), 5)</script>\n"
  },
  {
    "path": "r2/r2/templates/sidebarmultilist.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<ul id=\"side-multi-list\">\n%for multi in thing.multis:\n  <li><a href=\"${multi.path}\" title=\"${multi.name}\">${multi.name}</a></li>\n%endfor\n</ul>\n\n<script>new r.ui.Summarize($('#side-multi-list'), 15)</script>\n"
  },
  {
    "path": "r2/r2/templates/sidebox.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%namespace file=\"utils.html\" import=\"plain_link\"/>\n\n<div class=\"sidebox ${thing.css_class}${' disabled' if thing.disabled else ''}\">\n  <div class=\"morelink\">\n    ${plain_link(thing.title, thing.link, _sr_path=thing.sr_path,\n                 _class='login-required access-required' if thing.show_cover else None,\n                 target=thing.target, data=thing.data_attrs)}\n    <div class=\"nub\"> </div>\n  </div>\n\n  %if thing.subtitles:\n    %if thing.show_icon:\n      <div class=\"spacer\">\n      ${plain_link('', thing.link, _sr_path=thing.sr_path,\n                   _class='login-required access-required' if thing.show_cover else None)}\n    %else:\n      <div class=\"spacer no-icon\">\n    %endif\n      %for subtitle in thing.subtitles:\n        <div class=\"subtitle\">${subtitle}</div>\n      %endfor\n    </div>\n  %endif\n</div>\n"
  },
  {
    "path": "r2/r2/templates/sidecontentbox.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%namespace file=\"utils.html\" import=\"tags\"/>\n<div class=\"sidecontentbox ${thing.extra_class or ''} ${'collapsible' if thing.collapsible else ''}\" ${tags(_id=thing._id)}>\n  %if thing.helplink:\n    ${thing.helplink}\n  %endif\n  <div class=\"title\">\n    <h1>${thing.title.upper()}</h1>\n    %if thing.collapsible:\n      <span class=\"collapse-button\">-</span>\n    %endif\n  </div>\n  <ul class=\"content\">\n    %for c in thing.content:\n      <li>${c}</li>\n    %endfor\n\n    %if thing.more_href:\n      <li class=\"more\">\n        <a href=\"${thing.more_href}\">\n          ${thing.more_text} &raquo;\n        </a>\n      </li>\n    %endif\n  </ul>\n\n  %if thing.collapsible:\n    <script>r.ui.collapsibleSideBox(\"${thing._id}\")</script>\n  %endif\n</div>\n"
  },
  {
    "path": "r2/r2/templates/sitewidetraffic.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%inherit file=\"reddittraffic.html\"/>\n\n<%!\n    from r2.lib.template_helpers import format_number\n%>\n\n<%def name=\"sidetables()\">\n  ${parent.sidetables()}\n\n  <table class=\"traffic-table\">\n  <caption>${_(\"top subreddits\")} <span class=\"normal\">(<a href=\"/traffic/subreddits/report\">${_(\"make report\")}</a>)</span></caption>\n  <thead>\n  <tr>\n    <th scope=\"col\">${_(\"subreddit\")}</th>\n    <th scope=\"col\">${_(\"uniques\")}</th>\n    <th scope=\"col\">${_(\"pageviews\")}</th>\n  </tr>\n  </thead>\n  <tbody>\n  % for (name, url), data in thing.subreddit_summary:\n  <tr>\n  % if url:\n    <th scope=\"row\"><a href=\"${url}\">${name}</a></th>\n  % else:\n    <th scope=\"row\">${name}</th>\n  % endif\n    % for datum in data:\n    <td>${format_number(datum)}</td>\n    % endfor\n  </tr>\n  % endfor\n  </tbody>\n  </table>\n</%def>\n"
  },
  {
    "path": "r2/r2/templates/sitewidetrafficpage.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%inherit file=\"trafficpage.html\"/>\n\n<%!\n    from r2.lib import js\n%>\n\n<%def name=\"javascript()\">\n  ${parent.javascript()}\n  ${unsafe(js.use('traffic'))}\n</%def>\n"
  },
  {
    "path": "r2/r2/templates/sponsorlookupuser.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%namespace name=\"utils\" file=\"utils.html\"/>\n\n<div class=\"sponsored-page\">\n  <form name=\"lookup-user\" action=\"\" method=\"get\">\n    <div class=\"editor lookup-user\">\n      <header>\n        <h2>look up a user</h2>\n      </header>\n      <div class=\"editor-group\">\n        <%utils:line_field title=\"${_('search')}\" css_class=\"rounded lookup-user-field\">\n          <label class=\"form-group\">\n            <div class=\"label\">${_('id')}</div>\n            <input type=\"text\" name=\"name\" value=\"${thing.id_user._fullname if thing.id_user else \"\"}\">\n          </label>\n          <div class='form-group lookup-user-results'>\n            <div class=\"label\">name</div>\n            %if thing.id_user:\n              ${utils.plain_link(thing.id_user.name, \"/user/%s/promoted\" % thing.id_user.name)}\n            %endif\n          </div>\n\n          <label class=\"form-group\">\n            <div class=\"label\">email</div>\n            <input type=\"text\" name=\"email\" value=\"${thing.email or \"\"}\">\n          </label>\n          <div class='form-group lookup-user-results'>\n            <div class=\"label\">name(s)</div>\n            %for user in thing.email_users:\n              <div>\n                ${utils.plain_link(user.name, \"/user/%s/promoted\" % user.name)}\n              </div>\n            %endfor\n          </div>\n        </%utils:line_field>\n      </div>\n      <footer class=\"buttons\">\n        <button type=\"submit\">search</button>\n      </footer>\n    </form>\n  </div>\n</div>\n"
  },
  {
    "path": "r2/r2/templates/sponsorshipbox.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<div class=\"sponsorshipbox\"></div>\n"
  },
  {
    "path": "r2/r2/templates/sponsorsidebar.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n\n"
  },
  {
    "path": "r2/r2/templates/spotlightlisting.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%namespace file=\"printablebuttons.html\" import=\"ynbutton\"/>\n<%namespace file=\"utils.html\" import=\"tags, text_with_links, classes\"/>\n<%\n   import json\n   from r2.lib.template_helpers import static, format_html\n   from r2.lib.wrapped import Templated\n%>\n\n<%!\n   from babel.numbers import format_currency\n%>\n\n<div id=\"siteTable_organic\" ${classes('organic-listing', 'loading',\n    'show-placeholder' if thing.show_placeholder else None,\n  )}>\n  %if thing.things:\n    %for link in thing.things:\n      ${unsafe(link.render(display=False))}\n    %endfor\n  %endif\n\n  %if thing.interestbar:\n    <div class=\"thing interestbar\" style=\"display:none\">\n      ${unsafe(thing.interestbar.render())}\n    </div>\n  %endif\n\n  %if thing.navigable:\n    <div class=\"nextprev\">\n      <div class=\"throbber\"></div>\n      <button class=\"arrow prev\">${_(\"prev\")}</button>\n      <button class=\"arrow next\">${_(\"next\")}</button>\n    </div>\n  %endif\n\n  <div class=\"help help-hoverable\">\n    ${_(\"what's this?\")}\n    <div id=\"spotlight-help\" class=\"hover-bubble help-bubble anchor-top\">\n      <div class=\"help-section help-promoted\">\n        <p>\n          ${text_with_links(\n            _(\"This sponsored link is an advertisement generated with our %(self_serve_advertisement_tool)s.\"),\n              self_serve_advertisement_tool=dict(link_text=_(\"self-serve advertisement tool\"), path=\"https://www.reddit.com/wiki/selfserve\")\n          )}\n        </p>\n        <p>\n          ${text_with_links(\n            _(\"Use of this tool is open to all members of reddit.com, and for as little as %(price)s you can advertise in this area. %%(get_started)s\") % dict(price=format_currency(g.min_total_budget_pennies / 100., 'USD', locale=c.locale)),\n              get_started=dict(link_text=format_html(\"%s &rsaquo;\", _(\"Get started\")), path=\"/advertising\")\n          )}\n        </p>\n      </div>\n      <div class=\"help-section help-organic\">\n        <p>\n          ${_(\"This area shows new and upcoming links. Vote on\" +\n                \" links here to help them become popular, and click\" +\n                \" the forwards and backwards buttons to view more. \")}\n        </p>\n        %if c.user_is_loggedin:\n          ${ynbutton(_(\"here\"), _(\"This element has been disabled.\"),\n                     \"disable_ui\",\n                     format = _(\"Click %(here)s to disable this feature.\"),\n                     format_arg = \"here\",\n                     hidden_data = dict(id=\"organic\"))}\n        %endif\n      </div>\n      <div class=\"help-section help-interestbar\">\n        <p>${_(\"Enter a keyword or topic to discover new subreddits around your interests. Be specific!\")}</p>\n        <p>\n          ${text_with_links(\n            _(\"You can access this tool at any time on the %(reddits)s page.\"),\n              reddits=dict(link_text=\"/subreddits/\", path=\"/subreddits/\")\n          )}\n        </p>\n      </div>\n    </div>\n  </div>\n</div>\n<script>\n  r.spotlight.setup(\n    ${unsafe(json.dumps([link._fullname for link in thing.things]))},\n    ${unsafe(json.dumps(thing.interestbar_prob))},\n    ${unsafe(json.dumps(thing.show_promo))},\n    ${unsafe(json.dumps(thing.keywords))}\n  )\n</script>\n\n"
  },
  {
    "path": "r2/r2/templates/starkcomment.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%inherit file=\"comment.html\"/>\n\n<%def name=\"entry()\">\n  <div class=\"commentbox\">\n    <p class=\"tagline\">\n      ${self.tagline()}\n      &#32;\n      <a target=\"_top\" href=\"${thing.permalink}\">\n        ${_(\"context\")}\n      </a>\n    </p>\n\n    <span class=\"commentbody\">\n      ${self.commentBody()}\n    </span>\n  </div>\n</%def>\n"
  },
  {
    "path": "r2/r2/templates/subreddit.compact",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n   from r2.lib.template_helpers import add_sr\n %>\n<%namespace file=\"printable.compact\" import=\"delete_report_buttons\"/>\n<%namespace file=\"subreddit.html\" import=\"tagline, permission_icons\"/>\n<%namespace file=\"utils.html\" import=\"plain_link\" />\n<%namespace file=\"printablebuttons.html\" import=\"toggle_button\"/>\n\n  <% \n     like_cls = \"\"\n     if getattr(thing, \"likes\", None):\n         like_cls = \"likes\"\n     elif getattr(thing, \"likes\", None) is False:\n         like_cls = \"dislikes\"\n   %>\n<div class=\"thing subreddit id-${thing._fullname}\">\n  <div class=\"midcol\"\n  ${toggle_button(\"fancy-toggle-button\", \"+\", \"-\",\n                  \"subscribe('%s')\" % thing._fullname, \n                  \"unsubscribe('%s')\" % thing._fullname, \n                  css_class = \"add button\", alt_css_class = \"remove button\",\n                  reverse = thing.subscriber)}\n  </div>\n  <div class=\"entry ${like_cls}\">\n    %if thing.public_description or thing.description:\n      <a href=\"javascript:void(0)\" class=\"expando-button collapsed button\">\n          Aa\n      </a>\n    %endif\n    <p class=\"title\">\n      ${plain_link(thing.name, thing.path, _class=\"title\")}\n      ${plain_link(thing.title, thing.path, _class=\"domain\")}\n    </p>\n    <p class=\"tagline\">\n      ${tagline()}\n    </p>\n  %if thing.public_description_usertext:\n    <div class=\"expando\" style=\"display:none\">\n      <div class=\"description\">\n        ${thing.public_description_usertext}\n      </div>\n    </div>\n  %endif\n  </div>\n  <div class=\"clear\"></div>\n</div>\n"
  },
  {
    "path": "r2/r2/templates/subreddit.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n   from r2.lib.template_helpers import static\n   from r2.lib.strings import strings\n   from r2.lib.utils import timesince\n   from r2.lib.pages import SubscribeButton\n   %>\n<%inherit  file=\"printable.html\"/>\n\n<%namespace file=\"utils.html\" import=\"plain_link\"/>\n\n<%def name=\"numcol()\">\n</%def>\n\n<%def name=\"entry()\">\n<% fullname = thing._fullname %>\n<p class=\"titlerow\">\n  ${plain_link('%s: %s' % (thing.path.rstrip('/'), thing.title), thing.path, _class=\"title\")}\n  %if c.user_is_admin:\n  ${self.admintagline()}\n  %endif\n</p>\n%if thing.public_description_usertext:\n  <div class=\"description\">\n    ${thing.public_description_usertext}\n  </div>\n%endif\n<p class=\"tagline\">\n  ${self.tagline()}\n</p>\n<ul class=\"flat-list buttons\">\n  ${self.buttons()}\n</ul>\n<div class=\"reportform report-${thing._fullname}\"></div>\n</%def>\n\n<%def name=\"tagline()\">\n  %if not thing.score_hidden:\n    ${self.score(thing)},\n  %endif\n  ${_(\"a community for %(time)s\") % dict(time=timesince(thing._date))}\n</%def>\n\n<%def name=\"sr_type_icon(name, title)\">\n  <span class=\"sr-type-icon sr-type-icon-${name}\"\n       title=\"${title}\"\n       alt=\"${title}\"></span>\n</%def>\n\n##this function is used by subscriptionbox.html\n<%def name=\"permission_icons(sr)\">\n  %if sr.spammy():\n    ${self.sr_type_icon(\"banned\", _(\"banned\"))}\n  %else:\n    %if sr.moderator:\n      ${self.sr_type_icon(\"moderator\", _(\"moderator\"))}\n    %elif sr.type in (\"restricted\", \"private\"):\n      %if sr.contributor:\n        ${self.sr_type_icon(\"approved\", _(\"approved submitter\"))}\n      %else:\n        ${self.sr_type_icon(\"restricted\", _(\"not approved\"))}\n      %endif\n    %endif \n\n    %if sr.type in sr.private_types:\n      ${self.sr_type_icon(\"private\", _(\"private\"))}\n    %endif\n\n    %if sr.quarantine:\n      ${self.sr_type_icon(\"quarantined\", _(\"quarantined\"))}\n    %endif\n\n    %if sr.over_18:\n      ${self.sr_type_icon(\"nsfw\", _(\"NSFW\"))}\n    %endif\n  %endif\n</%def>\n\n<%def name=\"midcol(display=True, cls='')\">\n  <div class=\"midcol\">\n    ${SubscribeButton(thing)}\n    ${permission_icons(thing)}\n  </div>\n</%def>\n\n<%def name=\"child()\">\n</%def>\n\n"
  },
  {
    "path": "r2/r2/templates/subreddit.mobile",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n   from r2.lib.strings import strings\n   %>\n<%inherit  file=\"printable.mobile\"/>\n\n<%namespace file=\"utils.html\" import=\"plain_link\"/>\n\n<%def name=\"numcol()\">\n</%def>\n\n<%def name=\"entry()\">\n<% fullname = thing._fullname %>\n<p class=\"subreddit\">\n  ${plain_link(thing.title, thing.path, _class=\"title\")}\n  &#32;<span class=\"domain\">(${thing.path})</span> \n</p>\n</%def>\n\n<%def name=\"tagline()\">\n</%def>\n\n<%def name=\"child()\">\n</%def>\n\n<%def name=\"buttons()\">\n  ${parent.delete_or_report_buttons(delete=False)}\n  ${parent.buttons()}\n</%def>\n\n<%def name=\"leave_button(name, where)\">\n${self.yes_no_button(\"leave\", name, _(\"leave\"), \n  \"return deletetoggle(this, 'leave');\", _(\"left\"),\n  location = where)}\n</%def>\n"
  },
  {
    "path": "r2/r2/templates/subreddit.xml",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n<%!\n    from r2.lib.template_helpers import html_datetime\n    from r2.lib.template_helpers import get_domain, header_url\n    from r2.lib.utils import UrlParser\n%>\n<%namespace name=\"utils\" file=\"utils.xml\"/>\n<%\n    domain = get_domain(subreddit=False)\n    url = g.default_scheme+\"://\"+domain+thing.path\n%>\n<entry>\n    %if getattr(thing, 'author', None):\n        <%utils:atom_author author=\"${thing.author}\"/>\n    %endif\n\n    <%utils:atom_content>\n        %if thing.header:\n            <img src=\"${header_url(thing.header, c.secure)}\" />\n        %endif\n\n        %if thing.public_description:\n            <div>\n                ${thing.public_description}\n            </div>\n        %endif\n        <div>\n            <a href=\"${url}\">${_(\"[link]\")}</a>\n        </div>\n    </%utils:atom_content>\n\n    <id>${thing._fullname}</id>\n    <link href=\"${url}\" />\n    <updated>${html_datetime(thing._date)}</updated>\n    <title>${thing.title}</title>\n</entry>\n"
  },
  {
    "path": "r2/r2/templates/subredditfacets.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n   from r2.lib.template_helpers import search_url\n%>\n\n%if thing.facets and len(thing.facets) > 1:\n<div class=\"searchfacets\">\n  <h4 class=\"title\">${_(\"too many results? narrow it down to a subreddit!\")}</h4>\n  <ol class=\"list\">\n  %for subreddit, count in thing.facets:\n    <li class=\"searchfacet reddit\">\n      <a class=\"facet title word\" href=\"${search_url(thing.prev_search, subreddit.name, restrict_sr='on', sort=thing.sort, recent=thing.recent, ref='search_facets')}\">/r/${subreddit.name}</a>&nbsp;\n      <span class=\"facet count number\">(${count})</span>\n    </li>&nbsp;\n  %endfor\n  </ol>\n</div>\n%endif\n"
  },
  {
    "path": "r2/r2/templates/subredditinfobar.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n   from r2.config import feature\n   from r2.lib.filters import websafe\n   from r2.lib.strings import strings, Score\n   from r2.lib.pages import WrappedUser, QuarantineOptoutButton, SubscribeButton\n   from r2.lib.template_helpers import _ws, _wsf\n   from r2.models.listing import ModListing\n %>\n\n<%namespace file=\"utils.html\" import=\"plain_link, thing_timestamp, text_with_links, _mdf\"/>\n<%namespace file=\"printablebuttons.html\" import=\"ynbutton, state_button\" />\n\n<div class=\"titlebox\">\n  <h1 class=\"hover redditname\">\n    ${plain_link(thing.sr.name, thing.sr.path, _sr_path=False, _class=\"hover\")}\n  </h1>\n\n  %if thing.quarantine:\n    <div class=\"quarantine-notice\">\n      <div class=\"md-container\">\n        ${_mdf(\"This community is [quarantined](%(link)s) because of its shocking or highly offensive content.\",\n               wrap=True, link='https://reddit.zendesk.com/hc/en-us/articles/205701245')}\n      </div>\n      <form method='post' action='/api/quarantine_optout'>\n        <input type=\"hidden\" name=\"uh\" value=\"${c.modhash}\"/>\n        <input type=\"hidden\" name=\"sr_name\" value=\"${thing.sr.name}\"/>\n        <button class=\"c-btn btn-quarantine\" name=\"submit\" value=\"submit\" type=\"submit\">\n          ${_(\"Leave this community\")}\n        </button>\n      </form>\n    </div>\n  %endif\n\n  ${SubscribeButton(thing.sr)}\n  %if not thing.sr.hide_num_users_info:\n  <span class=\"subscribers\">${unsafe(Score.readers(thing.subscribers))}</span>\n\n    %if thing.active_visitors and thing.active_visitors.logged_in:\n  <p class=\"users-online ${'fuzzed' if thing.active_visitors.logged_in.is_fuzzed else ''}\" title=\"${_('logged-in users viewing this subreddit in the past 15 minutes')}\">\n    ${unsafe(Score.users_here_now(thing.active_visitors.logged_in.count, prepend='~' if thing.active_visitors.logged_in.is_fuzzed else ''))}\n  </p>\n    %endif\n  %endif\n\n  %if thing.sr.moderator:\n    <div class=\"leavemoderator\">\n      ${text_with_links(ModListing.remove_self_title % dict(action='(%(action)s)'),\n          _sr_path=True,\n          action=dict(\n            ## TRANSLATORS: this label links to the edit moderators page.\n            link_text=_('change'),\n            path='about/moderators'))}\n    </div>\n  %endif\n\n  %if thing.sr.contributor:\n    ${ynbutton(op='leavecontributor',\n               title=_('leave'),\n               executed=_('you are no longer an approved submitter'),\n               question=_('stop being an approved submitter?'),\n               format=_('you are an approved submitter on this subreddit. (%(leave)s)'),\n               format_arg='leave',\n               hidden_data=dict(\n                 id=thing.sr._fullname),\n               access_required=False,\n              )}\n  %endif\n\n  %if thing.sr_style_toggle:\n    <form class=\"toggle sr_style_toggle\">\n      <input id=\"sr_style_enabled\" type=\"checkbox\" name=\"sr_style_enabled\"\n        ${'checked=\"checked\"' if thing.use_subreddit_style else \"\"}\n      >\n      <label for=\"sr_style_enabled\">\n        ${_(\"Show this subreddit's theme\")}\n      </label>\n      <div id=\"sr_style_throbber\" class=\"throbber\"></div>\n    </form>\n  %endif\n\n  %if thing.flair_prefs:\n    ${thing.flair_prefs}\n  %endif\n\n  %if thing.description_usertext:\n    ${thing.description_usertext}\n  %endif\n\n  <div class=\"bottom\">\n    %if thing.sr.author:\n      ${_wsf(\"created by %(user)s\", user=unsafe(thing.creator_text))}\n    %endif\n    \n    <span class=\"age\">\n      ${_(\"a community for\")}&#32;${thing_timestamp(thing.sr)}\n    </span>  \n  </div>\n\n  %if c.user_is_admin:\n    <div class=\"raisedbox spacer\">\n      %if thing.sr.ban_count:\n        <p>\n          ${strings.times_banned % thing.sr.ban_count}\n        </p>\n      %endif\n      \n      %if thing.sr._spam:\n        ${ynbutton(op='approve',\n                   title=_('approve this subreddit'), \n                   executed=_('approved'),\n                   hidden_data = dict(id = thing.sr._fullname),\n        )}\n        \n        <form id=\"banmessage-form\" method=\"post\" onsubmit=\"return post_form(this, 'admin/add_ban_message')\">\n          <input type=\"hidden\" name=\"thing\" value=\"${thing.sr._fullname}\">\n          <input type=\"hidden\" name=\"system\" value=\"subreddit\">\n          <textarea name=\"message\" rows=4>\n            %if getattr(thing.sr, 'ban_info', {}) and 'message' in thing.sr.ban_info:\n              ${thing.sr.ban_info['message'] or ''}\n            %endif\n          </textarea>\n          <input type=\"submit\" class=\"notes-button\" \n            value=\"Set public ban message (blank for default)\">\n        </form>\n        \n        %if hasattr(thing.sr, \"banner\"):\n          <p>\n           ${strings.banned_by % thing.sr.banner}\n          </p>\n        %endif\n        %if getattr(thing.sr, 'ban_info', {}):\n          <p>\n            ${strings.time_banned % thing.sr.ban_info.get('banned_at') or 'N/A'}\n          </p>\n        %endif\n\n      %else:\n        ${ynbutton(op='remove',\n                   title=_('ban this subreddit'), \n                   executed=_('banned'),\n                   hidden_data = dict(id = thing.sr._fullname),\n        )}\n        %if getattr(thing.sr, 'ban_info', {}):\n          <p>\n            ${strings.time_approved % thing.sr.ban_info.get('unbanned_at') or 'N/A'}\n          </p>\n        %endif\n      %endif\n    </div>\n    <div class=\"clear\"></div>\n\n    <div class=\"quarantine-tool raisedbox spacer collapsed\">\n      <div name=\"expander\">\n        <a href=\"javascript:void(0)\" class=\"expand\" onclick=\"return toggleSrQuarantine(this)\">\n          ${\"[%s]\" % \"+\"}\n        </a>\n        ${\"Show unquarantine tool\" if thing.sr.quarantine else \"Show quarantine tool\"}\n      </div>\n      <div class=\"quarantine-info\">\n        %if not thing.sr.quarantine:\n          <%\n            subject = \"ATTN: Your subreddit has been quarantined\"\n            body = \"Your subreddit has been [quarantined](https://reddit.zendesk.com/hc/en-us/articles/205701245) due to offensive content.\"\n            button_label = \"Quarantine and send modmail\"\n          %>\n        %else:\n          <%\n            subject = \"ATTN: Your subreddit is no longer quarantined\"\n            body = \"Your subreddit has been unquarantined.\"\n            button_label = \"Unquarantine and send modmail\"\n          %>\n        %endif\n        <form id=\"quarantinemessage-form\" method=\"post\" onsubmit=\"return post_form(this, 'quarantine')\">\n          <input type=\"hidden\" name=\"subreddit\" value=\"${thing.sr._fullname}\">\n          <input type=\"hidden\" name=\"quarantine\" value=\"${not thing.sr.quarantine}\">\n          <label for=\"subject\">Subject:</label>\n          <br>\n          <textarea name=\"subject\">${subject}</textarea>\n          <br>\n          <label for=\"body\">Body:</label>\n          <br>\n          <textarea name=\"body\" rows=4>${body}</textarea>\n          <br>\n          <input type=\"submit\" class=\"notes-button\" value=\"${button_label}\">\n          <br>\n          <label>Message will be sent by u/reddit and won't send if left blank</label>\n        </form>\n      </div>\n    </div>\n  %endif\n\n  <div class=\"clear\"> </div>\n  \n\n</div>\n\n"
  },
  {
    "path": "r2/r2/templates/subredditreportform.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n<%namespace file=\"utils.html\" import=\"error_field\" />\n\n<%def name=\"report_form_base(fullname, rules_page_link)\">\n    <form id=\"report-action-form\"\n            class=\"subreddit-report-form rounded\">\n        <input type=\"hidden\"\n                name=\"thing_id\"\n                value=\"${fullname}\">\n        <a href=\"${rules_page_link}\"\n                class=\"action-icon action-icon-info c-hide-text may-blank\"\n                title=\"${_('View the community rules')}\">\n            ${_('View the community rules')}\n        </a>\n        <div class=\"reason-prompt report-header\">\n            ${_(\"What rule does this break?\")}\n        </div>\n\n        ${caller.body()}\n\n        <div class=\"c-submit-group\">\n            <button type=\"button\" class=\"btn c-btn c-btn-secondary report-cancel\">\n                ${_(\"cancel\")}\n            </button>&#32;\n            <button type=\"submit\" class=\"btn c-btn c-btn-primary submit-action-thing\" disabled>\n                ${_(\"report\")}\n            </button>\n        </div>\n        <span class=\"status\" style=\"display: none\"></span>\n    </form>\n</%def>\n\n<%def name=\"subreddit_report_form(fullname, system_rules, rules, sr_name)\">\n    <%\n        rules_page_link = \"/r/%s/about/rules\" % sr_name\n    %>\n    <%self:report_form_base\n            fullname=\"${fullname}\"\n            rules_page_link=\"${unsafe(rules_page_link)}\"\n            >\n        <ol class=\"report-reason-list\">\n            %if rules:\n                %for rule in rules:\n                    ${self.report_form_reason(rule.get(\"short_name\"))}\n                %endfor\n            %endif\n            <li class=\"report-reason-item report-reason-reddit\" \n                <label>\n                    <input type=\"radio\"\n                            class=\"site-reason-radio\"\n                            name=\"reason\"\n                            value=\"site_reason_selected\">\n                    <div class=\"report-reason-display\">\n                        <select name=\"site_reason\">\n                            %for rule in system_rules:\n                                <option value=\"${rule}\">\n                                    ${_(\"Reddit rule: %(rule_name)s\" % dict(rule_name=rule))}\n                                </option>\n                            %endfor\n                        </select>\n                    </div>\n                </label>\n            </li>\n            ${self.report_form_reason_other()}\n            <div class=\"report-header\">\n                ${_(\"Reports go to community moderators anonymously\")}\n            </div>\n        </ol>\n    </%self:report_form_base>\n</%def>\n\n<%def name=\"reddit_report_form(fullname, system_rules, sr_name=None)\">\n    <%self:report_form_base\n            fullname=\"${fullname}\"\n            rules_page_link=\"/help/contentpolicy\"\n            >\n        <ol class=\"report-reason-list\">\n            %for rule in system_rules:\n                ${self.report_form_reason(rule)}\n            %endfor\n            ${self.report_form_reason_other()}\n        </ol>\n        <div class=\"report-header\">\n            %if sr_name:\n                ${_(\"Reports go to community moderators anonymously\")}\n            %else:\n                ${_(\"Reports go to Reddit admins\")}\n            %endif\n        </div>\n    </%self:report_form_base>\n</%def>\n\n<%def name=\"report_form_reason_other()\">\n    <li class=\"report-reason-item report-reason-other\">\n        <label>\n            <input type=\"radio\"\n                    name=\"reason\"\n                    value=\"other\">\n            <div class=\"report-reason-display\">\n                <div>\n                    ${_(\"Other (max %(num)s characters):\") % dict(num=100)}\n                </div>\n                <input type=\"text\"\n                        class=\"c-form-control\"\n                        name=\"other_reason\"\n                        value=\"\"\n                        maxlength=\"100\"\n                        disabled>\n                ${error_field(\"TOO_LONG\", \"other_reason\", \"span\")}\n            </div>\n        </label>\n    </li>\n</%def>\n\n<%def name=\"report_form_reason(rule)\">\n    <li class=\"report-reason-item\">\n        <label>\n          <input type=\"radio\" name=\"reason\" value=\"${rule}\">\n          <div class=\"report-reason-display\">${rule}</div>\n        </label>\n    </li>\n</%def>\n\n%if thing.rules:\n    ${self.subreddit_report_form(\n        fullname=thing.thing_fullname,\n        rules=thing.rules,\n        system_rules=thing.system_rules,\n        sr_name=thing.sr_name,\n    )}\n%else:\n    ${self.reddit_report_form(\n        fullname=thing.thing_fullname,\n        system_rules=thing.system_rules,\n    )}\n%endif\n"
  },
  {
    "path": "r2/r2/templates/subredditselector.html",
    "content": "<%namespace file=\"utils.html\" import=\"error_field\"/>\n\n<%doc>\n  Fires custom event when subreddit selection changes. Add handler like:\n  $(\"#sr-autocomplete\").bind(\"sr-changed\", function() { dostuff; })\n</%doc>\n\n<div id=\"sr-autocomplete-area\">\n  <input id=\"sr-autocomplete\" name=\"sr\" type=\"text\"\n         autocomplete=\"off\"\n         %if thing.include_searches:\n           onkeyup=\"sr_name_up(event)\"\n         %endif\n         onkeydown=\"return sr_name_down(event)\"\n         onblur=\"hide_sr_name_list()\"\n         % if thing.default_sr:\n           value=\"${thing.default_sr.name}\"\n         % endif\n         % if thing.required:\n         required\n         % endif\n         % if thing.class_name:\n          class=\"${thing.class_name}\"\n         % endif\n         % if thing.placeholder:\n          placeholder=\"${thing.placeholder}\"\n         % endif\n         />\n  % if thing.show_add:\n    <button class=\"add\">${_(\"add\")}</button>\n  % endif\n  <ul id=\"sr-drop-down\">\n    <li class=\"sr-name-row\"\n        onmouseover=\"highlight_reddit(this)\"\n        onmousedown=\"return sr_dropdown_mdown(this)\"\n        onmouseup=\"sr_dropdown_mup(this)\">nothin</li>\n  </ul>\n</div>\n<script type=\"text/javascript\">\n  r.config.sr_cache = ${unsafe(thing.sr_searches)};\n</script>\n${error_field(\"SUBREDDIT_NOEXIST\", \"sr\", \"div\")}\n${error_field(\"SUBREDDIT_NOTALLOWED\", \"sr\", \"div\")}\n${error_field(\"SUBREDDIT_REQUIRED\", \"sr\", \"div\")}\n\n<div id=\"suggested-reddits\">\n  % for title, subreddits in thing.subreddit_names:\n    <h3>${title}</h3>\n    <ul>\n      %for name in subreddits:\n      <li>\n        <a href=\"#\" tabindex=\"100\" onclick=\"set_sr_name(this); return false\">${name}</a>&#32;\n      </li>\n      %endfor\n    </ul>\n  % endfor\n</div>\n"
  },
  {
    "path": "r2/r2/templates/subredditstylesheet.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n  from r2.lib.filters import keep_space\n  from r2.lib.template_helpers import add_sr, static\n  import os\n\n%>\n<%namespace file=\"utils.html\" import=\"error_field, image_upload\"/>\n<%namespace file=\"subredditstylesheetbase.html\" import=\"make_li\"/>\n\n<div class=\"stylesheet-customize-container\">\n  <form\n     onsubmit=\"return post_form(this, 'subreddit_stylesheet')\"\n     name=\"subreddit_stylesheet\" id=\"subreddit_stylesheet\"\n     class=\"pretty-form sr-form\"\n     action=\"/post/subreddit_stylesheet\" method=\"post\" >\n    \n  <input type=\"hidden\" name=\"r\"  value=\"${thing.site.name}\" />\n  <input type=\"hidden\" name=\"op\"  value=\"\" />\n\n  <h2>${_(\"stylesheet\")}</h2>\n  <div class=\"sheets\">\n    <div class=\"col\">\n      <div>\n        <textarea\n           rows=\"20\"\n           cols=\"20\"\n           id=\"stylesheet_contents\"\n           name=\"stylesheet_contents\"\n           >\n          ${keep_space(thing.stylesheet_contents) or ''}\n        </textarea>\n        <div>\n            <label for=\"reason\">${_('reason for revision')}</label>\n            <input type=\"text\" name=\"reason\" maxlength=\"256\">\n            % if thing.site.prev_stylesheet:\n                <span class=\"btn right\"><a target=\"_blank\" href=\"${add_sr(\"/wiki/revisions/config/stylesheet/\")}\">${_(\"see previous versions\")}</a></span>\n            % endif\n        </div>\n      </div>\n    </div>\n    <div class=\"clearleft\"></div>\n    <div class=\"buttons\">\n      <button class=\"btn\" name=\"save\" type=\"submit\" \n             onclick=\"this.form.op.value='save'; return true;\">\n        ${_('save')}\n      </button>\n      <button class=\"btn\" name=\"preview\" type=\"submit\" \n             onclick=\"this.form.op.value='preview'; return true;\">\n        ${_('preview')}\n      </button>\n      <span class=\"status error\"></span>\n    </div>\n  </div>\n  <div class=\"errors\" style=\"display:none\">\n    <h2>${_(\"errors\")}</h2>\n    <ul><li></li>\n      <!-- populated from AJAX requests to /api/subreddit_stylesheet -->\n    </ul>\n  </div>\n  \n  </form>\n\n  <div id=\"preview-table\" style=\"display:none\">\n    <h2><a name=\"preview\">${_(\"preview\")}</a></h2>\n    <table>\n      <tr>\n        <th>${_(\"normal link\")}</th>\n        <td id=\"preview_link_normal\"></td>\n      </tr>\n      <tr>\n        <th>${_(\"compressed link\")}</th>\n        <td id=\"preview_link_compressed\"></td>\n      </tr>\n      <tr>\n        <th>${_(\"link with thumbnail\")}</th>\n        <td id=\"preview_link_media\"></td>\n      </tr>\n      <tr>\n        <th>${_(\"stickied link\")}</th>\n        <td id=\"preview_link_stickied\"></td>\n      </tr>\n      <tr>\n        <th>${_(\"comment\")}</th>\n        <td id=\"preview_comment\"></td>\n      </tr>\n      <tr>\n        <th>${_(\"gilded comment\")}</th>\n        <td id=\"preview_comment_gilded\"></td>\n      </tr>\n    </table>\n  </div>\n\n  %if thing.allow_image_upload:\n    <div id=\"images\">\n      <h2><a name=\"images\">${_(\"images\")}</a></h2>\n\n    <%call expr=\"image_upload('/api/upload_sr_img', '', \n                                onchange='return file_changed(this)',\n                                label = _('image file'), ask_type=True)\">\n      \n      <br/>\n      <label for=\"img-name\">${_(\"new image name:\")}</label>\n      <input id=\"img-name\" name=\"name\" value=\"\" type=\"text\"/>\n      ${error_field(\"BAD_CSS_NAME\", \"name\")}\n      <br/>\n      <span class=\"little gray\">\n        ${_(\"(image names should consist of alphanumeric characters and '-' only)\")}\n      </span>\n    </%call>\n  <p class=\"error\">\n    ${_(\"Note: any changes to images here will be reflected immediately on reload and cannot be undone.\")}\n  </p>\n      <script type=\"text/javascript\">\n        /* <![CDATA[ */\n          function create_new_image(name) {\n                var list = $(\".image-list:first\");\n                var new_li = list.children(\"li:first\")\n                    .clone(true).attr(\"id\", \"\")\n                    .find(\".img-name\").html(name).end()\n                    .find(\".img-url\").html(\"url(%%\" + name + \"%%)\").end()\n                    .find(\"form input[name=img_name]\").val(name).end()\n                    .find(\"img\").attr(\"id\", \"img-preview-\" + name).end();\n                \n                list.append(new_li);\n                img = new_li.find(\"img\");\n                \n                $(\"#old-names\").append(\"<option>\" + name + \"</option>\");\n                return img;           \n          }\n\n          function on_image_success(img) {\n             $(img).parents(\"li:first\").fadeIn();\n             $(img).parent(\"a\").attr(\"href\", $(img).attr(\"src\"));\n          }\n\n          function paste_url(source) {\n              var txt = $(source).siblings(\"pre:first\").html();\n              $(\"#stylesheet_contents\").insertAtCursor(txt);\n              return false; \n          }\n          function delete_img(button) {\n              $(button).parents(\"li:first\").fadeOut(function() {\n                  $(this).remove();\n              })\n          }\n          function file_changed(file_input) {\n              $(\"#submit-header-img\").show();\n              $(\".img-status\").html(\"\");\n              if(file_input.value) {\n                  if(! $('#img-name').val()) {\n                     var f = file_input.value\n                          .replace(/.*[\\/\\\\]/, \"\").split('.')[0]\n                          .replace(/[ _]/g, \"-\");\n                      $('#img-name').val(f);\n                  }\n\n                  var ext = file_input.value\n                      .split('.').pop().toLowerCase()\n                      .replace(\"jpeg\", \"jpg\");\n                  if (ext == 'png' || ext == 'jpg') {\n                      $('input:radio[name=img_type]').attr('checked', false);\n                      $('input:radio[name=img_type][value=\"' + ext + '\"]').attr('checked', true);\n                  }\n              }\n          }\n      /* ]]> */\n      </script>\n      <ul id=\"image-preview-list\" class=\"image-list\">\n        ${make_li(prototype=True)}\n        %for name, url in thing.images.iteritems():\n           ${make_li(name=name, img=url)}\n        %endfor\n      </ul>\n\n      <iframe src=\"about:blank\" width=\"600\" height=\"200\" style=\"display: none;\"\n              name=\"upload-iframe\" id=\"upload-iframe\"></iframe>\n      \n    </div>\n  %endif\n</div>\n\n"
  },
  {
    "path": "r2/r2/templates/subredditstylesheetbase.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%namespace file=\"printablebuttons.html\" import=\"ynbutton\"/>\n\n<%def name=\"make_li(name='', img=None, prototype=False, mod=True)\">\n  <li ${\"style='display:none'\" if img is None else \"\"}>\n    <a href=\"${img}\" class=\"preview\">\n      <img id=\"img-preview-${name}\" src=\"${img}\" \n           alt=\"Image ${name}\" title=\"click to preview\"/>\n    </a>\n    <div class=\"description\">\n      <b class=\"img-name\">\n        ${name}\n      </b>\n      <br/>\n      <span>link:</span>\n      <pre class=\"img-url\">url(%%${name}%%)</pre>\n      %if mod:\n        <br/>\n        <a href=\"javascript:void(0)\" onclick=\"return paste_url(this)\">\n          ${_(\"paste into stylesheet\")}\n        </a>\n        <br/>\n        ${ynbutton(_(\"delete this image\"), _(\"deleted\"),\n                   \"delete_sr_img\", callback = \"delete_img\",\n                    hidden_data = dict(img_name = name))}\n      %endif\n    </div>\n  </li>\n</%def>\n\n"
  },
  {
    "path": "r2/r2/templates/subredditstylesheetsource.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n    from r2.lib import js\n    from r2.lib.filters import SC_OFF, SC_ON\n    from r2.lib.template_helpers import static\n%>\n<%namespace file=\"subredditstylesheetbase.html\" import=\"make_li\"/>\n\n\n%if thing.stylesheet_contents:\n  <link rel=\"stylesheet\" type=\"text/css\" href=\"${static(\"highlight.css\")}\">\n\n  <pre class=\"subreddit-stylesheet-source\">\n  <code class=\"language-css\">${unsafe(SC_OFF)}${thing.stylesheet_contents}${unsafe(SC_ON)}</code>\n  </pre>\n\n  ${unsafe(js.use(\"highlight\"))}\n%endif\n\n%if thing.images:\n  <div id=\"images\">\n    <h2><a name=\"images\">${_(\"images\")}</a></h2>\n    <ul id=\"image-preview-list\" class=\"image-list\">\n      %for name, url in thing.images.iteritems():\n         ${make_li(name=name, img=url, mod=False)}\n      %endfor\n    </ul>\n%endif\n\n%if not (thing.stylesheet_contents or thing.images):\n  <p class=\"error\">${_(\"there doesn't seem to be anything here\")}</p>\n%endif\n"
  },
  {
    "path": "r2/r2/templates/subreddittopbar.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n   from r2.lib.template_helpers import format_html\n%>\n<%namespace file=\"utils.html\" import=\"plain_link\"/>\n\n<div id=\"sr-header-area\">\n  <div class=\"width-clip\">\n    ${thing.my_subreddits_dropdown}\n\n    <div class=\"sr-list\">\n      %for m in thing.sr_bar():\n        ${m.render()}\n      %endfor\n\n      <%\n        editmore = 'edit' if c.user_is_loggedin else 'more'\n      %>\n    </div>\n\n    ${plain_link(format_html(\"%s &raquo;\", _(editmore)),\n                 \"/subreddits/\", id=\"sr-more-link\")}\n  </div>\n</div>\n"
  },
  {
    "path": "r2/r2/templates/subreddittraffic.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%inherit file=\"reddittraffic.html\"/>\n<%namespace file=\"reddittraffic.html\" import=\"load_timeseries_js\"/>\n<%namespace file=\"utils.html\" import=\"_md\"/>\n\n<%!\n    from r2.lib.filters import safemarkdown\n    from r2.lib.strings import strings\n%>\n\n<%def name=\"preamble()\">\n  ${unsafe(safemarkdown(strings.traffic_subreddit_explanation % dict(subreddit=thing.place)))}\n  <p>${_md(\"All times are in [UTC](http://en.wikipedia.org/wiki/UTC).\", wrap=True)}</p>\n\n  ${load_timeseries_js()}\n</%def>\n"
  },
  {
    "path": "r2/r2/templates/subreddittrafficreport.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n  from r2.lib.filters import SC_OFF, SC_ON\n  from r2.lib.template_helpers import js_timestamp, format_number\n%>\n\n<div class=\"raisedbox traffic-form\">\n<p>${_(\"Enter one subreddit per line. Traffic numbers shown are for the last full month.\")}</p>\n\n<form method=\"GET\" action=\"/traffic/subreddits/report\">\n  ${unsafe(SC_OFF)}<textarea name=\"subreddits\" cols=\"30\" rows=\"20\">${thing.textarea}</textarea>${unsafe(SC_ON)}\n  <input type=\"submit\" value=\"${_('make report')}\">\n</form>\n</div>\n\n% if thing.report or thing.invalid_srs:\n<table class=\"traffic-table\">\n  <caption>${_(\"subreddit traffic\")} <span class=\"normal\"><a href=\"${thing.csv_url}\">(download as .csv)</a></span></caption>\n  <thead>\n    <tr>\n      <th scope=\"col\">${_(\"subreddit\")}</th>\n      <th scope=\"col\">${_(\"uniques\")}</th>\n      <th scope=\"col\">${_(\"pageviews\")}</th>\n    </tr>\n  </thead>\n  <tbody>\n    % for (name, url), data in thing.report:\n    <tr>\n    % if url:\n      <th scope=\"row\"><a href=\"${url}\">${name}</a></th>\n    % else:\n      <th scope=\"row\">${name}</th>\n    % endif\n      % for datum in data:\n      <td>${format_number(datum)}</td>\n      % endfor\n    </tr>\n    % endfor\n    % for name in thing.invalid_srs:\n    <tr>\n      <th scope=\"row\" class=\"error\">${name}</th>\n      <td colspan=\"2\">${_(\"not found\")}</td>\n    </tr>\n    % endfor\n  </tbody>\n</table>\n% endif\n"
  },
  {
    "path": "r2/r2/templates/subscribebutton.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%namespace file=\"printablebuttons.html\" import=\"toggle_button\"/>\n\n${self.subscribe_button(thing.sr, thing.data_attrs)}\n\n<%def name=\"subscribe_button(sr, data_attrs, css_class='subscribe-button')\">\n    ${toggle_button(\n        class_name=\"fancy-toggle-button \" + css_class,\n        title=_(\"subscribe\"),\n        alt_title=_(\"unsubscribe\"),\n        callback=\"subscribe('%s')\" % sr._fullname,\n        cancelback=\"unsubscribe('%s')\" % sr._fullname,\n        css_class=\"add\",\n        alt_css_class=\"remove\",\n        reverse=sr.subscriber,\n        login_required=True,\n        data_attrs=data_attrs,\n    )}\n</%def>\n"
  },
  {
    "path": "r2/r2/templates/subscriptionbox.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%namespace file=\"utils.html\" import=\"plain_link\"/>\n<%namespace file=\"subreddit.html\" import=\"permission_icons\"/>\n\n<%\n  from r2.models import MultiReddit\n  from r2.lib.pages import SubscribeButton\n  is_multi = isinstance(c.site, MultiReddit)\n%>\n\n<div class=\"subscription-box\">\n  % if thing.prelink or thing.goldlink:\n  <div class=\"box-top\">\n    % if thing.prelink:\n    <span class=\"column centered\">\n      <a href=\"${thing.prelink[0]}\">${thing.prelink[1]}</a>\n    </span>\n    % endif\n    % if thing.goldlink:\n    <span class=\"giftgold column\">\n    <a href=\"${thing.goldlink}\">\n      ${thing.goldmsg}\n    </a>\n    </span>\n    % endif\n  </div>\n  % endif\n  <div class=\"clear\">\n  % if thing.prelink or thing.goldlink:\n    <div class=\"box-separator\"></div>\n  % endif\n    <ul>\n    %if thing.multi_path and thing.multi_text:\n      ${plain_link(thing.multi_text, thing.multi_path, _class=\"title\")}\n      <div class=\"box-separator\"></div>\n    %endif\n    %for sr in thing.reddits:\n      <% is_spam = hasattr(sr, \"_spam\") and sr._spam %>\n      <li>\n        %if is_multi and is_spam:\n          <span class=\"fancy-toggle-button\">\n            <span class=\"active banned\">${_(\"banned\")}</span>\n          </span>\n        %else:\n          ${SubscribeButton(sr)}\n        %endif\n\n        %if is_spam:\n          <span class=\"title banned\">${sr.name}</span>\n        %else:\n          ${plain_link(sr.name, sr.path, _class=\"title\")}\n        %endif\n\n        ${permission_icons(sr)}\n      </li>\n    %endfor\n    </ul>\n  </div>\n</div>\n"
  },
  {
    "path": "r2/r2/templates/suspiciouspaymentemail.email",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n    from r2.lib.promote import promo_edit_url\n%>\n\n<%namespace file=\"utils.html\" import=\"plain_link\"/>\n\nThere is suspicious payment activity by ${plain_link(\"/u/%s\" % thing.user.name, \"/user/%s/promoted\" % thing.user.name, _sr_path=False)}.\n\nThis report was triggered by the promotion ${plain_link(thing.link.title, promo_edit_url(thing.link), _sr_path=False)}.\n"
  },
  {
    "path": "r2/r2/templates/tabbedpane.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n${thing.tabmenu}\n<div class=\"tabpane-content\">\n  %for i, (name, title, pane) in enumerate(thing.tabs):\n    <div id=\"tabbedpane-${name}\" class=\"tabbedpane\"\n         ${\"style='display:none'\" if i > 0 else \"\"}>\n      ${pane}\n    </div>\n  %endfor\n</div>\n%if thing.linkable:\n  <script>\n    $(function() {\n        var target = \"tab-\" + $(window.location).attr(\"hash\").substr(1);\n        $(\".tabmenu li\").each(function() {\n            if (this.id == target) {\n                $(this).find(\"a\").click();\n            }\n        });\n    });\n  </script>\n%endif\n"
  },
  {
    "path": "r2/r2/templates/tablelisting.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n   from r2.lib.template_helpers import format_html\n %>\n<%namespace file=\"utils.html\" import=\"plain_link\" />\n\n<%\n   _id = (\"_%s\" % thing.parent_name) if hasattr(thing, 'parent_name') else ''\n   cls = thing.lookups[0].__class__.__name__.lower()\n %>\n\n<style type=\"text/css\">\n  .generic-table {\n    font-size: small;\n    color: black;\n    margin-left: 5px;}\n</style>\n\n<div id=\"siteTable${_id}\" class=\"sitetable ${cls}\">\n  <table class=\"generic-table\">\n  %for a in thing.things:\n      ${a}\n  %endfor\n  </table>\n</div>\n\n%if thing.nextprev and (thing.prev or thing.next):\n  <p class=\"nextprev\"> ${_(\"view more:\")}&#32;\n  %if thing.prev:\n    ${plain_link(_(\"first\"), thing.first, rel=\"nofollow first\")}\n    <span class=\"separator\"></span>\n    ${plain_link(format_html(\"&lsaquo; %s\", _(\"prev\")), thing.prev, rel=\"nofollow prev\")}\n  %endif\n  %if thing.prev and thing.next:\n    <span class=\"separator\"></span>\n  %endif\n  %if thing.next:\n  ${plain_link(format_html(\"%s &rsaquo;\", _(\"next\")), thing.next, rel=\"nofollow next\")}\n  %endif\n  </p>\n%endif\n%if not thing.things:\n  <p id=\"noresults\" class=\"error\">${_(\"there doesn't seem to be anything here\")}</p>\n%endif\n"
  },
  {
    "path": "r2/r2/templates/takedownpane.compact",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%inherit file=\"takedownpane.html\" />\n"
  },
  {
    "path": "r2/r2/templates/takedownpane.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n    from r2.lib.filters import unsafe, safemarkdown, keep_space\n    from r2.lib.template_helpers import static\n%>\n\n<div class=\"infobar red\">\n%if getattr(thing.link, \"takedown_img\", True):\n  <img src=\"${static('gagged-alien.png')}\"/>\n%else:\n  <img src=\"${static('noimage.png')}\"/>\n%endif\n${unsafe(safemarkdown(thing.explanation, nofollow=True))}\n<div class=\"clearleft\"></div>\n</div>\n"
  },
  {
    "path": "r2/r2/templates/thanks.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%namespace file=\"utils.html\" import=\"error_field, radio_type\"/>\n<%namespace name=\"utils\" file=\"utils.html\"/>\n\n<%! from r2.lib.strings import strings %>\n\n<form id=\"thanks\" action=\"/api/claimgold\" method=\"post\"\n      class=\"content\"\n      onsubmit=\"return post_form(this, 'claimgold');\">\n%if thing.status == \"creddits\":\n  <h1>${_(\"thanks for buying creddits!\")}</h1>\n%else:\n  <h1>${_(\"thanks for subscribing!\")}</h1>\n%endif\n\n<p class=\"blurb\">\n%if thing.status == \"creddits\":\n  ${_(\"enter your confirmation code below to claim your gift creddits\")}\n%elif thing.status == \"mundane\":\n  ${_(\"enter your confirmation code below to activate reddit gold\")}\n%else:\n  ${_(\"You're already a reddit gold subscriber.\")}\n  &#32;\n  ${_(\"But if you just gave us even more money, enter the new confirmation code below and we'll add the extra credit to your account.\")}\n</p>\n%endif\n\n<div class=\"spacer\">\n  <%utils:round_field title=\"\">\n    <input type=\"text\" name=\"code\" value=\"${thing.secret}\" />\n    ${error_field(\"INVALID_CODE\", \"code\")}\n    ${error_field(\"CLAIMED_CODE\", \"code\")}\n    ${error_field(\"NO_TEXT\", \"code\")}\n  </%utils:round_field>\n</div>\n\n<button type=\"submit\" class=\"btn\">${_(\"claim\")}</button>\n</form>\n"
  },
  {
    "path": "r2/r2/templates/thingupdater.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%\n  from r2.lib.filters import scriptsafe_dumps\n %>\n<script type=\"text/javascript\">\n  (function() {\n    var updates = ${scriptsafe_dumps(thing.updates)};\n\n    if (!updates || !updates.length) {\n      return;\n    }\n\n    var delayedUpdates = [];\n    var update;\n\n    updates.forEach(function(update) {\n      var thing = document.getElementById('thing_' + update.id);\n\n      if (!thing) {\n        return;\n      }\n\n      $(thing).updateThing(update);\n\n      if (update.gilded) {\n        delayedUpdates.push(update);\n      }\n    });\n\n    /* Delay until dom ready because this may be called before r.gold is defined otherwise. */\n    $(function() {\n      delayedUpdates.forEach(function(update) {\n        var gilding_data = update.gilded;\n\n        r.gold.gildThing(update.id, gilding_data[0], gilding_data[1]);\n      });\n    });\n  })();\n</script>\n"
  },
  {
    "path": "r2/r2/templates/timeserieschart.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n    import time\n    import babel.dates\n    from r2.lib.template_helpers import js_timestamp, format_number\n%>\n\n<%\n    month_names = babel.dates.get_month_names(locale=c.locale)\n%>\n\n<table id=\"${thing.id}\" class=\"timeseries ${thing.classes}\" data-interval=\"${thing.interval}\">\n<caption>${thing.title}</caption>\n<thead>\n<tr>\n  <th scope=\"col\">${_(\"date\")}</th>\n  % for col in thing.columns:\n  % if \"color\" in col:\n  <th scope=\"col\" title=\"${col['title']}\" data-color=\"${col['color']}\">${col['shortname']}</th>\n  % else:\n  <th>${col['shortname']}</th>\n  % endif\n  % endfor\n</tr>\n</thead>\n<tbody>\n% for date, data in thing.rows:\n<tr\n  % if thing.interval == \"day\" and date.weekday() in (5, 6):\n  class=\"dow-${date.weekday()}\"\n  % endif\n  >\n  <th data-value=\"${js_timestamp(date)}\" scope=\"row\">\n      % if thing.make_period_link:\n      <a href=\"${thing.make_period_link(thing.interval, date)}\">\n      % endif\n      % if thing.interval == \"hour\":\n      ${babel.dates.format_datetime(date, format=\"short\", locale=c.locale)}\n      % elif thing.interval == \"day\":\n      ${babel.dates.format_date(date, format=\"short\", locale=c.locale)}\n      % else:\n      ${month_names[date.month]}\n      % endif\n      % if thing.make_period_link:\n      </a>\n      % endif\n  </th>\n  % for datum in data:\n  % if date < thing.latest_available_data:\n  <td data-value=\"${datum}\">${format_number(datum)}</td>\n  % else:\n  <td data-value=\"-1\">${_(\"unavailable\")}</td>\n  % endif\n  % endfor\n</tr>\n% endfor\n</tbody>\n</table>\n"
  },
  {
    "path": "r2/r2/templates/trafficpage.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%inherit file=\"reddit.html\"/>\n<%namespace file=\"reddittraffic.html\" import=\"load_timeseries_js\"/>\n\n<%def name=\"javascript()\">\n  ${parent.javascript()}\n  ${load_timeseries_js()}\n</%def>\n"
  },
  {
    "path": "r2/r2/templates/trendingsubredditsbar.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n    from r2.models import Link\n%>\n\n<div class=\"trending-subreddits\">\n  <div class=\"rank-spacer\"></div>\n  <div class=\"midcol-spacer\"></div>\n  <div class=\"trending-subreddits-content\">\n    <strong>${_('trending subreddits')}</strong>\n    <ul>\n    %for i, subreddit_name in enumerate(thing.subreddit_names):\n      <li><a href=\"${Link.tracking_link('/r/%s'%subreddit_name, context='trending_subreddits_bar', element_name='trending_sr_%s'%(i+1))}\" target=\"_blank\">/r/${subreddit_name}</a></li>\n    %endfor\n    </ul>\n    <a href=\"${thing.comment_url}\" class=\"${thing.comment_label_cls}\">${thing.comment_label}</a>\n  </div>\n</div>\n"
  },
  {
    "path": "r2/r2/templates/trophycase.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%namespace file=\"printablebuttons.html\" import=\"ynbutton\" />\n\n<%def name=\"trophy_info(trophy, center)\">\n  <td class=\"trophy-info\"\n      %if center:\n        colspan=\"2\"\n      %endif\n      >\n    <div>\n      <% trophy_url = trophy.trophy_url %>\n      %if trophy_url:\n       <a href=\"${trophy_url}\">\n      %endif\n       <img class=\"trophy-icon\" src=\"${trophy._thing2.imgurl % 40}\" />\n       <br/>\n       <span class=\"trophy-name\">${trophy._thing2.title}</span>\n       <br/>\n       %if hasattr(trophy, \"description\"):\n       <span class=\"trophy-description\">${trophy.description}</span>\n       <br/>\n       %endif\n      %if trophy_url:\n       </a>\n      %endif\n      %if c.user_is_admin:\n      ${ynbutton(_(\"remove\"), _(\"removed\"), \"removetrophy\", \"hide_thing\",\n      hidden_data=dict(trophy_fn=trophy._id36))}\n      %endif\n    </div>\n  </td>\n</%def>\n\n## for now\n%if not thing.trophies:\n  <div class=\"dust\">${_(\"dust\")}</div>\n%endif\n\n<%def name=\"trophy_table(trophies, header='')\">\n\n  ${unsafe(header)}\n\n  <table class=\"trophy-table\">\n    %for i, trophy in enumerate(trophies):\n      %if i % 2 == 0:\n       <tr>\n      %endif\n\n      ${trophy_info(trophy, i == len(trophies) - 1)}\n\n      %if i % 2 == 1:\n       </tr>\n      %endif\n    %endfor\n\n    %if len(trophies) % 2 == 1:\n      </tr>\n    %endif\n  </table>\n</%def>\n\n${trophy_table(thing.trophies)}\n\n%if c.user_is_admin and thing.invisible_trophies:\n  ${trophy_table(thing.invisible_trophies, \"<p>Invisibles:</p>\")}\n%endif\n"
  },
  {
    "path": "r2/r2/templates/trycompact.compact",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n   from r2.lib import tracking\n %>\n<%inherit file=\"reddit.compact\" />\n\n<%def name=\"bodyContent()\">\n  <%include file=\"redditheader.compact\"/>\n  <div class=\"tryme\">\n    <p>\n      You look to be using a modern mobile browser, and we've made a modern, mobile interface.\n    </p>\n    <p>\n      Here are your choices:\n    </p>\n    <div class=\"choices\">\n      <a class=\"button\" href=\"${thing.compact}\">\n        Try new mobile interface.\n      </a>\n      <a class=\"button\" href=\"${thing.like}\">\n        Always use new mobile interface.\n      </a>\n      <a class=\"button\" href=\"${thing.mobile}\">\n        Not a chance. Old one is just fine.\n      </a>\n    </div>\n  </div>\n\n  %if g.tracker_url and thing.site_tracking:\n    <img alt=\"\" src=\"${tracking.get_pageview_pixel_url()}\"/>\n  %endif\n</%def>\n"
  },
  {
    "path": "r2/r2/templates/unreadmessagessuggestions.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<span class=\"next-suggestions\">\n  ${_('or')}\n  <span class=\"mark-all-read-container\">\n    <a class=\"basic-button mark-all-read\" href=\"#\">${_('mark all as read')}</a>\n    <span class=\"throbber\">${_('(this may take a while)')}</span>\n  </span>\n</span>\n"
  },
  {
    "path": "r2/r2/templates/uploadedadsimage.html",
    "content": ""
  },
  {
    "path": "r2/r2/templates/uploadedimage.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%! \n   import simplejson\n %>\n<html>\n  <head>\n    <script type=\"text/javascript\">\n      <%\n         errors = simplejson.dumps(thing.errors)\n       %>\n      parent.completedUploadImage('${thing.status}','${thing.img_src or \"\"}',\n                                  '${thing.name or \"\"}', ${unsafe(errors)},'${thing.form_id}');    \n    </script>\n  </head>\n  <body>\n    you shouldn't be here\n  </body>\n</html>\n"
  },
  {
    "path": "r2/r2/templates/userawards.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%def name=\"other_awards(heading, awards)\">\n  <br class=\"clear\" />\n  <h1>${heading}</h1>\n  %for award in awards:\n    <div class=\"award-square mini\">\n      <a href=\"/wiki/awards/#${award.codename}\">\n        <img src=\"${award.imgurl % 70}\" />\n        <br/>\n        <span class=\"award-name mini\">${award.title}</span>\n      </a>\n    </div>\n  %endfor\n</%def>\n\n<div class=\"award-square-container\">\n  %if thing.manuals:\n    <h1>${_(\"Ongoing awards, and their most recent winners:\")}</h1>\n  %else:\n    <h1>&nbsp;</h1>\n  %endif\n\n  %for award, winner, trophy in thing.regular_winners:\n\n    <div class=\"award-square\">\n      <a href=\"/wiki/awards/#${award.codename}\">\n        <img src=\"${award.imgurl % 70}\" />\n        <span class=\"award-name\">${award.title}</span>\n      </a>\n\n      <div class=\"winner-info\">\n        %if hasattr(trophy, \"description\"):\n          ${_(\"won by\")}\n        %else:\n          <br/>\n          ${_(\"recently won by\")}\n        %endif\n        <span class=\"winner-name\">\n          &#32;\n          <a href=\"/user/${winner}\">${winner}</a>\n        </span>\n\n        <br/>\n\n        %if hasattr(trophy, \"description\") and not trophy.description.startswith(\"20\"):\n          (\n          %if hasattr(trophy, \"url\"):\n           <a href=\"${trophy.url}\">\n             ${trophy.description}\n           </a>\n          %else:\n             ${trophy.description}\n          %endif\n          )\n        %else:\n          %if hasattr(trophy, \"description\"):\n            on\n            ${trophy.description}\n          %endif\n\n          %if hasattr(trophy, \"url\"):\n           &#32;\n           <a href=\"${trophy.url}\">\n             ${_(\"for this\")}\n           </a>\n          %endif\n        %endif\n      </div>\n\n    </div>\n  %endfor\n\n  %if thing.manuals:\n    ${other_awards(_(\"Special awards:\"), thing.manuals)}\n  %endif\n\n  %if c.user_is_admin and thing.invisibles:\n    ${other_awards(\"Invisible awards:\", thing.invisibles)}\n  %endif\n\n</div>\n"
  },
  {
    "path": "r2/r2/templates/userblockedinterstitial.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n  from r2.lib.template_helpers import _wsf\n%>\n\n<%inherit file=\"bannedinterstitial.html\"/>\n\n<%def name=\"interstitial_title()\">\n  ${_wsf(\"You've blocked this user. (Out of sight, out of mind.)\")}\n</%def>\n\n<%def name=\"interstitial_message()\">\n  ## no message\n</%def>\n"
  },
  {
    "path": "r2/r2/templates/useriphistory.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%namespace file=\"prefapps.html\" import=\"authorized_app\"/>\n<%namespace file=\"utils.html\" import=\"error_field, timestamp\"/>\n<%namespace name=\"utils\" file=\"utils.html\"/>\n\n<%\n    from r2.lib.strings import strings\n    ip_format = {'address': request.ip}\n%>\n\n<div id=\"account-activity\" class=\"instructions\">\n<h1>${_(\"Recent activity on your account\")}</h1>\n\n<p>${strings.account_activity_blurb}</p>\n\n<p>${strings.your_current_ip_is % ip_format}</p>\n<table>\n    <thead>\n        <tr>\n            <th>${_(\"IP address\")}</th>\n            <th>${_(\"Location\")}</th>\n            <th>${_(\"Last Visit\")}</th>\n            <th>${_(\"Organization\")}</th>\n        </tr>\n    </thead>\n    <tbody>\n        % for ip_data in thing.ips[:10]:\n        <% \n            ip, last_visit, location, org = ip_data[:4]\n        %>\n        <tr>\n            <td>${ip}</td>\n            <td>${location.get('country_name', '')}</td>\n            <td>${timestamp(last_visit, live=True, include_tense=True)}</td>\n            <td>${org}</td>\n        </tr>\n        % endfor\n    </tbody>\n</table>\n</div>\n\n<hr/>\n\n<h1>${_(\"Log out of all other sessions\")}</h1>\n\n<form action=\"/post/clear_sessions\" method=\"post\"\n      onsubmit=\"return post_form(this, 'clear_sessions')\" id=\"clear_sessions\">\n\n<div class=\"spacer\">\n  <%utils:round_field title=\"${_('current password')}\" description=\"${_('(required)')}\">\n  <input type=\"password\" name=\"curpass\" />\n  ${error_field(\"WRONG_PASSWORD\", \"curpass\")}\n  </%utils:round_field>\n</div>\n<button type=\"submit\" class=\"btn\">${_('clear sessions')}</button>\n<span class=\"status error\"></span>\n\n</form>\n\n%if thing.my_apps:\n  <hr/>\n  <div id=\"account-activity-apps\" class=\"instructions\">\n    <h1>${_(\"Apps you have authorized\")}</h1>\n    <p>${strings.account_activity_apps_blurb}</p>\n    %for app_data in thing.my_apps.values():\n      ${authorized_app(app_data)}\n    %endfor\n  </div>\n%endif\n"
  },
  {
    "path": "r2/r2/templates/userlisting.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n   from r2.config import feature\n   from r2.lib.template_helpers import format_html\n %>\n<%namespace file=\"utils.html\" import=\"error_field, plain_link\" />\n\n<%def name=\"add_form(title, dest, add_type, container_name, verb=None, permissions_form=None)\">\n  <form action=\"/post/${dest}\"\n        method=\"post\" class=\"pretty-form medium-text friend-add\"\n        onsubmit=\"return post_form(this, '${dest}')\"\n        id=\"${add_type}\">\n    <h1>${title}</h1>\n\n    <input type=\"hidden\" name=\"action\" value=\"add\">\n    <input type=\"hidden\" name=\"container\" value=\"${container_name}\">\n    <input type=\"hidden\" name=\"type\" value=\"${add_type}\">\n    %if add_type in (\"banned\", \"wikibanned\"):\n        <label for=\"name\">${_('who to ban?')} &nbsp;</label>\n        <input type=\"text\" class=\"friend-name\" name=\"name\" id=\"name\">\n        <div>\n        %if feature.is_enabled(\"subreddit_rules\", subreddit=c.site.name):\n          <label for=\"ban_reason\">${_(\"reason\")}</label>\n          <select name=\"ban_reason\">\n            %if thing.rules:\n              <optgroup label=\"Subreddit Rules\">\n                %for rule in thing.rules:\n                  <option value=\"${rule['short_name']}\">${rule['short_name']}</option>\n                %endfor\n              </optgroup>\n            %endif\n            <optgroup label=\"Site Rules\">\n              %for rule in thing.system_rules:\n                <option value=\"${rule}\">${rule}</option>\n              %endfor\n            </optgroup>\n            <option value=\"other\">${_(\"Other\")}</option>\n          </select>\n        %endif\n          <div>\n            <label for=\"note\">${_(\"mod note\")}\n            <input type=\"text\" maxlength=\"300\" name=\"note\" id=\"note\">\n            <span>${_('(will not be visible to user)')}</span>\n          </div>\n        </div>\n        <div>\n            <label for=\"duration\">${_('how long?')}</label>\n            <input type=\"number\" min=\"1\" max=\"999\" name=\"duration\" id=\"duration\">\n            <span>${_('days (leave blank for permanent)')}</span>\n        </div>\n    %elif add_type == \"muted\":\n        <label for=\"name\">${_('who to mute?')} &nbsp;</label>\n        <input type=\"text\" class=\"friend-name\" name=\"name\" id=\"name\">\n        <div>\n            <label for=\"note\">${_('why the mute?')}</label>\n            <input type=\"text\" maxlength=\"300\" name=\"note\" id=\"note\">\n            <span>${_('(will not be visible to user)')}</span>\n        </div>\n    %else:\n        <input type=\"text\" name=\"name\" id=\"name\">\n    %endif\n    %if add_type == \"banned\":\n      <div>\n          <label for=\"note\">${_('note to include in ban PM')}</label>\n          <textarea name=\"ban_message\" id=\"ban_message\"></textarea>\n      </div>\n    %endif\n    %if permissions_form:\n      ${permissions_form}\n      &#32;\n      <span class=\"permissions-edit\">\n        (<a href=\"javascript:void(0)\">${_('change')}</a>)\n      </span>\n    %endif\n    <button class=\"btn\" type=\"submit\">${verb or _(\"add\")}</button>\n    <span class=\"status\"></span>\n    ${error_field(\"NO_USER\", \"name\")}\n    ${error_field(\"USER_DOESNT_EXIST\", \"name\")}\n    ${error_field(\"ALREADY_MODERATOR\", \"name\")}\n    ${error_field(\"CANT_RESTRICT_MODERATOR\", \"name\")}\n    ${error_field(\"BANNED_FROM_SUBREDDIT\", \"name\")}\n    ${error_field(\"MUTED_FROM_SUBREDDIT\", \"name\")}\n    %if caller:\n      ${caller.body()}\n    %endif\n  </form>\n</%def>\n\n<%def name=\"listing()\">\n  <div class=\"${thing.type}-table\"\n    style=\"${'display:none' if not thing.things and not thing.show_not_found else ''}\">\n    <h1>\n      ${thing.title}\n    </h1>\n\n    <table>\n      %if thing.headers:\n        <tr>\n        %for header in thing.headers:\n            <th>${header}</th>\n        %endfor\n        </tr>\n        %endif\n        %if thing.things:\n            %for item in thing.things:\n                ${item}\n            %endfor\n        %else:\n        <tr class=\"notfound\"><td>${_('No items found') if thing.show_not_found else ''}</td></tr>\n      %endif\n    </table>\n  </div>\n\n%if thing.nextprev and (thing.prev or thing.next):\n  <p class=\"nextprev\"> ${_(\"view more:\")}&#32;\n  %if thing.prev:\n    ${plain_link(_(\"first\"), thing.first, rel=\"nofollow first\")} \n    <span class=\"separator\"></span>\n    ${plain_link(format_html(\"&lsaquo; %s\", _(\"prev\")), thing.prev, rel=\"nofollow prev\")}\n  %endif\n  %if thing.prev and thing.next:\n    <span class=\"separator\"></span>\n  %endif\n  %if thing.next:\n  ${plain_link(format_html(\"%s &rsaquo;\", _(\"next\")), thing.next, rel=\"nofollow next\")}\n  %endif\n  </p>\n%endif\n\n</%def>\n\n<div class=\"${thing._class} usertable\">\n  %if thing.addable and thing.has_add_form:\n    ${add_form(thing.form_title, thing.destination, thing.type, thing.container_name, permissions_form=thing.permissions_form)}\n  %endif\n\n  %if thing.show_jump_to:\n    <h1>${_('jump to')}</h1>\n    <form class=\"pretty-form medium-text\">\n        <label for=\"user\">${_('username')}&nbsp;</label>\n        <input type=\"text\" id=\"user\" name=\"user\"\n        %if thing.jump_to_value:\n            value=\"${thing.jump_to_value}\"\n        %endif\n        >\n        <button type=\"submit\">${_('go')}</button>\n    </form>\n  %endif\n\n${listing()}\n\n%if thing.jump_to_value:\n    <p class=\"nextprev\">\n        ${plain_link(_(\"show all\"), request.path, rel=\"nofollow\")}\n    </p>\n%endif\n\n</div>\n"
  },
  {
    "path": "r2/r2/templates/usertableitem.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n  from r2.lib.template_helpers import display_link_karma\n%>\n<%namespace file=\"printablebuttons.html\" import=\"ynbutton\" />\n<%namespace file=\"utils.html\" import=\"plain_link, timestamp\"/>\n\n<tr class=\"${thing.author_cls}\">\n  %for cell_type in thing.cells:\n    <td>\n      ${usertablecell(cell_type)}\n    </td>\n  %endfor\n</tr>\n\n<%def name=\"usertablecell(cell_type)\">\n  %if cell_type == \"user\":\n      <span class=\"user\">\n         ${plain_link(thing.user.name, \"/user/%s/\" % thing.user.name,\n                      _sr_path=False)}\n         &nbsp;(<b>${display_link_karma(thing.user.link_karma)}</b>)\n      </span>\n      &nbsp;\n  %elif c.user_is_loggedin and cell_type == \"sendmessage\" and c.user != thing.user:\n\n      <a href=\"${'/message/compose/?to=%s' % thing.user.name}\" class=\"access-required\"\n         data-type=\"account\"\n         data-fullname=\"${thing.user._fullname}\"\n         data-event-action=\"compose\">\n         ${_(\"send message\")}\n      </a>\n      &nbsp;\n  %elif cell_type == \"remove\":\n    %if thing.editable:\n      <%\n        access_required = getattr(thing, 'remove_access_required', True)\n      %>\n      ${ynbutton(_(\"remove\"), \"removed\", thing.remove_action,\n                 callback=\"deleteRow\",\n                 hidden_data = dict(type = thing.type,\n                                    id = thing.user._fullname,\n                                    container = thing.container_name),\n                 access_required=access_required)}\n    %else:\n      %if c.user != thing.user:\n        <span class=\"gray\">${_(\"can't remove\")}</span>\n      %endif\n    %endif\n  %elif cell_type == \"note\":\n    <form action=\"/post/${thing.type}note\" id=\"${thing.type}note-${thing.rel._fullname}\"\n          method=\"post\" class=\"pretty-form medium-text rel-note ${thing.type}-note\"\n          onsubmit=\"return post_form(this, '${thing.type}note');\">\n      <input type=\"hidden\" name=\"name\" value=\"${thing.user.name}\" />\n      <input type=\"text\" maxlength=\"300\" name=\"note\"\n             onchange=\"$(this).parent().addClass('edited')\"\n             value=\"${getattr(thing.rel, 'note', '')}\" />\n      <button onclick=\"$(this).parent().removeClass('edited')\" type=\"submit\">\n        ${_(\"submit\")}\n      </button>\n    </form>\n  %elif cell_type == \"age\":\n      ${timestamp(thing.rel._date, include_tense=True)}\n  %elif cell_type == \"permissions\":\n      ${thing.permissions}\n  %elif cell_type == \"permissionsctl\":\n    %if thing.editable:\n      <span class=\"permissions-edit\">\n        (<a class=\"access-required\"\n            href=\"javascript:void(0)\"\n            data-type=\"account\"\n            data-fullname=\"${thing.user._fullname}\"\n            data-event-action=\"editsettings\"\n            data-event-detail=\"set_permissions\"\n            >${_('change')}</a>)\n      </span>\n    %endif\n  %elif cell_type == \"temp\" and hasattr(thing, \"tempban\"):\n    ${_('%(time)s days left') % dict(time=thing.tempban)}\n    ${ynbutton(_('make permanent'), _(\"made permanent\"), \"friend\",\n        hidden_data = dict(type=thing.type,\n                           name=thing.user.name,\n                           container=thing.container_name))}\n  %endif\n</%def>\n"
  },
  {
    "path": "r2/r2/templates/usertext.compact",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n   from r2.lib.filters import unsafe, safemarkdown, keep_space\n   from r2.lib.strings import strings\n   from r2.lib.utils import randstr\n%>\n\n<%namespace file=\"printablebuttons.html\" import=\"toggle_button\" />\n<%namespace file=\"utils.html\" import=\"error_field, md\"/>\n\n<%def name=\"action_button(name, btn_type, onclick, display)\">\n  <button type=\"${btn_type}\" onclick=\"${onclick}\" class=\"${name}\"\n          ${\"style='display:none'\" if not display else \"\"}>\n    ${name}\n  </button>\n</%def>\n\n%if thing.have_form:\n  <form action=\"#\" class=\"${thing.css_class}\"\n        onsubmit=\"return post_form(this, '${thing.post_form}')\"\n        ${\"style='display:none'\" if not thing.display else \"\"}\n        id=\"form-${thing.fullname + randstr(3)}\">\n%else:\n  <div class=\"${thing.css_class}\">\n%endif\n\n  ##this is set for both editting selftext and creating new comments\n  <input type=\"hidden\" name=\"thing_id\" value=\"${thing.fullname or ''}\"/>\n\n  %if not thing.creating:\n    % if not thing.expunged:\n    <div class=\"usertext-body\">\n      ${unsafe(safemarkdown(thing.text, nofollow = thing.nofollow,\n                                        target = thing.target))}\n    </div>\n    % else:\n    <em>${_(\"[removed]\")}</em>&#32;\n    % endif\n  %endif\n\n  %if thing.editable or thing.creating:\n    ##keep this on one line so we don't add extra spaces\n    <div class=\"usertext-edit\"\n         style=\"${\"\" if thing.creating else 'display: none'}\">\n      ##this div prevents IE7 from adding extra margin to the textarea.\n      <div>\n        <textarea rows=\"1\" cols=\"1\"\n                  name=\"${thing.name}\"\n                  >${keep_space(thing.text)}</textarea>\n      </div>\n\n      <div class=\"bottom-area\">\n        <div class=\"usertext-buttons\">\n          <a href=\"#\" class=\"help-toggle newbutton\">${_(\"help\")}</a>\n          <button type=\"submit\" class=\"save newbutton\">${_(\"Send\")}</button>\n          <button type=\"button\" class=\"cancel newbutton\" style=\"display: none;\"\n            onclick=\"return cancel_usertext(this)\">\n            ${_(\"Cancel\")}</button>\n          %if thing.have_form:\n            <span class=\"status\"></span>\n          %endif\n        <div style=\"clear: both\"></div>\n        </div>\n        ${error_field(\"TOO_LONG\", thing.name, \"span\")}\n        ${error_field(\"RATELIMIT\", \"ratelimit\", \"span\")}\n        ${error_field(\"NO_TEXT\", thing.name, \"span\")}\n        ${error_field(\"DELETED_COMMENT\", \"parent\", \"span\")}\n        ${error_field(\"USER_BLOCKED\", \"parent\", \"span\")}\n        ${error_field(\"USER_MUTED\", \"parent\", \"span\")}\n        ${error_field(\"MUTED_FROM_SUBREDDIT\", \"parent\", \"span\")}\n      </div>\n      <div class=\"markhelp-parent\">\n      <p>${md(strings.formatting_help_info)}</p>\n      <table class=\"markhelp\">\n        <tr style=\"background-color: #ffff99; text-align: center\">\n          <td><em>${_( \"you type:\")}</em></td>\n          <td><em>${_( \"you see:\")}</em></td>\n        </tr>\n        <tr>\n          <td>*${_( \"italics\")}*</td>\n          <td><em>${_( \"italics\")}</em></td>\n        </tr>\n        <tr>\n          <td>**${_( \"bold\")}**</td>\n          <td><b>${_( \"bold\")}</b></td>\n        </tr>\n        <tr>\n          <td>[reddit!](https://reddit.com)</td>\n          <td><a href=\"https://reddit.com\">reddit!</a></td>\n        </tr>\n        <tr>\n          <td>\n            * ${_( \"item\")} 1<br/>\n            * ${_( \"item\")} 2<br/>\n            * ${_( \"item\")} 3\n          </td>\n          <td>\n            <ul>\n              <li>${_( \"item\")} 1</li>\n              <li>${_( \"item\")} 2</li>\n              <li>${_( \"item\")} 3</li>\n            </ul>\n          </td>\n        </tr>\n        <tr>\n          <td>&gt; ${_( \"quoted text\")}</td>\n          <td><blockquote>${_( \"quoted text\" )}</blockquote></td>\n        </tr>\n        <tr>\n            <td>\n                Lines starting with four spaces <br/>\n                are treated like code:<br/><br/>\n                <span class=\"spaces\">\n                    &nbsp;&nbsp;&nbsp;&nbsp;\n                </span>\n                if 1 * 2 &lt; 3:<br/>\n                <span class=\"spaces\">\n                    &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;\n                </span>\n                print \"hello, world!\"<br/>\n            </td>\n            <td>Lines starting with four spaces <br/>\n                are treated like code:<br/>\n                <pre>if 1 * 2 &lt; 3:<br/>&nbsp;&nbsp;&nbsp;&nbsp;print \"hello,\n                world!\"</pre>\n            </td>\n        </tr>\n        <tr>\n            <td>~~strikethrough~~</td>\n            <td><strike>strikethrough</strike></td>\n        </tr>\n        <tr>\n            <td>super^script</td>\n            <td>super<sup>script</sup></td>\n        </tr>\n      </table>\n      </div>\n    </div>\n  %endif\n\n%if thing.have_form:\n  </form>\n%else:\n  </div>\n%endif\n"
  },
  {
    "path": "r2/r2/templates/usertext.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n   from r2.lib.filters import unsafe, safemarkdown, keep_space\n   from r2.lib.strings import strings\n   from r2.lib.utils import randstr\n%>\n\n<%namespace file=\"printablebuttons.html\" import=\"toggle_button\" />\n<%namespace file=\"utils.html\" import=\"data, error_field, md, _md\"/>\n\n<%def name=\"markhelp(show_embed_help=False)\">\n  <div class=\"markhelp\" style=\"display:none\">\n    <p>${md(strings.formatting_help_info)}</p>\n    <table class=\"md\">\n      <tr style=\"background-color: #ffff99; text-align: center\">\n        <td><em>${_( \"you type:\")}</em></td>\n        <td><em>${_( \"you see:\")}</em></td>\n      </tr>\n      <tr>\n        <td>*${_( \"italics\")}*</td>\n        <td><em>${_( \"italics\")}</em></td>\n      </tr>\n      <tr>\n        <td>**${_( \"bold\")}**</td>\n        <td><b>${_( \"bold\")}</b></td>\n      </tr>\n      <tr>\n        <td>[reddit!](https://reddit.com)</td>\n        <td><a href=\"https://reddit.com\">reddit!</a></td>\n      </tr>\n      <tr>\n        <td>\n          * ${_( \"item\")} 1<br/>\n          * ${_( \"item\")} 2<br/>\n          * ${_( \"item\")} 3\n        </td>\n        <td>\n          <ul>\n            <li>${_( \"item\")} 1</li>\n            <li>${_( \"item\")} 2</li>\n            <li>${_( \"item\")} 3</li>\n          </ul>\n        </td>\n      </tr>\n      <tr>\n        <td>&gt; ${_( \"quoted text\")}</td>\n        <td><blockquote>${_( \"quoted text\" )}</blockquote></td>\n      </tr>\n      <tr>\n          <td>\n              Lines starting with four spaces <br/>\n              are treated like code:<br/><br/>\n              <span class=\"spaces\">\n                  &nbsp;&nbsp;&nbsp;&nbsp;\n              </span>\n              if 1 * 2 &lt; 3:<br/>\n              <span class=\"spaces\">\n                  &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;\n              </span>\n              print \"hello, world!\"<br/>\n          </td>\n          <td>Lines starting with four spaces <br/>\n              are treated like code:<br/>\n              <pre>if 1 * 2 &lt; 3:<br/>&nbsp;&nbsp;&nbsp;&nbsp;print \"hello,\n              world!\"</pre>\n          </td>\n      </tr>\n      <tr>\n          <td>~~strikethrough~~</td>\n          <td><strike>strikethrough</strike></td>\n      </tr>\n      <tr>\n          <td>super^script</td>\n          <td>super<sup>script</sup></td>\n      </tr>\n      % if show_embed_help:\n      <tr>\n          <td>${_md('Links on their own line will be embedded:\\n\\nhttps://example.com')}</td>\n          <td>${_('an embedded version of that link')}</td>\n      </tr>\n      % endif\n    </table>\n  </div>\n</%def>\n\n<%def name=\"action_button(name, btn_type, onclick, display)\">\n  <button type=\"${btn_type}\" onclick=\"${onclick}\" class=\"${name}\"\n          ${\"style='display:none'\" if not display else \"\"}>\n    ${name}\n  </button>\n</%def>\n\n%if thing.have_form:\n  <form action=\"#\" class=\"${thing.css_class} warn-on-unload\"\n      %if thing.post_form:\n        onsubmit=\"return post_form(this, '${thing.post_form}')\"\n      %endif\n        ${\"style='display:none'\" if not thing.display else \"\"}\n        id=\"form-${thing.fullname + randstr(3)}\">\n%else:\n  <div class=\"${thing.css_class}\">\n%endif\n\n  ##this is set for both editting selftext and creating new comments\n  <input type=\"hidden\" name=\"thing_id\" value=\"${thing.fullname}\"/>\n\n  %if thing.source:\n    <input type=\"hidden\" name=\"source\" value=\"${thing.source}\">\n  %endif\n\n  %if not thing.creating:\n    <div class=\"usertext-body may-blank-within md-container ${'admin_takedown' if thing.admin_takedown else ''}\">\n      % if not thing.expunged:\n      ${unsafe(safemarkdown(thing.text, nofollow = thing.nofollow,\n                                        target = thing.target))}\n      % else:\n      <em>${_(\"[removed]\")}</em>&#32;\n      % endif\n    </div>\n  %endif\n\n  %if thing.editable or thing.creating:\n    ##keep this on one line so we don't add extra spaces\n    <div class=\"usertext-edit md-container\"\n         style=\"${\"\" if thing.creating else 'display: none'}\">\n      <div class=\"md\">\n        <textarea rows=\"1\" cols=\"1\"\n                  name=\"${thing.name}\"\n                  class=\"${thing.textarea_class}\"\n                  ${data(**thing.data_attrs)}\n                  >${keep_space(thing.text)}</textarea>\n      </div>\n\n      <div class=\"bottom-area\">\n        ${toggle_button(\"help-toggle\", _(\"formatting help\"), _(\"hide help\"),\n                        \"helpon\", \"helpoff\",\n                         style = \"\" if thing.creating else \"display: none\")}\n\n        <a href=\"/help/contentpolicy\" class=\"reddiquette\" target=\"_blank\" tabindex=\"100\">${_('content policy')}</a>\n\n        %if thing.include_errors:\n          ${error_field(\"TOO_LONG\", thing.name, \"span\")}\n          ${error_field(\"RATELIMIT\", \"ratelimit\", \"span\")}\n          ${error_field(\"NO_TEXT\", thing.name, \"span\")}\n          ${error_field(\"TOO_OLD\", \"parent\", \"span\")}\n          ${error_field(\"THREAD_LOCKED\", \"parent\", \"span\")}\n          ${error_field(\"DELETED_COMMENT\", \"parent\", \"span\")}\n          ${error_field(\"USER_BLOCKED\", \"parent\", \"span\")}\n          ${error_field(\"USER_MUTED\", \"parent\", \"span\")}\n          ${error_field(\"MUTED_FROM_SUBREDDIT\", \"parent\", \"span\")}\n        %endif\n        <div class=\"usertext-buttons\">\n          ${action_button(\"save\", \"submit\", \"\",\n                          thing.creating and thing.have_form)}\n          ${action_button(\"cancel\", \"button\", \"return cancel_usertext(this);\", False)}\n          %if thing.have_form:\n            <span class=\"status\"></span>\n          %endif\n        </div>\n      </div>\n\n      ${markhelp(show_embed_help=thing.show_embed_help)}\n    </div>\n  %endif\n\n%if thing.have_form:\n  </form>\n%else:\n  </div>\n%endif\n"
  },
  {
    "path": "r2/r2/templates/usertext.mobile",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n   from r2.lib.filters import safemarkdown\n %>\n\n%if thing.text:\n%if not thing.expunged:\n<div class=\"usertext-body\">\n    ${unsafe(safemarkdown(thing.text, nofollow = thing.nofollow,\n                          target = thing.target))}\n</div>\n%else:\n<em>${_(\"[removed]\")}</em>&#32;\n%endif\n%endif\n"
  },
  {
    "path": "r2/r2/templates/utils/gold.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\nfrom r2.lib.strings import Score\n%>\n\n<%def name=\"gold_dropdown(what, chosen_months, somethings=None, append_or_somethings=None)\">\n  <%\n    if not somethings:\n      somethings = what\n  %>\n  <%!\n    month_options = (1, 3)\n    year_options = (1, 2, 3)\n  %>\n  <select id=${what} name=${what} class=\"gold-dropdown\">\n    %for i in month_options:\n       <option value=\"${i}\" ${\"selected\" if chosen_months == i else \"\"}>\n         ${Score.somethings(i, somethings)}: ${g.gold_month_price * i}\n         ${\" or %s\" % Score.somethings(i, append_or_somethings) if append_or_somethings else \"\"}\n       </option>\n    %endfor\n    %for i in year_options:\n       <option value=\"${i * 12}\" ${\"selected\" if (chosen_months == (i * 12) or (not chosen_months and i == 1)) else \"\"}>\n         ${Score.somethings(i * 12, somethings)} &#32; (special price!): ${g.gold_year_price * i}\n         ${\" or %s\" % Score.somethings(i * 12, append_or_somethings) if append_or_somethings else \"\"}\n       </option>\n    %endfor\n  </select>\n</%def>\n"
  },
  {
    "path": "r2/r2/templates/utils.compact",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n\timport string\n%>\n\n<%def name=\"icon_button(text, css_class, href='javascript:void(0)', outer_class='', **kw)\">\n    <a href=\"${href}\"\n        %if outer_class :\n            class=\"${outer_class}\"\n        %endif\n        %for k, v in kw.iteritems():\n            ${k}=\"${v}\"\n        %endfor\n    >\n    <div class=\"${css_class}\"></div>${text}\n    </a>\n</%def>\n<%def name=\"toggle_button(togglename, toggled=False)\">\n    <%\n    if toggled:\n        togglestyle = {\"style\": \"display: none;\"}\n        untogglestyle = {}\n    else:\n        togglestyle = {}\n        untogglestyle = {\"style\": \"display: none;\"}\n    endif\n    \n    if togglename == \"hide\":\n        untext = \"\"\n    else:\n        untext = \"un\"\n    endif\n    %>\n    ##Hide/Save\n    ${icon_button( string.capitalize(togglename), togglename + \"-icon\", onclick=\"change_state(this,'\" + togglename + \"', \" + togglename + \"_thing)\", outer_class=togglename + \"-button\", **togglestyle)}\n    ##Unhide/Unsave\n    ${icon_button( \"Un\" + togglename, \"un\" + togglename + \"-icon\", onclick=\"change_state(this,'un\" + togglename + \"', \" + untext + togglename + \"_thing)\", outer_class= \"un\" + togglename + \"-button\", **untogglestyle)}\n</%def>\n"
  },
  {
    "path": "r2/r2/templates/utils.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n    import json\n    from datetime import datetime\n\n    from r2.lib import tracking\n    from r2.lib.filters import spaceCompress, unsafe, safemarkdown, jssafe, scriptsafe_dumps\n    from r2.lib.template_helpers import (\n        add_sr,\n        html_datetime,\n        js_config,\n        make_url_protocol_relative,\n        simplified_timesince,\n        static,\n    )\n    from r2.lib.utils import long_datetime\n%>\n<%def name=\"tags(**kw)\">\n%for k, v in kw.iteritems():\n  %if v:\n    %if k == \"data\":\n      ${data(**v)}\n    %else:\n      ${k.strip('_')}=\"${v}\" \\\n    %endif\n  %endif\n%endfor\n</%def>\n\n## override the link _class so that we can globally update\n## the way links are handled (if need be)\n<%def name=\"_a(**kw)\">\n<a ${tags(**kw)}>${caller.body()}</a>\n</%def>\n\n<%def name=\"_a_buffered(body, **kw)\" buffered=\"True\">\n<a ${tags(**kw)}>${body}</a>\n</%def>\n\n\n## thing should be global\n<%def name=\"_id(arg)\">\nid=\"${arg}_${thing and thing._fullname or ''}\"\n</%def>\n\n<%def name=\"classes(*class_names)\">\nclass=\"${\" \".join(filter(None, class_names))}\"\n</%def>\n\n<%def name=\"data(**data_attrs)\">\n%for name, value in data_attrs.iteritems():\ndata-${name}=\"${value}\"\n%endfor\n</%def>\n\n<%def name=\"submit_form(onsubmit='', action='', _class='', method='post', _id='', **params)\">\n<form class=\"${_class or ''}\" onsubmit=\"${onsubmit or ''}\" \n      action=\"${action or ''}\" ${_id and \"id='\" + _id + \"'\" or \"\"} method=\"${method}\"\n      >\n  %if c.user_is_loggedin:\n     <input type=\"hidden\" name=\"uh\" value=\"${c.modhash}\">\n  %endif\n  %for key, value in params.iteritems():\n  <input type=\"hidden\" name=\"${key}\" value=\"${value}\" />\n  %endfor\n  ${caller.body()}\n</form>\n</%def>\n\n<%def name=\"first_defined(*kw)\">\n%if not kw or kw[0] == UNDEFINED or not kw[0]:\n${first_defined(kw[1:])}\n%endif\n</%def>\n\n<%def name=\"form_group(field_name, *args, **kwargs)\">\n  <% error = c.errors.get_first(field_name, *args) %>\n  <div class=\"c-form-group ${'c-has-feedback c-has-error' if (error and kwargs['show_errors']) else ''}\">\n    ${caller.body()}\n    %if kwargs['show_errors']:\n      <div class=\"c-form-control-feedback-wrapper ${'inside-input' if kwargs.get('feedback_inside_input') else ''}\">\n        <span class=\"c-form-control-feedback c-form-control-feedback-throbber\"></span>\n        <span class=\"c-form-control-feedback c-form-control-feedback-error\" title=\"${_(error.message) if error else ''}\"></span>\n        <span class=\"c-form-control-feedback c-form-control-feedback-success\"></span>\n      </div>\n    %endif\n  </div>\n</%def>\n\n<%def name=\"error_field(error_name, field_name, kind='span')\">\n  <% error_key = (error_name, field_name) %>\n  <${kind} class=\"error ${error_name} field-${field_name}\" \n  style=\"${'' if error_key in c.errors else 'display:none'}\">\n   %if error_key in c.errors:\n     ${c.errors[error_key].message}\n   %endif\n  </${kind}>\n</%def>\n\n<%def name=\"img_link(link_text, img, path, _id='', target='', img_id=None, size=None, **kw)\">\n  <% \n     if target:\n         kw['target'] = target\n\n     path = add_sr(path, sr_path = False)\n     kw['target'] = target\n     \n     if size is None:\n         size_str = \"\"\n     else:\n         size_str = \"width='%d' height='%d'\" % (size[0], size[1])\n  %>\n  <%call expr=\"_a(href=path, _id=_id, **kw)\">\n    <img ${(\"id='%s'\" % img_id) if img_id else ''} src=\"${img}\" ${size_str} alt=\"${link_text}\"/>\n  </%call>\n</%def>\n\n<%def name=\"plain_link(link_text, path, _sr_path = True, nocname=False, fmt='', target='', **kw)\">\n  ## caching comment: \n  ##  in addition to the args, this function also makes use of\n  ##  both c.site.name and c.render_style via add_sr.\n  ##\n  ##  This function is called by (among other places) NavMenu as the\n  ##  primary rendering view.  Any changes to the c-usage of this function \n  ##  will have to be propagated up. \n  <% \n     if target:\n         kw['target'] = target\n\n     link = _a_buffered(link_text, \n                        href=path and add_sr(path, sr_path=_sr_path),\n                        **kw) \n  %>\n\n  ${unsafe((fmt % link) if fmt else link)}\n</%def>\n\n<%def name=\"post_link(link_text, post_path, redir_path, params, _sr_path=True,\n                      nocname=False, fmt='', target='', **kw)\">\n  <%\n    action = add_sr(post_path, sr_path=_sr_path)\n    href = add_sr(redir_path, sr_path=_sr_path)\n    if target:\n        kw['target'] = target\n    onclick = \"$(this).parent().submit(); return false;\"\n    link = _a_buffered(link_text, href=href, onclick=onclick, **kw)\n  %>\n  <form method=\"POST\" action=\"${action}\">\n    %for k, v in params.iteritems():\n      <input type=\"hidden\" name=\"${k}\" value=\"${v}\">\n    %endfor\n    ${unsafe((fmt % link) if fmt else link)}\n  </form>\n</%def>\n\n\n<%def name=\"text_with_links(txt, _sr_path = False, nocname=False, **kw)\">\n<%\n   from r2.lib.filters import conditional_websafe\n   for key, link_args in kw.iteritems():\n      link_args.setdefault(\"_sr_path\", _sr_path)\n      kw[key]=spaceCompress(capture(plain_link, **link_args))\n   txt = conditional_websafe(txt) % kw\n   txt = txt.replace(\" <\", \"&#32;<\").replace(\"> \", \">&#32;\")\n\n%>\n${unsafe(txt)}\n</%def>\n\n<%def name=\"language_tool(name='lang', allow_blank = False, \n                          default_lang = g.lang,\n                          show_regions = False, \n                          all_langs = False)\">\n<% \n   langs = g.all_languages if all_langs else g.languages \n   if not show_regions:\n       langs = [x for x in langs if len(x) == 2]\n%>\n%if langs:\n<select id=\"${name}\" name=\"${name}\">\n  %if allow_blank:\n  <option ${(not default_lang) and \"selected='selected'\" or \"\"}>\n  </option>\n  %endif\n  %for x in langs:\n  <option ${x == default_lang  and \"selected='selected'\" or \"\"} value=\"${x}\">\n    ${g.lang_name[x][0]} [${x}] ${g.lang_name[x][1]}\n  </option>\n  %endfor\n</select>\n%endif\n</%def>\n\n<%def name=\"separator(separator_char)\">\n  <span class=\"separator\">${separator_char}</span>\n</%def>\n\n<%def name=\"optionalstyle(style)\">\n  %if request.GET.get('style') != \"off\":\n    style=\"${style}\"\n  %endif\n</%def>\n\n<%def name=\"checkbox(name, text, val)\">\n  <input type=\"checkbox\" ${'checked=\"checked\"' if val else ''}\n    name=\"${name}\">\n    ${text}\n  </input>\n</%def>\n\n<%def name=\"ajax_upload(target, form_id)\">\n  <form method=\"post\" enctype=\"multipart/form-data\" target=\"${form_id}-iframe\"\n        id=\"${form_id}\" class=\"ajax-upload-form pretty-form\" action=\"${target}\"\n        onsubmit=\"return post_multipart_form(this, '${target}')\">\n    <input type=\"hidden\" name=\"id\" value=\"#${form_id}\" />\n    <input type=\"hidden\" name=\"uh\" value=\"${c.modhash}\" />\n    <input type=\"file\" name=\"file\" />\n    <button type=\"submit\">\n      ${_('upload')}\n    </button>\n    ${error_field('IMAGE_ERROR', '')}\n    %if caller:\n      ${caller.body()}\n    %endif\n    <span class=\"status\"></span>\n    <script type=\"text/javascript\">\n      function completedUploadImage (status, img_src, name, errors, form_id) {\n        /* should only be called when the uploaded file is too large */\n        form_id = $.with_default(form_id, \"${form_id}\");\n        if (status == \"failed\") {\n          $(\"#${form_id}\").find(\".status\").html(\"\");\n          for (var i in errors) {\n            $(\"#${form_id}\").find(\".error.\" + errors[i][0])\n                .show().text(errors[i][1]);\n          }\n        }\n      }\n    </script>\n    <iframe src=\"about:blank\" name=\"${form_id}-iframe\" id=\"${form_id}-iframe\">\n    </iframe>\n  </form>\n</%def>\n\n\n<%def name=\"image_upload_inline(form_id='file', field_id='inline-file')\">\n  <div id=\"${field_id}\">\n    <input type=\"file\" name=\"${form_id}\" id=\"${form_id}\">\n    ${error_field(\"IMAGE_ERROR\", \"span\")}\n    <script>\n      function completedUploadImage (status, img_src, name, errors, form_id) {\n        if (status == 'failed') {\n          var $form = $('#${field_id}');\n          $form.find('.status').html('');\n          for (var i in errors) {\n            $form.find('.error.' + errors[i][0])\n                .show().text(errors[i][1]);\n          }\n          var top = $form.position().top;\n          $(window).scrollTop(top);\n        }\n      }\n    </script>\n  </div>\n</%def>\n\n<%def name=\"s3_image_upload(id, width, height, src=None, data=None)\">\n  <form\n      id=\"${id}\"\n      class=\"c-image-upload\"\n      method=\"POST\"\n      enctype=\"multipart/form-data\"\n      target=\"${id}-frame\"\n      ${tags(**dict(data=data))}\n    >\n    <div class=\"c-image-upload-preview-container\">\n      <img\n        alt=\"${id} preview\"\n        class=\"c-image-upload-preview\"\n        style=\"width: ${width}px; max-height: ${height}px\"\n        %if src:\n          src=\"${make_url_protocol_relative(src)}\"\n        %else:\n          src=\"${static(width + 'x' + height + '-placeholder.png')}\"\n        %endif\n      >\n      <input type=\"file\" name=\"file\" id=\"${id}-input\" class=\"c-image-upload-input\" title=\"${_(\"click to upload\")}\">\n      <div class=\"c-progress\">\n        <div class=\"c-progress-bar\"></div>\n      </div>\n    </div>\n    <button class=\"c-image-upload-btn\" type=\"button\">${_(\"select\")}</button>\n  </form>\n  <iframe id=\"${id}-frame\" name=\"${id}-frame\" src=\"about:blank\" style=\"display:none;\"></iframe>\n  %if caller:\n    ${caller.body()}\n  %endif\n</%def>\n\n<%def name=\"image_upload(post_target, current_image = None, onsubmit = '', \n                         onchange = '', label = '', form_id = 'image-upload',\n                         ask_type = False, hidden_data=None)\">\n  <form id=\"${form_id}\" enctype=\"multipart/form-data\"\n        class=\"image-upload\"\n        target=\"upload-iframe\"\n        %if onsubmit:\n           onsubmit=\"${onsubmit}\"\n        %endif\n        action=\"${post_target}\" method=\"post\">\n      %if label:\n         <label for=\"file\">${label}</label>\n         <br>\n      %endif\n          <input type=\"hidden\" name=\"uh\" value=\"${c.modhash}\" />\n          %if not c.default_sr:\n            <input type=\"hidden\" name=\"r\"  value=\"${c.site.name}\" />\n          %endif\n          <input type=\"hidden\" name=\"formid\" value=\"${form_id}\" />\n          %if hidden_data:\n            %for name, value in hidden_data.iteritems():\n              <input type=\"hidden\" name=\"${name}\" value=\"${value}\" />\n            %endfor\n          %endif\n          %if ask_type:\n            <label for=\"img_type\">${_(\"Type: \")}</label>\n            <label><input type=\"radio\" name=\"img_type\" value=\"jpg\" />JPEG</label>\n            &nbsp;&nbsp;\n            <label><input type=\"radio\" name=\"img_type\" checked value=\"png\" />PNG</label>\n            <br/>\n          %endif\n          <input type=\"file\" name=\"file\" id=\"file\" \n                 onchange=\"$(this).next().prop('disabled', false); ${onchange}\"/>\n          <button id=\"submit-img\" class=\"submit-img primary-button\"\n                  type=\"submit\" name=\"upload\" \n                  onclick=\"$(this).parent().addClass('uploading').find('.img-status').show().text('${jssafe(_('uploading'))}'); return true;\"\n                  disabled>\n            ${_('upload')}\n          </button>\n\n          <span style=\"display: none;\" class=\"error img-status\"></span>\n          ${error_field(\"IMAGE_ERROR\", \"span\")}\n          <script type=\"text/javascript\">\n       function on_image_success(img) {}\n       function create_new_image(name) {}\n\n       function completedUploadImage(status, img_src, name, errors, form_id) {\n           var form = form_id ? $(\"#\" + form_id) : $('form.image-upload.uploading');\n           form.removeClass('uploading');\n\n           if (status) \n               form.find(\".img-status\").show().html(status);\n           else\n               form.find(\".img-status\").hide().html(\"\");\n           $.map(errors, function(e) {\n                   if(e[1]) \n                       form.find(\".\" + e[0]).html(e[1]).show();\n                   else\n                       form.find(\".\" + e[0]).html('').hide();\n               });\n           if(img_src) {\n              form.get(0).reset();\n              var img = (name) ? $(\"#img-preview-\" + name) :\n                  form.find(\"img.img-preview:first\");\n              if(!$.defined(img) || img.length == 0) \n                  img = create_new_image(name);\n              if(img)\n                  img.attr(\"src\", \"\").attr(\"src\", img_src);\n              img.show().parent().show();\n              form.find(\".delete-img\").show();\n              on_image_success(img);\n          }\n       }\n          </script>\n          \n          <iframe src=\"about:blank\" width=\"600\" height=\"200\" \n                  style=\"display: none;\"\n                  name=\"upload-iframe\" id=\"upload-iframe\"></iframe>\n\n          <div id=\"img-preview-container\" class=\"img-preview-container\"\n               style=\"${'' if current_image else 'display:none;'}\">\n            <img id=\"img-preview-upload\" alt=\"header preview\" \n                 class=\"img-preview\"\n                 %if current_image:\n                   src=\"${make_url_protocol_relative(current_image)}\"\n                 %else:\n                   src=\"${static('kill.png')}\"\n                 %endif\n                 /><br />\n          </div>\n     %if caller:\n       ${caller.body()}\n     %endif\n  </form>\n  <script type=\"text/javascript\">\n    $(function() {\n      var max_width = 0;\n      $(\".preftable th *\").each(function() {\n        max_width = Math.max(max_width, $(this).width());\n      }).each(function() {\n        $(this).width(max_width);\n      });\n    });\n       \n  </script>\n</%def>\n\n\n<%def name=\"js_setup(extra_config=None)\">\n  <script type=\"text/javascript\" id=\"config\">\n    r.setup(${scriptsafe_dumps(js_config(extra_config))})\n  </script>\n</%def>\n\n<%def name=\"googletagmanager()\">\n  %if g.googletagmanager and thing.site_tracking and thing.dnt_enabled:\n    <script>\n      if (!window.DO_NOT_TRACK) {\n        var frame = document.createElement('iframe');\n\n        frame.style.display = 'none';\n        frame.referrer = 'no-referrer';\n        frame.id = 'gtm-jail';\n        frame.name = JSON.stringify({ subreddit: r.config.post_site });\n        frame.src = '//' + ${scriptsafe_dumps(g.media_domain)} + '/gtm/jail';\n\n        document.body.appendChild(frame);\n      }\n    </script>\n  %endif\n</%def>\n\n<%def name=\"googleanalytics(uitype, is_gold_page=False)\">\n  %if (g.googleanalytics or g.googleanalytics_gold) and thing.site_tracking:\n    <script type=\"text/javascript\">\n      %if thing.dnt_enabled:\n      if (!window.DO_NOT_TRACK) {\n      %endif\n        window.user_type = '${\"guest\" if not c.user_is_loggedin else \"goldloggedin\" if c.user.gold else \"loggedin\"}';\n        window.is_gold_page = '${is_gold_page}'.toLowerCase() === 'true';\n      %if thing.dnt_enabled:\n      }\n      %endif\n    </script>\n  %endif\n\n  %if g.googleanalytics and thing.site_tracking:\n  ## it uses old ga.js\n  <script type=\"text/javascript\">\n    %if thing.dnt_enabled:\n    if (!window.DO_NOT_TRACK) {\n    %endif\n      var _gaq = _gaq || [];\n      _gaq.push(\n          ['_require', 'inpage_linkid', '//www.google-analytics.com/plugins/ga/inpage_linkid.js'],\n          ['_setAccount', '${g.googleanalytics}'],\n          ['_setDomainName', '${g.domain}'],\n          ['_setCustomVar', 1, 'site', '${tracking.get_site()}', 3],\n          ['_setCustomVar', 2, 'srpath', '${tracking.get_srpath()}', 3],\n          ['_setCustomVar', 3, 'usertype', user_type, 2],\n          ['_setCustomVar', 4, 'uitype', '${uitype}', 3],\n          ['_setCustomVar', 5, 'style_override', '${jssafe(c.user.pref_default_theme_sr)}', 2],\n          %if g.googleanalytics_sample_rate:\n          ['_setSampleRate', '${g.googleanalytics_sample_rate}'],\n          %endif\n          ['_trackPageview']\n      );\n\n      (function() {\n        var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;\n        ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';\n        var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);\n      })();\n    %if thing.dnt_enabled:\n    }\n    %endif\n  </script>\n  %endif\n\n  %if g.googleanalytics_gold and thing.site_tracking:\n  ## it uses new analytics.js\n  <script type=\"text/javascript\">\n    %if thing.dnt_enabled:\n    if (!window.DO_NOT_TRACK) {\n    %endif\n      (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){\n      (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),\n      m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)\n      })(window,document,'script','//www.google-analytics.com/analytics.js','_ga');\n\n      window._ga('create', '${g.googleanalytics_gold}', {\n        'name': 'goldTracker',\n        'cookieDomain': '${g.domain}',\n        '1': '${tracking.get_site()}',\n        '2': '${tracking.get_srpath()}',\n        '3': window.user_type,\n        '4': '${uitype}',\n        'sampleRate': ${g.googleanalytics_sample_rate_gold}\n      });\n\n      if (window.is_gold_page) {\n        window._ga('goldTracker.send', 'pageview');\n      }\n    %if thing.dnt_enabled:\n    }\n    %endif\n  </script>\n  %endif\n</%def>\n\n<%def name=\"logout(top=False,dest=None,a_class='')\">\n  <form method=\"post\" action=\"${add_sr('/logout', sr_path=False)}\" class=\"logout hover\"\n    %if top:\n      target=\"_top\"\n    %endif\n    >\n    <input type=\"hidden\" name=\"uh\" value=\"${c.modhash}\"/>\n    <input type=\"hidden\" name=\"top\" value=\"${'on' if top else 'off'}\"/>\n    %if dest:\n      <input type=\"hidden\" name=\"dest\" value=\"${dest}\"/>\n    %endif\n    \n    <a href=\"javascript:void(0)\" onclick=\"$(this).parent().submit()\"\n       %if a_class:\n         class=\"${a_class}\" \n       %endif\n    >\n      ${_(\"logout\")}\n    </a>\n  </form>\n</%def>\n\n<%def name=\"block_field(kind, title, description = '', css_class= '', **kw)\">\n  <div class=\"${kind} ${css_class}\"\n       %for k, v in kw.iteritems():\n         ${k}=\"${v}\"\n       %endfor\n       >\n    <span class=\"title\">${title}</span>\n    &#32;\n    %if description:\n      <span class=\"little gray ${kind}-description\">${description}</span>\n    %endif\n    <div class=\"${kind}-content\">\n      ${caller.body()}\n    </div>\n  </div>\n</%def>\n\n<%def name=\"round_field(title, description = '', css_class= '', **kw)\">\n  <%call expr=\"block_field('roundfield', title, description = description, css_class= css_class, **kw)\">\n     ${caller.body()}\n  </%call>\n</%def>\n\n<%def name=\"line_field(title, description = '', css_class= '', **kw)\">\n  <%call expr=\"block_field('linefield', title, description = description, css_class= css_class, **kw)\">\n     ${caller.body()}\n  </%call>\n</%def>\n\n<%def name=\"radio_type(field_name, val_name, title, text=None, checked=False, disabled=False, hover_title='[?]', hover_text=None)\">\n  <% full_name = field_name + \"_\" + val_name %>\n  <tr>\n    <td class=\"nowrap nopadding\">\n      <input name=\"${field_name}\" type=\"radio\" id=\"${full_name}\"\n             value=\"${val_name}\" class=\"nomargin\"\n             %if checked:\n               checked=\"checked\"\n             %endif\n             %if disabled:\n               disabled=\"disabled\"\n             %endif\n             />\n      <label for=\"${full_name}\">${title}</label>\n    %if hover_text:\n      <span class=\"help help-hoverable\">\n        <sup>${hover_title}</sup>\n        <div class=\"hover-bubble help-bubble anchor-top-left\">\n          <p>${hover_text}</p>\n        </div>\n      </span>\n    %endif\n    </td>\n    %if text:\n      <td class=\"leftpad\"><span class=\"gray\">${text}</span></td>\n    %endif\n  </tr>\n</%def>\n\n<%def name=\"inline_radio_type(field_name, val_name, text=None, checked=False)\">\n\t<% full_name = field_name + \"_\" + val_name %>\n\t<label>\n\t<input class=\"nomargin\" type=\"radio\" name=\"${field_name}\"\n\t\tid=\"${full_name}\" value=\"${val_name}\"\n\t\t%if checked:\n\t\t\tchecked=\"checked\"\n\t\t%endif\n\t>\n\t%if text:\n\t\t${text}\n\t%endif\n\t</label>\n</%def>\n\n<%def name=\"timestamp(date, since=None, live=False, include_tense=False)\">\n  ## todo: use pubdate attribute once things are <article> tags.\n  ## note: comment and link templates will pass a CachedVariable stub as since.\n  <%\n    timestamp_class = unsafe(' class=\"live-timestamp\"') if live else ''\n  %>\n  <time title=\"${long_datetime(date)}\" datetime=\"${html_datetime(date)}\"${timestamp_class}>\n    ${(since or simplified_timesince(date, include_tense))}\n  </time>\n</%def>\n\n<%def name=\"buffered_timestamp(date, since=None, live=False, include_tense=False)\" buffered=\"True\">\n  ${timestamp(date, since, live, include_tense)}\n</%def>\n\n<%def name=\"thing_timestamp(thing, since=None, live=False, include_tense=False)\">\n  ## todo: use pubdate attribute once things are <article> tags.\n  ## note: comment and link templates will pass a CachedVariable stub as since.\n  ${timestamp(thing._date, since=since, live=live, include_tense=include_tense)}\n</%def>\n\n<%def name=\"percentage(slice, total)\">\n  %if total is None or total == \"\" or total == 0 or slice is None or slice == \"\":\n    --\n  %else:\n    ${int(100 * slice / total)}%\n  %endif\n</%def>\n\n<%def name=\"pretty_button(label, func=None, func_vars='', extra_class='', event_action=None)\">\n  <a class=\"pretty-button access-required ${extra_class}\" href=\"#\"\n     %if func is None:\n       onclick=\"alert('please don\\'t do that again');return false;\"\n     %elif func_vars:\n       onclick=\"return ${func}($(this), ${func_vars})\"\n     %else:\n       onclick=\"return ${func}($(this))\"\n     %endif\n     %if event_action:\n       data-event-action=\"${event_action}\"\n     %endif\n     >\n       ${label}\n  </a>\n</%def>\n\n<%def name=\"edited(thing, lastedited=None)\">\n  %if isinstance(thing.editted, datetime):\n    <time class=\"edited-timestamp\" title=\"${_('last edited')} ${unsafe(lastedited or simplified_timesince(thing.editted))}\" datetime=\"${html_datetime(thing.editted)}\">*</time>\n  %elif thing.editted:\n    <em>*</em>\n  %endif\n</%def>\n\n<%def name=\"md(text, wrap=False, **kwargs)\">\n  ${unsafe(safemarkdown(text, wrap=wrap, **kwargs))}\n</%def>\n\n<%def name=\"_md(text, wrap=False)\">\n  ${md(_(text), wrap=wrap)}\n</%def>\n\n<%def name=\"_mdf(text, wrap=False, **kwargs)\">\n  ${md(_(text) % kwargs, wrap=wrap)}\n</%def>\n\n<%def name=\"nsfw_stamp()\">\n  <acronym title=\"${_('Adult content: Not Safe For Work')}\">\n    ${_(\"NSFW\")}\n  </acronym>\n</%def>\n\n<%def name=\"quarantine_stamp()\">\n  <span title=\"${_('Quarantined content: Content may be highly offensive')}\">\n    ${_(\"Quarantined\")}\n  </span>\n</%def>\n\n<%def name=\"thumbnail_img(thing)\">\n  %if thing.thumbnail and not thing.thumbnail_sprited:\n    <%\n        if hasattr(thing, 'thumbnail_size'):\n            scaling_factor = 1\n            if thing.thumbnail_size[0] > g.thumbnail_size[0]:\n              # hidpi scaling, calculate in case hidpi changes definition in the future and\n              # we have multiple sets of image dimensions. Currently should always be 1 or 2.\n              # Width is always the maximum allowed, so we don't need to check height.\n              scaling_factor = thing.thumbnail_size[0] // g.thumbnail_size[0]\n\n            size_str = \"width='%d' height='%d'\" % (thing.thumbnail_size[0] // scaling_factor, thing.thumbnail_size[1] // scaling_factor)\n        else:\n            size_str = \"\"\n    %>\n    <img src=\"${make_url_protocol_relative(thing.thumbnail)}\" ${size_str} alt=\"\">\n  %endif\n</%def>\n"
  },
  {
    "path": "r2/r2/templates/utils.xml",
    "content": "<%!\n    from r2.lib.filters import websafe, keep_space\n    from r2.lib.template_helpers import add_sr\n%>\n<%def name=\"atom_content(type='html', tag_name='content')\">\n    ## Atom supports 3 types of <content> tags: xhtml, html, and text. here we\n    ## try to support all 3\n    ## https://tools.ietf.org/html/rfc4287#section-3.1\n    %if type == 'xhtml':\n        <${tag_name} type=\"xhtml\" xml:base=\"${request.fullpath}\">\n            ## this <div> switches them into the xhtml namespace so they can say\n            ## <b> instead of <xhtml:b>, which is necessary for the output from\n            ## safemarkdown\n            <div xmlns=\"http://www.w3.org/1999/xhtml\">\n                ${caller.body()}\n            </div>\n        </${tag_name}>\n    %elif type == 'html':\n        <${tag_name} type=\"html\">\n            <%\n                # this must be double escaped\n                full_body = capture(caller.body)\n                full_body = websafe(full_body)\n            %>\n            ${full_body}\n        </${tag_name}>\n    %elif type == 'text':\n        <${tag_name}>${keep_space(caller.body())}</${tag_name}>\n    %else:\n        <% raise Exception(\"Unknown html type %r\" % (type,)) %>\n    %endif\n</%def>\n\n<%def name=\"atom_author(author)\">\n    %if not author._deleted:\n        <author>\n            <name>/u/${author.name}</name>\n            <uri>${add_sr('/user/'+author.name,\n                          sr_path=False,\n                          force_hostname=True,\n                          retain_extension=False)}</uri>\n        </author>\n    %endif\n</%def>\n"
  },
  {
    "path": "r2/r2/templates/verifyemail.email",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\nyour username is:\n\n    ${thing.user.name}\n\nvisit this link to verify your email address:\n\n    ${thing.emaillink}\n\nthanks for using the site!\n"
  },
  {
    "path": "r2/r2/templates/welcomebar.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<div class=\"infobar welcome\">\n  <h1>${thing.message[0]}</h1>\n  <div class=\"button-row\">\n    <h2>${thing.message[1]}</h2>\n    <a href=\"/about\">${_(\"learn more\")} &rsaquo;</a>\n  </div>\n</div>\n"
  },
  {
    "path": "r2/r2/templates/widgetdemopanel.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n   from r2.lib.template_helpers import get_domain, _wsf\n%>\n\n<% \n   domain = get_domain(subreddit=False)\n   sr_domain = get_domain(subreddit=True)\n %>\n\n<script type=\"text/javascript\">\nfunction escapeHTML(text) {\n  var div = document.createElement('div');\n  var text = document.createTextNode(text);\n  div.appendChild(text);\n  return div.innerHTML;\n}\n\nfunction getrval(r) {\n    for (var i=0; i < r.length; i++) {\n        if (r[i].checked) return r[i].value;\n    } \n}\n\nfunction showPreview(val) {\n  $(\"#previewbox\").html(val)\n}\n\nfunction update() {\n    f = document.forms.widget;\n    which = getrval(f.which);\n    if (which == \"all\") {\n        url = \"${g.default_scheme}://${sr_domain}/\" + f.what.value + \"/.embed?limit=\" +\n                      f.num.value + \"&t=\" + f.when.value;\n        if(f.what.value == \"new\") {\n           url += \"&sort=new\";\n        }\n    } else if (which == \"one\") {\n        if (!f.who.value) return;\n        url = \"${g.default_scheme}://${domain}/user/\"+f.who.value+\"/\"+\n                      f.where2.value+\".embed?limit=\" + f.num.value + \n                      \"&sort=\"+f.what.value;\n\n    } else if (which == \"two\") {\n        if(!f.domain.value) return;\n         url = \"${g.default_scheme}://${domain}/domain/\" + f.domain.value + \"/\" +\n                      f.what.value + \"/.embed?limit=\" \n                      + f.num.value  + \"&t=\" + f.when.value;\n        if(f.what.value == \"new\") {\n           url += \"&sort=new\";\n        }\n    } else {\n        alert(which);\n    }\n    $(\"#preview\").css(\"width\", \"\");\n    if(f.expanded.checked) {\n      url += \"&expanded=1\";\n    }\n    if(f.nostyle.checked) {\n      url += \"&style=off\";\n      $(\"#css-options\").hide();\n    }\n    else {\n      $(\"#css-options\").show();\n      if(f.border.checked && f.bord_color.value) {\n        url += \"&bordercolor=\" + f.bord_color.value;\n      }\n      if(f.background.checked && f.bg_color.value) {\n        url += \"&bgcolor=\" + f.bg_color.value;\n      }\n      if(f.twocol.checked) {\n        url += \"&twocolumn=true\";\n        $(\"#preview\").css(\"width\", \"550px\");\n      }\n    }\n\n    script = '<script src=\"' + \n                      escapeHTML(url).replace(/&amp;/g, '&') + \n                      '\" type=\"text/javascript\"><'+'/script>';\n    $(\"#codebox\").html(escapeHTML(script));\n    $.getScript(url+'&callback=showPreview');\n                      \n    }\n</script>\n\n<div class=\"instructions\">\n  \n  <div id=\"preview\">\n    <span>preview</span>\n    <div id=\"previewbox\">\n      <script src=\"${g.default_scheme}://${sr_domain}/.embed?limit=5\" type=\"text/javascript\"></script>\n    </div>\n  </div>\n\n  <h1>${_(\"get live %(site)s headlines on your site\") % dict(site=c.site.name)}</h1>\n  \n  <p>${_(\"just cut and paste the generated code into your site and your specified %(site)s feed will be displayed and updated as new stories bubble up\") % dict(site=c.site.name)}</p>\n\n  <h2>${_(\"which links do you want to display?\")}</h2>\n  \n  <form name=\"widget\" action=\"\" onsubmit=\"update(); return false\" id=\"widget\"\n        class=\"pretty-form\">\n    <%def name=\"when()\" buffered=\"True\">\n      <select name=\"when\" onchange=\"update()\" onfocus=\"update()\">\n        <option value=\"hour\">${_(\"this hour\")}</option>\n        <option value=\"day\">${_(\"today\")}</option>\n        <option value=\"week\">${_(\"this week\")}</option>\n        <option value=\"month\">${_(\"this month\")}</option>\n        <option value=\"all\" selected=\"selected\">${_(\"all-time\")}</option>\n      </select>\n    </%def>\n    <%def name=\"where2()\" buffered=\"True\">\n      <select name=\"where2\" onchange=\"update()\"\n              onfocus=\"this.parentNode.firstChild.checked='checked'\">\n        <option value=\"submitted\">${_(\"submitted by\")}</option>\n        <option value=\"saved\">${_(\"saved by\")}</option>\n        <option value=\"liked\">${_(\"upvoted by\")}</option>\n        <option value=\"disliked\">${_(\"downvoted by\")}</option>\n      </select> \n    </%def>\n    <%def name=\"text_input(name)\" buffered=\"True\">\n      <input type=\"text\" name=\"${name}\" value=\"\" \n             onfocus=\"this.parentNode.firstChild.checked='checked'\"\n             onchange=\"update()\" onblur=\"update()\" />\n    </%def>\n   <table class=\"widget-preview preftable\">\n     <tr>\n       <th>\n         ${_(\"listing options\")}\n       </th>\n       <td class=\"prefright\">\n         <p>\n           <input type=\"radio\" name=\"which\" value=\"all\" checked=\"checked\" \n                  onclick=\"update()\" /> \n           ${_(\"links from %(domain)s\") % dict(domain = get_domain())}\n         </p>\n         <p>\n          <input type=\"radio\" name=\"which\" value=\"one\" onclick=\"update()\" /> \n           ${_wsf(\"links %(submitted_by)s the user %(who)s\", submitted_by=unsafe(where2()), who=unsafe(text_input(\"who\")))}\n         </p>\n         <p>\n           <input type=\"radio\" name=\"which\" value=\"two\" onclick=\"update()\" /> \n          ${_wsf(\"links from the domain %(domain)s\", domain=unsafe(text_input(\"domain\")))}\n         </p>\n       </td>\n     </tr>\n     <tr>\n       <th>\n         ${_(\"sorting options\")}\n       </th>\n       <td class=\"prefright\">\n         <p>\n           <%def name=\"what()\" buffered=\"True\">\n           <select name=\"what\" onchange=\"update()\">\n             <option value=\"hot\" selected=\"selected\">${_(\"hottest\")}</option>\n             <option value=\"new\">${_(\"newest\")}</option>\n             <option value=\"top\">${_(\"top\")}</option>\n           </select>\n           </%def>\n           ${_wsf(\"sort links by %(what)s\", what=unsafe(what()))}\n         </p>\n         <p>\n           ${_wsf(\"date range includes %(when)s\", when=unsafe(when()))}\n         </p>\n         <p>\n           ${_(\"number of links to show\")}:\n           <select name=\"num\" onchange=\"update()\">\n             <option value=\"5\" selected=\"selected\">5</option>\n             <option value=\"10\">10</option>\n             <option value=\"20\">20</option>\n           </select>\n         </p>\n       </td>\n     </tr>\n     <tr>\n       <th>\n         ${_(\"display options\")}\n       </th>\n       <td class=\"prefright\">\n         <p>\n           <input name=\"nostyle\" id=\"nostyle\" type=\"checkbox\"\n                  onchange=\"update()\"/>\n           <label for=\"nostyle\">\n             ${_(\"remove css styling\")}  \n             &#32;<span class=\"little gray\">\n               ${_(\"(the widget will inherit all styles from the page)\")}\n             </span>\n           </label>\n         </p>\n         <p>\n           <input name=\"expanded\" id=\"expanded\" type=\"checkbox\"\n                  onchange=\"update()\"/>\n           <label for=\"expanded\">\n             ${_(\"enable in-widget voting\")}  \n             &#32;<span class=\"little gray\">\n               ${_(\"(will slow down the rendering)\")}\n             </span>\n           </label>\n         </p>\n         <div id=\"css-options\">\n         <p>\n           <input name=\"twocol\" id=\"twocol\" type=\"checkbox\"\n                  onchange=\"update()\"/>\n           <label for=\"twocol\">\n             ${_(\"two columns\")}\n           </label>\n         </p>\n         <p>\n           <input name=\"background\" id=\"background\" type=\"checkbox\"\n                  onchange=\"update()\"/>\n           <label for=\"background\">\n             ${_(\"background color\")}\n           </label>\n           &nbsp;#${unsafe(text_input(\"bg_color\"))}\n           &#32;<span class=\"little gray\">\n             ${_(\"(e.g., FF0000 = red)\")}\n           </span>\n         </p>\n         <p>\n           <input name=\"border\" id=\"border\" type=\"checkbox\" \n                  onchange=\"update()\"/>\n           <label for=\"border\"> \n             ${_(\"border color\")}\n           </label>\n           &nbsp;#${unsafe(text_input(\"bord_color\"))}\n         </p>\n         </div>\n       </td>\n     </tr>\n  </table>\n  </form>\n\n  <h2>${_(\"the code\")}</h2>\n\n  <p>${_(\"add this into your HTML where you want the %(site)s links displayed\") %dict(site=c.site.name)}</p>\n\n  <p>\n    <textarea rows=\"5\" cols=\"50\" id=\"codebox\">\n      &lt;script src=\"${g.default_scheme}://${domain}/.embed?limit=5\" type=\"text/javascript\">&lt;/script>\n    </textarea>\n  </p>\n</div>\n"
  },
  {
    "path": "r2/r2/templates/wikibasepage.html",
    "content": "﻿## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n<%!\n    from r2.config import feature\n%>\n\n<%namespace file=\"less.html\" import=\"less_stylesheet\"/>\n<%inherit file=\"reddit.html\"/>\n\n<%def name=\"global_stylesheets()\">\n    ${parent.global_stylesheets()}\n    ${less_stylesheet('wiki.less')}\n</%def>\n\n<%def name=\"actionsbar(actions)\">\n    %for action in actions:\n        <a class=\"wikiaction wikiaction-${action[0]}\n        %if action[0] == thing.action[0]:\n            wikiaction-current\n        %endif\n        \"\n        %if action[2]:\n            href=\"${thing.base_url}/${action[0]}/${thing.page}\"\n        %else:\n            href=\"${thing.base_url}/${action[0]}\"\n        %endif\n        data-type=\"subreddit\"\n        %if action[3]:\n            data-event-action=\"pageview\"\n            data-event-detail=\"${action[3]}\"\n        %endif\n        >${action[1]}</a>\n    %endfor\n</%def>\n\n<%def name=\"content()\">\n    ${thing.infobar}\n    <span>\n        <h1 class=\"wikititle\">\n            %if thing.pagetitle:\n                ${thing.pagetitle}\n            %endif\n            %if thing.page:\n                <strong>${thing.page}</strong>\n            %endif\n        </h1>\n        \n        %if thing.pageactions:\n            <span class=\"pageactions\">\n                ${actionsbar(thing.pageactions)}\n            </span>\n        %endif\n    </span>\n        \n    <div class=\"wiki-page-content md-container\">\n        %if thing.description:\n            <div class=\"description\">\n                <h2>\n                    %for desc in thing.description:\n                        ${desc}<br/>\n                    %endfor\n                </h2>\n            </div>\n        %endif\n        ${thing.content()}\n    </div>\n\n    <!--reddit wikis are powered by Cray-1™ supercomputers-->\n</%def>\n"
  },
  {
    "path": "r2/r2/templates/wikieditpage.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n  from r2.lib.filters import keep_space\n%>\n\n<%namespace file=\"printablebuttons.html\" import=\"toggle_button\" />\n<%namespace file=\"usertext.html\" import=\"markhelp\" />\n\n<div style=\"display: none;\" id=\"wiki_edit_conflict\">\n    <h2 class=\"error\">there was a conflict editing</h2>\n    <h1>${_(\"your edit\")}</h1>\n    <em>${_(\"this edit is for you to resolve the conflict, any text in the box below will not save.\")}</em><br/>\n    <textarea id=\"youredit\"></textarea>\n    <span id=\"yourdiff\"></span>\n    <h1>${_(\"current edit\")}</h1>\n</div>\n\n<div style=\"display: none;\" id=\"wiki_special_error\">\n    <h1>Errors: </h1>\n    <span id=\"specials\" class=\"error\"></span>\n</div>\n\n<form method=\"post\" id=\"editform\" onsubmit=\"r.wiki.submitEdit(event)\">\n    <textarea name=\"content\" rows=\"20\" cols=\"20\" style=\"width: 100%\" id=\"wiki_page_content\">${keep_space(thing.page_content)}</textarea>\n    ${toggle_button(\"help-toggle\", _(\"formatting help\"), _(\"hide help\"), \"r.wiki.helpon\", \"r.wiki.helpoff\")}\n    ${markhelp()}\n    <br/><br/>\n    <label for=\"reason\">${_(\"reason for revision\")}</label><br/>\n    <input type=\"text\" name=\"reason\" maxlength=\"256\" id=\"wiki_revision_reason\" />\n    <input type=\"hidden\" id=\"previous\" name=\"previous\" value=\"${thing.previous}\" />\n    <br/><br/><input type=\"submit\" id=\"wiki_save_button\" class=\"wiki_button\" value=\"${_('save page')}\" />\n    <span class=\"throbber\" />\n</form>\n"
  },
  {
    "path": "r2/r2/templates/wikipagediscussions.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%!\n   from r2.lib.template_helpers import get_domain\n%>\n\n${thing.listing}\n\n<div class=\"morelink discussionlink\">\n<a class=\"access-required\"\n   href=\"/submit?url=${g.default_scheme}://${get_domain()}/wiki/${thing.page}&resubmit=true&no_self=true&title=Check+out+this+wiki+page\"\n   type=\"subreddit\"\n   data-event-action=\"submit\"\n   data-event-detail=\"link\"\n>${_(\"submit a discussion\")}\n<div class=\"nub\"></div>\n</a>\n</div>\n\n"
  },
  {
    "path": "r2/r2/templates/wikipagediscussions.xml",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n${thing.listing}\n\n"
  },
  {
    "path": "r2/r2/templates/wikipagelisting.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<% from r2.models.wiki import WikiPage %>\n\n<%def name=\"listing(pages)\">\n    %for name, page_info in pages.iteritems():\n        <ul>\n        <li>\n        %if page_info[0]:\n            <a href=\"${c.wiki_base_url}/${page_info[0].name}\" target=\"_blank\">${name}</a></li>\n        %else:\n            ${name}\n        </li>\n         %endif\n        ${listing(page_info[1])}\n        </ul>\n    %endfor\n</%def>\n\n<div class=\"pagelisting\">\n${listing(thing.pages)}\n</div>\n"
  },
  {
    "path": "r2/r2/templates/wikipagenotfound.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n${_('The page \"%s\" was not found in this subreddit.') % thing.page}<br/>\n<a href=\"${c.wiki_base_url}/create/${thing.page}\">${_('Create page \"%s\"') % thing.page}\n"
  },
  {
    "path": "r2/r2/templates/wikipagerevisions.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n${thing.listing}\n%if thing.page and thing.listing.things:\n    <button onclick=\"r.wiki.goCompare()\">${_(\"compare selected\")}</button>\n%endif\n<br/>\n"
  },
  {
    "path": "r2/r2/templates/wikipagerevisions.xml",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n${thing.listing}\n"
  },
  {
    "path": "r2/r2/templates/wikipagesettings.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%namespace file=\"printablebuttons.html\" import=\"ynbutton\" />\n<%namespace name=\"utils\" file=\"utils.html\"/>\n\n<%!\n   from urllib import quote\n   from r2.lib.pages import WrappedUser\n%>\n\n<div class=\"fancy-settings\">\n    %if thing.show_settings:\n        <form id=\"pagesettings\" method=\"post\">\n            <input type=\"hidden\" name=\"uh\" value=\"${c.modhash}\" />\n            <%utils:line_field title=\" ${_('who can edit this page?')}\">\n                <input type=\"radio\" name=\"permlevel\" id=\"permlevel0\" value=\"0\"\n                    %if thing.permlevel == 0:\n                        checked\n                    %endif\n                /><label for=\"permlevel0\">${_('use subreddit wiki permissions')}</label><br/>\n                <input type=\"radio\" name=\"permlevel\" id=\"permlevel1\" value=\"1\"\n                %if thing.permlevel == 1:\n                    checked\n                %endif\n                /><label for=\"permlevel1\">${_('only approved wiki contributors for this page may edit')}</label><br/>\n                <input type=\"radio\" name=\"permlevel\" id=\"permlevel2\" value=\"2\"\n                %if thing.permlevel == 2:\n                    checked\n                %endif\n                /><label for=\"permlevel2\">${_('only mods may edit and view')}</label><br/>\n            </%utils:line_field>\n\n            <%utils:line_field title=\" ${_('show this page on the listing?')}\">\n                <input type=\"checkbox\" name=\"listed\" id=\"listed\"\n                    %if thing.listed:\n                        checked\n                    %endif\n                /><label for=\"listed\">${_('show this page on the list of wiki pages')}</label><br/>\n            </%utils:line_field>\n        </form>\n    %endif\n    %if thing.show_editors and thing.permlevel != 2:\n    <br/>\n    <%utils:line_field title=\"${_('allow users to edit page')}\">\n        <form id=\"WikiAllowEditor\" onsubmit=\"r.wiki.addUser(event)\">\n            <input name=\"username\" maxlength=\"20\" type=\"text\" style=\"width: 430px;\" />\n            <button type=\"submit\" style=\"font-size: 100%;\">${_('add')}</button>\n            <h3 class=\"error\" style=\"display:none\" id=\"usereditallowerror\">${_('username does not exist')}</h2>\n        </form>\n        <br/>\n        <ul>\n        %for user in thing.mayedit:\n            <li>\n                ${WrappedUser(user)}\n                &mdash;&nbsp;\n                ${ynbutton(_(\"delete\"), _(\"done\"), quote(\"..%s/alloweditor/del\" % (c.wiki_api_url)), hidden_data=dict(username=user.name, page=thing.page), post_callback=\"$.refresh\")}\n            </li>\n        %endfor\n        </ul>\n    </%utils:line_field>\n    %endif\n\n    <input type=\"submit\" class=\"wiki_button\" onclick=\"$('#pagesettings').submit()\" value=\"${_('save settings')}\" />\n</div>\n"
  },
  {
    "path": "r2/r2/templates/wikirevision.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%namespace file=\"utils.html\" import=\"timestamp\"/>\n<%namespace file=\"printablebuttons.html\" import=\"ynbutton\" />\n\n<%!\n   from urllib import quote\n%>\n\n<tr class=\"revision\n    %if thing._hidden:\n       hidden\n    %endif\n    %if thing.admin_deleted:\n        deleted\n    %endif\n    \">\n    \n        %if thing.show_compare:\n            <td style=\"white-space: nowrap;\">\n                <input type=\"radio\" name=\"v1\" value=\"${thing._id}\" checked=\"yes\">\n                <input type=\"radio\" name=\"v2\" value=\"${thing._id}\" checked=\"yes\">\n            </td>\n        %endif\n    \n    <td style=\"white-space: nowrap;\">\n        ${timestamp(thing.date, live=True, include_tense=True)}\n    </td>\n    \n    %if not thing.show_extended:\n        <td>\n            <a href=\"${c.wiki_base_url}/${thing.page}\">${thing.page}</a>\n        </td>\n    %endif\n    \n    <td>\n        <a href=\"${c.wiki_base_url}/${thing.page}?v=${thing._id}\">view</a>\n    </td>\n    \n    <td>\n        ${thing.printable_author}\n    </td>\n    \n    <td style=\"font-style: italic;\">\n        ${thing._get('reason')}\n    </td>\n   \n    %if thing.show_extended:\n        <td>\n            <a href=\"#\" class=\"revision_hide access-required\"\n               data-revision=\"${thing._id}\"\n               data-type=\"wikipage\"\n               data-event-action=\"wikirevise\"\n               data-event-detail=\"hide\"\n               >hide</a>\n        </td>\n        <td class=\"wiki_revert\" style=\"white-space: nowrap;\">\n            ${ynbutton(_(\"revert here\"), \n                       _(\"done\"),\n                       quote(\"..%s/revert\" % c.wiki_api_url), \n                       hidden_data=dict(revision=thing._id, page=thing.page), \n                       post_callback=\"$.refresh\",\n                       event_target='wikipage',\n                       event_action='wikirevise',\n                       event_detail='revert'\n                      )\n             }\n        </td>\n    %endif\n\n    %if c.user_is_admin:\n        <td>\n            <a href=\"#\" class=\"revision_delete\" data-revision=\"${thing._id}\">delete</a>\n        </td>\n    %endif\n</tr>\n"
  },
  {
    "path": "r2/r2/templates/wikirevision.xml",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n<%!\n    from r2.lib.template_helpers import add_sr\n    from r2.lib.template_helpers import html_datetime\n%>\n<%namespace name=\"utils\" file=\"utils.xml\"/>\n<%\n    link = add_sr(('/%(wiki_base_url)s/%(page)s?v=%(thing_id)s'\n                   % dict(wiki_base_url=c.wiki_base_url,\n                          page=thing.page,\n                          thing_id=thing._id)),\n                  force_hostname=True,\n                  retain_extension=False,\n                  sr_path=False)\n    category = ('%(sr)s/%(page)s'\n                % dict(sr=thing.sr,\n                       page=thing.page))\n%>\n<entry>\n    <author><name>${thing.printable_author}</name></author>\n    <category term=\"${category}\" label=\"${category}\" />\n    <%utils:atom_content type=\"text\">${thing._get('reason')}</%utils:atom_content>\n    <id>${thing._fullname}</id>\n    <link href=\"${link}\" />\n    <updated>${html_datetime(thing.date)}</updated>\n    <title>${thing._id}</title> ## is this really the best title?\n</entry>\n"
  },
  {
    "path": "r2/r2/templates/wikiview.compact",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<p>\n    %if not thing.page_content_md:\n        <em>${_(\"this page is empty\")}</em>\n    %else:\n        ${unsafe(thing.page_content)}\n    %endif\n</p>\n"
  },
  {
    "path": "r2/r2/templates/wikiview.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%namespace file=\"utils.html\" import=\"timestamp\"/>\n\n<%!\n    from r2.lib.pages import WrappedUser\n    from r2.lib.filters import SC_OFF, SC_ON\n    from r2.lib.template_helpers import _wsf\n%>\n\n%if thing.diff:\n    <p>\n        ${unsafe(thing.diff)}\n    </p>\n%endif\n<p>\n    %if not thing.page_content_md:\n        <em>${_(\"this page is empty\")}</em>\n    %else:\n        ${unsafe(thing.page_content)}\n    %endif\n    ${unsafe(SC_OFF)}\n    <textarea readonly class=\"source\" rows=\"20\" cols=\"20\">${thing.page_content_md}</textarea>\n    ${unsafe(SC_ON)}\n</p>\n<hr/>\n<em>\n%if thing.edit_date:\n    %if thing.edit_by:\n         ${_wsf(\"revision by %(user)s\", user=WrappedUser(thing.edit_by))}\n         &mdash;&nbsp;\n    %endif\n    ${timestamp(thing.edit_date, include_tense=True)}\n%endif\n<a href=\"#\" class=\"toggle-source\">${_(\"view source\")}</a>\n</em>\n"
  },
  {
    "path": "r2/r2/templates/wrappeduser.compact",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%inherit file=\"wrappeduser.html\" />\n"
  },
  {
    "path": "r2/r2/templates/wrappeduser.html",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%namespace file=\"utils.html\" import=\"plain_link\" />\n\n<%def name=\"flair(user, enabled=None)\">\n  %if enabled is None:\n    <% enabled = user.flair_enabled %>\n  %endif\n  %if user.has_flair and enabled:\n    <span class=\"flair ${user.flair_css_class}\" title=\"${user.flair_text}\">${user.flair_text}</span>\n  %endif\n</%def>\n\n<%def name=\"make_distinguish(distinguish_tuples)\">\n<span class=\"userattrs\">\n%if distinguish_tuples:\n  [\n  %for priority, abbv, css_class, label, attr_link in distinguish_tuples:\n    %if attr_link:\n      <a class=\"${css_class}\" title=\"${label}\"\n      %if target:\n         target=\"${target}\"\n      %endif\n         href=\"${attr_link}\">\n        ${unsafe(abbv)}\n      </a>\n    %else:\n      <span class=\"${css_class}\" title=\"${label}\">${abbv}</span>\n    %endif\n\n    ## this is a hack to print a comma after all but the final attr\n    %if priority != distinguish_tuples[-1][0]:\n      ,\n    %endif\n  %endfor\n  ]\n%endif\n</span>\n</%def>\n\n%if context_deleted and not c.user_is_admin:\n  <span class=\"author\">[deleted]</span>\n%else:\n  %if thing.user_deleted:\n    <span class=\"author\">[deleted]</span>\n  %elif thing.name == '[blocked]':\n    <span class=\"author\">${_(thing.thing.original_author.name)}</span>\n  %else:\n    %if thing.flair_position == 'left':\n      ${flair(thing, enabled=thing.force_show_flair)}\n    %endif\n    <%\n      classes = [thing.author_cls, 'may-blank', 'id-%s' % thing.fullname]\n      if thing.include_flair_selector:\n          classes.append('flairselectable')\n    %>\n    ${plain_link(thing.name + thing.karma, \"/user/%s\" % thing.name,\n                 _class = ' '.join(classes),\n                 _sr_path = False, target=target, title=thing.author_title)}\n    %if thing.flair_position == 'right':\n      ${flair(thing, enabled=thing.force_show_flair)}\n    %endif\n    %if thing.include_flair_selector:\n      (<a class=\"flairselectbtn access-required\"\n          data-name=\"${thing.name}\"\n          data-type=\"account\" data-fullname=\"${thing.fullname}\"\n          data-event-action=\"editflair\" data-event-detail=\"set\"\n          href=\"javascript://void(0)\">${_('edit')}</a>)\n      <div class=\"flairselector drop-choices\"></div>\n    %endif\n    ${make_distinguish(thing.attribs)}\n  %endif\n%endif\n\n%if thing.ip_span:\n  ${unsafe(thing.ip_span)}\n%endif\n\n%if thing.show_details_link and thing.context_thing_fullname:\n  &#32;\n  <a class=\"adminbox\" href=\"/details/${thing.context_thing_fullname}\">voting</a>\n%endif\n"
  },
  {
    "path": "r2/r2/templates/wrappeduser.mobile",
    "content": "## The contents of this file are subject to the Common Public Attribution\n## License Version 1.0. (the \"License\"); you may not use this file except in\n## compliance with the License. You may obtain a copy of the License at\n## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n## License Version 1.1, but Sections 14 and 15 have been added to cover use of\n## software over a computer network and provide for limited attribution for the\n## Original Developer. In addition, Exhibit A has been modified to be\n## consistent with Exhibit B.\n##\n## Software distributed under the License is distributed on an \"AS IS\" basis,\n## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n## the specific language governing rights and limitations under the License.\n##\n## The Original Code is reddit.\n##\n## The Original Developer is the Initial Developer.  The Initial Developer of\n## the Original Code is reddit Inc.\n##\n## All portions of the code written by reddit are Copyright (c) 2006-2015\n## reddit Inc. All Rights Reserved.\n###############################################################################\n\n<%\nif thing.has_flair and thing.flair_enabled:\n  flair = thing.flair_text\n  flair_left = thing.flair_position == 'left'\nelse:\n  flair = False\n%>\n\n%if thing.user_deleted:\n  <span>[deleted]</span>\n%else:\n  %if flair and flair_left:\n    <span class=\"flair\">${flair}</span>\n  %endif\n  <a href=\"/user/${thing.name}\" class=\"${thing.author_cls}\">\n  <b>${thing.name}</b></a>\n  %if flair and not flair_left:\n    <span class=\"flair\">${flair}</span>\n  %endif\n%endif\n"
  },
  {
    "path": "r2/r2/tests/__init__.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport os\nimport sys\nimport Queue\nfrom unittest import TestCase\nfrom mock import patch, MagicMock\nfrom collections import defaultdict\n\nimport pylons\nfrom pylons.i18n.translation import _get_translator\nfrom routes.util import URLGenerator\nfrom pylons import url\nimport baseplate.events\nimport pkg_resources\nimport paste.fixture\nimport paste.script.appinstall\nfrom paste.deploy import loadapp\n\nfrom routes.util import url_for\nfrom r2.lib.utils import query_string\nfrom r2.lib import eventcollector\n\n\n__all__ = ['RedditTestCase', 'RedditControllerTestCase']\n\nhere_dir = os.path.dirname(os.path.abspath(__file__))\nconf_dir = os.path.dirname(os.path.dirname(here_dir))\n\nsys.path.insert(0, conf_dir)\npkg_resources.working_set.add_entry(conf_dir)\npkg_resources.require('Paste')\npkg_resources.require('PasteScript')\n\n\n# on case-insensitive file systems, Captcha gets masked by\n# r2.lib.captcha which is also in sys.path.  This import ensures\n# that the subsequent import is already in sys.modules, sidestepping\n# the issue\ntry:\n    from Captcha import Base\nexcept ImportError:\n    with patch.object(\n        sys, \"path\",\n        [x for x in sys.path if not x.endswith(\"r2/r2/lib\")]\n    ):\n        from Captcha import Base\n\nfrom pylons import app_globals as g\nfrom r2.config.middleware import RedditApp\n\n# unfortunately, because of the deep intertwinded dependency we have in the\n# orm with app_globals, we unfortunately have to do some pylons-setup\n# at import time\nbaseplate.events.EventQueue = Queue.Queue\nwsgiapp = loadapp('config:test.ini', relative_to=conf_dir)\npylons.app_globals._push_object(wsgiapp.config['pylons.app_globals'])\npylons.config._push_object(wsgiapp.config)\n\n# Initialize a translator for tests that utilize i18n\ntranslator = _get_translator(pylons.config.get('lang'))\npylons.translator._push_object(translator)\n\nurl._push_object(URLGenerator(pylons.config['routes.map'], {}))\n\n\ndef diff_dicts(d, expected, prefix=None):\n    \"\"\"Given 2 dicts, return a summary of their differences\n\n    :param dict d: dict to match from (\"got\")\n    :param dict expected: dict to match against (\"want\")\n    :param prefix: key prefix (used for recursion)\n    :type prefix: list or None\n    :rtype: dict\n    :returns: mapping of flattened keys to 2-ples of (got, want)\n    \"\"\"\n    prefix = prefix or []\n    diffs = {}\n    for k in set(d.keys() + expected.keys()):\n        current_prefix = prefix + [k]\n        want = d.get(k)\n        got = expected.get(k)\n        if isinstance(want, dict) and isinstance(got, dict):\n            diffs.update(diff_dicts(got, want, prefix=current_prefix))\n        elif got != want:\n            key = \".\".join(current_prefix)\n            diffs[key] = (got, want)\n    return diffs\n\n\nclass DiffAssertionError(AssertionError):\n    def __init__(self, diffs):\n        s = \"\\n\".join(\n            \"\\t{key}: {want} != {got}\".format(\n                key=key, want=repr(want), got=repr(got),\n            ) for key, (want, got) in sorted(diffs.iteritems())\n        )\n        super(DiffAssertionError, self).__init__(\n            \"Mismatched ditionaries:\\n%s\" % s\n        )\n\n\ndef assert_same_dict(data, expected_data):\n    \"\"\"Asserts two dicts are the same (recursively)\n\n    :param dict data: dictionary to be compared from\n    :param dict expected_data: expected dictionary\n\n    :raises: :py:class:`DiffAssertionError`\n    \"\"\"\n    diffs = diff_dicts(data, expected_data)\n    if diffs:\n        raise DiffAssertionError(diffs)\n\n\nclass MockAmqp(object):\n    \"\"\"An amqp replacement, suitable for unit tests.\n    Besides providing a mock `queue` for storing all received events, this\n    class provides a set of handy assert-style functions for checking what\n    was previously queued.\n    \"\"\"\n    def __init__(self, test_cls):\n        self.queue = defaultdict(list)\n        self.test_cls = test_cls\n\n    def add_item(self, name, body, **kw):\n        self.queue[name].append((body, kw))\n\n    def assert_item_count(self, name, count=None):\n        \"\"\"Assert that `count` items have been queued in queue `name`.\n\n        If count is none, just asserts that at least one item has been added\n        to that queue\n        \"\"\"\n        if count is None:\n            self.test_cls.assertTrue(bool(self.queue.get(name)))\n        else:\n            err = \"expected %d events in queue, saw %d\" % (\n                count, len(self.queue)\n            )\n            assert len(self.queue) == count, err\n\n    def assert_event_item(\n        self, expected_data, expected_num=1, name=\"event_collector\"\n    ):\n        candidates = []\n\n        queue = self.queue[name]\n\n        # find candidate events that are of the same topic as the provided\n        # error.\n        for data, _ in queue:\n            data = data.copy()\n            # and do they have a timestamp, uuid, and payload?\n            assert data.pop(\"event_ts\", None) is not None, \\\n                \"event_ts is missing\"\n            assert data.pop(\"uuid\", None) is not None, \"uuid is missing\"\n            # there is some variability, but this should at least be present\n            assert \"event_topic\" in data, \"event_topic is missing\"\n\n            if data['event_topic'] == expected_data['event_topic']:\n                candidates.append(data)\n\n        # No candidates when expecting some, fail early.\n        if not candidates and expected_num > 0:\n            raise AssertionError(\n                \"No %r events found\" % expected_data['event_topic']\n            )\n\n        # for each candidate, look for an exact dictionary match\n        diffs = []\n        nmatches = 0\n        for candidate in candidates:\n            diff = diff_dicts(candidate, expected_data)\n            if not diff:\n                nmatches += 1\n            diffs.append(diff)\n\n        # if no matches, present the closest match.\n        if nmatches == 0 and expected_num > 0:\n            raise DiffAssertionError(min(diffs, key=len))\n\n        # raise if we found matches, but not the correct number\n        if nmatches != expected_num:\n            raise AssertionError(\"Expected %d event, got %d of %r\" % (\n                expected_num, nmatches, expected_data,\n            ))\n\n\nclass RedditTestCase(TestCase):\n    \"\"\"Base Test Case for tests that require the app environment to run.\n\n    App startup does take time, so try to use unittest.TestCase directly when\n    this isn't necessary as it'll save time.\n\n    \"\"\"\n    def setUp(self):\n\n        # disable controllers for this type of test, and make sure to set\n        # things back to where they started.\n        def reset_test_mode(orig_value=RedditApp.test_mode):\n            RedditApp.test_mode = orig_value\n        self.addCleanup(reset_test_mode)\n        RedditApp.test_mode = True\n\n        self.app = paste.fixture.TestApp(wsgiapp)\n        test_response = self.app.get(\"/_test_vars\")\n        request_id = int(test_response.body)\n        self.app.pre_request_hook = lambda self: \\\n            paste.registry.restorer.restoration_end()\n        self.app.post_request_hook = lambda self: \\\n            paste.registry.restorer.restoration_begin(request_id)\n        paste.registry.restorer.restoration_begin(request_id)\n\n    def assert_same_dict(self, data, expected_data, prefix=None):\n        prefix = prefix or []\n        for k in set(data.keys() + expected_data.keys()):\n            current_prefix = prefix + [k]\n            want = expected_data.get(k)\n            got = data.get(k)\n            if isinstance(want, dict) and isinstance(got, dict):\n                self.assert_same_dict(got, want, prefix=current_prefix)\n            else:\n                self.assertEqual(\n                    got, want,\n                    \"Mismatch for %s: %r != %r\" % (\n                        \".\".join(current_prefix), got, want\n                    )\n                )\n\n    def mock_eventcollector(self):\n        \"\"\"Mock out the parts of the event collector which write to the queue.\n\n        Also mocks `domain` and `to_epoch_milliseconds` as it makes writing\n        tests easier since we pass in mock data to the events as well.\n        \"\"\"\n        p = patch.object(eventcollector.json, \"dumps\", lambda x: x)\n        p.start()\n        self.addCleanup(p.stop)\n\n        amqp = MockAmqp(self)\n        self.amqp = self.autopatch(g.events, \"queue\", amqp)\n\n        self.domain_mock = self.autopatch(eventcollector, \"domain\")\n\n        self.created_ts_mock = MagicMock(name=\"created_ts\")\n        self.to_epoch_milliseconds = self.autopatch(\n            eventcollector, \"to_epoch_milliseconds\",\n            return_value=self.created_ts_mock)\n\n    def autopatch(self, obj, attr, *a, **kw):\n        \"\"\"Helper method to patch an object and automatically cleanup.\"\"\"\n        p = patch.object(obj, attr, *a, **kw)\n        m = p.start()\n        self.addCleanup(p.stop)\n        return m\n\n    def patch_g(self, **kw):\n        \"\"\"Helper method to patch attrs on pylons.g.\n\n        Since we do this all the time.  autpatch g with the provided kw.\n        \"\"\"\n        for k, v in kw.iteritems():\n            self.autopatch(g, k, v, create=not hasattr(g, k))\n\n    def patch_liveconfig(self, k, v):\n        \"\"\"Helper method to patch g.live_config (with cleanup).\"\"\"\n        def cleanup(orig=g.live_config[k]):\n            g.live_config[k] = orig\n        g.live_config[k] = v\n        self.addCleanup(cleanup)\n\n\nclass NonCache(object):\n    def get(self, *a, **kw):\n        return\n\n    def get_multi(self, *a, **kw):\n        return {}\n\n    def set(self, *a, **kw):\n        return\n\n    def set_multi(self, *a, **kw):\n        return\n\n    def add(self, *a, **kw):\n        return\n\n    def incr(self, *a, **kw):\n        return\n\n\nclass RedditControllerTestCase(RedditTestCase):\n    CONTROLLER = None\n    ACTIONS = {}\n\n    def setUp(self):\n        super(RedditControllerTestCase, self).setUp()\n        from r2.models import Link, Subreddit, Account\n        # unfortunately, these classes' _type attrs are used as import\n        # side effects for some controllers, and need to be set for things\n        # to work properly\n        for i, _cls in enumerate((Link, Subreddit, Account)):\n            if not hasattr(_cls, \"_type_id\"):\n                self.autopatch(_cls, \"_type_id\", i + 1000, create=True)\n            if not hasattr(_cls, \"_type_name\"):\n                self.autopatch(\n                    _cls, \"_type_name\", _cls.__name__.lower(),\n                    create=True)\n        # The same is true for _by_name on Subreddit and Account\n        self.subreddit_by_name = self.autopatch(Subreddit, \"_by_name\")\n        self.account_by_name = self.autopatch(Account, \"_by_name\")\n\n        # mock out any Memcached side effects\n        self.patch_g(\n            rendercache=NonCache(),\n            ratelimitcache=NonCache(),\n            commentpanecache=NonCache(),\n            gencache=NonCache(),\n            memoizecache=NonCache(),\n        )\n\n        self.mock_eventcollector()\n\n        self.simple_event = self.autopatch(g.stats, \"simple_event\")\n\n        self.user_agent = \"Hacky McBrowser/1.0\"\n        self.device_id = None\n\n        # Lastly, pull the app out of test mode so it'll load controllers on\n        # first use\n        RedditApp.test_mode = False\n\n    def do_post(self, action, params, headers=None, expect_errors=False):\n\n        assert self.CONTROLLER is not None\n\n        body = self.make_qs(**params)\n\n        headers = headers or {}\n        headers.setdefault('User-Agent', self.user_agent)\n        if self.device_id:\n            headers.setdefault('Client-Vendor-ID', self.device_id)\n        for k, v in self.additional_headers(headers, body).iteritems():\n            headers.setdefault(k, v)\n        headers = {k: v for k, v in headers.iteritems() if v is not None}\n        return self.app.post(\n            url_for(controller=self.CONTROLLER,\n                    action=self.ACTIONS.get(action, action)),\n            extra_environ={\"REMOTE_ADDR\": \"1.2.3.4\"},\n            headers=headers,\n            params=body,\n            expect_errors=expect_errors,\n        )\n\n    def make_qs(self, **kw):\n        \"\"\"Convert the provided kw into a kw string suitable for app.post.\"\"\"\n        return query_string(kw).lstrip(\"?\")\n\n    def additional_headers(self, headers, body):\n        \"\"\"Additional generated headers to be added to the request.\"\"\"\n        return {}\n"
  },
  {
    "path": "r2/r2/tests/functional/__init__.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\n"
  },
  {
    "path": "r2/r2/tests/functional/controller/__init__.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n"
  },
  {
    "path": "r2/r2/tests/functional/controller/del_msg_test.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\nimport contextlib\n\nfrom r2.tests import RedditControllerTestCase\nfrom mock import patch, MagicMock\nfrom r2.lib.validator import VByName, VUser, VModhash\n\nfrom r2.models import Link, Message, Account\n\nfrom pylons import app_globals as g\n\n\nclass DelMsgTest(RedditControllerTestCase):\n    CONTROLLER = \"api\"\n\n    def setUp(self):\n        super(DelMsgTest, self).setUp()\n\n        self.id = 1\n\n    def test_del_msg_success(self):\n        \"\"\"Del_msg succeeds: Returns 200 and sets del_on_recipient.\"\"\"\n        message = MagicMock(spec=Message)\n        message.name = \"msg_1\"\n        message.to_id = self.id\n        message.del_on_recipient = False\n\n        with self.mock_del_msg(message):\n            res = self.do_del_msg(message.name)\n\n            self.assertEqual(res.status, 200)\n            self.assertTrue(message.del_on_recipient)\n\n    def test_del_msg_failure_with_link(self):\n        \"\"\"Del_msg fails: Returns 200 and does not set del_on_recipient.\"\"\"\n        link = MagicMock(spec=Link)\n        link.del_on_recipient = False\n        link.name = \"msg_2\"\n\n        with self.mock_del_msg(link):\n            res = self.do_del_msg(link.name)\n\n            self.assertEqual(res.status, 200)\n            self.assertFalse(link.del_on_recipient)\n\n    def test_del_msg_failure_with_null_msg(self):\n        \"\"\"Del_msg fails: Returns 200 and does not set del_on_recipient.\"\"\"\n        message = MagicMock(spec=Message)\n        message.name = \"msg_3\"\n        message.to_id = self.id\n        message.del_on_recipient = False\n\n        with self.mock_del_msg(message, False):\n            res = self.do_del_msg(message.name)\n\n            self.assertEqual(res.status, 200)\n            self.assertFalse(message.del_on_recipient)\n\n    def test_del_msg_failure_with_sender(self):\n        \"\"\"Del_msg fails: Returns 200 and does not set del_on_recipient.\"\"\"\n        message = MagicMock(spec=Message)\n        message.name = \"msg_3\"\n        message.to_id = self.id + 1\n        message.del_on_recipient = False\n\n        with self.mock_del_msg(message):\n            res = self.do_del_msg(message.name)\n\n            self.assertEqual(res.status, 200)\n            self.assertFalse(message.del_on_recipient)\n\n    def mock_del_msg(self, thing, ret=True):\n        \"\"\"Context manager for mocking del_msg.\"\"\"\n\n        return contextlib.nested(\n            patch.object(VByName, \"run\", return_value=thing if ret else None),\n            patch.object(VModhash, \"run\", side_effect=None),\n            patch.object(VUser, \"run\", side_effect=None),\n            patch.object(thing, \"_commit\", side_effect=None),\n            patch.object(Account, \"_id\", self.id, create=True),\n            patch.object(g.events, \"message_event\", side_effect=None),\n        )\n\n    def do_del_msg(self, name, **kw):\n        return self.do_post(\"del_msg\", {\"id\": name}, **kw)\n"
  },
  {
    "path": "r2/r2/tests/functional/controller/login/__init__.py",
    "content": ""
  },
  {
    "path": "r2/r2/tests/functional/controller/login/api_tests.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\nfrom r2.tests import RedditControllerTestCase\nfrom common import LoginRegBase\n\n\nclass LoginRegTests(LoginRegBase, RedditControllerTestCase):\n    CONTROLLER = \"api\"\n\n    def assert_success(self, res):\n        self.assertEqual(res.status, 200)\n        self.assertTrue(\"error\" not in res)\n\n    def assert_failure(self, res, code=None):\n        self.assertEqual(res.status, 200)\n        self.assertTrue(\"error\" in res)\n        self.assertTrue(code in res)\n"
  },
  {
    "path": "r2/r2/tests/functional/controller/login/apiv1_tests.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\nimport contextlib\nimport unittest\nimport json\nfrom mock import patch, MagicMock\n\nfrom r2.lib import signing\nfrom r2.tests import RedditControllerTestCase\nfrom r2.lib.validator import VThrottledLogin, VUname\nfrom common import LoginRegBase\n\n\nclass APIV1LoginTests(LoginRegBase, RedditControllerTestCase):\n    CONTROLLER = \"apiv1login\"\n\n    def setUp(self):\n        super(APIV1LoginTests, self).setUp()\n        self.device_id = \"dead-beef\"\n\n    def make_ua_signature(self, platform=\"test\", version=1):\n        payload = \"User-Agent:{}|Client-Vendor-ID:{}\".format(\n            self.user_agent, self.device_id,\n        )\n        return self.sign(payload, platform, version)\n\n    def sign(self, payload, platform=\"test\", version=1):\n        return signing.sign_v1_message(payload, platform, version)\n\n    def additional_headers(self, headers, body):\n        return {\n            signing.SIGNATURE_UA_HEADER: self.make_ua_signature(),\n            signing.SIGNATURE_BODY_HEADER: self.sign(\"Body:\" + body),\n        }\n\n    def assert_success(self, res):\n        self.assertEqual(res.status, 200)\n        body = res.body\n        body = json.loads(body)\n        self.assertTrue(\"json\" in body)\n        errors = body['json'].get(\"errors\")\n        self.assertEqual(len(errors), 0)\n        data = body['json'].get(\"data\")\n        self.assertTrue(bool(data))\n        self.assertTrue(\"modhash\" in data)\n        self.assertTrue(\"cookie\" in data)\n\n    def assert_failure(self, res, code=None):\n        self.assertEqual(res.status, 200)\n        body = res.body\n        body = json.loads(body)\n        self.assertTrue(\"json\" in body)\n        errors = body['json'].get(\"errors\")\n        self.assertTrue(code in [x[0] for x in errors])\n        data = body['json'].get(\"data\")\n        self.assertFalse(bool(data))\n\n    def assert_403_response(self, res, calling):\n        self.assertEqual(res.status, 403)\n        self.simple_event.assert_any_call(calling)\n        self.assert_headers(\n            res,\n            \"content-type\",\n            \"application/json; charset=UTF-8\",\n        )\n\n    def test_nosigning_login(self):\n        res = self.do_login(\n            headers={\n                signing.SIGNATURE_UA_HEADER: None,\n                signing.SIGNATURE_BODY_HEADER: None,\n            },\n            expect_errors=True,\n        )\n        self.assert_403_response(res, \"signing.ua.invalid.invalid_format\")\n\n    def test_no_body_signing_login(self):\n        res = self.do_login(\n            headers={\n                signing.SIGNATURE_BODY_HEADER: None,\n            },\n            expect_errors=True,\n        )\n        self.assert_403_response(res, \"signing.body.invalid.invalid_format\")\n\n    def test_nosigning_register(self):\n        res = self.do_register(\n            headers={\n                signing.SIGNATURE_UA_HEADER: None,\n                signing.SIGNATURE_BODY_HEADER: None,\n            },\n            expect_errors=True,\n        )\n        self.assert_403_response(res, \"signing.ua.invalid.invalid_format\")\n\n    def test_no_body_signing_register(self):\n        res = self.do_login(\n            headers={\n                signing.SIGNATURE_BODY_HEADER: None,\n            },\n            expect_errors=True,\n        )\n        self.assert_403_response(res, \"signing.body.invalid.invalid_format\")\n\n    @unittest.skip(\"registration captcha is unfinished\")\n    def test_captcha_blocking(self):\n        with contextlib.nested(\n            self.mock_register(),\n            self.failed_captcha()\n        ):\n            res = self.do_register()\n            self.assert_success(res)\n"
  },
  {
    "path": "r2/r2/tests/functional/controller/login/common.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2016 reddit\n# Inc. All Rights Reserved.\n###############################################################################\nimport contextlib\nimport unittest\nfrom mock import patch, MagicMock\n\nfrom pylons import app_globals as g\n\nfrom r2.lib.validator import VThrottledLogin, VUname, validator\nfrom r2.models import Account, NotFound\n\n\nclass LoginRegBase(object):\n    \"\"\"Mixin for login-centered controller tests.\n\n    This class is (purposely) not a test case that'll be picked up by nose\n    but rather should be added as a mixin on a RedditControllerTestCase\n    subclass. The subclass needs to implement\n\n     * assert_success - passed a result of do_post, and invoked in places\n       where we expect the request to have succeeded\n     * assert_failure - same, for failed and error cases from the server.\n\n    Included are base test cases that should be common to all controllers\n    which use r2.lib.controlers.login as part of the flow.\n    \"\"\"\n    def do_login(self, user=\"test\", passwd=\"test123\", **kw):\n        return self.do_post(\"login\", {\"user\": user, \"passwd\": passwd}, **kw)\n\n    def do_register(\n        self, user=\"test\", passwd=\"test123\", passwd2=\"test123\", **kw\n    ):\n        return self.do_post(\"register\", {\n            \"user\": user,\n            \"passwd\": passwd,\n            \"passwd2\": passwd2,\n        }, **kw)\n\n    def mock_login(self, name=\"test\", cookie=\"cookievaluehere\"):\n        \"\"\"Context manager for mocking login.\n\n        Patches VThrottledLogin to always return a mock with the provided\n        name and cookie value\n        \"\"\"\n        account = MagicMock()\n        account.name = name\n        account.make_cookie.return_value = cookie\n        return patch.object(VThrottledLogin, \"run\", return_value=account)\n\n    def mock_register(self):\n        \"\"\"Context manager for mocking out registration.\n\n        Within this context, new users can be registered but they will\n        be mock objects.  Also all usernames can be registered as the account\n        lookup is bypassed and Account._by_name always raises NotFound.\n        \"\"\"\n        from r2.controllers import login\n        return contextlib.nested(\n            patch.object(login, \"register\"),\n            patch.object(VUname, \"run\", return_value=\"test\"),\n            # ensure this user does not currently exist\n            patch.object(Account, \"_by_name\", side_effect=NotFound),\n        )\n\n    def failed_captcha(self):\n        \"\"\"Context manager for mocking a failed captcha.\"\"\"\n        return contextlib.nested(\n            # ensure that a captcha is needed\n            patch.object(\n                validator,\n                \"need_provider_captcha\",\n                return_value=True,\n            ),\n            # ensure that the captcha is invalid\n            patch.object(\n                g.captcha_provider,\n                \"validate_captcha\",\n                return_value=False,\n            ),\n        )\n\n    def disabled_captcha(self):\n        \"\"\"Context manager for mocking a disabled captcha.\n\n        Will raise an AssertionError if the captcha code is called.\n        \"\"\"\n        return contextlib.nested(\n            # ensure that a captcha is not needed\n            patch.object(\n                validator,\n                \"need_provider_captcha\",\n                return_value=False,\n            ),\n            # ensure that the captcha is unused\n            patch.object(\n                g.captcha_provider,\n                \"validate_captcha\",\n                side_effect=AssertionError,\n            ),\n        )\n\n    def find_headers(self, res, name):\n        \"\"\"Find header in res\"\"\"\n        for k, v in res.headers:\n            if k == name.lower():\n                yield v\n\n    def assert_headers(self, res, name, test):\n        \"\"\"Assert header value with test (lambda function or value)\"\"\"\n        for value in self.find_headers(res, name):\n            if callable(test) and test(value):\n                return\n            elif value == test:\n                return\n        raise AssertionError(\"No matching %s header found\" % name)\n\n    def assert_success(self, res):\n        \"\"\"Test that is run when we expect the post to succeed.\"\"\"\n        raise NotImplementedError\n\n    def assert_failure(self, res, code=None):\n        \"\"\"Test that is run when we expect the post to fail.\"\"\"\n        raise NotImplementedError\n\n    def test_login(self):\n        with self.mock_login():\n            res = self.do_login()\n            self.assert_success(res)\n\n    def test_login_wrong_password(self):\n        with patch.object(Account, \"_by_name\", side_effect=NotFound):\n            res = self.do_login()\n            self.assert_failure(res, \"WRONG_PASSWORD\")\n\n    def test_register(self):\n        with self.mock_register():\n            res = self.do_register()\n            self.assert_success(res)\n\n    def test_register_username_taken(self):\n        with patch.object(\n            Account, \"_by_name\", return_value=MagicMock(_deleted=False)\n        ):\n            res = self.do_register()\n            self.assert_failure(res, \"USERNAME_TAKEN\")\n\n    @unittest.skip(\"registration captcha is unfinished\")\n    def test_captcha_blocking(self):\n        with contextlib.nested(\n            self.mock_register(),\n            self.failed_captcha()\n        ):\n            res = self.do_register()\n            self.assert_failure(res, \"BAD_CAPTCHA\")\n\n    @unittest.skip(\"registration captcha is unfinished\")\n    def test_captcha_disabling(self):\n        with contextlib.nested(\n            self.mock_register(),\n            self.disabled_captcha()\n        ):\n            res = self.do_register()\n            self.assert_success(res)\n"
  },
  {
    "path": "r2/r2/tests/functional/controller/login/post_tests.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\nfrom r2.tests import RedditControllerTestCase\nfrom r2.lib.errors import error_list\nfrom r2.lib.unicode import _force_unicode\nfrom r2.models import Subreddit\nfrom common import LoginRegBase\n\n\nclass PostLoginRegTests(LoginRegBase, RedditControllerTestCase):\n    CONTROLLER = \"post\"\n    ACTIONS = {\n        \"register\": \"reg\",\n    }\n\n    def setUp(self):\n        super(PostLoginRegTests, self).setUp()\n        self.autopatch(Subreddit, \"_byID\", return_value=[])\n        self.dest = \"/foo\"\n\n    def assert_success(self, res):\n        # On sucess, we redirect the user to the provided \"dest\" parameter\n        # that has been added in make_qs\n        self.assertEqual(res.status, 302)\n        self.assert_headers(\n            res,\n            \"Location\",\n            lambda value: value.endswith(self.dest)\n        )\n        self.assert_headers(\n            res,\n            \"Set-Cookie\",\n            lambda value: value.startswith(\"reddit_session=\")\n        )\n\n    def assert_failure(self, res, code=None):\n        # counterintuitively, failure to login will return a 200\n        # (compared to a redirect).\n        self.assertEqual(res.status, 200)\n        # recaptcha is done entirely in JS\n        if code != \"BAD_CAPTCHA\":\n            self.assertTrue(error_list[code] in _force_unicode(res.body))\n\n    def make_qs(self, **kw):\n        kw['dest'] = self.dest\n        return super(PostLoginRegTests, self).make_qs(**kw)\n"
  },
  {
    "path": "r2/r2/tests/functional/controller/prefs/__init__.py",
    "content": ""
  },
  {
    "path": "r2/r2/tests/unit/__init__.py",
    "content": ""
  },
  {
    "path": "r2/r2/tests/unit/config/__init__.py",
    "content": ""
  },
  {
    "path": "r2/r2/tests/unit/config/experiment_test.py",
    "content": "#!/usr/bin/env python\n# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\nimport collections\nimport itertools\nimport math\nfrom mock import MagicMock\n\nfrom pylons import app_globals as g\n\nfrom r2.config.feature.state import FeatureState\nfrom . feature_test import TestFeatureBase, MockAccount\n\n\nclass TestExperiment(TestFeatureBase):\n    _world = None\n    # Append user-supplied error messages to the default output, rather than\n    # overwriting it.\n    longMessage = True\n\n    def setUp(self):\n        super(TestExperiment, self).setUp()\n        # for the purposes of this test, logged in users will be generated as\n        # MockAccount objects and logged out users will be None.  This is in\n        # keeping with how c.user is treated in the default unlogged-in case.\n        self.world.is_user_loggedin = bool\n        self.mock_eventcollector()\n        # test by default with the logged out functionality enabled.\n        self.patch_g(enable_loggedout_experiments=True)\n\n    def get_loggedin_users(self, num_users):\n        users = []\n        for i in xrange(num_users):\n            users.append(MockAccount(name=str(i), _fullname=\"t2_%s\" % str(i)))\n        return users\n\n    @staticmethod\n    def get_loggedout_users(num_users):\n        return [None for _ in xrange(num_users)]\n\n    def test_calculate_bucket(self):\n        \"\"\"Test FeatureState's _calculate_bucket function.\"\"\"\n        feature_state = self.world._make_state(config={})\n\n        # Give ourselves enough users that we can get some reasonable amount of\n        # precision when checking amounts per bucket.\n        NUM_USERS = FeatureState.NUM_BUCKETS * 2000\n        fullnames = []\n        for i in xrange(NUM_USERS):\n            fullnames.append(\"t2_%s\" % str(i))\n\n        counter = collections.Counter()\n        for fullname in fullnames:\n            bucket = feature_state._calculate_bucket(fullname)\n            counter[bucket] += 1\n            # Ensure bucketing is deterministic.\n            self.assertEqual(bucket, feature_state._calculate_bucket(fullname))\n\n        for bucket in xrange(FeatureState.NUM_BUCKETS):\n            # We want an even distribution across buckets.\n            expected = NUM_USERS / FeatureState.NUM_BUCKETS\n            actual = counter[bucket]\n            # Calculating the percentage difference instead of looking at the\n            # raw difference scales better as we change NUM_USERS.\n            percent_equal = float(actual) / expected\n            self.assertAlmostEqual(percent_equal, 1.0, delta=.10,\n                                   msg='bucket: %s' % bucket)\n\n    def test_choose_variant(self):\n        \"\"\"Test FeatureState's _choose_variant function.\"\"\"\n        no_variants = {}\n        three_variants = {\n            'remove_vote_counters': 5,\n            'control_1': 10,\n            'control_2': 5,\n        }\n        three_variants_more = {\n            'remove_vote_counters': 15.6,\n            'control_1': 10,\n            'control_2': 20,\n        }\n\n        counters = collections.defaultdict(collections.Counter)\n        for bucket in xrange(FeatureState.NUM_BUCKETS):\n            variant = FeatureState._choose_variant(bucket, no_variants)\n            if variant:\n                counters['no_variants'][variant] += 1\n            # Ensure variant-choosing is deterministic.\n            self.assertEqual(\n                variant,\n                FeatureState._choose_variant(bucket, no_variants))\n\n            variant = FeatureState._choose_variant(bucket, three_variants)\n            if variant:\n                counters['three_variants'][variant] += 1\n            # Ensure variant-choosing is deterministic.\n            self.assertEqual(\n                variant,\n                FeatureState._choose_variant(bucket, three_variants))\n\n            previous_variant = variant\n            variant = FeatureState._choose_variant(bucket, three_variants_more)\n            if variant:\n                counters['three_variants_more'][variant] += 1\n            # Ensure variant-choosing is deterministic.\n            self.assertEqual(\n                variant,\n                FeatureState._choose_variant(bucket, three_variants_more))\n            # If previously we had a variant, we should still have the same one\n            # now.\n            if previous_variant:\n                self.assertEqual(variant, previous_variant)\n\n        # Only controls chosen in the no-variant case.\n        for variant, percentage in FeatureState.DEFAULT_CONTROL_GROUPS.items():\n            count = counters['no_variants'][variant]\n            # The variant percentage is expressed as a part of 100, so we need\n            # to calculate the fraction-of-1 percentage and scale it\n            # accordingly.\n            scaled_percentage = float(count) / (FeatureState.NUM_BUCKETS / 100)\n            self.assertEqual(scaled_percentage, percentage)\n        for variant, percentage in three_variants.items():\n            count = counters['three_variants'][variant]\n            scaled_percentage = float(count) / (FeatureState.NUM_BUCKETS / 100)\n            self.assertEqual(scaled_percentage, percentage)\n        for variant, percentage in three_variants_more.items():\n            count = counters['three_variants_more'][variant]\n            scaled_percentage = float(count) / (FeatureState.NUM_BUCKETS / 100)\n            self.assertEqual(scaled_percentage, percentage)\n\n        # Test boundary conditions around the maximum percentage allowed for\n        # variants.\n        fifty_fifty = {\n            'control_1': 50,\n            'control_2': 50,\n        }\n        almost_fifty_fifty = {\n            'control_1': 49,\n            'control_2': 51,\n        }\n        for bucket in xrange(FeatureState.NUM_BUCKETS):\n            variant = FeatureState._choose_variant(bucket, fifty_fifty)\n            counters['fifty_fifty'][variant] += 1\n            variant = FeatureState._choose_variant(bucket, almost_fifty_fifty)\n            counters['almost_fifty_fifty'][variant] += 1\n        count = counters['fifty_fifty']['control_1']\n        scaled_percentage = float(count) / (FeatureState.NUM_BUCKETS / 100)\n        self.assertEqual(scaled_percentage, 50)\n\n        count = counters['fifty_fifty']['control_2']\n        scaled_percentage = float(count) / (FeatureState.NUM_BUCKETS / 100)\n        self.assertEqual(scaled_percentage, 50)\n\n        count = counters['almost_fifty_fifty']['control_1']\n        scaled_percentage = float(count) / (FeatureState.NUM_BUCKETS / 100)\n        self.assertEqual(scaled_percentage, 49)\n\n        count = counters['almost_fifty_fifty']['control_2']\n        scaled_percentage = float(count) / (FeatureState.NUM_BUCKETS / 100)\n        self.assertEqual(scaled_percentage, 50)\n\n    def do_experiment_simulation(self, users, loid_generator=None, **cfg):\n        num_users = len(users)\n        if loid_generator is None:\n            loid_generator = iter(self.generate_loid, None)\n\n        feature_state = self.world._make_state(cfg)\n        counter = collections.Counter()\n        for user, loid in zip(users, loid_generator):\n            # on every loop, we have a distinct user and a possibly distinct\n            # loid which will be used multiple times.\n            self.world.current_loid.return_value = loid\n            # is_enabled() and variant() are related but independent code\n            # paths so check both are set together.\n            variant = feature_state.variant(user)\n            if feature_state.is_enabled(user):\n                self.assertIsNotNone(\n                    variant, \"an enabled experiment should have a variant!\")\n                counter[variant] += 1\n\n        # this test will still probabilistically fail, but we can mitigate\n        # the likeliness of that happening\n        error_bar_percent = 100. / math.sqrt(num_users)\n        for variant, percent in cfg['experiment']['variants'].items():\n            # Our actual percentage should be within our expected percent\n            # (expressed as a part of 100 rather than a fraction of 1)\n            # +- 1%.\n            measured_percent = (float(counter[variant]) / num_users) * 100\n            self.assertAlmostEqual(\n                measured_percent, percent, delta=error_bar_percent\n            )\n\n    def assert_no_experiment(self, users, **cfg):\n        feature_state = self.world._make_state(cfg)\n        for user in users:\n            self.assertFalse(feature_state.is_enabled(user))\n\n    def test_loggedin_experiment(self, num_users=2000):\n        \"\"\"Test variant distn for logged in users.\"\"\"\n        self.do_experiment_simulation(\n            self.get_loggedin_users(num_users),\n            experiment={\n                'loggedin': True,\n                'variants': {'larger': 5, 'smaller': 10},\n            }\n        )\n\n    def test_loggedin_experiment_explicit_enable(self, num_users=2000):\n        \"\"\"Test variant distn for logged in users with explicit enable.\"\"\"\n        self.do_experiment_simulation(\n            self.get_loggedin_users(num_users),\n            experiment={\n                'loggedin': True,\n                'variants': {'larger': 5, 'smaller': 10},\n                'enabled': True,\n            },\n        )\n\n    def test_loggedin_experiment_explicit_disable(self, num_users=2000):\n        \"\"\"Test explicit disable for logged in users actually disables.\"\"\"\n        self.assert_no_experiment(\n            self.get_loggedin_users(num_users),\n            experiment={\n                'loggedin': True,\n                'variants': {'larger': 5, 'smaller': 10},\n                'enabled': False,\n            },\n        )\n\n    def test_loggedout_experiment(self, num_users=2000):\n        \"\"\"Test variant distn for logged out users.\"\"\"\n        self.do_experiment_simulation(\n            self.get_loggedout_users(num_users),\n            experiment={\n                \"loggedout\": True,\n                'variants': {'larger': 5, 'smaller': 10},\n            },\n        )\n\n    def test_loggedout_experiment_missing_loids(self, num_users=2000):\n        \"\"\"Ensure logged out experiments with no loids do not bucket.\"\"\"\n        self.assert_no_experiment(\n            self.get_loggedout_users(num_users),\n            loid_generator=itertools.repeat(None),\n            experiment={\n                'loggedout': True,\n                'variants': {'larger': 5, 'smaller': 10},\n            },\n        )\n\n    def test_loggedout_experiment_explicit_enable(self, num_users=2000):\n        \"\"\"Test variant distn for logged out users with explicit enable.\"\"\"\n        self.do_experiment_simulation(\n            self.get_loggedout_users(num_users),\n            experiment={\n                'loggedout': True,\n                'variants': {'larger': 5, 'smaller': 10},\n                'enabled': True,\n            },\n        )\n\n    def test_loggedout_experiment_explicit_disable(self, num_users=2000):\n        \"\"\"Test explicit disable for logged in users actually disables.\"\"\"\n        self.assert_no_experiment(\n            self.get_loggedout_users(num_users),\n            experiment={\n                'loggedout': True,\n                'variants': {'larger': 5, 'smaller': 10},\n                'enabled': False,\n            }\n        )\n\n    def test_loggedout_experiment_global_disable(self, num_users=2000):\n        \"\"\"Test we can disable loid-experiments via configuration.\"\"\"\n        # we already patch this attr in setUp, so we can just explicitly change\n        # it and rely on *that* cleanup\n        g.enable_loggedout_experiments = False\n        self.assert_no_experiment(\n            self.get_loggedout_users(num_users),\n            experiment={\n                'loggedout': True,\n                'variants': {'larger': 5, 'smaller': 10},\n                'enabled': True,\n            }\n        )\n\n    def test_mixed_experiment(self, num_users=2000):\n        \"\"\"Test a combination of loggedin/out users balances variants.\"\"\"\n        self.do_experiment_simulation(\n            (\n                self.get_loggedin_users(num_users / 2) +\n                self.get_loggedout_users(num_users / 2)\n            ),\n            experiment={\n                'loggedin': True,\n                'loggedout': True,\n                'variants': {'larger': 5, 'smaller': 10},\n            }\n        )\n\n    def test_mixed_experiment_disable(self, num_users=2000):\n        \"\"\"Test a combination of loggedin/out users disables properly.\"\"\"\n        self.assert_no_experiment(\n            (\n                self.get_loggedin_users(num_users / 2) +\n                self.get_loggedout_users(num_users / 2)\n            ),\n            experiment={\n                'loggedin': True,\n                'loggedout': True,\n                'variants': {'larger': 5, 'smaller': 10},\n                'enabled': False,\n            }\n        )\n"
  },
  {
    "path": "r2/r2/tests/unit/config/feature_test.py",
    "content": "#!/usr/bin/env python\n# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport collections\nimport random\nimport string\nimport unittest\n\nimport mock\n\nfrom r2.config.feature.state import FeatureState\nfrom r2.config.feature.world import World\nfrom r2.tests import RedditTestCase\n\n\nclass MockAccount(object):\n    def __init__(self, name, _fullname):\n        self.name = name\n        self._fullname = _fullname\n        _, _, _id = _fullname.partition(\"_\")\n        self._id = int(_id, 36)\n\ngary = MockAccount(name='gary', _fullname='t2_beef')\nall_uppercase = MockAccount(name='ALL_UPPERCASE', _fullname='t2_f00d')\n\nclass MockWorld(World):\n    def _make_state(self, config):\n        # Mock by hand because _parse_config is called in __init__, so we\n        # can't instantiate then update.\n        class MockState(FeatureState):\n            def _parse_config(*args, **kwargs):\n                return config\n        return MockState('test_state', self)\n\nclass TestFeatureBase(RedditTestCase):\n    _world = None\n    # Append user-supplied error messages to the default output, rather than\n    # overwriting it.\n    longMessage = True\n\nclass TestFeatureBase(RedditTestCase):\n    # Append user-supplied error messages to the default output, rather than\n    # overwriting it.\n    longMessage = True\n\n    def setUp(self):\n        self.world = MockWorld()\n        self.world.current_user = mock.Mock(return_value='')\n        self.world.current_subreddit = mock.Mock(return_value='')\n        self.world.current_loid = mock.Mock(return_value='')\n\n\nclass TestFeatureBase(RedditTestCase):\n    # Append user-supplied error messages to the default output, rather than\n    # overwriting it.\n    longMessage = True\n\n    def setUp(self):\n        super(TestFeatureBase, self).setUp()\n        self.world = MockWorld()\n        self.world.current_user = mock.Mock(return_value='')\n        self.world.current_subreddit = mock.Mock(return_value='')\n        self.world.current_loid = mock.Mock(return_value='')\n\n    @classmethod\n    def generate_loid(cls):\n        return ''.join(random.sample(string.letters + string.digits, 16))\n\n\nclass TestFeature(TestFeatureBase):\n\n    def _assert_fuzzy_percent_true(self, results, percent):\n        stats = collections.Counter(results)\n        total = sum(stats.values())\n        # _roughly_ `percent` should have been `True`\n        diff = abs((float(stats[True]) / total) - (percent / 100.0))\n        self.assertTrue(diff < 0.1)\n\n    def test_enabled(self):\n        cfg = {'enabled': 'on'}\n        feature_state = self.world._make_state(cfg)\n        self.assertTrue(feature_state.is_enabled())\n        self.assertTrue(feature_state.is_enabled(user=gary))\n\n    def test_disabled(self):\n        cfg = {'enabled': 'off'}\n        feature_state = self.world._make_state(cfg)\n        self.assertFalse(feature_state.is_enabled())\n        self.assertFalse(feature_state.is_enabled(user=gary))\n\n    def test_admin_enabled(self):\n        cfg = {'admin': True}\n        self.world.is_admin = mock.Mock(return_value=True)\n        feature_state = self.world._make_state(cfg)\n        self.assertTrue(feature_state.is_enabled(user=gary))\n\n    def test_admin_disabled(self):\n        cfg = {'admin': True}\n        self.world.is_admin = mock.Mock(return_value=False)\n        feature_state = self.world._make_state(cfg)\n        self.assertFalse(feature_state.is_enabled(user=gary))\n\n    def test_employee_enabled(self):\n        cfg = {'employee': True}\n        self.world.is_employee = mock.Mock(return_value=True)\n        feature_state = self.world._make_state(cfg)\n        self.assertTrue(feature_state.is_enabled(user=gary))\n\n    def test_employee_disabled(self):\n        cfg = {'employee': True}\n        self.world.is_employee = mock.Mock(return_value=False)\n        feature_state = self.world._make_state(cfg)\n        self.assertFalse(feature_state.is_enabled(user=gary))\n\n    def test_beta_enabled(self):\n        cfg = {'beta': True}\n        self.world.user_has_beta_enabled = mock.Mock(return_value=True)\n        feature_state = self.world._make_state(cfg)\n        self.assertTrue(feature_state.is_enabled(user=gary))\n\n    def test_beta_disabled(self):\n        cfg = {'beta': True}\n        self.world.user_has_beta_enabled = mock.Mock(return_value=False)\n        feature_state = self.world._make_state(cfg)\n        self.assertFalse(feature_state.is_enabled(user=gary))\n\n    def test_gold_enabled(self):\n        cfg = {'gold': True}\n        self.world.has_gold = mock.Mock(return_value=True)\n        feature_state = self.world._make_state(cfg)\n        self.assertTrue(feature_state.is_enabled(user=gary))\n\n    def test_gold_disabled(self):\n        cfg = {'gold': True}\n        self.world.has_gold = mock.Mock(return_value=False)\n        feature_state = self.world._make_state(cfg)\n        self.assertFalse(feature_state.is_enabled(user=gary))\n\n    def test_loggedin_enabled(self):\n        cfg = {'loggedin': True}\n        self.world.is_user_loggedin = mock.Mock(return_value=True)\n        feature_state = self.world._make_state(cfg)\n        self.assertTrue(feature_state.is_enabled(user=gary))\n\n    def test_loggedin_disabled(self):\n        cfg = {'loggedin': False}\n        self.world.is_user_loggedin = mock.Mock(return_value=True)\n        feature_state = self.world._make_state(cfg)\n        self.assertFalse(feature_state.is_enabled(user=gary))\n\n    def test_loggedout_enabled(self):\n        cfg = {'loggedout': True}\n        self.world.is_user_loggedin = mock.Mock(return_value=False)\n        feature_state = self.world._make_state(cfg)\n        self.assertTrue(feature_state.is_enabled(user=gary))\n\n    def test_loggedout_disabled(self):\n        cfg = {'loggedout': False}\n        self.world.is_user_loggedin = mock.Mock(return_value=False)\n        feature_state = self.world._make_state(cfg)\n        self.assertFalse(feature_state.is_enabled(user=gary))\n\n    def test_percent_loggedin(self):\n        num_users = 2000\n        users = []\n        for i in xrange(num_users):\n            users.append(MockAccount(name=str(i), _fullname=\"t2_%s\" % str(i)))\n\n        def simulate_percent_loggedin(wanted_percent):\n            cfg = {'percent_loggedin': wanted_percent}\n            self.world.is_user_loggedin = mock.Mock(return_value=True)\n            feature_state = self.world._make_state(cfg)\n            return (feature_state.is_enabled(x) for x in users)\n\n        self.assertFalse(any(simulate_percent_loggedin(0)))\n        self.assertTrue(all(simulate_percent_loggedin(100)))\n        self._assert_fuzzy_percent_true(simulate_percent_loggedin(25), 25)\n        self._assert_fuzzy_percent_true(simulate_percent_loggedin(10), 10)\n        self._assert_fuzzy_percent_true(simulate_percent_loggedin(50), 50)\n        self._assert_fuzzy_percent_true(simulate_percent_loggedin(99), 99)\n\n    def test_percent_loggedout(self):\n        num_users = 2000\n\n        def simulate_percent_loggedout(wanted_percent):\n            cfg = {'percent_loggedout': wanted_percent}\n            for i in xrange(num_users):\n                loid = self.generate_loid()\n                self.world.current_loid = mock.Mock(return_value=loid)\n                self.world.is_user_loggedin = mock.Mock(return_value=False)\n                feature_state = self.world._make_state(cfg)\n                yield feature_state.is_enabled()\n\n        self.assertFalse(any(simulate_percent_loggedout(0)))\n        self.assertTrue(all(simulate_percent_loggedout(100)))\n        self._assert_fuzzy_percent_true(simulate_percent_loggedout(25), 25)\n        self._assert_fuzzy_percent_true(simulate_percent_loggedout(10), 10)\n        self._assert_fuzzy_percent_true(simulate_percent_loggedout(50), 50)\n        self._assert_fuzzy_percent_true(simulate_percent_loggedout(99), 99)\n\n\n    def test_url_enabled(self):\n\n        cfg = {'url': 'test_state'}\n        self.world.url_features = mock.Mock(return_value={'test_state'})\n        feature_state = self.world._make_state(cfg)\n        self.assertTrue(feature_state.is_enabled())\n        self.assertTrue(feature_state.is_enabled(user=gary))\n\n        cfg = {'url': 'test_state'}\n        self.world.url_features = mock.Mock(return_value={'x', 'test_state'})\n        feature_state = self.world._make_state(cfg)\n        self.assertTrue(feature_state.is_enabled())\n        self.assertTrue(feature_state.is_enabled(user=gary))\n\n        cfg = {'url': {'test_state_a': 'a', 'test_state_b': 'b'}}\n        self.world.url_features = mock.Mock(return_value={'x', 'test_state_b'})\n        feature_state = self.world._make_state(cfg)\n        self.assertTrue(feature_state.is_enabled())\n        self.assertEqual(feature_state.variant(user=gary), 'b')\n\n    def test_url_disabled(self):\n\n        cfg = {'url': 'test_state'}\n        self.world.url_features = mock.Mock(return_value={})\n        feature_state = self.world._make_state(cfg)\n        self.assertFalse(feature_state.is_enabled())\n        self.assertFalse(feature_state.is_enabled(user=gary))\n\n        cfg = {'url': 'test_state'}\n        self.world.url_features = mock.Mock(return_value={'x'})\n        feature_state = self.world._make_state(cfg)\n        self.assertFalse(feature_state.is_enabled())\n        self.assertFalse(feature_state.is_enabled(user=gary))\n\n        cfg = {'url': {'test_state_a': 'a', 'test_state_b': 'b'}}\n        self.world.url_features = mock.Mock(return_value={'x'})\n        feature_state = self.world._make_state(cfg)\n        self.assertFalse(feature_state.is_enabled())\n\n        cfg = {'url': {'test_state_c1': 'control_1', 'test_state_c2': 'control_2'}}\n        self.world.url_features = mock.Mock(return_value={'x', 'test_state_c2'})\n        feature_state = self.world._make_state(cfg)\n        self.assertFalse(feature_state.is_enabled())\n\n    def test_user_in(self):\n        cfg = {'users': ['Gary']}\n        feature_state = self.world._make_state(cfg)\n        self.assertTrue(feature_state.is_enabled(user=gary))\n\n        cfg = {'users': ['ALL_UPPERCASE']}\n        feature_state = self.world._make_state(cfg)\n        self.assertTrue(feature_state.is_enabled(user=all_uppercase))\n\n        cfg = {'users': ['dave', 'gary']}\n        feature_state = self.world._make_state(cfg)\n        self.assertTrue(feature_state.is_enabled(user=gary))\n\n    def test_user_not_in(self):\n        cfg = {'users': ['']}\n        featurestate = self.world._make_state(cfg)\n        self.assertFalse(featurestate.is_enabled(user=gary))\n\n        cfg = {'users': ['dave', 'joe']}\n        featurestate = self.world._make_state(cfg)\n        self.assertFalse(featurestate.is_enabled(user=gary))\n\n    def test_subreddit_in(self):\n        cfg = {'subreddits': ['WTF']}\n        feature_state = self.world._make_state(cfg)\n        self.assertTrue(feature_state.is_enabled(subreddit='wtf'))\n\n        cfg = {'subreddits': ['wtf']}\n        feature_state = self.world._make_state(cfg)\n        self.assertTrue(feature_state.is_enabled(subreddit='WTF'))\n\n        cfg = {'subreddits': ['aww', 'wtf']}\n        feature_state = self.world._make_state(cfg)\n        self.assertTrue(feature_state.is_enabled(subreddit='wtf'))\n\n    def test_subreddit_not_in(self):\n        cfg = {'subreddits': []}\n        feature_state = self.world._make_state(cfg)\n        self.assertFalse(feature_state.is_enabled(subreddit='wtf'))\n\n        cfg = {'subreddits': ['aww', 'wtfoobar']}\n        feature_state = self.world._make_state(cfg)\n        self.assertFalse(feature_state.is_enabled(subreddit='wtf'))\n\n    def test_subdomain_in(self):\n        cfg = {'subdomains': ['BETA']}\n        feature_state = self.world._make_state(cfg)\n        self.assertTrue(feature_state.is_enabled(subdomain='beta'))\n\n        cfg = {'subdomains': ['beta']}\n        feature_state = self.world._make_state(cfg)\n        self.assertTrue(feature_state.is_enabled(subdomain='BETA'))\n\n        cfg = {'subdomains': ['www', 'beta']}\n        feature_state = self.world._make_state(cfg)\n        self.assertTrue(feature_state.is_enabled(subdomain='beta'))\n\n    def test_subdomain_not_in(self):\n        cfg = {'subdomains': []}\n        feature_state = self.world._make_state(cfg)\n        self.assertFalse(feature_state.is_enabled(subdomain='beta'))\n        self.assertFalse(feature_state.is_enabled(subdomain=''))\n\n        cfg = {'subdomains': ['www', 'betanauts']}\n        feature_state = self.world._make_state(cfg)\n        self.assertFalse(feature_state.is_enabled(subdomain='beta'))\n\n    def test_multiple(self):\n        # is_admin, globally off should still be False\n        cfg = {'enabled': 'off', 'admin': True}\n        self.world.is_admin = mock.Mock(return_value=True)\n        featurestate = self.world._make_state(cfg)\n        self.assertFalse(featurestate.is_enabled(user=gary))\n\n        # globally on but not admin should still be True\n        cfg = {'enabled': 'on', 'admin': True}\n        self.world.is_admin = mock.Mock(return_value=False)\n        featurestate = self.world._make_state(cfg)\n        self.assertTrue(featurestate.is_enabled(user=gary))\n        self.assertTrue(featurestate.is_enabled())\n\n        # no URL but admin should still be True\n        cfg = {'url': 'test_featurestate', 'admin': True}\n        self.world.url_features = mock.Mock(return_value={})\n        self.world.is_admin = mock.Mock(return_value=True)\n        featurestate = self.world._make_state(cfg)\n        self.assertTrue(featurestate.is_enabled(user=gary))\n"
  },
  {
    "path": "r2/r2/tests/unit/lib/__init__.py",
    "content": ""
  },
  {
    "path": "r2/r2/tests/unit/lib/authorize/__init__.py",
    "content": ""
  },
  {
    "path": "r2/r2/tests/unit/lib/authorize/test_api.py",
    "content": "#!/usr/bin/env python\n# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom mock import MagicMock, Mock, patch\nfrom unittest import TestCase\n\nfrom r2.lib.authorize.api import (TRANSACTION_NOT_FOUND,\n                                  TRANSACTION_ERROR,\n                                  TRANSACTION_DUPLICATE,\n                                  AuthorizationHoldNotFound,\n                                  AuthorizeNetException,\n                                  DuplicateTransactionError,\n                                  TransactionError,\n                                  create_customer_profile,\n                                  get_customer_profile,\n                                  create_payment_profile,\n                                  update_payment_profile,\n                                  delete_payment_profile,\n                                  create_authorization_hold,\n                                  capture_authorization_hold,\n                                  void_authorization_hold,\n                                  refund_transaction)\nfrom r2.tests import RedditTestCase\n\n\nclass AuthorizeNetExceptionTest(RedditTestCase):\n\n    def test_exception_message(self):\n        from r2.lib.authorize.api import AuthorizeNetException\n        card_number = \"<cardNumber>1111222233334444</cardNumber>\"\n        expected = \"<cardNumber>...4444</cardNumber>\"\n        full_msg = \"Wrong Card %s was given\"\n\n        exp = AuthorizeNetException(full_msg % (card_number))\n\n        self.assertNotEqual(str(exp), (full_msg % card_number))\n        self.assertEqual(str(exp), (full_msg % expected))\n\nclass SimpleXMLObjectTest(RedditTestCase):\n\n    def setUp(self):\n        from r2.lib.authorize.api import SimpleXMLObject\n        self.basic_object = SimpleXMLObject(name=\"Test\",\n                                           test=\"123\",\n                                           )\n\n    def test_to_xml(self):\n        self.assertEqual(self.basic_object.toXML(),\n                         \"<test>123</test><name>Test</name>\",\n                         \"Unexpected XML produced\")\n\n    def test_simple_tag(self):\n        from r2.lib.authorize.api import SimpleXMLObject\n        xml_output = SimpleXMLObject.simple_tag(\"cat\", \"Jini\", breed=\"calico\",\n                                                               demenor=\"evil\",\n                                                               )\n        self.assertEqual(xml_output,\n                         '<cat breed=\"calico\" demenor=\"evil\">Jini</cat>')\n\n    def test_from_xml(self):\n        from r2.lib.authorize.api import SimpleXMLObject\n        from BeautifulSoup import BeautifulStoneSoup\n        class TestXML(SimpleXMLObject):\n            _keys = [\"color\", \"breed\"]\n\n        parsed = BeautifulStoneSoup(\"<dog>\" +\n                                    \"<color>black</color>\" +\n                                    \"<breed>mixed</breed>\" +\n                                    \"<something>else</something>\" +\n                                    \"</dog>\")\n        constructed = TestXML.fromXML(parsed)\n        expected = SimpleXMLObject(color=\"black\",\n                                   breed=\"mixed\",\n                                   )\n        self.assertEqual(constructed.toXML(), expected.toXML(),\n                         \"Constructed does not match expected\")\n\n    def test_address(self):\n        from r2.lib.authorize import Address\n        address = Address(firstName=\"Bob\",\n                          lastName=\"Smith\",\n                          company=\"Reddit Inc.\",\n                          address=\"123 Main St.\",\n                          city=\"San Francisco\",\n                          state=\"California\",\n                          zip=\"12345\",\n                          country=\"USA\",\n                          phoneNumber=\"415-555-1234\",\n                          faxNumber=\"415-555-4321\",\n                          customerPaymentProfileId=\"1234567890\",\n                          customerAddressId=\"2233\",\n                          )\n        expected = (\"<firstName>Bob</firstName>\" +\n                   \"<lastName>Smith</lastName>\" +\n                   \"<company>Reddit Inc.</company>\" +\n                   \"<address>123 Main St.</address>\" +\n                   \"<city>San Francisco</city>\" +\n                   \"<state>California</state>\" +\n                   \"<zip>12345</zip>\" +\n                   \"<country>USA</country>\" +\n                   \"<phoneNumber>415-555-1234</phoneNumber>\" +\n                   \"<faxNumber>415-555-4321</faxNumber>\" +\n                   \"<customerPaymentProfileId>1234567890</customerPaymentProfileId>\" +\n                   \"<customerAddressId>2233</customerAddressId>\")\n\n        self.assertEqual(address.toXML(), expected)\n\n    def test_credit_card(self):\n        from r2.lib.authorize import CreditCard\n        card = CreditCard(cardNumber=\"1111222233334444\",\n                          expirationDate=\"11/22/33\",\n                          cardCode=\"123\"\n                          )\n        expected = (\"<cardNumber>1111222233334444</cardNumber>\" +\n                    \"<expirationDate>11/22/33</expirationDate>\" +\n                    \"<cardCode>123</cardCode>\")\n        self.assertEqual(card.toXML(), expected)\n\n    def test_payment_profile(self):\n        from r2.lib.authorize.api import PaymentProfile\n        profile = PaymentProfile(billTo=\"Joe\",\n                                 customerPaymentProfileId=\"222\",\n                                 card=\"1111222233334444\",\n                                 validationMode=\"42\",\n                                 )\n        expected = (\"<billTo>Joe</billTo>\" +\n                    \"<payment>\" +\n                        \"<creditCard>1111222233334444</creditCard>\" +\n                    \"</payment>\" +\n                    \"<customerPaymentProfileId>222</customerPaymentProfileId>\" +\n                    \"<validationMode>42</validationMode>\")\n        self.assertEqual(profile.toXML(), expected)\n\n    def test_transaction(self):\n        from r2.lib.authorize.api import Transaction\n        transaction = Transaction(amount=\"42.42\",\n                                  customerProfileId=\"112233\",\n                                  customerPaymentProfileId=\"1111\",\n                                  transId=\"2222\",\n                                  order=\"42\",\n                                  )\n\n        expected = (\"<transaction>\" +\n                        \"<amount>42.42</amount>\" +\n                        \"<customerProfileId>112233</customerProfileId>\" +\n                        \"<customerPaymentProfileId>1111</customerPaymentProfileId>\" +\n                        \"<transId>2222</transId>\" +\n                        \"<order>42</order>\" +\n                    \"</transaction>\")\n        self.assertEqual(transaction.toXML(), expected)\n\n\nclass ApiFunctionTest(TestCase):\n\n    def setUp(self):\n        # Set up a few commonly used variables\n        self.customer_id = 1\n        self.payment_profile_id = 1000\n        self.amount = 100\n        self.transaction_id = 99\n\n    @patch('r2.lib.authorize.api.CreateCustomerProfileRequest')\n    @patch('r2.lib.authorize.api.Profile')\n    def test_create_customer_profile(self, Profile, CreateRequest):\n        merchant_customer_id = 99\n        description = 'some description'\n\n        # Set up profile mock\n        profile = Mock()\n        Profile.return_value = profile\n        # Set up request mock\n        _request = MagicMock()\n        _request.make_request.return_value = self.customer_id\n        CreateRequest.return_value = _request\n\n        # Scenario: successful call\n        return_value = create_customer_profile(merchant_customer_id,\n                                               description)\n        Profile.assert_called_once_with(description=description,\n                                        merchantCustomerId=merchant_customer_id,\n                                        paymentProfiles=None,\n                                        customerProfileId=None)\n        CreateRequest.assert_called_once_with(profile=profile)\n        self.assertTrue(_request.make_request.called)\n        self.assertEqual(return_value, self.customer_id)\n\n        # Scenario: call raises AuthorizeNetException\n        _request.make_request.side_effect = AuthorizeNetException('')\n        return_value = create_customer_profile(merchant_customer_id,\n                                               description)\n        self.assertEqual(return_value, None)\n\n    @patch('r2.lib.authorize.api.GetCustomerProfileRequest')\n    def test_get_customer_profile(self, GetRequest):\n        profile_mock = Mock()\n        _request = Mock()\n        _request.make_request.return_value = profile_mock\n        GetRequest.return_value = _request\n\n        # Scenario: call is successful\n        return_value = get_customer_profile(self.customer_id)\n        GetRequest.assert_called_once_with(customerProfileId=self.customer_id)\n        self.assertTrue(_request.make_request.called)\n        self.assertEqual(return_value, profile_mock)\n\n        # Scenario: call raises AuthorizeNetException\n        _request.make_request.side_effect = AuthorizeNetException('')\n        return_value = get_customer_profile(self.customer_id)\n        self.assertEqual(return_value, None)\n\n    @patch('r2.lib.authorize.api.CreateCustomerPaymentProfileRequest')\n    @patch('r2.lib.authorize.api.PaymentProfile')\n    def test_create_payment_profile(self, PaymentProfile, CreateRequest):\n        payment_profile = Mock()\n        PaymentProfile.return_value = payment_profile\n        _request = Mock()\n        _request.make_request.return_value = self.payment_profile_id\n        CreateRequest.return_value = _request\n\n        # Scenario: call is successful, no validationMode is passed\n        return_value = create_payment_profile(self.customer_id, 'address',\n                                              'credit_card')\n        PaymentProfile.assert_called_once_with(billTo='address',\n                                               card='credit_card')\n        CreateRequest.assert_called_once_with(customerProfileId=self.customer_id,\n                                              paymentProfile=payment_profile,\n                                              validationMode=None)\n        self.assertTrue(_request.make_request.called)\n        self.assertEqual(return_value, self.payment_profile_id)\n\n        # Scenario: call is successful, validationMode is passed\n        create_payment_profile(self.customer_id, 'address', 'credit_card',\n                               'liveMode')\n        CreateRequest.assert_called_with(customerProfileId=self.customer_id,\n                                         paymentProfile=payment_profile,\n                                         validationMode='liveMode')\n\n        # Scenario: call raises AuthorizeNetException\n        _request.make_request.side_effect = AuthorizeNetException('')\n        self.assertRaises(AuthorizeNetException, create_payment_profile,\n                          self.customer_id, 'address', 'credit_card')\n\n    @patch('r2.lib.authorize.api.UpdateCustomerPaymentProfileRequest')\n    @patch('r2.lib.authorize.api.PaymentProfile')\n    def test_update_payment_profile(self, PaymentProfile, UpdateRequest):\n        _request = Mock()\n        _request.make_request.return_value = self.payment_profile_id\n        UpdateRequest.return_value = _request\n\n        # Scenario: call is successful\n        return_value = update_payment_profile(self.customer_id,\n                                              self.payment_profile_id,\n                                              'address', 1234)\n        self.assertTrue(UpdateRequest.called)\n        self.assertTrue(_request.make_request.called)\n        self.assertEqual(return_value, self.payment_profile_id)\n\n        # Scenario: call raises AuthorizeNetException\n        _request.make_request.side_effect = AuthorizeNetException('')\n        self.assertRaises(AuthorizeNetException, update_payment_profile,\n                          self.customer_id, self.payment_profile_id, 'address',\n                          1234)\n\n    @patch('r2.lib.authorize.api.DeleteCustomerPaymentProfileRequest')\n    def test_delete_payment_profile(self, DeleteRequest):\n        _request = Mock()\n        DeleteRequest.return_value = _request\n\n        # Scenario: call is successful\n        return_value = delete_payment_profile(self.customer_id,\n                                              self.payment_profile_id)\n        DeleteRequest.assert_called_once_with(customerProfileId=self.customer_id,\n                                              customerPaymentProfileId=self.payment_profile_id)\n        self.assertTrue(return_value)\n\n        # Scenario: call raises AuthorizeNetException\n        _request.make_request.side_effect = AuthorizeNetException('')\n        return_value = delete_payment_profile(self.customer_id,\n                                              self.payment_profile_id)\n        self.assertFalse(return_value)\n\n\n    @patch('r2.lib.authorize.api.CreateCustomerProfileTransactionRequest')\n    def test_create_authorization_hold(self, CreateRequest):\n        _response = Mock()\n        _response.trans_id = self.transaction_id\n        _request = Mock()\n        _request.make_request.return_value = (True, _response)\n        CreateRequest.return_value = _request\n\n        # Scenario: call is successful; pass customer_ip\n        return_value = create_authorization_hold(self.customer_id,\n                                                 self.payment_profile_id,\n                                                 self.amount, 12345,\n                                                 '127.0.0.1')\n        self.assertTrue(CreateRequest.called)\n        args, kwargs = CreateRequest.call_args\n        self.assertEqual(kwargs['extraOptions'], {'x_customer_ip': '127.0.0.1'})\n        self.assertEqual(return_value, self.transaction_id)\n\n        # Scenario: call raises transaction_error\n        _request.make_request.return_value = (False, _response)\n        self.assertRaises(TransactionError, create_authorization_hold,\n                          self.customer_id, self.payment_profile_id,\n                          self.amount, 12345, '127.0.0.1')\n\n        # Scenario: call returns duplicate errors\n        _response.response_code = TRANSACTION_ERROR\n        _response.response_reason_code = TRANSACTION_DUPLICATE\n        _request.make_request.return_value = (True, _response)\n        self.assertRaises(DuplicateTransactionError, create_authorization_hold,\n                          self.customer_id, self.payment_profile_id,\n                          self.amount, 12345)\n\n    @patch('r2.lib.authorize.api.CreateCustomerProfileTransactionRequest')\n    def test_capture_authorization_hold(self, CreateRequest):\n        _response = Mock()\n        _request = Mock()\n        _request.make_request.return_value = (True, _response)\n        CreateRequest.return_value = _request\n\n        # Scenario: call is successful\n        return_value = capture_authorization_hold(self.customer_id,\n                                                  self.payment_profile_id,\n                                                  self.amount,\n                                                  self.transaction_id)\n        self.assertTrue(CreateRequest.called)\n        self.assertTrue(_request.make_request.called)\n        self.assertEqual(return_value, None)\n\n        # Scenario: call raises TransactionError\n        _request.make_request.return_value = (False, _response)\n        self.assertRaises(TransactionError, capture_authorization_hold,\n                          self.customer_id, self.payment_profile_id,\n                          self.amount, self.transaction_id)\n\n        # Scenario: _request call returns not found error\n        _response.get.return_value = TRANSACTION_NOT_FOUND\n        self.assertRaises(AuthorizationHoldNotFound, capture_authorization_hold,\n                          self.customer_id, self.payment_profile_id,\n                          self.amount, self.transaction_id)\n\n    @patch('r2.lib.authorize.api.CreateCustomerProfileTransactionRequest')\n    def test_void_authorization_hold(self, CreateRequest):\n        _response = Mock()\n        _response.trans_id = self.transaction_id\n        _request = Mock()\n        _request.make_request.return_value = (True, _response)\n        CreateRequest.return_value = _request\n\n        # Scenario: call is successful\n        return_value = void_authorization_hold(self.customer_id,\n                                               self.payment_profile_id,\n                                               self.transaction_id)\n        self.assertTrue(CreateRequest.called)\n        self.assertTrue(_request.make_request.called)\n        self.assertEqual(return_value, self.transaction_id)\n\n        # Scenario: call raises TransactionError\n        _request.make_request.return_value = (False, _response)\n        self.assertRaises(TransactionError, void_authorization_hold,\n                          self.customer_id, self.payment_profile_id,\n                          self.transaction_id)\n\n    @patch('r2.lib.authorize.api.CreateCustomerProfileTransactionRequest')\n    def test_refund_transaction(self, CreateRequest):\n        _request = Mock()\n        _request.make_request.return_value = (True, None)\n        CreateRequest.return_value = _request\n\n        # Scenario: call is successful\n        refund_transaction(self.customer_id, self.payment_profile_id,\n                           self.amount, self.transaction_id)\n        self.assertTrue(_request.make_request.called)\n\n        # Scenario: call raises TransactionError\n        _request.make_request.return_value = (False, Mock())\n        self.assertRaises(TransactionError, refund_transaction,\n                          self.customer_id, self.payment_profile_id,\n                          self.amount, self.transaction_id)\n\n"
  },
  {
    "path": "r2/r2/tests/unit/lib/authorize/test_interaction.py",
    "content": "#!/usr/bin/env python\n# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom mock import MagicMock, Mock, patch\nfrom unittest import TestCase\n\nfrom r2.lib.authorize.api import (AuthorizationHoldNotFound,\n                                  DuplicateTransactionError,\n                                  TRANSACTION_NOT_FOUND,\n                                  TransactionError,)\nfrom r2.lib.authorize.interaction import (get_or_create_customer_profile,\n                                          add_payment_method,\n                                          update_payment_method,\n                                          delete_payment_method,\n                                          add_or_update_payment_method,\n                                          auth_freebie_transaction,\n                                          auth_transaction,\n                                          charge_transaction,\n                                          void_transaction,\n                                          refund_transaction,)\nfrom r2.lib.db.thing import NotFound\nfrom r2.models import Account, Link\nfrom r2.tests import RedditTestCase\n\n\nclass InteractionTest(RedditTestCase):\n\n    def setUp(self):\n        self.user = Mock(spec=Account)\n        self.user._id = 1\n        self.user.name = 'name'\n        self.user._fullname = 'fullname'\n\n    @patch('r2.lib.authorize.interaction.api.get_customer_profile')\n    @patch('r2.lib.authorize.interaction.api.create_customer_profile')\n    def test_get_or_create_customer_profile(self, create_customer_profile,\n                                            get_customer_profile):\n        \"\"\"Test get_or_create_customer_profile\"\"\"\n        create_customer_profile.return_value = 123\n\n        profile = MagicMock()\n        profile.merchantCustomerId = self.user._fullname\n        get_customer_profile.return_value = profile\n\n        get_or_create_customer_profile(self.user)\n\n        # Assert that on the first pass, a customer is created and retrieved\n        self.assertEqual(create_customer_profile.call_count, 1)\n        self.assertEqual(get_customer_profile.call_count, 1)\n\n        with patch('r2.lib.authorize.interaction.CustomerID.get_id') as get_id:\n            get_id.return_value = create_customer_profile.return_value\n\n            get_or_create_customer_profile(self.user)\n\n            # Assert that on the second pass, a customer is only retrieved\n            self.assertEqual(create_customer_profile.call_count, 1)\n            self.assertEqual(get_customer_profile.call_count, 2)\n\n    @patch('r2.lib.authorize.interaction.PayID.add')\n    @patch('r2.lib.authorize.interaction.api.create_payment_profile')\n    @patch('r2.lib.authorize.interaction.CustomerID.get_id')\n    def test_add_payment_method(self, get_id, create_payment_profile, add):\n        \"\"\"Test add_payment_method\"\"\"\n        payment_method_id = 999\n        create_payment_profile.return_value = payment_method_id\n\n        return_value = add_payment_method(self.user, Mock(), Mock())\n\n        # Assert that get_id is called once\n        get_id.assert_called_once_with(self.user._id)\n        # Assert that create_payment_profile is called\n        self.assertTrue(create_payment_profile.called)\n        # Assert that add is called\n        self.assertTrue(add.called)\n        # Assert that function returns payment_method_id value\n        self.assertEqual(return_value, payment_method_id)\n\n    @patch('r2.lib.authorize.interaction.api.update_payment_profile')\n    def test_update_payment_method(self, update_payment_profile):\n        \"\"\"Test update_payment_method\"\"\"\n        update_payment_method(self.user, Mock(), Mock(), Mock())\n\n        # Assert that update_payment_profile was called once\n        self.assertEqual(update_payment_profile.call_count, 1)\n\n    @patch('r2.lib.authorize.interaction.PayID.delete')\n    @patch('r2.lib.authorize.interaction.api.delete_payment_profile')\n    def test_delete_payment_method(self, delete_payment_profile, delete):\n        \"\"\"Test delete_payment_method\"\"\"\n        delete_payment_method(self.user, Mock())\n\n        # Assert that delete_payment_profile was called once\n        self.assertEqual(delete_payment_profile.call_count, 1)\n        # Assert that delete was called once\n        self.assertEqual(delete.call_count, 1)\n\n        # Reset delete mock and run test again\n        delete.reset_mock()\n        delete_payment_method(self.user, Mock())\n\n        # Assert that delete_payment_profile was called twice\n        self.assertEqual(delete_payment_profile.call_count, 2)\n        # Assert that delete was not called again\n        self.assertEqual(delete.call_count, 1)\n\n    @patch('r2.lib.authorize.interaction.add_payment_method')\n    @patch('r2.lib.authorize.interaction.update_payment_method')\n    def test_add_or_update_payment_method(self, update_payment_method,\n                                          add_payment_method):\n        \"\"\"Test add_or_update_payment_method\"\"\"\n        # If pay_id is None, assert that payment method is only added\n        add_or_update_payment_method(self.user, Mock(), Mock())\n        self.assertTrue(add_payment_method.called)\n        self.assertFalse(update_payment_method.called)\n\n        # Reset mocks\n        add_payment_method.reset_mock()\n        update_payment_method.reset_mock()\n\n        # If pay_id is set, assert that payment method is only updated\n        add_or_update_payment_method(self.user, Mock(), Mock(), 999)\n        self.assertFalse(add_payment_method.called)\n        self.assertTrue(update_payment_method.called)\n\n    @patch('r2.lib.authorize.interaction.Bid._new')\n    def test_auth_freebie_transaction(self, _new):\n        \"\"\"Test auth_freebie_transaction\"\"\"\n        link = Mock(spec=Link)\n        link._id = 99\n        amount = 100\n        campaign_id = 99\n\n        # Can't test that NotFound is thrown since the exception is handled,\n        # so assert that _new is called\n        return_value = auth_freebie_transaction(amount, self.user, link,\n                                                campaign_id)\n        self.assertTrue(_new.called)\n        # Assert that return value of auth_freebie_transaction is correct\n        self.assertEqual(return_value, (-link._id, ''))\n\n        # When a Bid is found, assert that auth is called\n        with patch('r2.lib.authorize.interaction.Bid.one') as one:\n            one_mock = MagicMock()\n            one.return_value = one_mock\n            auth_freebie_transaction(amount, self.user, link, campaign_id)\n            self.assertTrue(one_mock.auth.called)\n\n    @patch('r2.lib.authorize.interaction.request')\n    @patch('r2.lib.authorize.interaction.api.create_authorization_hold')\n    @patch('r2.lib.authorize.interaction.CustomerID.get_id')\n    @patch('r2.lib.authorize.interaction.PayID.get_ids')\n    def test_auth_transaction(self, get_ids, get_id, create_authorization_hold,\n                              request):\n        \"\"\"Test auth_transaction\"\"\"\n        link = Mock(spec=Link)\n        link._id = 99\n        amount = 100\n        payment_method_id = 50\n        campaign_id = 99\n        request.ip = '127.0.0.1'\n        transaction_id = 123\n\n        # If get_ids is empty, assert that the proper value is returned\n        get_ids.return_value = []\n        return_value = auth_transaction(amount, self.user, payment_method_id,\n                                        link, campaign_id)\n        self.assertEqual(return_value, (None, 'invalid payment method'))\n\n        # Make get_ids return a valid payment_method_id\n        get_ids.return_value.append(payment_method_id)\n        # Assign arbitrary CustomerID, which comes from Authorize\n        get_id.return_value = 1000\n        create_authorization_hold.return_value = transaction_id\n\n        # Scenario: create_authorization_hold raises DuplicateTransactionError\n        duplicate_transaction_error = DuplicateTransactionError(transaction_id=transaction_id)\n        create_authorization_hold.side_effect = duplicate_transaction_error\n        # Why does patch.multiple return an AttributeError?\n        with patch('r2.lib.authorize.interaction.Bid.one') as one:\n            one.side_effect = NotFound()\n            return_value = auth_transaction(amount, self.user,\n                                            payment_method_id, link,\n                                            campaign_id)\n            # If create_authorization_hold raises NotFound, assert return value\n            self.assertEqual(return_value, (transaction_id, None))\n\n        # Scenario: create_authorization_hold successfully returns\n        with patch('r2.lib.authorize.interaction.Bid._new') as _new:\n            return_value = auth_transaction(amount, self.user,\n                                            payment_method_id, link,\n                                            campaign_id)\n            self.assertTrue(_new.called)\n            # If create_authorization_hold works, assert return value\n            self.assertEqual(return_value, (transaction_id, None))\n\n        # Scenario: creat_authorization_hold raises TransactionError\n        create_authorization_hold.side_effect = TransactionError('')\n        return_value = auth_transaction(amount, self.user, payment_method_id,\n                                        link, campaign_id)\n        # If create_authorization_hold raises TransactionError, assert return\n        self.assertEqual(return_value[0], None)\n\n    @patch('r2.lib.authorize.interaction.api.capture_authorization_hold')\n    @patch('r2.lib.authorize.interaction.Bid.one')\n    def test_charge_transaction(self, one, capture_authorization_hold):\n        transaction_id = 123\n        campaign_id = 99\n        bid = Mock()\n\n        one.return_value = bid\n\n        # Scenario: bid.is_charged() return True\n        bid.is_charged.return_value = True\n        return_value = charge_transaction(self.user, transaction_id,\n                                          campaign_id)\n        self.assertEqual(return_value, (True, None))\n\n        # Scenario: transaction_id < 0\n        bid.is_charged.return_value = False\n        return_value = charge_transaction(self.user, -transaction_id,\n                                          campaign_id)\n        self.assertTrue(bid.charged.called)\n        self.assertEqual(return_value, (True, None))\n\n        # Scenario: capture_authorization_hold is successful\n        return_value = charge_transaction(self.user, transaction_id,\n                                          campaign_id)\n        self.assertTrue(bid.charged.called)\n        self.assertEqual(return_value, (True, None))\n\n        # Scenario: capture_authorization_hold raises AuthorizationHoldNotFound\n        capture_authorization_hold.side_effect = AuthorizationHoldNotFound('')\n        return_value = charge_transaction(self.user, transaction_id,\n                                          campaign_id)\n        self.assertTrue(bid.void.called)\n        self.assertEqual(return_value, (False, TRANSACTION_NOT_FOUND))\n\n        # Scenario: capture_authorization_hold raises TransactionError\n        capture_authorization_hold.side_effect = TransactionError('')\n        return_value = charge_transaction(self.user, transaction_id,\n                                          campaign_id)\n        self.assertEqual(return_value[0], False)\n\n    @patch('r2.lib.authorize.interaction.Bid.one')\n    def test_void_transaction(self, one):\n        bid = Mock()\n        bid.pay_id = 111\n        transaction_id = 123\n        campaign_id = 99\n\n        one.return_value = bid\n\n        # Scenario: transaction_id < 0\n        return_value = void_transaction(self.user, -transaction_id, campaign_id)\n        self.assertTrue(bid.void.called)\n        self.assertEqual(return_value, (True, None))\n\n        with patch('r2.lib.authorize.interaction.api.void_authorization_hold') as void:\n            # Scenario: void_authorization_hold is successful\n            return_value = void_transaction(self.user, transaction_id,\n                                            campaign_id)\n            self.assertTrue(bid.void.called)\n            self.assertEqual(return_value, (True, None))\n\n            # Scenario: void_authorization_hold raises TransactionError\n            void.side_effect = TransactionError('')\n            return_value = void_transaction(self.user, transaction_id,\n                                            campaign_id)\n            self.assertEqual(return_value[0], False)\n\n    @patch('r2.lib.authorize.interaction.Bid.one')\n    def test_refund_transaction(self, one):\n        bid = Mock()\n        bid.pay_id = 111\n        transaction_id = 123\n        campaign_id = 99\n        amount = 100\n\n        one.return_value = bid\n\n        # Scenario: transaction_id < 0\n        return_value = refund_transaction(self.user, -transaction_id,\n                                          campaign_id, amount)\n        bid.refund.assert_called_once_with(amount)\n        self.assertEqual(return_value, (True, None))\n\n        with patch('r2.lib.authorize.interaction.api.refund_transaction') as refund:\n            # Scenario: refund_transaction is successful\n            bid.reset_mock()\n            return_value = refund_transaction(self.user, transaction_id,\n                                              campaign_id, amount)\n            bid.refund.assert_called_once_with(amount)\n            self.assertEqual(return_value, (True, None))\n\n            # Scenario: refund_transaction raises TransactionError\n            bid.reset_mock()\n            refund.side_effect = TransactionError('')\n            return_value = refund_transaction(self.user, transaction_id,\n                                              campaign_id, amount)\n            self.assertEqual(return_value[0], False)\n"
  },
  {
    "path": "r2/r2/tests/unit/lib/configparse_test.py",
    "content": "#!/usr/bin/env python\n# coding=utf-8\n# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\nimport datetime\nimport unittest\n\nfrom r2.lib.configparse import ConfigValue\n\n\nclass TestConfigValue(unittest.TestCase):\n\n    def test_str(self):\n        self.assertEquals('x', ConfigValue.str('x'))\n\n    def test_int(self):\n        self.assertEquals(3, ConfigValue.int('3'))\n        self.assertEquals(-3, ConfigValue.int('-3'))\n        with self.assertRaises(ValueError):\n            ConfigValue.int('asdf')\n\n    def test_float(self):\n        self.assertEquals(3.0, ConfigValue.float('3'))\n        self.assertEquals(-3.0, ConfigValue.float('-3'))\n        with self.assertRaises(ValueError):\n            ConfigValue.float('asdf')\n\n    def test_bool(self):\n        self.assertEquals(True, ConfigValue.bool('TrUe'))\n        self.assertEquals(False, ConfigValue.bool('fAlSe'))\n        with self.assertRaises(ValueError):\n            ConfigValue.bool('asdf')\n\n    def test_tuple(self):\n        self.assertEquals((), ConfigValue.tuple(''))\n        self.assertEquals(('a', 'b'), ConfigValue.tuple('a, b'))\n\n    def test_set(self):\n        self.assertEquals(set([]), ConfigValue.set(''))\n        self.assertEquals(set(['a', 'b']), ConfigValue.set('a, b'))\n\n    def test_set_of(self):\n        self.assertEquals(set([]), ConfigValue.set_of(str)(''))\n        self.assertEquals(set(['a', 'b']), ConfigValue.set_of(str)('a, b, b'))\n        self.assertEquals(set(['a', 'b']),\n                          ConfigValue.set_of(str, delim=':')('b : a : b'))\n\n    def test_tuple_of(self):\n        self.assertEquals((), ConfigValue.tuple_of(str)(''))\n        self.assertEquals(('a', 'b'), ConfigValue.tuple_of(str)('a, b'))\n        self.assertEquals(('a', 'b'),\n                          ConfigValue.tuple_of(str, delim=':')('a : b'))\n\n    def test_dict(self):\n        self.assertEquals({}, ConfigValue.dict(str, str)(''))\n        self.assertEquals({'a': ''}, ConfigValue.dict(str, str)('a'))\n        self.assertEquals({'a': 3}, ConfigValue.dict(str, int)('a: 3'))\n        self.assertEquals({'a': 3, 'b': 4},\n                          ConfigValue.dict(str, int)('a: 3, b: 4'))\n        self.assertEquals({'a': (3, 5), 'b': (4, 6)},\n                          ConfigValue.dict(\n                              str, ConfigValue.tuple_of(int), delim=';')\n                          ('a: 3, 5;  b: 4, 6'))\n\n    def test_choice(self):\n        self.assertEquals(1, ConfigValue.choice(alpha=1)('alpha'))\n        self.assertEquals(2, ConfigValue.choice(alpha=1, beta=2)('beta'))\n        with self.assertRaises(ValueError):\n            ConfigValue.choice(alpha=1)('asdf')\n\n    def test_timeinterval(self):\n        self.assertEquals(datetime.timedelta(0, 60),\n                          ConfigValue.timeinterval('1 minute'))\n        with self.assertRaises(KeyError):\n            ConfigValue.timeinterval('asdf')\n\n# TODO: test ConfigValue.messages\n# TODO: test ConfigValue.baseplate\n# TODO: test ConfigValue.json_dict\n"
  },
  {
    "path": "r2/r2/tests/unit/lib/cookie_upgrade_test.py",
    "content": "#!/usr/bin/env python\n# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport uuid\n\nimport datetime as dt\n\nfrom pylons import tmpl_context as c\nfrom pylons import app_globals as g\nfrom pylons import request\n\nfrom r2.lib.cookies import Cookies, Cookie, upgrade_cookie_security, NEVER\nfrom r2.models import Account, bcrypt_password, COOKIE_TIMESTAMP_FORMAT\nfrom r2.tests import RedditTestCase\n\n\nclass TestCookieUpgrade(RedditTestCase):\n\n    def setUp(self):\n        name = \"unit_tester_%s\" % uuid.uuid4().hex\n        self._password = uuid.uuid4().hex\n        self._account = Account(\n            name=name,\n            password=bcrypt_password(self._password)\n        )\n        self._account._id = 1337\n\n        c.cookies = Cookies()\n        c.secure = True\n        c.user_is_loggedin = True\n        c.user = self._account\n        c.oauth_user = None\n        request.method = \"POST\"\n\n    def tearDown(self):\n        c.cookies.clear()\n        c.user_is_loggedin = False\n        c.user = None\n\n    def _setSessionCookie(self, days_old=0):\n        date = dt.datetime.now() - dt.timedelta(days=days_old)\n        date_str = date.strftime(COOKIE_TIMESTAMP_FORMAT)\n        session_cookie = self._account.make_cookie(date_str)\n        c.cookies[g.login_cookie] = Cookie(\n            value=session_cookie,\n            dirty=False,\n        )\n\n    def test_no_upgrade_loggedout(self):\n        # We might have a now-invalid session cookie, don't bother upgrading\n        # it if it's not acceptable.\n        c.user_is_loggedin = False\n        c.user = None\n        self._setSessionCookie(days_old=60)\n        upgrade_cookie_security()\n        self.assertFalse(c.cookies[g.login_cookie].dirty)\n\n    def test_no_upgrade_http(self):\n        c.secure = False\n        self._setSessionCookie(days_old=60)\n        upgrade_cookie_security()\n        self.assertFalse(c.cookies[g.login_cookie].dirty)\n\n    def test_no_upgrade_no_cookie(self):\n        # Don't send back a cookie if we didn't even use cookie auth\n        upgrade_cookie_security()\n        self.assertFalse(g.login_cookie in c.cookies)\n\n    def test_no_upgrade_oauth(self):\n        # When g.domain == g.oauth_domain we might send a cookie even though\n        # we're not using it for auth. Don't echo it back in responses.\n        c.oauth_user = self._account\n        self._setSessionCookie(days_old=60)\n        upgrade_cookie_security()\n        self.assertFalse(c.cookies[g.login_cookie].dirty)\n\n    def test_no_upgrade_gets(self):\n        request.method = \"GET\"\n        self._setSessionCookie(days_old=60)\n        upgrade_cookie_security()\n        self.assertFalse(c.cookies[g.login_cookie].dirty)\n\n    def test_no_upgrade_secure_session(self):\n        self._setSessionCookie(days_old=60)\n        c.cookies[\"secure_session\"] = Cookie(value=\"1\")\n        upgrade_cookie_security()\n        self.assertFalse(c.cookies[g.login_cookie].dirty)\n\n    def test_upgrade_posts(self):\n        self._setSessionCookie(days_old=60)\n        upgrade_cookie_security()\n        self.assertTrue(c.cookies[g.login_cookie].dirty)\n        self.assertTrue(c.cookies[g.login_cookie].secure)\n\n    def test_cookie_unchanged(self):\n        self._setSessionCookie(days_old=60)\n        old_session = c.cookies[g.login_cookie].value\n        upgrade_cookie_security()\n        self.assertTrue(c.cookies[g.login_cookie].dirty)\n        self.assertEqual(old_session, c.cookies[g.login_cookie].value)\n\n    def test_remember_old_session(self):\n        self._setSessionCookie(days_old=60)\n        upgrade_cookie_security()\n        self.assertTrue(c.cookies[g.login_cookie].dirty)\n        self.assertEqual(c.cookies[g.login_cookie].expires, NEVER)\n\n    def test_dont_remember_recent_session(self):\n        self._setSessionCookie(days_old=5)\n        upgrade_cookie_security()\n        self.assertTrue(c.cookies[g.login_cookie].dirty)\n        self.assertNotEqual(c.cookies[g.login_cookie].expires, NEVER)\n"
  },
  {
    "path": "r2/r2/tests/unit/lib/cssfilter_test.py",
    "content": "#!/usr/bin/env python\n# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport unittest\n\nfrom r2.lib.cssfilter import validate_css\n\n\nclass TestCSSFilter(unittest.TestCase):\n    def assertInvalid(self, css):\n        serialized, errors = validate_css(css, {})\n        self.assertNotEqual(errors, [])\n\n    def test_offsite_url(self):\n        testcase = u\"*{background-image:url('http://foobar/')}\"\n        self.assertInvalid(testcase)\n\n    def test_nested_url(self):\n        testcase = u\"*{background-image:calc(url('http://foobar/'))}\"\n        self.assertInvalid(testcase)\n\n    def test_url_prelude(self):\n        testcase = u\"*[foo=url('http://foobar/')]{color:red;}\"\n        self.assertInvalid(testcase)\n\n    def test_invalid_property(self):\n        testcase = u\"*{foo: red;}\"\n        self.assertInvalid(testcase)\n\n    def test_import(self):\n        testcase = u\"@import 'foobar'; *{}\"\n        self.assertInvalid(testcase)\n\n    def test_import_rule(self):\n        testcase = u\"*{ @import 'foobar'; }\"\n        self.assertInvalid(testcase)\n\n    # IE<8 XSS\n    def test_invalid_function(self):\n        testcase = u\"*{color:expression(alert(1));}\"\n        self.assertInvalid(testcase)\n\n    def test_invalid_function_prelude(self):\n        testcase = u\"*[foo=expression(alert(1))]{color:red;}\"\n        self.assertInvalid(testcase)\n\n    # Safari 5.x parser resynchronization issues\n    def test_semicolon_function(self):\n        testcase = u\"*{color: calc(;color:red;);}\"\n        self.assertInvalid(testcase)\n\n    def test_semicolon_block(self):\n        testcase = u\"*{color: [;color:red;];}\"\n        self.assertInvalid(testcase)\n\n    # Safari 5.x prelude escape\n    def test_escape_prelude(self):\n        testcase = u\"*[foo=bar{}*{color:blue}]{color:red;}\"\n        self.assertInvalid(testcase)\n\n    # Multi-browser url() escape via spaces inside quotes\n    def test_escape_url(self):\n        testcase = u\"*{background-image: url('foo bar');}\"\n        self.assertInvalid(testcase)\n\n    # Control chars break out of quotes in multiple browsers\n    def test_control_chars(self):\n        testcase = u\"*{font-family:'foobar\\x03;color:red;';}\"\n        self.assertInvalid(testcase)\n\n    def test_embedded_nulls(self):\n        testcase = u\"*{font-family:'foo\\x00bar'}\"\n        self.assertInvalid(testcase)\n\n    # Firefox allows backslashes in function names\n    def test_escaped_url(self):\n        testcase = u\"*{background-image:\\\\u\\\\r\\\\l('http://foobar/')}\"\n        self.assertInvalid(testcase)\n\n    # IE<8 allows backslash escapes in place of pretty much any char\n    def test_escape_function_obfuscation(self):\n        testcase = u\"*{color: expression\\\\28 alert\\\\28 1 \\\\29 \\\\29 }\"\n        self.assertInvalid(testcase)\n\n    # This is purely speculative, and may never affect actual browsers\n    # https://developer.mozilla.org/en-US/docs/Web/CSS/attr\n    def test_attr_url(self):\n        testcase = u\"*{background-image:attr(foobar url);}\"\n        self.assertInvalid(testcase)\n"
  },
  {
    "path": "r2/r2/tests/unit/lib/eventcollector_tests.py",
    "content": "#!/usr/bin/env python\n# coding=utf-8\n# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport datetime\n\nimport pytz\nimport json\nfrom pylons import app_globals as g\nfrom mock import MagicMock, patch\n\nfrom r2.tests import RedditTestCase\nfrom r2.models import Link\nfrom r2.lib import hooks\nfrom r2 import models\n\n\nFAKE_DATE = datetime.datetime(2005, 6, 23, 3, 14, 0, tzinfo=pytz.UTC)\n\n\nclass TestEventCollector(RedditTestCase):\n\n    def setUp(self):\n        super(TestEventCollector, self).setUp()\n        self.mock_eventcollector()\n        self.autopatch(hooks, \"get_hook\")\n\n    def test_vote_event(self):\n        self.patch_liveconfig(\"events_collector_vote_sample_rate\", 1.0)\n        enum_name = \"foo\"\n        enum_note = \"bar\"\n        notes = \"%s(%s)\" % (enum_name, enum_note)\n        initial_vote = MagicMock(is_upvote=True, is_downvote=False,\n                                 is_automatic_initial_vote=True,\n                                 previous_vote=None,\n                                 data={\"rank\": MagicMock()},\n                                 name=\"initial_vote\",\n                                 effects=MagicMock(\n                                     note_codes=[enum_name],\n                                     serializable_data={\"notes\": notes}))\n        g.events.vote_event(initial_vote)\n\n        self.amqp.assert_event_item(\n            dict(\n                event_topic=\"vote_server\",\n                event_type=\"server_vote\",\n                payload={\n                    'vote_direction': 'up',\n                    'target_type': 'magicmock',\n                    'target_age_seconds': initial_vote.thing._age.total_seconds(),\n                    'target_rank': initial_vote.data['rank'],\n                    'sr_id': initial_vote.thing.subreddit_slow._id,\n                    'sr_name': initial_vote.thing.subreddit_slow.name,\n                    'target_fullname': initial_vote.thing._fullname,\n                    'target_name': initial_vote.thing.name,\n                    'target_id': initial_vote.thing._id,\n                    'details_text': notes,\n                    'process_notes': enum_name,\n                    'auto_self_vote': True,\n                }\n            )\n        )\n\n    def test_vote_event_with_prev(self):\n        self.patch_liveconfig(\"events_collector_vote_sample_rate\", 1.0)\n        upvote = MagicMock(name=\"upvote\",\n                           is_automatic_initial_vote=False,\n                           data={\"rank\": MagicMock()})\n        upvote.previous_vote = MagicMock(name=\"previous_vote\",\n                                         is_upvote=False, is_downvote=True)\n        g.events.vote_event(upvote)\n\n        self.amqp.assert_event_item(\n            dict(\n                event_topic=\"vote_server\",\n                event_type=\"server_vote\",\n                payload={\n                    'vote_direction': 'up',\n                    'target_type': 'magicmock',\n                    'target_age_seconds': upvote.thing._age.total_seconds(),\n                    'target_rank': upvote.data['rank'],\n                    'sr_id': upvote.thing.subreddit_slow._id,\n                    'sr_name': upvote.thing.subreddit_slow.name,\n                    'target_fullname': upvote.thing._fullname,\n                    'target_name': upvote.thing.name,\n                    'target_id': upvote.thing._id,\n                    'prev_vote_ts': self.created_ts_mock,\n                    'prev_vote_direction': 'down',\n                }\n            )\n        )\n\n    def test_submit_event(self):\n        self.patch_liveconfig(\"events_collector_submit_sample_rate\", 1.0)\n        new_link = MagicMock(name=\"new_link\")\n        context = MagicMock(name=\"context\")\n        request = MagicMock(name=\"request\")\n        request.ip = \"1.2.3.4\"\n        g.events.submit_event(new_link, context=context, request=request)\n\n        self.amqp.assert_event_item(\n            dict(\n                event_topic=\"submit_events\",\n                event_type=\"ss.submit\",\n                payload={\n                    'domain': request.host,\n                    'user_id': context.user._id,\n                    'user_name': context.user.name,\n                    'user_neutered': new_link.author_slow._spam,\n                    'post_id': new_link._id,\n                    'post_fullname': new_link._fullname,\n                    'post_title': new_link.title,\n                    'post_type': \"self\",\n                    'post_body': new_link.selftext,\n                    'sr_id': new_link.subreddit_slow._id,\n                    'sr_name': new_link.subreddit_slow.name,\n                    'geoip_country': context.location,\n                    'oauth2_client_id': context.oauth2_client._id,\n                    'oauth2_client_app_type': context.oauth2_client.app_type,\n                    'oauth2_client_name': context.oauth2_client.name,\n                    'referrer_domain': self.domain_mock(),\n                    'referrer_url': request.headers.get(),\n                    'user_agent': request.user_agent,\n                    'user_agent_parsed': request.parsed_agent.to_dict(),\n                    'obfuscated_data': {\n                        'client_ip': request.ip,\n                        'client_ipv4_24': \"1.2.3\",\n                        'client_ipv4_16': \"1.2\",\n                    }\n                }\n            )\n        )\n\n    def test_report_event_link(self):\n        self.patch_liveconfig(\"events_collector_report_sample_rate\", 1.0)\n\n        target = MagicMock(name=\"target\")\n        target.__class__ = Link\n        target._deleted = False\n        target.author_slow._deleted = False\n\n        context = MagicMock(name=\"context\")\n        request = MagicMock(name=\"request\")\n        request.ip = \"1.2.3.4\"\n        g.events.report_event(\n            target=target, context=context, request=request\n        )\n\n        self.amqp.assert_event_item(\n            {\n                'event_type': \"ss.report\",\n                'event_topic': 'report_events',\n                'payload': {\n                    'process_notes': \"CUSTOM\",\n                    'target_fullname': target._fullname,\n                    'target_name': target.name,\n                    'target_title': target.title,\n                    'target_type': \"self\",\n                    'target_author_id': target.author_slow._id,\n                    'target_author_name': target.author_slow.name,\n                    'target_id': target._id,\n                    'target_age_seconds': target._age.total_seconds(),\n                    'target_created_ts': self.created_ts_mock,\n                    'domain': request.host,\n                    'user_agent': request.user_agent,\n                    'user_agent_parsed': request.parsed_agent.to_dict(),\n                    'referrer_url': request.headers.get(),\n                    'user_id': context.user._id,\n                    'user_name': context.user.name,\n                    'oauth2_client_id': context.oauth2_client._id,\n                    'oauth2_client_app_type': context.oauth2_client.app_type,\n                    'oauth2_client_name': context.oauth2_client.name,\n                    'referrer_domain': self.domain_mock(),\n                    'geoip_country': context.location,\n                    'obfuscated_data': {\n                        'client_ip': request.ip,\n                        'client_ipv4_24': \"1.2.3\",\n                        'client_ipv4_16': \"1.2\",\n                    }\n                }\n            }\n        )\n\n    def test_mod_event(self):\n        self.patch_liveconfig(\"events_collector_mod_sample_rate\", 1.0)\n        mod = MagicMock(name=\"mod\")\n        modaction = MagicMock(name=\"modaction\")\n        subreddit = MagicMock(name=\"subreddit\")\n        context = MagicMock(name=\"context\")\n        request = MagicMock(name=\"request\")\n        request.ip = \"1.2.3.4\"\n        g.events.mod_event(\n            modaction, subreddit, mod, context=context, request=request\n        )\n\n        self.amqp.assert_event_item(\n            {\n                'event_type': modaction.action,\n                'event_topic': 'mod_events',\n                'payload': {\n                    'sr_id': subreddit._id,\n                    'sr_name': subreddit.name,\n                    'domain': request.host,\n                    'user_agent': request.user_agent,\n                    'user_agent_parsed': request.parsed_agent.to_dict(),\n                    'referrer_url': request.headers.get(),\n                    'user_id': context.user._id,\n                    'user_name': context.user.name,\n                    'oauth2_client_id': context.oauth2_client._id,\n                    'oauth2_client_app_type': context.oauth2_client.app_type,\n                    'oauth2_client_name': context.oauth2_client.name,\n                    'referrer_domain': self.domain_mock(),\n                    'details_text': modaction.details_text,\n                    'geoip_country': context.location,\n                    'obfuscated_data': {\n                        'client_ip': request.ip,\n                        'client_ipv4_24': \"1.2.3\",\n                        'client_ipv4_16': \"1.2\",\n                    }\n                }\n            }\n        )\n\n    def test_quarantine_event(self):\n        self.patch_liveconfig(\"events_collector_quarantine_sample_rate\", 1.0)\n        event_type = MagicMock(name=\"event_type\")\n        subreddit = MagicMock(name=\"subreddit\")\n        context = MagicMock(name=\"context\")\n        request = MagicMock(name=\"request\")\n        request.ip = \"1.2.3.4\"\n        g.events.quarantine_event(\n            event_type, subreddit, context=context, request=request\n        )\n\n        self.amqp.assert_event_item(\n            {\n                'event_type': event_type,\n                'event_topic': 'quarantine',\n                \"payload\": {\n                    'domain': request.host,\n                    'referrer_domain': self.domain_mock(),\n                    'verified_email': context.user.email_verified,\n                    'user_id': context.user._id,\n                    'sr_name': subreddit.name,\n                    'referrer_url': request.headers.get(),\n                    'user_agent': request.user_agent,\n                    'user_agent_parsed': request.parsed_agent.to_dict(),\n                    'sr_id': subreddit._id,\n                    'user_name': context.user.name,\n                    'oauth2_client_id': context.oauth2_client._id,\n                    'oauth2_client_app_type': context.oauth2_client.app_type,\n                    'oauth2_client_name': context.oauth2_client.name,\n                    'geoip_country': context.location,\n                    'obfuscated_data': {\n                        'client_ip': request.ip,\n                        'client_ipv4_24': \"1.2.3\",\n                        'client_ipv4_16': \"1.2\",\n                    }\n                }\n            }\n        )\n\n    def test_modmail_event(self):\n        self.patch_liveconfig(\"events_collector_modmail_sample_rate\", 1.0)\n        message = MagicMock(name=\"message\", _date=FAKE_DATE)\n        first_message = MagicMock(name=\"first_message\")\n        message_cls = self.autopatch(models, \"Message\")\n        message_cls._byID.return_value = first_message\n        context = MagicMock(name=\"context\")\n        request = MagicMock(name=\"request\")\n        request.ip = \"1.2.3.4\"\n        g.events.modmail_event(\n            message, context=context, request=request\n        )\n\n        self.amqp.assert_event_item(\n            {\n                'event_type': \"ss.send_message\",\n                'event_topic': \"message_events\",\n                \"payload\": {\n                    'domain': request.host,\n                    'referrer_domain': self.domain_mock(),\n                    'user_id': message.author_slow._id,\n                    'user_name': message.author_slow.name,\n                    'message_id': message._id,\n                    'message_fullname': message._fullname,\n                    'message_kind': \"modmail\",\n                    'message_body': message.body,\n                    'message_subject': message.subject,\n                    'first_message_fullname': first_message._fullname,\n                    'first_message_id': first_message._id,\n                    'sender_type': \"moderator\",\n                    'is_third_party': True,\n                    'third_party_metadata': \"mailgun\",\n                    'referrer_url': request.headers.get(),\n                    'user_agent': request.user_agent,\n                    'user_agent_parsed': request.parsed_agent.to_dict(),\n                    'sr_id': message.subreddit_slow._id,\n                    'sr_name': message.subreddit_slow.name,\n                    'oauth2_client_id': context.oauth2_client._id,\n                    'oauth2_client_app_type': context.oauth2_client.app_type,\n                    'oauth2_client_name': context.oauth2_client.name,\n                    'geoip_country': context.location,\n                    'obfuscated_data': {\n                        'client_ip': request.ip,\n                        'client_ipv4_24': \"1.2.3\",\n                        'client_ipv4_16': \"1.2\",\n                    },\n                },\n            }\n        )\n\n    def test_message_event(self):\n        self.patch_liveconfig(\"events_collector_modmail_sample_rate\", 1.0)\n        message = MagicMock(name=\"message\", _date=FAKE_DATE)\n        first_message = MagicMock(name=\"first_message\")\n        message_cls = self.autopatch(models, \"Message\")\n        message_cls._byID.return_value = first_message\n        context = MagicMock(name=\"context\")\n        request = MagicMock(name=\"request\")\n        request.ip = \"1.2.3.4\"\n        g.events.message_event(\n            message, context=context, request=request\n        )\n\n        self.amqp.assert_event_item(\n            {\n                'event_type': \"ss.send_message\",\n                'event_topic': \"message_events\",\n                \"payload\": {\n                    'domain': request.host,\n                    'referrer_domain': self.domain_mock(),\n                    'user_id': message.author_slow._id,\n                    'user_name': message.author_slow.name,\n                    'message_id': message._id,\n                    'message_fullname': message._fullname,\n                    'message_kind': \"message\",\n                    'message_body': message.body,\n                    'message_subject': message.subject,\n                    'first_message_fullname': first_message._fullname,\n                    'first_message_id': first_message._id,\n                    'sender_type': \"user\",\n                    'is_third_party': True,\n                    'third_party_metadata': \"mailgun\",\n                    'referrer_url': request.headers.get(),\n                    'user_agent': request.user_agent,\n                    'user_agent_parsed': request.parsed_agent.to_dict(),\n                    'oauth2_client_id': context.oauth2_client._id,\n                    'oauth2_client_app_type': context.oauth2_client.app_type,\n                    'oauth2_client_name': context.oauth2_client.name,\n                    'geoip_country': context.location,\n                    'obfuscated_data': {\n                        'client_ip': request.ip,\n                        'client_ipv4_24': \"1.2.3\",\n                        'client_ipv4_16': \"1.2\",\n                    },\n                },\n            }\n        )\n"
  },
  {
    "path": "r2/r2/tests/unit/lib/js_test.py",
    "content": "#!/usr/bin/env python\n# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2016 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport unittest\n\nfrom r2.lib import js\n\n\ndef concat_sources(sources):\n    return \";\".join(sources)\n\n\nclass TestFileSource(js.FileSource):\n    def get_source(self, *args, **kwargs):\n        return self.name\n\n\nclass TestModule(js.Module):\n    def get_default_source(self, source):\n        return TestFileSource(source)\n\n    def build(self, *args, **kwargs):\n        sources = self.get_flattened_sources([])\n        sources = [s.get_source() for s in sources]\n        return concat_sources(sources)\n\n\nclass TestModuleGetFlattenedSources(unittest.TestCase):\n    def test_flat_modules_include_all_sources(self):\n        test_files = [\"foo.js\", \"bar.js\", \"baz.js\", \"qux.js\"]\n        test_module = TestModule(\"test_module\", *test_files)\n        self.assertEqual(test_module.build(), concat_sources(test_files))\n\n    def test_nested_modules_include_all_sources(self):\n        test_files_a = [\"foo.js\", \"bar.js\"]\n        test_module_a = TestModule(\"test_module_a\", *test_files_a)\n        test_files_b = [\"baz.js\", \"qux.js\"]\n        test_module_b = TestModule(\"test_module_b\", *test_files_b)\n        test_module = TestModule(\"test_mobule\", test_module_a, test_module_b)\n        self.assertEqual(test_module.build(), concat_sources(test_files_a + test_files_b))\n\n    def test_flat_modules_only_include_sources_once(self):\n        test_files = [\"foo.js\", \"bar.js\", \"baz.js\", \"qux.js\"]\n        test_files_dup = test_files * 2\n        test_module = TestModule(\"test_module\", *test_files_dup)\n        self.assertEqual(test_module.build(), concat_sources(test_files))\n\n    def test_nested_modules_only_include_sources_once(self):\n        test_files = [\"foo.js\", \"bar.js\", \"baz.js\", \"qux.js\"]\n        test_module_a = TestModule(\"test_module_a\", *test_files)\n        test_module_b = TestModule(\"test_module_b\", *test_files)\n        test_module = TestModule(\"test_mobule\", test_module_a, test_module_b)\n        self.assertEqual(test_module.build(), concat_sources(test_files))\n\n    def test_filtered_modules_do_not_include_filtered_sources(self):\n        test_files = [\"foo.js\", \"bar.js\"]\n        filtered_files = [\"baz.js\", \"qux.js\"]\n        all_files = test_files + filtered_files\n        filter_module = TestModule(\"filter_module\", *filtered_files)\n        test_module = TestModule(\"test_module\", filter_module=filter_module, *all_files)\n        self.assertEqual(test_module.build(), concat_sources(test_files))\n"
  },
  {
    "path": "r2/r2/tests/unit/lib/loid_tests.py",
    "content": "from mock import MagicMock, ANY, call\nfrom urllib import quote\nfrom r2.tests import RedditTestCase\nfrom r2.lib import hooks\nfrom r2.lib.loid import LoId, LOID_COOKIE, LOID_CREATED_COOKIE, isodate\nfrom r2.lib.utils import to_epoch_milliseconds\n\n\nclass LoidTests(RedditTestCase):\n\n    def setUp(self):\n        super(LoidTests, self).setUp()\n        self.mock_eventcollector()\n\n    def test_ftue_autocreate(self):\n        request = MagicMock()\n        context = MagicMock()\n        request.cookies = {}\n        loid = LoId.load(request, context, create=True)\n        self.assertIsNotNone(loid.loid)\n        self.assertIsNotNone(loid.created)\n        self.assertTrue(loid.new)\n\n        loid.save()\n\n        context.cookies.add.assert_has_calls([\n            call(\n                LOID_COOKIE,\n                quote(loid.loid),\n                expires=ANY,\n            ),\n            call(\n                LOID_CREATED_COOKIE,\n                isodate(loid.created),\n                expires=ANY,\n            )\n        ])\n        self.amqp.assert_event_item(\n            dict(\n                event_topic=\"loid_events\",\n                event_type=\"ss.create_loid\",\n                payload={\n                    'loid_new': True,\n                    'loid': loid.loid,\n                    'loid_created': to_epoch_milliseconds(loid.created),\n                    'loid_version': 0,\n\n                    'user_id': context.user._id,\n                    'user_name': context.user.name,\n\n                    'request_url': request.fullpath,\n                    'domain': request.host,\n                    'geoip_country': context.location,\n                    'oauth2_client_id': context.oauth2_client._id,\n                    'oauth2_client_app_type': context.oauth2_client.app_type,\n                    'oauth2_client_name': context.oauth2_client.name,\n                    'referrer_domain': self.domain_mock(),\n                    'referrer_url': request.headers.get(),\n                    'user_agent': request.user_agent,\n                    'user_agent_parsed': request.parsed_agent.to_dict(),\n                    'obfuscated_data': {\n                        'client_ip': request.ip,\n                    }\n                },\n            )\n        )\n\n    def test_ftue_nocreate(self):\n        request = MagicMock()\n        context = MagicMock()\n        request.cookies = {}\n        loid = LoId.load(request, context, create=False)\n        self.assertFalse(loid.new)\n        self.assertFalse(loid.serializable)\n        loid.save()\n        self.assertFalse(bool(context.cookies.add.called))\n\n    def test_returning(self):\n        request = MagicMock()\n        context = MagicMock()\n        request.cookies = {LOID_COOKIE: \"foo\", LOID_CREATED_COOKIE: \"bar\"}\n        loid = LoId.load(request, context, create=False)\n        self.assertEqual(loid.loid, \"foo\")\n        self.assertNotEqual(loid.created, \"bar\")\n        self.assertFalse(loid.new)\n        self.assertTrue(loid.serializable)\n        loid.save()\n        self.assertFalse(bool(context.cookies.add.called))\n"
  },
  {
    "path": "r2/r2/tests/unit/lib/media_test.py",
    "content": "#!/usr/bin/env python\n# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport unittest\n\nfrom mock import patch\n\nfrom r2.lib.media import _get_scrape_url\nfrom r2.models import Link\n\nclass TestGetScrapeUrl(unittest.TestCase):\n    @patch('r2.lib.media.Link')\n    def test_link_post(self, Link):\n        post = Link()\n        post.url = 'https://example.com'\n        post.is_self = False\n        url = _get_scrape_url(post)\n        self.assertEqual(url, 'https://example.com')\n\n    def test_simple_self_post(self):\n        post = Link(is_self=True, selftext='''\nSome text here.\nhttps://example.com\nhttps://reddit.com''')\n        url = _get_scrape_url(post)\n        self.assertEqual(url, 'https://example.com')\n\n    def test_imgur_link(self):\n        post = Link(is_self=True, selftext='''\nSome text here.\nhttps://example.com\nhttps://imgur.com''')\n        url = _get_scrape_url(post)\n        self.assertEqual(url, 'https://imgur.com')\n\n    def test_image_link(self):\n        post = Link(is_self=True, selftext='''\nSome text here.\nhttps://example.com\nhttps://reddit.com/a.jpg''')\n        url = _get_scrape_url(post)\n        self.assertEqual(url, 'https://reddit.com/a.jpg')\n\n        post = Link(is_self=True, selftext='''\nSome text here.\nhttps://example.com\nhttps://reddit.com/a.PNG''')\n        url = _get_scrape_url(post)\n        self.assertEqual(url, 'https://reddit.com/a.PNG')\n\n        post = Link(is_self=True, selftext='''\nSome text here.\nhttps://example.com\nhttps://reddit.com/a.jpg/b''')\n        url = _get_scrape_url(post)\n        self.assertEqual(url, 'https://example.com')\n"
  },
  {
    "path": "r2/r2/tests/unit/lib/permissions_test.py",
    "content": "#!/usr/bin/env python\n# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport unittest\n\nfrom r2.lib.permissions import PermissionSet, ModeratorPermissionSet\n\nclass TestPermissionSet(PermissionSet):\n    info = dict(x={}, y={})\n\nclass PermissionSetTest(unittest.TestCase):\n    def test_dumps(self):\n        self.assertEquals(\n            '+all', PermissionSet(all=True).dumps())\n        self.assertEquals(\n            '+all', PermissionSet(all=True, other=True).dumps())\n        self.assertEquals(\n            '+a,-b', PermissionSet(a=True, b=False).dumps())\n\n    def test_loads(self):\n        self.assertEquals(\"\", TestPermissionSet.loads(None).dumps())\n        self.assertEquals(\"\", TestPermissionSet.loads(\"\").dumps())\n        self.assertEquals(\"+x,+y\", TestPermissionSet.loads(\"+x,+y\").dumps())\n        self.assertEquals(\"+x,-y\", TestPermissionSet.loads(\"+x,-y\").dumps())\n        self.assertEquals(\"+all\", TestPermissionSet.loads(\"+x,-y,+all\").dumps())\n        self.assertEquals(\"+x,-y,+z\",\n                          TestPermissionSet.loads(\"+x,-y,+z\").dumps())\n        self.assertRaises(ValueError,\n                          TestPermissionSet.loads, \"+x,-y,+z\", validate=True)\n        self.assertEquals(\n            \"+x,-y\",\n            TestPermissionSet.loads(\"-all,+x,-y\", validate=True).dumps())\n\n    def test_is_superuser(self):\n        perm_set = PermissionSet()\n        self.assertFalse(perm_set.is_superuser())\n        perm_set[perm_set.ALL] = True\n        self.assertTrue(perm_set.is_superuser())\n        perm_set[perm_set.ALL] = False\n        self.assertFalse(perm_set.is_superuser())\n\n    def test_is_valid(self):\n        perm_set = PermissionSet()\n        self.assertFalse(perm_set.is_valid())\n\n        perm_set = TestPermissionSet()\n        self.assertTrue(perm_set.is_valid())\n        perm_set['x'] = True\n        self.assertTrue(perm_set.is_valid())\n        perm_set[perm_set.ALL] = True\n        self.assertTrue(perm_set.is_valid())\n        perm_set['z'] = True\n        self.assertFalse(perm_set.is_valid())\n\n    def test_getitem(self):\n        perm_set = PermissionSet()\n        perm_set[perm_set.ALL] = True\n        self.assertFalse(perm_set['x'])\n\n        perm_set = TestPermissionSet()\n        perm_set['x'] = True\n        self.assertTrue(perm_set['x'])\n        self.assertFalse(perm_set['y'])\n        perm_set['x'] = False\n        self.assertFalse(perm_set['x'])\n        perm_set[perm_set.ALL] = True\n        self.assertTrue(perm_set['x'])\n        self.assertTrue(perm_set['y'])\n        self.assertFalse(perm_set['z'])\n        self.assertTrue(perm_set.get('x', False))\n        self.assertFalse(perm_set.get('z', False))\n        self.assertTrue(perm_set.get('z', True))\n\n\nclass ModeratorPermissionSetTest(unittest.TestCase):\n    def test_loads(self):\n        self.assertTrue(ModeratorPermissionSet.loads(None).is_superuser())\n        self.assertFalse(ModeratorPermissionSet.loads('').is_superuser())\n\n"
  },
  {
    "path": "r2/r2/tests/unit/lib/promote_test.py",
    "content": "import datetime\nimport unittest\n\nfrom mock import MagicMock, Mock, patch\n\nfrom r2.lib.promote import (\n    get_nsfw_collections_srnames,\n    get_refund_amount,\n    refund_campaign,\n    srnames_from_site,\n)\nfrom r2.models import (\n    Account,\n    Collection,\n    FakeAccount,\n    Frontpage,\n    PromoCampaign,\n    Subreddit,\n    MultiReddit,\n)\nfrom r2.tests import RedditTestCase, NonCache\n\n\nsubscriptions_srnames = [\"foo\", \"bar\"]\nsubscriptions = map(lambda srname: Subreddit(name=srname), subscriptions_srnames)\nmulti_srnames = [\"bing\", \"bat\"]\nmulti_subreddits = map(lambda srname: Subreddit(name=srname), multi_srnames)\nnice_srname = \"mylittlepony\"\nnsfw_srname = \"pr0n\"\nquestionably_nsfw = \"sexstories\"\nquarantined_srname = \"croontown\"\nnaughty_subscriptions = [\n    Subreddit(name=nice_srname),\n    Subreddit(name=nsfw_srname, over_18=True),\n    Subreddit(name=quarantined_srname, quarantine=True),\n]\nnsfw_collection_srnames = [questionably_nsfw, nsfw_srname]\nnsfw_collection = Collection(\n    name=\"after dark\",\n    sr_names=nsfw_collection_srnames,\n    over_18=True\n)\n\nclass TestSRNamesFromSite(RedditTestCase):\n    def setUp(self):\n        self.logged_in = Account(name=\"test\")\n        self.logged_out = FakeAccount()\n\n        self.patch_g(memoizecache=NonCache())\n\n    def test_frontpage_logged_out(self):\n        srnames = srnames_from_site(self.logged_out, Frontpage)\n\n        self.assertEqual(srnames, {Frontpage.name})\n\n    @patch(\"r2.models.Subreddit.user_subreddits\")\n    def test_frontpage_logged_in(self, user_subreddits):\n        user_subreddits.return_value = subscriptions\n        srnames = srnames_from_site(self.logged_in, Frontpage)\n\n        self.assertEqual(srnames, set(subscriptions_srnames) | {Frontpage.name})\n\n    def test_multi_logged_out(self):\n        multi = MultiReddit(path=\"/user/test/m/multi_test\", srs=multi_subreddits)\n        srnames = srnames_from_site(self.logged_out, multi)\n\n        self.assertEqual(srnames, set(multi_srnames))\n\n    @patch(\"r2.models.Subreddit.user_subreddits\")\n    def test_multi_logged_in(self, user_subreddits):\n        user_subreddits.return_value = subscriptions\n        multi = MultiReddit(path=\"/user/test/m/multi_test\", srs=multi_subreddits)\n        srnames = srnames_from_site(self.logged_in, multi)\n\n        self.assertEqual(srnames, set(multi_srnames))\n\n    def test_subreddit_logged_out(self):\n        srname = \"test1\"\n        subreddit = Subreddit(name=srname)\n        srnames = srnames_from_site(self.logged_out, subreddit)\n\n        self.assertEqual(srnames, {srname})\n\n    @patch(\"r2.models.Subreddit.user_subreddits\")\n    def test_subreddit_logged_in(self, user_subreddits):\n        user_subreddits.return_value = subscriptions\n        srname = \"test1\"\n        subreddit = Subreddit(name=srname)\n        srnames = srnames_from_site(self.logged_in, subreddit)\n\n        self.assertEqual(srnames, {srname})\n\n    @patch(\"r2.models.Subreddit.user_subreddits\")\n    def test_quarantined_subscriptions_are_never_included(self, user_subreddits):\n        user_subreddits.return_value = naughty_subscriptions\n        subreddit = Frontpage\n        srnames = srnames_from_site(self.logged_in, subreddit)\n\n        self.assertEqual(srnames, {subreddit.name} | {nice_srname})\n        self.assertTrue(len(srnames & {quarantined_srname}) == 0)\n\n    @patch(\"r2.models.Subreddit.user_subreddits\")\n    def test_nsfw_subscriptions_arent_included_when_viewing_frontpage(self, user_subreddits):\n        user_subreddits.return_value = naughty_subscriptions\n        srnames = srnames_from_site(self.logged_in, Frontpage)\n\n        self.assertEqual(srnames, {Frontpage.name} | {nice_srname})\n        self.assertTrue(len(srnames & {nsfw_srname}) == 0)\n\n    @patch(\"r2.models.Collection.get_all\")\n    def test_get_nsfw_collections_srnames(self, get_all):\n        get_all.return_value = [nsfw_collection]\n        srnames = get_nsfw_collections_srnames()\n\n        self.assertEqual(srnames, set(nsfw_collection_srnames))\n\n    @patch(\"r2.lib.promote.get_nsfw_collections_srnames\")\n    def test_remove_nsfw_collection_srnames_on_frontpage(self, get_nsfw_collections_srnames):\n        get_nsfw_collections_srnames.return_value = set(nsfw_collection.sr_names)\n        srname = \"test1\"\n        subreddit = Subreddit(name=srname)\n        Subreddit.user_subreddits = MagicMock(return_value=[\n            Subreddit(name=nice_srname),\n            Subreddit(name=questionably_nsfw),\n        ])\n\n        frontpage_srnames = srnames_from_site(self.logged_in, Frontpage)\n        swf_srnames = srnames_from_site(self.logged_in, subreddit)\n\n        self.assertEqual(frontpage_srnames, {Frontpage.name, nice_srname})\n        self.assertTrue(len(frontpage_srnames & {questionably_nsfw}) == 0)\n\n\nclass TestPromoteRefunds(unittest.TestCase):\n    def setUp(self):\n        self.link = Mock()\n        self.campaign = MagicMock(spec=PromoCampaign)\n        self.campaign._id = 1\n        self.campaign.owner_id = 1\n        self.campaign.trans_id = 1\n        self.campaign.bid_pennies = 1\n        self.campaign.start_date = datetime.datetime.now()\n        self.campaign.end_date = (datetime.datetime.now() +\n            datetime.timedelta(days=1))\n        self.campaign.total_budget_dollars = 200.\n        self.refund_amount = 100.\n        self.billable_amount = 100.\n        self.billable_impressions = 1000\n\n    @patch('r2.lib.promote.authorize.refund_transaction')\n    @patch('r2.lib.promote.PromotionLog.add')\n    @patch('r2.lib.promote.queries.unset_underdelivered_campaigns')\n    @patch('r2.lib.promote.emailer.refunded_promo')\n    def test_refund_campaign_success(self, emailer_refunded_promo,\n            queries_unset, promotion_log_add, refund_transaction):\n        \"\"\"Assert return value and that correct calls are made on success.\"\"\"\n        refund_transaction.return_value = (True, None)\n\n        # the refund process attemtps a db lookup. We don't need it for the\n        # purpose of the test.\n        with patch.object(Account, \"_byID\"):\n            success = refund_campaign(\n                link=self.link,\n                camp=self.campaign,\n                refund_amount=self.refund_amount,\n                billable_amount=self.billable_amount,\n                billable_impressions=self.billable_impressions,\n            )\n\n        self.assertTrue(refund_transaction.called)\n        self.assertTrue(promotion_log_add.called)\n        queries_unset.assert_called_once_with(self.campaign)\n        emailer_refunded_promo.assert_called_once_with(self.link)\n        self.assertTrue(success)\n\n    @patch('r2.lib.promote.authorize.refund_transaction')\n    @patch('r2.lib.promote.PromotionLog.add')\n    def test_refund_campaign_failed(self, promotion_log_add,\n            refund_transaction):\n        \"\"\"Assert return value and that correct calls are made on failure.\"\"\"\n        refund_transaction.return_value = (False, None)\n\n        # the refund process attemtps a db lookup. We don't need it for the\n        # purpose of the test.\n        with patch.object(Account, \"_byID\"):\n            success = refund_campaign(\n                link=self.link,\n                camp=self.campaign,\n                refund_amount=self.refund_amount,\n                billable_amount=self.billable_amount,\n                billable_impressions=self.billable_impressions,\n            )\n\n        self.assertTrue(refund_transaction.called)\n        self.assertTrue(promotion_log_add.called)\n        self.assertFalse(success)\n\n    def test_get_refund_amount_when_zero(self):\n        \"\"\"\n        Assert that correct value is returned when existing refund_amount is\n        zero.\n        \"\"\"\n        campaign = MagicMock(spec=('total_budget_dollars',))\n        campaign.total_budget_dollars = 200.\n        refund_amount = get_refund_amount(campaign, self.billable_amount)\n        self.assertEquals(refund_amount,\n            campaign.total_budget_dollars - self.billable_amount)\n\n    def test_get_refund_amount_rounding(self):\n        \"\"\"Assert that inputs are correctly rounded up to the nearest penny.\"\"\"\n        # If campaign.refund_amount is less than a fraction of a penny,\n        # the refund_amount should be campaign.total_budget_dollars.\n        self.campaign.refund_amount = 0.00000001\n        refund_amount = get_refund_amount(self.campaign, self.billable_amount)\n        self.assertEquals(refund_amount, self.billable_amount)\n\n        self.campaign.refund_amount = 0.00999999\n        refund_amount = get_refund_amount(self.campaign, self.billable_amount)\n        self.assertEquals(refund_amount, self.billable_amount)\n\n        # If campaign.refund_amount is just slightly more than a penny,\n        # the refund amount should be campaign.total_budget_dollars - 0.01.\n        self.campaign.refund_amount = 0.01000001\n        refund_amount = get_refund_amount(self.campaign, self.billable_amount)\n        self.assertEquals(refund_amount, self.billable_amount - 0.01)\n\n        # Even if campaign.refund_amount is just barely short of two pennies,\n        # the refund amount should be campaign.total_budget_dollars - 0.01.\n        self.campaign.refund_amount = 0.01999999\n        refund_amount = get_refund_amount(self.campaign, self.billable_amount)\n        self.assertEquals(refund_amount, self.billable_amount - 0.01)\n"
  },
  {
    "path": "r2/r2/tests/unit/lib/providers/__init__.py",
    "content": ""
  },
  {
    "path": "r2/r2/tests/unit/lib/providers/image_resizing/__init__.py",
    "content": ""
  },
  {
    "path": "r2/r2/tests/unit/lib/providers/image_resizing/imgix_test.py",
    "content": "#!/usr/bin/env python\n# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom r2.tests import RedditTestCase\n\nfrom r2.lib.providers.image_resizing import NotLargeEnough\nfrom r2.lib.providers.image_resizing.imgix import ImgixImageResizingProvider\nfrom r2.lib.utils import UrlParser\n\n\nURLENCODED_COMMA = '%2C'\n\n\nclass TestImgixResizer(RedditTestCase):\n    def setUp(self):\n        self.provider = ImgixImageResizingProvider()\n        self.patch_g(\n            imgix_domain='example.com',\n            imgix_signing=False,\n        )\n\n    def test_no_resize(self):\n        image = dict(url='http://s3.amazonaws.com/a.jpg', width=1200,\n                      height=800)\n        url = self.provider.resize_image(image)\n        self.assertEqual(url, 'https://example.com/a.jpg')\n\n    def test_too_small(self):\n        image = dict(url='http://s3.amazonaws.com/a.jpg', width=12,\n                      height=8)\n        with self.assertRaises(NotLargeEnough):\n            self.provider.resize_image(image, 108)\n\n    def test_resize(self):\n        image = dict(url='http://s3.amazonaws.com/a.jpg', width=1200,\n                      height=800)\n        for width in (108, 216, 320, 640, 960, 1080):\n            url = self.provider.resize_image(image, width)\n            self.assertEqual(url, 'https://example.com/a.jpg?w=%d' % width)\n\n    def test_cropping(self):\n        image = dict(url='http://s3.amazonaws.com/a.jpg', width=1200,\n                      height=800)\n        max_ratio = 0.5\n        url = self.provider.resize_image(image, max_ratio=max_ratio)\n        crop = URLENCODED_COMMA.join(('faces', 'entropy'))\n        self.assertEqual(url,\n                ('https://example.com/a.jpg?fit=crop&crop=%s&arh=%s'\n                    % (crop, max_ratio)))\n\n        width = 108\n        url = self.provider.resize_image(image, width, max_ratio=max_ratio)\n        self.assertEqual(url,\n                ('https://example.com/a.jpg?fit=crop&crop=%s&arh=%s&w=%s'\n                    % (crop, max_ratio, width)))\n\n    def test_sign_url(self):\n        u = UrlParser('http://examples.imgix.net/frog.jpg?w=100')\n        signed_url = self.provider._sign_url(u, 'abcdef')\n        self.assertEqual(signed_url.unparse(),\n                'http://examples.imgix.net/frog.jpg?w=100&s=cd3bdf071108af73b15c21bdcee5e49c')\n\n        u = UrlParser('http://examples.imgix.net/frog.jpg')\n        u.update_query(w=100)\n        signed_url = self.provider._sign_url(u, 'abcdef')\n        self.assertEqual(signed_url.unparse(),\n                'http://examples.imgix.net/frog.jpg?w=100&s=cd3bdf071108af73b15c21bdcee5e49c')\n\n    def test_censor(self):\n        image = dict(url='http://s3.amazonaws.com/a.jpg', width=1200,\n                      height=800)\n        url = self.provider.resize_image(image, censor_nsfw=True)\n        self.assertEqual(url, 'https://example.com/a.jpg?blur=600&px=32')\n"
  },
  {
    "path": "r2/r2/tests/unit/lib/providers/image_resizing/no_op_test.py",
    "content": "#!/usr/bin/env python\n# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport unittest\n\nfrom r2.lib.providers.image_resizing.no_op import NoOpImageResizingProvider\n\n\nclass TestLocalResizer(unittest.TestCase):\n    @classmethod\n    def setUpClass(cls):\n        cls.provider = NoOpImageResizingProvider()\n\n    def test_no_resize(self):\n        image = dict(url='http://s3.amazonaws.com/a.jpg', width=1200,\n                      height=800)\n        url = self.provider.resize_image(image)\n        self.assertEqual(url, 'http://s3.amazonaws.com/a.jpg')\n\n    def test_resize(self):\n        image = dict(url='http://s3.amazonaws.com/a.jpg', width=1200,\n                      height=800)\n\n        for width in (108, 216, 320, 640, 960, 1080):\n            url = self.provider.resize_image(image, width)\n            self.assertEqual(url, 'http://s3.amazonaws.com/a.jpg')\n"
  },
  {
    "path": "r2/r2/tests/unit/lib/providers/image_resizing/unsplashit_test.py",
    "content": "#!/usr/bin/env python\n# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport unittest\n\nfrom r2.lib.providers.image_resizing.unsplashit import UnsplashitImageResizingProvider\n\n\nclass TestLocalResizer(unittest.TestCase):\n    @classmethod\n    def setUpClass(cls):\n        cls.provider = UnsplashitImageResizingProvider()\n\n    def test_no_resize(self):\n        image = dict(url='http://s3.amazonaws.com/a.jpg', width=200,\n                      height=800)\n        url = self.provider.resize_image(image)\n        self.assertEqual(url, 'https://unsplash.it/200/400')\n\n    def test_resize(self):\n        image = dict(url='http://s3.amazonaws.com/a.jpg', width=1200,\n                      height=800)\n\n        for width in (108, 216, 320, 640, 960, 1080):\n            url = self.provider.resize_image(image, width)\n            self.assertEqual(url, 'https://unsplash.it/%d/%d' % (width,\n                width*2))\n"
  },
  {
    "path": "r2/r2/tests/unit/lib/reddit_agent_parser_test.py",
    "content": "#!/usr/bin/env python\n# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2016 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\n\nfrom r2.lib.utils.reddit_agent_parser import (\n    AlienBlueDetector,\n    BaconReaderDetector,\n    detect,\n    McRedditDetector,\n    NarwhalForRedditDetector,\n    ReaditDetector,\n    RedditAndroidDetector,\n    RedditIsFunDetector,\n    RedditIOSDetector,\n    RedditSyncDetector,\n    RelayForRedditDetector)\nfrom r2.tests import RedditTestCase\n\n\nclass AgentDetectorTest(RedditTestCase):\n    def test_reddit_is_fun_detector(self):\n        user_agent = 'reddit is fun (Android) 4.1.15'\n        agent_parsed = {}\n        result = RedditIsFunDetector().detect(user_agent, agent_parsed)\n        self.assertTrue(result)\n        self.assertEqual(agent_parsed['browser']['name'], 'reddit is fun')\n        self.assertEqual(agent_parsed['browser']['version'], '4.1.15')\n        self.assertEqual(agent_parsed['platform']['name'], 'Android')\n        self.assertEqual(agent_parsed['app_name'],\n                         agent_parsed['browser']['name'])\n\n    def test_reddit_android_detector(self):\n        user_agent = 'RedditAndroid 1.1.5'\n        agent_parsed = {}\n        result = RedditAndroidDetector().detect(user_agent, agent_parsed)\n        self.assertTrue(result)\n        self.assertEqual(agent_parsed['browser']['name'],\n                         RedditAndroidDetector.name)\n        self.assertEqual(agent_parsed['browser']['version'], '1.1.5')\n        self.assertTrue(agent_parsed['app_name'],\n                        agent_parsed['browser']['name'])\n\n    def test_reddit_ios_detector(self):\n        user_agent = ('Reddit/Version 1.1/Build 1106/iOS Version 9.3.2 '\n                      '(Build 13F69)')\n        agent_parsed = {}\n        result = RedditIOSDetector().detect(user_agent, agent_parsed)\n        self.assertEqual(agent_parsed['browser']['name'],\n                         RedditIOSDetector.name)\n        self.assertEqual(agent_parsed['browser']['version'], '1.1')\n        self.assertEqual(agent_parsed['platform']['name'], 'iOS')\n        self.assertEqual(agent_parsed['platform']['version'], '9.3.2')\n        self.assertEqual(agent_parsed['app_name'],\n                         agent_parsed['browser']['name'])\n\n    def test_alian_blue_detector(self):\n        user_agent = 'AlienBlue/2.9.10.0.2 CFNetwork/758.4.3 Darwin/15.5.0'\n        agent_parsed = {}\n        result = AlienBlueDetector().detect(user_agent, agent_parsed)\n        self.assertTrue(result)\n        self.assertEqual(agent_parsed['browser']['name'],\n                         AlienBlueDetector.name)\n        self.assertEqual(agent_parsed['browser']['version'], '2.9.10.0.2')\n        self.assertEqual(agent_parsed['app_name'],\n                         agent_parsed['app_name'])\n\n    def test_relay_for_reddit_detector(self):\n        user_agent = 'Relay by /u/DBrady v7.9.32'\n        agent_parsed = {}\n        result = RelayForRedditDetector().detect(user_agent, agent_parsed)\n        self.assertTrue(result)\n        self.assertEqual(agent_parsed['browser']['name'],\n                         RelayForRedditDetector.name)\n        self.assertEqual(agent_parsed['browser']['version'], '7.9.32')\n        self.assertEqual(agent_parsed['app_name'],\n                         agent_parsed['browser']['name'])\n\n    def test_reddit_sync_detector(self):\n        user_agent = ('android:com.laurencedawson.reddit_sync:v11.4 '\n                      '(by /u/ljdawson)')\n        agent_parsed = {}\n        result = RedditSyncDetector().detect(user_agent, agent_parsed)\n        self.assertTrue(result)\n        self.assertEqual(agent_parsed['browser']['name'],\n                         RedditSyncDetector.name)\n        self.assertEqual(agent_parsed['browser']['version'], '11.4')\n        self.assertEqual(agent_parsed['app_name'],\n                         agent_parsed['browser']['name'])\n\n    def test_narwhal_detector(self):\n        user_agent = 'narwhal-iOS/2306 by det0ur'\n        agent_parsed = {}\n        result = NarwhalForRedditDetector().detect(user_agent, agent_parsed)\n        self.assertTrue(result)\n        self.assertEqual(agent_parsed['browser']['name'],\n                         NarwhalForRedditDetector.name)\n        self.assertEqual(agent_parsed['platform']['name'], 'iOS')\n        self.assertEqual(agent_parsed['app_name'],\n                         agent_parsed['browser']['name'])\n\n    def test_mcreddit_detector(self):\n        user_agent = 'McReddit - Reddit Client for iOS'\n        agent_parsed = {}\n        result = McRedditDetector().detect(user_agent, agent_parsed)\n        self.assertTrue(result)\n        self.assertEqual(agent_parsed['browser']['name'],\n                         McRedditDetector.name)\n        self.assertEqual(agent_parsed['platform']['name'], 'iOS')\n        self.assertEqual(agent_parsed['app_name'],\n                         agent_parsed['browser']['name'])\n\n    def test_readit_detector(self):\n        user_agent = (\n            '(Readit for WP /u/MessageAcrossStudios) (Readit for WP '\n            '/u/MessageAcrossStudios)')\n        agent_parsed = {}\n        result = ReaditDetector().detect(user_agent, agent_parsed)\n        self.assertTrue(result)\n        self.assertEqual(agent_parsed['browser']['name'], ReaditDetector.name)\n        self.assertIsNone(agent_parsed.get('app_name'))\n\n    def test_bacon_reader_detector(self):\n        user_agent = 'BaconReader/3.0 (iPhone; iOS 9.3.2; Scale/2.00)'\n        agent_parsed = {}\n        result = BaconReaderDetector().detect(user_agent, agent_parsed)\n        self.assertTrue(result)\n        self.assertEqual(agent_parsed['browser']['name'],\n                         BaconReaderDetector.name)\n        self.assertEqual(agent_parsed['browser']['version'], '3.0')\n        self.assertEqual(agent_parsed['platform']['name'], 'iOS')\n        self.assertEqual(agent_parsed['platform']['version'], '9.3.2')\n        self.assertEqual(agent_parsed['app_name'],\n                         agent_parsed['browser']['name'])\n\n\nclass HAPIntegrationTests(RedditTestCase):\n    \"\"\"Tests to ensure that parsers don't confilct with existing onex.\"\"\"\n    # TODO (katie.atkinson): Add tests to ensure reddit parsers don't conflict\n    # with httpagentparser detectors.\n    def test_reddit_is_fun_integration(self):\n        user_agent = 'reddit is fun (Android) 4.1.15'\n        outs = detect(user_agent)\n        self.assertEqual(outs['browser']['name'], 'reddit is fun')\n        self.assertEqual(outs['dist']['name'], 'Android')\n\n    def test_reddit_android_integration(self):\n        user_agent = 'RedditAndroid 1.1.5'\n        outs = detect(user_agent)\n        self.assertEqual(outs['browser']['name'], 'Reddit: The Official App')\n        self.assertEqual(outs['dist']['name'], 'Android')\n\n    def test_reddit_ios_integration(self):\n        user_agent = ('Reddit/Version 1.1/Build 1106/iOS Version 9.3.2 '\n                      '(Build 13F69)')\n        outs = detect(user_agent)\n        self.assertEqual(outs['browser']['name'], RedditIOSDetector.name)\n\n    def test_alien_blue_detector(self):\n        user_agent = 'AlienBlue/2.9.10.0.2 CFNetwork/758.4.3 Darwin/15.5.0'\n        outs = detect(user_agent)\n        self.assertEqual(outs['browser']['name'], AlienBlueDetector.name)\n\n    def test_relay_for_reddit_detector(self):\n        user_agent = '  Relay by /u/DBrady v7.9.32'\n        outs = detect(user_agent)\n        self.assertEqual(outs['browser']['name'], RelayForRedditDetector.name)\n\n    def test_reddit_sync_detector(self):\n        user_agent = ('android:com.laurencedawson.reddit_sync:v11.4 '\n                      '(by /u/ljdawson)')\n        outs = detect(user_agent)\n        self.assertEqual(outs['browser']['name'], RedditSyncDetector.name)\n\n    def test_narwhal_detector(self):\n        user_agent = 'narwhal-iOS/2306 by det0ur'\n        outs = detect(user_agent)\n        self.assertEqual(outs['browser']['name'],\n                         NarwhalForRedditDetector.name)\n\n    def test_mcreddit_detector(self):\n        user_agent = 'McReddit - Reddit Client for iOS'\n        outs = detect(user_agent)\n        self.assertEqual(outs['browser']['name'], McRedditDetector.name)\n\n    def test_readit_detector(self):\n        user_agent = (\n            '(Readit for WP /u/MessageAcrossStudios) '\n            '(Readit for WP /u/MessageAcrossStudios)')\n        outs = detect(user_agent)\n        self.assertEqual(outs['browser']['name'], ReaditDetector.name)\n\n    def test_bacon_reader_detector(self):\n        user_agent = 'BaconReader/3.0 (iPhone; iOS 9.3.2; Scale/2.00)'\n        outs = detect(user_agent)\n        self.assertEqual(outs['browser']['name'], BaconReaderDetector.name)\n"
  },
  {
    "path": "r2/r2/tests/unit/lib/signing_tests.py",
    "content": "from mock import MagicMock, patch\n\nfrom pylons import app_globals as g\n\nfrom r2.tests import RedditTestCase\nfrom r2.lib import signing\n\n\nclass SigningTests(RedditTestCase):\n    def setUp(self):\n        super(RedditTestCase, self).setUp()\n        g.secrets['request_signature_secret'] = \"super_secret_do_not_share\"\n\n    def test_get_token(self):\n        # since the secret is static, so are these, so we're testing backward\n        # compat of the algo as well as verifying it varies with all inputs!\n        self.assertEqual(\n            signing.get_secret_token(\"test\", 1, 1),\n            \"008c42a8952d949b9c95109eea5016bb00a5a0ac141b35a0691fe6a01f084241\",\n        )\n        self.assertEqual(\n            signing.get_secret_token(\"test\", 2, 1),\n            \"5081cd2623e0391da6b81d9590e9272e00bd17c29b4e3fb9b0044ff999cf5ae2\",\n        )\n        self.assertRaises(\n            AssertionError,\n            lambda: signing.get_secret_token(\"test\", 1, 2),\n        )\n        self.assertEqual(\n            signing.get_secret_token(\"test2\", 1, 1),\n            \"07e87fdff4b8300b5282993cf30f8d652383854bf37a96da018354f7f5481832\",\n        )\n\n    def make_sig_header(self, body, platform=\"test\", version=1, epoch=None):\n        return signing.sign_v1_message(\n            body,\n            platform=platform,\n            version=version,\n            epoch=epoch,\n        )\n\n    def _assert_validity(self, body, header, success, error, **expected):\n        request = MagicMock(body=body, headers={})\n        if header:\n            request.headers[signing.SIGNATURE_BODY_HEADER] = header\n        signature = signing.valid_post_signature(request)\n        self.assertEqual(signature.is_valid(), bool(success))\n        if error:\n            self.assertIn(error.code, [code for code, _ in signature.errors])\n        else:\n            self.assertEqual(len(signature.errors), 0)\n        has_mac = expected.pop(\"has_mac\", False)\n        for k, v in expected.iteritems():\n            got = getattr(signature, k)\n            self.assertEqual(got, v, \"signature.%s: %s != %s\" % (k, got, v))\n\n        if has_mac:\n            self.assertTrue(bool(signature.mac))\n        else:\n            self.assertIsNone(signature.mac)\n\n    def assert_valid(self, body, header, **expected):\n        expected['success'] = True\n        expected['error'] = None\n        expected['has_mac'] = True\n        return self._assert_validity(body, header, **expected)\n\n    def assert_invalid(self, body, header, error, **expected):\n        expected.setdefault(\"global_version\", -1)\n        expected.setdefault(\"version\", -1)\n        expected.setdefault(\"platform\", None)\n        expected.setdefault('has_mac', False)\n        expected['success'] = False\n        expected['error'] = error\n        return self._assert_validity(body, header, **expected)\n\n    def test_signing(self):\n        epoch_time = 1234567890\n        header = self.make_sig_header(\n            '{\"user\": \"reddit\", \"password\": \"hunter2\"}',\n            epoch=epoch_time,\n        )\n        self.assertEqual(\n            header,\n            \"1:test:1:1234567890:\"\n            \"0fc3d90d83ac7433a5376c17f2aea9b470c368740c91c513e819e3a4980349de\"\n        )\n\n    def test_valid_header(self):\n        body = '{\"user\": \"reddit\", \"password\": \"hunter2\"}'\n        platform = \"something\"\n        version = 2\n        header = self.make_sig_header(\n            \"Body:{}\".format(body),\n            platform=platform,\n            version=version,\n        )\n        self.assert_valid(\n            body,\n            header,\n            version=version,\n            platform=platform,\n            global_version=1,\n        )\n\n    def test_no_header(self):\n        body = '{\"user\": \"reddit\", \"password\": \"hunter2\"}'\n        self.assert_invalid(body, \"\", signing.ERRORS.INVALID_FORMAT)\n\n    def test_garbage_header(self):\n        body = '{\"user\": \"reddit\", \"password\": \"hunter2\"}'\n        self.assert_invalid(\n            body,\n            header=\"idontneednosignature\",\n            error=signing.ERRORS.INVALID_FORMAT,\n        )\n\n    def test_future_header(self):\n        body = '{\"user\": \"reddit\", \"password\": \"hunter2\"}'\n        self.assert_invalid(\n            body,\n            header=\"2:awesomefuturespec\",\n            error=signing.ERRORS.UNKOWN_GLOBAL_VERSION,\n            global_version=2,\n        )\n\n    @patch.object(signing, \"is_invalid_token\", return_value=True)\n    def test_invalid(self, _):\n        body = '{\"user\": \"reddit\", \"password\": \"hunter2\"}'\n        platform = \"something\"\n        version = 2\n        # this is a perfectly valid signature (from `test_valid_header`)\n        # and a properly constructed request, but we've patched\n        # is_invalid_token\n        header = self.make_sig_header(body, platform=platform, version=version)\n        self.assert_invalid(\n            body,\n            header=header,\n            error=signing.ERRORS.INVALIDATED_TOKEN,\n            global_version=1,\n            version=version,\n            platform=platform,\n            has_mac=True,\n        )\n\n    def test_invalid_header(self):\n        body = '{\"user\": \"reddit\", \"password\": \"hunter2\"}'\n        platform = \"test\"\n        version = 1\n        header = \"1:%s:%s:deadbeef\" % (platform, version)\n        self.assert_invalid(\n            body,\n            header=header,\n            error=signing.ERRORS.UNPARSEABLE,\n            global_version=1,\n        )\n\n    def test_expired_header(self):\n        body = '{\"user\": \"reddit\", \"password\": \"hunter2\"}'\n        platform = \"test\"\n        version = 1\n        header = \"1:%s:%s:0:deadbeef\" % (platform, version)\n        self.assert_invalid(\n            body,\n            header=header,\n            error=signing.ERRORS.EXPIRED_TOKEN,\n            global_version=1,\n            platform=platform,\n            version=version,\n            has_mac=True,\n        )\n"
  },
  {
    "path": "r2/r2/tests/unit/lib/souptest_test.py",
    "content": "#!/usr/bin/env python\n# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport unittest\n\nfrom r2.lib.souptest import (\n    souptest_fragment,\n    SoupDetectedCrasherError,\n    SoupError,\n    SoupSyntaxError,\n    SoupUnexpectedCDataSectionError,\n    SoupUnexpectedCommentError,\n    SoupUnsupportedAttrError,\n    SoupUnsupportedEntityError,\n    SoupUnsupportedNodeError,\n    SoupUnsupportedSchemeError,\n    SoupUnsupportedTagError,\n)\n\n\nclass TestSoupTest(unittest.TestCase):\n    def assertFragmentRaises(self, fragment, error):\n        self.assertRaises(error, souptest_fragment, fragment)\n\n    def assertFragmentValid(self, fragment):\n        souptest_fragment(fragment)\n\n    def test_benign(self):\n        \"\"\"A typical example of what we might get out of `safemarkdown()`\"\"\"\n        testcase = \"\"\"\n            <!-- SC_OFF -->\n            <div class=\"md\"><a href=\"http://zombo.com/\">Welcome</a></div>\n            <!-- SC_ON -->\n        \"\"\"\n        self.assertFragmentValid(testcase)\n\n    def test_unbalanced(self):\n        self.assertFragmentRaises(\"<div></div></div>\", SoupSyntaxError)\n\n    def test_unclosed_comment(self):\n        self.assertFragmentRaises(\"<!--\", SoupSyntaxError)\n\n    def test_invalid_comment(self):\n        testcase = \"<!--[if IE 6]>WHAT YEAR IS IT?<![endif]-->\"\n        self.assertFragmentRaises(testcase, SoupUnexpectedCommentError)\n\n    def test_quoting(self):\n        self.assertFragmentRaises(\"<div class=`poor IE`></div>\",\n                                  SoupSyntaxError)\n\n    def test_processing_instruction(self):\n        self.assertFragmentRaises(\"<?php not even once ?>\",\n                                  SoupUnsupportedNodeError)\n\n    def test_doctype(self):\n        self.assertFragmentRaises('<!DOCTYPE VRML>', SoupSyntaxError)\n\n    def test_entity_declarations(self):\n        testcase = '<!ENTITY lol \"bad things\">'\n        self.assertFragmentRaises(testcase, SoupSyntaxError)\n        testcase = '<!DOCTYPE div- [<!ENTITY lol \"bad things\">]>'\n        self.assertFragmentRaises(testcase, SoupSyntaxError)\n\n    def test_cdata_section(self):\n        testcase = '<![CDATA[If only XHTML 2 went anywhere]]>'\n        self.assertFragmentRaises(testcase, SoupUnexpectedCDataSectionError)\n\n    def test_entities(self):\n        self.assertFragmentRaises('&xml:what;', SoupError)\n        self.assertFragmentRaises('&foo,bar;', SoupError)\n        self.assertFragmentRaises('&#999999999999;', SoupUnsupportedEntityError)\n        self.assertFragmentRaises('&#00;', SoupUnsupportedEntityError)\n        self.assertFragmentRaises('&foo-bar;', SoupUnsupportedEntityError)\n        self.assertFragmentRaises('&foobar;', SoupUnsupportedEntityError)\n        self.assertFragmentValid('&nbsp;')\n        self.assertFragmentValid('&Omicron;')\n\n    def test_tag_whitelist(self):\n        testcase = \"<div><a><a><script>alert(1)</script></a></a></div>\"\n        self.assertFragmentRaises(testcase, SoupUnsupportedTagError)\n\n    def test_attr_whitelist(self):\n        testcase = '<div><a><a><em onclick=\"alert(1)\">FOO!</em></a></a></div>'\n        self.assertFragmentRaises(testcase, SoupUnsupportedAttrError)\n\n    def test_tag_xmlns(self):\n        self.assertFragmentRaises('<xml:div></xml:div>',\n                                  SoupUnsupportedTagError)\n        self.assertFragmentRaises('<div xmlns=\"http://zombo.com/foo\"></div>',\n                                  SoupError)\n\n    def test_attr_xmlns(self):\n        self.assertFragmentRaises('<div xml:class=\"baz\"></div>',\n                                  SoupUnsupportedAttrError)\n\n    def test_schemes(self):\n        self.assertFragmentValid('<a href=\"http://google.com\">a</a>')\n        self.assertFragmentValid('<a href=\"Http://google.com\">a</a>')\n        self.assertFragmentValid('<a href=\"/google.com\">a</a>')\n        self.assertFragmentRaises('<a href=\"javascript://google.com\">a</a>',\n                                  SoupUnsupportedSchemeError)\n\n    def test_crashers(self):\n        # Chrome crashes on weirdly encoded nulls.\n        self.assertFragmentRaises('<a href=\"http://example.com/%%30%30\">foo</a>',\n                                  SoupDetectedCrasherError)\n        self.assertFragmentRaises('<a href=\"http://example.com/%0%30\">foo</a>',\n                                  SoupDetectedCrasherError)\n        self.assertFragmentRaises('<a href=\"http://example.com/%%300\">foo</a>',\n                                  SoupDetectedCrasherError)\n        # Chrome crashes on extremely long hostnames\n        self.assertFragmentRaises('<a href=\"http://%s.com\">foo</a>' % (\"x\" * 300),\n                                  SoupDetectedCrasherError)\n"
  },
  {
    "path": "r2/r2/tests/unit/lib/stats_test.py",
    "content": "#!/usr/bin/env python\n# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport unittest\n\nfrom r2.lib import stats\n\nclass TimingStatBufferTest(unittest.TestCase):\n    def test_tsb(self):\n        tsb = stats.TimingStatBuffer()\n        self.assertEquals([], list(tsb.flush()))\n\n        for i in xrange(1, 4):\n            for j in xrange(i):\n                tsb.record(str(i), 0, 0.1 * (j + 1))\n        self.assertEquals(\n            set([('1', '100.0|ms'),\n                 ('2', '150.0|ms'),  # (0.1 + 0.2) / 2\n                 ('3', '200.0|ms'),  # (0.1 + 0.2 + 0.3) / 3\n                ]), set(tsb.flush()))\n\nclass CountingStatBufferTest(unittest.TestCase):\n    def test_csb(self):\n        csb = stats.CountingStatBuffer()\n        self.assertEquals([], list(csb.flush()))\n\n        for i in xrange(1, 4):\n            for j in xrange(i):\n                csb.record(str(i), j + 1)\n        self.assertEquals(\n            set([('1', '1|c'),\n                 ('2', '3|c'),\n                 ('3', '6|c')]),\n            set(csb.flush()))\n\nclass StringCountBufferTest(unittest.TestCase):\n    def test_encode_string(self):\n        enc = stats.StringCountBuffer._encode_string\n        self.assertEquals('test', enc('test'))\n        self.assertEquals('\\\\n\\\\&\\\\\\\\&', enc('\\n|\\\\&'))\n\n    def test_scb(self):\n        scb = stats.StringCountBuffer()\n        self.assertEquals([], list(scb.flush()))\n\n        for i in xrange(1, 4):\n            for j in xrange(i):\n                for k in xrange(j + 1):\n                    scb.record(str(i), str(j))\n        self.assertEquals(\n            set([('1', '1|s|0'),\n                 ('2', '1|s|0'),\n                 ('2', '2|s|1'),\n                 ('3', '1|s|0'),\n                 ('3', '2|s|1'),\n                 ('3', '3|s|2')]),\n            set(scb.flush()))\n\nclass FakeUdpSocket:\n    def __init__(self, *ignored_args):\n        self.host = None\n        self.port = None\n        self.datagrams = []\n\n    def sendto(self, datagram, host_port):\n        self.datagrams.append(datagram)\n\nclass StatsdConnectionUnderTest(stats.StatsdConnection):\n    _make_socket = FakeUdpSocket\n\nclass StatsdConnectionTest(unittest.TestCase):\n    @staticmethod\n    def connect(compress=False):\n         return StatsdConnectionUnderTest('host:1000', compress=compress)\n\n    def test_parse_addr(self):\n        self.assertEquals(\n            ('1:2', 3), stats.StatsdConnection._parse_addr('1:2:3'))\n\n    def test_send(self):\n        conn = self.connect()\n        conn.send((i, i) for i in xrange(1, 6))\n        self.assertEquals(\n            ['1:1\\n2:2\\n3:3\\n4:4\\n5:5'],\n            conn.sock.datagrams)\n\n        # verify compression\n        data = [('a.b.c.w', 1), ('a.b.c.x', 2), ('a.b.c.y', 3), ('a.b.z', 4),\n                ('bbb', 5), ('bbc', 6)]\n        conn = self.connect(compress=True)\n        conn.send(reversed(data))\n        self.assertEquals(\n            ['a.b.c.w:1\\n^06x:2\\n^06y:3\\n^04z:4\\nbbb:5\\nbbc:6'],\n            conn.sock.datagrams)\n        conn = self.connect(compress=False)\n        conn.send(reversed(data))\n        self.assertEquals(\n            ['bbc:6\\nbbb:5\\na.b.z:4\\na.b.c.y:3\\na.b.c.x:2\\na.b.c.w:1'],\n            conn.sock.datagrams)\n\n        # ensure send is a no-op when not connected\n        conn.sock = None\n        conn.send((i, i) for i in xrange(1, 6))\n\nclass StatsdClientUnderTest(stats.StatsdClient):\n    @classmethod\n    def _data_iterator(cls, x):\n       return sorted(iter(x))\n\n    @classmethod\n    def _make_conn(cls, addr):\n        return StatsdConnectionUnderTest(addr, compress=False)\n\nclass StatsdClientTest(unittest.TestCase):\n    def test_flush(self):\n        client = StatsdClientUnderTest('host:1000')\n        client.timing_stats.record('t', 0, 1)\n        client.counting_stats.record('c', 1)\n        client.flush()\n        self.assertEquals(\n            ['c:1|c\\nt:1000.0|ms'],\n            client.conn.sock.datagrams)\n\nclass CounterAndTimerTest(unittest.TestCase):\n    @staticmethod\n    def client():\n        return StatsdClientUnderTest('host:1000')\n\n    def test_get_stat_name(self):\n        self.assertEquals(\n            'a.b.c',\n            stats._get_stat_name('a', '', u'b', None, 'c', 0))\n\n    def test_counter(self):\n        c = stats.Counter(self.client(), 'c')\n        c.increment('a')\n        c.increment('b', 2)\n        c.decrement('c')\n        c.decrement('d', 2)\n        c += 1\n        c -= 2\n        self.assertEquals(\n            set([('c.a', '1|c'),\n                 ('c.b', '2|c'),\n                 ('c.c', '-1|c'),\n                 ('c.d', '-2|c'),\n                 ('c', '-1|c')]),\n            set(c.client.counting_stats.flush()))\n        self.assertEquals(set(), set(c.client.counting_stats.flush()))\n\n    def test_timer(self):\n        t = stats.Timer(self.client(), 't')\n        t._time = iter(i / 10.0 for i in xrange(10)).next\n        self.assertRaises(AssertionError, t.intermediate, 'fail')\n        self.assertRaises(AssertionError, t.stop)\n\n        t.start()\n        t.intermediate('a')\n        t.intermediate('b')\n        t.intermediate('c')\n        t.stop(subname='t')\n\n        self.assertRaises(AssertionError, t.intermediate, 'fail')\n        self.assertRaises(AssertionError, t.stop)\n        t.send('x', 0, 0.5)\n\n        self.assertEquals(\n            set([('t.a', '100.0|ms'),\n                 ('t.b', '100.0|ms'),\n                 ('t.c', '100.0|ms'),\n                 ('t.t', '400.0|ms'),\n                 ('t.x', '500.0|ms')]),\n            set(t.client.timing_stats.flush()))\n        self.assertEquals(set(), set(t.client.timing_stats.flush()))\n"
  },
  {
    "path": "r2/r2/tests/unit/lib/tracking_test.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport unittest\n\n\nMESSAGE = \"the quick brown fox jumped over...\"\nBLOCK_O_PADDING = (\"\\x10\\x10\\x10\\x10\\x10\\x10\\x10\\x10\"\n                   \"\\x10\\x10\\x10\\x10\\x10\\x10\\x10\\x10\")\nSECRET = \"abcdefghijklmnopqrstuvwxyz\"\nENCRYPTED = (\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaIbzth1QTzJxzHbHGnJywG5V1uR3tWtSB\"\n             \"8hTyIcfg6rUZC4Wo0pT8jkEt9o1c%2FkTn\")\n\n\nclass TestPadding(unittest.TestCase):\n    def test_pad_empty_string(self):\n        from r2.lib.tracking import _pad_message\n        padded = _pad_message(\"\")\n        self.assertEquals(padded, BLOCK_O_PADDING)\n\n    def test_pad_round_string(self):\n        from r2.lib.tracking import _pad_message, KEY_SIZE\n        padded = _pad_message(\"x\" * KEY_SIZE)\n        self.assertEquals(len(padded), KEY_SIZE * 2)\n        self.assertEquals(padded[KEY_SIZE:], BLOCK_O_PADDING)\n\n    def test_unpad_empty_message(self):\n        from r2.lib.tracking import _unpad_message\n        unpadded = _unpad_message(\"\")\n        self.assertEquals(unpadded, \"\")\n\n    def test_unpad_evil_message(self):\n        from r2.lib.tracking import _unpad_message\n        evil = (\"a\" * 88) + chr(57)\n        result = _unpad_message(evil)\n        self.assertEquals(result, \"\")\n\n    def test_padding_roundtrip(self):\n        from r2.lib.tracking import _unpad_message, _pad_message\n        tested = _unpad_message(_pad_message(MESSAGE))\n        self.assertEquals(MESSAGE, tested)\n\n\nclass TestEncryption(unittest.TestCase):\n    def test_salt(self):\n        from r2.lib.tracking import _make_salt, SALT_SIZE\n        self.assertEquals(len(_make_salt()), SALT_SIZE)\n\n    def test_encrypt(self):\n        from r2.lib.tracking import _encrypt, SALT_SIZE\n        encrypted = _encrypt(\n            \"a\" * SALT_SIZE,\n            MESSAGE,\n            SECRET,\n        )\n        self.assertEquals(encrypted, ENCRYPTED)\n\n    def test_decrypt(self):\n        from r2.lib.tracking import _decrypt\n        decrypted = _decrypt(ENCRYPTED, SECRET)\n        self.assertEquals(MESSAGE, decrypted)\n"
  },
  {
    "path": "r2/r2/tests/unit/lib/urlparser_test.py",
    "content": "#!/usr/bin/env python\n# coding=utf-8\n# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\nimport unittest\n\nfrom r2.lib.utils import UrlParser\nfrom r2.tests import RedditTestCase\nfrom pylons import app_globals as g\n\n\nclass TestIsRedditURL(RedditTestCase):\n\n    def setUp(self):\n        self.patch_g(offsite_subdomains=['blog'])\n\n    def _is_safe_reddit_url(self, url, subreddit=None):\n        web_safe = UrlParser(url).is_web_safe_url()\n        return web_safe and UrlParser(url).is_reddit_url(subreddit)\n\n    def assertIsSafeRedditUrl(self, url, subreddit=None):\n        self.assertTrue(self._is_safe_reddit_url(url, subreddit))\n\n    def assertIsNotSafeRedditUrl(self, url, subreddit=None):\n        self.assertFalse(self._is_safe_reddit_url(url, subreddit))\n\n    def test_normal_urls(self):\n        self.assertIsSafeRedditUrl(\"https://%s/\" % g.domain)\n        self.assertIsSafeRedditUrl(\"https://en.%s/\" % g.domain)\n        self.assertIsSafeRedditUrl(\"https://foobar.baz.%s/quux/?a\" % g.domain)\n        self.assertIsSafeRedditUrl(\"#anchorage\")\n        self.assertIsSafeRedditUrl(\"?path_relative_queries\")\n        self.assertIsSafeRedditUrl(\"/\")\n        self.assertIsSafeRedditUrl(\"/cats\")\n        self.assertIsSafeRedditUrl(\"/cats/\")\n        self.assertIsSafeRedditUrl(\"/cats/#maru\")\n        self.assertIsSafeRedditUrl(\"//foobaz.%s/aa/baz#quux\" % g.domain)\n        # XXX: This is technically a legal relative URL, are there any UAs\n        # stupid enough to treat this as absolute?\n        self.assertIsSafeRedditUrl(\"path_relative_subpath.com\")\n        # \"blog.reddit.com\" is not a reddit URL.\n        self.assertIsNotSafeRedditUrl(\"http://blog.%s/\" % g.domain)\n        self.assertIsNotSafeRedditUrl(\"http://foo.blog.%s/\" % g.domain)\n\n    def test_incorrect_anchoring(self):\n        self.assertIsNotSafeRedditUrl(\"http://www.%s.whatever.com/\" % g.domain)\n\n    def test_protocol_relative(self):\n        self.assertIsNotSafeRedditUrl(\"//foobaz.example.com/aa/baz#quux\")\n\n    def test_weird_protocols(self):\n        self.assertIsNotSafeRedditUrl(\n            \"javascript://%s/%%0d%%0aalert(1)\" % g.domain\n        )\n        self.assertIsNotSafeRedditUrl(\"hackery:whatever\")\n\n    def test_http_auth(self):\n        # There's no legitimate reason to include HTTP auth details in the URL,\n        # they only serve to confuse everyone involved.\n        # For example, this used to be the behaviour of `UrlParser`, oops!\n        # > UrlParser(\"http://everyoneforgets:aboutthese@/baz.com/\").unparse()\n        # 'http:///baz.com/'\n        self.assertIsNotSafeRedditUrl(\"http://foo:bar@/example.com/\")\n\n    def test_browser_quirks(self):\n        # Some browsers try to be helpful and ignore characters in URLs that\n        # they think might have been accidental (I guess due to things like:\n        # `<a href=\" http://badathtml.com/ \">`. We need to ignore those when\n        # determining if a URL is local.\n        self.assertIsNotSafeRedditUrl(\"/\\x00/example.com\")\n        self.assertIsNotSafeRedditUrl(\"\\x09//example.com\")\n        self.assertIsNotSafeRedditUrl(\" http://example.com/\")\n\n        # This is makes sure we're not vulnerable to a bug in\n        # urlparse / urlunparse.\n        # urlunparse(urlparse(\"////foo.com\")) == \"//foo.com\"! screwy!\n        self.assertIsNotSafeRedditUrl(\"////example.com/\")\n        self.assertIsNotSafeRedditUrl(\"//////example.com/\")\n        # Similar, but with a scheme\n        self.assertIsNotSafeRedditUrl(r\"http:///example.com/\")\n        # Webkit and co like to treat backslashes as equivalent to slashes in\n        # different places, maybe to make OCD Windows users happy.\n        self.assertIsNotSafeRedditUrl(r\"/\\example.com/\")\n        # On chrome this goes to example.com, not a subdomain of reddit.com!\n        self.assertIsNotSafeRedditUrl(\n            r\"http://\\\\example.com\\a.%s/foo\" % g.domain\n        )\n\n        # Combo attacks!\n        self.assertIsNotSafeRedditUrl(r\"///\\example.com/\")\n        self.assertIsNotSafeRedditUrl(r\"\\\\example.com\")\n        self.assertIsNotSafeRedditUrl(\"/\\x00//\\\\example.com/\")\n        self.assertIsNotSafeRedditUrl(\n            \"\\x09javascript://%s/%%0d%%0aalert(1)\" % g.domain\n        )\n        self.assertIsNotSafeRedditUrl(\n            \"http://\\x09example.com\\\\%s/foo\" % g.domain\n        )\n\n    def test_url_mutation(self):\n        u = UrlParser(\"http://example.com/\")\n        u.hostname = g.domain\n        self.assertTrue(u.is_reddit_url())\n\n        u = UrlParser(\"http://%s/\" % g.domain)\n        u.hostname = \"example.com\"\n        self.assertFalse(u.is_reddit_url())\n\n    def test_nbsp_allowances(self):\n        # We have to allow nbsps in URLs, let's just allow them where they can't\n        # do any damage.\n        self.assertIsNotSafeRedditUrl(\"http://\\xa0.%s/\" % g.domain)\n        self.assertIsNotSafeRedditUrl(\"\\xa0http://%s/\" % g.domain)\n        self.assertIsSafeRedditUrl(\"http://%s/\\xa0\" % g.domain)\n        self.assertIsSafeRedditUrl(\"/foo/bar/\\xa0baz\")\n        # Make sure this works if the URL is unicode\n        self.assertIsNotSafeRedditUrl(u\"http://\\xa0.%s/\" % g.domain)\n        self.assertIsNotSafeRedditUrl(u\"\\xa0http://%s/\" % g.domain)\n        self.assertIsSafeRedditUrl(u\"http://%s/\\xa0\" % g.domain)\n        self.assertIsSafeRedditUrl(u\"/foo/bar/\\xa0baz\")\n\n\nclass TestSwitchSubdomainByExtension(RedditTestCase):\n    def setUp(self):\n        self.patch_g(\n            domain='reddit.com',\n            domain_prefix='www',\n        )\n\n    def test_normal_urls(self):\n        u = UrlParser('http://www.reddit.com/r/redditdev')\n        u.switch_subdomain_by_extension('compact')\n        result = u.unparse()\n        self.assertEquals('http://i.reddit.com/r/redditdev', result)\n\n        u = UrlParser(result)\n        u.switch_subdomain_by_extension('mobile')\n        result = u.unparse()\n        self.assertEquals('http://simple.reddit.com/r/redditdev', result)\n\n    def test_default_prefix(self):\n        u = UrlParser('http://i.reddit.com/r/redditdev')\n        u.switch_subdomain_by_extension()\n        self.assertEquals('http://www.reddit.com/r/redditdev', u.unparse())\n\n        u = UrlParser('http://i.reddit.com/r/redditdev')\n        u.switch_subdomain_by_extension('does-not-exist')\n        self.assertEquals('http://www.reddit.com/r/redditdev', u.unparse())\n\n\nclass TestPathExtension(unittest.TestCase):\n    def test_no_path(self):\n        u = UrlParser('http://example.com')\n        self.assertEquals('', u.path_extension())\n\n    def test_directory(self):\n        u = UrlParser('http://example.com/')\n        self.assertEquals('', u.path_extension())\n\n        u = UrlParser('http://example.com/foo/')\n        self.assertEquals('', u.path_extension())\n\n    def test_no_extension(self):\n        u = UrlParser('http://example.com/a')\n        self.assertEquals('', u.path_extension())\n\n    def test_root_file(self):\n        u = UrlParser('http://example.com/a.jpg')\n        self.assertEquals('jpg', u.path_extension())\n\n    def test_nested_file(self):\n        u = UrlParser('http://example.com/foo/a.jpg')\n        self.assertEquals('jpg', u.path_extension())\n\n    def test_empty_extension(self):\n        u = UrlParser('http://example.com/a.')\n        self.assertEquals('', u.path_extension())\n\n    def test_two_extensions(self):\n        u = UrlParser('http://example.com/a.jpg.exe')\n        self.assertEquals('exe', u.path_extension())\n\n    def test_only_extension(self):\n        u = UrlParser('http://example.com/.bashrc')\n        self.assertEquals('bashrc', u.path_extension())\n\n\nclass TestEquality(unittest.TestCase):\n    def test_different_objects(self):\n        u = UrlParser('http://example.com')\n        self.assertNotEquals(u, None)\n\n    def test_different_protocols(self):\n        u = UrlParser('http://example.com')\n        u2 = UrlParser('https://example.com')\n        self.assertNotEquals(u, u2)\n\n    def test_different_domains(self):\n        u = UrlParser('http://example.com')\n        u2 = UrlParser('http://example.org')\n        self.assertNotEquals(u, u2)\n\n    def test_different_ports(self):\n        u = UrlParser('http://example.com')\n        u2 = UrlParser('http://example.com:8000')\n        u3 = UrlParser('http://example.com:8008')\n        self.assertNotEquals(u, u2)\n        self.assertNotEquals(u2, u3)\n\n    def test_different_paths(self):\n        u = UrlParser('http://example.com')\n        u2 = UrlParser('http://example.com/a')\n        u3 = UrlParser('http://example.com/b')\n        self.assertNotEquals(u, u2)\n        self.assertNotEquals(u2, u3)\n\n    def test_different_params(self):\n        u = UrlParser('http://example.com/')\n        u2 = UrlParser('http://example.com/;foo')\n        u3 = UrlParser('http://example.com/;bar')\n        self.assertNotEquals(u, u2)\n        self.assertNotEquals(u2, u3)\n\n    def test_different_queries(self):\n        u = UrlParser('http://example.com/')\n        u2 = UrlParser('http://example.com/?foo')\n        u3 = UrlParser('http://example.com/?foo=bar')\n        self.assertNotEquals(u, u2)\n        self.assertNotEquals(u2, u3)\n\n    def test_different_fragments(self):\n        u = UrlParser('http://example.com/')\n        u2 = UrlParser('http://example.com/#foo')\n        u3 = UrlParser('http://example.com/#bar')\n        self.assertNotEquals(u, u2)\n        self.assertNotEquals(u2, u3)\n\n    def test_same_url(self):\n        u = UrlParser('http://example.com:8000/a;b?foo=bar&bar=baz#spam')\n        u2 = UrlParser('http://example.com:8000/a;b?bar=baz&foo=bar#spam')\n        self.assertEquals(u, u2)\n\n        u3 = UrlParser('')\n        u3.scheme = 'http'\n        u3.hostname = 'example.com'\n        u3.port = 8000\n        u3.path = '/a'\n        u3.params = 'b'\n        u3.update_query(foo='bar', bar='baz')\n        u3.fragment = 'spam'\n        self.assertEquals(u, u3)\n\n    def test_integer_query_params(self):\n        u = UrlParser('http://example.com/?page=1234')\n        u2 = UrlParser('http://example.com/')\n        u2.update_query(page=1234)\n        self.assertEquals(u, u2)\n\n    def test_unicode_query_params(self):\n        u = UrlParser(u'http://example.com/?page=ｕｎｉｃｏｄｅ：（')\n        u2 = UrlParser('http://example.com/')\n        u2.update_query(page=u'ｕｎｉｃｏｄｅ：（')\n        self.assertEquals(u, u2)\n"
  },
  {
    "path": "r2/r2/tests/unit/lib/utils_test.py",
    "content": "#!/usr/bin/env python\n# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport collections\nimport unittest\nimport contextlib\nimport math\nimport functools\nimport traceback\nimport sys\n\nfrom mock import MagicMock, patch\nfrom pylons import request\nfrom pylons import app_globals as g\n\nfrom r2.lib import utils\n\n\nclass CrappyQuery(object):\n    \"\"\"Helper class for testing.\n    It satisfies the methods fetch_things2 will call on a query\n    and also generator interface.\n\n    It is expected that fetch_things2 will set _after on\n    instance of this class.\"\"\"\n\n    def __init__(self,\n                 start,\n                 end,\n                 failures_between_chunks=0,\n                 chunk_num_to_fail_on=None\n                 ):\n        \"\"\"\n        :param int start: integer to stat yielding from.\n        :param int end: integer to end yielding at (exclusive).\n        :param int failure_between_chunks: number of times to\n            fail before starting to yield numbers from next chunk.\n        :param int chunk_num_to_fail_on:  If not None, fail only\n            after this chunk.  Do not fail between other chunks.\n        \"\"\"\n        self.start = start\n        self.end = end\n        self.failures_between_chunks = failures_between_chunks\n        self.chunk_num_to_fail_on = chunk_num_to_fail_on\n\n        self._sort = \"ascending\"\n        self._rules = []\n\n        self.current_chunk = 0\n        self.num_after_was_called = 0\n        self.total_num_failed = 0\n        self.num_failed = 0\n        self.should_fail = False\n        self._reset_state()\n\n    def _reset_state(self):\n        self.i = self.start\n\n    def __iter__(self):\n        return self\n\n    def _after(self, num):\n        self.num_after_was_called += 1\n        self.start = num + 1\n\n    def __next__(self):\n        if (self.i >= self.start + self._limit or\n                self.i >= self.end):\n            # quit iterating if we reach end or end of chunk\n            self.current_chunk += 1\n            raise StopIteration()\n        if self.i == self.start:\n            # if we are at a start of a chunk, do some failing\n            if (self.num_failed < self.failures_between_chunks and\n                (self.chunk_num_to_fail_on is None or\n                    self.chunk_num_to_fail_on == self.current_chunk)):\n                self.should_fail = True\n            else:\n                self.should_fail = False\n                self.num_failed = 0\n        if self.should_fail:\n            self.num_failed += 1\n            self.total_num_failed += 1\n            raise ValueError(\"FOO %d %d %d\" %\n                             (self.i,\n                              self.current_chunk,\n                              self.num_failed))\n        ret = self.i\n        self.i += 1\n        return ret\n\n    def next(self):\n        return self.__next__()\n\n    def __call__(self):\n        self._reset_state()\n        return self\n\n\nclass UtilsTest(unittest.TestCase):\n    def test_weighted_lottery_errors(self):\n        self.assertRaises(ValueError, utils.weighted_lottery, {})\n        self.assertRaises(ValueError, utils.weighted_lottery, {'x': 0})\n        self.assertRaises(\n            ValueError, utils.weighted_lottery,\n            collections.OrderedDict([('x', -1), ('y', 1)]))\n\n    @contextlib.contextmanager\n    def check_exponential_backoff_sleep_times(self,\n                                              start,\n                                              num):\n        sleepy_times = []\n\n        def record_sleep_times(sec):\n            sleepy_times.append(sec)\n\n        try:\n            with patch('time.sleep', new=record_sleep_times):\n                yield\n        finally:\n            self.assertEquals(len(sleepy_times), num)\n            walker = start / 1000.0\n            for i in sleepy_times:\n                self.assertEquals(i, walker)\n                walker *= 2\n\n    def test_exponential_retrier(self):\n\n        num_retries = 5\n\n        def make_crappy_function(fail_start=1, fail_end=5):\n            \"\"\"Make a function that iterates from zero to infinity.\n            However when it is iterating in the range [fail_start,fail_end]\n            it will throw a ValueError exception but still increment\n            \"\"\"\n            side_effects = [0]\n\n            def ret():\n                ret = side_effects[0]\n                side_effects[0] += 1\n                if ret >= fail_start and ret <= fail_end:\n                    raise ValueError(\"foo %d\" % ret)\n                return ret\n\n            return ret\n\n        crappy_function = make_crappy_function()\n\n        with self.check_exponential_backoff_sleep_times(500, 0):\n            # first call to crappy_function should return zero without\n            # any retrying\n            self.assertEquals(0,\n                              utils.exponential_retrier(\n                                  crappy_function,\n                                  max_retries=num_retries))\n\n        with self.check_exponential_backoff_sleep_times(500, num_retries):\n            # this should return 6 as we will retry 5 times\n            self.assertEquals(6,\n                              utils.exponential_retrier(\n                                  crappy_function,\n                                  max_retries=num_retries))\n\n        with self.check_exponential_backoff_sleep_times(500, 0):\n            self.assertEqual(7,\n                             utils.exponential_retrier(\n                                 crappy_function,\n                                 max_retries=num_retries))\n\n        with self.check_exponential_backoff_sleep_times(1, num_retries):\n            # make sure this will fail in exponential_retrier and\n            # test that last exception is re_thrown\n            crappy_function = make_crappy_function(fail_start=0,\n                                                   fail_end=1000)\n\n            error = None\n            try:\n                utils.exponential_retrier(crappy_function,\n                                          retry_min_wait_ms=1,\n                                          max_retries=num_retries)\n            except ValueError as e:\n                # test that exception caught here has proper stack trace\n                self.assertTrue(any(map(\n                    lambda x: x[3] == \"raise ValueError(\\\"foo %d\\\" % ret)\",\n                    traceback.extract_tb(sys.exc_traceback))))\n                error = e\n\n            self.assertEquals(error.message, \"foo %d\" % num_retries)\n\n        with patch('time.sleep'):\n            # check that exception is rethrown if we pass\n            # exception filter that returns False if exception\n            # is ValueError\n            crappy_function = make_crappy_function(fail_start=0,\n                                                   fail_end=0)\n\n            def exception_filter(exception):\n                return type(exception) is not ValueError\n\n            error = None\n            try:\n                utils.exponential_retrier(crappy_function,\n                                          retry_min_wait_ms=1,\n                                          max_retries=100000,\n                                          exception_filter=exception_filter)\n            except ValueError as e:\n                error = e\n\n            self.assertEquals(error.message, \"foo 0\")\n\n    def test_retriable_fetch_things_passthrough(self):\n        # test simple pass through case\n\n        num_retries = 5\n        chunk_size = 5\n        end = 20\n        num_chunks = int(math.ceil(end / float(chunk_size)))\n\n        fetch_things_with_retry = functools.partial(\n            utils.fetch_things_with_retry,\n            chunk_size=chunk_size,\n            max_retries=num_retries,\n            retry_min_wait_ms=1)\n\n        crappy_query = CrappyQuery(0, end, 0)\n        generated = list(fetch_things_with_retry(crappy_query))\n\n        self.assertEquals(generated, range(0, end))\n        self.assertEquals(crappy_query.total_num_failed, 0)\n        self.assertEquals(crappy_query.num_after_was_called, num_chunks)\n\n    def test_retriable_fetch_things_exception_rethrow(self):\n        # test that exception is rethrown if we run out of retries\n\n        num_retries = 5\n        chunk_size = 5\n        end = 20\n        num_chunks = int(math.ceil(end / float(chunk_size)))\n\n        fetch_things_with_retry = functools.partial(\n            utils.fetch_things_with_retry,\n            chunk_size=chunk_size,\n            max_retries=num_retries,\n            retry_min_wait_ms=1)\n\n        with self.check_exponential_backoff_sleep_times(1, num_retries):\n            error = None\n            crappy_query = CrappyQuery(0, end, num_retries + 1)\n            try:\n                list(fetch_things_with_retry(crappy_query))\n            except ValueError as e:\n                error = e\n\n            # after should not have ever been called because\n            # getting the first chunk should have failed\n            self.assertEquals(0, crappy_query.num_after_was_called)\n            self.assertEquals(\"FOO %d %d %d\" % (0, 0, num_retries + 1),\n                              error.message)\n\n        # test same thing but failing in subsequent chunk\n        with self.check_exponential_backoff_sleep_times(1, num_retries):\n            crappy_query = CrappyQuery(0, end, num_retries + 1, 2)\n            generated = []\n            try:\n                # cant use list here as it wont get cbunks that succeeded\n                for i in fetch_things_with_retry(crappy_query):\n                    generated.append(i)\n            except ValueError as e:\n                error = e\n\n            # we should have generated some partial results\n            self.assertEquals(generated, range(0, chunk_size * 2))\n            self.assertEquals(\"FOO %d %d %d\" % (10, 2, num_retries + 1),\n                              error.message)\n            self.assertEquals(2, crappy_query.num_after_was_called)\n\n    def test_retriable_fetch_things_recover_from_fail(self):\n        # test that we get all of the numbers in the range\n        # if we the number of failures is less than number of retries\n\n        num_retries = 5\n        chunk_size = 5\n        end = 20\n        num_chunks = int(math.ceil(end / float(chunk_size)))\n\n        fetch_things_with_retry = functools.partial(\n            utils.fetch_things_with_retry,\n            chunk_size=chunk_size,\n            max_retries=num_retries,\n            retry_min_wait_ms=1)\n\n        with patch('time.sleep'):\n            crappy_query = CrappyQuery(0, end, num_retries - 1)\n            generated = list(fetch_things_with_retry(crappy_query))\n\n            self.assertEquals(generated, range(0, end))\n            self.assertEqual(num_chunks,\n                             crappy_query.num_after_was_called)\n\n            # same thing but fail in the subsequent chunk\n            crappy_query = CrappyQuery(0, end, num_retries - 1, 2)\n            generated = list(fetch_things_with_retry(crappy_query))\n\n            self.assertEquals(generated, range(0, end))\n            self.assertEquals(num_chunks,\n                              crappy_query.num_after_was_called)\n\n        # test same thing as above but with chunks=True\n        with patch('time.sleep'):\n            expected = []\n            for i in range(0, num_chunks):\n                expected.append(range(i * chunk_size,\n                                      i * chunk_size + chunk_size))\n\n            crappy_query = CrappyQuery(0, end, num_retries - 1)\n            generated = list(fetch_things_with_retry(crappy_query,\n                                                     chunks=True))\n\n            self.assertEquals(generated, expected)\n            self.assertEqual(num_chunks,\n                             crappy_query.num_after_was_called)\n\n            # same thing but fail in the subsequent chunk\n            crappy_query = CrappyQuery(0, end, num_retries - 1, 2)\n            generated = list(fetch_things_with_retry(crappy_query,\n                                                     chunks=True))\n\n            self.assertEquals(generated, expected)\n            self.assertEquals(num_chunks,\n                              crappy_query.num_after_was_called)\n\n    def test_weighted_lottery(self):\n        weights = collections.OrderedDict(\n            [('x', 2), (None, 0), (None, 0), ('y', 3), ('z', 1)])\n\n        def expect(result, random_value):\n            scaled_r = float(random_value) / sum(weights.itervalues())\n            self.assertEquals(\n                result,\n                utils.weighted_lottery(weights, _random=lambda: scaled_r))\n\n        expect('x', 0)\n        expect('x', 1)\n        expect('y', 2)\n        expect('y', 3)\n        expect('y', 4)\n        expect('z', 5)\n        self.assertRaises(ValueError, expect, None, 6)\n\n    def test_extract_subdomain(self):\n        self.assertEquals(\n            utils.extract_subdomain('beta.reddit.com', 'reddit.com'),\n            'beta')\n\n        self.assertEquals(\n            utils.extract_subdomain('beta.reddit.local:8000', 'reddit.local'),\n            'beta')\n\n        self.assertEquals(\n            utils.extract_subdomain('reddit.com', 'reddit.com'),\n            '')\n\n        self.assertEquals(\n            utils.extract_subdomain('internet-frontpage.com', 'reddit.com'),\n            '')\n\n    def test_coerce_url_to_protocol(self):\n        self.assertEquals(\n            utils.coerce_url_to_protocol('http://example.com/foo'),\n            'http://example.com/foo')\n\n        self.assertEquals(\n            utils.coerce_url_to_protocol('https://example.com/foo'),\n            'http://example.com/foo')\n\n        self.assertEquals(\n            utils.coerce_url_to_protocol('//example.com/foo'),\n            'http://example.com/foo')\n\n        self.assertEquals(\n            utils.coerce_url_to_protocol('http://example.com/foo', 'https'),\n            'https://example.com/foo')\n\n        self.assertEquals(\n            utils.coerce_url_to_protocol('https://example.com/foo', 'https'),\n            'https://example.com/foo')\n\n        self.assertEquals(\n            utils.coerce_url_to_protocol('//example.com/foo', 'https'),\n            'https://example.com/foo')\n\n    def test_sanitize_url(self):\n        self.assertEquals(\n            utils.sanitize_url('http://dk./'),\n            'http://dk/'\n        )\n\n        self.assertEquals(\n            utils.sanitize_url('http://google.com./'),\n            'http://google.com/'\n        )\n\n        self.assertEquals(\n            utils.sanitize_url('http://google.com/'),\n            'http://google.com/'\n        )\n\n        self.assertEquals(\n            utils.sanitize_url('https://github.com/reddit/reddit/pull/1302'),\n            'https://github.com/reddit/reddit/pull/1302'\n        )\n\n        self.assertEquals(\n            utils.sanitize_url('http://dk../'),\n            None\n        )\n\n\nclass TestCanonicalizeEmail(unittest.TestCase):\n    def test_empty_string(self):\n        canonical = utils.canonicalize_email(\"\")\n        self.assertEquals(canonical, \"\")\n\n    def test_unicode(self):\n        canonical = utils.canonicalize_email(u\"\\u2713@example.com\")\n        self.assertEquals(canonical, \"\\xe2\\x9c\\x93@example.com\")\n\n    def test_localonly(self):\n        canonical = utils.canonicalize_email(\"invalid\")\n        self.assertEquals(canonical, \"\")\n\n    def test_multiple_ats(self):\n        canonical = utils.canonicalize_email(\"invalid@invalid@invalid\")\n        self.assertEquals(canonical, \"\")\n\n    def test_remove_dots(self):\n        canonical = utils.canonicalize_email(\"d.o.t.s@example.com\")\n        self.assertEquals(canonical, \"dots@example.com\")\n\n    def test_remove_plus_address(self):\n        canonical = utils.canonicalize_email(\"fork+nork@example.com\")\n        self.assertEquals(canonical, \"fork@example.com\")\n\n    def test_unicode_in_byte_str(self):\n        # this shouldn't ever happen, but some entries in postgres appear\n        # to be byte strings with non-ascii in 'em.\n        canonical = utils.canonicalize_email(\"\\xe2\\x9c\\x93@example.com\")\n        self.assertEquals(canonical, \"\\xe2\\x9c\\x93@example.com\")\n\n\nclass TestTruncString(unittest.TestCase):\n    def test_empty_string(self):\n        truncated = utils.trunc_string('', 80)\n        self.assertEqual(truncated, '')\n\n    def test_short_enough(self):\n        truncated = utils.trunc_string('short string', 80)\n        self.assertEqual(truncated, 'short string')\n\n    def test_word_breaks(self):\n        truncated = utils.trunc_string('two words', 6)\n        self.assertEqual(truncated, 'two...')\n\n    def test_suffix(self):\n        truncated = utils.trunc_string('two words', 6, '')\n        self.assertEqual(truncated, 'two')\n\n    def test_really_long_words(self):\n        truncated = utils.trunc_string('ThisIsALongWord', 10)\n        self.assertEqual(truncated, 'ThisIsA...')\n\n\nclass TestUrlToThing(unittest.TestCase):\n\n    def test_subreddit_noslash(self):\n        with patch('r2.models.Subreddit') as MockSubreddit:\n            MockSubreddit._by_name.return_value = s.Subreddit\n            self.assertEqual(\n                utils.url_to_thing('http://reddit.local/r/pics'),\n                s.Subreddit,\n            )\n\n    def test_subreddit(self):\n        with patch('r2.models.Subreddit') as MockSubreddit:\n            MockSubreddit._by_name.return_value = s.Subreddit\n            self.assertEqual(\n                utils.url_to_thing('http://reddit.local/r/pics/'),\n                s.Subreddit,\n            )\n\n    def test_frontpage(self):\n        self.assertEqual(\n            utils.url_to_thing('http://reddit.local/'),\n            None,\n        )\n"
  },
  {
    "path": "r2/r2/tests/unit/lib/validator/__init__.py",
    "content": ""
  },
  {
    "path": "r2/r2/tests/unit/lib/validator/test_validator.py",
    "content": "#!/usr/bin/env python\n# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport unittest\nfrom r2.tests import RedditTestCase\n\nfrom datetime import datetime as dt\nfrom mock import MagicMock, patch\nfrom pylons import tmpl_context as c\nfrom pylons import app_globals as g\nfrom webob.exc import HTTPForbidden\n\nfrom r2.lib.errors import errors, ErrorSet\nfrom r2.lib.validator import (\n    VByName,\n    VSubmitParent,\n    VSubredditName,\n    ValidEmail,\n)\nfrom r2.models import Account, Comment, Link, Message, Subreddit\n\n\nclass ValidatorTests(RedditTestCase):\n    def _test_failure(self, input, error):\n        \"\"\"Helper for testing bad inputs.\"\"\"\n        self.validator.run(input)\n        self.assertTrue(self.validator.has_errors)\n        self.assertTrue(c.errors.get((error, None)))\n\n    def _test_success(self, input, assertEqual=True):\n        result = self.validator.run(input)\n        self.assertFalse(self.validator.has_errors)\n        self.assertEqual(len(c.errors), 0)\n        if assertEqual:\n            self.assertEqual(result, input)\n\n        return result\n\n\nclass TestVSubmitParent(ValidatorTests):\n    def setUp(self):\n        super(TestVSubmitParent, self).setUp()\n        # Reset the validator state and errors before every test.\n        self.validator = VSubmitParent(None)\n        c.errors = ErrorSet()\n\n        c.user_is_loggedin = True\n        c.user_is_admin = False\n        c.user = Account(id=100)\n\n        self.autopatch(Account, \"enemy_ids\", return_value=[])\n        self.autopatch(Subreddit, \"_byID\", return_value=None)\n\n    def _mock_message(self, id=1, author_id=1, **kwargs):\n        kwargs['id'] = id\n        kwargs['author_id'] = author_id\n\n        message = Message(**kwargs)\n        self.autopatch(VByName, \"run\", return_value=message)\n\n        return message\n\n    def _mock_link(self, id=1, author_id=1, sr_id=1, can_comment=True,\n                   can_view_promo=True, **kwargs):\n        kwargs['id'] = id\n        kwargs['author_id'] = author_id\n        kwargs['sr_id'] = sr_id\n\n        link = Link(**kwargs)\n        self.autopatch(VByName, \"run\", return_value=link)\n\n        sr = Subreddit(id=sr_id)\n        self.autopatch(Subreddit, \"_byID\", return_value=sr)\n        self.autopatch(Subreddit, \"can_comment\", return_value=can_comment)\n        self.autopatch(Link, \"can_view_promo\", return_value=can_view_promo)\n\n        return link\n\n    def _mock_comment(self,\n                      id=1, author_id=1, link_id=1, sr_id=1, can_comment=True,\n                      can_view_promo=True, is_moderator=False, **kwargs):\n        kwargs['id'] = id\n        kwargs['author_id'] = author_id\n        kwargs['link_id'] = link_id\n        kwargs['sr_id'] = sr_id\n\n        comment = Comment(**kwargs)\n        self.autopatch(VByName, \"run\", return_value=comment)\n\n        link = Link(id=link_id, sr_id=sr_id)\n        self.autopatch(Link, \"_byID\", return_value=link)\n\n        sr = Subreddit(id=sr_id)\n        self.autopatch(Subreddit, \"_byID\", return_value=sr)\n        self.autopatch(Subreddit, \"can_comment\", return_value=can_comment)\n        self.autopatch(Link, \"can_view_promo\", return_value=can_view_promo)\n        self.autopatch(Subreddit, \"is_moderator\", return_value=is_moderator)\n\n        return comment\n\n    def test_no_fullname(self):\n        with self.assertRaises(HTTPForbidden):\n            self.validator.run('', None)\n\n        self.assertFalse(self.validator.has_errors)\n\n    def test_not_found(self):\n        with self.assertRaises(HTTPForbidden):\n            with patch.object(VByName, \"run\", return_value=None):\n                self.validator.run('fullname', None)\n\n        self.assertFalse(self.validator.has_errors)\n\n    def test_invalid_thing(self):\n        with self.assertRaises(HTTPForbidden):\n            sr = Subreddit(id=1)\n            with patch.object(VByName, \"run\", return_value=sr):\n                self.validator.run('fullname', None)\n\n        self.assertFalse(self.validator.has_errors)\n\n    def test_not_loggedin(self):\n        with self.assertRaises(HTTPForbidden):\n            c.user_is_loggedin = False\n\n            self._mock_comment()\n            self.validator.run('fullname', None)\n\n        self.assertFalse(self.validator.has_errors)\n\n    def test_blocked_user(self):\n        message = self._mock_message()\n        with patch.object(\n            Account, \"enemy_ids\", return_value=[message.author_id]\n        ):\n            result = self.validator.run('fullname', None)\n\n            self.assertEqual(result, message)\n            self.assertTrue(self.validator.has_errors)\n            self.assertIn((errors.USER_BLOCKED, None), c.errors)\n\n    def test_valid_message(self):\n        message = self._mock_message()\n        result = self.validator.run('fullname', None)\n\n        self.assertEqual(result, message)\n        self.assertFalse(self.validator.has_errors)\n\n    def test_valid_link(self):\n        link = self._mock_link()\n        result = self.validator.run('fullname', None)\n\n        self.assertEqual(result, link)\n        self.assertFalse(self.validator.has_errors)\n\n    def test_removed_link(self):\n        link = self._mock_link(_spam=True)\n        result = self.validator.run('fullname', None)\n\n        self.assertEqual(result, link)\n        self.assertFalse(self.validator.has_errors)\n\n    def test_archived_link(self):\n        link = self._mock_link(date=dt.now(g.tz).replace(year=2000))\n        result = self.validator.run('fullname', None)\n\n        self.assertEqual(result, link)\n        self.assertTrue(self.validator.has_errors)\n        self.assertIn((errors.TOO_OLD, None), c.errors)\n\n    def test_locked_link(self):\n        link = self._mock_link(locked=True)\n        with patch.object(Subreddit, \"can_distinguish\", return_value=False):\n            result = self.validator.run('fullname', None)\n\n            self.assertEqual(result, link)\n            self.assertTrue(self.validator.has_errors)\n            self.assertIn((errors.THREAD_LOCKED, None), c.errors)\n\n    def test_locked_link_mod_reply(self):\n        link = self._mock_link(locked=True)\n        with patch.object(Subreddit, \"can_distinguish\", return_value=True):\n            result = self.validator.run('fullname', None)\n\n            self.assertEqual(result, link)\n            self.assertFalse(self.validator.has_errors)\n\n    def test_invalid_link(self):\n        with self.assertRaises(HTTPForbidden):\n            self._mock_link(can_comment=False)\n            self.validator.run('fullname', None)\n\n        self.assertFalse(self.validator.has_errors)\n\n    def test_invalid_promo(self):\n        with self.assertRaises(HTTPForbidden):\n            self._mock_link(can_view_promo=False)\n            self.validator.run('fullname', None)\n\n        self.assertFalse(self.validator.has_errors)\n\n    def test_valid_comment(self):\n        comment = self._mock_comment()\n        result = self.validator.run('fullname', None)\n\n        self.assertEqual(result, comment)\n        self.assertFalse(self.validator.has_errors)\n\n    def test_deleted_comment(self):\n        comment = self._mock_comment(_deleted=True)\n        result = self.validator.run('fullname', None)\n\n        self.assertEqual(result, comment)\n        self.assertTrue(self.validator.has_errors)\n        self.assertIn((errors.DELETED_COMMENT, None), c.errors)\n\n    def test_removed_comment(self):\n        comment = self._mock_comment(_spam=True)\n        result = self.validator.run('fullname', None)\n\n        self.assertEqual(result, comment)\n        self.assertTrue(self.validator.has_errors)\n        self.assertIn((errors.DELETED_COMMENT, None), c.errors)\n\n    def test_removed_comment_self_reply(self):\n        comment = self._mock_comment(author_id=c.user._id, _spam=True)\n        result = self.validator.run('fullname', None)\n\n        self.assertEqual(result, comment)\n        self.assertFalse(self.validator.has_errors)\n\n    def test_removed_comment_mod_reply(self):\n        comment = self._mock_comment(_spam=True, is_moderator=True)\n        result = self.validator.run('fullname', None)\n\n        self.assertEqual(result, comment)\n        self.assertFalse(self.validator.has_errors)\n\n    def test_invalid_comment(self):\n        with self.assertRaises(HTTPForbidden):\n            comment = self._mock_comment(can_comment=False)\n            self.validator.run('fullname', None)\n\n        self.assertFalse(self.validator.has_errors)\n\n\nclass TestVSubredditName(ValidatorTests):\n    def setUp(self):\n        # Reset the validator state and errors before every test.\n        self.validator = VSubredditName(None)\n        c.errors = ErrorSet()\n\n    def _test_failure(self, input, error=errors.BAD_SR_NAME):\n        super(TestVSubredditName, self)._test_failure(input, error)\n\n    # Most of this validator's logic is already covered in `IsValidNameTest`.\n\n    def test_slash_r_slash(self):\n        result = self._test_success('/r/foo', assertEqual=False)\n        self.assertEqual(result, 'foo')\n\n    def test_r_slash(self):\n        result = self._test_success('r/foo', assertEqual=False)\n        self.assertEqual(result, 'foo')\n\n    def test_two_prefixes(self):\n        self._test_failure('/r/r/foo')\n\n    def test_slash_not_prefix(self):\n        self._test_failure('foo/r/')\n\n\nclass TestValidEmail(ValidatorTests):\n    \"\"\"Lightly test email address (\"addr-spec\") validation against RFC 2822.\n\n    http://www.faqs.org/rfcs/rfc2822.html\n    \"\"\"\n    def setUp(self):\n        # Reset the validator state and errors before every test.\n        self.validator = ValidEmail()\n        c.errors = ErrorSet()\n\n    def test_valid_emails(self):\n        self._test_success('test@example.com')\n        self._test_success('test@example.co.uk')\n        self._test_success('test+foo@example.com')\n\n    def _test_failure(self, email, error=errors.BAD_EMAIL):\n        super(TestValidEmail, self)._test_failure(email, error)\n\n    def test_blank_email(self):\n        self._test_failure('', errors.NO_EMAIL)\n        self.setUp()\n        self._test_failure(' ', errors.NO_EMAIL)\n\n    def test_no_whitespace(self):\n        self._test_failure('test @example.com')\n        self.setUp()\n        self._test_failure('test@ example.com')\n        self.setUp()\n        self._test_failure('test@example. com')\n        self.setUp()\n        self._test_failure(\"test@\\texample.com\")\n\n    def test_no_hostname(self):\n        self._test_failure('example')\n        self.setUp()\n        self._test_failure('example@')\n\n    def test_no_username(self):\n        self._test_failure('example.com')\n        self.setUp()\n        self._test_failure('@example.com')\n\n    def test_two_hostnames(self):\n        self._test_failure('test@example.com@example.com')\n"
  },
  {
    "path": "r2/r2/tests/unit/lib/validator/test_vverifypassword.py",
    "content": "#!/usr/bin/env python\n# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport uuid\nimport unittest\n\nfrom pylons import tmpl_context as c\nfrom webob.exc import HTTPException\n\n# Needs to be done before other r2 imports, since some code run on module import\n# expects a sane pylons env\nfrom r2.tests import RedditTestCase\n\nfrom r2.lib.db.thing import NotFound\nfrom r2.lib.errors import errors, ErrorSet, UserRequiredException\nfrom r2.lib.validator import VVerifyPassword\nfrom r2.models import Account, AccountExists, bcrypt_password\n\n\nclass TestVVerifyPassword(unittest.TestCase):\n    \"\"\"Test that only the current user's password satisfies VVerifyPassword\"\"\"\n    @classmethod\n    def setUpClass(cls):\n        # Create a dummy account for testing with; won't touch the database\n        # as long as we don't `._commit()`\n        name = \"unit_tester_%s\" % uuid.uuid4().hex\n        cls._password = uuid.uuid4().hex\n        cls._account = Account(\n            name=name,\n            password=bcrypt_password(cls._password)\n        )\n\n    def setUp(self):\n        c.user_is_loggedin = True\n        c.user = self._account\n\n    def _checkFails(self, password, fatal=False, error=errors.WRONG_PASSWORD):\n        # So we don't have any stale errors laying around\n        c.errors = ErrorSet()\n        validator = VVerifyPassword('dummy', fatal=fatal)\n\n        if fatal:\n            try:\n                validator.run(password)\n            except HTTPException:\n                return True\n            return False\n        else:\n            validator.run(password)\n\n            return validator.has_errors or c.errors.get((error, None))\n\n    def test_loggedout(self):\n        c.user = \"\"\n        c.user_is_loggedin = False\n        self.assertRaises(UserRequiredException, self._checkFails, \"dummy\")\n\n    def test_right_password(self):\n        self.assertFalse(self._checkFails(self._password, fatal=False))\n        self.assertFalse(self._checkFails(self._password, fatal=True))\n\n    def test_wrong_password(self):\n        bad_pass = \"~\" + self._password[1:]\n        self.assertTrue(self._checkFails(bad_pass, fatal=False))\n        self.assertTrue(self._checkFails(bad_pass, fatal=True))\n\n    def test_no_password(self):\n        self.assertTrue(self._checkFails(None, fatal=False))\n        self.assertTrue(self._checkFails(None, fatal=True))\n\n        self.assertTrue(self._checkFails(\"\", fatal=False))\n        self.assertTrue(self._checkFails(\"\", fatal=True))\n"
  },
  {
    "path": "r2/r2/tests/unit/models/__init__.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n"
  },
  {
    "path": "r2/r2/tests/unit/models/commentbuilder_test.py",
    "content": "#!/usr/bin/env python\n# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom collections import namedtuple, defaultdict\nfrom mock import MagicMock\n\nfrom r2.lib.utils.comment_tree_utils import get_tree_details, calc_num_children\nfrom r2.lib.db import operators\nfrom r2.models import builder\nfrom r2.models import Comment\nfrom r2.models.builder import CommentBuilder\nfrom r2.models.comment_tree import CommentTree\nfrom r2.tests import RedditTestCase\n\n\nCommentTreeElement = namedtuple(\n    \"CommentTreeElement\", [\"id\", \"score\", \"children\"])\n\n\nTREE = [\n    CommentTreeElement(id=100, score=100, children=[\n        CommentTreeElement(id=101, score=90, children=[]),\n        CommentTreeElement(id=102, score=80, children=[\n            CommentTreeElement(id=104, score=95, children=[]),\n            CommentTreeElement(id=105, score=85, children=[]),\n            CommentTreeElement(id=106, score=75, children = []),\n        ]),\n        CommentTreeElement(id=103, score=70, children=[]),\n    ]),\n    CommentTreeElement(id=107, score=60, children=[]),\n    CommentTreeElement(id=108, score=55, children=[\n        CommentTreeElement(id=110, score=110, children=[]),\n    ]),\n    CommentTreeElement(id=109, score=50, children=[]),\n]\n\n\nDISTINGUISHES = {\n    104: \"yes\",\n}\n\nAUTHOR_IDS = {\n    100: \"a\",\n    101: \"b\",\n    102: \"c\",\n    103: \"d\",\n    104: \"e\",\n    105: \"f\",\n    106: \"g\",\n    107: \"h\",\n    108: \"i\",\n    109: \"j\",\n    110: \"k\",\n}\n\n\ndef make_comment_tree(link):\n    tree = {}\n\n    def _add_comment(comment, parent):\n        tree[comment.id] = [child.id for child in comment.children]\n        for child in comment.children:\n            _add_comment(child, parent=comment)\n\n    tree[None] = [comment.id for comment in TREE]\n\n    for comment in TREE:\n        _add_comment(comment, parent=None)\n\n    cids, depth, parents = get_tree_details(tree)\n    num_children = calc_num_children(tree)\n    num_children = defaultdict(int, num_children)\n\n    return CommentTree(link, cids, tree, depth, parents, num_children)\n\n\ndef make_comment_scores():\n    scores_by_id = {}\n\n    def _add_comment(comment):\n        scores_by_id[comment.id] = comment.score\n        for child in comment.children:\n            _add_comment(child)\n\n    for comment in TREE:\n        _add_comment(comment)\n\n    return scores_by_id\n\n\nFakeComment = namedtuple(\n    \"Comment\", [\"parent_id\", \"author_id\", \"distinguished\"])\n\ndef comments_by_id():\n    comment_tree = make_comment_tree(None)\n    ret = {}\n\n    for comment_id in comment_tree.cids:\n        parent_id = comment_tree.parents[comment_id]\n        author_id = AUTHOR_IDS[comment_id]\n        distinguished = DISTINGUISHES.get(comment_id, \"no\")\n        ret[comment_id] = FakeComment(parent_id, author_id, distinguished)\n\n    return ret\n\n\nclass CommentOrderTest(RedditTestCase):\n    def setUp(self):\n        self.link = MagicMock()\n        self.link._id = 1000\n        self.link.sticky_comment_id = None\n        self.link.precomputed_sorts = None\n\n        comment_scores = make_comment_scores()\n        self.autopatch(\n            builder, \"get_comment_scores\", return_value=comment_scores)\n\n        comment_tree_for_link = make_comment_tree(self.link)\n        self.autopatch(\n            CommentTree, \"by_link\", return_value=comment_tree_for_link)\n\n        fake_comments = comments_by_id()\n        self.autopatch(\n            Comment, \"_byID\", return_value=fake_comments)\n\n    def tearDown(self):\n        self.link = None\n\n    def test_comment_order_full(self):\n        sort = operators.desc(\"_confidence\")\n        builder = CommentBuilder(self.link, sort, num=1500)\n        builder.load_comment_order()\n        comment_order = [\n            comment_tuple.comment_id\n            for comment_tuple in builder.ordered_comment_tuples\n        ]\n        self.assertEqual(comment_order,\n            [100, 101, 102, 104, 105, 106, 103, 107, 108, 110, 109])\n        self.assertEqual(builder.missing_root_comments, set())\n        self.assertEqual(builder.missing_root_count, 0)\n\n    def test_comment_order_full_asc(self):\n        sort = operators.asc(\"_confidence\")\n        builder = CommentBuilder(self.link, sort, num=1500)\n        builder.load_comment_order()\n        comment_order = [\n            comment_tuple.comment_id\n            for comment_tuple in builder.ordered_comment_tuples\n        ]\n        self.assertEqual(comment_order,\n            [109, 108, 107, 100, 103, 102, 106, 105, 101, 104, 110])\n        self.assertEqual(builder.missing_root_comments, set())\n        self.assertEqual(builder.missing_root_count, 0)\n\n    def test_comment_order_limit(self):\n        sort = operators.desc(\"_confidence\")\n        builder = CommentBuilder(self.link, sort, num=5)\n        builder.load_comment_order()\n        comment_order = [\n            comment_tuple.comment_id\n            for comment_tuple in builder.ordered_comment_tuples\n        ]\n        self.assertEqual(comment_order, [100, 101, 102, 104, 105])\n        self.assertEqual(builder.missing_root_comments, {107, 108, 109})\n        self.assertEqual(builder.missing_root_count, 4)\n\n    def test_comment_order_depth(self):\n        sort = operators.desc(\"_confidence\")\n        builder = CommentBuilder(self.link, sort, num=1500, max_depth=1)\n        builder.load_comment_order()\n        comment_order = [\n            comment_tuple.comment_id\n            for comment_tuple in builder.ordered_comment_tuples\n        ]\n        self.assertEqual(comment_order, [100, 107, 108, 109])\n        self.assertEqual(builder.missing_root_comments, set())\n        self.assertEqual(builder.missing_root_count, 0)\n\n    def test_comment_order_sticky(self):\n        self.link.sticky_comment_id = 100\n        sort = operators.desc(\"_confidence\")\n        builder = CommentBuilder(self.link, sort, num=1500)\n        builder.load_comment_order()\n        comment_order = [\n            comment_tuple.comment_id\n            for comment_tuple in builder.ordered_comment_tuples\n        ]\n        self.assertEqual(comment_order, [100, 107, 108, 110, 109])\n        self.assertEqual(builder.missing_root_comments, set())\n        self.assertEqual(builder.missing_root_count, 0)\n\n    def test_comment_order_invalid_sticky(self):\n        self.link.sticky_comment_id = 101\n        sort = operators.desc(\"_confidence\")\n        builder = CommentBuilder(self.link, sort, num=1500)\n        builder.load_comment_order()\n        comment_order = [\n            comment_tuple.comment_id\n            for comment_tuple in builder.ordered_comment_tuples\n        ]\n        self.assertEqual(comment_order,\n            [100, 101, 102, 104, 105, 106, 103, 107, 108, 110, 109])\n        self.assertEqual(builder.missing_root_comments, set())\n        self.assertEqual(builder.missing_root_count, 0)\n\n    def test_comment_order_permalink(self):\n        sort = operators.desc(\"_confidence\")\n        comment = MagicMock()\n        comment._id = 100\n        builder = CommentBuilder(self.link, sort, comment=comment, num=1500)\n        builder.load_comment_order()\n        comment_order = [\n            comment_tuple.comment_id\n            for comment_tuple in builder.ordered_comment_tuples\n        ]\n        self.assertEqual(comment_order, [100, 101, 102, 104, 105, 106, 103])\n        self.assertEqual(builder.missing_root_comments, set())\n        self.assertEqual(builder.missing_root_count, 0)\n\n    def test_comment_order_permalink_context(self):\n        sort = operators.desc(\"_confidence\")\n        comment = MagicMock()\n        comment._id = 104\n        builder = CommentBuilder(\n            self.link, sort, comment=comment, context=3, num=1500)\n        builder.load_comment_order()\n        comment_order = [\n            comment_tuple.comment_id\n            for comment_tuple in builder.ordered_comment_tuples\n        ]\n        self.assertEqual(comment_order, [100, 102, 104])\n        self.assertEqual(builder.missing_root_comments, set())\n        self.assertEqual(builder.missing_root_count, 0)\n\n    def test_comment_order_invalid_permalink_defocus(self):\n        sort = operators.desc(\"_confidence\")\n        comment = MagicMock()\n        comment._id = 999999\n        builder = CommentBuilder(self.link, sort, comment=comment, num=1500)\n        builder.load_comment_order()\n        comment_order = [\n            comment_tuple.comment_id\n            for comment_tuple in builder.ordered_comment_tuples\n        ]\n        self.assertEqual(comment_order,\n            [100, 101, 102, 104, 105, 106, 103, 107, 108, 110, 109])\n        self.assertEqual(builder.missing_root_comments, set())\n        self.assertEqual(builder.missing_root_count, 0)\n\n    def test_comment_order_children(self):\n        sort = operators.desc(\"_confidence\")\n        builder = CommentBuilder(\n            self.link, sort, children=[101, 102, 103], num=1500)\n        builder.load_comment_order()\n        comment_order = [\n            comment_tuple.comment_id\n            for comment_tuple in builder.ordered_comment_tuples\n        ]\n        self.assertEqual(comment_order, [101, 102, 104, 105, 106, 103])\n        self.assertEqual(builder.missing_root_comments, set())\n        self.assertEqual(builder.missing_root_count, 0)\n\n    def test_comment_order_children_limit(self):\n        sort = operators.desc(\"_confidence\")\n        builder = CommentBuilder(\n            self.link, sort, children=[107, 108, 109], num=3)\n        builder.load_comment_order()\n        comment_order = [\n            comment_tuple.comment_id\n            for comment_tuple in builder.ordered_comment_tuples\n        ]\n        self.assertEqual(comment_order, [107, 108, 110])\n        self.assertEqual(builder.missing_root_comments, {109})\n        self.assertEqual(builder.missing_root_count, 1)\n\n    def test_comment_order_children_limit_bug(self):\n        sort = operators.desc(\"_confidence\")\n        builder = CommentBuilder(\n            self.link, sort, children=[101, 102, 103], num=3)\n        builder.load_comment_order()\n        comment_order = [\n            comment_tuple.comment_id\n            for comment_tuple in builder.ordered_comment_tuples\n        ]\n        self.assertEqual(comment_order, [101, 102, 104])\n        # missing_root_comments SHOULD be {103}, but there's a bug here.\n        # if the requested children are not root level but we don't show some\n        # of them we should add a MoreChildren to allow a subsequent request\n        # to get the missing comments.\n        self.assertEqual(builder.missing_root_comments, set())\n        self.assertEqual(builder.missing_root_count, 0)\n\n    def test_comment_order_qa(self):\n        self.link.responder_ids = (\"c\",)\n        sort = operators.desc(\"_qa\")\n        builder = CommentBuilder(self.link, sort, num=1500)\n        builder.load_comment_order()\n        comment_order = [\n            comment_tuple.comment_id\n            for comment_tuple in builder.ordered_comment_tuples\n        ]\n        self.assertEqual(comment_order,\n            [100, 102, 104, 105, 106, 107, 108, 109])\n        self.assertEqual(builder.missing_root_comments, set())\n        self.assertEqual(builder.missing_root_count, 0)\n\n    def test_comment_order_qa_multiple_responders(self):\n        self.link.responder_ids = (\"c\", \"d\", \"e\")\n        sort = operators.desc(\"_qa\")\n        builder = CommentBuilder(self.link, sort, num=1500)\n        builder.load_comment_order()\n        comment_order = [\n            comment_tuple.comment_id\n            for comment_tuple in builder.ordered_comment_tuples\n        ]\n        self.assertEqual(comment_order,\n            [100, 102, 104, 105, 106, 103, 107, 108, 109])\n        self.assertEqual(builder.missing_root_comments, set())\n        self.assertEqual(builder.missing_root_count, 0)\n"
  },
  {
    "path": "r2/r2/tests/unit/models/link_test.py",
    "content": "#!/usr/bin/env python\n# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport unittest\n\nfrom mock import MagicMock, patch\n\nfrom r2.models.link import Link, Comment\n\nTINY_COMMENT = 'rekt'\nSHORT_COMMENT = 'What is your favorite car from a rival brand?'\nMEDIUM_COMMENT = '''I'm humbled by how many of you were interested in talking\nto me and hearing about what we're doing with autonomous driving. When I signed\noff last night, I never thought there would be so many more questions than what\nI was able to answer after I left. I wish I had had more time last night, but\nI'm answering more questions today.'''\nLONG_COMMENT = '''That was a really good question then and still is and gets\nback to something I said earlier about the difference between autonomous drive\nvehicles, in which the driver remains in control, and self-driving cars, in\nwhich a driver is not even required.\nThe focus of our R&D right now is on autonomous drive vehicles that enhance the\ndriving experience, by giving the driver the option of letting the car handle\nsome functions and by improving the car's ability to avoid accidents. That\ntechnology exists already and will be rolled out in phases over the next\nseveral years.\nWe are not dismissive of self-driving vehicles, but the reality is the timeline\nfor them is much further out into the future.'''\n\nclass CommentMock(Comment):\n    \"\"\"This class exists to allow us to call the _qa() method on Comments\n    without having to dick around with everything else they support.\"\"\"\n    _nodb = True\n\n    def __init__(self, ups=1, downs=0, body=TINY_COMMENT, author_id=None):\n        self._ups = ups\n        self._downs = downs\n        self.body = body\n        self.author_id = author_id\n\n    def __setattr__(self, attr, val):\n        self.__dict__[attr] = val\n\n    def __getattr__(self, attr):\n        return self.attr\n\n\nclass TestCommentQaSort(unittest.TestCase):\n    def test_simple_upvotes(self):\n        \"\"\"All else equal, do upvoted comments score better?\"\"\"\n        no_upvotes = CommentMock(ups=1)\n        one_upvote = CommentMock(ups=2)\n        many_upvotes = CommentMock(ups=50)\n        no_upvotes_score = no_upvotes._qa((), ())\n        one_upvote_score = one_upvote._qa((), ())\n        many_upvotes_score = many_upvotes._qa((), ())\n\n        self.assertLess(no_upvotes_score, one_upvote_score)\n        self.assertLess(one_upvote_score, many_upvotes_score)\n\n    def test_simple_downvotes(self):\n        \"\"\"All else equal, do downvoted comments score worse?\"\"\"\n        no_downvotes = CommentMock(downs=0)\n        one_downvote = CommentMock(downs=1)\n        many_downvotes = CommentMock(downs=50)\n        no_downvotes_score = no_downvotes._qa((), ())\n        one_downvote_score = one_downvote._qa((), ())\n        many_downvotes_score = many_downvotes._qa((), ())\n\n        self.assertGreater(no_downvotes_score, one_downvote_score)\n        self.assertGreater(one_downvote_score, many_downvotes_score)\n\n    def test_simple_length(self):\n        \"\"\"All else equal, do longer comments score better?\"\"\"\n        tiny = CommentMock(body=TINY_COMMENT)\n        short = CommentMock(body=SHORT_COMMENT)\n        medium = CommentMock(body=MEDIUM_COMMENT)\n        long = CommentMock(body=LONG_COMMENT)\n        tiny_score = tiny._qa((), ())\n        short_score = short._qa((), ())\n        medium_score = medium._qa((), ())\n        long_score = long._qa((), ())\n\n        self.assertLess(tiny_score, short_score)\n        self.assertLess(short_score, medium_score)\n        self.assertLess(medium_score, long_score)\n\n    def test_simple_op_responses(self):\n        \"\"\"All else equal, do OP answers bump up the score of comments?\"\"\"\n        question = CommentMock()\n        answer = CommentMock(author_id=1)\n        no_answer_score = question._qa((), ())\n        no_op_answer_score = question._qa((answer,), (2,))\n        with_op_answer_score = question._qa((answer,), (1,))\n\n        self.assertEqual(no_answer_score, no_op_answer_score)\n        self.assertLess(no_op_answer_score, with_op_answer_score)\n\n    def test_multiple_op_responses(self):\n        \"\"\"What effect do multiple OP responses have on a comment's score?\"\"\"\n        question = CommentMock()\n        op_answer = CommentMock(author_id=1)\n        another_op_answer = CommentMock(author_id=1)\n        one_answer_score = question._qa((op_answer,), (1,))\n        two_answers_score = question._qa((op_answer, another_op_answer), (1,))\n\n        self.assertEqual(one_answer_score, two_answers_score)\n\n        bad_op_answer = CommentMock(ups=0, author_id=1)\n        good_op_answer = CommentMock(ups=30, author_id=1)\n        good_op_answer_score = question._qa((good_op_answer,), (1,))\n        op_answers = (bad_op_answer, good_op_answer, op_answer)\n        three_op_answers_score = question._qa(op_answers, (1,))\n\n        # Are we basing the score on the highest-scoring OP answer?\n        self.assertEqual(good_op_answer_score, three_op_answers_score)\n\n    def test_simple_op_comments(self):\n        \"\"\"All else equal, do comments from OP score better?\"\"\"\n        comment = CommentMock(author_id=1)\n        op_score = comment._qa((), (1,))\n        non_op_score = comment._qa((), (2,))\n\n        self.assertLess(non_op_score, op_score)\n\n\nID = 0\ndef _mock_id(instance):\n    if not getattr(instance, \"__id\", False):\n        instance.__id = ID + 1\n    return instance.__id\n\n\nclass LinkMock(Link):\n    _nodb = True\n\n    def __init__(self, **kwargs):\n        for key, value in kwargs.iteritems():\n            setattr(self, key, value)\n\n    def __setattr__(self, attr, val):\n        self.__dict__[attr] = val\n\n    def __getattr__(self, attr):\n        return getattr(\n            self.__dict__,\n            attr,\n            getattr(self._defaults, attr, None),\n        )\n\n    @property\n    def _id(self):\n        return _mock_id(self)\n\n    @property\n    def subreddit_slow(self):\n        return SubredditMock()\n\n    def _commit(self):\n        pass\n\n    @classmethod\n    @patch('r2.lib.voting.cast_vote')\n    def _submit(cls, cast_vote, *args, **kwargs):\n        \"\"\"A _submit that mocks calls we don't care about testing.\"\"\"\n        return super(LinkMock, cls)._submit(*args, **kwargs)\n\n\nclass ThingMock():\n    _nodb = True\n\n    @property\n    def _id(self):\n        return _mock_id(self)\n\n\nclass AccountMock(ThingMock):\n    @property\n    def _spam(self):\n        return False\n\n    def _commit(self, *a, **kw):\n        pass\n\n\nclass SubredditMock(ThingMock):\n    @property\n    def lang(self):\n        return \"en\"\n\n    @property\n    def name(self):\n        return \"linktests\"\n\n    @property\n    def spam_selfposts(self):\n        return \"high\"\n\n    @property\n    def spam_links(self):\n        return \"low\"\n\n\nclass TestSubmit(unittest.TestCase):\n    def setUp(self):\n        from r2.models import (\n            LinksByAccount,\n            LinksByUrlAndSubreddit,\n            SubredditParticipationByAccount,\n            SubredditsActiveForFrontPage,\n        )\n\n        LinksByAccount.add_link = MagicMock()\n        SubredditParticipationByAccount.mark_participated = MagicMock()\n        SubredditsActiveForFrontPage.mark_new_post = MagicMock()\n\n        self.links_by_url_add_link = MagicMock()\n        LinksByUrlAndSubreddit.add_link = self.links_by_url_add_link\n        self.links_by_url_remove_link = MagicMock()\n        LinksByUrlAndSubreddit.remove_link = self.links_by_url_remove_link\n\n    def test_new_self_post_has_url(self):\n        l = LinkMock._submit(\n            is_self=True,\n            content=\"this is a self post\",\n            title=\"test post\",\n            ip=\"127.0.0.1\",\n            sr=SubredditMock(),\n            author=AccountMock()\n        )\n\n        self.assertEqual(l.url, u\"/r/linktests/comments/%s/test_post/\" % l._id36)\n\n    def test_new_self_post_doesnt_modify_links_by_url(self):\n        l = LinkMock._submit(\n            is_self=True,\n            content=\"this is a self post\",\n            title=\"test post\",\n            ip=\"127.0.0.1\",\n            sr=SubredditMock(),\n            author=AccountMock()\n        )\n\n        self.assertEqual(self.links_by_url_add_link.call_count, 0)\n        self.assertEqual(self.links_by_url_remove_link.call_count, 0)\n\n    def test_changing_non_promo_fails(self):\n        l = LinkMock._submit(\n            is_self=True,\n            content=\"this is a self post\",\n            title=\"test post\",\n            ip=\"127.0.0.1\",\n            sr=SubredditMock(),\n            author=AccountMock()\n        )\n\n        with self.assertRaises(ValueError):\n            l.set_content(False, \"http://test.com/1\")\n\n    def test_changing_from_self_doesnt_remove_links_by_url(self):\n        l = LinkMock._submit(\n            is_self=True,\n            content=\"this is a self post\",\n            title=\"test post\",\n            ip=\"127.0.0.1\",\n            sr=SubredditMock(),\n            author=AccountMock()\n        )\n        l.promoted = True\n\n        url = \"http://test.com/1\"\n\n        self.assertEqual(self.links_by_url_add_link.call_count, 0)\n        self.assertEqual(self.links_by_url_remove_link.call_count, 0)\n\n        l.set_content(False, url)\n\n        self.assertEqual(l.url, url)\n        self.assertEqual(self.links_by_url_remove_link.call_count, 0)\n\n    def test_changing_url_adds_links_by_url(self):\n        url1 = \"http://test.com/1\"\n        url2 = \"http://test.com/2\"\n        l = LinkMock._submit(\n            is_self=False,\n            content=url1,\n            title=\"test post\",\n            ip=\"127.0.0.1\",\n            sr=SubredditMock(),\n            author=AccountMock()\n        )\n        l.promoted = True\n\n        self.assertEqual(self.links_by_url_add_link.call_count, 1)\n        self.assertEqual(self.links_by_url_remove_link.call_count, 0)\n\n        l.set_content(False, url2)\n\n        self.assertEqual(self.links_by_url_add_link.call_count, 2)\n        self.assertEqual(self.links_by_url_remove_link.call_count, 1)\n\n    def test_changing_to_self_removes_links_by_url(self):\n        url = \"http://test.com/1\"\n        l = LinkMock._submit(\n            is_self=False,\n            content=url,\n            title=\"test post\",\n            ip=\"127.0.0.1\",\n            sr=SubredditMock(),\n            author=AccountMock()\n        )\n        l.promoted = True\n\n        self.assertEqual(self.links_by_url_add_link.call_count, 1)\n\n        l.set_content(True, \"change to self post\")\n\n        self.assertEqual(self.links_by_url_remove_link.call_count, 1)\n"
  },
  {
    "path": "r2/r2/tests/unit/models/promo_test.py",
    "content": "#!/usr/bin/env python\n# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom random import shuffle\nfrom mock import MagicMock, Mock\n\nfrom r2.models import Collection, CollectionStorage\nfrom r2.tests import RedditTestCase\n\n\nclass CollectionStorageTest(RedditTestCase):\n\n    def setUp(self):\n        self.name = 'fake name'\n\n    def test_set_attributes(self):\n        \"\"\"Assert that _set_attributes properly handles invalid attributes\"\"\"\n        attribute_error_message = 'No attribute on %s called %s'\n        invalid_attribute = {'invalid_attribute': None}\n        valid_attribute = {'is_spotlight': None}\n\n        collection = Collection(name=self.name, sr_names=[])\n\n        Collection.by_name = Mock()\n        Collection.by_name.return_value = collection\n\n        # Assert that a bad attribute will raise NotFoundException\n        with self.assertRaises(AttributeError) as e:\n            CollectionStorage._set_attributes(self.name, invalid_attribute)\n        self.assertEqual(e.exception.message, attribute_error_message %\n            (self.name, invalid_attribute.keys()[0]))\n\n        # Should throw even if there's a bad attribute AND valid attribute\n        with self.assertRaises(AttributeError) as e:\n            CollectionStorage._set_attributes(self.name,\n            dict(invalid_attribute, **valid_attribute))\n        self.assertEqual(e.exception.message, attribute_error_message %\n            (self.name, invalid_attribute.keys()[0]))\n\n        CollectionStorage._set_values = MagicMock()\n        CollectionStorage._set_attributes(self.name,\n            valid_attribute)\n\n        # Assert that no exception thrown if valid attributes passed\n        CollectionStorage._set_values.assert_called_once_with(self.name,\n            valid_attribute)\n\n    def test_set_over_18(self):\n        \"\"\"Assert that set_over_18 invokes _set_attributes\"\"\"\n        CollectionStorage._set_attributes = MagicMock()\n        CollectionStorage.set_over_18(self.name, True)\n\n        CollectionStorage._set_attributes.assert_called_once_with(self.name,\n            {'over_18': 'True'})\n\n    def test_set_is_spotlight(self):\n        \"\"\"Assert that set_is_spotlight invokes _set_attributes\"\"\"\n        CollectionStorage._set_attributes = MagicMock()\n        CollectionStorage.set_is_spotlight(self.name, True)\n\n        CollectionStorage._set_attributes.assert_called_once_with(self.name,\n            {'is_spotlight': 'True'})\n\n\nclass CollectionTest(RedditTestCase):\n\n    def test_is_spotlight_default(self):\n        \"\"\"Assert that is_spotlight defaults to False\"\"\"\n        collection = Collection(name='fake name', sr_names=[])\n        self.assertFalse(collection.is_spotlight)\n\n        setattr(collection, 'is_spotlight', True)\n        self.assertTrue(collection.is_spotlight)\n\n\nclass CollectionOrderTest(RedditTestCase):\n    \"\"\"\n    Assert that Collection.get_all() sorts in the following sequence:\n    1. SFW/NSFW\n    2. Spotlighted\n    3. Alphabetical\n    \"\"\"\n\n    def setUp(self):\n        # Setup all collections\n        self.spotlight_a = Collection(name='spotlight_a', sr_names=[],\n            is_spotlight=True)\n        self.spotlight_z = Collection(name='spotlight_z', sr_names=[],\n            is_spotlight=True)\n        self.sfw_a = Collection(name='sfw_a', sr_names=[])\n        self.sfw_b = Collection(name='sfw_B', sr_names=[])\n        self.sfw_z = Collection(name='sfw_z', sr_names=[])\n        self.nsfw_spotlight = Collection(name='nsfw_spotlight', sr_names=[],\n            over_18=True, is_spotlight=True)\n        self.nsfw_non_spotlight = Collection(name='nsfw_non_spotlight',\n            sr_names=[], over_18=True)\n\n        self.correct_order = [\n            'spotlight_a',\n            'spotlight_z',\n            'sfw_a',\n            'sfw_B',\n            'sfw_z',\n            'nsfw_spotlight',\n            'nsfw_non_spotlight',\n        ]\n\n        # Mock the get_all method on CollectionStorage,\n        # which returns an unordered list of collections\n        CollectionStorage.get_all = MagicMock()\n\n    def _assert_scenario(self, unordered_collections):\n        CollectionStorage.get_all.return_value = unordered_collections\n        self.assertEqual(\n            [collection.name for collection in Collection.get_all()],\n            self.correct_order)\n\n    def test_scenario_reversed(self):\n        \"\"\"Assert that reversed order will order correctly\"\"\"\n        unordered_collections = [\n            self.nsfw_spotlight,\n            self.nsfw_non_spotlight,\n            self.sfw_z,\n            self.sfw_b,\n            self.sfw_a,\n            self.spotlight_z,\n            self.spotlight_a,\n        ]\n        self._assert_scenario(unordered_collections)\n\n    def test_scenario_semi_sorted(self):\n        \"\"\"\n        Assert that SFW and spotlight sorted list that is\n        unordered alphabetically will order correctly\n        \"\"\"\n        unordered_collections = [\n            self.spotlight_z,\n            self.spotlight_a,\n            self.sfw_z,\n            self.sfw_b,\n            self.sfw_a,\n            self.nsfw_spotlight,\n            self.nsfw_non_spotlight,\n        ]\n        self._assert_scenario(unordered_collections)\n\n    def test_scenario_random(self):\n        \"\"\"Assert that totally random list will order correctly\"\"\"\n        unordered_collections = [\n            self.sfw_z,\n            self.nsfw_non_spotlight,\n            self.sfw_a,\n            self.spotlight_a,\n            self.nsfw_spotlight,\n            self.spotlight_z,\n            self.sfw_b,\n        ]\n        self._assert_scenario(unordered_collections)\n\n    def test_scenario_casing(self):\n        \"\"\"Assert that ordering is case-insensitive\"\"\"\n        unordered_collections = [\n            self.sfw_b,\n            self.sfw_a,\n            self.sfw_z,\n            self.spotlight_a,\n            self.spotlight_z,\n            self.nsfw_spotlight,\n            self.nsfw_non_spotlight,\n        ]\n        self._assert_scenario(unordered_collections)\n"
  },
  {
    "path": "r2/r2/tests/unit/models/subreddit_test.py",
    "content": "#!/usr/bin/env python\n# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport unittest\n\nfrom mock import MagicMock\nfrom pylons import app_globals as g\n\nfrom r2.lib.permissions import PermissionSet\n\nfrom r2.models import NotFound\nfrom r2.models.account import Account\nfrom r2.models.subreddit import SRMember, Subreddit\n\nclass TestPermissionSet(PermissionSet):\n    info = dict(x={}, y={})\n\n\nclass SRMemberTest(unittest.TestCase):\n    def setUp(self):\n        a = Account()\n        a._id = 1\n        sr = Subreddit()\n        sr._id = 2\n        self.rel = SRMember(sr, a, 'test')\n\n    def test_get_permissions(self):\n        self.assertRaises(NotImplementedError, self.rel.get_permissions)\n        self.rel._permission_class = TestPermissionSet\n        self.assertEquals('', self.rel.get_permissions().dumps())\n        self.rel.encoded_permissions = '+x,-y'\n        self.assertEquals('+x,-y', self.rel.get_permissions().dumps())\n\n    def test_has_permission(self):\n        self.assertRaises(NotImplementedError, self.rel.has_permission, 'x')\n        self.rel._permission_class = TestPermissionSet\n        self.assertFalse(self.rel.has_permission('x'))\n        self.rel.encoded_permissions = '+x,-y'\n        self.assertTrue(self.rel.has_permission('x'))\n        self.assertFalse(self.rel.has_permission('y'))\n        self.rel.encoded_permissions = '+all'\n        self.assertTrue(self.rel.has_permission('x'))\n        self.assertTrue(self.rel.has_permission('y'))\n        self.assertFalse(self.rel.has_permission('z'))\n\n    def test_update_permissions(self):\n        self.assertRaises(NotImplementedError,\n                          self.rel.update_permissions, x=True)\n        self.rel._permission_class = TestPermissionSet\n        self.rel.update_permissions(x=True, y=False)\n        self.assertEquals('+x,-y', self.rel.encoded_permissions)\n        self.rel.update_permissions(x=None)\n        self.assertEquals('-y', self.rel.encoded_permissions)\n        self.rel.update_permissions(y=None, z=None)\n        self.assertEquals('', self.rel.encoded_permissions)\n        self.rel.update_permissions(x=True, y=False, all=True)\n        self.assertEquals('+all', self.rel.encoded_permissions)\n\n    def test_set_permissions(self):\n        self.rel.set_permissions(PermissionSet(x=True, y=False))\n        self.assertEquals('+x,-y', self.rel.encoded_permissions)\n\n    def test_is_superuser(self):\n        self.assertRaises(NotImplementedError, self.rel.is_superuser)\n        self.rel._permission_class = TestPermissionSet\n        self.assertFalse(self.rel.is_superuser())\n        self.rel.encoded_permissions = '+all'\n        self.assertTrue(self.rel.is_superuser())\n\n\nclass IsValidNameTest(unittest.TestCase):\n    def test_empty(self):\n        self.assertFalse(Subreddit.is_valid_name(None))\n\n    def test_short(self):\n        self.assertTrue(Subreddit.is_valid_name('aaa'))\n\n    def test_too_short(self):\n        self.assertFalse(Subreddit.is_valid_name('aa'))\n\n    def test_long(self):\n        self.assertTrue(Subreddit.is_valid_name('aaaaaaaaaaaaaaaaaaaaa'))\n\n    def test_too_long(self):\n        self.assertFalse(Subreddit.is_valid_name('aaaaaaaaaaaaaaaaaaaaaa'))\n\n    def test_underscore(self):\n        self.assertTrue(Subreddit.is_valid_name('a_a'))\n\n    def test_leading_underscore(self):\n        self.assertFalse(Subreddit.is_valid_name('_aa'))\n\n    def test_capitals(self):\n        self.assertTrue(Subreddit.is_valid_name('AZA'))\n\n    def test_numerics(self):\n        self.assertTrue(Subreddit.is_valid_name('090'))\n\n\nclass ByNameTest(unittest.TestCase):\n    def setUp(self):\n        self.cache = MagicMock()\n        g.gencache = self.cache\n\n        self.subreddit_byID = MagicMock()\n        Subreddit._byID = self.subreddit_byID\n\n        self.subreddit_query = MagicMock()\n        Subreddit._query = self.subreddit_query\n\n    def testSingleCached(self):\n        subreddit = Subreddit(id=1, name=\"exists\")\n        self.cache.get_multi.return_value = {\"exists\": subreddit._id}\n        self.subreddit_byID.return_value = [subreddit]\n\n        ret = Subreddit._by_name(\"exists\")\n\n        self.assertEqual(ret, subreddit)\n        self.assertEqual(self.subreddit_query.call_count, 0)\n\n    def testSingleFromDB(self):\n        subreddit = Subreddit(id=1, name=\"exists\")\n        self.cache.get_multi.return_value = {}\n        self.subreddit_query.return_value = [subreddit]\n        self.subreddit_byID.return_value = [subreddit]\n\n        ret = Subreddit._by_name(\"exists\")\n\n        self.assertEqual(ret, subreddit)\n        self.assertEqual(self.cache.set_multi.call_count, 1)\n\n    def testSingleNotFound(self):\n        self.cache.get_multi.return_value = {}\n        self.subreddit_query.return_value = []\n\n        with self.assertRaises(NotFound):\n            Subreddit._by_name(\"doesnotexist\")\n\n    def testSingleInvalid(self):\n        with self.assertRaises(NotFound):\n            Subreddit._by_name(\"_illegalunderscore\")\n\n        self.assertEqual(self.cache.get_multi.call_count, 0)\n        self.assertEqual(self.subreddit_query.call_count, 0)\n\n    def testMultiCached(self):\n        srs = [\n            Subreddit(id=1, name=\"exists\"),\n            Subreddit(id=2, name=\"also\"),\n        ]\n        self.cache.get_multi.return_value = {sr.name: sr._id for sr in srs}\n        self.subreddit_byID.return_value = srs\n\n        ret = Subreddit._by_name([\"exists\", \"also\"])\n\n        self.assertEqual(ret, {sr.name: sr for sr in srs})\n        self.assertEqual(self.subreddit_query.call_count, 0)\n\n    def testMultiCacheMissesAllExist(self):\n        srs = [\n            Subreddit(id=1, name=\"exists\"),\n            Subreddit(id=2, name=\"also\"),\n        ]\n\n        self.cache.get_multi.return_value = {}\n        self.subreddit_query.return_value = srs\n        self.subreddit_byID.return_value = srs\n\n        ret = Subreddit._by_name([\"exists\", \"also\"])\n\n        self.assertEqual(ret, {sr.name: sr for sr in srs})\n        self.assertEqual(self.cache.get_multi.call_count, 1)\n        self.assertEqual(self.subreddit_query.call_count, 1)\n\n    def testMultiSomeDontExist(self):\n        sr = Subreddit(id=1, name=\"exists\")\n        self.cache.get_multi.return_value = {sr.name: sr._id}\n        self.subreddit_query.return_value = []\n        self.subreddit_byID.return_value = [sr]\n\n        ret = Subreddit._by_name([\"exists\", \"doesnt\"])\n\n        self.assertEqual(ret, {sr.name: sr})\n        self.assertEqual(self.cache.get_multi.call_count, 1)\n        self.assertEqual(self.subreddit_query.call_count, 1)\n\n    def testMultiSomeInvalid(self):\n        sr = Subreddit(id=1, name=\"exists\")\n        self.cache.get_multi.return_value = {sr.name: sr._id}\n        self.subreddit_query.return_value = []\n        self.subreddit_byID.return_value = [sr]\n\n        ret = Subreddit._by_name([\"exists\", \"_illegalunderscore\"])\n\n        self.assertEqual(ret, {sr.name: sr})\n        self.assertEqual(self.cache.get_multi.call_count, 1)\n        self.assertEqual(self.subreddit_query.call_count, 0)\n\n    def testForceUpdate(self):\n        sr = Subreddit(id=1, name=\"exists\")\n        self.cache.get_multi.return_value = {sr.name: sr._id}\n        self.subreddit_query.return_value = [sr]\n        self.subreddit_byID.return_value = [sr]\n\n        ret = Subreddit._by_name(\"exists\", _update=True)\n\n        self.assertEqual(ret, sr)\n        self.cache.set_multi.assert_called_once_with(\n            keys={sr.name: sr._id},\n            prefix=\"srid:\",\n            time=43200,\n        )\n\n    def testCacheNegativeResults(self):\n        self.cache.get_multi.return_value = {}\n        self.subreddit_query.return_value = []\n        self.subreddit_byID.return_value = []\n\n        with self.assertRaises(NotFound):\n            Subreddit._by_name(\"doesnotexist\")\n\n        self.cache.set_multi.assert_called_once_with(\n            keys={\"doesnotexist\": Subreddit.SRNAME_NOTFOUND},\n            prefix=\"srid:\",\n            time=43200,\n        )\n\n    def testExcludeNegativeLookups(self):\n        self.cache.get_multi.return_value = {\"doesnotexist\": Subreddit.SRNAME_NOTFOUND}\n\n        with self.assertRaises(NotFound):\n            Subreddit._by_name(\"doesnotexist\")\n        self.assertEqual(self.subreddit_query.call_count, 0)\n        self.assertEqual(self.subreddit_byID.call_count, 0)\n        self.assertEqual(self.cache.set_multi.call_count, 0)\n\n\nif __name__ == '__main__':\n    unittest.main()\n"
  },
  {
    "path": "r2/r2/tests/unit/models/thing_test.py",
    "content": "#!/usr/bin/env python\n# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom mock import MagicMock, patch\n\nfrom r2.lib.db.thing import (\n    CreationError,\n    hooks,\n    NotFound,\n    tdb,\n    Thing,\n)\nfrom r2.lib.lock import TimeoutExpired\nfrom r2.tests import RedditTestCase\n\n\nclass SimpleThing(Thing):\n    _nodb = True\n    _type_name = \"simplething\"\n    _type_id = 100\n    _cache = MagicMock()\n    _data_int_props = (\"prop_for_data\",)\n    _defaults = {\n        \"prop_for_data\": 0,\n    }\n\n\nclass TestThingReadCaching(RedditTestCase):\n    def setUp(self):\n        self.get_things_from_cache = self.autopatch(Thing, \"get_things_from_cache\")\n        self.get_things_from_db = self.autopatch(Thing, \"get_things_from_db\")\n        self.write_things_to_cache = self.autopatch(Thing, \"write_things_to_cache\")\n\n    def test_not_found(self):\n        things_by_id = {}\n\n        self.get_things_from_cache.return_value = {}\n        self.get_things_from_db.return_value = things_by_id\n\n        with self.assertRaises(NotFound):\n            SimpleThing._byID([1, 2, 3], stale=False)\n\n        self.get_things_from_cache.assert_called_once_with([1, 2, 3], stale=False)\n        self.get_things_from_db.assert_called_once_with([1, 2, 3])\n        self.write_things_to_cache.assert_not_called()\n\n    def test_partial_not_found(self):\n        things_by_id = {\n            1: \"one\",\n        }\n\n        self.get_things_from_cache.return_value = {}\n        self.get_things_from_db.return_value = things_by_id\n\n        with self.assertRaises(NotFound):\n            SimpleThing._byID([1, 2, 3], stale=False)\n\n        self.get_things_from_cache.assert_called_once_with([1, 2, 3], stale=False)\n        self.get_things_from_db.assert_called_once_with([1, 2, 3])\n        self.write_things_to_cache.assert_called_once_with(things_by_id)\n\n    def test_partial_not_found_ignore(self):\n        things_by_id = {\n            1: \"one\",\n        }\n\n        self.get_things_from_cache.return_value = {}\n        self.get_things_from_db.return_value = things_by_id\n\n        ret = SimpleThing._byID([1, 2, 3], stale=False, ignore_missing=True)\n        self.get_things_from_cache.assert_called_once_with([1, 2, 3], stale=False)\n        self.get_things_from_db.assert_called_once_with([1, 2, 3])\n        self.write_things_to_cache.assert_called_once_with(things_by_id)\n        self.assertEqual(ret, things_by_id)\n\n    def test_cache_miss(self):\n        things_by_id = {\n            1: \"one\",\n            2: \"two\",\n            3: \"three\",\n        }\n\n        self.get_things_from_cache.return_value = {}\n        self.get_things_from_db.return_value = things_by_id\n\n        ret = SimpleThing._byID([1, 2, 3], stale=False)\n        self.get_things_from_cache.assert_called_once_with(\n            [1, 2, 3], stale=False)\n        self.get_things_from_db.assert_called_once_with([1, 2, 3])\n        self.write_things_to_cache.assert_called_once_with(things_by_id)\n        self.assertEqual(ret, things_by_id)\n\n    def test_cache_hit(self):\n        things_by_id = {\n            1: \"one\",\n            2: \"two\",\n            3: \"three\",\n        }\n\n        self.get_things_from_cache.return_value = things_by_id\n\n        ret = SimpleThing._byID([1, 2, 3], stale=False)\n        self.get_things_from_cache.assert_called_once_with([1, 2, 3], stale=False)\n        self.get_things_from_db.assert_not_called()\n        self.write_things_to_cache.assert_not_called()\n        self.assertEqual(ret, things_by_id)\n\n    def test_partial_hit(self):\n        things_by_id = {\n            1: \"one\",\n            2: \"two\",\n            3: \"three\",\n        }\n\n        self.get_things_from_cache.return_value = {1: \"one\"}\n        self.get_things_from_db.return_value = {2: \"two\", 3: \"three\"}\n\n        ret = SimpleThing._byID([1, 2, 3], stale=False)\n        self.get_things_from_cache.assert_called_once_with([1, 2, 3], stale=False)\n        self.get_things_from_db.assert_called_once_with([2, 3])\n        self.write_things_to_cache.assert_called_once_with({2: \"two\", 3: \"three\"})\n        self.assertEqual(ret, things_by_id)\n\n    def test_return_list(self):\n        things_by_id = {\n            1: \"one\",\n            2: \"two\",\n            3: \"three\",\n        }\n\n        self.get_things_from_cache.return_value = things_by_id\n        self.get_things_from_db.return_value = things_by_id\n\n        ret = SimpleThing._byID([1, 2, 3], stale=False, return_dict=False)\n        self.get_things_from_cache.assert_called_once_with([1, 2, 3], stale=False)\n        self.get_things_from_db.assert_not_called()\n        self.write_things_to_cache.assert_not_called()\n        self.assertEqual(ret, [\"one\", \"two\", \"three\"])\n\n    def test_single_not_found(self):\n        things_by_id = {}\n\n        self.get_things_from_cache.return_value = {}\n        self.get_things_from_db.return_value = {}\n\n        with self.assertRaises(NotFound):\n            SimpleThing._byID(1, stale=False)\n\n        self.get_things_from_cache.assert_called_once_with((1,), stale=False)\n        self.get_things_from_db.assert_called_once_with([1])\n        self.write_things_to_cache.assert_not_called()\n\n    def test_single_miss(self):\n        things_by_id = {\n            1: \"one\",\n        }\n\n        self.get_things_from_cache.return_value = {}\n        self.get_things_from_db.return_value = things_by_id\n\n        ret = SimpleThing._byID(1, stale=False)\n        self.get_things_from_cache.assert_called_once_with((1,), stale=False)\n        self.get_things_from_db.assert_called_once_with([1])\n        self.write_things_to_cache.assert_called_once_with(things_by_id)\n        self.assertEqual(ret, \"one\")\n\n    def test_single_hit(self):\n        things_by_id = {\n            1: \"one\",\n        }\n\n        self.get_things_from_cache.return_value = things_by_id\n        self.get_things_from_db.return_value = things_by_id\n\n        ret = SimpleThing._byID(1, stale=False)\n        self.get_things_from_cache.assert_called_once_with((1,), stale=False)\n        self.get_things_from_db.assert_not_called()\n        self.write_things_to_cache.assert_not_called()\n        self.assertEqual(ret, \"one\")\n\n\nclass FakeLock(object):\n    def __init__(self):\n        self.have_lock = True\n\n    def acquire(self):\n        return\n\n    def release(self):\n        return\n\n    def __enter__(self):\n        return self\n\n    def __exit__(self, *args):\n        return\n\n\nclass TestThingWrite(RedditTestCase):\n    def setUp(self):\n        self.lock = FakeLock()\n        self.thing_id = 333\n\n        self.autopatch(tdb, \"transactions\")\n        self.autopatch(hooks, \"get_hook\")\n        self.autopatch(Thing, \"write_new_thing_to_db\", return_value=self.thing_id)\n        self.autopatch(Thing, \"get_read_modify_write_lock\", return_value=self.lock)\n        self.autopatch(Thing, \"write_props_to_db\")\n        self.autopatch(Thing, \"write_thing_to_cache\")\n        self.autopatch(Thing, \"update_from_cache\")\n\n    def reset_mocks(self):\n        SimpleThing.write_new_thing_to_db.reset_mock()\n        SimpleThing.get_read_modify_write_lock.reset_mock()\n        SimpleThing.update_from_cache.reset_mock()\n        SimpleThing.write_props_to_db.reset_mock()\n        SimpleThing.write_thing_to_cache.reset_mock()\n\n    def test_create(self):\n        thing = SimpleThing(\n            ups=1,\n            downs=0,\n            spam=False,\n            deleted=False,\n        )\n        thing.other_prop = 100\n        thing._commit()\n\n        SimpleThing.write_new_thing_to_db.assert_called_once_with()\n        SimpleThing.update_from_cache.assert_not_called()\n        SimpleThing.write_props_to_db.assert_called_once_with({}, {'other_prop': 100}, True)\n        SimpleThing.write_thing_to_cache.assert_called_once_with(lock=None, brand_new_thing=True)\n\n    def test_modify(self):\n        thing = SimpleThing(\n            ups=1,\n            downs=0,\n            spam=False,\n            deleted=False,\n        )\n        thing.other_prop = 100\n        thing._commit()\n\n        self.reset_mocks()\n\n        thing.other_prop = 101\n        thing._ups = 12\n        thing._commit()\n\n        SimpleThing.write_new_thing_to_db.assert_not_called()\n        SimpleThing.get_read_modify_write_lock.assert_called_once_with()\n        SimpleThing.update_from_cache.assert_called_once_with(self.lock)\n        SimpleThing.write_props_to_db.assert_called_once_with({'ups': 12}, {'other_prop': 101}, False)\n        SimpleThing.write_thing_to_cache.assert_called_once_with(self.lock)\n\n\nclass TestThingIncr(RedditTestCase):\n    def setUp(self):\n        self.lock = FakeLock()\n        self.thing_id = 333\n\n        self.autopatch(tdb, \"transactions\")\n        self.autopatch(hooks, \"get_hook\")\n        self.autopatch(Thing, \"write_new_thing_to_db\", return_value=self.thing_id)\n        self.autopatch(Thing, \"get_read_modify_write_lock\", return_value=self.lock)\n        self.autopatch(Thing, \"write_props_to_db\")\n        self.autopatch(Thing, \"write_thing_to_cache\")\n        self.autopatch(Thing, \"update_from_cache\")\n\n    def reset_mocks(self):\n        SimpleThing.write_new_thing_to_db.reset_mock()\n        SimpleThing.get_read_modify_write_lock.reset_mock()\n        SimpleThing.update_from_cache.reset_mock()\n        SimpleThing.write_props_to_db.reset_mock()\n        SimpleThing.write_thing_to_cache.reset_mock()\n\n    @patch(\"r2.lib.db.tdb_sql.incr_thing_prop\")\n    def test_incr_base_prop(self, incr_thing_prop):\n        thing = SimpleThing(\n            ups=1,\n            downs=0,\n            spam=False,\n            deleted=False,\n        )\n        thing._commit()\n\n        self.reset_mocks()\n\n        thing._incr(\"_ups\")\n        incr_thing_prop.assert_called_once_with(\n            type_id=SimpleThing._type_id,\n            thing_id=thing._id,\n            prop=\"ups\",\n            amount=1,\n        )\n\n    @patch(\"r2.lib.db.tdb_sql.incr_thing_data\")\n    def test_incr_data_prop(self, incr_thing_data):\n        thing = SimpleThing(\n            ups=1,\n            downs=0,\n            spam=False,\n            deleted=False,\n        )\n        thing.prop_for_data = 100\n        thing._commit()\n\n        self.reset_mocks()\n\n        thing._incr(\"prop_for_data\")\n        incr_thing_data.assert_called_once_with(\n            type_id=SimpleThing._type_id,\n            thing_id=thing._id,\n            prop=\"prop_for_data\",\n            amount=1,\n        )\n\n    @patch(\"r2.lib.db.tdb_sql.set_thing_data\")\n    def test_incr_unset_data_prop(self, set_thing_data):\n        thing = SimpleThing(\n            ups=1,\n            downs=0,\n            spam=False,\n            deleted=False,\n        )\n        thing._commit()\n\n        self.reset_mocks()\n\n        thing._incr(\"prop_for_data\")\n        set_thing_data.assert_called_once_with(\n            type_id=SimpleThing._type_id,\n            thing_id=thing._id,\n            brand_new_thing=False,\n            prop_for_data=1,\n        )\n\n    def test_incr_dirty(self):\n        thing = SimpleThing(\n            ups=1,\n            downs=0,\n            spam=False,\n            deleted=False,\n        )\n        thing._commit()\n\n        self.reset_mocks()\n\n        thing.other_prop = 100\n\n        with self.assertRaises(AssertionError):\n            thing._incr(\"_ups\")\n\n\nclass TestThingWriteConflict(RedditTestCase):\n    def setUp(self):\n        self.lock = FakeLock()\n        self.thing_id = 333\n\n        self.autopatch(tdb, \"transactions\")\n        self.autopatch(hooks, \"get_hook\")\n        self.autopatch(Thing, \"write_new_thing_to_db\", return_value=self.thing_id)\n        self.autopatch(Thing, \"get_read_modify_write_lock\", return_value=self.lock)\n        self.autopatch(Thing, \"write_props_to_db\")\n        self.autopatch(Thing, \"write_thing_to_cache\")\n\n    def reset_mocks(self):\n        SimpleThing.write_new_thing_to_db.reset_mock()\n        SimpleThing.get_read_modify_write_lock.reset_mock()\n        SimpleThing.write_props_to_db.reset_mock()\n        SimpleThing.write_thing_to_cache.reset_mock()\n\n    @patch(\"r2.lib.db.thing.Thing.get_things_from_cache\")\n    def test_dont_overwrite(self, get_things_from_cache):\n        other_thing = SimpleThing(\n            ups=2,\n            downs=0,\n            spam=False,\n            deleted=False,\n            id=self.thing_id,\n        )\n        other_thing.__setattr__(\"another_prop\", 3, make_dirty=False)\n        get_things_from_cache.return_value = {self.thing_id: other_thing}\n\n        thing = SimpleThing(\n            ups=1,\n            downs=0,\n            spam=False,\n            deleted=False,\n        )\n        thing.other_prop = 100\n        thing._commit()\n\n        self.reset_mocks()\n\n        thing._downs = 1\n        thing.other_prop = 102\n        thing._commit()\n\n        SimpleThing.write_new_thing_to_db.assert_not_called()\n        SimpleThing.get_read_modify_write_lock.assert_called_once_with()\n        get_things_from_cache.assert_called_once_with([self.thing_id], allow_local=False)\n        SimpleThing.write_props_to_db.assert_called_once_with({'downs': 1}, {'other_prop': 102}, False)\n        SimpleThing.write_thing_to_cache.assert_called_once_with(self.lock)\n        self.assertEqual(thing.another_prop, 3)\n\n    @patch(\"r2.lib.db.thing.Thing.get_read_modify_write_lock\")\n    def test_lock_fail(self, get_read_modify_write_lock):\n        get_read_modify_write_lock.side_effect = TimeoutExpired()\n\n        thing = SimpleThing(\n            ups=2,\n            downs=0,\n            spam=False,\n            deleted=False,\n            id=self.thing_id,\n        )\n\n        self.reset_mocks()\n\n        thing._ups = 3\n\n        with self.assertRaises(TimeoutExpired):\n            thing._commit()\n\n        tdb.transactions.rollback.assert_not_called()\n\n    @patch(\"r2.lib.db.thing.Thing.write_new_thing_to_db\")\n    def test_create_fail(self, write_new_thing_to_db):\n        write_new_thing_to_db.side_effect = CreationError()\n\n        thing = SimpleThing(\n            ups=2,\n            downs=0,\n            spam=False,\n            deleted=False,\n        )\n        thing.other_prop = 13\n\n        with self.assertRaises(CreationError):\n            thing._commit()\n\n        tdb.transactions.rollback.assert_called_once_with()\n\n    @patch(\"r2.lib.db.thing.Thing.write_changes_to_db\")\n    def test_modify_fail(self, write_changes_to_db):\n        write_changes_to_db.side_effect = CreationError()\n\n        thing = SimpleThing(\n            ups=2,\n            downs=0,\n            spam=False,\n            deleted=False,\n            id=self.thing_id,\n        )\n\n        self.reset_mocks()\n\n        thing._ups = 3\n\n        with self.assertRaises(CreationError):\n            thing._commit()\n\n        tdb.transactions.rollback.assert_called_once_with()\n"
  },
  {
    "path": "r2/r2/tests/unit/models/user_message_builder_test.py",
    "content": "#!/usr/bin/env python\n# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\nimport contextlib\n\nfrom r2.tests import RedditTestCase\n\nfrom mock import patch, MagicMock\n\nfrom r2.models import Message\nfrom r2.models.builder import UserMessageBuilder, MessageBuilder\n\nfrom pylons import tmpl_context as c\n\n\nclass UserMessageBuilderTest(RedditTestCase):\n    def setUp(self):\n        super(UserMessageBuilderTest, self).setUp()\n        self.user = MagicMock(name=\"user\")\n        self.message = MagicMock(spec=Message)\n\n    def test_view_message_on_receiver_side_and_spam(self):\n        user = MagicMock(name=\"user\")\n        userMessageBuilder = UserMessageBuilder(user)\n\n        self.user._id = 1\n        self.message.author_id = 2\n        self.message._spam = True\n\n        with self.mock_preparation():\n            self.assertFalse(\n                    userMessageBuilder._viewable_message(self.message))\n\n    def test_view_message_on_receiver_side_and_del(self):\n        user = MagicMock(name=\"user\")\n        userMessageBuilder = UserMessageBuilder(user)\n\n        self.user._id = 1\n        self.message.author_id = 2\n        self.message.to_id = self.user._id\n        self.message._spam = False\n        self.message.del_on_recipient = True\n\n        with self.mock_preparation():\n            self.assertFalse(\n                    userMessageBuilder._viewable_message(self.message))\n\n    def test_view_message_on_receiver_side(self):\n        user = MagicMock(name=\"user\")\n        userMessageBuilder = UserMessageBuilder(user)\n\n        self.user._id = 1\n        self.message.author_id = 2\n        self.message.to_id = self.user._id\n        self.message._spam = False\n        self.message.del_on_recipient = False\n\n        with self.mock_preparation():\n            self.assertTrue(\n                userMessageBuilder._viewable_message(self.message))\n\n    def test_view_message_on_sender_side_and_del(self):\n        user = MagicMock(name=\"user\")\n        userMessageBuilder = UserMessageBuilder(user)\n\n        self.message.to_id = 1\n        self.user._id = 2\n        self.message.author_id = self.user._id\n        self.message._spam = False\n        self.message.del_on_recipient = True\n\n        with self.mock_preparation():\n            self.assertTrue(\n                userMessageBuilder._viewable_message(self.message))\n\n    def test_view_message_on_admin_and_del(self):\n        user = MagicMock(name=\"user\")\n        userMessageBuilder = UserMessageBuilder(user)\n\n        self.user._id = 1\n        self.message.author_id = 2\n        self.message.to_id = self.user._id\n        self.message._spam = False\n        self.message.del_on_recipient = True\n\n        with self.mock_preparation(True):\n            self.assertTrue(\n                userMessageBuilder._viewable_message(self.message))\n\n    def mock_preparation(self, is_admin=False):\n        \"\"\" Context manager for mocking function calls. \"\"\"\n\n        return contextlib.nested(\n            patch.object(c, \"user\", self.user, create=True),\n            patch.object(c, \"user_is_admin\", is_admin, create=True),\n            patch.object(MessageBuilder,\n                         \"_viewable_message\", return_value=True)\n        )\n\n"
  },
  {
    "path": "r2/r2/tests/unit/models/vote_test.py",
    "content": "from mock import patch, MagicMock\nfrom datetime import datetime\n\nimport pytz\n\nfrom r2.lib.utils import tup\nfrom r2.models.vote import Vote\nfrom r2.tests import RedditTestCase\n\n\nclass TestVoteValidator(RedditTestCase):\n\n    def setUp(self):\n        self.user = MagicMock(name=\"user\")\n        self.user._id36 = 'userid36'\n        self.thing = MagicMock(name=\"thing\")\n        self.vote_data = {}\n        super(RedditTestCase, self).setUp()\n\n    def cast_vote(self, **kw):\n        kw.setdefault(\"date\", datetime.now(pytz.UTC))\n        kw.setdefault(\"direction\", Vote.DIRECTIONS.up)\n        kw.setdefault(\"get_previous_vote\", False)\n        kw.setdefault(\"data\", self.vote_data)\n        return Vote(\n            user=self.user,\n            thing=self.thing,\n            **kw\n        )\n\n    def assert_vote_effects(\n        self, vote,\n        affects_score=True,\n        affects_karma=True,\n        affected_thing_attr=\"_ups\",\n        notes=None,\n    ):\n        notes = set(tup(notes) if notes else [])\n        self.assertEqual(vote.effects.affects_score, affects_score)\n        self.assertEqual(vote.effects.affects_karma, affects_karma)\n        self.assertEqual(vote.affected_thing_attr, affected_thing_attr)\n        self.assertEqual(set(vote.effects.notes), notes)\n        return vote\n\n    def test_upvote_effects(self):\n        vote = self.cast_vote()\n        self.assertTrue(vote.is_upvote)\n        self.assertFalse(vote.is_downvote)\n        self.assertFalse(vote.is_self_vote)\n        self.assert_vote_effects(vote)\n\n    def test_downvote_effects(self):\n        vote = self.cast_vote(direction=Vote.DIRECTIONS.down)\n        self.assertFalse(vote.is_upvote)\n        self.assertTrue(vote.is_downvote)\n        self.assertFalse(vote.is_self_vote)\n        self.assert_vote_effects(vote, affected_thing_attr=\"_downs\")\n"
  },
  {
    "path": "r2/r2/tests/unit/ratelimit_test.py",
    "content": "#!/usr/bin/env python\n# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2016 reddit\n# Inc. All Rights Reserved.\n###############################################################################\nimport unittest\nfrom mock import patch\n\nimport pylibmc\nfrom pylons import app_globals as g\n\nfrom r2.lib import ratelimit\nfrom r2.lib.cache import LocalCache\n\n\nclass RateLimitStandaloneFunctionsTest(unittest.TestCase):\n    def setUp(self):\n        self.patch('ratelimit.time.time', lambda: self.now)\n\n        self.cache = LocalCache()\n        self.patch('ratelimit.g.ratelimitcache', self.cache)\n\n    def patch(self, *a, **kw):\n        p = patch(*a,  **kw)\n        p.start()\n        self.addCleanup(p.stop)\n\n    def test_get_timeslice(self):\n        self.now = 125\n        ts = ratelimit.get_timeslice(60)\n        self.assertEquals(120, ts.beginning)\n        self.assertEquals(180, ts.end)\n        self.assertEquals(55, ts.remaining)\n\n    def test_make_ratelimit_cache_key_1s(self):\n        self.now = 14\n        ts = ratelimit.get_timeslice(1)\n        key = ratelimit._make_ratelimit_cache_key('a', ts)\n        self.assertEquals('rl:a-000014', key)\n\n    def test_make_ratelimit_cache_key_1m(self):\n        self.now = 65\n        ts = ratelimit.get_timeslice(60)\n        key = ratelimit._make_ratelimit_cache_key('a', ts)\n        self.assertEquals('rl:a-000100', key)\n\n    def test_make_ratelimit_cache_key_1h(self):\n        self.now = 3650\n        ts = ratelimit.get_timeslice(3600)\n        key = ratelimit._make_ratelimit_cache_key('a', ts)\n        self.assertEquals('rl:a-010000', key)\n\n    def test_make_ratelimit_cache_key_1d(self):\n        self.now = 24 * 3600 + 5\n        ts = ratelimit.get_timeslice(24 * 3600)\n        key = ratelimit._make_ratelimit_cache_key('a', ts)\n        self.assertEquals('rl:a-@86400', key)\n\n    def test_make_ratelimit_cache_key_1w(self):\n        self.now = 7 * 24 * 3600 + 5\n        ts = ratelimit.get_timeslice(24 * 3600)\n        key = ratelimit._make_ratelimit_cache_key('a', ts)\n        self.assertEquals('rl:a-@604800', key)\n\n    def test_record_usage(self):\n        self.now = 24 * 3600 + 5\n        ts = ratelimit.get_timeslice(3600)\n        ratelimit.record_usage('a', ts)\n        self.assertEquals(1, self.cache['rl:a-000000'])\n        ratelimit.record_usage('a', ts)\n        self.assertEquals(2, self.cache['rl:a-000000'])\n\n        self.now = 24 * 3600 + 5 * 3600\n        ts = ratelimit.get_timeslice(3600)\n        ratelimit.record_usage('a', ts)\n        self.assertEquals(1, self.cache['rl:a-050000'])\n\n    def test_record_usage_across_slice_expiration(self):\n        self.now = 24 * 3600 + 5\n        ts = ratelimit.get_timeslice(3600)\n        real_incr = self.cache.incr\n        evicted = False\n\n        def fake_incr(key):\n            if evicted:\n                del self.cache[key]\n                raise pylibmc.NotFound()\n            return real_incr(key)\n\n        with patch.object(self.cache, 'incr', fake_incr):\n            # Forcibly evict the key before incr() is called, but after the\n            # initial add() call inside record_usage().\n            evicted = True\n            ratelimit.record_usage('a', ts)\n            self.assertEquals(1, self.cache['rl:a-000000'])\n\n    def test_get_usage(self):\n        self.now = 24 * 3600 + 5 * 3600\n        ts = ratelimit.get_timeslice(3600)\n        self.assertEquals(None, ratelimit.get_usage('a', ts))\n        ratelimit.record_usage('a', ts)\n        self.assertEquals(1, ratelimit.get_usage('a', ts))\n\n\nclass RateLimitTest(unittest.TestCase):\n    class TestRateLimit(ratelimit.RateLimit):\n        event_name = 'TestRateLimit'\n        event_type = 'tests'\n        key = 'tests'\n        limit = 1\n        seconds = 3600\n\n    def setUp(self):\n        self.patch('ratelimit.time.time', lambda: self.now)\n\n        self.cache = LocalCache()\n        self.patch('ratelimit.g.ratelimitcache', self.cache)\n\n    def patch(self, *a, **kw):\n        p = patch(*a,  **kw)\n        p.start()\n        self.addCleanup(p.stop)\n\n    def test_record_usage(self):\n        rl = self.TestRateLimit()\n\n        self.now = 24 * 3600 + 5\n        rl.record_usage()\n        self.assertEquals(1, self.cache['rl:tests-000000'])\n        rl.record_usage()\n        self.assertEquals(2, self.cache['rl:tests-000000'])\n\n        self.now = 24 * 3600 + 5 * 3600\n        rl.record_usage()\n        self.assertEquals(1, self.cache['rl:tests-050000'])\n\n    def test_get_usage(self):\n        rl = self.TestRateLimit()\n        self.now = 24 * 3600 + 5 * 3600\n        self.assertTrue(rl.check())\n        rl.record_usage()\n        self.assertFalse(rl.check())\n\n\nclass LiveConfigRateLimitTest(unittest.TestCase):\n    class TestRateLimit(ratelimit.LiveConfigRateLimit):\n        event_name = 'TestRateLimit'\n        event_type = 'tests'\n        key = 'rl-tests'\n        limit_live_key = 'RL_TESTS'\n        seconds_live_key = 'RL_TESTS_RESET_SECS'\n\n    def patch_liveconfig(self, k, v):\n        \"\"\"Helper method to patch g.live_config (with cleanup).\"\"\"\n        def cleanup(orig=g.live_config.get(k), has=k in g.live_config):\n            if has:\n                g.live_config[k] = orig\n            else:\n                del g.live_config[k]\n        g.live_config[k] = v\n        self.addCleanup(cleanup)\n\n    def configure_rate_limit(self, num, per_unit):\n        self.patch_liveconfig('RL_TESTS', num)\n        self.patch_liveconfig('RL_TESTS_RESET_SECS', per_unit)\n\n    def test_limit(self):\n        self.configure_rate_limit(1, 3600)\n        rl = self.TestRateLimit()\n        self.assertEquals(1, rl.limit)\n\n        self.configure_rate_limit(2, 3600)\n        self.assertEquals(2, rl.limit)\n\n    def test_seconds(self):\n        self.configure_rate_limit(1, 3600)\n        rl = self.TestRateLimit()\n        self.assertEquals(3600, rl.seconds)\n\n        self.configure_rate_limit(1, 300)\n        self.assertEquals(300, rl.seconds)\n"
  },
  {
    "path": "r2/setup.cfg",
    "content": "[egg_info]\ntag_build = dev\n\n[nosetests]\nwhere = r2/tests/\n\n[extract_messages]\nadd_comments = TRANSLATORS:\nkeywords =  P_:1,2 NP_:1,2 _md _mdf _ws _wsf:1\nmapping_file = babel.cfg\nwidth = 80\n"
  },
  {
    "path": "r2/setup.py",
    "content": "#!/usr/bin/env python\n# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport os\nimport fnmatch\nimport sys\nfrom setuptools import setup, find_packages, Extension\n\n\ncommands = {}\n\n\ntry:\n    from Cython.Build import cythonize\nexcept ImportError:\n    print \"Cannot find Cython. Skipping Cython build.\"\n    pyx_extensions = []\nelse:\n    pyx_files = []\n    for root, directories, files in os.walk('.'):\n        for f in fnmatch.filter(files, '*.pyx'):\n            pyx_files.append(os.path.join(root, f))\n    pyx_extensions = cythonize(pyx_files)\n\n\n# guard against import errors in case this is the first run of setup.py and we\n# don't have any dependencies (including baseplate) yet\ntry:\n    from baseplate.integration.thrift.command import ThriftBuildPyCommand\nexcept ImportError:\n    print \"Cannot find Baseplate. Skipping Thrift build.\"\nelse:\n    commands[\"build_py\"] = ThriftBuildPyCommand\n\n\nsetup(\n    name=\"r2\",\n    version=\"\",\n    install_requires=[\n        \"Pylons\",\n        \"Routes\",\n        \"mako>=0.5\",\n        \"boto >= 2.0\",\n        \"pytz\",\n        \"pycrypto\",\n        \"Babel>=1.0\",\n        \"cython>=0.14\",\n        \"SQLAlchemy\",\n        \"BeautifulSoup\",\n        \"chardet\",\n        \"psycopg2\",\n        \"pycassa>=1.7.0\",\n        \"pycaptcha\",\n        \"amqplib\",\n        \"py-bcrypt\",\n        \"snudown>=1.1.0\",\n        \"l2cs>=2.0.2\",\n        \"lxml\",\n        \"kazoo\",\n        \"stripe\",\n        \"requests\",\n        \"tinycss2\",\n        \"unidecode\",\n        \"PyYAML\",\n        \"Pillow\",\n        \"pylibmc==1.2.2\",\n        \"webob\",\n        \"webtest\",\n        \"python-snappy\",\n        \"httpagentparser==1.7.8\",\n        \"raven\",\n    ],\n    # setup tests (allowing for \"python setup.py test\")\n    tests_require=['mock', 'nose', 'coverage'],\n    test_suite=\"nose.collector\",\n    dependency_links=[\n        \"https://github.com/reddit/snudown/archive/v1.1.3.tar.gz#egg=snudown-1.1.3\",\n        \"https://s3.amazonaws.com/code.reddit.com/pycaptcha-0.4.tar.gz#egg=pycaptcha-0.4\",\n    ],\n    packages=find_packages(exclude=[\"ez_setup\"]),\n    cmdclass=commands,\n    ext_modules=pyx_extensions + [\n        Extension(\n            \"Cfilters\",\n            sources=[\n                \"r2/lib/c/filters.c\",\n            ]\n        ),\n    ],\n    entry_points=\"\"\"\n    [paste.app_factory]\n    main=r2:make_app\n    [paste.paster_command]\n    run = r2.commands:RunCommand\n    shell = pylons.commands:ShellCommand\n    [paste.filter_app_factory]\n    gzip = r2.lib.gzipper:make_gzip_middleware\n    [r2.provider.media]\n    s3 = r2.lib.providers.media.s3:S3MediaProvider\n    filesystem = r2.lib.providers.media.filesystem:FileSystemMediaProvider\n    [r2.provider.cdn]\n    fastly = r2.lib.providers.cdn.fastly:FastlyCdnProvider\n    cloudflare = r2.lib.providers.cdn.cloudflare:CloudFlareCdnProvider\n    null = r2.lib.providers.cdn.null:NullCdnProvider\n    [r2.provider.auth]\n    cookie = r2.lib.providers.auth.cookie:CookieAuthenticationProvider\n    http = r2.lib.providers.auth.http:HttpAuthenticationProvider\n    [r2.provider.support]\n    zendesk = r2.lib.providers.support.zendesk:ZenDeskProvider\n    [r2.provider.search]\n    cloudsearch = r2.lib.providers.search.cloudsearch:CloudSearchProvider\n    solr = r2.lib.providers.search.solr:SolrSearchProvider\n    [r2.provider.image_resizing]\n    imgix = r2.lib.providers.image_resizing.imgix:ImgixImageResizingProvider\n    no_op = r2.lib.providers.image_resizing.no_op:NoOpImageResizingProvider\n    unsplashit = r2.lib.providers.image_resizing.unsplashit:UnsplashitImageResizingProvider\n    [r2.provider.email]\n    null = r2.lib.providers.email.null:NullEmailProvider\n    mailgun = r2.lib.providers.email.mailgun:MailgunEmailProvider\n    \"\"\",\n)\n"
  },
  {
    "path": "r2/updateini.py",
    "content": "#!/usr/bin/env python\n# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom ConfigParser import MissingSectionHeaderError\nfrom StringIO import StringIO\nimport sys\n\nfrom r2.lib.utils import parse_ini_file\n\nHEADER = '''\n# YOU DO NOT NEED TO EDIT THIS FILE\n# This is a generated file. To update the configuration,\n# edit the *.update file of the same name, and then\n# run 'make ini'\n# Configuration settings in the *.update file will override\n# or be added to the base 'example.ini' file.\n'''\n\ndef main(source_ini, update_ini):\n    with open(source_ini) as source:\n        parser = parse_ini_file(source)\n    with open(update_ini) as f:\n        updates = f.read()\n    try:\n        # Existing *.update files don't include section\n        # headers; inject a [DEFAULT] header if the parsing\n        # fails\n        parser.readfp(StringIO(updates))\n    except MissingSectionHeaderError:\n        updates = \"[DEFAULT]\\n\" + updates\n        parser.readfp(StringIO(updates))\n    print HEADER\n    parser.write(sys.stdout)\n\nif __name__ == '__main__':\n    args = sys.argv\n    if len(args) != 3:\n        print 'usage: %s [source] [update]' % sys.argv[0]\n        sys.exit(1)\n    else:\n        main(sys.argv[1], sys.argv[2])\n"
  },
  {
    "path": "scripts/add_to_collection",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom r2.lib.db.thing import NotFound\nfrom r2.models import Target, Collection, CollectionStorage, Subreddit\n\n\ndef run(collection, sr):\n    try:\n        sr = Subreddit._by_name(sr)\n    except NotFound:\n        raise ValueError(\"specified subreddit does not exist\")\n\n    collection = Collection.by_name(collection)\n    if not collection:\n        raise ValueError(\"specified collection does not exist\")\n    \n    srs = Target(collection).subreddits_slow\n\n    if sr not in srs:\n        srs.append(sr)\n    else:\n        raise ValueError(\"specified subreddit is already in that collection\")\n\n    srs.sort(key=lambda sr: sr._downs, reverse=True)\n    sr_names = [sr.name for sr in srs]\n    sr_names_column = {'sr_names': \n                       CollectionStorage.SR_NAMES_DELIM.join(sr_names)}\n\n    CollectionStorage._set_values(collection.name, sr_names_column)\n    print \"%s has been added to %s\" % (sr.name, collection.name)\n"
  },
  {
    "path": "scripts/compute_time_listings",
    "content": "#!/bin/bash\n# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nset -e\n\n# expects two environment variables\n#   REDDIT_ROOT = path to the root of the reddit public code; the directory with the Makefile\n#   REDDIT_INI = path to the ini file to use\n# which should be supplied via:\nsource /etc/default/reddit\n# additionally, some configuration can be overridden in the environment\nexport TMPDIR=${TMPDIR:-/tmp}\nexport PGUSER=${PGUSER:-reddit}\nexport PGHOST=${PGHOST:-localhost}\n\n## command line args\n# one of \"link\" or \"comment\"\nexport THING_CLS=\"$1\"\n# period of data to extract from postgres: e.g. \"hour\", \"week\", \"year\", \"all\"\nexport INTERVAL=\"$2\"\n# which period listings to update.\n# formatted as python tuple of strings: e.g. '(\"hour\",)' or (\"week\", \"all\",) etc\nexport TIMES=\"$3\"\n\necho \"Starting $THING_CLS processing\"\n\n# enable reporting to sentry\nexport REDDIT_ERRORS_TO_SENTRY=1\n\nTHING_DUMP=$TMPDIR/$THING_CLS-$INTERVAL-thing.dump\nDATA_DUMP=$TMPDIR/$THING_CLS-$INTERVAL-data.dump\nfunction clean_up {\n    rm -f $THING_DUMP $DATA_DUMP\n}\n\nif [ -e $THING_DUMP ]; then\n    echo cannot start because $THING_DUMP exists\n    ls -l $THING_DUMP\n    exit 1\nfi\ntouch $THING_DUMP\n\n# since we're in charge of this run now, we have to clean up afterwards\ntrap clean_up EXIT\n\nfunction run_query {\n    psql -F\"\\t\" -A -t -c \"$1\"\n}\n\nfunction mrsort {\n    LC_ALL=C sort -S200m\n}\n\nfunction reddit {\n    reddit_usage() {\n        echo \"reddit: [-jN] cmd...\" 2>&1\n        exit\n    }\n\n    local OPTIND o njobs\n\n    njobs=1\n\n    while getopts \":j:\" o; do\n        case \"${o}\" in\n            j)\n                njobs=\"${OPTARG}\"\n                ;;\n            *)\n                reddit_usage\n                ;;\n        esac\n    done\n    shift $((OPTIND-1))\n\n    cmd=\"paster --plugin=r2 run $REDDIT_INI $REDDIT_ROOT/r2/lib/mr_top.py -c \\\"$@ # $THING_CLS $INTERVAL $TIMES\\\"\"\n\n    if [ \"$njobs\" = \"1\" ]; then\n        sh -c \"$cmd\" # just execute it directly\n    else\n        $REDDIT_ROOT/../scripts/hashdist.py -n\"$njobs\" -- sh -c \"$cmd\"\n    fi\n}\n\n# Hack to let pg fetch all things with intervals\nif [ $INTERVAL = \"all\" ]; then\n   export INTERVAL=\"century\"\nfi\n\nMINID=$(run_query \"SELECT thing_id\n                   FROM reddit_thing_$THING_CLS\n                   WHERE\n                      date > now() - interval '1 $INTERVAL' AND\n                      date < now()\n                   ORDER BY date\n                   LIMIT 1\")\nif [ -z $MINID ]; then\n    echo \\$MINID is empty. Replication is likely behind.\n    exit 1\nfi\n\nrun_query \"\\\\copy (SELECT thing_id, 'thing', '$THING_CLS', ups, downs, deleted, spam, extract(epoch from date)\n                   FROM reddit_thing_$THING_CLS\n                   WHERE\n                       not deleted AND\n                       thing_id >= $MINID\n                  ) to $THING_DUMP\"\n\nrun_query \"\\\\copy (SELECT thing_id, 'data', '$THING_CLS', key, value\n                   FROM reddit_data_$THING_CLS\n                   WHERE\n                       key IN ('url', 'sr_id', 'author_id') AND\n                       thing_id >= $MINID\n                  ) to $DATA_DUMP\"\n\ncat $THING_DUMP $DATA_DUMP |\n    mrsort |\n    reddit \"join_things('$THING_CLS')\" |\n    reddit \"time_listings($TIMES, '$THING_CLS')\" |\n    mrsort |\n    reddit -j4 \"write_permacache()\"\n\necho 'Done.'\n"
  },
  {
    "path": "scripts/geoip_service.py",
    "content": "#!/usr/bin/python\n# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n############################################################################### \n\"\"\"\nThis is a tiny Flask app used for geoip lookups against a maxmind database.\n\nIf you are using this service be sure to set `geoip_location` in your ini file.\n\n\"\"\"\n\nimport json\n\nimport GeoIP\nfrom flask import Flask, make_response\n\napplication = Flask(__name__)\n\n# SET THESE PATHS TO YOUR MAXMIND GEOIP LEGACY DATABASES\n# http://dev.maxmind.com/geoip/legacy/geolite/\nCOUNTRY_DB_PATH = '/usr/share/GeoIP/GeoIP.dat'\nCITY_DB_PATH = '/var/lib/GeoIP/GeoIPCity.dat'\nORG_DB_PATH = '/var/lib/GeoIP/GeoIPOrg.dat'\n\n\ntry:\n    gc = GeoIP.open(COUNTRY_DB_PATH, GeoIP.GEOIP_MEMORY_CACHE)\nexcept:\n    gc = None\n\ntry:\n    gi = GeoIP.open(CITY_DB_PATH, GeoIP.GEOIP_MEMORY_CACHE)\nexcept:\n    gi = None\n\ntry:\n    go = GeoIP.open(ORG_DB_PATH, GeoIP.GEOIP_MEMORY_CACHE)\nexcept:\n    go = None\n\n\ndef json_response(result):\n    json_output = json.dumps(result, ensure_ascii=False, encoding='iso-8859-1')\n    response = make_response(json_output.encode('utf-8'), 200)\n    response.headers['Content-Type'] = 'application/json; charset=utf-8'\n    return response\n\n\n@application.route('/geoip/<ips>')\ndef get_record(ips):\n    if gi:\n        result = {ip: gi.record_by_addr(ip) for ip in ips.split('+')}\n    elif gc:\n        result = {\n            ip : {\n                'country_code': gc.country_code_by_addr(ip),\n                'country_name': gc.country_name_by_addr(ip),\n            } for ip in ips.split('+')\n        }\n    else:\n        result = {}\n\n    return json_response(result)\n\n\n@application.route('/org/<ips>')\ndef get_organizations(ips):\n    if go:\n        return json_response({ip: go.org_by_addr(ip) for ip in ips.split('+')})\n    else:\n        return json_response({})\n\n\nif __name__ == \"__main__\":\n    application.run()\n"
  },
  {
    "path": "scripts/hashdist.py",
    "content": "#!/usr/bin/env python2.7\n\nfrom Queue import Queue\nimport argparse\nimport logging\nimport multiprocessing\nimport os\nimport re\nimport string\nimport subprocess\nimport sys\nimport threading\n\n\ndef parse_size(s):\n    def mult(multiplier):\n        return int(s[:-1])*multiplier\n\n    if all(x in string.digits for x in s):\n        return int(s)\n    if s.endswith('b'):\n        return mult(1)\n    if s.endswith('k'):\n        return mult(1024)\n    if s.endswith('m'):\n        return mult(1024*1024)\n    if s.endswith('g'):\n        return mult(1024*1024*1024)\n    raise Exception(\"Can't parse %r\" % (s,))\n\n\nclass JobInputter(threading.Thread):\n    \"\"\"\n    Takes input originally from stdin through iq and sends it to the job\n    \"\"\"\n    def __init__(self, job_name, popen, iq):\n        self.job_name = job_name\n        self.popen = popen\n        self.iq = iq\n        super(JobInputter, self).__init__()\n\n    def __repr__(self):\n        return \"<%s %s>\" % (self.__class__.__name__, self.job_name)\n\n    def run(self):\n        while True:\n            item = self.iq.get()\n            logging.debug(\"%r got item %r\", self, item)\n            if item is None:\n                logging.debug(\"%r closing %r\", self, self.popen.stdin)\n                self.popen.stdin.close()\n                self.iq.task_done()\n                break\n\n            try:\n                self.popen.stdin.write(item)\n                self.popen.stdin.flush()\n                self.iq.task_done()\n            except IOError:\n                logging.exception(\"exception writing to popen %r\", self.popen)\n                return os._exit(1)\n\n\nclass JobOutputter(threading.Thread):\n    \"\"\"\n    Takes output from the job and sends it to stdout\n    \"\"\"\n    def __init__(self, job_name, popen, out_fd, lock):\n        self.job_name = job_name\n        self.popen = popen\n        self.out_fd = out_fd\n        self.lock = lock\n        super(JobOutputter, self).__init__()\n\n    def __repr__(self):\n        return \"<%s %s>\" % (self.__class__.__name__, self.job_name)\n\n    def run(self):\n        for line in self.popen.stdout:\n            logging.debug(\"%r read %d bytes\", self, len(line))\n            with self.lock:\n                try:\n                    self.out_fd.write(line)\n                except IOError as e:\n                    if e.errno != errno.EPIPE:\n                        logging.exception(\"exception writing to output %r\", self.out_fd)\n                    return os._exit(1)\n\n            logging.debug(\"Got eof on %r\", self)\n\n\ndef hash_select(key, choices):\n    return choices[hash(key) % len(choices)]\n\n\ndef main():\n    try:\n        return _main()\n    except KeyboardInterrupt:\n        # because we mess with threads a lot, we need to make sure that ^C is\n        # actually a nuclear kill\n        os._exit(1)\n\ndef _main():\n    parser = argparse.ArgumentParser()\n    parser.add_argument('-n', metavar='N', type=int,\n                        default=multiprocessing.cpu_count(), dest='nprocs')\n    parser.add_argument('-b', '--buffer', metavar='N', type=parse_size,\n                        help=\"size (in lines) of input buffer for each process\",\n                        default=1024,\n                        dest='bufsize')\n    parser.add_argument('-f', metavar='FIELDSEP', type=str, default='\\t',\n                        dest='field_sep')\n    parser.add_argument('-r', metavar='FIELDRE', type=str, default=None,\n                        dest='field_re')\n    parser.add_argument('--logging', help=argparse.SUPPRESS, default='error')\n    parser.add_argument('cmd', nargs='+')\n\n    args = parser.parse_args()\n\n    if args.field_re and args.field_sep:\n        args.print_usage()\n        return sys.exit(1)\n\n    if args.nprocs == 1:\n        # if you only want one, what do you need me for?\n        os.execvp(args.cmd[0], args.cmd)\n        return sys.exit(1) # will never get here\n\n    if args.field_re:\n        first_field_re = re.compile(args.field_re)\n    else:\n        first_field_re = re.compile('^([^'+re.escape(args.field_sep)+']+)')\n\n    logging.basicConfig(level=getattr(logging, args.logging.upper()))\n\n    stdout_mutex = threading.Lock()\n    processes = []\n\n    for x in range(args.nprocs):\n        logging.debug(\"Starting %r (%d)\", args.cmd, x)\n        ps = subprocess.Popen(args.cmd,\n                              stdin=subprocess.PIPE,\n                              stdout=subprocess.PIPE)\n        psi = JobInputter(x, ps, Queue(maxsize=args.bufsize))\n        pso = JobOutputter(x, ps, sys.stdout, stdout_mutex)\n        psi.start()\n        pso.start()\n        processes.append((psi, pso))\n\n    for line in sys.stdin:\n        if not line:\n            continue\n\n        logging.debug(\"Read %d bytes from stdin\", len(line))\n\n        first_field_m = first_field_re.match(line)\n        first_field = first_field_m.group(0)\n        psi, _pso = hash_select(first_field, processes)\n        logging.debug(\"Writing %d bytes to %r (%r)\", len(line), psi, first_field)\n        psi.iq.put(line)\n\n    logging.debug(\"Hit eof on stdin\")\n\n    for x, (psi, pso) in enumerate(processes):\n        logging.debug(\"Sending terminator to %d (%r)\", x, psi)\n        psi.iq.put(None)\n\n    for x, (psi, pso) in enumerate(processes):\n        logging.debug(\"Waiting for q %d (%r)\", x, psi)\n        psi.iq.join()\n        logging.debug(\"Waiting for psi %d (%r)\", x, psi)\n        psi.join()\n        logging.debug(\"Waiting for pso %d (%r)\", x, psi)\n        pso.join()\n\n    return sys.exit(0)\n\n\nif __name__ == '__main__':\n    main()\n"
  },
  {
    "path": "scripts/inject_test_data.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom __future__ import division\n\nimport collections\nimport HTMLParser\nimport itertools\nimport random\nimport string\nimport time\n\nimport requests\n\nfrom pylons import app_globals as g\n\nfrom r2.lib.db import queries\nfrom r2.lib import amqp\nfrom r2.lib.utils import weighted_lottery, get_requests_resp_json\nfrom r2.lib.voting import cast_vote\nfrom r2.models import (\n    Account,\n    Comment,\n    Link,\n    LocalizedDefaultSubreddits,\n    LocalizedFeaturedSubreddits,\n    NotFound,\n    register,\n    Subreddit,\n    Vote,\n)\n\n\nunescape_htmlentities = HTMLParser.HTMLParser().unescape\n\n\nclass TextGenerator(object):\n    \"\"\"A Markov Chain based text mimicker.\"\"\"\n\n    def __init__(self, order=8):\n        self.order = order\n        self.starts = collections.Counter()\n        self.start_lengths = collections.defaultdict(collections.Counter)\n        self.models = [\n            collections.defaultdict(collections.Counter)\n            for i in xrange(self.order)]\n\n    @staticmethod\n    def _in_groups(input_iterable, n):\n        iterables = itertools.tee(input_iterable, n)\n        for offset, iterable in enumerate(iterables):\n            for _ in xrange(offset):\n                next(iterable, None)\n        return itertools.izip(*iterables)\n\n    def add_sample(self, sample):\n        \"\"\"Add a sample to the model of text for this generator.\"\"\"\n\n        if len(sample) <= self.order:\n            return\n\n        start = sample[:self.order]\n        self.starts[start] += 1\n        self.start_lengths[start][len(sample)] += 1\n        for order, model in enumerate(self.models, 1):\n            for chars in self._in_groups(sample, order+1):\n                prefix = \"\".join(chars[:-1])\n                next_char = chars[-1]\n                model[prefix][next_char] += 1\n\n    def generate(self):\n        \"\"\"Generate a string similar to samples previously fed in.\"\"\"\n\n        start = weighted_lottery(self.starts)\n        desired_length = weighted_lottery(self.start_lengths[start])\n        desired_length = max(desired_length, self.order)\n\n        generated = []\n        generated.extend(start)\n        while len(generated) < desired_length:\n            # try each model, from highest order down, til we find\n            # something\n            for order, model in reversed(list(enumerate(self.models, 1))):\n                current_prefix = \"\".join(generated[-order:])\n                frequencies = model[current_prefix]\n                if frequencies:\n                    generated.append(weighted_lottery(frequencies))\n                    break\n            else:\n                generated.append(random.choice(string.lowercase))\n\n        return \"\".join(generated)\n\n\ndef fetch_listing(path, limit=1000, batch_size=100):\n    \"\"\"Fetch a reddit listing from reddit.com.\"\"\"\n\n    session = requests.Session()\n    session.headers.update({\n        \"User-Agent\": \"reddit-test-data-generator/1.0\",\n    })\n\n    base_url = \"https://api.reddit.com\" + path\n\n    after = None\n    count = 0\n    while count < limit:\n        params = {\"limit\": batch_size, \"count\": count}\n        if after:\n            params[\"after\"] = after\n\n        print \"> {}-{}\".format(count, count+batch_size)\n        response = session.get(base_url, params=params)\n        response.raise_for_status()\n\n        listing = get_requests_resp_json(response)[\"data\"]\n        for child in listing[\"children\"]:\n            yield child[\"data\"]\n            count += 1\n\n        after = listing[\"after\"]\n        if not after:\n            break\n\n        # obey reddit.com's ratelimits\n        # see: https://github.com/reddit/reddit/wiki/API#rules\n        time.sleep(2)\n\n\nclass Modeler(object):\n    def __init__(self):\n        self.usernames = TextGenerator(order=2)\n\n    def model_subreddit(self, subreddit_name):\n        \"\"\"Return a model of links and comments in a given subreddit.\"\"\"\n\n        subreddit_path = \"/r/{}\".format(subreddit_name)\n        print \">>>\", subreddit_path\n\n        print \">> Links\"\n        titles = TextGenerator(order=5)\n        selfposts = TextGenerator(order=8)\n        link_count = self_count = 0\n        urls = set()\n        for link in fetch_listing(subreddit_path, limit=500):\n            self.usernames.add_sample(link[\"author\"])\n            titles.add_sample(unescape_htmlentities(link[\"title\"]))\n            if link[\"is_self\"]:\n                self_count += 1\n                selfposts.add_sample(unescape_htmlentities(link[\"selftext\"]))\n            else:\n                urls.add(link[\"url\"])\n            link_count += 1\n        self_frequency = self_count / link_count\n\n        print \">> Comments\"\n        comments = TextGenerator(order=8)\n        for comment in fetch_listing(subreddit_path + \"/comments\"):\n            self.usernames.add_sample(comment[\"author\"])\n            comments.add_sample(unescape_htmlentities(comment[\"body\"]))\n\n        return SubredditModel(\n            subreddit_name, titles, selfposts, urls, comments, self_frequency)\n\n    def generate_username(self):\n        \"\"\"Generate and return a username like those seen on reddit.com.\"\"\"\n        return self.usernames.generate()\n\n\nclass SubredditModel(object):\n    \"\"\"A snapshot of a subreddit's links and comments.\"\"\"\n\n    def __init__(self, name, titles, selfposts, urls, comments, self_frequency):\n        self.name = name\n        self.titles = titles\n        self.selfposts = selfposts\n        self.urls = list(urls)\n        self.comments = comments\n        self.selfpost_frequency = self_frequency\n\n    def generate_link_title(self):\n        \"\"\"Generate and return a title like those seen in the subreddit.\"\"\"\n        return self.titles.generate()\n\n    def generate_link_url(self):\n        \"\"\"Generate and return a URL from one seen in the subreddit.\n\n        The URL returned may be \"self\" indicating a self post. This should\n        happen with the same frequency it is seen in the modeled subreddit.\n\n        \"\"\"\n        if random.random() < self.selfpost_frequency:\n            return \"self\"\n        else:\n            return random.choice(self.urls)\n\n    def generate_selfpost_body(self):\n        \"\"\"Generate and return a self-post body like seen in the subreddit.\"\"\"\n        return self.selfposts.generate()\n\n    def generate_comment_body(self):\n        \"\"\"Generate and return a comment body like seen in the subreddit.\"\"\"\n        return self.comments.generate()\n\n\ndef fuzz_number(number):\n    return int(random.betavariate(2, 8) * 5 * number)\n\n\ndef ensure_account(name):\n    \"\"\"Look up or register an account and return it.\"\"\"\n    try:\n        account = Account._by_name(name)\n        print \">> found /u/{}\".format(name)\n        return account\n    except NotFound:\n        print \">> registering /u/{}\".format(name)\n        return register(name, \"password\", \"127.0.0.1\")\n\n\ndef ensure_subreddit(name, author):\n    \"\"\"Look up or create a subreddit and return it.\"\"\"\n    try:\n        sr = Subreddit._by_name(name)\n        print \">> found /r/{}\".format(name)\n        return sr\n    except NotFound:\n        print \">> creating /r/{}\".format(name)\n        sr = Subreddit._new(\n            name=name,\n            title=\"/r/{}\".format(name),\n            author_id=author._id,\n            lang=\"en\",\n            ip=\"127.0.0.1\",\n        )\n        sr._commit()\n        return sr\n\n\ndef inject_test_data(num_links=25, num_comments=25, num_votes=5):\n    \"\"\"Flood your reddit install with test data based on reddit.com.\"\"\"\n\n    print \">>>> Ensuring configured objects exist\"\n    system_user = ensure_account(g.system_user)\n    ensure_account(g.automoderator_account)\n    ensure_subreddit(g.default_sr, system_user)\n    ensure_subreddit(g.takedown_sr, system_user)\n    ensure_subreddit(g.beta_sr, system_user)\n    ensure_subreddit(g.promo_sr_name, system_user)\n\n    print\n    print\n\n    print \">>>> Fetching real data from reddit.com\"\n    modeler = Modeler()\n    subreddits = [\n        modeler.model_subreddit(\"pics\"),\n        modeler.model_subreddit(\"videos\"),\n        modeler.model_subreddit(\"askhistorians\"),\n    ]\n    extra_settings = {\n        \"pics\": {\n            \"show_media\": True,\n        },\n        \"videos\": {\n            \"show_media\": True,\n        },\n    }\n\n    print\n    print\n\n    print \">>>> Generating test data\"\n    print \">>> Accounts\"\n    account_query = Account._query(sort=\"_date\", limit=500, data=True)\n    accounts = [a for a in account_query if a.name != g.system_user]\n    accounts.extend(\n        ensure_account(modeler.generate_username())\n        for i in xrange(50 - len(accounts)))\n\n    print \">>> Content\"\n    things = []\n    for sr_model in subreddits:\n        sr_author = random.choice(accounts)\n        sr = ensure_subreddit(sr_model.name, sr_author)\n\n        # make the system user subscribed for easier testing\n        if sr.add_subscriber(system_user):\n            sr._incr(\"_ups\", 1)\n\n        # apply any custom config we need for this sr\n        for setting, value in extra_settings.get(sr.name, {}).iteritems():\n            setattr(sr, setting, value)\n        sr._commit()\n\n        for i in xrange(num_links):\n            link_author = random.choice(accounts)\n            url = sr_model.generate_link_url()\n            is_self = (url == \"self\")\n            content = sr_model.generate_selfpost_body() if is_self else url\n            link = Link._submit(\n                is_self=is_self,\n                title=sr_model.generate_link_title(),\n                content=content,\n                author=link_author,\n                sr=sr,\n                ip=\"127.0.0.1\",\n            )\n            queries.new_link(link)\n            things.append(link)\n\n            comments = [None]\n            for i in xrange(fuzz_number(num_comments)):\n                comment_author = random.choice(accounts)\n                comment, inbox_rel = Comment._new(\n                    comment_author,\n                    link,\n                    parent=random.choice(comments),\n                    body=sr_model.generate_comment_body(),\n                    ip=\"127.0.0.1\",\n                )\n                queries.new_comment(comment, inbox_rel)\n                comments.append(comment)\n                things.append(comment)\n\n    for thing in things:\n        for i in xrange(fuzz_number(num_votes)):\n            direction = random.choice([\n                Vote.DIRECTIONS.up,\n                Vote.DIRECTIONS.unvote,\n                Vote.DIRECTIONS.down,\n            ])\n            voter = random.choice(accounts)\n\n            cast_vote(voter, thing, direction)\n\n    amqp.worker.join()\n\n    srs = [Subreddit._by_name(n) for n in (\"pics\", \"videos\", \"askhistorians\")]\n    LocalizedDefaultSubreddits.set_global_srs(srs)\n    LocalizedFeaturedSubreddits.set_global_srs([Subreddit._by_name('pics')])\n"
  },
  {
    "path": "scripts/manage-consumers",
    "content": "#!/bin/bash\n\ncommand=${UPSTART_JOB#reddit-consumers-}\nfor consumerpath in $REDDIT_CONSUMER_CONFIG/*; do\n    consumer=$(basename $consumerpath)\n\n    # allow targeting which consumer the event is meant for (defaulting to 'all')\n    if [ ! -z \"$TARGET\" -a \"x$TARGET\" != \"xall\" -a \"x$TARGET\" != \"x$consumer\" ]; then\n        continue\n    fi\n\n    if [ -d $consumerpath ]; then\n        types=$consumerpath/*\n    else\n        types=$consumerpath\n    fi\n\n    for typepath in $types; do\n        instance_count=$(cat $typepath)\n        type_=$(basename $typepath)\n\n        for i in $(seq 1 \"$instance_count\"); do\n            \"/sbin/$command\" \"reddit-consumer-$consumer\" \"type=$type_\" \"x=$i\"\n        done\n    done\ndone\n\n"
  },
  {
    "path": "scripts/migrate/backfill/comment_scores_by_link.py",
    "content": "from datetime import datetime\n\nfrom pylons import app_globals as g\n\nfrom r2.models import (\n    CommentSortsCache,\n    CommentScoresByLink,\n)\n\n# estimate 93,233,408 rows in CommentSortsCache\n\n\ndef run():\n    start_time = datetime.now(g.tz)\n    epoch_micro_seconds = int(start_time.strftime(\"%s\")) * 1000000\n    count = 0\n\n    for rowkey, columns in CommentSortsCache._cf.get_range(column_count=1000):\n        # CommentSortsCache rowkey is \"{linkid36}{sort}\"\n        # CommentScoresByLink rowkey is the same\n\n        if len(columns) == 1000:\n            columns = CommentSortsCache._cf.xget(rowkey)\n            # convert str column values to floats\n            float_columns = {k: float(v) for k, v in columns}\n        else:\n            # convert str column values to floats\n            float_columns = {k: float(v) for k, v in columns.iteritems()}\n\n        # write with a timestamp to not overwrite any writes since our read\n        CommentScoresByLink._cf.insert(\n            rowkey, float_columns, timestamp=epoch_micro_seconds)\n\n        count += 1\n        if count % 1000 == 0:\n            print \"processed %s rows, last seen was %s\" % (count, rowkey)\n"
  },
  {
    "path": "scripts/migrate/backfill/fix_preview_images.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\n\"\"\"\nFix the urls of previously-uploaded preview images so they all work.\n\"\"\"\n\nimport sys\n\nimport boto\nimport pycassa\n\nfrom boto.s3.key import Key\nfrom pylons import app_globals as g\n\nfrom r2.lib.media import _get_scrape_url\nfrom r2.lib.providers.media.s3 import S3MediaProvider\nfrom r2.lib.utils import UrlParser\nfrom r2.models.link import Link, LinksByImage\nfrom r2.models.media_cache import MediaByURL\n\ndef good_preview_object(preview_object):\n    if not preview_object or not 'url' in preview_object:\n        print '  aborting - bad preview object: %s' % preview_object\n        return False\n    if not preview_object['url']:\n        print '  aborting - bad preview url: %s' % preview_object['url']\n        return False\n    return True\n\ns3 = boto.connect_s3(g.S3KEY_ID or None, g.S3SECRET_KEY or None)\n\nfor uid, columns in LinksByImage._cf.get_range():\n# When resuming, use:\n#for uid, columns in LinksByImage._cf.get_range(start='<uid>'):\n    print 'Looking at image %s' % uid\n    link_ids = columns.keys()\n    links = Link._byID36(link_ids, return_dict=False, data=True)\n    if not links:\n        continue\n\n    # Pull information about the image from the first link (they *should* all\n    # be the same).\n    link = links[0]\n    preview_object = link.preview_object\n    if not good_preview_object(preview_object):\n        continue\n\n    u = UrlParser(preview_object['url'])\n    if preview_object['url'].startswith(g.media_fs_base_url_http):\n        # Uploaded to the local filesystem instead of s3.  Should only be in\n        # dev.\n        print '  non-s3 image'\n        continue\n    elif u.hostname == 's3.amazonaws.com':\n        parts = u.path.lstrip('/').split('/')\n\n        bucket = parts.pop(0)\n        filename = '/'.join(parts)\n    else:\n        bucket = u.hostname\n        filename = u.path.lstrip('/')\n\n    print '  bucket: %s' % bucket\n    print '  filename: %s' % filename\n\n    if bucket in g.s3_image_buckets:\n        print '  skipping - already in correct place'\n        continue\n\n    k = Key(s3.get_bucket(bucket))\n    k.key = filename\n    k.copy(s3.get_bucket(g.s3_image_buckets[0]), filename)\n    url = 'http://s3.amazonaws.com/%s/%s' % (g.s3_image_buckets[0], filename)\n    print '  new url: %s' % url\n    for link in links:\n        print '  altering Link %s' % link\n        if not good_preview_object(link.preview_object):\n            continue\n        if not link.preview_object == preview_object:\n            print \"  aborting - preview objects don't match\"\n            print '    first: %s' % preview_object\n            print '    ours:  %s' % link.preview_object\n            continue\n\n        link.preview_object['url'] = url\n        link._commit()\n        # Guess at the key that'll contain the (now-incorrect) cache of the\n        # preview object so we can delete it and not end up inserting old info\n        # into new Links.\n        #\n        # These parameters are what's used in most of the code; the only place\n        # they're overridden is for promoted links, where they could be\n        # anything.  We'll just have to deal with those as they come up.\n        image_url = _get_scrape_url(link)\n        cache_key = MediaByURL._rowkey(image_url, autoplay=False, maxwidth=600)\n        print '  deleting cache with key %s' % cache_key\n        cache = MediaByURL(_id=cache_key)\n        cache._committed = True\n        try:\n            cache._destroy()\n        except pycassa.cassandra.ttypes.InvalidRequestException as e:\n            print '    skipping cache deletion (%s)' % e.why\n            continue\n    # Delete *after* we've updated all the Links so they'll continue to work\n    # while we're in the migration process.\n    k.delete()\n"
  },
  {
    "path": "scripts/migrate/backfill/gilded_by_subreddit.py",
    "content": "from collections import defaultdict\nfrom datetime import datetime\n\nfrom pylons import app_globals as g\n\nfrom r2.lib.db.operators import desc\nfrom r2.lib.utils import fetch_things2\nfrom r2.models import (\n    calculate_server_seconds,\n    Comment,\n    Link,\n    Subreddit,\n)\n\nLINK_GILDING_START = datetime(2014, 2, 1, 0, 0, tzinfo=g.tz)\nCOMMENT_GILDING_START = datetime(2012, 10, 1, 0, 0, tzinfo=g.tz)\n\nqueries = [\n    Link._query(\n        Link.c.gildings != 0, Link.c._date > LINK_GILDING_START, data=True,\n        sort=desc('_date'),\n    ),\n    Comment._query(\n        Comment.c.gildings != 0, Comment.c._date > COMMENT_GILDING_START,\n        data=True, sort=desc('_date'),\n    ),\n]\n\nseconds_by_srid = defaultdict(int)\ngilding_price = g.gold_month_price.pennies\n\nfor q in queries:\n    for things in fetch_things2(q, chunks=True, chunk_size=100):\n        print things[0]._fullname\n\n        for thing in things:\n            seconds_per_gilding = calculate_server_seconds(gilding_price, thing._date)\n            seconds_by_srid[thing.sr_id] += int(thing.gildings * seconds_per_gilding)\n\nfor sr_id, seconds in seconds_by_srid:\n    sr = Subreddit._byID(sr_id, data=True)\n    print \"%s: %s seconds\" % (sr.name, seconds)\n    sr._incr(\"gilding_server_seconds\", seconds)\n"
  },
  {
    "path": "scripts/migrate/backfill/gilded_comments.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\"\"\"Fill in the gilded comment listing.\n\nThis listing is stored in get_gilded_comments and seen on /comments/gilded.\n\n\"\"\"\n\nimport datetime\n\nfrom pylons import app_globals as g\n\nfrom r2.lib.db.queries import get_gilded_comments, get_all_gilded_comments\nfrom r2.lib.utils import Storage\nfrom r2.models import GildingsByDay, Thing, Comment\nfrom r2.models.query_cache import CachedQueryMutator\n\n\ndate = datetime.datetime.now(g.tz)\nearliest_date = datetime.datetime(2012, 10, 01, tzinfo=g.tz)\n\nalready_seen = set()\n\nwith CachedQueryMutator() as m:\n    while date > earliest_date:\n        gildings = GildingsByDay.get_gildings(date)\n        fullnames = [x[\"thing\"] for x in gildings]\n        things = Thing._by_fullname(fullnames, data=True, return_dict=False)\n        comments = {t._fullname: t for t in things if isinstance(t, Comment)}\n\n        for gilding in gildings:\n            fullname = gilding[\"thing\"]\n            if fullname in comments and fullname not in already_seen:\n                thing = gilding[\"thing\"] = comments[fullname]\n                gilding_object = Storage(gilding)\n                m.insert(get_gilded_comments(thing.sr_id), [gilding_object])\n                m.insert(get_all_gilded_comments(), [gilding_object])\n                already_seen.add(fullname)\n        date -= datetime.timedelta(days=1)\n"
  },
  {
    "path": "scripts/migrate/backfill/gilded_user_comments.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\"\"\"Fill in the gilded comment listing for users.\n\nThis listing is stored in get_gilded_user_comments and seen on\n/user/<username>/gilded.\n\n\"\"\"\n\nimport datetime\n\nfrom pylons import app_globals as g\n\nfrom r2.lib.db.queries import get_gilded_user_comments\nfrom r2.lib.utils import Storage\nfrom r2.models import GildingsByDay, Thing, Comment\nfrom r2.models.query_cache import CachedQueryMutator\n\n\ndate = datetime.datetime.now(g.tz)\nearliest_date = datetime.datetime(2012, 10, 01, tzinfo=g.tz)\n\nalready_seen = set()\n\nwith CachedQueryMutator() as m:\n    while date > earliest_date:\n        gildings = GildingsByDay.get_gildings(date)\n        fullnames = [x[\"thing\"] for x in gildings]\n        things = Thing._by_fullname(fullnames, data=True, return_dict=False)\n        comments = {t._fullname: t for t in things if isinstance(t, Comment)}\n\n        for gilding in gildings:\n            fullname = gilding[\"thing\"]\n            if fullname in comments and fullname not in already_seen:\n                thing = gilding[\"thing\"] = comments[fullname]\n                gilding_object = Storage(gilding)\n                m.insert(get_gilded_user_comments(thing.author_id),\n                         [gilding_object])\n                already_seen.add(fullname)\n        date -= datetime.timedelta(days=1)\n"
  },
  {
    "path": "scripts/migrate/backfill/modaction_by_srandmod.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\nfrom r2.lib.db.operators import asc\nfrom r2.lib.utils import fetch_things2\nfrom r2.models import ModAction, ModActionBySRActionMod, Subreddit\n\ndef backfill(after=None):\n    q = Subreddit._query(sort=asc('_date'))\n    if after:\n        sr = Subreddit._by_name(after)\n        q = q._after(sr)\n\n    for sr in fetch_things2(q):\n        backfill_sr(sr)\n\n\ndef backfill_sr(sr):\n    print \"processing %s\" % sr.name\n    after = None\n    count = 100\n    q = ModAction.get_actions(sr, after=after, count=count)\n    actions = list(q)\n    while actions:\n        for ma in actions:\n            ModActionBySRActionMod.add_object(ma)\n        q = ModAction.get_actions(sr, after=actions[-1], count=count)\n        actions = list(q)\n"
  },
  {
    "path": "scripts/migrate/backfill/modmsgtime.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\"\"\"Ensure modmsgtime is properly set on all accounts.\n\nSee the comment in Account.is_moderator_somewhere for possible values of this\nattribute now.\n\n\"\"\"\n\nfrom r2.lib.db.operators import desc\nfrom r2.lib.utils import fetch_things2, progress\nfrom r2.models import Account, Subreddit\n\n\nall_accounts = Account._query(sort=desc(\"_date\"))\nfor account in progress(fetch_things2(all_accounts)):\n    is_moderator_somewhere = bool(Subreddit.reverse_moderator_ids(account))\n    if is_moderator_somewhere:\n        if not account.modmsgtime:\n            account.modmsgtime = False\n        else:\n            # the account already has a date for modmsgtime meaning unread mail\n            pass\n    else:\n        account.modmsgtime = None\n    account._commit()\n"
  },
  {
    "path": "scripts/migrate/backfill/msgtime_to_inbox_count.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\"\"\"Converts msgtime for users to inbox_count, for inbox count tracking.\"\"\"\n\nimport sys\n\nfrom r2.lib.db import queries\nfrom r2.lib.db.operators import desc\nfrom r2.lib.utils import fetch_things2, progress\nfrom r2.models import Account, Message\n\nfrom pylons import app_globals as g\n\n\ndef _keep(msg, account):\n    \"\"\"Adapted from listingcontroller.MessageController's keep_fn.\"\"\"\n    if msg._deleted:\n        return False\n\n    if msg._spam and msg.author_id != account._id:\n        return False\n\n    if msg.author_id in account.enemies:\n        return False\n\n    # do not keep messages which were deleted on recipient\n    if (isinstance(msg, Message) and\n            msg.to_id == account._id and msg.del_on_recipient):\n        return False\n\n    # don't show user their own unread stuff\n    if msg.author_id == account._id:\n        return False\n\n    return True\n\nresume_id = long(sys.argv[1]) if len(sys.argv) > 1 else None\n\nmsg_accounts = Account._query(sort=desc(\"_date\"), data=True)\n\nif resume_id:\n    msg_accounts._filter(Account.c._id < resume_id)\n\nfor account in progress(fetch_things2(msg_accounts), estimate=resume_id):\n    current_inbox_count = account.inbox_count\n    unread_messages = list(queries.get_unread_inbox(account))\n\n    if account._id % 100000 == 0:\n        g.reset_caches()\n\n    if not len(unread_messages):\n        if current_inbox_count:\n            account._incr('inbox_count', -current_inbox_count)\n    else:\n        msgs = Message._by_fullname(\n            unread_messages,\n            data=True,\n            return_dict=False,\n            ignore_missing=True,\n        )\n        kept_msgs = sum(1 for msg in msgs if _keep(msg, account))\n\n        if kept_msgs or current_inbox_count:\n            account._incr('inbox_count', kept_msgs - current_inbox_count)\n"
  },
  {
    "path": "scripts/migrate/backfill/num_gildings.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\"\"\"Fill in the num_gildings for users\n\nThis is used to determine which gilding trophy level they should have.\n\"\"\"\nfrom pylons import app_globals as g\n\nfrom r2.models import Account\nfrom r2.models.gold import gold_table, ENGINE\nfrom r2admin.lib.trophies import add_to_trophy_queue\nfrom sqlalchemy.sql.expression import select\nfrom sqlalchemy.sql.functions import count as sa_count\n\n\ndef update_num_gildings(update_trophy=True, user_id=None):\n    \"\"\"Returns total number of link, comment, and user gildings\"\"\"\n    query = (select([gold_table.c.paying_id, sa_count(gold_table.c.trans_id)])\n        .where(gold_table.c.trans_id.like('X%'))\n        .group_by(gold_table.c.paying_id)\n        .order_by(sa_count(gold_table.c.trans_id).desc())\n    )\n    if user_id:\n        query = query.where(gold_table.c.paying_id == str(user_id))\n\n    rows = ENGINE.execute(query)\n    total_updated = 0\n    for paying_id, count in rows:\n        try:\n            a = Account._byID(int(paying_id), data=True)\n            a.num_gildings = count\n            a._commit()\n            total_updated += 1\n            #if 'server seconds paid' for are public, update gilding trophies\n            if update_trophy and a.pref_public_server_seconds:\n                add_to_trophy_queue(a, \"gilding\")\n        except:\n            g.log.debug(\"update_num_gildings: paying_id %s is invalid\" % paying_id)\n\n    g.log.debug(\"update_num_gildings: updated %s accounts\" % total_updated)\n"
  },
  {
    "path": "scripts/migrate/backfill/scrub_deleted_users.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\n\"\"\"\nScript for backunfilling data from deleted users.\n\nYou might want to change `run_changed()` to `run_changed(use_safe_get=True)`\nin `reddit-consumer-cloudsearch_q.conf` unless you're sure *everything* in\n`LinksByAccount` is a valid `Link`. Otherwise, you're gonna back up the\ncloudsearch queue.\n\"\"\"\n\nimport time\nimport sys\n\nfrom r2.lib.db.operators import desc\nfrom r2.lib.utils import fetch_things2, progress\nfrom r2.lib import amqp\nfrom r2.models import Account\n\n\ndef get_queue_length(name):\n    # https://stackoverflow.com/questions/1038318/check-rabbitmq-queue-size-from-client\n    chan = amqp.connection_manager.get_channel()\n    queue_response = chan.queue_declare(name, passive=True)\n    return queue_response[1]\n\n\ndef backfill_deleted_accounts(resume_id=None):\n    del_accts = Account._query(Account.c._deleted == True, sort=desc('_date'))\n    if resume_id:\n        del_accts._filter(Account.c._id < resume_id)\n\n    for i, account in enumerate(progress(fetch_things2(del_accts))):\n        # Don't kill the rabbit! Wait for the relevant queues to calm down.\n        if i % 1000 == 0:\n            del_len = get_queue_length('del_account_q')\n            cs_len = get_queue_length('cloudsearch_changes')\n            while (del_len > 1000 or\n                    cs_len > 10000):\n                sys.stderr.write((\"CS: %d, DEL: %d\" % (cs_len, del_len)) + \"\\n\")\n                sys.stderr.flush()\n                time.sleep(1)\n                del_len = get_queue_length('del_account_q')\n                cs_len = get_queue_length('cloudsearch_changes')\n        amqp.add_item('account_deleted', account._fullname)\n"
  },
  {
    "path": "scripts/migrate/backfill/srmember_to_cassandra.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nfrom collections import defaultdict\n\nimport time\n\nfrom r2.lib.db.operators import desc\nfrom r2.lib.utils import fetch_things2, to36\nfrom r2.models.subreddit import SRMember, SubscribedSubredditsByAccount\n\n\ndef get_query(after_user_id):\n    q = SRMember._query(\n        SRMember.c._name == \"subscriber\",\n        SRMember.c._thing2_id < after_user_id,\n        sort=desc(\"_thing2_id\"),\n    )\n    return q\n\n\ndef get_srmembers(after_user_id):\n    previous_user_id = None\n\n    while True:\n        # there isn't a good index on rel_id so we need to get a new query\n        # for each batch rather than relying solely on fetch_things2\n        q = get_query(after_user_id)\n        users_seen = 0\n\n        for rel in fetch_things2(q):\n            user_id = rel._thing2_id\n\n            if user_id != previous_user_id:\n                if users_seen >= 20:\n                    # set after_user_id to the previous id so we will pick up\n                    # the query at this same point\n                    after_user_id = previous_user_id\n                    break\n\n                users_seen += 1\n                previous_user_id = user_id\n\n            yield rel\n\n\ndef migrate_srmember_subscribers(after_user_id=39566712):\n    columns = {}\n    rowkey = None\n    proc_time = time.time()\n\n    for i, rel in enumerate(get_srmembers(after_user_id)):\n        sr_id = rel._thing1_id\n        user_id = rel._thing2_id\n        action_date = rel._date\n        new_rowkey = to36(user_id)\n\n        if new_rowkey != rowkey and columns:\n            SubscribedSubredditsByAccount._cf.insert(\n                rowkey, columns, timestamp=1434403336829573)\n            columns = {}\n\n        columns[to36(sr_id)] = action_date\n        rowkey = new_rowkey\n\n        if i % 1000 == 0:\n            new_proc_time = time.time()\n            duration = new_proc_time - proc_time\n            print \"%s (%.3f): %s - %s\" % (i, duration, user_id, action_date)\n            proc_time = new_proc_time\n"
  },
  {
    "path": "scripts/migrate/backfill/subreddit_images.py",
    "content": "\n# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport urllib2\n\nfrom pylons import app_globals as g\n\nfrom r2.lib.db.operators import desc\nfrom r2.lib.utils import fetch_things2\nfrom r2.lib.media import upload_media\nfrom r2.models.subreddit import Subreddit\nfrom r2.models.wiki import WikiPage, ImagesByWikiPage\n\n\nall_subreddits = Subreddit._query(sort=desc(\"_date\"))\nfor sr in fetch_things2(all_subreddits):\n    images = sr.images.copy()\n    images.pop(\"/empties/\", None)\n\n    if not images:\n        continue\n\n    print 'Processing /r/%s (id36: %s)' % (sr.name, sr._id36)\n\n    # upgrade old-style image ids to urls\n    for name, image_url in images.items():\n        if not isinstance(image_url, int):\n            continue\n\n        print \"  upgrading image %r\" % image_url\n        url = \"http://%s/%s_%d.png\" % (g.s3_old_thumb_bucket,\n                                       sr._fullname, image_url)\n        image_data = urllib2.urlopen(url).read()\n        new_url = upload_media(image_data, file_type=\".png\")\n        images[name] = new_url\n\n    # use a timestamp of zero to make sure that we don't overwrite any changes\n    # from live dual-writes.\n    rowkey = WikiPage.id_for(sr, \"config/stylesheet\")\n    ImagesByWikiPage._cf.insert(rowkey, images, timestamp=0)\n"
  },
  {
    "path": "scripts/migrate/backfill/user_gildings.py",
    "content": "# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\"\"\"Fill in the gildings listing for users.\n\nThis listing is stored in get_user_gildings and seen on\n/user/<username>/gildings.\n\n\"\"\"\n\nimport datetime\n\nfrom pylons import app_globals as g\n\nfrom r2.lib.db.queries import get_user_gildings\nfrom r2.lib.utils import Storage\nfrom r2.models import GildingsByDay, Thing, Comment\nfrom r2.models.query_cache import CachedQueryMutator\n\n\ndate = datetime.datetime.now(g.tz)\nearliest_date = datetime.datetime(2012, 10, 01, tzinfo=g.tz)\n\nalready_seen = set()\n\nwith CachedQueryMutator() as m:\n    while date > earliest_date:\n        gildings = GildingsByDay.get_gildings(date)\n        fullnames = [x[\"thing\"] for x in gildings]\n        things = Thing._by_fullname(fullnames, data=True, return_dict=False)\n        comments = {t._fullname: t for t in things if isinstance(t, Comment)}\n\n        for gilding in gildings:\n            fullname = gilding[\"thing\"]\n            if fullname in comments and fullname not in already_seen:\n                thing = gilding[\"thing\"] = comments[fullname]\n                gilding_object = Storage(gilding)\n                m.insert(get_user_gildings(gilding[\"user\"]), [gilding_object])\n                already_seen.add(fullname)\n        date -= datetime.timedelta(days=1)\n"
  },
  {
    "path": "scripts/migrate/comment-participation.pig",
    "content": "-- Backfill data for CommentParticipationByAccount CF.\n\n%default SCRIPT_ROOT 'udfs/dist/lib'\n%default INPUT 'input'\n%default OUTPUT 'output'\n\n\nREGISTER '$SCRIPT_ROOT/reddit-pig-udfs.jar';\n\n\nitems =\nLOAD '$INPUT/comment.dump'\n    USING PigStorage()\n    AS (id:long,\n        ups:int,\n        downs:int,\n        deleted:chararray,\n        spam:chararray,\n        timestamp:double);\n\n\ndata =\nLOAD '$INPUT/comment-data.dump'\n    USING PigStorage()\n    AS (id:long,\n        key:chararray,\n        value);\n\n\ngrouped_with_data =\nCOGROUP items BY id, data BY id;\n\n\nitems_with_data =\nFOREACH grouped_with_data\n    GENERATE FLATTEN(items),\n             com.reddit.pig.MAKE_MAP(data.(key, value)) AS data;\n\n\ncomments_unfiltered =\nFOREACH items_with_data\n    GENERATE (long)data#'link_id' as link_id,\n             (long)data#'author_id' as author_id;\n\n\nlink_x_author_full =\nFILTER comments_unfiltered\n    BY link_id IS NOT NULL AND\n       author_id IS NOT NULL;\n\n\nlink_x_author =\nDISTINCT link_x_author_full;\n\n\ncolumns =\nFOREACH link_x_author\n    GENERATE com.reddit.pig.TO_36(author_id) AS rowkey,\n             com.reddit.pig.TO_36(link_id) AS name,\n             '';\n\n\nSTORE columns INTO '$OUTPUT/CommentParticipationByAccount/';\n"
  },
  {
    "path": "scripts/migrate/dump-all.sh",
    "content": "#!/bin/bash\n\nset -x -e\n\nRELS=savehide vote_account_link inbox_account_comment inbox_account_message moderatorinbox\nTHINGS=link comment message\n\nfor rel in $RELS; do\n    ./dump-rel.sh $rel\ndone\n\nfor thing in $THINGS; do\n    ./dump-thing.sh $thing\ndone\n"
  },
  {
    "path": "scripts/migrate/dump-rel.sh",
    "content": "#!/bin/bash\n\nset -x\nset -e\n\nRELNAME=$1\n\ncat <<DUMP_REL | ./run-query.sh\nSET enable_bitmapscan=false;\n\n\\\\copy(SELECT rel_id, thing1_id, thing2_id, name, EXTRACT(epoch FROM date) FROM reddit_rel_$RELNAME) to 'input/$RELNAME.dump';\nDUMP_REL\n\ncat <<DUMP_RELDATA | ./run-query.sh\nSET enable_bitmapscan=false;\n\n\\\\copy (SELECT thing_id, key, value FROM reddit_data_rel_$RELNAME WHERE key = 'new') to 'input/$RELNAME-data.dump';\nDUMP_RELDATA\n"
  },
  {
    "path": "scripts/migrate/dump-thing.sh",
    "content": "#!/bin/bash\n\nset -x\nset -e\n\nTHING=$1\n\ncat <<DUMP_THING | ./run-query.sh\nSET enable_bitmapscan=false;\n\n\\\\copy (SELECT thing_id, ups, downs, deleted, spam, EXTRACT(epoch FROM date) FROM reddit_thing_$THING) to 'input/$THING.dump';\nDUMP_THING\n\ncat <<DUMP_THINGDATA | ./run-query.sh\nSET enable_bitmapscan=false;\n\n\\\\copy (SELECT thing_id, key, value FROM reddit_data_$THING WHERE key IN ('author_id', 'reported', 'sr_id', 'url', 'verdict', 'link_id')) to 'input/$THING-data.dump';\nDUMP_THINGDATA\n"
  },
  {
    "path": "scripts/migrate/example.sh",
    "content": "#!/bin/bash\n# This is an example of how regenerating the query cache\n# works. You will need to configure several things first:\n#\n# * configure postgres environment, see run-query.sh\n# * prepare pig:\n#     * make sure the environment is set up for pig\n#     * rearrange the values in TypeIDs.java to match your\n#       instance's IDs\n#     * compile the pig UDFs (ant build in udfs/)\n# * ensure the Cassandra JARs are on the CLASSPATH\n\n# dump the relevant postgres tables to input/\n./dump-all.sh\n\n# process the postgres dumps in hadoop and generate\n# output data suitable for cassandra\npig regenerate-query-cache.py\n\n# for each column family we'll be writing to, generate\n# sstables from the map-reduce output\nfor $cf in $(ls output/); do\n    jython tuples_to_sstables.py $cf output/$cf/*/part*\ndone\n\n# bulk-load the sstables into cassandra\nsstableloader reddit/\n"
  },
  {
    "path": "scripts/migrate/regenerate-query-cache.py",
    "content": "#!/usr/bin/python\n\nfrom org.apache.pig.scripting import Pig\n\n\nSCRIPT_ROOT = \"udfs/dist/lib/\"\nINPUT_ROOT = \"input/\"\nOUTPUT_ROOT = \"output\"\n\nrelations = {\"savehide\": (\"UserQueryCache\", \"link\"),\n             \"inbox_account_comment\": (\"UserQueryCache\", \"comment\"),\n             \"inbox_account_message\": (\"UserQueryCache\", \"message\"),\n             \"moderatorinbox\": (\"SubredditQueryCache\", \"message\"),\n             \"vote_account_link\": (\"UserQueryCache\", \"link\"),\n            }\n\n####### Pig script fragments\nload_things = \"\"\"\nthings =\nLOAD '$THINGS'\n    USING PigStorage()\n    AS (id:long,\n        ups:int,\n        downs:int,\n        deleted:chararray,\n        spam:chararray,\n        timestamp:double);\n\"\"\"\n\nmake_things_items = \"\"\"\nitems =\nFOREACH things GENERATE *;\n\"\"\"\n\nload_rels = \"\"\"\nitems =\nLOAD '$RELS'\n    USING PigStorage()\n    AS (id:long,\n        thing1_id:long,\n        thing2_id:long,\n        name:chararray,\n        timestamp:double);\n\"\"\"\n\n\nload_and_map_data = \"\"\"\ndata =\nLOAD '$DATA'\n    USING PigStorage()\n    AS (id:long,\n        key:chararray,\n        value);\n\ngrouped_with_data =\nCOGROUP items BY id, data BY id;\n\nitems_with_data =\nFOREACH grouped_with_data\n    GENERATE FLATTEN(items),\n             com.reddit.pig.MAKE_MAP(data.(key, value)) AS data;\n\"\"\"\n\nadd_unread = \"\"\"\nSPLIT items_with_data\n    INTO inbox IF 1 == 1,\n         unread IF (chararray)data#'new' == 't';\n\ninbox_with_relname =\nFOREACH inbox GENERATE '$RELATION' AS relation, *;\n\nunread_with_relname =\nFOREACH unread GENERATE '$RELATION:unread' AS relation, *;\n\nrels_with_relname =\nUNION ONSCHEMA inbox_with_relname,\n               unread_with_relname;\n\"\"\"\n\nadd_relname = \"\"\"\nrels_with_relname =\nFOREACH items GENERATE '$RELATION' AS relation, *;\n\"\"\"\n\ngenerate_rel_items = \"\"\"\nminimal_things =\nFOREACH things GENERATE id, deleted;\n\njoined =\nJOIN rels_with_relname BY thing2_id LEFT OUTER,\n     minimal_things BY id;\n\nonly_valid =\nFILTER joined BY minimal_things::id IS NOT NULL AND\n                 deleted == 'f';\n\npotential_columns =\nFOREACH only_valid\n    GENERATE com.reddit.pig.MAKE_ROWKEY(relation, name, thing1_id) AS rowkey,\n             com.reddit.pig.MAKE_THING2_FULLNAME(relation, thing2_id) AS colkey,\n             timestamp AS value;\n\"\"\"\n\nstore_top_1000_per_rowkey = \"\"\"\nnon_null =\nFILTER potential_columns BY rowkey IS NOT NULL AND colkey IS NOT NULL;\n\ngrouped =\nGROUP non_null BY rowkey;\n\nlimited =\nFOREACH grouped {\n    sorted = ORDER non_null BY value DESC;\n    limited = LIMIT sorted 1000;\n    GENERATE group AS rowkey, FLATTEN(limited.(colkey, value));\n};\n\njsonified =\nFOREACH limited GENERATE rowkey,\n                         colkey,\n                         com.reddit.pig.TO_JSON(value);\n\nSTORE jsonified INTO '$OUTPUT' USING PigStorage();\n\"\"\"\n\n###### run the jobs\n# register the reddit udfs\nPig.registerJar(SCRIPT_ROOT + \"reddit-pig-udfs.jar\")\n\n# process rels\nfor rel, (cf, thing2_type) in relations.iteritems():\n    # build source for a script\n    script = \"SET default_parallel 10;\"\n    script += load_rels\n    if \"inbox\" in rel:\n        script += load_and_map_data\n        script += add_unread\n    else:\n        script += add_relname\n    script += load_things\n    script += generate_rel_items\n    script += store_top_1000_per_rowkey\n\n    # run it\n    compiled = Pig.compile(script)\n    bound = compiled.bind({\n        \"RELS\": INPUT_ROOT + rel + \".dump\",\n        \"DATA\": INPUT_ROOT + rel + \"-data.dump\",\n        \"THINGS\": INPUT_ROOT + thing2_type + \".dump\",\n        \"RELATION\": rel,\n        \"OUTPUT\": \"/\".join((OUTPUT_ROOT, cf, rel)),\n    })\n    bound.runSingle()\n\n# rebuild message-based queries (just get_sent right now)\nif False:\n    script = \"SET default_parallel 10;\"\n    script += load_things\n    script += make_things_items\n    script += load_and_map_data\n    script += \"\"\"\n    non_null =\n    FILTER items_with_data BY data#'author_id' IS NOT NULL;\n\n    potential_columns =\n    FOREACH non_null\n    GENERATE\n        CONCAT('sent.', com.reddit.pig.TO_36(data#'author_id')) AS rowkey,\n        com.reddit.pig.MAKE_FULLNAME('message', id) AS colkey,\n        timestamp AS value;\n    \"\"\"\n    script += store_top_1000_per_rowkey\n    compiled = Pig.compile(script)\n    bound = compiled.bind({\n        \"THINGS\": INPUT_ROOT + \"message.dump\",\n        \"DATA\": INPUT_ROOT + \"message-data.dump\",\n        \"OUTPUT\": \"/\".join((OUTPUT_ROOT, \"UserQueryCache\", \"sent\")),\n    })\n    result = bound.runSingle()\n\n# rebuild comment-based queries\nif True:\n    script = \"SET default_parallel 10;\"\n    script += load_things\n    script += make_things_items\n    script += load_and_map_data\n    script += \"\"\"\n    SPLIT items_with_data INTO\n        spam_comments IF spam == 't',\n        ham_comments IF spam == 'f';\n\n    ham_comments_with_name =\n    FOREACH ham_comments GENERATE 'sr_comments' AS name, *;\n\n    reported_comments =\n    FILTER ham_comments BY (int)data#'reported' > 0;\n\n    reported_comments_with_name =\n    FOREACH reported_comments GENERATE 'reported_comments' AS name, *;\n\n    spam_comments_with_name =\n    FOREACH spam_comments GENERATE 'spam_comments' AS name, *;\n\n    comments_with_name =\n    UNION ONSCHEMA ham_comments_with_name,\n                   reported_comments_with_name,\n                   spam_comments_with_name;\n\n    potential_columns =\n    FOREACH comments_with_name GENERATE\n        CONCAT(name, CONCAT('.', com.reddit.pig.TO_36(data#'sr_id'))) AS rowkey,\n        com.reddit.pig.MAKE_FULLNAME('comment', id) AS colkey,\n        timestamp AS value;\n    \"\"\"\n    script += store_top_1000_per_rowkey\n    compiled = Pig.compile(script)\n    bound = compiled.bind({\n        \"THINGS\": INPUT_ROOT + \"comment.dump\",\n        \"DATA\": INPUT_ROOT + \"comment-data.dump\",\n        \"OUTPUT\": \"/\".join((OUTPUT_ROOT, \"SubredditQueryCache\", \"comment\")),\n    })\n    result = bound.runSingle()\n"
  },
  {
    "path": "scripts/migrate/run-query.sh",
    "content": "#!/bin/bash\n\nset -x\npsql -h ${DB_HOST:-localhost} \\\n     -d ${DB_NAME:-reddit} \\\n     -U ${DB_USER:-reddit} \\\n     -p ${DB_PORT:-5432} \\\n     -F\"\\t\" -A -t\n"
  },
  {
    "path": "scripts/migrate/tuples_to_sstables.py",
    "content": "#!/usr/bin/jython\n\"\"\"A jython script that takes as input a set of tuples meant to be\nbulk-loaded into Cassandra, and outputs a set of sstables usable\nby Cassandra's sstableloader.\n\nThe Cassandra jars and configuration must be on the classpath for this\nto function properly.\n\"\"\"\n\nimport time\nimport os\nimport os.path\nimport logging\n\nfrom org.apache.cassandra.utils.ByteBufferUtil import bytes\nfrom java.nio import ByteBuffer\n\n\ndef utf8(val):\n    return bytes(val)\n\ndef datetime(val):\n    milliseconds = long(float(val) * 1e3)\n    return ByteBuffer.allocate(8).putLong(0, milliseconds)\n\nCOERCERS = dict(utf8=utf8,\n                datetime=datetime)\n\n\ndef convert_to_sstables(input_files, column_family,\n                        output_dir_name, keyspace, timestamp, buffer_size,\n                        data_type, verbose=False):\n    import fileinput\n    from java.io import File\n    from org.apache.cassandra.io.sstable import SSTableSimpleUnsortedWriter\n    from org.apache.cassandra.db.marshal import AsciiType\n    from org.apache.cassandra.service import StorageService\n    from org.apache.cassandra.io.compress import CompressionParameters\n\n    partitioner = StorageService.getPartitioner()\n\n    try:\n        coercer = COERCERS[data_type]\n    except KeyError:\n        raise ValueError(\"invalid data type\")\n\n    output_dir = File(output_dir_name)\n\n    if not output_dir.exists():\n        output_dir.mkdir()\n\n    compression_options = CompressionParameters.create({\n        'sstable_compression': 'org.apache.cassandra.io.compress.SnappyCompressor',\n        'chunk_length_kb': '64'\n    })\n\n    writer = SSTableSimpleUnsortedWriter(output_dir,\n                                         partitioner,\n                                         keyspace,\n                                         column_family,\n                                         AsciiType.instance,\n                                         None,\n                                         buffer_size,\n                                         compression_options)\n\n\n    try:\n        previous_rowkey = None\n        for line in fileinput.input(input_files):\n            ttl = None\n\n            t_columns = line.rstrip(\"\\n\").split(\"\\t\")\n            if len(t_columns) == 3:\n                rowkey, colkey, value = t_columns\n            elif len(t_columns) == 4:\n                rowkey, colkey, value, ttl = t_columns\n                ttl = int(ttl)\n            else:\n                raise Exception(\"unknown data format for %r\" % (t_columns,))\n\n            if rowkey != previous_rowkey:\n                writer.newRow(bytes(rowkey))\n\n            coerced = coercer(value)\n\n            if ttl is None:\n                writer.addColumn(bytes(colkey), coerced, timestamp)\n            else:\n                # see\n                # https://svn.apache.org/repos/asf/cassandra/trunk/src/java/org/apache/cassandra/io/sstable/AbstractSSTableSimpleWriter.java addExpiringColumn:expirationTimestampMS\n                # for explanation\n                expirationTimestampMS = (timestamp / 1000) + (ttl * 1000)\n                writer.addExpiringColumn(bytes(colkey), coerced,\n                                         timestamp, ttl, expirationTimestampMS)\n\n            if verbose and fileinput.lineno() % 10000 == 0:\n                print \"%d items processed (%s)\" % (fileinput.lineno(),\n                                                   fileinput.filename())\n    except:\n        # it's common that whatever causes us to fail also cases the finally\n        # clause below to fail, which masks the original exception\n        logging.exception(\"Failed\")\n        raise\n    finally:\n        writer.close()\n\n\ndef main():\n    import os\n    import optparse\n\n    parser = optparse.OptionParser(\n        usage=\"USAGE: tuples_to_sstables [options] COLUMN_FAMILY INPUT [...]\")\n    parser.add_option(\"--timestamp\",\n                      type=\"long\",\n                      nargs=1, dest=\"timestamp\",\n                      default=int(time.time()*1000000),\n                      help=\"timestamp to use for each column\")\n    parser.add_option(\"--buffer-size\",\n                      type=\"int\",\n                      nargs=1, dest=\"buffer_size\", default=128,\n                      help=\"size in MB to buffer before writing SSTables\")\n    parser.add_option(\"--data-type\",\n                      nargs=1, dest=\"data_type\", default=\"utf8\",\n                      help=\"type to coerce data into for column values\")\n    parser.add_option(\"-k\", \"--keyspace\",\n                      nargs=1, dest=\"keyspace\", default=\"reddit\",\n                      help=\"the name of the keyspace the data is for\")\n    parser.add_option(\"-o\", \"--output-root\",\n                      nargs=1, dest=\"output_root\", default=\".\",\n                      help=\"the root directory to write the SSTables into\")\n    parser.add_option(\"-v\", \"--verbose\",\n                      action=\"store_true\", dest=\"verbose\", default=False,\n                      help=\"print status messages to stdout\")\n\n    options, args = parser.parse_args()\n    options = dict(options.__dict__) # in jython, __dict__ is a StringMap\n    options[\"output_dir_name\"] = os.path.join(options.pop(\"output_root\"))\n    options[\"column_family\"], input_files = args[0], args[1:]\n\n    return convert_to_sstables(input_files, **options)\n\n\nif __name__ == \"__main__\":\n    import sys\n    sys.exit(main())\n"
  },
  {
    "path": "scripts/migrate/udfs/build.xml",
    "content": "<project name=\"reddit Pig UDFs\" default=\"dist\" basedir=\".\">\n    <property name=\"src\" location=\"src/\" />\n    <property name=\"build\" location=\"build/\" />\n    <property name=\"dist\" location=\"dist/\" />\n    <property environment=\"env\" />\n\n    <target name=\"init\">\n        <tstamp />\n        <mkdir dir=\"${build}\" />\n    </target>\n\n    <target name=\"compile\" depends=\"init\">\n        <javac srcdir=\"${src}\" destdir=\"${build}\" includeantruntime=\"false\">\n            <classpath>\n                <pathelement location=\"${env.PIG_HOME}/pig.jar\" />\n            </classpath>\n        </javac>\n    </target>\n\n    <target name=\"dist\" depends=\"compile\">\n        <mkdir dir=\"${dist}/lib\" />\n        <jar jarfile=\"${dist}/lib/reddit-pig-udfs.jar\" basedir=\"${build}\" />\n    </target>\n\n    <target name=\"clean\">\n        <delete dir=\"${build}\" />\n        <delete dir=\"${dist}\" />\n    </target>\n</project>\n"
  },
  {
    "path": "scripts/migrate/udfs/src/com/reddit/pig/MAKE_FULLNAME.java",
    "content": "package com.reddit.pig;\n\nimport java.io.IOException;\nimport org.apache.pig.EvalFunc;\nimport org.apache.pig.data.Tuple;\n\n\npublic class MAKE_FULLNAME extends EvalFunc<String> {\n    public String exec(Tuple input) throws IOException {\n        String name = (String)input.get(0);\n        Long id = (Long)input.get(1);\n        TypeID typeId = this.getTypeID(name);\n        if (typeId == null)\n            return null;\n        return String.format(\"t%d_%s\", typeId.ordinal(), Long.toString(id, 36));\n    }\n\n    private TypeID getTypeID(String thingName) {\n        String enumName = thingName.toUpperCase();\n        return TypeID.valueOf(enumName);\n    }\n}\n"
  },
  {
    "path": "scripts/migrate/udfs/src/com/reddit/pig/MAKE_MAP.java",
    "content": "package com.reddit.pig;\n\nimport java.util.Map;\nimport java.util.HashMap;\nimport java.lang.System;\nimport java.io.IOException;\nimport org.apache.pig.EvalFunc;\nimport org.apache.pig.data.Tuple;\nimport org.apache.pig.data.DataBag;\n\n\npublic class MAKE_MAP extends EvalFunc<Map<Object, Object>> {\n    public Map<Object, Object> exec(Tuple input) throws IOException {\n        Map<Object, Object> map = new HashMap<Object, Object>();\n\n        DataBag bag = (DataBag)input.get(0);\n        for (Tuple tuple : bag) {\n            String key = (String)tuple.get(0);\n            Object value = tuple.get(1);\n\n            map.put(key, value.toString());\n        }\n\n        return map;\n    }\n}\n"
  },
  {
    "path": "scripts/migrate/udfs/src/com/reddit/pig/MAKE_ROWKEY.java",
    "content": "package com.reddit.pig;\n\nimport java.io.IOException;\nimport org.apache.pig.EvalFunc;\nimport org.apache.pig.data.Tuple;\n\n\npublic class MAKE_ROWKEY extends EvalFunc<String> {\n    public String exec(Tuple input) throws IOException {\n        String rel = (String)input.get(0);\n        String name = (String)input.get(1);\n        Long id = (Long)input.get(2);\n\n        String queryName = MAKE_ROWKEY.getQueryName(rel, name);\n        if (queryName == null) {\n            return null;\n        }\n\n        return queryName + \".\" + Long.toString(id, 36);\n    }\n\n    private static String getQueryName(String rel, String name) {\n        if (name.equals(\"1\")) {\n            if (rel.equals(\"vote_account_link\"))\n                return \"liked\";\n        } else if (name.equals(\"-1\")) {\n            if (rel.equals(\"vote_account_link\"))\n                return \"disliked\";\n        } else if (name.equals(\"save\")) {\n            if (rel.equals(\"savehide\"))\n                return \"saved\";\n        } else if (name.equals(\"hide\")) {\n            if (rel.equals(\"savehide\"))\n                return \"hidden\";\n        } else if (name.equals(\"inbox\")) {\n            if (rel.equals(\"inbox_account_comment\")) {\n                return \"inbox_comments\";\n            } else if (rel.equals(\"inbox_account_message\")) {\n                return \"inbox_messages\";\n            } else if (rel.equals(\"moderatorinbox\")) {\n                return \"subreddit_messages\";\n            } else if (rel.equals(\"inbox_account_comment:unread\")) {\n                return \"unread_comments\";\n            } else if (rel.equals(\"inbox_account_message:unread\")) {\n                return \"unread_messages\";\n            } else if (rel.equals(\"moderatorinbox:unread\")) {\n                return \"unread_subreddit_messages\";\n            }\n        } else if (name.equals(\"selfreply\")) {\n            if (rel.equals(\"inbox_account_comment\")) {\n                return \"inbox_selfreply\";\n            } else if (rel.equals(\"inbox_account_comment:unread\")) {\n                return \"unread_selfreply\";\n            }\n        }\n\n        return null;\n    }\n}\n"
  },
  {
    "path": "scripts/migrate/udfs/src/com/reddit/pig/MAKE_THING2_FULLNAME.java",
    "content": "package com.reddit.pig;\n\nimport java.io.IOException;\nimport org.apache.pig.EvalFunc;\nimport org.apache.pig.data.Tuple;\n\n\npublic class MAKE_THING2_FULLNAME extends MAKE_FULLNAME {\n    private TypeID getTypeID(String rel) {\n        if (rel.equals(\"savehide\")) {\n            return TypeID.LINK;\n        } else if (rel.startsWith(\"inbox_account_comment\")) {\n            return TypeID.COMMENT;\n        } else if (rel.startsWith(\"inbox_account_message\")) {\n            return TypeID.MESSAGE;\n        } else if (rel.startsWith(\"moderatorinbox\")) {\n            return TypeID.MESSAGE;\n        } else if (rel.equals(\"vote_account_link\")) {\n            return TypeID.LINK;\n        }\n\n        return null;\n    }\n}\n"
  },
  {
    "path": "scripts/migrate/udfs/src/com/reddit/pig/TO_36.java",
    "content": "package com.reddit.pig;\n\nimport java.lang.Number;\nimport java.io.IOException;\nimport org.apache.pig.EvalFunc;\nimport org.apache.pig.data.Tuple;\n\n\npublic class TO_36 extends EvalFunc<String> {\n    public String exec(Tuple input) throws IOException {\n        Object obj = input.get(0);\n        Number number;\n\n        if (obj instanceof Number) {\n            number = (Number)obj;\n        } else {\n            number = Long.decode(obj.toString());\n        }\n\n        return Long.toString(number.longValue(), 36);\n    }\n}\n"
  },
  {
    "path": "scripts/migrate/udfs/src/com/reddit/pig/TO_JSON.java",
    "content": "package com.reddit.pig;\n\nimport java.lang.*;\nimport java.io.IOException;\nimport org.apache.pig.EvalFunc;\nimport org.apache.pig.data.Tuple;\n\n\npublic class TO_JSON extends EvalFunc<String> {\n    public String exec(Tuple input) throws IOException {\n        StringBuilder builder = new StringBuilder(\"[\");\n\n        int size = input.size();\n        for (int i = 0; i < size; i++) {\n            Object obj = input.get(i);\n\n            // TODO: support integers and strings\n            if (obj == null) {\n                builder.append(\"null\");\n            } else if (obj instanceof Double || obj instanceof Float) {\n                String formatted = String.format(\"%f\", obj);\n                builder.append(formatted);\n            } else {\n                throw new UnsupportedOperationException(\"can only encode nulls and floating point numbers\");\n            }\n\n            if (i + 1 != size)\n                builder.append(\",\");\n        }\n\n        builder.append(\"]\");\n        return builder.toString();\n    }\n}\n"
  },
  {
    "path": "scripts/migrate/udfs/src/com/reddit/pig/TypeID.java",
    "content": "package com.reddit.pig;\n\n\n// customize these to match your instance's typeids\nenum TypeID {\n    INVALID,\n    COMMENT,\n    ACCOUNT,\n    LINK,\n    MESSAGE,\n    SUBREDDIT,\n}\n"
  },
  {
    "path": "scripts/promoted_links.py",
    "content": "#!/usr/bin/python\n# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\"\"\"Tools for evaluating promoted link distribution.\"\"\"\n\nfrom collections import defaultdict\nimport datetime\nfrom math import sqrt\n\nfrom pylons import app_globals as g\nfrom sqlalchemy.sql.functions import sum as sa_sum\n\nfrom r2.lib import promote\nfrom r2.lib.db.operators import and_, or_\nfrom r2.lib.utils import to36, weighted_lottery\nfrom r2.models.traffic import (\n    Session,\n    TargetedImpressionsByCodename,\n    PageviewsBySubredditAndPath,\n)\nfrom r2.models.bidding import PromotionWeights\nfrom r2.models import (\n    Link,\n    PromoCampaign,\n    DefaultSR,\n)\n\nLINK_PREFIX = Link._type_prefix + str(Link._type_id)\nPC_PREFIX = PromoCampaign._type_prefix + str(PromoCampaign._type_id)\n\n\ndef error_statistics(errors):\n    mean_error = sum(errors) / len(errors)\n    min_error = min([abs(i) for i in errors])\n    max_error = max([abs(i) for i in errors])\n    stdev_error = sqrt(\n        (sum([i ** 2 for i in errors]) / len(errors))\n        - mean_error ** 2)\n    return (mean_error, min_error, max_error, stdev_error)\n\n\ndef get_scheduled(date, sr_name=''):\n    campaign_ids = PromotionWeights.get_campaign_ids(date, sr_names=[sr_name])\n    campaigns = PromoCampaign._byID(campaign_ids, return_dict=False, data=True)\n    links = Link._by_fullname({camp.link_id for camp in campaigns},\n                              return_dict=False, data=True)\n    links = {l._id: l for l in links}\n    kept = []\n    for camp in campaigns:\n        if camp.trans_id == 0:\n            continue\n\n        link = links[camp.link_id]\n        if link._spam or not promote.is_accepted(link):\n            continue\n\n        kept.append(camp._id)\n\n    return [(camp._fullname, camp.link_id, camp.total_budget_dollars)\n        for camp in kept]\n\n\ndef get_campaign_pageviews(date, sr_name=''):\n    # ads go live at hour=5\n    start = datetime.datetime(date.year, date.month, date.day, 5, 0)\n    hours = [start + datetime.timedelta(hours=i) for i in xrange(24)]\n\n    traffic_cls = TargetedImpressionsByCodename\n    codename_string = PC_PREFIX + '_%'\n    q = (Session.query(traffic_cls.codename,\n                       sa_sum(traffic_cls.pageview_count).label('daily'))\n            .filter(traffic_cls.subreddit == sr_name)\n            .filter(traffic_cls.codename.like(codename_string))\n            .filter(traffic_cls.interval == 'hour')\n            .filter(traffic_cls.date.in_(hours))\n            .group_by(traffic_cls.codename))\n\n    pageviews = dict(q)\n    return pageviews\n\n\ndef filter_campaigns(date, fullnames):\n    campaigns = PromoCampaign._by_fullname(fullnames, data=True,\n                                           return_dict=False)\n\n    # filter out campaigns that shouldn't be live\n    pc_date = datetime.datetime(date.year, date.month, date.day, 0, 0,\n                                tzinfo=g.tz)\n\n    campaigns = [camp for camp in campaigns\n                 if camp.start_date <= pc_date <= camp.end_date]\n\n    # check for links with targeted campaigns - we can't handle them now\n    has_targeted = [camp.link_id for camp in campaigns if camp.sr_name != '']\n    return [camp for camp in campaigns if camp.link_id not in has_targeted]\n\n\ndef get_frontpage_pageviews(date):\n    sr_name = DefaultSR.name\n    traffic_cls = PageviewsBySubredditAndPath\n    q = (Session.query(traffic_cls.srpath, traffic_cls.pageview_count)\n           .filter(traffic_cls.interval == 'day')\n           .filter(traffic_cls.date == date)\n           .filter(traffic_cls.srpath == '%s-GET_listing' % sr_name))\n    r = list(q)\n    return r[0][1]\n\n\ndef compare_pageviews(daysago=0, verbose=False):\n    \"\"\"Evaluate past delivery for promoted links.\n\n    Check frontpage promoted links for their actual delivery compared to what\n    would be expected based on their bids.\n\n    \"\"\"\n\n    date = (datetime.datetime.now(g.tz) -\n            datetime.timedelta(days=daysago)).date()\n\n    scheduled = get_scheduled(date)\n    pageviews_by_camp = get_campaign_pageviews(date)\n    campaigns = filter_campaigns(date, pageviews_by_camp.keys())\n    actual = []\n    for camp in campaigns:\n        link_fullname = '%s_%s' % (LINK_PREFIX, to36(camp.link_id))\n        i = (camp._fullname, link_fullname, pageviews_by_camp[camp._fullname])\n        actual.append(i)\n\n    scheduled_links = {link for camp, link, pageviews in scheduled}\n    actual_links = {link for camp, link, pageviews in actual}\n\n    bid_by_link = defaultdict(int)\n    total_bid = 0\n\n    pageviews_by_link = defaultdict(int)\n    total_pageviews = 0\n\n    for camp, link, bid in scheduled:\n        if link not in actual_links:\n            if verbose:\n                print '%s not found in actual, skipping' % link\n            continue\n\n        bid_by_link[link] += bid\n        total_bid += bid\n\n    for camp, link, pageviews in actual:\n        # not ideal: links shouldn't be here\n        if link not in scheduled_links:\n            if verbose:\n                print '%s not found in schedule, skipping' % link\n            continue\n\n        pageviews_by_link[link] += pageviews\n        total_pageviews += pageviews\n\n    errors = []\n    for link, bid in sorted(bid_by_link.items(), key=lambda t: t[1]):\n        pageviews = pageviews_by_link.get(link, 0)\n        expected = bid / total_bid\n        realized = float(pageviews) / total_pageviews\n        difference = (realized - expected) / expected\n        errors.append(difference)\n        if verbose:\n            print '%s - %s - %s - %s' % (link, expected, realized, difference)\n\n    mean_error, min_error, max_error, stdev_error = error_statistics(errors)\n\n    print '%s' % date\n    print ('error %s max, %s min, %s +- %s' %\n           (max_error, min_error, mean_error, stdev_error))\n    print 'total bid %s' % total_bid\n    print ('pageviews for promoted links targeted only to frontpage %s' %\n           total_pageviews)\n    print ('frontpage pageviews for all promoted links %s' %\n           sum(pageviews_by_camp.values()))\n    print 'promoted eligible pageviews %s' % get_frontpage_pageviews(date)\n\n\nPROMOS = [('promo_%s' % i, i + 1) for i in xrange(100)]\n\n\ndef select_subset(n, weighted=False):\n    promos = copy(PROMOS)\n    selected = []\n\n    if weighted:\n        d = {(name, weight): weight for name, weight in promos}\n        while len(selected) < n and d:\n            i = weighted_lottery(d)\n            del d[i]\n            selected.append(i)\n    else:\n        # Sample without replacement\n        if n > len(promos):\n            return promos\n        else:\n            return random.sample(promos, n)\n    return selected\n\n\ndef pick(subset, weighted=False):\n    if weighted:\n        d = {(name, weight): weight for name, weight in subset}\n        picked = weighted_lottery(d)\n    else:\n        picked = random.choice(subset)\n    return picked\n\n\ndef benchmark(subsets=1440, picks=6945, weighted_subset=False,\n              weighted_pick=True, subset_size=10, verbose=False):\n    \"\"\"Test 2 stage randomization.\n\n    First stage picks a subset of promoted links, second stage picks a single\n    promoted link. This is to simulate the server side subset plus client side\n    randomization of promoted link display.\n\n    \"\"\"\n\n    counts = {(name, weight): 0 for name, weight in PROMOS}\n\n    for i in xrange(subsets):\n        subset = select_subset(subset_size, weighted=weighted_subset)\n\n        for j in xrange(picks):\n            name, weight = pick(subset, weighted=weighted_pick)\n            counts[(name, weight)] += 1\n\n    total_weight = sum(counts.values())\n    errors = []\n    for name, weight in sorted(counts.keys(), key=lambda t: t[1]):\n        count = counts[(name, weight)]\n        actual = float(count) / (subsets * picks)\n        expected = float(weight) / total_weight\n        error = (actual - expected) / expected\n        errors.append(error)\n        if verbose:\n            print ('%s - expected: %s - actual: %s - error %s' %\n                   (name, expected, actual, error))\n\n    mean_error, min_error, max_error, stdev_error = error_statistics(errors)\n\n    if verbose:\n        print ('Error %s max, %s min, %s +- %s' %\n               (max_error, min_error, mean_error, stdev_error))\n\n    return (max_error, min_error, mean_error, stdev_error)\n"
  },
  {
    "path": "scripts/read_secrets",
    "content": "#!/usr/bin/env python\n# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport ConfigParser\nimport base64\nimport cStringIO\nimport os\nimport sys\n\nfrom r2.lib.utils import parse_ini_file\nfrom r2.lib.zookeeper import connect_to_zookeeper\nfrom r2.lib.app_globals import fetch_secrets\n\n\ndef read_secrets_from_zookeeper(config):\n    zk_hostlist = config.get(\"DEFAULT\", \"zookeeper_connection_string\")\n    username = config.get(\"DEFAULT\", \"zookeeper_username\")\n    password = config.get(\"DEFAULT\", \"zookeeper_password\")\n\n    client = connect_to_zookeeper(zk_hostlist, (username, password))\n    secrets = fetch_secrets(client)\n\n    ini = ConfigParser.RawConfigParser()\n    ini.optionxform = str\n    ini.add_section(\"secrets\")\n    for name, secret in secrets.iteritems():\n        ini.set(\"secrets\", name, base64.b64encode(secret))\n\n    output = cStringIO.StringIO()\n    ini.write(output)\n    return output.getvalue()\n\n\ndef main():\n    progname = os.path.basename(sys.argv[0])\n\n    try:\n        ini_file_name = sys.argv[1]\n    except IndexError:\n        print >> sys.stderr, \"USAGE: %s INI\" % progname\n        return 1\n\n    try:\n        with open(ini_file_name) as ini_file:\n            config = parse_ini_file(ini_file)\n    except (IOError, ConfigParser.Error), e:\n        print >> sys.stderr, \"%s: %s: %s\" % (progname, ini_file_name, e)\n        return 1\n\n    print read_secrets_from_zookeeper(config)\n\n\nif __name__ == \"__main__\":\n    sys.exit(main())\n"
  },
  {
    "path": "scripts/saferun.sh",
    "content": "#!/bin/bash\n# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nif [ $# -lt 2 ]; then\n    echo \"usage: $0 [pidfile] [command]\" 1>&2\n    exit 1\nfi\n\nPIDFILE=$1\nshift 1\nCOMMAND=$1\nshift 1\n\n#check pid file for process\nif [ -a $PIDFILE ]; then\n    c=$(ps -p $(cat $PIDFILE) | wc -l)\n    if [ $c -eq 2 ]; then\n        echo 'already running' 1>&2\n        ls -l $PIDFILE 1>&2\n        exit 1\n    fi\nfi\n\n#dump pid\necho \"$$\" > $PIDFILE\n\n#run command\n$COMMAND \"$@\"\n\n#remove pid file\nrm $PIDFILE\n"
  },
  {
    "path": "scripts/stylecheck_git_diff.sh",
    "content": "#!/usr/bin/env bash\n###############################################################################\n# git diff style checker\n# ----------------------\n# This script runs a style check within our Drone setup, or within the\n# `drone exec` runner.\n#\n# Since the codebase has a substantial body of non-conformant code, style\n# checks are only ran on the diffs (compared to master). As a consequence of\n# this, style checks also only run on non-master branches.\n###############################################################################\n\nif [[ ${CI_BRANCH} = \"master\" ]]; then\n    echo \"Skipping style checks on commit(s) to the master branch.\"\n    exit 0\nfi\n\nif [[ ${CI_REPO:=} = \"\" ]]; then\n    # This assumed to be `drone exec`.\n    echo \"Running style checks on staged local changes...\"\n    git diff --cached | pep8 --diff\nelse\n    echo \"Running style checks within Drone...\"\n    git fetch --no-tags --depth=10 origin master\n    git diff origin/${CI_BRANCH} origin/master | pep8 --diff\nfi\nerror_encountered=$?\n\nif [[ ${error_encountered} = 1 ]]; then\n    echo \"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\"\n    echo \"pep8 issues found. reddit follows pep8: https://github.com/reddit/styleguide\"\n    echo \"              Please commit a fix or ignore inline with: noqa\"\n    echo \"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\"\n    exit 1\nfi\n\necho \"Style checks passed. Good jerb!\"\n"
  },
  {
    "path": "scripts/tracker.py",
    "content": "#!/usr/bin/python\n# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\"\"\"\nThis is a tiny Flask app used for a couple of self-serve ad tracking\nmechanisms. The URLs it provides are:\n\n/click\n\n    Promoted links have their URL replaced with a /click URL by the JS\n    (after a call to /fetch-trackers). Redirect to the actual URL after logging\n    the click. This must be run in a place whose logs are stored for traffic\n    analysis.\n\nFor convenience, the script can compile itself into a Zip archive suitable for\nuse on Amazon Elastic Beanstalk (and possibly other systems).\n\n\"\"\"\n\n\nimport cStringIO\nimport os\nimport hashlib\nimport hmac\nimport time\nimport urllib\nfrom urlparse import parse_qsl, urlparse, urlunparse\n\nfrom ConfigParser import RawConfigParser\nfrom wsgiref.handlers import format_date_time\n\nfrom flask import Flask, request, json, make_response, abort, redirect\n\n\napplication = Flask(__name__)\nREQUIRED_PACKAGES = [\n    \"flask\",\n]\n\n\nclass ApplicationConfig(object):\n    \"\"\"A thin wrapper around ConfigParser that remembers what we read.\n\n    The remembered settings can then be written out to a minimal config file\n    when building the Elastic Beanstalk zipfile.\n\n    \"\"\"\n    def __init__(self):\n        self.input = RawConfigParser()\n        config_filename = os.environ.get(\"CONFIG\", \"production.ini\")\n        with open(config_filename) as f:\n            self.input.readfp(f)\n        self.output = RawConfigParser()\n\n    def get(self, section, key):\n        value = self.input.get(section, key)\n\n        # remember that we needed this configuration value\n        if (section.upper() != \"DEFAULT\" and\n            not self.output.has_section(section)):\n            self.output.add_section(section)\n        self.output.set(section, key, value)\n\n        return value\n\n    def to_config(self):\n        io = cStringIO.StringIO()\n        self.output.write(io)\n        return io.getvalue()\n\n\nconfig = ApplicationConfig()\ntracking_secret = config.get('DEFAULT', 'tracking_secret')\nreddit_domain = config.get('DEFAULT', 'domain')\nreddit_domain_prefix = config.get('DEFAULT', 'domain_prefix')\n\n\n@application.route(\"/\")\ndef healthcheck():\n    return \"I am healthy.\"\n\n\n@application.route('/click')\ndef click_redirect():\n    destination = request.args['url'].encode('utf-8')\n    fullname = request.args['id'].encode('utf-8')\n    observed_mac = request.args['hash']\n\n    expected_hashable = ''.join((destination, fullname))\n    expected_mac = hmac.new(\n            tracking_secret, expected_hashable, hashlib.sha1).hexdigest()\n\n    if not constant_time_compare(expected_mac, observed_mac):\n        abort(403)\n\n    # fix encoding in the query string of the destination\n    u = urlparse(destination)\n    if u.query:\n        u = _fix_query_encoding(u)\n        destination = u.geturl()\n\n    return _redirect_nocache(destination)\n\n\n@application.route('/event_redirect')\ndef event_redirect():\n    destination = request.args['url'].encode('utf-8')\n\n    # Parse and avoid open redirects\n    netloc = \"%s.%s\" % (reddit_domain_prefix, reddit_domain)\n    u = urlparse(destination)._replace(netloc=netloc, scheme=\"https\")\n\n    if u.query:\n        u = _fix_query_encoding(u)\n        destination = u.geturl()\n\n    return _redirect_nocache(destination)\n\n\n@application.route('/event_click')\ndef event_click():\n    \"\"\"Take in an evented request, append session data to payload, and redirect.\n\n    This is only useful for situations in which we're navigating from a request\n    that does not have session information - i.e. served from redditmedia.com.\n    If we want to track a click and the user that did so from these pages,\n    we need to identify the user before sending the payload.\n\n    Note: If we add hmac validation, this will need verify and resign before\n    redirecting. We can also probably drop a redirect here once we're not\n    relying on log files for event tracking and have a proper events endpoint.\n    \"\"\"\n    try:\n        session_str = urllib.unquote(request.cookies.get('reddit_session', ''))\n        user_id = int(session_str.split(',')[0])\n    except ValueError:\n        user_id = None\n\n    args = request.args.to_dict()\n    if user_id:\n        payload = args.get('data').encode('utf-8')\n        try:\n            payload_json = json.loads(payload)\n        except ValueError:\n            # if we fail to load the JSON, continue on to the redirect to not\n            # block the user - ETL can deal with/report the malformed data.\n            pass\n        else:\n            payload_json['user_id'] = user_id\n            args['data'] = json.dumps(payload_json)\n\n    return _redirect_nocache('/event_redirect?%s' % urllib.urlencode(args))\n\n\ndef _fix_query_encoding(parse_result):\n    \"Fix encoding in the query string.\"\n    query_params = parse_qsl(parse_result.query, keep_blank_values=True)\n\n    # this effectively calls urllib.quote_plus on every query value\n    return parse_result._replace(query=urllib.urlencode(query_params))\n\n\ndef _redirect_nocache(destination):\n    now = format_date_time(time.time())\n    response = redirect(destination)\n    response.headers['Cache-control'] = 'no-cache'\n    response.headers['Pragma'] = 'no-cache'\n    response.headers['Date'] = now\n    response.headers['Expires'] = now\n    return response\n\n\n# copied from r2.lib.utils\ndef constant_time_compare(actual, expected):\n    \"\"\"\n    Returns True if the two strings are equal, False otherwise\n\n    The time taken is dependent on the number of characters provided\n    instead of the number of characters that match.\n    \"\"\"\n    actual_len   = len(actual)\n    expected_len = len(expected)\n    result = actual_len ^ expected_len\n    if expected_len > 0:\n        for i in xrange(actual_len):\n            result |= ord(actual[i]) ^ ord(expected[i % expected_len])\n    return result == 0\n\n\nif __name__ == \"__main__\":\n    # package up for elastic beanstalk\n    import zipfile\n\n    with zipfile.ZipFile(\"/tmp/tracker.zip\", \"w\", zipfile.ZIP_DEFLATED) as zip:\n        zip.write(__file__, \"application.py\")\n        zip.writestr(\"production.ini\", config.to_config())\n        zip.writestr(\"requirements.txt\", \"\\n\".join(REQUIRED_PACKAGES) + \"\\n\")\n"
  },
  {
    "path": "scripts/traffic/Makefile",
    "content": "CCOPTS=-Wall -O2 -std=c99\n\nall: parse_hour decrypt_userinfo verify\n\ndecrypt_userinfo: decrypt_userinfo.o utils.o\n\tgcc ${CCOPTS} -o $@ $< utils.o -lcrypto\n\nverify: verify.o utils.o\n\tgcc ${CCOPTS} -o $@ $< utils.o -lcrypto\n\nparse_hour: parse.o\n\tgcc ${CCOPTS} -o $@ $< -lpcre -lz\n\n%.o: %.c\n\tgcc ${CCOPTS} -c -o $@ $<\n\n.PHONY: clean realclean\n\nclean:\n\t-rm parse.o utils.o verify.o decrypt_userinfo.o \n\t\nrealclean: clean\n\t-rm parse_hour decrypt_userinfo verify\n"
  },
  {
    "path": "scripts/traffic/decrypt_userinfo.c",
    "content": "#include <stdio.h>\n#include <assert.h>\n#include <stdbool.h>\n#include <string.h>\n#include <ctype.h>\n\n#include <openssl/bio.h>\n#include <openssl/evp.h>\n\n#include \"utils.h\"\n\n#define MAX_LINE 2048\n#define KEY_SIZE 16\n\nenum InputField {\n    FIELD_USER=0,\n    FIELD_SRPATH,\n    FIELD_LANG,\n    FIELD_CNAME,\n\n    FIELD_COUNT\n};\n\nint main(int argc, char** argv)\n{\n    const char* secret;\n    secret = getenv(\"TRACKING_SECRET\");\n    if (!secret) {\n        fprintf(stderr, \"TRACKING_SECRET not set\\n\");\n        return 1;\n    }\n    \n    char input_line[MAX_LINE];\n    char plaintext[MAX_LINE];\n    const EVP_CIPHER* cipher = EVP_aes_128_cbc();\n\n    while (fgets(input_line, MAX_LINE, stdin) != NULL) {\n        /* split the line into unique_id and query */\n        char *unique_id, *query;\n        split_fields(input_line, &unique_id, &query, NO_MORE_FIELDS);\n\n        /* parse the query string to get the value we need */\n        char *blob = NULL;\n\n        char *key, *value;\n        while (parse_query_param(&query, &key, &value) >= 0) {\n            if (strcmp(key, \"v\") == 0) {\n                blob = value;\n                break;\n            }\n        }\n\n        if (blob == NULL)\n            continue;\n\n        /* undo url encoding on the query string */\n        int b64_size = url_decode(blob);\n        if (b64_size < 0) \n            continue;\n\n        /* split off the initialization vector from the actual ciphertext */\n        char *initialization_vector, *ciphertext;\n\n        initialization_vector = blob;\n        initialization_vector[KEY_SIZE] = '\\0';\n        ciphertext = blob + 32;\n        b64_size -= 32;\n\n        /* base 64 decode and decrypt the ciphertext */\n        BIO* bio = BIO_new_mem_buf(ciphertext, b64_size);\n        if (bio == NULL) {\n            fprintf(stderr, \"Failed to allocate buffer for b64 ciphertext\\n\");\n            return 1;\n        }\n\n        BIO* b64 = BIO_new(BIO_f_base64());\n        if (b64 == NULL) {\n            fprintf(stderr, \"Failed to allocate base64 filter\\n\");\n            return 1;\n        }\n        BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL);\n        bio = BIO_push(b64, bio);\n\n        BIO* aes = BIO_new(BIO_f_cipher());\n        if (aes == NULL) {\n            fprintf(stderr, \"Failed to allocate AES cipher\\n\");\n            return 1;\n        }\n        BIO_set_cipher(\n            aes,\n            cipher,\n            (unsigned char*)secret,\n            (unsigned char*)initialization_vector,\n            0 /* decryption */\n        );\n        bio = BIO_push(aes, bio);\n\n        /* stream the output through the filters */\n        int plaintext_length = BIO_read(bio, plaintext, b64_size);\n        plaintext[plaintext_length] = '\\0';\n\n        if (!BIO_get_cipher_status(bio)) {\n            BIO_free_all(bio);\n            continue;\n        }\n\n        /* clean up */\n        BIO_free_all(bio);\n\n        /* check that the plaintext isn't garbage; if there are\n         * non-ascii characters in it, it's likely bad */\n        bool non_ascii_junk = false;\n        for (unsigned char* c = (unsigned char*)plaintext; *c != '\\0'; c++) {\n            if (*c > 0x7F) {\n                non_ascii_junk = true;\n                break;\n            }\n        }\n\n        if (non_ascii_junk) {\n            continue;\n        }\n\n        /* write out the unique id since we don't need it for ourselves */\n        fputs(unique_id, stdout);\n\n        /* split the fields out of the plaintext */\n        char* current_string = plaintext;\n        int field_index = 0;\n\n        for (int i = 0; i < plaintext_length; i++) {\n            char *c = plaintext + i;\n            if (*c != '|')\n                continue;\n            \n            *c = '\\0';\n\n            switch (field_index) {\n            case FIELD_USER:\n                /* we don't use the user id; skip it */\n                break;\n            case FIELD_SRPATH:\n                fputc('\\t', stdout);\n                fputs(current_string, stdout);\n\n                fputc('\\t', stdout);\n                for (char* c2=current_string; *c2 != '\\0'; c2++) {\n                    if (*c2 == '-') {\n                        *c2 = '\\0';\n                        break;\n                    }\n                }\n                fputs(current_string, stdout);\n                break;\n            case FIELD_LANG:\n                fputc('\\t', stdout);\n                for (char* c2=current_string; *c2 != '\\0'; c2++) {\n                    *c2 = tolower(*c2);\n                }\n                fputs(current_string, stdout);\n                break;\n            case FIELD_CNAME:\n                assert(0!=1);\n            }\n\n            current_string = c + 1;\n            field_index += 1;\n        }\n\n        if (field_index < FIELD_COUNT) {\n            fputc('\\t', stdout);\n            fputs(current_string, stdout);\n            field_index += 1;\n        }\n\n        for (; field_index < FIELD_COUNT; field_index++) \n            fputc('\\t', stdout);\n\n        /* all done! */\n        fputc('\\n', stdout);\n    }\n\n    return 0;\n}\n\n"
  },
  {
    "path": "scripts/traffic/mr_aggregate.pig",
    "content": "/*  Aggregate output from processed logs:\n *\n *  Go from entry per unique_id (including # of impressions)\n *  to total # of uniques, total # of impressions\n *\n *  Needs to be passed: INPUT, OUTPUT\n */\n\n/****************************************************\n * DEFINITIONS\n ****************************************************/\n\n-- Cleanup\nrmf $OUTPUT\n\n/****************************************************\n * AGGREGATE\n ****************************************************/\n\n-- sitewide --\nsitewide = LOAD '$INPUT/sitewide' AS (unique_id, count:long);\n\nsitewide_grouped = GROUP sitewide BY unique_id;\nsitewide_combined = FOREACH sitewide_grouped\n                      GENERATE group AS unique_id,\n                               SUM(sitewide.count) as count;\n\nsitewide_grouped2 = GROUP sitewide_combined ALL;\nsitewide_totals = FOREACH sitewide_grouped2\n                    GENERATE group,\n                             COUNT(sitewide_combined),\n                             SUM(sitewide_combined.count);\n\nSTORE sitewide_totals INTO '$OUTPUT/sitewide';\n\n-- subreddit --\nsubreddit = LOAD '$INPUT/subreddit' AS (subreddit, unique_id, count:long);\n\nsubreddit_grouped = GROUP subreddit BY (subreddit, unique_id);\nsubreddit_combined = FOREACH subreddit_grouped\n                        GENERATE group.subreddit AS subreddit,\n                                 group.unique_id AS unique_id,\n                                 SUM(subreddit.count) AS count;\n\nsubreddit_grouped2 = GROUP subreddit_combined BY subreddit;\nsubreddit_totals = FOREACH subreddit_grouped2\n                      GENERATE group,\n                      COUNT(subreddit_combined),\n                      SUM(subreddit_combined.count);\n\nSTORE subreddit_totals INTO '$OUTPUT/subreddit';\n\n-- subreddit path\nsrpath = LOAD '$INPUT/srpath' AS (srpath, unique_id, count:long);\n\nsrpath_grouped = GROUP srpath BY (srpath, unique_id);\nsrpath_combined = FOREACH srpath_grouped\n                    GENERATE group.srpath AS srpath,\n                             group.unique_id AS unique_id,\n                             SUM(srpath.count) AS count;\n\nsrpath_grouped2 = GROUP srpath_combined BY srpath;\nsrpath_totals = FOREACH srpath_grouped2\n                  GENERATE group,\n                           COUNT(srpath_combined),\n                           SUM(srpath_combined.count);\n\nSTORE srpath_totals INTO '$OUTPUT/srpath';\n\n-- language\nlang = LOAD '$INPUT/lang' AS (lang, unique_id, count:long);\n\nlang_grouped = GROUP lang BY (lang, unique_id);\nlang_combined = FOREACH lang_grouped\n                  GENERATE group.lang AS lang,\n                           group.unique_id AS unique_id,\n                           SUM(lang.count) AS count;\n\nlang_grouped2 = GROUP lang_combined BY lang;\nlang_totals = FOREACH lang_grouped2\n                GENERATE group,\n                         COUNT(lang_combined),\n                         SUM(lang_combined.count);\n\nSTORE lang_totals INTO '$OUTPUT/lang';\n\n-- clicks\nclick = LOAD '$INPUT/clicks' AS (fullname, unique_id, count:long);\n\nclick_grouped = GROUP click BY (fullname, unique_id);\nclick_combined = FOREACH click_grouped\n                    GENERATE group.fullname AS fullname,\n                             group.unique_id AS unique_id,\n                             SUM(click.count) AS count;\n\nclick_grouped2 = GROUP click_combined BY fullname;\nclick_totals = FOREACH click_grouped2\n                  GENERATE group,\n                           COUNT(click_combined),\n                           SUM(click_combined.count);\n\nSTORE click_totals INTO '$OUTPUT/clicks';\n\n-- targeted clicks\nt_click = LOAD '$INPUT/clicks_targeted' AS (fullname, sr, unique_id, count:long);\n\nt_click_grouped = GROUP t_click BY (fullname, sr, unique_id);\nt_click_combined = FOREACH t_click_grouped\n                      GENERATE group.fullname AS fullname,\n                               group.sr AS sr,\n                               group.unique_id AS unique_id,\n                               SUM(t_click.count) AS count;\n\nt_click_grouped2 = GROUP t_click_combined BY (fullname, sr);\nt_click_totals = FOREACH t_click_grouped2\n                    GENERATE group.fullname,\n                             group.sr,\n                             COUNT(t_click_combined),\n                             SUM(t_click_combined.count);\n\nSTORE t_click_totals INTO '$OUTPUT/clicks_targeted';\n\n-- things\nthing = LOAD '$INPUT/thing'AS (fullname, unique_id, count:long);\n\nthing_grouped = GROUP thing BY (fullname, unique_id);\nthing_combined = FOREACH thing_grouped\n                    GENERATE group.fullname AS fullname,\n                             group.unique_id AS unique_id,\n                             SUM(thing.count) AS count;\n\nthing_grouped2 = GROUP thing_combined BY fullname;\nthing_totals = FOREACH thing_grouped2\n                  GENERATE group,\n                           COUNT(thing_combined),\n                           SUM(thing_combined.count);\n\nSTORE thing_totals INTO '$OUTPUT/thing';\n\n-- targeted things\nt_thing = LOAD '$INPUT/thingtarget' AS (fullname, sr, unique_id, count:long);\n\nt_thing_grouped = GROUP t_thing BY (fullname, sr, unique_id);\nt_thing_combined = FOREACH t_thing_grouped\n                      GENERATE group.fullname AS fullname,\n                               group.sr AS sr,\n                               group.unique_id AS unique_id,\n                               SUM(t_thing.count) AS count;\n\nt_thing_grouped2 = GROUP t_thing_combined BY (fullname, sr);\nt_thing_totals = FOREACH t_thing_grouped2\n                    GENERATE group.fullname,\n                             group.sr,\n                             COUNT(t_thing_combined),\n                             SUM(t_thing_combined.count);\n\nSTORE t_thing_totals INTO '$OUTPUT/thingtarget';\n"
  },
  {
    "path": "scripts/traffic/mr_coalesce.pig",
    "content": "/*  EMR Version\n *\n *  Coalesce output from multiple processed logs within interval\n *  hours --> day\n *  days --> month\n *\n *  Needs to be passed: INPUT, OUTPUT\n */\n\n/****************************************************\n * DEFINITIONS\n ****************************************************/\n\n-- Cleanup\nrmf $OUTPUT\n\n/****************************************************\n * COALESCE\n ****************************************************/\n\n-- sitewide\nsitewide = LOAD '$INPUT/sitewide' AS (unique_id, count:long);  -- load all input files (multiple hours)\n\nsitewide_grouped = GROUP sitewide BY unique_id; -- (unique_id, {(unique_id, count), ...}, ...)\n\nsitewide_coalesced = FOREACH sitewide_grouped \n                     GENERATE group, SUM(sitewide.count); -- ((unique_id, SUM(sitewide.count), ...)\n\nSTORE sitewide_coalesced INTO '$OUTPUT/sitewide';\n\n-- subreddit\nsubreddit_counters = LOAD '$INPUT/subreddit' AS (subreddit, unique_id, count:long);\n\nsubreddits_grouped = GROUP subreddit_counters BY (subreddit, unique_id);\n\nsubreddits_coalesced = FOREACH subreddits_grouped\n                       GENERATE group.subreddit, group.unique_id,\n                                SUM(subreddit_counters.count) AS count;\n\nSTORE subreddits_coalesced INTO '$OUTPUT/subreddit';\n\n-- subreddit path\nsrpath = LOAD '$INPUT/srpath' AS (srpath, unique_id, count:long);\n\nsrpath_grouped = GROUP srpath BY (srpath, unique_id);\n\nsrpath_coalesced = FOREACH srpath_grouped\n                   GENERATE group.srpath, group.unique_id,\n                            SUM(srpath.count) AS count;\n\nSTORE srpath_coalesced INTO '$OUTPUT/srpath';\n\n-- language \nlang = LOAD '$INPUT/lang' AS (lang, unique_id, count:long);\n\nlang_grouped = GROUP lang BY (lang, unique_id);\n\nlang_coalesced = FOREACH lang_grouped\n                 GENERATE group.lang, group.unique_id,\n                          SUM(lang.count) AS count;\n\nSTORE lang_coalesced INTO '$OUTPUT/lang';\n\n-- click\nclick = LOAD '$INPUT/clicks' AS (fullname, unique_id, count:long);\n\nclick_grouped = GROUP click BY (fullname, unique_id);\n\nclick_coalesced = FOREACH click_grouped\n                  GENERATE group.fullname, group.unique_id,\n                           SUM(click.count) AS count;\n\nSTORE click_coalesced INTO '$OUTPUT/clicks';\n\n-- clicktarget\nclicktarget = LOAD '$INPUT/clicks_targeted' AS (fullname, sr, unique_id, count:long);\n\nclicktarget_grouped = GROUP clicktarget BY (fullname, sr, unique_id);\n\nclicktarget_coalesced = FOREACH clicktarget_grouped\n                        GENERATE group.fullname, group.sr, group.unique_id,\n                                 SUM(clicktarget.count) AS count;\n\nSTORE clicktarget_coalesced INTO '$OUTPUT/clicks_targeted';\n\n-- thing\nthing = LOAD '$INPUT/thing' AS (fullname, unique_id, count:long);\n\nthing_grouped = GROUP thing BY (fullname, unique_id);\n\nthing_coalesced = FOREACH thing_grouped\n                  GENERATE group.fullname, group.unique_id,\n                           SUM(thing.count) AS count;\n\nSTORE thing_coalesced INTO '$OUTPUT/thing';\n\n-- thingtarget\nthingtarget = LOAD '$INPUT/thingtarget' AS (fullname, sr, unique_id, count:long);\n\nthingtarget_grouped = GROUP thingtarget BY (fullname, sr, unique_id);\n\nthingtarget_coalesced = FOREACH thingtarget_grouped\n                        GENERATE group.fullname, group.sr, group.unique_id,\n                                 SUM(thingtarget.count) AS count;\n\nSTORE thingtarget_coalesced INTO '$OUTPUT/thingtarget';\n"
  },
  {
    "path": "scripts/traffic/mr_process_hour.pig",
    "content": "/*  EMR Version\n *\n *  Process hourly logfile\n *  for each category sitewide/subreddit/srpath/lang/clicks/clicks_targeted/thing/thingtarget\n *  generate output with entries like:\n *  category, unique_id, count (e.g. subreddit: pics, 123456, 9)\n *\n *  Needs to be passed: LOGFILE, OUTPUT\n */\n\n/****************************************************\n * DEFINITIONS\n ****************************************************/\n\n-- Binaries - location is specified in traffic_bootstrap.sh\nDEFINE PARSE_HOUR `/home/hadoop/traffic/parse_hour`;\nDEFINE DECRYPT_USERINFO `/home/hadoop/traffic/decrypt_userinfo`;\nDEFINE VERIFY `/home/hadoop/traffic/verify`;\n\n-- Pixel definitions\n%default URL_USERINFO '/pixel/of_destiny.png';\n%default URL_ADFRAME '/pixel/of_defenestration.png';\n%default URL_PROMOTEDLINK '/pixel/of_doom.png';\n%default URL_CLICK '/click';\n\n-- Cleanup\nrmf $OUTPUT\n\n/****************************************************\n * LOAD LOGFILE \n ****************************************************/\n\nlog_raw = LOAD '$LOGFILE' USING TextLoader() AS (line);\nlog_parsed = STREAM log_raw THROUGH PARSE_HOUR AS (ip, path:chararray, query, response_code, unique_id);\n\nSPLIT log_parsed INTO\n    pageviews_with_path IF path == '$URL_USERINFO',\n    unverified_hits IF (path == '$URL_ADFRAME' OR\n                        path == '$URL_PROMOTEDLINK' OR\n                        path == '$URL_CLICK');\n\npageviews_encrypted = FOREACH pageviews_with_path GENERATE unique_id, query;\n\n /****************************************************\n * PAGEVIEWS\n ****************************************************/\n\npageviews = STREAM pageviews_encrypted THROUGH DECRYPT_USERINFO AS (unique_id, srpath, subreddit, lang, cname);\n\n-- sitewide\nsitewide_pageviews = FOREACH pageviews GENERATE unique_id;  -- (unique_id)\nsitewide_hourly_uniques_grouped = GROUP sitewide_pageviews BY unique_id; -- (unique_id, {(unique_id), ...}\nsitewide_hourly_uniques = FOREACH sitewide_hourly_uniques_grouped\n                          GENERATE group AS unique_id, COUNT(sitewide_pageviews) AS count;  -- (unique_id, count)\n\nSTORE sitewide_hourly_uniques INTO '$OUTPUT/sitewide';\n\n-- subreddit\nsubreddit_pageviews_filtered = FILTER pageviews \n                               BY subreddit IS NOT NULL;  -- exclude entries without subreddit\nsubreddit_pageviews_raw = FOREACH subreddit_pageviews_filtered \n                          GENERATE subreddit, unique_id;  -- limit to (subreddit, unique_id)\nsubreddit_hourly_uniques_grouped = GROUP subreddit_pageviews_raw\n                                   BY (subreddit, unique_id); -- (subreddit, unique_id, {(subreddit, unique_id), ...})\nsubreddit_hourly_uniques = FOREACH subreddit_hourly_uniques_grouped\n                           GENERATE group.subreddit, group.unique_id,\n                                    COUNT(subreddit_pageviews_raw) AS count; -- (subreddit, unique_id, count)\n\nSTORE subreddit_hourly_uniques INTO '$OUTPUT/subreddit';\n\n-- subreddit path\nsrpath_filtered = FILTER pageviews BY srpath IS NOT NULL;\nsrpath_pageviews = FOREACH srpath_filtered\n                   GENERATE srpath, unique_id;\nsrpath_hourly_uniques_grouped = GROUP srpath_pageviews\n                                BY (srpath, unique_id);\nsrpath_hourly_uniques = FOREACH srpath_hourly_uniques_grouped\n                        GENERATE group.srpath, group.unique_id,\n                                 COUNT(srpath_pageviews) AS count;\n\nSTORE srpath_hourly_uniques INTO '$OUTPUT/srpath';\n\n-- language\nlang_filtered = FILTER pageviews BY lang IS NOT NULL;\nlang_pageviews = FOREACH lang_filtered GENERATE lang, unique_id;\nlang_hourly_uniques_grouped = GROUP lang_pageviews BY (lang, unique_id);\nlang_hourly_uniques = FOREACH lang_hourly_uniques_grouped\n                      GENERATE group.lang, group.unique_id,\n                               COUNT(lang_pageviews) AS count;\n\nSTORE lang_hourly_uniques INTO '$OUTPUT/lang';\n \n /****************************************************\n * HITS \n ****************************************************/\n\n-- process unverified hits\nverified = STREAM unverified_hits THROUGH VERIFY AS (unique_id, path:chararray, fullname, sr);\n\n-- ads and promoted links\n\nSPLIT verified INTO\n    clicks_raw IF path == '$URL_CLICK',\n    ad_impressions IF (path == '$URL_ADFRAME' OR\n                       path == '$URL_PROMOTEDLINK');\n\n-- clicks\nclicks = FOREACH clicks_raw GENERATE fullname, unique_id;\nclicks_grouped = GROUP clicks BY (fullname, unique_id);\nclicks_by_hour = FOREACH clicks_grouped\n                 GENERATE group.fullname, group.unique_id,\n                          COUNT(clicks) AS count;\n\nSTORE clicks_by_hour INTO '$OUTPUT/clicks';\n\n-- targeted clicks\ntargeted_clicks = FOREACH clicks_raw GENERATE fullname, sr, unique_id;\ntargeted_clicks_grouped = GROUP targeted_clicks BY (fullname, sr, unique_id);\ntargeted_clicks_by_hour = FOREACH targeted_clicks_grouped\n                          GENERATE group.fullname, group.sr, group.unique_id,\n                                   COUNT(targeted_clicks) AS count;\n\nSTORE targeted_clicks_by_hour INTO '$OUTPUT/clicks_targeted';\n\n-- things\nthing_impressions = FOREACH ad_impressions GENERATE fullname, unique_id;\nthing_impressions_grouped = GROUP thing_impressions BY (fullname, unique_id);\nthing_impressions_hourly = FOREACH thing_impressions_grouped\n                           GENERATE group.fullname, group.unique_id,\n                                    COUNT(thing_impressions) AS count;\n\nSTORE thing_impressions_hourly INTO '$OUTPUT/thing';\n\n-- targeted things\ntargeted_thing_impressions = FOREACH ad_impressions \n                             GENERATE fullname, sr, unique_id;\ntargeted_thing_impressions_grouped = GROUP targeted_thing_impressions\n                                     BY (fullname, sr, unique_id);\ntargeted_thing_impressions_hourly = FOREACH targeted_thing_impressions_grouped\n                                    GENERATE group.fullname, group.sr,\n                                             group.unique_id,\n                                             COUNT(targeted_thing_impressions)\n                                             AS count;\n\nSTORE targeted_thing_impressions_hourly INTO '$OUTPUT/thingtarget';\n"
  },
  {
    "path": "scripts/traffic/parse.c",
    "content": "#include <stdio.h>\n#include <string.h>\n#include <assert.h>\n#include <stdint.h>\n#include <inttypes.h>\n\n#include <arpa/inet.h>\n\n#include <pcre.h>\n#include <zlib.h>\n\n#define MAX_LINE 2048 \n\n/******************************\n * this regular expression has the following capture groups in it (in order):\n *  - ip\n *  - path\n *  - query\n *  - user agent\n ******************************/\n#define RE  \"(?:[0-9.]+,\\\\ )*([0-9.]+)\"\\\n            \"[^\\\"]+\"\\\n            \"\\\"GET\\\\s([^\\\\s?]+)\\\\?([^\\\\s]+)\\\\s[^\\\"]+\\\"\"\\\n            \"\\\\s([^\\\\s]+)[^\\\"]+\"\\\n            \"\\\"[^\\\"]+\\\"\"\\\n            \"[^\\\"]+\"\\\n            \"\\\"([^\\\"]+)\\\"\"\n\n#define GROUP_IP    1\n#define GROUP_PATH  2\n#define GROUP_QUERY 3\n#define GROUP_CODE  4\n#define GROUP_UA    5\n\nint main(int argc, char** argv) \n{\n    /* compile the pattern */\n    const char* error;\n    int error_offset;\n    pcre *re = pcre_compile(RE, 0, &error, &error_offset, NULL);\n    if (re == NULL) {\n        fprintf(\n            stderr, \n            \"character %d: failed to compile regex: %s\\n\", \n            error_offset, \n            error\n        );\n\n        return 1;\n    }\n\n    /* study it to speed it up */\n    pcre_extra *extra = pcre_study(re, 0, &error);\n\n    /* allocate enough space for the capturing groups */\n    int group_count;\n    pcre_fullinfo(re, extra, PCRE_INFO_CAPTURECOUNT, &group_count);\n    int match_vector_size = (group_count + 1) * 3;\n    int *matches = malloc(sizeof(int) * match_vector_size);\n    if (matches == NULL) {\n        fprintf(stderr, \"Couldn't allocate memory for regex groups!\\n\");\n        return 1;\n    }\n\n    /* iterate through the input */\n    char input_line[MAX_LINE];\n    while (fgets(input_line, MAX_LINE, stdin)) {\n        int length = strlen(input_line);\n\n        /* run the regular expression against the line */\n        int match_result = pcre_exec(\n            re, \n            extra, \n            input_line, \n            length, \n            0, \n            0, \n            matches, \n            match_vector_size\n        );\n\n        /* bail out if the line didn't match */\n        if (match_result < 0) {\n            continue;\n        }\n\n        /* iterate through the groups */\n        /* NOTE: the crc function uses int32_t instead of uint32_t\n         * and has the funky (2^31 - crc) bit of math for backwards\n         * compatibility with the old python code. fix this when \n         * such compatibility is no longer necessary. */\n        uint32_t address = 0;\n        int32_t crc;\n        uint64_t unique_id;\n\n        for (int i = 1; i < match_result; i++) {\n            int start_position = matches[i * 2];\n            int end_position = matches[i * 2 + 1];\n            int substr_length = end_position - start_position;\n\n            switch (i) {\n            case GROUP_UA:\n                crc = crc32(0L, Z_NULL, 0);\n                crc = crc32(crc, (unsigned char*)input_line + start_position, \n                            substr_length);\n                unique_id = (((uint64_t)address << 32) & 0xffffffff00000000) | \n                            (2147483648 - crc);\n                fprintf(stdout, \"%\" PRIu64, unique_id);\n                break;\n            case GROUP_IP:\n                /* parse and store the ip so we can use it in GROUP_UA\n                 * to calculate the unique id */\n                input_line[end_position] = 0;\n                address = inet_addr(input_line + start_position);\n\n                /* fall through so it gets written out as well */\n            case GROUP_PATH:\n            case GROUP_CODE:\n            case GROUP_QUERY:\n                /* write them out verbatim */\n                (void)fwrite(\n                    input_line + start_position,\n                    sizeof(char),\n                    substr_length,\n                    stdout\n                );\n                break;\n            }\n\n            /* tab-delimit the data */\n            fputc('\\t', stdout);\n        }\n\n        fputc('\\n', stdout);\n    }\n\n    return 0;\n}\n\n"
  },
  {
    "path": "scripts/traffic/traffic_bootstrap.sh",
    "content": "#!/bin/bash\n# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nTRAFFIC_SRC_DIR=$1\nTRAFFIC_INSTALL_DIR=$HADOOP_HOME/traffic\n\n# Build traffic regexes\nmkdir $TRAFFIC_INSTALL_DIR\nhadoop dfs -copyToLocal $TRAFFIC_SRC_DIR/* $TRAFFIC_INSTALL_DIR\ncd $TRAFFIC_INSTALL_DIR\nmake\n\n# Export userinfo secret\nTARGET=$HADOOP_HOME/.pam_environment\nTRACKING_SECRET=$2\necho \"TRACKING_SECRET=$TRACKING_SECRET\" >> $TARGET\n"
  },
  {
    "path": "scripts/traffic/utils.c",
    "content": "#include \"utils.h\"\n\n#include <stdio.h>\n#include <string.h>\n#include <assert.h>\n\nvoid split_fields(char *line, ...)\n{\n\tva_list args;\n\n\tva_start(args, line);\n\tchar* c = line;\n\tfor (char** field = va_arg(args, char**); field != NULL; \n\t\t\t    field = va_arg(args, char**)) {\n\t\t*field = c;\n\t\t\n\t\tfor (; *c != '\\t' && *c != '\\n'; c++) {\n\t\t\tassert(*c != '\\0');\n\t\t}\n\n\t\t*c = '\\0';\n\t\tc += 1;\n\t}\n\tva_end(args);\n}\n\nint url_decode(char* buffer)\n{\n\tchar *c = buffer, *o = buffer;\n\tint size = 0;\n\n\tfor (;*c != '\\0';) {\n\t\tif (*c == '%') {\n\t\t\tint count = sscanf(c + 1, \"%2hhx\", o);\n\t\t\tif (count != 1) {\n\t\t\t\tsize = -1;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\t\n\t\t\tc += 3;\n\t\t} else {\n\t\t\t*o = *c;\n\t\t\tc += 1;\n\t\t}\n\n\t\to += 1;\n\t\tsize += 1;\n\t}\n\n\tif (size > 0)\n\t\t*o = '\\0';\n\n\treturn size;\n}\n\nint parse_query_param(char** query, char** key, char** value)\n{\n\tif (*query == NULL) {\n\t\treturn -1;\n\t}\n\n\t*key = *query;\n\t*value = NULL;\n\n\tbool string_ended = false;\n\tint value_length = -1;\n\tchar *c;\n\n\tfor(c = *query; *c != '&'; c++) {\n\t\tif (*c == '\\0') {\n\t\t\tstring_ended = true;\n\t\t\tbreak;\n\t\t} else if (*value == NULL && *c == '=') {\n\t\t\t*c = '\\0';\n\t\t\t*value = c + 1;\n\t\t} \n\t\t\n\t\tif (*value != NULL) {\n\t\t\tvalue_length += 1;\n\t\t}\n\t}\n\n\t*c = '\\0';\n\n\tif (!string_ended) {\n\t\t*query = c + 1;\n\t} else {\n\t\t*query = NULL;\n\t}\n\n\treturn value_length;\n}\n\n"
  },
  {
    "path": "scripts/traffic/utils.h",
    "content": "#ifndef UTILS_H\n#define UTILS_H\n\n#include <stdarg.h>\n#include <stdbool.h>\n\n/*****\n * split a tab separated line of text into its constituent fields.\n */\n#define NO_MORE_FIELDS ((char**)NULL)\nvoid split_fields(char* line, ...);\n\n/*****\n * parse one parameter from the query string. returns length of value.\n */\nint parse_query_param(char** query, char** key, char** value);\n\n/*****\n * undo url encoding in place. returns length of decoded string.\n */\nint url_decode(char* encoded);\n\n#endif/*UTILS_H_*/\n\n"
  },
  {
    "path": "scripts/traffic/verify.c",
    "content": "#include <stdio.h>\n#include <assert.h>\n#include <stdbool.h>\n#include <string.h>\n#include <stdlib.h>\n\n#include <openssl/sha.h>\n#include <openssl/hmac.h>\n\n#include \"utils.h\"\n\n#define MAX_LINE 2048\n#define SHA1_HASH_LENGTH 40\n\nint main(int argc, char** argv)\n{\n    const char* secret;\n    secret = getenv(\"TRACKING_SECRET\");\n    if (!secret) {\n        fprintf(stderr, \"TRACKING_SECRET not set\\n\");\n        return 1;\n    }\n\n    char input_line[MAX_LINE];\n    unsigned int hash_length = SHA_DIGEST_LENGTH;\n    unsigned char input_hash[hash_length];\n    unsigned char expected_hash[hash_length];\n    int secret_length = strlen(secret);\n\n    while (fgets(input_line, MAX_LINE, stdin) != NULL) {\n        /* get the fields */\n        char *ip, *path, *query, *response_code, *unique_id;\n\n        split_fields(\n            input_line, \n            &ip, \n            &path, \n            &query, \n            &response_code, \n            &unique_id, \n            NO_MORE_FIELDS\n        );\n\n        /* in the query string, grab the fields we want to verify */\n        char *id = NULL;\n        char *hash = NULL;\n        char *url = NULL;\n\n        char *key, *value;\n        while (parse_query_param(&query, &key, &value) >= 0) {\n            if (strcmp(key, \"id\") == 0) {\n                id = value;\n            } else if (strcmp(key, \"hash\") == 0) {\n                hash = value;\n            } else if (strcmp(key, \"url\") == 0) {\n                url = value;\n            }\n        }\n\n        if (id == NULL || hash == NULL)\n            continue;\n\n        /* decode the params */\n        int id_length = url_decode(id);\n        if (id_length < 0)\n            continue;\n\n        if (url_decode(hash) != SHA1_HASH_LENGTH)\n            continue;\n\n        int url_length = 0;\n        if (url != NULL) {\n            url_length = url_decode(url);\n            if (url_length < 0)\n                continue;\n        }\n\n        /* validation:\n            * for clicks just check the response code--validation was done in\n              the click redirect app\n            * for impression pixels check the hash\n        */\n        if (strcmp(\"/click\", path) == 0) {\n            if (strcmp(response_code, \"302\") != 0) {\n                continue;\n            }\n        } else {\n            /* turn the expected hash into bytes */\n            bool bad_hash = false;\n            for (int i = 0; i < hash_length; i++) {\n                int count = sscanf(&hash[i*2], \"%2hhx\", &input_hash[i]);\n                if (count != 1) {\n                    bad_hash = true;\n                    break;\n                }\n            }\n\n            if (bad_hash)\n                continue;\n\n            /* generate the expected hash */\n            HMAC_CTX ctx;\n\n            // NOTE: EMR has openssl <1.0, so these HMAC methods don't return\n            // error codes -- see https://www.openssl.org/docs/crypto/hmac.html\n            HMAC_Init(&ctx, secret, secret_length, EVP_sha1());\n            HMAC_Update(&ctx, id, id_length);\n            HMAC_Final(&ctx, expected_hash, &hash_length);\n\n            /* check that the hashes match */\n            if (memcmp(input_hash, expected_hash, SHA_DIGEST_LENGTH) != 0)\n                continue;\n        }\n\n        /* split out the fullname and subreddit if necessary */\n        char *fullname = id;\n        char *subreddit = NULL;\n\n        for (char *c = id; *c != '\\0'; c++) {\n            if (*c == '-') {\n                subreddit = c + 1;\n                *c = '\\0';\n                break;\n            }\n        }\n\n        /* output stuff! */\n        fputs(unique_id, stdout);\n        fputc('\\t', stdout);\n\n        fputs(path, stdout);\n        fputc('\\t', stdout);\n\n        fputs(fullname, stdout);\n        fputc('\\t', stdout);\n\n        if (subreddit != NULL) {\n            fputs(subreddit, stdout);\n        }\n\n        fputc('\\n', stdout);\n    }\n}\n"
  },
  {
    "path": "scripts/upload_static_files_to_s3.py",
    "content": "#!/usr/bin/python\n# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\n\nimport os\nimport argparse\nimport mimetypes\nimport urlparse\n\nimport boto\n\n\nNEVER = 'Thu, 31 Dec 2037 23:59:59 GMT'\n\n\ndef upload(static_root, bucket_url):\n    s3 = boto.connect_s3()\n    bucket = s3.get_bucket(bucket_url.netloc, validate=False)\n\n    # build a list of files already in the bucket\n    print \"checking existing files on s3...\"\n    remote_files = {key.name : key.etag.strip('\"') for key in bucket.list()}\n\n    # upload local files not already in the bucket\n    for root, dirs, files in os.walk(static_root):\n        for file in files:\n            absolute_path = os.path.join(root, file)\n            relative_path = os.path.relpath(absolute_path, start=static_root)\n            key_name = os.path.join(bucket_url.path, relative_path).lstrip(\"/\")\n\n            type, encoding = mimetypes.guess_type(file)\n            if not type:\n                continue\n            headers = {}\n            headers['Expires'] = NEVER\n            headers['Content-Type'] = type\n            if encoding:\n                headers['Content-Encoding'] = encoding\n\n            key = bucket.new_key(key_name)\n            with open(absolute_path, 'rb') as f:\n                etag, base64_tag = key.compute_md5(f)\n\n                # don't upload the file if it already exists unmodified in the bucket\n                if remote_files.get(key_name, None) == etag:\n                    continue\n\n                print \"uploading\", key_name, \"to S3...\"\n                key.set_contents_from_file(\n                    f,\n                    headers=headers,\n                    policy='public-read',\n                    md5=(etag, base64_tag),\n                )\n\n    print \"all done\"\n\n\ndef s3_url(text):\n    parsed = urlparse.urlparse(text)\n    if parsed.scheme != \"s3\":\n        raise ValueError(\"not an s3 url\")\n    if parsed.params or parsed.query or parsed.fragment:\n        raise ValueError(\"params, query, and fragment not supported\")\n    return parsed\n\n\ndef main():\n    parser = argparse.ArgumentParser()\n    parser.add_argument(\"root\")\n    parser.add_argument(\"bucket\", type=s3_url)\n    args = parser.parse_args()\n    upload(args.root, args.bucket)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "scripts/wrap-job",
    "content": "#!/usr/bin/python\n\"\"\"\nWrap a command, setuid/setgid, change directory to the reddit\ncode, and send output to syslog.\n\nThe following environment variables may be used to control\nthe environment the wrapped command runs in:\n\nREDDIT_USER\n\n    The user to run the job as. Defaults to \"reddit\".\n\nREDDIT_GROUP\n\n    The group to run the job as. Defaults to $REDDIT_USER.\n\nREDDIT_ROOT\n\n    The root directory of the reddit package. It's where the makefile lives.\n\nREDDIT_LOG_FACILITY\n\n    The syslog facility to write messages to.\n\"\"\"\n\nimport os\nimport sys\nimport grp\nimport pwd\nimport syslog\nimport subprocess\n\n\nCONSUMER_PREFIX = \"reddit-consumer-\"\n\n\n# drop permissions\nuser = os.environ.get(\"REDDIT_USER\", \"reddit\")\ngroup = os.environ.get(\"REDDIT_GROUP\", user)\nuid = pwd.getpwnam(user).pw_uid\ngid = grp.getgrnam(group).gr_gid\nos.setgroups([])\nos.setgid(gid)\nos.setuid(uid)\n\n# change directory to the reddit code root\nroot = os.environ.get(\"REDDIT_ROOT\", \"/opt/reddit/lib/public/r2\")\nos.chdir(root)\n\n# configure syslog\njob_name = os.environ.get(\"UPSTART_JOB\", \"-\".join(sys.argv[1:]))\nif job_name.startswith(CONSUMER_PREFIX):\n    # consumers are a bit different from crons, while crons want an\n    # ident of reddit-job-JOBNAME, we want consumers to have an ident\n    # of CONSUMERNAME_INSTANCE\n    job_name = (job_name[len(CONSUMER_PREFIX):] +\n                \"_\" +\n                os.environ.get(\"UPSTART_INSTANCE\", \"\"))\nfacility = getattr(syslog, \"LOG_\" + os.environ.get(\"REDDIT_LOG_FACILITY\", \"CRON\"))\nsyslog.openlog(ident=job_name, facility=facility)\n\n# run the wrapped command\nchild = subprocess.Popen(sys.argv[1:],\n                         stdout=subprocess.PIPE,\n                         stderr=subprocess.STDOUT,\n                         bufsize=1)\n\n# write out to syslog\nwhile True:\n    line = child.stdout.readline()\n\n    if not line:\n        break\n\n    line = line.rstrip('\\n')\n    syslog.syslog(syslog.LOG_NOTICE, line)\n    print line\n\n# our success depends on our child's success\nchild.wait()\nsys.exit(child.returncode)\n"
  },
  {
    "path": "scripts/write_live_config",
    "content": "#!/usr/bin/env python\n# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\"\"\"Read config from an INI file and put it in ZooKeeper for instant use.\"\"\"\n\nfrom difflib import SequenceMatcher\nimport itertools\nimport os\nfrom pprint import PrettyPrinter\nimport sys\nimport json\nimport getpass\nimport ConfigParser\nimport zlib\n\nimport kazoo.client\nfrom kazoo.security import make_acl, make_digest_acl\nfrom kazoo.exceptions import NoAuthException\n\nfrom r2.lib.zookeeper import LiveConfig, connect_to_zookeeper\nfrom r2.lib.app_globals import extract_live_config, LIVE_CONFIG_NODE\nfrom r2.lib.configparse import ConfigValue\nfrom r2.lib.plugin import PluginLoader\nfrom r2.lib.utils import parse_ini_file\n\n\nUSERNAME = \"live-config\"\n\n\ndef write_config_to_zookeeper(node, username, password, config, live_config):\n    \"\"\"Write given configuration to ZooKeeper with correct security etc.\"\"\"\n\n    # read the zk configuration from the app's config\n    zk_hostlist = config.get(\"DEFAULT\", \"zookeeper_connection_string\")\n    app_username = config.get(\"DEFAULT\", \"zookeeper_username\")\n    app_password = config.get(\"DEFAULT\", \"zookeeper_password\")\n\n    # connect to zk!\n    client = connect_to_zookeeper(zk_hostlist, (username, password))\n\n    # ensure that the path leading up to the config node exists. if it doesn't,\n    # create it with ACLs such that new stuff can be added below it, but no one\n    # but we can delete nodes.\n    parent_path = os.path.dirname(node)\n    client.ensure_path(parent_path, acl=[\n        # only we can delete children\n        make_digest_acl(username, password, delete=True),\n\n        # anyone authenticated can read/list children/create children\n        make_acl(\"auth\", \"\", read=True, create=True),\n    ])\n\n    # create or update the config node ensuring that only we can write to it.\n    json_data = json.dumps(live_config)\n    compressed_data = \"gzip\" + zlib.compress(json_data)\n\n    try:\n        client.create(node, compressed_data, acl=[\n            make_digest_acl(username, password, read=True, write=True),\n            make_digest_acl(app_username, app_password, read=True),\n        ])\n    except kazoo.exceptions.NodeExistsException:\n        client.set(node, compressed_data)\n\n\ndef get_comparable_repr(obj):\n    \"\"\"Return a representation of the object that can be string-compared.\"\"\"\n\n    # If the object is a dict, we'll use the pprint module, because it\n    # automatically sorts by key when generating its output for dicts\n    if isinstance(obj, dict):\n        # specify a huge width so it never tries to wrap the output\n        printer = PrettyPrinter(width=1000000)\n        return printer.pformat(obj)\n\n    # Otherwise, just use the standard repr for that object type\n    return repr(obj)\n\n\ndef print_dict_diff(old, new):\n    \"\"\"Output changes between two dicts.\"\"\"\n\n    old_keys = set(old.keys())\n    new_keys = set(new.keys())\n\n    # figure out which keys are interesting that we need to output\n    removed_keys = old_keys - new_keys\n    added_keys = new_keys - old_keys\n    both_keys = old_keys & new_keys\n    changed_keys = [key for key in both_keys if new[key] != old[key]]\n\n    for key in changed_keys[:]:\n        # if the value type changed, or it's a type with short values,\n        # just display it as a removal and addition\n        if (type(new[key]) != type(old[key]) or\n                isinstance(new[key], (int, float, bool))):\n            added_keys.add(key)\n            removed_keys.add(key)\n            changed_keys.remove(key)\n            continue\n\n        # otherwise, see how similar the reprs are, and if it's less\n        # than 50% similar, just display it as a removal and addition\n        old_repr = get_comparable_repr(old[key])\n        new_repr = get_comparable_repr(new[key])\n        matcher = SequenceMatcher(a=old_repr, b=new_repr)\n\n        if matcher.ratio() < 0.5:\n            added_keys.add(key)\n            removed_keys.add(key)\n            changed_keys.remove(key)\n            continue\n\n    keys = sorted(set(itertools.chain(removed_keys, added_keys, changed_keys)))\n    if not keys:\n        print \"No changes found. Did you forget to deploy the INI change?\"\n        return\n\n    max_key_length = max(len(key) for key in keys)\n\n    for key in keys:\n        if key in removed_keys:\n            print \"- {key:<{length}s} = {value!r}\".format(\n                key=key, value=old[key], length=max_key_length)\n\n        if key in added_keys:\n            print \"+ {key:<{length}s} = {value!r}\".format(\n                key=key, value=new[key], length=max_key_length)\n            \n        if key in changed_keys:\n            print \"! {key:<{length}s} :\".format(key=key, length=max_key_length)\n\n            old_repr = get_comparable_repr(old[key])\n            new_repr = get_comparable_repr(new[key])\n            matcher = SequenceMatcher(a=old_repr, b=new_repr)\n\n            for tag, i, j, m, n in matcher.get_opcodes():\n                if tag == \"equal\":\n                    continue\n                elif tag == \"replace\":\n                    print '    ! \"{}\" to \"{}\"'.format(\n                        matcher.a[i:j], matcher.b[m:n])\n                elif tag == \"insert\":\n                    print '    + \"{}\"'.format(matcher.b[m:n])\n                elif tag == \"delete\":\n                    print '    - \"{}\"'.format(matcher.a[i:j])\n\n\ndef get_current_live_config(config):\n    \"\"\"Return the current live config values as a dict.\"\"\"\n\n    # read the zk configuration from the app's config\n    zk_hostlist = config.get(\"DEFAULT\", \"zookeeper_connection_string\")\n    username = config.get(\"DEFAULT\", \"zookeeper_username\")\n    password = config.get(\"DEFAULT\", \"zookeeper_password\")\n\n    client = connect_to_zookeeper(zk_hostlist, (username, password))\n    return LiveConfig(client, LIVE_CONFIG_NODE).data\n\n\ndef confirm_config(old_config, new_config):\n    \"\"\"Display the changes and confirm that we should continue.\"\"\"\n\n    # we need to convert the new config to json and back so values like\n    # tuples are converted to the same format as the existing config\n    new_config = json.loads(json.dumps(new_config))\n\n    print (\"Updates (a line starting with + is a new setting, - is removed, \"\n        \"and ! is changed, with additions/removals/replacements indented):\\n\")\n    print_dict_diff(old_config, new_config)\n    answer = raw_input(\"\\nContinue? [y|N] \")\n    return answer.lower() == \"y\"\n\n\ndef main():\n    \"\"\"Get and validate input from the user via CLI then write to ZK.\"\"\"\n\n    progname = os.path.basename(sys.argv[0])\n\n    try:\n        ini_file_name = sys.argv[1]\n    except IndexError:\n        print >> sys.stderr, \"USAGE: %s INI\" % progname\n        return 1\n\n    try:\n        with open(ini_file_name) as ini_file:\n            config = parse_ini_file(ini_file)\n    except (IOError, ConfigParser.Error), e:\n        print >> sys.stderr, \"%s: %s: %s\" % (progname, ini_file_name, e)\n        return 1\n\n    try:\n        plugin_config = config.get(\"DEFAULT\", \"plugins\")\n        plugin_names = ConfigValue.tuple(plugin_config)\n        plugins = PluginLoader(plugin_names=plugin_names)\n        live = extract_live_config(config, plugins)\n    except ValueError as e:\n        print >> sys.stderr, \"%s: %s\" % (progname, e)\n        return 1\n    else:\n        current_config = get_current_live_config(config)\n        if not confirm_config(current_config, live):\n            print \"Oh, well, never mind then. Bye :(\"\n            return 1\n\n    password = getpass.getpass(\"Password: \")\n\n    try:\n        write_config_to_zookeeper(LIVE_CONFIG_NODE,\n                                  USERNAME, password,\n                                  config, live)\n    except NoAuthException:\n        print >> sys.stderr, \"%s: incorrect password\" % progname\n        return 1\n    except Exception as e:\n        print >> sys.stderr, \"%s: %s\" % (progname, e)\n        return 1\n\n    print \"Succesfully updated live config!\"\n\n    return 0\n\n\nif __name__ == \"__main__\":\n    sys.exit(main())\n"
  },
  {
    "path": "scripts/write_secrets",
    "content": "#!/usr/bin/env python\n# The contents of this file are subject to the Common Public Attribution\n# License Version 1.0. (the \"License\"); you may not use this file except in\n# compliance with the License. You may obtain a copy of the License at\n# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public\n# License Version 1.1, but Sections 14 and 15 have been added to cover use of\n# software over a computer network and provide for limited attribution for the\n# Original Developer. In addition, Exhibit A has been modified to be consistent\n# with Exhibit B.\n#\n# Software distributed under the License is distributed on an \"AS IS\" basis,\n# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for\n# the specific language governing rights and limitations under the License.\n#\n# The Original Code is reddit.\n#\n# The Original Developer is the Initial Developer.  The Initial Developer of\n# the Original Code is reddit Inc.\n#\n# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit\n# Inc. All Rights Reserved.\n###############################################################################\n\nimport base64\nimport ConfigParser\nimport fileinput\nimport getpass\nimport json\nimport os\nimport sys\n\nimport kazoo\n\nfrom kazoo.security import make_digest_acl\n\nfrom r2.lib.utils import parse_ini_file\nfrom r2.lib.zookeeper import connect_to_zookeeper\nfrom r2.lib.app_globals import SECRETS_NODE, extract_secrets\n\n\nUSERNAME = \"live-config\"\n\n\ndef _encode_secrets(secrets):\n    return json.dumps({key: base64.b64encode(secret)\n                       for key, secret in secrets.iteritems()})\n\n\ndef write_secrets_to_zookeeper(reddit_config, username, password, secrets):\n    # read the zk configuration from the app's config\n    zk_hostlist = reddit_config.get(\"DEFAULT\", \"zookeeper_connection_string\")\n    app_username = reddit_config.get(\"DEFAULT\", \"zookeeper_username\")\n    app_password = reddit_config.get(\"DEFAULT\", \"zookeeper_password\")\n\n    # connect to zk!\n    client = connect_to_zookeeper(zk_hostlist, (username, password))\n\n    # we're going to assume that any parent parts of the node path were\n    # already created by write_live_config.\n    json_data = _encode_secrets(secrets)\n    try:\n        client.create(SECRETS_NODE, json_data, acl=[\n            make_digest_acl(username, password, read=True, write=True),\n            make_digest_acl(app_username, app_password, read=True),\n        ])\n    except kazoo.exceptions.NodeExistsException:\n        client.set(SECRETS_NODE, json_data)\n\n\ndef main():\n    progname = os.path.basename(sys.argv[0])\n\n    input = fileinput.input()\n    try:\n        config = parse_ini_file(input)\n    except (IOError, ConfigParser.Error), e:\n        print >> sys.stderr, \"%s: %s\" % (progname, e)\n        return 1\n\n    secrets = extract_secrets(config)\n    password = getpass.getpass(\"ZooKeeper Password: \")\n\n    try:\n        write_secrets_to_zookeeper(config, USERNAME, password, secrets)\n    except kazoo.exceptions.NoAuthException:\n        print >> sys.stderr, \"%s: incorrect password\" % progname\n        return 1\n    except Exception as e:\n        print >> sys.stderr, \"%s: %s\" % (progname, e)\n        return 1\n\n\nif __name__ == \"__main__\":\n    sys.exit(main())\n"
  },
  {
    "path": "solr/README.md",
    "content": "## Solr Search Provider for Reddit\n\nThe Solr search provider lets you run Reddit's search on your own Solr server.\n\n\n### Quickstart\n\nTo set up your own solr instance:\n\n```\nsudo apt-get -y install solr-tomcat\nsudo ln -s /path/to/reddit/solr/schema.xml /usr/share/solr/conf\nsudo service tomcat6 start\n```\n\nYou should now be able to connect to Solr at http://127.0.0.1:8080\n\nTo configure reddit to use Solr for search, set the search provider to **solr**\nin your .ini file:\n\n```\nsearch_provider = solr\n```\n\nThen add the following config lines:\n```\n# version of solr service--versions 1.x and 4.x have been tested. \n# only the major version number matters here\nsolr_version = 1\n# solr search service hostname or IP\nsolr_search_host = 127.0.0.1\n# hostname or IP for link upload\nsolr_doc_host = 127.0.0.1\n# hostname or IP for subreddit search\nsolr_subreddit_search_host = 127.0.0.1\n# hostname or IP subreddit upload\nsolr_subreddit_doc_host = 127.0.0.1\n# solr port (assumed same on all hosts)\nsolr_port = 8080\n# solr4 core name (not used with Solr 1.x)\nsolr_core = collection1\n# default batch size \n# limit is hard-coded to 1000\n# set to 1 for testing\nsolr_min_batch = 500\n# optionally, you may select your solr query parser here\n# see documentation for your version of Solr\nsolr_query_parser = \n```\n\n### Notes\n\nIf you build Solr from source, the default port will be 8983.\n\n"
  },
  {
    "path": "solr/schema.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n<!--\n Licensed to the Apache Software Foundation (ASF) under one or more\n contributor license agreements.  See the NOTICE file distributed with\n this work for additional information regarding copyright ownership.\n The ASF licenses this file to You under the Apache License, Version 2.0\n (the \"License\"); you may not use this file except in compliance with\n the License.  You may obtain a copy of the License at\n\n     http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n-->\n\n<!--  \n This is the Solr schema file. This file should be named \"schema.xml\" and\n should be in the conf directory under the solr home\n (i.e. ./solr/conf/schema.xml by default) \n or located where the classloader for the Solr webapp can find it.\n\n This example schema is the recommended starting point for users.\n It should be kept correct and concise, usable out-of-the-box.\n\n For more information, on how to customize this file, please see\n http://wiki.apache.org/solr/SchemaXml\n\n PERFORMANCE NOTE: this schema includes many optional features and should not\n be used for benchmarking.  To improve performance one could\n  - set stored=\"false\" for all fields possible (esp large fields) when you\n    only need to search on the field but don't need to return the original\n    value.\n  - set indexed=\"false\" if you don't need to search on the field, but only\n    return the field as a result of searching on other indexed fields.\n  - remove all unneeded copyField statements\n  - for best index size and searching performance, set \"index\" to false\n    for all general text fields, use copyField to copy them to the\n    catchall \"text\" field, and use that for searching.\n  - For maximum indexing performance, use the StreamingUpdateSolrServer\n    java client.\n  - Remember to run the JVM in server mode, and use a higher logging level\n    that avoids logging every request\n-->\n\n<schema name=\"reddit\" version=\"1.2\">\n  <!-- attribute \"name\" is the name of this schema and is only used for display purposes.\n       Applications should change this to reflect the nature of the search collection.\n       version=\"1.2\" is Solr's version number for the schema syntax and semantics.  It should\n       not normally be changed by applications.\n       1.0: multiValued attribute did not exist, all fields are multiValued by nature\n       1.1: multiValued attribute introduced, false by default \n       1.2: omitTermFreqAndPositions attribute introduced, true by default except for text fields.\n     -->\n\n  <types>\n    <!-- field type definitions. The \"name\" attribute is\n       just a label to be used by field definitions.  The \"class\"\n       attribute and any other attributes determine the real\n       behavior of the fieldType.\n         Class names starting with \"solr\" refer to java classes in the\n       org.apache.solr.analysis package.\n    -->\n\n    <!-- The StrField type is not analyzed, but indexed/stored verbatim.  \n       - StrField and TextField support an optional compressThreshold which\n       limits compression (if enabled in the derived fields) to values which\n       exceed a certain size (in characters).\n    -->\n    <fieldType name=\"string\" class=\"solr.StrField\" sortMissingLast=\"true\" omitNorms=\"true\"/>\n\n    <!-- boolean type: \"true\" or \"false\" -->\n    <fieldType name=\"boolean\" class=\"solr.BoolField\" sortMissingLast=\"true\" omitNorms=\"true\"/>\n    <!--Binary data type. The data should be sent/retrieved in as Base64 encoded Strings -->\n    <fieldtype name=\"binary\" class=\"solr.BinaryField\"/>\n\n    <!-- The optional sortMissingLast and sortMissingFirst attributes are\n         currently supported on types that are sorted internally as strings.\n\t       This includes \"string\",\"boolean\",\"sint\",\"slong\",\"sfloat\",\"sdouble\",\"pdate\"\n       - If sortMissingLast=\"true\", then a sort on this field will cause documents\n         without the field to come after documents with the field,\n         regardless of the requested sort order (asc or desc).\n       - If sortMissingFirst=\"true\", then a sort on this field will cause documents\n         without the field to come before documents with the field,\n         regardless of the requested sort order.\n       - If sortMissingLast=\"false\" and sortMissingFirst=\"false\" (the default),\n         then default lucene sorting will be used which places docs without the\n         field first in an ascending sort and last in a descending sort.\n    -->    \n\n    <!--\n      Default numeric field types. For faster range queries, consider the tint/tfloat/tlong/tdouble types.\n    -->\n    <fieldType name=\"int\" class=\"solr.TrieIntField\" precisionStep=\"0\" omitNorms=\"true\" positionIncrementGap=\"0\"/>\n    <fieldType name=\"float\" class=\"solr.TrieFloatField\" precisionStep=\"0\" omitNorms=\"true\" positionIncrementGap=\"0\"/>\n    <fieldType name=\"long\" class=\"solr.TrieLongField\" precisionStep=\"0\" omitNorms=\"true\" positionIncrementGap=\"0\"/>\n    <fieldType name=\"double\" class=\"solr.TrieDoubleField\" precisionStep=\"0\" omitNorms=\"true\" positionIncrementGap=\"0\"/>\n\n    <!--\n     Numeric field types that index each value at various levels of precision\n     to accelerate range queries when the number of values between the range\n     endpoints is large. See the javadoc for NumericRangeQuery for internal\n     implementation details.\n\n     Smaller precisionStep values (specified in bits) will lead to more tokens\n     indexed per value, slightly larger index size, and faster range queries.\n     A precisionStep of 0 disables indexing at different precision levels.\n    -->\n    <fieldType name=\"tint\" class=\"solr.TrieIntField\" precisionStep=\"8\" omitNorms=\"true\" positionIncrementGap=\"0\"/>\n    <fieldType name=\"tfloat\" class=\"solr.TrieFloatField\" precisionStep=\"8\" omitNorms=\"true\" positionIncrementGap=\"0\"/>\n    <fieldType name=\"tlong\" class=\"solr.TrieLongField\" precisionStep=\"8\" omitNorms=\"true\" positionIncrementGap=\"0\"/>\n    <fieldType name=\"tdouble\" class=\"solr.TrieDoubleField\" precisionStep=\"8\" omitNorms=\"true\" positionIncrementGap=\"0\"/>\n\n    <!-- The format for this date field is of the form 1995-12-31T23:59:59Z, and\n         is a more restricted form of the canonical representation of dateTime\n         http://www.w3.org/TR/xmlschema-2/#dateTime    \n         The trailing \"Z\" designates UTC time and is mandatory.\n         Optional fractional seconds are allowed: 1995-12-31T23:59:59.999Z\n         All other components are mandatory.\n\n         Expressions can also be used to denote calculations that should be\n         performed relative to \"NOW\" to determine the value, ie...\n\n               NOW/HOUR\n                  ... Round to the start of the current hour\n               NOW-1DAY\n                  ... Exactly 1 day prior to now\n               NOW/DAY+6MONTHS+3DAYS\n                  ... 6 months and 3 days in the future from the start of\n                      the current day\n                      \n         Consult the DateField javadocs for more information.\n\n         Note: For faster range queries, consider the tdate type\n      -->\n    <fieldType name=\"date\" class=\"solr.TrieDateField\" omitNorms=\"true\" precisionStep=\"0\" positionIncrementGap=\"0\"/>\n\n    <!-- A Trie based date field for faster date range queries and date faceting. -->\n    <fieldType name=\"tdate\" class=\"solr.TrieDateField\" omitNorms=\"true\" precisionStep=\"6\" positionIncrementGap=\"0\"/>\n\n\n    <!--\n      Note:\n      These should only be used for compatibility with existing indexes (created with older Solr versions)\n      or if \"sortMissingFirst\" or \"sortMissingLast\" functionality is needed. Use Trie based fields instead.\n\n      Plain numeric field types that store and index the text\n      value verbatim (and hence don't support range queries, since the\n      lexicographic ordering isn't equal to the numeric ordering)\n    -->\n    <fieldType name=\"pint\" class=\"solr.IntField\" omitNorms=\"true\"/>\n    <fieldType name=\"plong\" class=\"solr.LongField\" omitNorms=\"true\"/>\n    <fieldType name=\"pfloat\" class=\"solr.FloatField\" omitNorms=\"true\"/>\n    <fieldType name=\"pdouble\" class=\"solr.DoubleField\" omitNorms=\"true\"/>\n    <fieldType name=\"pdate\" class=\"solr.DateField\" sortMissingLast=\"true\" omitNorms=\"true\"/>\n\n\n    <!--\n      Note:\n      These should only be used for compatibility with existing indexes (created with older Solr versions)\n      or if \"sortMissingFirst\" or \"sortMissingLast\" functionality is needed. Use Trie based fields instead.\n\n      Numeric field types that manipulate the value into\n      a string value that isn't human-readable in its internal form,\n      but with a lexicographic ordering the same as the numeric ordering,\n      so that range queries work correctly.\n    -->\n    <fieldType name=\"sint\" class=\"solr.SortableIntField\" sortMissingLast=\"true\" omitNorms=\"true\"/>\n    <fieldType name=\"slong\" class=\"solr.SortableLongField\" sortMissingLast=\"true\" omitNorms=\"true\"/>\n    <fieldType name=\"sfloat\" class=\"solr.SortableFloatField\" sortMissingLast=\"true\" omitNorms=\"true\"/>\n    <fieldType name=\"sdouble\" class=\"solr.SortableDoubleField\" sortMissingLast=\"true\" omitNorms=\"true\"/>\n\n\n    <!-- The \"RandomSortField\" is not used to store or search any\n         data.  You can declare fields of this type it in your schema\n         to generate pseudo-random orderings of your docs for sorting \n         purposes.  The ordering is generated based on the field name \n         and the version of the index, As long as the index version\n         remains unchanged, and the same field name is reused,\n         the ordering of the docs will be consistent.  \n         If you want different psuedo-random orderings of documents,\n         for the same version of the index, use a dynamicField and\n         change the name\n     -->\n    <fieldType name=\"random\" class=\"solr.RandomSortField\" indexed=\"true\" />\n\n    <!-- solr.TextField allows the specification of custom text analyzers\n         specified as a tokenizer and a list of token filters. Different\n         analyzers may be specified for indexing and querying.\n\n         The optional positionIncrementGap puts space between multiple fields of\n         this type on the same document, with the purpose of preventing false phrase\n         matching across fields.\n\n         For more info on customizing your analyzer chain, please see\n         http://wiki.apache.org/solr/AnalyzersTokenizersTokenFilters\n     -->\n\n    <!-- One can also specify an existing Analyzer class that has a\n         default constructor via the class attribute on the analyzer element\n    <fieldType name=\"text_greek\" class=\"solr.TextField\">\n      <analyzer class=\"org.apache.lucene.analysis.el.GreekAnalyzer\"/>\n    </fieldType>\n    -->\n\n    <!-- A text field that only splits on whitespace for exact matching of words -->\n    <fieldType name=\"text_ws\" class=\"solr.TextField\" positionIncrementGap=\"100\">\n      <analyzer>\n        <tokenizer class=\"solr.WhitespaceTokenizerFactory\"/>\n      </analyzer>\n    </fieldType>\n\n    <!-- A text field that uses WordDelimiterFilter to enable splitting and matching of\n        words on case-change, alpha numeric boundaries, and non-alphanumeric chars,\n        so that a query of \"wifi\" or \"wi fi\" could match a document containing \"Wi-Fi\".\n        Synonyms and stopwords are customized by external files, and stemming is enabled.\n        -->\n    <fieldType name=\"text\" class=\"solr.TextField\" positionIncrementGap=\"100\">\n      <analyzer type=\"index\">\n        <tokenizer class=\"solr.WhitespaceTokenizerFactory\"/>\n        <!-- in this example, we will only use synonyms at query time\n        <filter class=\"solr.SynonymFilterFactory\" synonyms=\"index_synonyms.txt\" ignoreCase=\"true\" expand=\"false\"/>\n        -->\n        <!-- Case insensitive stop word removal.\n          add enablePositionIncrements=true in both the index and query\n          analyzers to leave a 'gap' for more accurate phrase queries.\n        -->\n        <filter class=\"solr.StopFilterFactory\"\n                ignoreCase=\"true\"\n                words=\"stopwords.txt\"\n                enablePositionIncrements=\"true\"\n                />\n        <filter class=\"solr.WordDelimiterFilterFactory\" generateWordParts=\"1\" generateNumberParts=\"1\" catenateWords=\"1\" catenateNumbers=\"1\" catenateAll=\"0\" splitOnCaseChange=\"1\"/>\n        <filter class=\"solr.LowerCaseFilterFactory\"/>\n        <filter class=\"solr.SnowballPorterFilterFactory\" language=\"English\" protected=\"protwords.txt\"/>\n      </analyzer>\n      <analyzer type=\"query\">\n        <tokenizer class=\"solr.WhitespaceTokenizerFactory\"/>\n        <filter class=\"solr.SynonymFilterFactory\" synonyms=\"synonyms.txt\" ignoreCase=\"true\" expand=\"true\"/>\n        <filter class=\"solr.StopFilterFactory\"\n                ignoreCase=\"true\"\n                words=\"stopwords.txt\"\n                enablePositionIncrements=\"true\"\n                />\n        <filter class=\"solr.WordDelimiterFilterFactory\" generateWordParts=\"1\" generateNumberParts=\"1\" catenateWords=\"0\" catenateNumbers=\"0\" catenateAll=\"0\" splitOnCaseChange=\"1\"/>\n        <filter class=\"solr.LowerCaseFilterFactory\"/>\n        <filter class=\"solr.SnowballPorterFilterFactory\" language=\"English\" protected=\"protwords.txt\"/>\n      </analyzer>\n    </fieldType>\n\n\n    <!-- Less flexible matching, but less false matches.  Probably not ideal for product names,\n         but may be good for SKUs.  Can insert dashes in the wrong place and still match. -->\n    <fieldType name=\"textTight\" class=\"solr.TextField\" positionIncrementGap=\"100\" >\n      <analyzer>\n        <tokenizer class=\"solr.WhitespaceTokenizerFactory\"/>\n        <filter class=\"solr.SynonymFilterFactory\" synonyms=\"synonyms.txt\" ignoreCase=\"true\" expand=\"false\"/>\n        <filter class=\"solr.StopFilterFactory\" ignoreCase=\"true\" words=\"stopwords.txt\"/>\n        <filter class=\"solr.WordDelimiterFilterFactory\" generateWordParts=\"0\" generateNumberParts=\"0\" catenateWords=\"1\" catenateNumbers=\"1\" catenateAll=\"0\"/>\n        <filter class=\"solr.LowerCaseFilterFactory\"/>\n        <filter class=\"solr.SnowballPorterFilterFactory\" language=\"English\" protected=\"protwords.txt\"/>\n        <!-- this filter can remove any duplicate tokens that appear at the same position - sometimes\n             possible with WordDelimiterFilter in conjuncton with stemming. -->\n        <filter class=\"solr.RemoveDuplicatesTokenFilterFactory\"/>\n      </analyzer>\n    </fieldType>\n\n\n    <!-- A general unstemmed text field - good if one does not know the language of the field -->\n    <fieldType name=\"textgen\" class=\"solr.TextField\" positionIncrementGap=\"100\">\n      <analyzer type=\"index\">\n        <tokenizer class=\"solr.WhitespaceTokenizerFactory\"/>\n        <filter class=\"solr.StopFilterFactory\" ignoreCase=\"true\" words=\"stopwords.txt\" enablePositionIncrements=\"true\" />\n        <filter class=\"solr.WordDelimiterFilterFactory\" generateWordParts=\"1\" generateNumberParts=\"1\" catenateWords=\"1\" catenateNumbers=\"1\" catenateAll=\"0\" splitOnCaseChange=\"0\"/>\n        <filter class=\"solr.LowerCaseFilterFactory\"/>\n      </analyzer>\n      <analyzer type=\"query\">\n        <tokenizer class=\"solr.WhitespaceTokenizerFactory\"/>\n        <filter class=\"solr.SynonymFilterFactory\" synonyms=\"synonyms.txt\" ignoreCase=\"true\" expand=\"true\"/>\n        <filter class=\"solr.StopFilterFactory\"\n                ignoreCase=\"true\"\n                words=\"stopwords.txt\"\n                enablePositionIncrements=\"true\"\n                />\n        <filter class=\"solr.WordDelimiterFilterFactory\" generateWordParts=\"1\" generateNumberParts=\"1\" catenateWords=\"0\" catenateNumbers=\"0\" catenateAll=\"0\" splitOnCaseChange=\"0\"/>\n        <filter class=\"solr.LowerCaseFilterFactory\"/>\n      </analyzer>\n    </fieldType>\n\n\n    <!-- A general unstemmed text field that indexes tokens normally and also\n         reversed (via ReversedWildcardFilterFactory), to enable more efficient \n\t leading wildcard queries. -->\n    <fieldType name=\"text_rev\" class=\"solr.TextField\" positionIncrementGap=\"100\">\n      <analyzer type=\"index\">\n        <tokenizer class=\"solr.WhitespaceTokenizerFactory\"/>\n        <filter class=\"solr.StopFilterFactory\" ignoreCase=\"true\" words=\"stopwords.txt\" enablePositionIncrements=\"true\" />\n        <filter class=\"solr.WordDelimiterFilterFactory\" generateWordParts=\"1\" generateNumberParts=\"1\" catenateWords=\"1\" catenateNumbers=\"1\" catenateAll=\"0\" splitOnCaseChange=\"0\"/>\n        <filter class=\"solr.LowerCaseFilterFactory\"/>\n        <filter class=\"solr.ReversedWildcardFilterFactory\" withOriginal=\"true\"\n           maxPosAsterisk=\"3\" maxPosQuestion=\"2\" maxFractionAsterisk=\"0.33\"/>\n      </analyzer>\n      <analyzer type=\"query\">\n        <tokenizer class=\"solr.WhitespaceTokenizerFactory\"/>\n        <filter class=\"solr.SynonymFilterFactory\" synonyms=\"synonyms.txt\" ignoreCase=\"true\" expand=\"true\"/>\n        <filter class=\"solr.StopFilterFactory\"\n                ignoreCase=\"true\"\n                words=\"stopwords.txt\"\n                enablePositionIncrements=\"true\"\n                />\n        <filter class=\"solr.WordDelimiterFilterFactory\" generateWordParts=\"1\" generateNumberParts=\"1\" catenateWords=\"0\" catenateNumbers=\"0\" catenateAll=\"0\" splitOnCaseChange=\"0\"/>\n        <filter class=\"solr.LowerCaseFilterFactory\"/>\n      </analyzer>\n    </fieldType>\n\n    <!-- charFilter + WhitespaceTokenizer  -->\n    <!--\n    <fieldType name=\"textCharNorm\" class=\"solr.TextField\" positionIncrementGap=\"100\" >\n      <analyzer>\n        <charFilter class=\"solr.MappingCharFilterFactory\" mapping=\"mapping-ISOLatin1Accent.txt\"/>\n        <tokenizer class=\"solr.WhitespaceTokenizerFactory\"/>\n      </analyzer>\n    </fieldType>\n    -->\n\n    <!-- This is an example of using the KeywordTokenizer along\n         With various TokenFilterFactories to produce a sortable field\n         that does not include some properties of the source text\n      -->\n    <fieldType name=\"alphaOnlySort\" class=\"solr.TextField\" sortMissingLast=\"true\" omitNorms=\"true\">\n      <analyzer>\n        <!-- KeywordTokenizer does no actual tokenizing, so the entire\n             input string is preserved as a single token\n          -->\n        <tokenizer class=\"solr.KeywordTokenizerFactory\"/>\n        <!-- The LowerCase TokenFilter does what you expect, which can be\n             when you want your sorting to be case insensitive\n          -->\n        <filter class=\"solr.LowerCaseFilterFactory\" />\n        <!-- The TrimFilter removes any leading or trailing whitespace -->\n        <filter class=\"solr.TrimFilterFactory\" />\n        <!-- The PatternReplaceFilter gives you the flexibility to use\n             Java Regular expression to replace any sequence of characters\n             matching a pattern with an arbitrary replacement string, \n             which may include back references to portions of the original\n             string matched by the pattern.\n             \n             See the Java Regular Expression documentation for more\n             information on pattern and replacement string syntax.\n             \n             http://java.sun.com/j2se/1.5.0/docs/api/java/util/regex/package-summary.html\n          -->\n        <filter class=\"solr.PatternReplaceFilterFactory\"\n                pattern=\"([^a-z])\" replacement=\"\" replace=\"all\"\n        />\n      </analyzer>\n    </fieldType>\n    \n    <fieldtype name=\"phonetic\" stored=\"false\" indexed=\"true\" class=\"solr.TextField\" >\n      <analyzer>\n        <tokenizer class=\"solr.StandardTokenizerFactory\"/>\n        <filter class=\"solr.DoubleMetaphoneFilterFactory\" inject=\"false\"/>\n      </analyzer>\n    </fieldtype>\n\n    <fieldtype name=\"payloads\" stored=\"false\" indexed=\"true\" class=\"solr.TextField\" >\n      <analyzer>\n        <tokenizer class=\"solr.WhitespaceTokenizerFactory\"/>\n        <!--\n        The DelimitedPayloadTokenFilter can put payloads on tokens... for example,\n        a token of \"foo|1.4\"  would be indexed as \"foo\" with a payload of 1.4f\n        Attributes of the DelimitedPayloadTokenFilterFactory : \n         \"delimiter\" - a one character delimiter. Default is | (pipe)\n\t \"encoder\" - how to encode the following value into a playload\n\t    float -> org.apache.lucene.analysis.payloads.FloatEncoder,\n\t    integer -> o.a.l.a.p.IntegerEncoder\n\t    identity -> o.a.l.a.p.IdentityEncoder\n            Fully Qualified class name implementing PayloadEncoder, Encoder must have a no arg constructor.\n         -->\n        <filter class=\"solr.DelimitedPayloadTokenFilterFactory\" encoder=\"float\"/>\n      </analyzer>\n    </fieldtype>\n\n    <!-- lowercases the entire field value, keeping it as a single token.  -->\n    <fieldType name=\"lowercase\" class=\"solr.TextField\" positionIncrementGap=\"100\">\n      <analyzer>\n        <tokenizer class=\"solr.KeywordTokenizerFactory\"/>\n        <filter class=\"solr.LowerCaseFilterFactory\" />\n      </analyzer>\n    </fieldType>\n\n\n    <!-- since fields of this type are by default not stored or indexed,\n         any data added to them will be ignored outright.  --> \n    <fieldtype name=\"ignored\" stored=\"false\" indexed=\"false\" multiValued=\"true\" class=\"solr.StrField\" /> \n\n </types>\n\n\n <fields>\n   <!-- Valid attributes for fields:\n     name: mandatory - the name for the field\n     type: mandatory - the name of a previously defined type from the \n       <types> section\n     indexed: true if this field should be indexed (searchable or sortable)\n     stored: true if this field should be retrievable\n     compressed: [false] if this field should be stored using gzip compression\n       (this will only apply if the field type is compressable; among\n       the standard field types, only TextField and StrField are)\n     multiValued: true if this field may contain multiple values per document\n     omitNorms: (expert) set to true to omit the norms associated with\n       this field (this disables length normalization and index-time\n       boosting for the field, and saves some memory).  Only full-text\n       fields or fields that need an index-time boost need norms.\n     termVectors: [false] set to true to store the term vector for a\n       given field.\n       When using MoreLikeThis, fields used for similarity should be\n       stored for best performance.\n     termPositions: Store position information with the term vector.  \n       This will increase storage costs.\n     termOffsets: Store offset information with the term vector. This \n       will increase storage costs.\n     default: a value that should be used if no value is specified\n       when adding a document.\n   -->\n\n   <field name=\"id\" type=\"string\" indexed=\"true\" stored=\"true\" required=\"true\" multiValued=\"false\" /> \n        \n   <field name=\"version\" type=\"int\" indexed=\"true\" stored=\"true\"/>\n   <field name=\"ups\" type=\"int\" indexed=\"true\" stored=\"true\"/>\n   <field name=\"downs\" type=\"int\" indexed=\"true\" stored=\"true\"/>\n   <field name=\"activity\" type=\"int\" indexed=\"true\" stored=\"true\"/>\n   <field name=\"num_comments\" type=\"int\" indexed=\"true\" stored=\"true\"/>\n   <field name=\"fullname\" type=\"string\" indexed=\"true\" stored=\"true\"/>\n   <field name=\"subreddit\" type=\"string\" indexed=\"true\" stored=\"true\"/>\n   <field name=\"reddit\" type=\"string\" indexed=\"true\" stored=\"true\"/>\n   <field name=\"title\" type=\"string\" indexed=\"true\" stored=\"true\"/>\n   <field name=\"sr_id\" type=\"int\" indexed=\"true\" stored=\"true\"/>\n   <field name=\"timestamp\" type=\"int\" indexed=\"true\" stored=\"true\"/>\n   <field name=\"over18\" type=\"boolean\" indexed=\"true\" stored=\"true\"/>\n   <field name=\"nsfw\" type=\"boolean\" indexed=\"true\" stored=\"true\"/>\n   <field name=\"self\" type=\"boolean\" indexed=\"true\" stored=\"true\"/>\n   <field name=\"selftext\" type=\"string\" indexed=\"true\" stored=\"true\"/>\n   <field name=\"is_self\" type=\"boolean\" indexed=\"true\" stored=\"true\"/>\n   <field name=\"author_fullname\" type=\"string\" indexed=\"true\" stored=\"true\"/>\n   <field name=\"author\" type=\"string\" indexed=\"true\" stored=\"true\"/>\n   <field name=\"type_id\" type=\"int\" indexed=\"true\" stored=\"true\"/>\n   <field name=\"flair\" type=\"string\" indexed=\"true\" stored=\"true\"/>\n   <field name=\"flair_css_class\" type=\"string\" indexed=\"true\" stored=\"true\"/>\n   <field name=\"flair_text\" type=\"string\" indexed=\"true\" stored=\"true\"/>\n   <field name=\"name\" type=\"string\" indexed=\"true\" stored=\"true\"/>\n   <field name=\"url\" type=\"string\" indexed=\"true\" stored=\"true\"/>\n   <field name=\"type\" type=\"string\" indexed=\"true\" stored=\"true\"/>\n   <field name=\"language\" type=\"string\" indexed=\"true\" stored=\"true\"/>\n   <field name=\"header_title\" type=\"string\" indexed=\"true\" stored=\"true\"/>\n   <field name=\"description\" type=\"string\" indexed=\"true\" stored=\"true\"/>\n   <field name=\"sidebar\" type=\"string\" indexed=\"true\" stored=\"true\"/>\n   <field name=\"link_type\" type=\"string\" indexed=\"true\" stored=\"true\"/>\n   <field name=\"actvity\" type=\"int\" indexed=\"true\" stored=\"true\"/>\n   <field name=\"subscribers\" type=\"int\" indexed=\"true\" stored=\"true\"/>\n   <field name=\"site\" type=\"string\" indexed=\"true\" stored=\"true\"/>\n\n   <!-- catchall field, containing all other searchable text fields (implemented\n        via copyField further on in this schema  -->\n   <field name=\"text\" type=\"text\" indexed=\"true\" stored=\"false\" multiValued=\"true\"/>\n\n   <!-- catchall text field that indexes tokens both normally and in reverse for efficient\n        leading wildcard queries. -->\n   <field name=\"text_rev\" type=\"text_rev\" indexed=\"true\" stored=\"false\" multiValued=\"true\"/>\n\n   <!-- non-tokenized version of manufacturer to make it easier to sort or group\n        results by manufacturer.  copied from \"manu\" via copyField -->\n   <field name=\"manu_exact\" type=\"string\" indexed=\"true\" stored=\"false\"/>\n\n   <field name=\"payloads\" type=\"payloads\" indexed=\"true\" stored=\"true\"/>\n\n   <!-- Uncommenting the following will create a \"timestamp\" field using\n        a default value of \"NOW\" to indicate when each document was indexed.\n     -->\n   <!--\n   <field name=\"timestamp\" type=\"date\" indexed=\"true\" stored=\"true\" default=\"NOW\" multiValued=\"false\"/>\n     -->\n   \n\n   <!-- Dynamic field definitions.  If a field name is not found, dynamicFields\n        will be used if the name matches any of the patterns.\n        RESTRICTION: the glob-like pattern in the name attribute must have\n        a \"*\" only at the start or the end.\n        EXAMPLE:  name=\"*_i\" will match any field ending in _i (like myid_i, z_i)\n        Longer patterns will be matched first.  if equal size patterns\n        both match, the first appearing in the schema will be used.  -->\n   <dynamicField name=\"*_i\"  type=\"int\"    indexed=\"true\"  stored=\"true\"/>\n   <dynamicField name=\"*_s\"  type=\"string\"  indexed=\"true\"  stored=\"true\"/>\n   <dynamicField name=\"*_l\"  type=\"long\"   indexed=\"true\"  stored=\"true\"/>\n   <dynamicField name=\"*_t\"  type=\"text\"    indexed=\"true\"  stored=\"true\"/>\n   <dynamicField name=\"*_b\"  type=\"boolean\" indexed=\"true\"  stored=\"true\"/>\n   <dynamicField name=\"*_f\"  type=\"float\"  indexed=\"true\"  stored=\"true\"/>\n   <dynamicField name=\"*_d\"  type=\"double\" indexed=\"true\"  stored=\"true\"/>\n   <dynamicField name=\"*_dt\" type=\"date\"    indexed=\"true\"  stored=\"true\"/>\n\n   <!-- some trie-coded dynamic fields for faster range queries -->\n   <dynamicField name=\"*_ti\" type=\"tint\"    indexed=\"true\"  stored=\"true\"/>\n   <dynamicField name=\"*_tl\" type=\"tlong\"   indexed=\"true\"  stored=\"true\"/>\n   <dynamicField name=\"*_tf\" type=\"tfloat\"  indexed=\"true\"  stored=\"true\"/>\n   <dynamicField name=\"*_td\" type=\"tdouble\" indexed=\"true\"  stored=\"true\"/>\n   <dynamicField name=\"*_tdt\" type=\"tdate\"  indexed=\"true\"  stored=\"true\"/>\n\n   <dynamicField name=\"*_pi\"  type=\"pint\"    indexed=\"true\"  stored=\"true\"/>\n\n   <dynamicField name=\"ignored_*\" type=\"ignored\" multiValued=\"true\"/>\n   <dynamicField name=\"attr_*\" type=\"textgen\" indexed=\"true\" stored=\"true\" multiValued=\"true\"/>\n\n   <dynamicField name=\"random_*\" type=\"random\" />\n\n   <!-- uncomment the following to ignore any fields that don't already match an existing \n        field name or dynamic field, rather than reporting them as an error. \n        alternately, change the type=\"ignored\" to some other type e.g. \"text\" if you want \n        unknown fields indexed and/or stored by default --> \n   <!--dynamicField name=\"*\" type=\"ignored\" multiValued=\"true\" /-->\n   \n </fields>\n\n <!-- Field to use to determine and enforce document uniqueness. \n      Unless this field is marked with required=\"false\", it will be a required field\n   -->\n <uniqueKey>id</uniqueKey>\n\n <!-- field for the QueryParser to use when an explicit fieldname is absent -->\n <defaultSearchField>text</defaultSearchField>\n\n <!-- SolrQueryParser configuration: defaultOperator=\"AND|OR\" -->\n <solrQueryParser defaultOperator=\"OR\"/>\n\n  <!-- copyField commands copy one field to another at the time a document\n        is added to the index.  It's used either to index the same field differently,\n        or to add multiple fields to the same field for easier/faster searching.  -->\n\n   <copyField source=\"name\" dest=\"text\"/>\n   <copyField source=\"fullname\" dest=\"text\"/>\n   <copyField source=\"subreddit\" dest=\"text\"/>\n   <copyField source=\"reddit\" dest=\"text\"/>\n   <copyField source=\"author\" dest=\"text\"/>\n   <copyField source=\"author_fullname\" dest=\"text\"/>\n   <copyField source=\"flair\" dest=\"text\"/>\n   <copyField source=\"flair_text\" dest=\"text\"/>\n   <copyField source=\"url\" dest=\"text\"/>\n   <copyField source=\"description\" dest=\"text\"/>\n   <copyField source=\"sidebar\" dest=\"text\"/>\n   <copyField source=\"header_title\" dest=\"text\"/>\n   <copyField source=\"selftext\" dest=\"text\"/>\n\n\n   <!-- Text fields from SolrCell to search by default in our catch-all field -->\n   <copyField source=\"title\" dest=\"text\"/>\n\n\t\n   <!-- Above, multiple source fields are copied to the [text] field. \n\t  Another way to map multiple source fields to the same \n\t  destination field is to use the dynamic field syntax. \n\t  copyField also supports a maxChars to copy setting.  -->\n\t   \n   <!-- <copyField source=\"*_t\" dest=\"text\" maxChars=\"3000\"/> -->\n\n   <!-- copy name to alphaNameSort, a field designed for sorting by name -->\n   <!-- <copyField source=\"name\" dest=\"alphaNameSort\"/> -->\n \n\n <!-- Similarity is the scoring routine for each document vs. a query.\n      A custom similarity may be specified here, but the default is fine\n      for most applications.  -->\n <!-- <similarity class=\"org.apache.lucene.search.DefaultSimilarity\"/> -->\n <!-- ... OR ...\n      Specify a SimilarityFactory class name implementation\n      allowing parameters to be used.\n -->\n <!--\n <similarity class=\"com.example.solr.CustomSimilarityFactory\">\n   <str name=\"paramkey\">param value</str>\n </similarity>\n -->\n\n\n</schema>\n"
  },
  {
    "path": "solr/schema4.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n<!--\n Licensed to the Apache Software Foundation (ASF) under one or more\n contributor license agreements.  See the NOTICE file distributed with\n this work for additional information regarding copyright ownership.\n The ASF licenses this file to You under the Apache License, Version 2.0\n (the \"License\"); you may not use this file except in compliance with\n the License.  You may obtain a copy of the License at\n\n     http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n-->\n\n<!--  \n This is the Solr schema file. This file should be named \"schema.xml\" and\n should be in the conf directory under the solr home\n (i.e. ./solr/conf/schema.xml by default) \n or located where the classloader for the Solr webapp can find it.\n\n This example schema is the recommended starting point for users.\n It should be kept correct and concise, usable out-of-the-box.\n\n For more information, on how to customize this file, please see\n http://wiki.apache.org/solr/SchemaXml\n\n PERFORMANCE NOTE: this schema includes many optional features and should not\n be used for benchmarking.  To improve performance one could\n  - set stored=\"false\" for all fields possible (esp large fields) when you\n    only need to search on the field but don't need to return the original\n    value.\n  - set indexed=\"false\" if you don't need to search on the field, but only\n    return the field as a result of searching on other indexed fields.\n  - remove all unneeded copyField statements\n  - for best index size and searching performance, set \"index\" to false\n    for all general text fields, use copyField to copy them to the\n    catchall \"text\" field, and use that for searching.\n  - For maximum indexing performance, use the ConcurrentUpdateSolrServer\n    java client.\n  - Remember to run the JVM in server mode, and use a higher logging level\n    that avoids logging every request\n-->\n\n<schema name=\"reddit\" version=\"1.5\">\n  <!-- attribute \"name\" is the name of this schema and is only used for display purposes.\n       version=\"x.y\" is Solr's version number for the schema syntax and \n       semantics.  It should not normally be changed by applications.\n\n       1.0: multiValued attribute did not exist, all fields are multiValued \n            by nature\n       1.1: multiValued attribute introduced, false by default \n       1.2: omitTermFreqAndPositions attribute introduced, true by default \n            except for text fields.\n       1.3: removed optional field compress feature\n       1.4: autoGeneratePhraseQueries attribute introduced to drive QueryParser\n            behavior when a single string produces multiple tokens.  Defaults \n            to off for version >= 1.4\n       1.5: omitNorms defaults to true for primitive field types \n            (int, float, boolean, string...)\n     -->\n\n\n   <!-- Valid attributes for fields:\n     name: mandatory - the name for the field\n     type: mandatory - the name of a field type from the \n       <types> fieldType section\n     indexed: true if this field should be indexed (searchable or sortable)\n     stored: true if this field should be retrievable\n     docValues: true if this field should have doc values. Doc values are\n       useful for faceting, grouping, sorting and function queries. Although not\n       required, doc values will make the index faster to load, more\n       NRT-friendly and more memory-efficient. They however come with some\n       limitations: they are currently only supported by StrField, UUIDField\n       and all Trie*Fields, and depending on the field type, they might\n       require the field to be single-valued, be required or have a default\n       value (check the documentation of the field type you're interested in\n       for more information)\n     multiValued: true if this field may contain multiple values per document\n     omitNorms: (expert) set to true to omit the norms associated with\n       this field (this disables length normalization and index-time\n       boosting for the field, and saves some memory).  Only full-text\n       fields or fields that need an index-time boost need norms.\n       Norms are omitted for primitive (non-analyzed) types by default.\n     termVectors: [false] set to true to store the term vector for a\n       given field.\n       When using MoreLikeThis, fields used for similarity should be\n       stored for best performance.\n     termPositions: Store position information with the term vector.  \n       This will increase storage costs.\n     termOffsets: Store offset information with the term vector. This \n       will increase storage costs.\n     required: The field is required.  It will throw an error if the\n       value does not exist\n     default: a value that should be used if no value is specified\n       when adding a document.\n   -->\n\n   <!-- field names should consist of alphanumeric or underscore characters only and\n      not start with a digit.  This is not currently strictly enforced,\n      but other field names will not have first class support from all components\n      and back compatibility is not guaranteed.  Names with both leading and\n      trailing underscores (e.g. _version_) are reserved.\n   -->\n\n   <!-- If you remove this field, you must _also_ disable the update log in solrconfig.xml\n      or Solr won't start. _version_ and update log are required for SolrCloud\n   --> \n   <field name=\"_version_\" type=\"long\" indexed=\"true\" stored=\"true\"/>\n   \n   <!-- points to the root document of a block of nested documents. Required for nested\n      document support, may be removed otherwise\n   -->\n   <field name=\"_root_\" type=\"string\" indexed=\"true\" stored=\"false\"/>\n\n   <!-- Only remove the \"id\" field if you have a very good reason to. While not strictly\n     required, it is highly recommended. A <uniqueKey> is present in almost all Solr \n     installations. See the <uniqueKey> declaration below where <uniqueKey> is set to \"id\".\n   -->   \n   <field name=\"id\" type=\"string\" indexed=\"true\" stored=\"true\" required=\"true\" multiValued=\"false\" /> \n        \n   <field name=\"version\" type=\"int\" indexed=\"true\" stored=\"true\"/>\n   <field name=\"ups\" type=\"int\" indexed=\"true\" stored=\"true\"/>\n   <field name=\"downs\" type=\"int\" indexed=\"true\" stored=\"true\"/>\n   <field name=\"activity\" type=\"int\" indexed=\"true\" stored=\"true\"/>\n   <field name=\"num_comments\" type=\"int\" indexed=\"true\" stored=\"true\"/>\n   <field name=\"fullname\" type=\"text_general\" indexed=\"true\" stored=\"true\"/>\n   <field name=\"subreddit\" type=\"text_general\" indexed=\"true\" stored=\"true\"/>\n   <field name=\"reddit\" type=\"text_general\" indexed=\"true\" stored=\"true\"/>\n   <field name=\"title\" type=\"text_general\" indexed=\"true\" stored=\"true\"/>\n   <field name=\"sr_id\" type=\"int\" indexed=\"true\" stored=\"true\"/>\n   <field name=\"timestamp\" type=\"int\" indexed=\"true\" stored=\"true\"/>\n   <field name=\"over18\" type=\"boolean\" indexed=\"true\" stored=\"true\"/>\n   <field name=\"nsfw\" type=\"boolean\" indexed=\"true\" stored=\"true\"/>\n   <field name=\"self\" type=\"boolean\" indexed=\"true\" stored=\"true\"/>\n   <field name=\"selftext\" type=\"text_general\" indexed=\"true\" stored=\"true\"/>\n   <field name=\"is_self\" type=\"boolean\" indexed=\"true\" stored=\"true\"/>\n   <field name=\"author_fullname\" type=\"text_general\" indexed=\"true\" stored=\"true\"/>\n   <field name=\"author\" type=\"text_general\" indexed=\"true\" stored=\"true\"/>\n   <field name=\"type_id\" type=\"int\" indexed=\"true\" stored=\"true\"/>\n   <field name=\"flair\" type=\"text_general\" indexed=\"true\" stored=\"true\"/>\n   <field name=\"flair_css_class\" type=\"text_general\" indexed=\"true\" stored=\"true\"/>\n   <field name=\"flair_text\" type=\"text_general\" indexed=\"true\" stored=\"true\"/>\n   <field name=\"name\" type=\"text_general\" indexed=\"true\" stored=\"true\"/>\n   <field name=\"url\" type=\"text_general\" indexed=\"true\" stored=\"true\"/>\n   <field name=\"type\" type=\"text_general\" indexed=\"true\" stored=\"true\"/>\n   <field name=\"language\" type=\"text_general\" indexed=\"true\" stored=\"true\"/>\n   <field name=\"header_title\" type=\"text_general\" indexed=\"true\" stored=\"true\"/>\n   <field name=\"description\" type=\"text_general\" indexed=\"true\" stored=\"true\"/>\n   <field name=\"sidebar\" type=\"text_general\" indexed=\"true\" stored=\"true\"/>\n   <field name=\"link_type\" type=\"text_general\" indexed=\"true\" stored=\"true\"/>\n   <field name=\"actvity\" type=\"int\" indexed=\"true\" stored=\"true\"/>\n   <field name=\"subscribers\" type=\"int\" indexed=\"true\" stored=\"true\"/>\n   <field name=\"site\" type=\"text_general\" indexed=\"true\" stored=\"true\"/>\n\n\n   <!-- catchall field, containing all other searchable text fields (implemented\n        via copyField further on in this schema  -->\n   <field name=\"text\" type=\"text_general\" indexed=\"true\" stored=\"false\" multiValued=\"true\"/>\n\n   <!-- catchall text field that indexes tokens both normally and in reverse for efficient\n        leading wildcard queries. -->\n   <field name=\"text_rev\" type=\"text_general_rev\" indexed=\"true\" stored=\"false\" multiValued=\"true\"/>\n\n   <!-- non-tokenized version of manufacturer to make it easier to sort or group\n        results by manufacturer.  copied from \"manu\" via copyField -->\n   <field name=\"manu_exact\" type=\"string\" indexed=\"true\" stored=\"false\"/>\n\n   <field name=\"payloads\" type=\"payloads\" indexed=\"true\" stored=\"true\"/>\n\n\n   <!--\n     Some fields such as popularity and manu_exact could be modified to\n     leverage doc values:\n     <field name=\"popularity\" type=\"int\" indexed=\"true\" stored=\"true\" docValues=\"true\" />\n     <field name=\"manu_exact\" type=\"string\" indexed=\"false\" stored=\"false\" docValues=\"true\" />\n     <field name=\"cat\" type=\"string\" indexed=\"true\" stored=\"true\" docValues=\"true\" multiValued=\"true\"/>\n\n\n     Although it would make indexing slightly slower and the index bigger, it\n     would also make the index faster to load, more memory-efficient and more\n     NRT-friendly.\n     -->\n\n   <!-- Dynamic field definitions allow using convention over configuration\n       for fields via the specification of patterns to match field names. \n       EXAMPLE:  name=\"*_i\" will match any field ending in _i (like myid_i, z_i)\n       RESTRICTION: the glob-like pattern in the name attribute must have\n       a \"*\" only at the start or the end.  -->\n   \n   <dynamicField name=\"*_i\"  type=\"int\"    indexed=\"true\"  stored=\"true\"/>\n   <dynamicField name=\"*_is\" type=\"int\"    indexed=\"true\"  stored=\"true\"  multiValued=\"true\"/>\n   <dynamicField name=\"*_s\"  type=\"string\"  indexed=\"true\"  stored=\"true\" />\n   <dynamicField name=\"*_ss\" type=\"string\"  indexed=\"true\"  stored=\"true\" multiValued=\"true\"/>\n   <dynamicField name=\"*_l\"  type=\"long\"   indexed=\"true\"  stored=\"true\"/>\n   <dynamicField name=\"*_ls\" type=\"long\"   indexed=\"true\"  stored=\"true\"  multiValued=\"true\"/>\n   <dynamicField name=\"*_t\"  type=\"text_general\"    indexed=\"true\"  stored=\"true\"/>\n   <dynamicField name=\"*_txt\" type=\"text_general\"   indexed=\"true\"  stored=\"true\" multiValued=\"true\"/>\n   <dynamicField name=\"*_en\"  type=\"text_en\"    indexed=\"true\"  stored=\"true\" multiValued=\"true\"/>\n   <dynamicField name=\"*_b\"  type=\"boolean\" indexed=\"true\" stored=\"true\"/>\n   <dynamicField name=\"*_bs\" type=\"boolean\" indexed=\"true\" stored=\"true\"  multiValued=\"true\"/>\n   <dynamicField name=\"*_f\"  type=\"float\"  indexed=\"true\"  stored=\"true\"/>\n   <dynamicField name=\"*_fs\" type=\"float\"  indexed=\"true\"  stored=\"true\"  multiValued=\"true\"/>\n   <dynamicField name=\"*_d\"  type=\"double\" indexed=\"true\"  stored=\"true\"/>\n   <dynamicField name=\"*_ds\" type=\"double\" indexed=\"true\"  stored=\"true\"  multiValued=\"true\"/>\n\n   <!-- Type used to index the lat and lon components for the \"location\" FieldType -->\n   <dynamicField name=\"*_coordinate\"  type=\"tdouble\" indexed=\"true\"  stored=\"false\" />\n\n   <dynamicField name=\"*_dt\"  type=\"date\"    indexed=\"true\"  stored=\"true\"/>\n   <dynamicField name=\"*_dts\" type=\"date\"    indexed=\"true\"  stored=\"true\" multiValued=\"true\"/>\n   <dynamicField name=\"*_p\"  type=\"location\" indexed=\"true\" stored=\"true\"/>\n\n   <!-- some trie-coded dynamic fields for faster range queries -->\n   <dynamicField name=\"*_ti\" type=\"tint\"    indexed=\"true\"  stored=\"true\"/>\n   <dynamicField name=\"*_tl\" type=\"tlong\"   indexed=\"true\"  stored=\"true\"/>\n   <dynamicField name=\"*_tf\" type=\"tfloat\"  indexed=\"true\"  stored=\"true\"/>\n   <dynamicField name=\"*_td\" type=\"tdouble\" indexed=\"true\"  stored=\"true\"/>\n   <dynamicField name=\"*_tdt\" type=\"tdate\"  indexed=\"true\"  stored=\"true\"/>\n\n   <dynamicField name=\"*_c\"   type=\"currency\" indexed=\"true\"  stored=\"true\"/>\n\n   <dynamicField name=\"ignored_*\" type=\"ignored\" multiValued=\"true\"/>\n   <dynamicField name=\"attr_*\" type=\"text_general\" indexed=\"true\" stored=\"true\" multiValued=\"true\"/>\n\n   <dynamicField name=\"random_*\" type=\"random\" />\n\n   <!-- uncomment the following to ignore any fields that don't already match an existing \n        field name or dynamic field, rather than reporting them as an error. \n        alternately, change the type=\"ignored\" to some other type e.g. \"text\" if you want \n        unknown fields indexed and/or stored by default --> \n   <!--dynamicField name=\"*\" type=\"ignored\" multiValued=\"true\" /-->\n   \n\n\n\n <!-- Field to use to determine and enforce document uniqueness. \n      Unless this field is marked with required=\"false\", it will be a required field\n   -->\n <uniqueKey>id</uniqueKey>\n\n <!-- DEPRECATED: The defaultSearchField is consulted by various query parsers when\n  parsing a query string that isn't explicit about the field.  Machine (non-user)\n  generated queries are best made explicit, or they can use the \"df\" request parameter\n  which takes precedence over this.\n  Note: Un-commenting defaultSearchField will be insufficient if your request handler\n  in solrconfig.xml defines \"df\", which takes precedence. That would need to be removed.\n <defaultSearchField>text</defaultSearchField> -->\n\n <!-- DEPRECATED: The defaultOperator (AND|OR) is consulted by various query parsers\n  when parsing a query string to determine if a clause of the query should be marked as\n  required or optional, assuming the clause isn't already marked by some operator.\n  The default is OR, which is generally assumed so it is not a good idea to change it\n  globally here.  The \"q.op\" request parameter takes precedence over this.\n <solrQueryParser defaultOperator=\"OR\"/> -->\n\n  <!-- copyField commands copy one field to another at the time a document\n        is added to the index.  It's used either to index the same field differently,\n        or to add multiple fields to the same field for easier/faster searching.  -->\n\n   <copyField source=\"name\" dest=\"text\"/>\n   <copyField source=\"fullname\" dest=\"text\"/>\n   <copyField source=\"subreddit\" dest=\"text\"/>\n   <copyField source=\"reddit\" dest=\"text\"/>\n   <copyField source=\"author\" dest=\"text\"/>\n   <copyField source=\"author_fullname\" dest=\"text\"/>\n   <copyField source=\"flair\" dest=\"text\"/>\n   <copyField source=\"flair_text\" dest=\"text\"/>\n   <copyField source=\"url\" dest=\"text\"/>\n   <copyField source=\"description\" dest=\"text\"/>\n   <copyField source=\"sidebar\" dest=\"text\"/>\n   <copyField source=\"header_title\" dest=\"text\"/>\n   <copyField source=\"selftext\" dest=\"text\"/>\n\n\n   <!-- Text fields from SolrCell to search by default in our catch-all field -->\n   <copyField source=\"title\" dest=\"text\"/>\n\n\t\n   <!-- Above, multiple source fields are copied to the [text] field. \n\t  Another way to map multiple source fields to the same \n\t  destination field is to use the dynamic field syntax. \n\t  copyField also supports a maxChars to copy setting.  -->\n\t   \n   <!-- <copyField source=\"*_t\" dest=\"text\" maxChars=\"3000\"/> -->\n\n   <!-- copy name to alphaNameSort, a field designed for sorting by name -->\n   <!-- <copyField source=\"name\" dest=\"alphaNameSort\"/> -->\n \n  \n    <!-- field type definitions. The \"name\" attribute is\n       just a label to be used by field definitions.  The \"class\"\n       attribute and any other attributes determine the real\n       behavior of the fieldType.\n         Class names starting with \"solr\" refer to java classes in a\n       standard package such as org.apache.solr.analysis\n    -->\n\n    <!-- The StrField type is not analyzed, but indexed/stored verbatim.\n       It supports doc values but in that case the field needs to be\n       single-valued and either required or have a default value.\n      -->\n    <fieldType name=\"string\" class=\"solr.StrField\" sortMissingLast=\"true\" />\n\n    <!-- boolean type: \"true\" or \"false\" -->\n    <fieldType name=\"boolean\" class=\"solr.BoolField\" sortMissingLast=\"true\"/>\n\n    <!-- sortMissingLast and sortMissingFirst attributes are optional attributes are\n         currently supported on types that are sorted internally as strings\n         and on numeric types.\n\t     This includes \"string\",\"boolean\", and, as of 3.5 (and 4.x),\n\t     int, float, long, date, double, including the \"Trie\" variants.\n       - If sortMissingLast=\"true\", then a sort on this field will cause documents\n         without the field to come after documents with the field,\n         regardless of the requested sort order (asc or desc).\n       - If sortMissingFirst=\"true\", then a sort on this field will cause documents\n         without the field to come before documents with the field,\n         regardless of the requested sort order.\n       - If sortMissingLast=\"false\" and sortMissingFirst=\"false\" (the default),\n         then default lucene sorting will be used which places docs without the\n         field first in an ascending sort and last in a descending sort.\n    -->    \n\n    <!--\n      Default numeric field types. For faster range queries, consider the tint/tfloat/tlong/tdouble types.\n\n      These fields support doc values, but they require the field to be\n      single-valued and either be required or have a default value.\n    -->\n    <fieldType name=\"int\" class=\"solr.TrieIntField\" precisionStep=\"0\" positionIncrementGap=\"0\"/>\n    <fieldType name=\"float\" class=\"solr.TrieFloatField\" precisionStep=\"0\" positionIncrementGap=\"0\"/>\n    <fieldType name=\"long\" class=\"solr.TrieLongField\" precisionStep=\"0\" positionIncrementGap=\"0\"/>\n    <fieldType name=\"double\" class=\"solr.TrieDoubleField\" precisionStep=\"0\" positionIncrementGap=\"0\"/>\n\n    <!--\n     Numeric field types that index each value at various levels of precision\n     to accelerate range queries when the number of values between the range\n     endpoints is large. See the javadoc for NumericRangeQuery for internal\n     implementation details.\n\n     Smaller precisionStep values (specified in bits) will lead to more tokens\n     indexed per value, slightly larger index size, and faster range queries.\n     A precisionStep of 0 disables indexing at different precision levels.\n    -->\n    <fieldType name=\"tint\" class=\"solr.TrieIntField\" precisionStep=\"8\" positionIncrementGap=\"0\"/>\n    <fieldType name=\"tfloat\" class=\"solr.TrieFloatField\" precisionStep=\"8\" positionIncrementGap=\"0\"/>\n    <fieldType name=\"tlong\" class=\"solr.TrieLongField\" precisionStep=\"8\" positionIncrementGap=\"0\"/>\n    <fieldType name=\"tdouble\" class=\"solr.TrieDoubleField\" precisionStep=\"8\" positionIncrementGap=\"0\"/>\n\n    <!-- The format for this date field is of the form 1995-12-31T23:59:59Z, and\n         is a more restricted form of the canonical representation of dateTime\n         http://www.w3.org/TR/xmlschema-2/#dateTime    \n         The trailing \"Z\" designates UTC time and is mandatory.\n         Optional fractional seconds are allowed: 1995-12-31T23:59:59.999Z\n         All other components are mandatory.\n\n         Expressions can also be used to denote calculations that should be\n         performed relative to \"NOW\" to determine the value, ie...\n\n               NOW/HOUR\n                  ... Round to the start of the current hour\n               NOW-1DAY\n                  ... Exactly 1 day prior to now\n               NOW/DAY+6MONTHS+3DAYS\n                  ... 6 months and 3 days in the future from the start of\n                      the current day\n                      \n         Consult the DateField javadocs for more information.\n\n         Note: For faster range queries, consider the tdate type\n      -->\n    <fieldType name=\"date\" class=\"solr.TrieDateField\" precisionStep=\"0\" positionIncrementGap=\"0\"/>\n\n    <!-- A Trie based date field for faster date range queries and date faceting. -->\n    <fieldType name=\"tdate\" class=\"solr.TrieDateField\" precisionStep=\"6\" positionIncrementGap=\"0\"/>\n\n\n    <!--Binary data type. The data should be sent/retrieved in as Base64 encoded Strings -->\n    <fieldtype name=\"binary\" class=\"solr.BinaryField\"/>\n\n    <!--\n      Note:\n      These should only be used for compatibility with existing indexes (created with lucene or older Solr versions).\n      Use Trie based fields instead. As of Solr 3.5 and 4.x, Trie based fields support sortMissingFirst/Last\n\n      Plain numeric field types that store and index the text\n      value verbatim (and hence don't correctly support range queries, since the\n      lexicographic ordering isn't equal to the numeric ordering)\n\n      NOTE: These field types are deprecated will be completely removed in Solr 5.0!\n    -->\n    <!--\n    <fieldType name=\"pint\" class=\"solr.IntField\"/>\n    <fieldType name=\"plong\" class=\"solr.LongField\"/>\n    <fieldType name=\"pfloat\" class=\"solr.FloatField\"/>\n    <fieldType name=\"pdouble\" class=\"solr.DoubleField\"/>\n    <fieldType name=\"pdate\" class=\"solr.DateField\" sortMissingLast=\"true\"/>\n    -->\n\n    <!-- The \"RandomSortField\" is not used to store or search any\n         data.  You can declare fields of this type it in your schema\n         to generate pseudo-random orderings of your docs for sorting \n         or function purposes.  The ordering is generated based on the field\n         name and the version of the index. As long as the index version\n         remains unchanged, and the same field name is reused,\n         the ordering of the docs will be consistent.  \n         If you want different psuedo-random orderings of documents,\n         for the same version of the index, use a dynamicField and\n         change the field name in the request.\n     -->\n    <fieldType name=\"random\" class=\"solr.RandomSortField\" indexed=\"true\" />\n\n    <!-- solr.TextField allows the specification of custom text analyzers\n         specified as a tokenizer and a list of token filters. Different\n         analyzers may be specified for indexing and querying.\n\n         The optional positionIncrementGap puts space between multiple fields of\n         this type on the same document, with the purpose of preventing false phrase\n         matching across fields.\n\n         For more info on customizing your analyzer chain, please see\n         http://wiki.apache.org/solr/AnalyzersTokenizersTokenFilters\n     -->\n\n    <!-- One can also specify an existing Analyzer class that has a\n         default constructor via the class attribute on the analyzer element.\n         Example:\n    <fieldType name=\"text_greek\" class=\"solr.TextField\">\n      <analyzer class=\"org.apache.lucene.analysis.el.GreekAnalyzer\"/>\n    </fieldType>\n    -->\n\n    <!-- A text field that only splits on whitespace for exact matching of words -->\n    <fieldType name=\"text_ws\" class=\"solr.TextField\" positionIncrementGap=\"100\">\n      <analyzer>\n        <tokenizer class=\"solr.WhitespaceTokenizerFactory\"/>\n      </analyzer>\n    </fieldType>\n\n    <!-- A text type for English text where stopwords and synonyms are managed using the REST API -->\n    <fieldType name=\"managed_en\" class=\"solr.TextField\" positionIncrementGap=\"100\">\n      <analyzer>\n        <tokenizer class=\"solr.StandardTokenizerFactory\"/>\n        <filter class=\"solr.ManagedStopFilterFactory\" managed=\"english\" />\n        <filter class=\"solr.ManagedSynonymFilterFactory\" managed=\"english\" />\n      </analyzer>\n    </fieldType>\n\n    <!-- A general text field that has reasonable, generic\n         cross-language defaults: it tokenizes with StandardTokenizer,\n\t removes stop words from case-insensitive \"stopwords.txt\"\n\t (empty by default), and down cases.  At query time only, it\n\t also applies synonyms. -->\n    <fieldType name=\"text_general\" class=\"solr.TextField\" positionIncrementGap=\"100\">\n      <analyzer type=\"index\">\n        <tokenizer class=\"solr.StandardTokenizerFactory\"/>\n        <filter class=\"solr.StopFilterFactory\" ignoreCase=\"true\" words=\"stopwords.txt\" />\n        <!-- in this example, we will only use synonyms at query time\n        <filter class=\"solr.SynonymFilterFactory\" synonyms=\"index_synonyms.txt\" ignoreCase=\"true\" expand=\"false\"/>\n        -->\n        <filter class=\"solr.LowerCaseFilterFactory\"/>\n      </analyzer>\n      <analyzer type=\"query\">\n        <tokenizer class=\"solr.StandardTokenizerFactory\"/>\n        <filter class=\"solr.StopFilterFactory\" ignoreCase=\"true\" words=\"stopwords.txt\" />\n        <filter class=\"solr.SynonymFilterFactory\" synonyms=\"synonyms.txt\" ignoreCase=\"true\" expand=\"true\"/>\n        <filter class=\"solr.LowerCaseFilterFactory\"/>\n      </analyzer>\n    </fieldType>\n\n    <!-- A text field with defaults appropriate for English: it\n         tokenizes with StandardTokenizer, removes English stop words\n         (lang/stopwords_en.txt), down cases, protects words from protwords.txt, and\n         finally applies Porter's stemming.  The query time analyzer\n         also applies synonyms from synonyms.txt. -->\n    <fieldType name=\"text_en\" class=\"solr.TextField\" positionIncrementGap=\"100\">\n      <analyzer type=\"index\">\n        <tokenizer class=\"solr.StandardTokenizerFactory\"/>\n        <!-- in this example, we will only use synonyms at query time\n        <filter class=\"solr.SynonymFilterFactory\" synonyms=\"index_synonyms.txt\" ignoreCase=\"true\" expand=\"false\"/>\n        -->\n        <!-- Case insensitive stop word removal.\n        -->\n        <filter class=\"solr.StopFilterFactory\"\n                ignoreCase=\"true\"\n                words=\"lang/stopwords_en.txt\"\n                />\n        <filter class=\"solr.LowerCaseFilterFactory\"/>\n\t<filter class=\"solr.EnglishPossessiveFilterFactory\"/>\n        <filter class=\"solr.KeywordMarkerFilterFactory\" protected=\"protwords.txt\"/>\n\t<!-- Optionally you may want to use this less aggressive stemmer instead of PorterStemFilterFactory:\n        <filter class=\"solr.EnglishMinimalStemFilterFactory\"/>\n\t-->\n        <filter class=\"solr.PorterStemFilterFactory\"/>\n      </analyzer>\n      <analyzer type=\"query\">\n        <tokenizer class=\"solr.StandardTokenizerFactory\"/>\n        <filter class=\"solr.SynonymFilterFactory\" synonyms=\"synonyms.txt\" ignoreCase=\"true\" expand=\"true\"/>\n        <filter class=\"solr.StopFilterFactory\"\n                ignoreCase=\"true\"\n                words=\"lang/stopwords_en.txt\"\n                />\n        <filter class=\"solr.LowerCaseFilterFactory\"/>\n\t<filter class=\"solr.EnglishPossessiveFilterFactory\"/>\n        <filter class=\"solr.KeywordMarkerFilterFactory\" protected=\"protwords.txt\"/>\n\t<!-- Optionally you may want to use this less aggressive stemmer instead of PorterStemFilterFactory:\n        <filter class=\"solr.EnglishMinimalStemFilterFactory\"/>\n\t-->\n        <filter class=\"solr.PorterStemFilterFactory\"/>\n      </analyzer>\n    </fieldType>\n\n    <!-- A text field with defaults appropriate for English, plus\n\t aggressive word-splitting and autophrase features enabled.\n\t This field is just like text_en, except it adds\n\t WordDelimiterFilter to enable splitting and matching of\n\t words on case-change, alpha numeric boundaries, and\n\t non-alphanumeric chars.  This means certain compound word\n\t cases will work, for example query \"wi fi\" will match\n\t document \"WiFi\" or \"wi-fi\".\n        -->\n    <fieldType name=\"text_en_splitting\" class=\"solr.TextField\" positionIncrementGap=\"100\" autoGeneratePhraseQueries=\"true\">\n      <analyzer type=\"index\">\n        <tokenizer class=\"solr.WhitespaceTokenizerFactory\"/>\n        <!-- in this example, we will only use synonyms at query time\n        <filter class=\"solr.SynonymFilterFactory\" synonyms=\"index_synonyms.txt\" ignoreCase=\"true\" expand=\"false\"/>\n        -->\n        <!-- Case insensitive stop word removal.\n        -->\n        <filter class=\"solr.StopFilterFactory\"\n                ignoreCase=\"true\"\n                words=\"lang/stopwords_en.txt\"\n                />\n        <filter class=\"solr.WordDelimiterFilterFactory\" generateWordParts=\"1\" generateNumberParts=\"1\" catenateWords=\"1\" catenateNumbers=\"1\" catenateAll=\"0\" splitOnCaseChange=\"1\"/>\n        <filter class=\"solr.LowerCaseFilterFactory\"/>\n        <filter class=\"solr.KeywordMarkerFilterFactory\" protected=\"protwords.txt\"/>\n        <filter class=\"solr.PorterStemFilterFactory\"/>\n      </analyzer>\n      <analyzer type=\"query\">\n        <tokenizer class=\"solr.WhitespaceTokenizerFactory\"/>\n        <filter class=\"solr.SynonymFilterFactory\" synonyms=\"synonyms.txt\" ignoreCase=\"true\" expand=\"true\"/>\n        <filter class=\"solr.StopFilterFactory\"\n                ignoreCase=\"true\"\n                words=\"lang/stopwords_en.txt\"\n                />\n        <filter class=\"solr.WordDelimiterFilterFactory\" generateWordParts=\"1\" generateNumberParts=\"1\" catenateWords=\"0\" catenateNumbers=\"0\" catenateAll=\"0\" splitOnCaseChange=\"1\"/>\n        <filter class=\"solr.LowerCaseFilterFactory\"/>\n        <filter class=\"solr.KeywordMarkerFilterFactory\" protected=\"protwords.txt\"/>\n        <filter class=\"solr.PorterStemFilterFactory\"/>\n      </analyzer>\n    </fieldType>\n\n    <!-- Less flexible matching, but less false matches.  Probably not ideal for product names,\n         but may be good for SKUs.  Can insert dashes in the wrong place and still match. -->\n    <fieldType name=\"text_en_splitting_tight\" class=\"solr.TextField\" positionIncrementGap=\"100\" autoGeneratePhraseQueries=\"true\">\n      <analyzer>\n        <tokenizer class=\"solr.WhitespaceTokenizerFactory\"/>\n        <filter class=\"solr.SynonymFilterFactory\" synonyms=\"synonyms.txt\" ignoreCase=\"true\" expand=\"false\"/>\n        <filter class=\"solr.StopFilterFactory\" ignoreCase=\"true\" words=\"lang/stopwords_en.txt\"/>\n        <filter class=\"solr.WordDelimiterFilterFactory\" generateWordParts=\"0\" generateNumberParts=\"0\" catenateWords=\"1\" catenateNumbers=\"1\" catenateAll=\"0\"/>\n        <filter class=\"solr.LowerCaseFilterFactory\"/>\n        <filter class=\"solr.KeywordMarkerFilterFactory\" protected=\"protwords.txt\"/>\n        <filter class=\"solr.EnglishMinimalStemFilterFactory\"/>\n        <!-- this filter can remove any duplicate tokens that appear at the same position - sometimes\n             possible with WordDelimiterFilter in conjuncton with stemming. -->\n        <filter class=\"solr.RemoveDuplicatesTokenFilterFactory\"/>\n      </analyzer>\n    </fieldType>\n\n    <!-- Just like text_general except it reverses the characters of\n\t each token, to enable more efficient leading wildcard queries. -->\n    <fieldType name=\"text_general_rev\" class=\"solr.TextField\" positionIncrementGap=\"100\">\n      <analyzer type=\"index\">\n        <tokenizer class=\"solr.StandardTokenizerFactory\"/>\n        <filter class=\"solr.StopFilterFactory\" ignoreCase=\"true\" words=\"stopwords.txt\" />\n        <filter class=\"solr.LowerCaseFilterFactory\"/>\n        <filter class=\"solr.ReversedWildcardFilterFactory\" withOriginal=\"true\"\n           maxPosAsterisk=\"3\" maxPosQuestion=\"2\" maxFractionAsterisk=\"0.33\"/>\n      </analyzer>\n      <analyzer type=\"query\">\n        <tokenizer class=\"solr.StandardTokenizerFactory\"/>\n        <filter class=\"solr.SynonymFilterFactory\" synonyms=\"synonyms.txt\" ignoreCase=\"true\" expand=\"true\"/>\n        <filter class=\"solr.StopFilterFactory\" ignoreCase=\"true\" words=\"stopwords.txt\" />\n        <filter class=\"solr.LowerCaseFilterFactory\"/>\n      </analyzer>\n    </fieldType>\n\n    <!-- charFilter + WhitespaceTokenizer  -->\n    <!--\n    <fieldType name=\"text_char_norm\" class=\"solr.TextField\" positionIncrementGap=\"100\" >\n      <analyzer>\n        <charFilter class=\"solr.MappingCharFilterFactory\" mapping=\"mapping-ISOLatin1Accent.txt\"/>\n        <tokenizer class=\"solr.WhitespaceTokenizerFactory\"/>\n      </analyzer>\n    </fieldType>\n    -->\n\n    <!-- This is an example of using the KeywordTokenizer along\n         With various TokenFilterFactories to produce a sortable field\n         that does not include some properties of the source text\n      -->\n    <fieldType name=\"alphaOnlySort\" class=\"solr.TextField\" sortMissingLast=\"true\" omitNorms=\"true\">\n      <analyzer>\n        <!-- KeywordTokenizer does no actual tokenizing, so the entire\n             input string is preserved as a single token\n          -->\n        <tokenizer class=\"solr.KeywordTokenizerFactory\"/>\n        <!-- The LowerCase TokenFilter does what you expect, which can be\n             when you want your sorting to be case insensitive\n          -->\n        <filter class=\"solr.LowerCaseFilterFactory\" />\n        <!-- The TrimFilter removes any leading or trailing whitespace -->\n        <filter class=\"solr.TrimFilterFactory\" />\n        <!-- The PatternReplaceFilter gives you the flexibility to use\n             Java Regular expression to replace any sequence of characters\n             matching a pattern with an arbitrary replacement string, \n             which may include back references to portions of the original\n             string matched by the pattern.\n             \n             See the Java Regular Expression documentation for more\n             information on pattern and replacement string syntax.\n             \n             http://docs.oracle.com/javase/7/docs/api/java/util/regex/package-summary.html\n          -->\n        <filter class=\"solr.PatternReplaceFilterFactory\"\n                pattern=\"([^a-z])\" replacement=\"\" replace=\"all\"\n        />\n      </analyzer>\n    </fieldType>\n    \n    <fieldtype name=\"phonetic\" stored=\"false\" indexed=\"true\" class=\"solr.TextField\" >\n      <analyzer>\n        <tokenizer class=\"solr.StandardTokenizerFactory\"/>\n        <filter class=\"solr.DoubleMetaphoneFilterFactory\" inject=\"false\"/>\n      </analyzer>\n    </fieldtype>\n\n    <fieldtype name=\"payloads\" stored=\"false\" indexed=\"true\" class=\"solr.TextField\" >\n      <analyzer>\n        <tokenizer class=\"solr.WhitespaceTokenizerFactory\"/>\n        <!--\n        The DelimitedPayloadTokenFilter can put payloads on tokens... for example,\n        a token of \"foo|1.4\"  would be indexed as \"foo\" with a payload of 1.4f\n        Attributes of the DelimitedPayloadTokenFilterFactory : \n         \"delimiter\" - a one character delimiter. Default is | (pipe)\n\t \"encoder\" - how to encode the following value into a playload\n\t    float -> org.apache.lucene.analysis.payloads.FloatEncoder,\n\t    integer -> o.a.l.a.p.IntegerEncoder\n\t    identity -> o.a.l.a.p.IdentityEncoder\n            Fully Qualified class name implementing PayloadEncoder, Encoder must have a no arg constructor.\n         -->\n        <filter class=\"solr.DelimitedPayloadTokenFilterFactory\" encoder=\"float\"/>\n      </analyzer>\n    </fieldtype>\n\n    <!-- lowercases the entire field value, keeping it as a single token.  -->\n    <fieldType name=\"lowercase\" class=\"solr.TextField\" positionIncrementGap=\"100\">\n      <analyzer>\n        <tokenizer class=\"solr.KeywordTokenizerFactory\"/>\n        <filter class=\"solr.LowerCaseFilterFactory\" />\n      </analyzer>\n    </fieldType>\n\n    <!-- \n      Example of using PathHierarchyTokenizerFactory at index time, so\n      queries for paths match documents at that path, or in descendent paths\n    -->\n    <fieldType name=\"descendent_path\" class=\"solr.TextField\">\n      <analyzer type=\"index\">\n\t<tokenizer class=\"solr.PathHierarchyTokenizerFactory\" delimiter=\"/\" />\n      </analyzer>\n      <analyzer type=\"query\">\n\t<tokenizer class=\"solr.KeywordTokenizerFactory\" />\n      </analyzer>\n    </fieldType>\n    <!-- \n      Example of using PathHierarchyTokenizerFactory at query time, so\n      queries for paths match documents at that path, or in ancestor paths\n    -->\n    <fieldType name=\"ancestor_path\" class=\"solr.TextField\">\n      <analyzer type=\"index\">\n\t<tokenizer class=\"solr.KeywordTokenizerFactory\" />\n      </analyzer>\n      <analyzer type=\"query\">\n\t<tokenizer class=\"solr.PathHierarchyTokenizerFactory\" delimiter=\"/\" />\n      </analyzer>\n    </fieldType>\n\n    <!-- since fields of this type are by default not stored or indexed,\n         any data added to them will be ignored outright.  --> \n    <fieldtype name=\"ignored\" stored=\"false\" indexed=\"false\" multiValued=\"true\" class=\"solr.StrField\" />\n\n    <!-- This point type indexes the coordinates as separate fields (subFields)\n      If subFieldType is defined, it references a type, and a dynamic field\n      definition is created matching *___<typename>.  Alternately, if \n      subFieldSuffix is defined, that is used to create the subFields.\n      Example: if subFieldType=\"double\", then the coordinates would be\n        indexed in fields myloc_0___double,myloc_1___double.\n      Example: if subFieldSuffix=\"_d\" then the coordinates would be indexed\n        in fields myloc_0_d,myloc_1_d\n      The subFields are an implementation detail of the fieldType, and end\n      users normally should not need to know about them.\n     -->\n    <fieldType name=\"point\" class=\"solr.PointType\" dimension=\"2\" subFieldSuffix=\"_d\"/>\n\n    <!-- A specialized field for geospatial search. If indexed, this fieldType must not be multivalued. -->\n    <fieldType name=\"location\" class=\"solr.LatLonType\" subFieldSuffix=\"_coordinate\"/>\n\n    <!-- An alternative geospatial field type new to Solr 4.  It supports multiValued and polygon shapes.\n      For more information about this and other Spatial fields new to Solr 4, see:\n      http://wiki.apache.org/solr/SolrAdaptersForLuceneSpatial4\n    -->\n    <fieldType name=\"location_rpt\" class=\"solr.SpatialRecursivePrefixTreeFieldType\"\n        geo=\"true\" distErrPct=\"0.025\" maxDistErr=\"0.000009\" units=\"degrees\" />\n\n    <!-- Spatial rectangle (bounding box) field. It supports most spatial predicates, and has\n     special relevancy modes: score=overlapRatio|area|area2D (local-param to the query).  DocValues is required for\n     relevancy. -->\n    <fieldType name=\"bbox\" class=\"solr.BBoxField\"\n        geo=\"true\" units=\"degrees\" numberType=\"_bbox_coord\" />\n    <fieldType name=\"_bbox_coord\" class=\"solr.TrieDoubleField\" precisionStep=\"8\" docValues=\"true\" stored=\"false\"/>\n\n   <!-- Money/currency field type. See http://wiki.apache.org/solr/MoneyFieldType\n        Parameters:\n          defaultCurrency: Specifies the default currency if none specified. Defaults to \"USD\"\n          precisionStep:   Specifies the precisionStep for the TrieLong field used for the amount\n          providerClass:   Lets you plug in other exchange provider backend:\n                           solr.FileExchangeRateProvider is the default and takes one parameter:\n                             currencyConfig: name of an xml file holding exchange rates\n                           solr.OpenExchangeRatesOrgProvider uses rates from openexchangerates.org:\n                             ratesFileLocation: URL or path to rates JSON file (default latest.json on the web)\n                             refreshInterval: Number of minutes between each rates fetch (default: 1440, min: 60)\n   -->\n    <fieldType name=\"currency\" class=\"solr.CurrencyField\" precisionStep=\"8\" defaultCurrency=\"USD\" currencyConfig=\"currency.xml\" />\n             \n\n\n   <!-- some examples for different languages (generally ordered by ISO code) -->\n\n    <!-- Arabic -->\n    <fieldType name=\"text_ar\" class=\"solr.TextField\" positionIncrementGap=\"100\">\n      <analyzer> \n        <tokenizer class=\"solr.StandardTokenizerFactory\"/>\n        <!-- for any non-arabic -->\n        <filter class=\"solr.LowerCaseFilterFactory\"/>\n        <filter class=\"solr.StopFilterFactory\" ignoreCase=\"true\" words=\"lang/stopwords_ar.txt\" />\n        <!-- normalizes ﻯ to ﻱ, etc -->\n        <filter class=\"solr.ArabicNormalizationFilterFactory\"/>\n        <filter class=\"solr.ArabicStemFilterFactory\"/>\n      </analyzer>\n    </fieldType>\n\n    <!-- Bulgarian -->\n    <fieldType name=\"text_bg\" class=\"solr.TextField\" positionIncrementGap=\"100\">\n      <analyzer> \n        <tokenizer class=\"solr.StandardTokenizerFactory\"/> \n        <filter class=\"solr.LowerCaseFilterFactory\"/>\n        <filter class=\"solr.StopFilterFactory\" ignoreCase=\"true\" words=\"lang/stopwords_bg.txt\" /> \n        <filter class=\"solr.BulgarianStemFilterFactory\"/>       \n      </analyzer>\n    </fieldType>\n    \n    <!-- Catalan -->\n    <fieldType name=\"text_ca\" class=\"solr.TextField\" positionIncrementGap=\"100\">\n      <analyzer> \n        <tokenizer class=\"solr.StandardTokenizerFactory\"/>\n        <!-- removes l', etc -->\n        <filter class=\"solr.ElisionFilterFactory\" ignoreCase=\"true\" articles=\"lang/contractions_ca.txt\"/>\n        <filter class=\"solr.LowerCaseFilterFactory\"/>\n        <filter class=\"solr.StopFilterFactory\" ignoreCase=\"true\" words=\"lang/stopwords_ca.txt\" />\n        <filter class=\"solr.SnowballPorterFilterFactory\" language=\"Catalan\"/>       \n      </analyzer>\n    </fieldType>\n    \n    <!-- CJK bigram (see text_ja for a Japanese configuration using morphological analysis) -->\n    <fieldType name=\"text_cjk\" class=\"solr.TextField\" positionIncrementGap=\"100\">\n      <analyzer>\n        <tokenizer class=\"solr.StandardTokenizerFactory\"/>\n        <!-- normalize width before bigram, as e.g. half-width dakuten combine  -->\n        <filter class=\"solr.CJKWidthFilterFactory\"/>\n        <!-- for any non-CJK -->\n        <filter class=\"solr.LowerCaseFilterFactory\"/>\n        <filter class=\"solr.CJKBigramFilterFactory\"/>\n      </analyzer>\n    </fieldType>\n\n    <!-- Kurdish -->\n    <fieldType name=\"text_ckb\" class=\"solr.TextField\" positionIncrementGap=\"100\">\n      <analyzer>\n        <tokenizer class=\"solr.StandardTokenizerFactory\"/>\n        <filter class=\"solr.SoraniNormalizationFilterFactory\"/>\n        <!-- for any latin text -->\n        <filter class=\"solr.LowerCaseFilterFactory\"/>\n        <filter class=\"solr.StopFilterFactory\" ignoreCase=\"true\" words=\"lang/stopwords_ckb.txt\"/>\n        <filter class=\"solr.SoraniStemFilterFactory\"/>\n      </analyzer>\n    </fieldType>\n\n    <!-- Czech -->\n    <fieldType name=\"text_cz\" class=\"solr.TextField\" positionIncrementGap=\"100\">\n      <analyzer> \n        <tokenizer class=\"solr.StandardTokenizerFactory\"/>\n        <filter class=\"solr.LowerCaseFilterFactory\"/>\n        <filter class=\"solr.StopFilterFactory\" ignoreCase=\"true\" words=\"lang/stopwords_cz.txt\" />\n        <filter class=\"solr.CzechStemFilterFactory\"/>       \n      </analyzer>\n    </fieldType>\n    \n    <!-- Danish -->\n    <fieldType name=\"text_da\" class=\"solr.TextField\" positionIncrementGap=\"100\">\n      <analyzer> \n        <tokenizer class=\"solr.StandardTokenizerFactory\"/>\n        <filter class=\"solr.LowerCaseFilterFactory\"/>\n        <filter class=\"solr.StopFilterFactory\" ignoreCase=\"true\" words=\"lang/stopwords_da.txt\" format=\"snowball\" />\n        <filter class=\"solr.SnowballPorterFilterFactory\" language=\"Danish\"/>       \n      </analyzer>\n    </fieldType>\n    \n    <!-- German -->\n    <fieldType name=\"text_de\" class=\"solr.TextField\" positionIncrementGap=\"100\">\n      <analyzer> \n        <tokenizer class=\"solr.StandardTokenizerFactory\"/>\n        <filter class=\"solr.LowerCaseFilterFactory\"/>\n        <filter class=\"solr.StopFilterFactory\" ignoreCase=\"true\" words=\"lang/stopwords_de.txt\" format=\"snowball\" />\n        <filter class=\"solr.GermanNormalizationFilterFactory\"/>\n        <filter class=\"solr.GermanLightStemFilterFactory\"/>\n        <!-- less aggressive: <filter class=\"solr.GermanMinimalStemFilterFactory\"/> -->\n        <!-- more aggressive: <filter class=\"solr.SnowballPorterFilterFactory\" language=\"German2\"/> -->\n      </analyzer>\n    </fieldType>\n    \n    <!-- Greek -->\n    <fieldType name=\"text_el\" class=\"solr.TextField\" positionIncrementGap=\"100\">\n      <analyzer> \n        <tokenizer class=\"solr.StandardTokenizerFactory\"/>\n        <!-- greek specific lowercase for sigma -->\n        <filter class=\"solr.GreekLowerCaseFilterFactory\"/>\n        <filter class=\"solr.StopFilterFactory\" ignoreCase=\"false\" words=\"lang/stopwords_el.txt\" />\n        <filter class=\"solr.GreekStemFilterFactory\"/>\n      </analyzer>\n    </fieldType>\n    \n    <!-- Spanish -->\n    <fieldType name=\"text_es\" class=\"solr.TextField\" positionIncrementGap=\"100\">\n      <analyzer> \n        <tokenizer class=\"solr.StandardTokenizerFactory\"/>\n        <filter class=\"solr.LowerCaseFilterFactory\"/>\n        <filter class=\"solr.StopFilterFactory\" ignoreCase=\"true\" words=\"lang/stopwords_es.txt\" format=\"snowball\" />\n        <filter class=\"solr.SpanishLightStemFilterFactory\"/>\n        <!-- more aggressive: <filter class=\"solr.SnowballPorterFilterFactory\" language=\"Spanish\"/> -->\n      </analyzer>\n    </fieldType>\n    \n    <!-- Basque -->\n    <fieldType name=\"text_eu\" class=\"solr.TextField\" positionIncrementGap=\"100\">\n      <analyzer> \n        <tokenizer class=\"solr.StandardTokenizerFactory\"/>\n        <filter class=\"solr.LowerCaseFilterFactory\"/>\n        <filter class=\"solr.StopFilterFactory\" ignoreCase=\"true\" words=\"lang/stopwords_eu.txt\" />\n        <filter class=\"solr.SnowballPorterFilterFactory\" language=\"Basque\"/>\n      </analyzer>\n    </fieldType>\n    \n    <!-- Persian -->\n    <fieldType name=\"text_fa\" class=\"solr.TextField\" positionIncrementGap=\"100\">\n      <analyzer>\n        <!-- for ZWNJ -->\n        <charFilter class=\"solr.PersianCharFilterFactory\"/>\n        <tokenizer class=\"solr.StandardTokenizerFactory\"/>\n        <filter class=\"solr.LowerCaseFilterFactory\"/>\n        <filter class=\"solr.ArabicNormalizationFilterFactory\"/>\n        <filter class=\"solr.PersianNormalizationFilterFactory\"/>\n        <filter class=\"solr.StopFilterFactory\" ignoreCase=\"true\" words=\"lang/stopwords_fa.txt\" />\n      </analyzer>\n    </fieldType>\n    \n    <!-- Finnish -->\n    <fieldType name=\"text_fi\" class=\"solr.TextField\" positionIncrementGap=\"100\">\n      <analyzer> \n        <tokenizer class=\"solr.StandardTokenizerFactory\"/>\n        <filter class=\"solr.LowerCaseFilterFactory\"/>\n        <filter class=\"solr.StopFilterFactory\" ignoreCase=\"true\" words=\"lang/stopwords_fi.txt\" format=\"snowball\" />\n        <filter class=\"solr.SnowballPorterFilterFactory\" language=\"Finnish\"/>\n        <!-- less aggressive: <filter class=\"solr.FinnishLightStemFilterFactory\"/> -->\n      </analyzer>\n    </fieldType>\n    \n    <!-- French -->\n    <fieldType name=\"text_fr\" class=\"solr.TextField\" positionIncrementGap=\"100\">\n      <analyzer> \n        <tokenizer class=\"solr.StandardTokenizerFactory\"/>\n        <!-- removes l', etc -->\n        <filter class=\"solr.ElisionFilterFactory\" ignoreCase=\"true\" articles=\"lang/contractions_fr.txt\"/>\n        <filter class=\"solr.LowerCaseFilterFactory\"/>\n        <filter class=\"solr.StopFilterFactory\" ignoreCase=\"true\" words=\"lang/stopwords_fr.txt\" format=\"snowball\" />\n        <filter class=\"solr.FrenchLightStemFilterFactory\"/>\n        <!-- less aggressive: <filter class=\"solr.FrenchMinimalStemFilterFactory\"/> -->\n        <!-- more aggressive: <filter class=\"solr.SnowballPorterFilterFactory\" language=\"French\"/> -->\n      </analyzer>\n    </fieldType>\n    \n    <!-- Irish -->\n    <fieldType name=\"text_ga\" class=\"solr.TextField\" positionIncrementGap=\"100\">\n      <analyzer> \n        <tokenizer class=\"solr.StandardTokenizerFactory\"/>\n        <!-- removes d', etc -->\n        <filter class=\"solr.ElisionFilterFactory\" ignoreCase=\"true\" articles=\"lang/contractions_ga.txt\"/>\n        <!-- removes n-, etc. position increments is intentionally false! -->\n        <filter class=\"solr.StopFilterFactory\" ignoreCase=\"true\" words=\"lang/hyphenations_ga.txt\"/>\n        <filter class=\"solr.IrishLowerCaseFilterFactory\"/>\n        <filter class=\"solr.StopFilterFactory\" ignoreCase=\"true\" words=\"lang/stopwords_ga.txt\"/>\n        <filter class=\"solr.SnowballPorterFilterFactory\" language=\"Irish\"/>\n      </analyzer>\n    </fieldType>\n    \n    <!-- Galician -->\n    <fieldType name=\"text_gl\" class=\"solr.TextField\" positionIncrementGap=\"100\">\n      <analyzer> \n        <tokenizer class=\"solr.StandardTokenizerFactory\"/>\n        <filter class=\"solr.LowerCaseFilterFactory\"/>\n        <filter class=\"solr.StopFilterFactory\" ignoreCase=\"true\" words=\"lang/stopwords_gl.txt\" />\n        <filter class=\"solr.GalicianStemFilterFactory\"/>\n        <!-- less aggressive: <filter class=\"solr.GalicianMinimalStemFilterFactory\"/> -->\n      </analyzer>\n    </fieldType>\n    \n    <!-- Hindi -->\n    <fieldType name=\"text_hi\" class=\"solr.TextField\" positionIncrementGap=\"100\">\n      <analyzer> \n        <tokenizer class=\"solr.StandardTokenizerFactory\"/>\n        <filter class=\"solr.LowerCaseFilterFactory\"/>\n        <!-- normalizes unicode representation -->\n        <filter class=\"solr.IndicNormalizationFilterFactory\"/>\n        <!-- normalizes variation in spelling -->\n        <filter class=\"solr.HindiNormalizationFilterFactory\"/>\n        <filter class=\"solr.StopFilterFactory\" ignoreCase=\"true\" words=\"lang/stopwords_hi.txt\" />\n        <filter class=\"solr.HindiStemFilterFactory\"/>\n      </analyzer>\n    </fieldType>\n    \n    <!-- Hungarian -->\n    <fieldType name=\"text_hu\" class=\"solr.TextField\" positionIncrementGap=\"100\">\n      <analyzer> \n        <tokenizer class=\"solr.StandardTokenizerFactory\"/>\n        <filter class=\"solr.LowerCaseFilterFactory\"/>\n        <filter class=\"solr.StopFilterFactory\" ignoreCase=\"true\" words=\"lang/stopwords_hu.txt\" format=\"snowball\" />\n        <filter class=\"solr.SnowballPorterFilterFactory\" language=\"Hungarian\"/>\n        <!-- less aggressive: <filter class=\"solr.HungarianLightStemFilterFactory\"/> -->   \n      </analyzer>\n    </fieldType>\n    \n    <!-- Armenian -->\n    <fieldType name=\"text_hy\" class=\"solr.TextField\" positionIncrementGap=\"100\">\n      <analyzer> \n        <tokenizer class=\"solr.StandardTokenizerFactory\"/>\n        <filter class=\"solr.LowerCaseFilterFactory\"/>\n        <filter class=\"solr.StopFilterFactory\" ignoreCase=\"true\" words=\"lang/stopwords_hy.txt\" />\n        <filter class=\"solr.SnowballPorterFilterFactory\" language=\"Armenian\"/>\n      </analyzer>\n    </fieldType>\n    \n    <!-- Indonesian -->\n    <fieldType name=\"text_id\" class=\"solr.TextField\" positionIncrementGap=\"100\">\n      <analyzer> \n        <tokenizer class=\"solr.StandardTokenizerFactory\"/>\n        <filter class=\"solr.LowerCaseFilterFactory\"/>\n        <filter class=\"solr.StopFilterFactory\" ignoreCase=\"true\" words=\"lang/stopwords_id.txt\" />\n        <!-- for a less aggressive approach (only inflectional suffixes), set stemDerivational to false -->\n        <filter class=\"solr.IndonesianStemFilterFactory\" stemDerivational=\"true\"/>\n      </analyzer>\n    </fieldType>\n    \n    <!-- Italian -->\n    <fieldType name=\"text_it\" class=\"solr.TextField\" positionIncrementGap=\"100\">\n      <analyzer> \n        <tokenizer class=\"solr.StandardTokenizerFactory\"/>\n        <!-- removes l', etc -->\n        <filter class=\"solr.ElisionFilterFactory\" ignoreCase=\"true\" articles=\"lang/contractions_it.txt\"/>\n        <filter class=\"solr.LowerCaseFilterFactory\"/>\n        <filter class=\"solr.StopFilterFactory\" ignoreCase=\"true\" words=\"lang/stopwords_it.txt\" format=\"snowball\" />\n        <filter class=\"solr.ItalianLightStemFilterFactory\"/>\n        <!-- more aggressive: <filter class=\"solr.SnowballPorterFilterFactory\" language=\"Italian\"/> -->\n      </analyzer>\n    </fieldType>\n    \n    <!-- Japanese using morphological analysis (see text_cjk for a configuration using bigramming)\n\n         NOTE: If you want to optimize search for precision, use default operator AND in your query\n         parser config with <solrQueryParser defaultOperator=\"AND\"/> further down in this file.  Use \n         OR if you would like to optimize for recall (default).\n    -->\n    <fieldType name=\"text_ja\" class=\"solr.TextField\" positionIncrementGap=\"100\" autoGeneratePhraseQueries=\"false\">\n      <analyzer>\n      <!-- Kuromoji Japanese morphological analyzer/tokenizer (JapaneseTokenizer)\n\n           Kuromoji has a search mode (default) that does segmentation useful for search.  A heuristic\n           is used to segment compounds into its parts and the compound itself is kept as synonym.\n\n           Valid values for attribute mode are:\n              normal: regular segmentation\n              search: segmentation useful for search with synonyms compounds (default)\n            extended: same as search mode, but unigrams unknown words (experimental)\n\n           For some applications it might be good to use search mode for indexing and normal mode for\n           queries to reduce recall and prevent parts of compounds from being matched and highlighted.\n           Use <analyzer type=\"index\"> and <analyzer type=\"query\"> for this and mode normal in query.\n\n           Kuromoji also has a convenient user dictionary feature that allows overriding the statistical\n           model with your own entries for segmentation, part-of-speech tags and readings without a need\n           to specify weights.  Notice that user dictionaries have not been subject to extensive testing.\n\n           User dictionary attributes are:\n                     userDictionary: user dictionary filename\n             userDictionaryEncoding: user dictionary encoding (default is UTF-8)\n\n           See lang/userdict_ja.txt for a sample user dictionary file.\n\n           Punctuation characters are discarded by default.  Use discardPunctuation=\"false\" to keep them.\n\n           See http://wiki.apache.org/solr/JapaneseLanguageSupport for more on Japanese language support.\n        -->\n        <tokenizer class=\"solr.JapaneseTokenizerFactory\" mode=\"search\"/>\n        <!--<tokenizer class=\"solr.JapaneseTokenizerFactory\" mode=\"search\" userDictionary=\"lang/userdict_ja.txt\"/>-->\n        <!-- Reduces inflected verbs and adjectives to their base/dictionary forms (辞書形) -->\n        <filter class=\"solr.JapaneseBaseFormFilterFactory\"/>\n        <!-- Removes tokens with certain part-of-speech tags -->\n        <filter class=\"solr.JapanesePartOfSpeechStopFilterFactory\" tags=\"lang/stoptags_ja.txt\" />\n        <!-- Normalizes full-width romaji to half-width and half-width kana to full-width (Unicode NFKC subset) -->\n        <filter class=\"solr.CJKWidthFilterFactory\"/>\n        <!-- Removes common tokens typically not useful for search, but have a negative effect on ranking -->\n        <filter class=\"solr.StopFilterFactory\" ignoreCase=\"true\" words=\"lang/stopwords_ja.txt\" />\n        <!-- Normalizes common katakana spelling variations by removing any last long sound character (U+30FC) -->\n        <filter class=\"solr.JapaneseKatakanaStemFilterFactory\" minimumLength=\"4\"/>\n        <!-- Lower-cases romaji characters -->\n        <filter class=\"solr.LowerCaseFilterFactory\"/>\n      </analyzer>\n    </fieldType>\n    \n    <!-- Latvian -->\n    <fieldType name=\"text_lv\" class=\"solr.TextField\" positionIncrementGap=\"100\">\n      <analyzer> \n        <tokenizer class=\"solr.StandardTokenizerFactory\"/>\n        <filter class=\"solr.LowerCaseFilterFactory\"/>\n        <filter class=\"solr.StopFilterFactory\" ignoreCase=\"true\" words=\"lang/stopwords_lv.txt\" />\n        <filter class=\"solr.LatvianStemFilterFactory\"/>\n      </analyzer>\n    </fieldType>\n    \n    <!-- Dutch -->\n    <fieldType name=\"text_nl\" class=\"solr.TextField\" positionIncrementGap=\"100\">\n      <analyzer> \n        <tokenizer class=\"solr.StandardTokenizerFactory\"/>\n        <filter class=\"solr.LowerCaseFilterFactory\"/>\n        <filter class=\"solr.StopFilterFactory\" ignoreCase=\"true\" words=\"lang/stopwords_nl.txt\" format=\"snowball\" />\n        <filter class=\"solr.StemmerOverrideFilterFactory\" dictionary=\"lang/stemdict_nl.txt\" ignoreCase=\"false\"/>\n        <filter class=\"solr.SnowballPorterFilterFactory\" language=\"Dutch\"/>\n      </analyzer>\n    </fieldType>\n    \n    <!-- Norwegian -->\n    <fieldType name=\"text_no\" class=\"solr.TextField\" positionIncrementGap=\"100\">\n      <analyzer> \n        <tokenizer class=\"solr.StandardTokenizerFactory\"/>\n        <filter class=\"solr.LowerCaseFilterFactory\"/>\n        <filter class=\"solr.StopFilterFactory\" ignoreCase=\"true\" words=\"lang/stopwords_no.txt\" format=\"snowball\" />\n        <filter class=\"solr.SnowballPorterFilterFactory\" language=\"Norwegian\"/>\n        <!-- less aggressive: <filter class=\"solr.NorwegianLightStemFilterFactory\" variant=\"nb\"/> -->\n        <!-- singular/plural: <filter class=\"solr.NorwegianMinimalStemFilterFactory\" variant=\"nb\"/> -->\n        <!-- The \"light\" and \"minimal\" stemmers support variants: nb=Bokmål, nn=Nynorsk, no=Both -->\n      </analyzer>\n    </fieldType>\n    \n    <!-- Portuguese -->\n    <fieldType name=\"text_pt\" class=\"solr.TextField\" positionIncrementGap=\"100\">\n      <analyzer> \n        <tokenizer class=\"solr.StandardTokenizerFactory\"/>\n        <filter class=\"solr.LowerCaseFilterFactory\"/>\n        <filter class=\"solr.StopFilterFactory\" ignoreCase=\"true\" words=\"lang/stopwords_pt.txt\" format=\"snowball\" />\n        <filter class=\"solr.PortugueseLightStemFilterFactory\"/>\n        <!-- less aggressive: <filter class=\"solr.PortugueseMinimalStemFilterFactory\"/> -->\n        <!-- more aggressive: <filter class=\"solr.SnowballPorterFilterFactory\" language=\"Portuguese\"/> -->\n        <!-- most aggressive: <filter class=\"solr.PortugueseStemFilterFactory\"/> -->\n      </analyzer>\n    </fieldType>\n    \n    <!-- Romanian -->\n    <fieldType name=\"text_ro\" class=\"solr.TextField\" positionIncrementGap=\"100\">\n      <analyzer> \n        <tokenizer class=\"solr.StandardTokenizerFactory\"/>\n        <filter class=\"solr.LowerCaseFilterFactory\"/>\n        <filter class=\"solr.StopFilterFactory\" ignoreCase=\"true\" words=\"lang/stopwords_ro.txt\" />\n        <filter class=\"solr.SnowballPorterFilterFactory\" language=\"Romanian\"/>\n      </analyzer>\n    </fieldType>\n    \n    <!-- Russian -->\n    <fieldType name=\"text_ru\" class=\"solr.TextField\" positionIncrementGap=\"100\">\n      <analyzer> \n        <tokenizer class=\"solr.StandardTokenizerFactory\"/>\n        <filter class=\"solr.LowerCaseFilterFactory\"/>\n        <filter class=\"solr.StopFilterFactory\" ignoreCase=\"true\" words=\"lang/stopwords_ru.txt\" format=\"snowball\" />\n        <filter class=\"solr.SnowballPorterFilterFactory\" language=\"Russian\"/>\n        <!-- less aggressive: <filter class=\"solr.RussianLightStemFilterFactory\"/> -->\n      </analyzer>\n    </fieldType>\n    \n    <!-- Swedish -->\n    <fieldType name=\"text_sv\" class=\"solr.TextField\" positionIncrementGap=\"100\">\n      <analyzer> \n        <tokenizer class=\"solr.StandardTokenizerFactory\"/>\n        <filter class=\"solr.LowerCaseFilterFactory\"/>\n        <filter class=\"solr.StopFilterFactory\" ignoreCase=\"true\" words=\"lang/stopwords_sv.txt\" format=\"snowball\" />\n        <filter class=\"solr.SnowballPorterFilterFactory\" language=\"Swedish\"/>\n        <!-- less aggressive: <filter class=\"solr.SwedishLightStemFilterFactory\"/> -->\n      </analyzer>\n    </fieldType>\n    \n    <!-- Thai -->\n    <fieldType name=\"text_th\" class=\"solr.TextField\" positionIncrementGap=\"100\">\n      <analyzer> \n        <tokenizer class=\"solr.ThaiTokenizerFactory\"/>\n        <filter class=\"solr.LowerCaseFilterFactory\"/>\n        <filter class=\"solr.StopFilterFactory\" ignoreCase=\"true\" words=\"lang/stopwords_th.txt\" />\n      </analyzer>\n    </fieldType>\n    \n    <!-- Turkish -->\n    <fieldType name=\"text_tr\" class=\"solr.TextField\" positionIncrementGap=\"100\">\n      <analyzer> \n        <tokenizer class=\"solr.StandardTokenizerFactory\"/>\n        <filter class=\"solr.ApostropheFilterFactory\"/>\n        <filter class=\"solr.TurkishLowerCaseFilterFactory\"/>\n        <filter class=\"solr.StopFilterFactory\" ignoreCase=\"false\" words=\"lang/stopwords_tr.txt\" />\n        <filter class=\"solr.SnowballPorterFilterFactory\" language=\"Turkish\"/>\n      </analyzer>\n    </fieldType>\n  \n  <!-- Similarity is the scoring routine for each document vs. a query.\n       A custom Similarity or SimilarityFactory may be specified here, but \n       the default is fine for most applications.  \n       For more info: http://wiki.apache.org/solr/SchemaXml#Similarity\n    -->\n  <!--\n     <similarity class=\"com.example.solr.CustomSimilarityFactory\">\n       <str name=\"paramkey\">param value</str>\n     </similarity>\n    -->\n\n</schema>\n"
  },
  {
    "path": "upstart/reddit-boot.conf",
    "content": "description \"start reddit on boot\"\n\ntask\nstart on runlevel [2345]\n\nexec initctl emit reddit-start\n"
  },
  {
    "path": "upstart/reddit-consumer-author_query_q.conf",
    "content": "description \"update links by author precomputed queries\"\n\ninstance $x\n\nstop on reddit-stop or runlevel [016]\n\nrespawn\nrespawn limit 10 5\n\nnice 10\nscript\n    . /etc/default/reddit\n    wrap-job paster run --proctitle author_query_q$x $REDDIT_INI $REDDIT_ROOT/r2/lib/voting.py -c \"consume_author_query_queue()\"\nend script\n"
  },
  {
    "path": "upstart/reddit-consumer-automoderator_q.conf",
    "content": "description \"Apply moderator-defined rules to submissions and comments\"\n\ninstance $x\n\nstop on reddit-stop or runlevel [016]\n\nrespawn\nrespawn limit 10 5\n\nnice 10\nscript\n    . /etc/default/reddit\n    wrap-job paster run --proctitle automoderator_q$x $REDDIT_INI $REDDIT_ROOT/r2/lib/automoderator.py -c 'run()'\nend script\n"
  },
  {
    "path": "upstart/reddit-consumer-butler_q.conf",
    "content": "description \"notify users when their username is mentioned\"\n\ninstance $x\n\nstop on reddit-stop or runlevel [016]\n\nrespawn\nrespawn limit 10 5\n\nnice 10\nscript\n    . /etc/default/reddit\n    wrap-job paster run --proctitle butler_q$x $REDDIT_INI -c 'from r2.lib.butler import run; run()'\nend script\n"
  },
  {
    "path": "upstart/reddit-consumer-commentstree_q.conf",
    "content": "description \"place new comments in the precomputed comment trees\"\n\ninstance $type$x\nenv type=commentstree_q\n\nstop on reddit-stop or runlevel [016]\n\nrespawn\nrespawn limit 10 5\n\nnice 10\nscript\n    . /etc/default/reddit\n    wrap-job paster run --proctitle $type$x $REDDIT_INI $REDDIT_ROOT/r2/lib/db/queries.py -c \"run_commentstree(qname='$type')\"\nend script\n"
  },
  {
    "path": "upstart/reddit-consumer-del_account_q.conf",
    "content": "description \"Perform expensive cleanup actions ASAP after account deletion\"\n\ninstance $x\n\nstop on reddit-stop or runlevel [016]\n\nrespawn\nrespawn limit 10 5\n\nnice 10\nscript\n    . /etc/default/reddit\n    wrap-job paster run --proctitle del_account_q$x $REDDIT_INI $REDDIT_ROOT/r2/lib/db/queries.py -c 'consume_deleted_accounts()'\nend script\n"
  },
  {
    "path": "upstart/reddit-consumer-domain_query_q.conf",
    "content": "description \"update links by domain precomputed queries\"\n\ninstance $x\n\nstop on reddit-stop or runlevel [016]\n\nrespawn\nrespawn limit 10 5\n\nnice 10\nscript\n    . /etc/default/reddit\n    wrap-job paster run --proctitle domain_query_q$x $REDDIT_INI $REDDIT_ROOT/r2/lib/voting.py -c \"consume_domain_query_queue()\"\nend script\n"
  },
  {
    "path": "upstart/reddit-consumer-event_collector_q.conf",
    "content": "description \"Collect events and publish them elsewhere for processing\"\n\ninstance $x\n\nstop on reddit-stop or runlevel [016]\n\nrespawn\nrespawn limit 10 5\n\nnice 10\nscript\n    . /etc/default/reddit\n    wrap-job paster run --proctitle event_collector_q$x $REDDIT_INI $REDDIT_ROOT/r2/lib/eventcollector.py -c 'from pylons import app_globals; process_events(app_globals, limit=100)'\nend script\n"
  },
  {
    "path": "upstart/reddit-consumer-markread_q.conf",
    "content": "description \"mark all messages as read for a user\"\n\ninstance $x\n\nstop on reddit-stop or runlevel [016]\n\nrespawn\nrespawn limit 10 5\n\nnice 10\nscript\n    . /etc/default/reddit\n    wrap-job paster run --proctitle markread_q$x $REDDIT_INI $REDDIT_ROOT/r2/lib/db/queries.py -c 'consume_mark_all_read()'\nend script\n"
  },
  {
    "path": "upstart/reddit-consumer-modmail_email_q.conf",
    "content": "description \"send modmail emails using mailgun\"\n\ninstance $x\n\nstop on reddit-stop or runlevel [016]\n\nrespawn\nrespawn limit 10 5\n\nnice 10\nscript\n    . /etc/default/reddit\n    wrap-job paster run --proctitle modmail_email_q$x \"$REDDIT_INI\" \"$REDDIT_ROOT\"/r2/lib/message_to_email.py -c 'process_modmail_email()'\nend script\n"
  },
  {
    "path": "upstart/reddit-consumer-newcomments_q.conf",
    "content": "description \"newcomments_q - update the /comments pages\"\n\ninstance $x\n\nstop on reddit-stop or runlevel [016]\n\nrespawn\nrespawn limit 10 5\n\nnice 10\nscript\n    . /etc/default/reddit\n    wrap-job paster run --proctitle newcomments_q$x $REDDIT_INI $REDDIT_ROOT/r2/lib/db/queries.py -c 'run_new_comments()'\nend script\n"
  },
  {
    "path": "upstart/reddit-consumer-scraper_q.conf",
    "content": "description \"find thumbnails/embedded content for newly submitted links\"\n\ninstance $x\n\nstop on reddit-stop or runlevel [016]\n\nrespawn\nrespawn limit 10 5\n\nnice 10\nscript\n    . /etc/default/reddit\n    wrap-job paster run --proctitle scraper_q$x $REDDIT_INI $REDDIT_ROOT/r2/lib/media.py -c 'run()'\nend script\n"
  },
  {
    "path": "upstart/reddit-consumer-search_q.conf",
    "content": "description \"update the cloudsearch index with new/changed documents\"\n\ninstance $x\n\nstop on reddit-stop or runlevel [016]\n\nrespawn\nrespawn limit 10 5\n\nnice 10\nscript\n    . /etc/default/reddit\n    wrap-job paster run --proctitle search_q$x $REDDIT_INI -c 'from pylons import app_globals; app_globals.search.run_changed()'\nend script\n"
  },
  {
    "path": "upstart/reddit-consumer-sitemaps_q.conf",
    "content": "description \"build sitemaps for almost every link on reddit\"\n\ninstance $x\n\nstop on reddit-stop or runlevel [016]\n\nrespawn\n\nnice 10\nscript\n    . /etc/default/reddit\n    wrap-job paster run --proctitle sitemaps_q$x $REDDIT_INI $REDDIT_ROOT/r2/lib/sitemaps/watcher.py -c 'watcher()'\nend script\n"
  },
  {
    "path": "upstart/reddit-consumer-subreddit_query_q.conf",
    "content": "description \"update links by subreddit precomputed queries\"\n\ninstance $x\n\nstop on reddit-stop or runlevel [016]\n\nrespawn\nrespawn limit 10 5\n\nnice 10\nscript\n    . /etc/default/reddit\n    wrap-job paster run --proctitle subreddit_query_q$x $REDDIT_INI $REDDIT_ROOT/r2/lib/voting.py -c \"consume_subreddit_query_queue()\"\nend script\n"
  },
  {
    "path": "upstart/reddit-consumer-vote_comment_q.conf",
    "content": "description \"process votes cast on comments\"\n\ninstance $x\n\nstop on reddit-stop or runlevel [016]\n\nrespawn\nrespawn limit 10 5\n\nnice 10\nscript\n    . /etc/default/reddit\n    wrap-job paster run --proctitle vote_comment_q$x $REDDIT_INI -c 'from r2.lib.voting import consume_comment_vote_queue; consume_comment_vote_queue()'\nend script\n"
  },
  {
    "path": "upstart/reddit-consumer-vote_link_q.conf",
    "content": "description \"process votes cast on links\"\n\ninstance $x\n\nstop on reddit-stop or runlevel [016]\n\nrespawn\nrespawn limit 10 5\n\nnice 10\nscript\n    . /etc/default/reddit\n    wrap-job paster run --proctitle vote_link_q$x $REDDIT_INI -c 'from r2.lib.voting import consume_link_vote_queue; consume_link_vote_queue()'\nend script\n"
  },
  {
    "path": "upstart/reddit-consumers-restart.conf",
    "content": "description \"restart queue consumers\"\n\ntask\n\nstart on reddit-restart or reddit-kill\n\nscript\n    . /etc/default/reddit\n    manage-consumers\nend script\n"
  },
  {
    "path": "upstart/reddit-consumers-start.conf",
    "content": "description \"start up queue consumers\"\n\ntask\n\nstart on runlevel [2345] or reddit-start\n\nscript\n    . /etc/default/reddit\n    manage-consumers\nend script\n"
  },
  {
    "path": "upstart/reddit-job-broken_things.conf",
    "content": "description \"find and delete new broken things (usually caused by failed transactions)\"\n\nmanual\ntask\nstop on reddit-stop or runlevel [016]\n\nnice 10\n\nscript\n    . /etc/default/reddit\n    wrap-job paster run $REDDIT_INI -c 'from r2.lib.utils import utils; utils.find_recent_broken_things(delete=True)'\nend script\n"
  },
  {
    "path": "upstart/reddit-job-clean_up_hardcache.conf",
    "content": "description \"remove expired tokens from hardcache\"\n\nmanual\ntask\nstop on reddit-stop or runlevel [016]\n\nnice 10\n\nscript\n    . /etc/default/reddit\n    wrap-job paster run $REDDIT_INI -c 'from r2.lib.hardcachebackend import delete_expired; delete_expired()'\nend script\n"
  },
  {
    "path": "upstart/reddit-job-email.conf",
    "content": "description \"send queued emails\"\n\nmanual\ntask\nstop on reddit-stop or runlevel [016]\n\nnice 10\n\nscript\n    . /etc/default/reddit\n    wrap-job paster run $REDDIT_INI -c 'from r2.lib import emailer; emailer.send_queued_mail()'\nend script\n"
  },
  {
    "path": "upstart/reddit-job-hourly_traffic.conf",
    "content": "description \"process hourly traffic logs on EMR\"\n\ninstance $slice\n\nmanual\ntask\nstop on reddit-stop or runlevel [016]\n\nnice 10\n\nscript\n    . /etc/default/reddit\n    wrap-job paster run $REDDIT_INI -c \"from r2.lib.traffic import process_hour; process_hour(\\\"$slice\\\")\"\nend script\n"
  },
  {
    "path": "upstart/reddit-job-rising.conf",
    "content": "description \"update the rising pages\"\n\ntask\nmanual\nstop on reddit-stop or runlevel [016]\n\nnice 10\n\nscript\n    . /etc/default/reddit\n    wrap-job paster run $REDDIT_INI -c 'from r2.lib import rising; rising.set_rising()'\nend script\n"
  },
  {
    "path": "upstart/reddit-job-subscribers.conf",
    "content": "description \"send subscriber stats to traffic\"\n\nmanual\ntask\nstop on reddit-stop or runlevel [016]\n\nnice 10\n\nscript\n    . /etc/default/reddit\n    wrap-job paster run $REDDIT_INI -c 'from r2.models.subreddit import SubscriptionsByDay; SubscriptionsByDay.write_counts(days_ago=1); SubscriptionsByDay.write_counts(days_ago=0)'\nend script\n"
  },
  {
    "path": "upstart/reddit-job-trylater.conf",
    "content": "description \"run events scheduled for later\"\n\nmanual\ntask\nstop on reddit-stop or runlevel [016]\n\nnice 10\n\nscript\n    . /etc/default/reddit\n    wrap-job paster run $REDDIT_INI -c 'from r2.models.trylater import TryLater; TryLater.run()'\nend script\n"
  },
  {
    "path": "upstart/reddit-job-update_geoip.conf",
    "content": "description \"refresh the geoip databases\"\n\nmanual\ntask\nstop on reddit-stop or runlevel [016]\n\nnice 10\n\nscript\n    . /etc/default/reddit\n    geoipupdate\n    service gunicorn reload geoip.conf\nend script\n"
  },
  {
    "path": "upstart/reddit-job-update_gold_users.conf",
    "content": "description \"trigger notification of impending expiration or actual expiration for gold membership\"\n\nmanual\ntask\nstop on reddit-stop or runlevel [016]\n\nnice 10\n\nscript\n    . /etc/default/reddit\n    wrap-job paster run $REDDIT_INI -c 'from r2.models import update_gold_users; update_gold_users()'\nend script\n"
  },
  {
    "path": "upstart/reddit-job-update_popular_subreddits.conf",
    "content": "description \"Update the popular subreddits.\"\n\nmanual\ntask\nstop on reddit-stop or runlevel [016]\n\nnice 10\n\nscript\n    . /etc/default/reddit\n    wrap-job paster run $REDDIT_INI -c 'from r2.models.subreddit import Subreddit; Subreddit.update_popular_subreddits()'\nend script\n"
  },
  {
    "path": "upstart/reddit-job-update_promo_metrics.conf",
    "content": "description \"pulls promotion stats from traffic db and writes them to cassandra\"\n\nmanual\ntask\nstop on reddit-stop or runlevel [016]\n\nnice 10\n\nscript\n    . /etc/default/reddit\n    wrap-job paster run $REDDIT_INI -c 'from r2.lib import inventory; inventory.update_prediction_data()'\nend script\n"
  },
  {
    "path": "upstart/reddit-job-update_promos.conf",
    "content": "description \"update promoted link listings\"\n\nmanual\ntask\nstop on reddit-stop or runlevel [016]\n\nnice 10\n\nscript\n    . /etc/default/reddit\n    wrap-job paster run $REDDIT_INI -c 'from r2.lib import promote; promote.Run()'\nend script\n"
  },
  {
    "path": "upstart/reddit-job-update_reddits.conf",
    "content": "description \"update /reddits sort scores\"\n\nmanual\ntask\nstop on reddit-stop or runlevel [016]\n\nnice 10\n\nscript\n    . /etc/default/reddit\n    wrap-job paster run $REDDIT_INI -c 'from r2.lib import sr_pops; sr_pops.run()'\nend script\n"
  },
  {
    "path": "upstart/reddit-job-update_sr_names.conf",
    "content": "description \"update the subreddit name search cache\"\n\nmanual\ntask\nstop on reddit-stop or runlevel [016]\n\nnice 10\n\nscript\n    . /etc/default/reddit\n    wrap-job paster run $REDDIT_INI -c 'from r2.lib import subreddit_search; subreddit_search.load_all_reddits()'\nend script\n"
  },
  {
    "path": "upstart/reddit-job-update_trending_subreddits.conf",
    "content": "description \"Update the current trending subreddits cache.\"\n\nmanual\ntask\nstop on reddit-stop or runlevel [016]\n\nnice 10\n\nscript\n    . /etc/default/reddit\n    wrap-job paster run $REDDIT_INI -c 'from r2.lib.trending import update_trending_subreddits; update_trending_subreddits()'\nend script\n"
  },
  {
    "path": "upstart/reddit-paster.conf",
    "content": "description \"the reddit app running under paster\"\n\nstop on reddit-stop or reddit-restart all or reddit-restart apps\nstart on reddit-start or reddit-restart all or reddit-restart apps\n\nrespawn\nrespawn limit 10 5\n\nscript\n    . /etc/default/reddit\n    wrap-job paster serve --reload $REDDIT_INI\nend script\n"
  }
]